@bendyline/squisq-react 1.3.0 → 1.3.2

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 (189) hide show
  1. package/dist/index.d.ts +710 -35
  2. package/dist/index.js +4288 -33
  3. package/dist/index.js.map +1 -1
  4. package/dist/squisq-player.global.js +1 -1
  5. package/dist/standalone-source.js +1 -1
  6. package/package.json +3 -3
  7. package/src/standalone-source.d.ts +10 -0
  8. package/dist/BlockRenderer.d.ts +0 -37
  9. package/dist/BlockRenderer.d.ts.map +0 -1
  10. package/dist/BlockRenderer.js +0 -54
  11. package/dist/BlockRenderer.js.map +0 -1
  12. package/dist/CaptionOverlay.d.ts +0 -33
  13. package/dist/CaptionOverlay.d.ts.map +0 -1
  14. package/dist/CaptionOverlay.js +0 -44
  15. package/dist/CaptionOverlay.js.map +0 -1
  16. package/dist/DocControlsBottom.d.ts +0 -28
  17. package/dist/DocControlsBottom.d.ts.map +0 -1
  18. package/dist/DocControlsBottom.js +0 -27
  19. package/dist/DocControlsBottom.js.map +0 -1
  20. package/dist/DocControlsOverlay.d.ts +0 -28
  21. package/dist/DocControlsOverlay.d.ts.map +0 -1
  22. package/dist/DocControlsOverlay.js +0 -80
  23. package/dist/DocControlsOverlay.js.map +0 -1
  24. package/dist/DocControlsSidebar.d.ts +0 -27
  25. package/dist/DocControlsSidebar.d.ts.map +0 -1
  26. package/dist/DocControlsSidebar.js +0 -10
  27. package/dist/DocControlsSidebar.js.map +0 -1
  28. package/dist/DocControlsSlideshow.d.ts +0 -20
  29. package/dist/DocControlsSlideshow.d.ts.map +0 -1
  30. package/dist/DocControlsSlideshow.js +0 -69
  31. package/dist/DocControlsSlideshow.js.map +0 -1
  32. package/dist/DocPlayer.d.ts +0 -97
  33. package/dist/DocPlayer.d.ts.map +0 -1
  34. package/dist/DocPlayer.js +0 -657
  35. package/dist/DocPlayer.js.map +0 -1
  36. package/dist/DocPlayerWithSidebar.d.ts +0 -41
  37. package/dist/DocPlayerWithSidebar.d.ts.map +0 -1
  38. package/dist/DocPlayerWithSidebar.js +0 -69
  39. package/dist/DocPlayerWithSidebar.js.map +0 -1
  40. package/dist/DocProgressBar.d.ts +0 -31
  41. package/dist/DocProgressBar.d.ts.map +0 -1
  42. package/dist/DocProgressBar.js +0 -122
  43. package/dist/DocProgressBar.js.map +0 -1
  44. package/dist/InlineAudioPlayer.d.ts +0 -15
  45. package/dist/InlineAudioPlayer.d.ts.map +0 -1
  46. package/dist/InlineAudioPlayer.js +0 -22
  47. package/dist/InlineAudioPlayer.js.map +0 -1
  48. package/dist/InlineVideoPlayer.d.ts +0 -21
  49. package/dist/InlineVideoPlayer.d.ts.map +0 -1
  50. package/dist/InlineVideoPlayer.js +0 -29
  51. package/dist/InlineVideoPlayer.js.map +0 -1
  52. package/dist/LinearDocView.d.ts +0 -73
  53. package/dist/LinearDocView.d.ts.map +0 -1
  54. package/dist/LinearDocView.js +0 -451
  55. package/dist/LinearDocView.js.map +0 -1
  56. package/dist/MarkdownRenderer.d.ts +0 -32
  57. package/dist/MarkdownRenderer.d.ts.map +0 -1
  58. package/dist/MarkdownRenderer.js +0 -259
  59. package/dist/MarkdownRenderer.js.map +0 -1
  60. package/dist/SocialCaptionOverlay.d.ts +0 -27
  61. package/dist/SocialCaptionOverlay.d.ts.map +0 -1
  62. package/dist/SocialCaptionOverlay.js +0 -185
  63. package/dist/SocialCaptionOverlay.js.map +0 -1
  64. package/dist/__tests__/BlockRenderer.test.d.ts +0 -2
  65. package/dist/__tests__/BlockRenderer.test.d.ts.map +0 -1
  66. package/dist/__tests__/BlockRenderer.test.js +0 -82
  67. package/dist/__tests__/BlockRenderer.test.js.map +0 -1
  68. package/dist/__tests__/DocControlsSlideshow.test.d.ts +0 -2
  69. package/dist/__tests__/DocControlsSlideshow.test.d.ts.map +0 -1
  70. package/dist/__tests__/DocControlsSlideshow.test.js +0 -89
  71. package/dist/__tests__/DocControlsSlideshow.test.js.map +0 -1
  72. package/dist/__tests__/JsonView.test.d.ts +0 -2
  73. package/dist/__tests__/JsonView.test.d.ts.map +0 -1
  74. package/dist/__tests__/JsonView.test.js +0 -93
  75. package/dist/__tests__/JsonView.test.js.map +0 -1
  76. package/dist/__tests__/LinearDocView.test.d.ts +0 -2
  77. package/dist/__tests__/LinearDocView.test.d.ts.map +0 -1
  78. package/dist/__tests__/LinearDocView.test.js +0 -250
  79. package/dist/__tests__/LinearDocView.test.js.map +0 -1
  80. package/dist/__tests__/MarkdownRenderer.test.d.ts +0 -2
  81. package/dist/__tests__/MarkdownRenderer.test.d.ts.map +0 -1
  82. package/dist/__tests__/MarkdownRenderer.test.js +0 -208
  83. package/dist/__tests__/MarkdownRenderer.test.js.map +0 -1
  84. package/dist/__tests__/TableLayer.test.d.ts +0 -2
  85. package/dist/__tests__/TableLayer.test.d.ts.map +0 -1
  86. package/dist/__tests__/TableLayer.test.js +0 -121
  87. package/dist/__tests__/TableLayer.test.js.map +0 -1
  88. package/dist/__tests__/exports.test.d.ts +0 -2
  89. package/dist/__tests__/exports.test.d.ts.map +0 -1
  90. package/dist/__tests__/exports.test.js +0 -48
  91. package/dist/__tests__/exports.test.js.map +0 -1
  92. package/dist/hooks/AudioProvider.d.ts +0 -71
  93. package/dist/hooks/AudioProvider.d.ts.map +0 -1
  94. package/dist/hooks/AudioProvider.js +0 -52
  95. package/dist/hooks/AudioProvider.js.map +0 -1
  96. package/dist/hooks/MediaContext.d.ts +0 -35
  97. package/dist/hooks/MediaContext.d.ts.map +0 -1
  98. package/dist/hooks/MediaContext.js +0 -72
  99. package/dist/hooks/MediaContext.js.map +0 -1
  100. package/dist/hooks/index.d.ts +0 -6
  101. package/dist/hooks/index.d.ts.map +0 -1
  102. package/dist/hooks/index.js +0 -5
  103. package/dist/hooks/index.js.map +0 -1
  104. package/dist/hooks/useAudioSync.d.ts +0 -18
  105. package/dist/hooks/useAudioSync.d.ts.map +0 -1
  106. package/dist/hooks/useAudioSync.js +0 -340
  107. package/dist/hooks/useAudioSync.js.map +0 -1
  108. package/dist/hooks/useAutoSurface.d.ts +0 -11
  109. package/dist/hooks/useAutoSurface.d.ts.map +0 -1
  110. package/dist/hooks/useAutoSurface.js +0 -24
  111. package/dist/hooks/useAutoSurface.js.map +0 -1
  112. package/dist/hooks/useDocPlayback.d.ts +0 -47
  113. package/dist/hooks/useDocPlayback.d.ts.map +0 -1
  114. package/dist/hooks/useDocPlayback.js +0 -179
  115. package/dist/hooks/useDocPlayback.js.map +0 -1
  116. package/dist/hooks/useViewportOrientation.d.ts +0 -31
  117. package/dist/hooks/useViewportOrientation.d.ts.map +0 -1
  118. package/dist/hooks/useViewportOrientation.js +0 -92
  119. package/dist/hooks/useViewportOrientation.js.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/dist/jsonView/JsonView.d.ts +0 -26
  122. package/dist/jsonView/JsonView.d.ts.map +0 -1
  123. package/dist/jsonView/JsonView.js +0 -12
  124. package/dist/jsonView/JsonView.js.map +0 -1
  125. package/dist/jsonView/RenderNode.d.ts +0 -22
  126. package/dist/jsonView/RenderNode.d.ts.map +0 -1
  127. package/dist/jsonView/RenderNode.js +0 -30
  128. package/dist/jsonView/RenderNode.js.map +0 -1
  129. package/dist/jsonView/index.d.ts +0 -3
  130. package/dist/jsonView/index.d.ts.map +0 -1
  131. package/dist/jsonView/index.js +0 -2
  132. package/dist/jsonView/index.js.map +0 -1
  133. package/dist/jsonView/useJsonViewTokens.d.ts +0 -14
  134. package/dist/jsonView/useJsonViewTokens.d.ts.map +0 -1
  135. package/dist/jsonView/useJsonViewTokens.js +0 -34
  136. package/dist/jsonView/useJsonViewTokens.js.map +0 -1
  137. package/dist/jsonView/viewers.d.ts +0 -30
  138. package/dist/jsonView/viewers.d.ts.map +0 -1
  139. package/dist/jsonView/viewers.js +0 -226
  140. package/dist/jsonView/viewers.js.map +0 -1
  141. package/dist/layers/ImageLayer.d.ts +0 -23
  142. package/dist/layers/ImageLayer.d.ts.map +0 -1
  143. package/dist/layers/ImageLayer.js +0 -104
  144. package/dist/layers/ImageLayer.js.map +0 -1
  145. package/dist/layers/MapLayer.d.ts +0 -29
  146. package/dist/layers/MapLayer.d.ts.map +0 -1
  147. package/dist/layers/MapLayer.js +0 -97
  148. package/dist/layers/MapLayer.js.map +0 -1
  149. package/dist/layers/ShapeLayer.d.ts +0 -20
  150. package/dist/layers/ShapeLayer.d.ts.map +0 -1
  151. package/dist/layers/ShapeLayer.js +0 -39
  152. package/dist/layers/ShapeLayer.js.map +0 -1
  153. package/dist/layers/TableLayer.d.ts +0 -23
  154. package/dist/layers/TableLayer.d.ts.map +0 -1
  155. package/dist/layers/TableLayer.js +0 -58
  156. package/dist/layers/TableLayer.js.map +0 -1
  157. package/dist/layers/TextLayer.d.ts +0 -23
  158. package/dist/layers/TextLayer.d.ts.map +0 -1
  159. package/dist/layers/TextLayer.js +0 -126
  160. package/dist/layers/TextLayer.js.map +0 -1
  161. package/dist/layers/VideoLayer.d.ts +0 -40
  162. package/dist/layers/VideoLayer.d.ts.map +0 -1
  163. package/dist/layers/VideoLayer.js +0 -105
  164. package/dist/layers/VideoLayer.js.map +0 -1
  165. package/dist/layers/index.d.ts +0 -6
  166. package/dist/layers/index.d.ts.map +0 -1
  167. package/dist/layers/index.js +0 -6
  168. package/dist/layers/index.js.map +0 -1
  169. package/dist/standalone-entry.d.ts +0 -69
  170. package/dist/standalone-entry.d.ts.map +0 -1
  171. package/dist/standalone-entry.js +0 -185
  172. package/dist/standalone-entry.js.map +0 -1
  173. package/dist/standalone-source.d.ts +0 -2
  174. package/dist/types.d.ts +0 -154
  175. package/dist/types.d.ts.map +0 -1
  176. package/dist/types.js +0 -21
  177. package/dist/types.js.map +0 -1
  178. package/dist/utils/animationUtils.d.ts +0 -8
  179. package/dist/utils/animationUtils.d.ts.map +0 -1
  180. package/dist/utils/animationUtils.js +0 -8
  181. package/dist/utils/animationUtils.js.map +0 -1
  182. package/dist/utils/layerUtils.d.ts +0 -17
  183. package/dist/utils/layerUtils.d.ts.map +0 -1
  184. package/dist/utils/layerUtils.js +0 -37
  185. package/dist/utils/layerUtils.js.map +0 -1
  186. package/dist/utils/mapTileUtils.d.ts +0 -105
  187. package/dist/utils/mapTileUtils.d.ts.map +0 -1
  188. package/dist/utils/mapTileUtils.js +0 -268
  189. package/dist/utils/mapTileUtils.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,34 +1,4289 @@
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 Fragment2, useRef as useRef5, useState as useState7, useEffect as useEffect7, useCallback as useCallback5, useMemo as useMemo7 } from "react";
3
+ import { isTemplateBlock as isTemplateBlock2, getCaptionAtTime as getCaptionAtTime2 } from "@bendyline/squisq/schemas";
4
+ import { applySurface as applySurface2 } from "@bendyline/squisq/schemas";
5
+
6
+ // src/utils/animationUtils.ts
7
+ import {
8
+ getAnimationStyle,
9
+ getDefaultAnimationDuration,
10
+ getTransitionClass,
11
+ getAnimationProgress
12
+ } from "@bendyline/squisq/doc";
13
+
14
+ // src/utils/layerUtils.ts
15
+ function resolveValue(value, dimension) {
16
+ if (typeof value === "number") {
17
+ return value;
18
+ }
19
+ if (value.endsWith("%")) {
20
+ const percent = parseFloat(value);
21
+ return percent / 100 * dimension;
22
+ }
23
+ return parseFloat(value);
24
+ }
25
+ function getAnchorOffset(anchor, width, height) {
26
+ switch (anchor) {
27
+ case "center":
28
+ return { x: -width / 2, y: -height / 2 };
29
+ case "top-right":
30
+ return { x: -width, y: 0 };
31
+ case "bottom-left":
32
+ return { x: 0, y: -height };
33
+ case "bottom-right":
34
+ return { x: -width, y: -height };
35
+ case "top-left":
36
+ default:
37
+ return { x: 0, y: 0 };
38
+ }
39
+ }
40
+
41
+ // src/hooks/MediaContext.tsx
42
+ import { createContext, useContext, useState, useEffect, useMemo } from "react";
43
+ var MediaContext = createContext(null);
44
+ function useMediaProvider() {
45
+ return useContext(MediaContext);
46
+ }
47
+ function useMediaUrl(relativePath, basePath) {
48
+ const provider = useMediaProvider();
49
+ const safePath = typeof relativePath === "string" ? relativePath : "";
50
+ const isAbsolute = !safePath || safePath.startsWith("http") || safePath.startsWith("/") || safePath.startsWith("data:") || safePath.startsWith("blob:");
51
+ const fallback = useMemo(
52
+ () => isAbsolute ? safePath : `${basePath}/${safePath}`,
53
+ [isAbsolute, safePath, basePath]
54
+ );
55
+ const needsProvider = !isAbsolute && !!provider;
56
+ const [url, setUrl] = useState(fallback);
57
+ useEffect(() => {
58
+ if (!needsProvider) {
59
+ setUrl(fallback);
60
+ return;
61
+ }
62
+ let cancelled = false;
63
+ provider.resolveUrl(safePath).then((resolved) => {
64
+ if (!cancelled) setUrl(resolved);
65
+ });
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [needsProvider, provider, safePath, fallback]);
70
+ return needsProvider ? url : fallback;
71
+ }
72
+
73
+ // src/layers/ImageLayer.tsx
74
+ import { jsx } from "react/jsx-runtime";
75
+ function ImageLayer({ layer, basePath, viewport, blockTime }) {
76
+ const { content, position, animation } = layer;
77
+ const x = resolveValue(position.x, viewport.width);
78
+ const y = resolveValue(position.y, viewport.height);
79
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
80
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
81
+ const offset = getAnchorOffset(position.anchor, width, height);
82
+ const finalX = x + offset.x;
83
+ const finalY = y + offset.y;
84
+ const src = useMediaUrl(content.src, basePath);
85
+ const animStyle = getAnimationStyle(animation, blockTime);
86
+ const preserveAspectRatio = getPreserveAspectRatio(content.fit);
87
+ const isCover = content.fit === "cover";
88
+ const isSpatialAnim = animation && SPATIAL_ANIMATION_TYPES.has(animation.type);
89
+ if (isCover && isSpatialAnim && animation) {
90
+ const kbAnim = remapToKenBurns(animation);
91
+ const kbStyle = getAnimationStyle(kbAnim, blockTime);
92
+ return /* @__PURE__ */ jsx("g", { className: "block-layer block-layer--image", "data-layer-id": layer.id, children: /* @__PURE__ */ jsx("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ style: {
96
+ width: `${width}px`,
97
+ height: `${height}px`,
98
+ overflow: "hidden"
99
+ },
100
+ children: /* @__PURE__ */ jsx(
101
+ "img",
102
+ {
103
+ src,
104
+ alt: content.alt || "",
105
+ className: kbStyle.className,
106
+ style: {
107
+ width: `${width}px`,
108
+ height: `${height}px`,
109
+ objectFit: "cover",
110
+ objectPosition: "center",
111
+ display: "block",
112
+ pointerEvents: "none",
113
+ transformOrigin: "center center",
114
+ ...kbStyle.style
115
+ }
116
+ }
117
+ )
118
+ }
119
+ ) }) });
120
+ }
121
+ if (isCover) {
122
+ return /* @__PURE__ */ jsx(
123
+ "g",
124
+ {
125
+ className: `block-layer block-layer--image ${animStyle.className}`,
126
+ style: animStyle.style,
127
+ "data-layer-id": layer.id,
128
+ children: /* @__PURE__ */ jsx("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx(
129
+ "img",
130
+ {
131
+ src,
132
+ alt: content.alt || "",
133
+ style: {
134
+ width: `${width}px`,
135
+ height: `${height}px`,
136
+ objectFit: "cover",
137
+ objectPosition: "center",
138
+ display: "block",
139
+ pointerEvents: "none"
140
+ }
141
+ }
142
+ ) })
143
+ }
144
+ );
145
+ }
146
+ return /* @__PURE__ */ jsx(
147
+ "g",
148
+ {
149
+ className: `block-layer block-layer--image ${animStyle.className}`,
150
+ style: animStyle.style,
151
+ "data-layer-id": layer.id,
152
+ children: /* @__PURE__ */ jsx(
153
+ "image",
154
+ {
155
+ href: src,
156
+ x: finalX,
157
+ y: finalY,
158
+ width,
159
+ height,
160
+ preserveAspectRatio,
161
+ style: { pointerEvents: "none" }
162
+ }
163
+ )
164
+ }
165
+ );
166
+ }
167
+ function getPreserveAspectRatio(fit) {
168
+ switch (fit) {
169
+ case "cover":
170
+ return "xMidYMid slice";
171
+ case "fill":
172
+ return "none";
173
+ case "contain":
174
+ default:
175
+ return "xMidYMid meet";
176
+ }
177
+ }
178
+ var SPATIAL_ANIMATION_TYPES = /* @__PURE__ */ new Set(["panLeft", "panRight", "slowZoom", "zoomIn", "zoomOut"]);
179
+ function remapToKenBurns(anim) {
180
+ switch (anim.type) {
181
+ case "panLeft":
182
+ return { ...anim, type: "slowZoom", panDirection: "left" };
183
+ case "panRight":
184
+ return { ...anim, type: "slowZoom", panDirection: "right" };
185
+ case "zoomIn":
186
+ return { ...anim, type: "slowZoom", direction: "in" };
187
+ case "zoomOut":
188
+ return { ...anim, type: "slowZoom", direction: "out" };
189
+ default:
190
+ return anim;
191
+ }
192
+ }
193
+
194
+ // src/layers/TextLayer.tsx
195
+ import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
196
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
197
+ function TextLayer({ layer, viewport, blockTime }) {
198
+ const { content, position, animation } = layer;
199
+ const { text, style } = content;
200
+ const x = resolveValue(position.x, viewport.width);
201
+ const y = resolveValue(position.y, viewport.height);
202
+ const maxWidth = position.width ? resolveValue(position.width, viewport.width) : void 0;
203
+ const textAnchor = getTextAnchor(style.textAlign, position.anchor);
204
+ const dominantBaseline = getDominantBaseline(position.anchor);
205
+ const animStyle = getAnimationStyle(animation, blockTime);
206
+ const rawLines = (text ?? "").split("\n");
207
+ let lines = maxWidth ? rawLines.reduce(
208
+ (acc, line) => acc.concat(wrapText(line, style.fontSize, maxWidth)),
209
+ []
210
+ ) : rawLines;
211
+ if (style.maxLines && lines.length > style.maxLines) {
212
+ lines = lines.slice(0, style.maxLines);
213
+ const last = lines[lines.length - 1];
214
+ lines[lines.length - 1] = last.replace(/\s*$/, "") + "...";
215
+ }
216
+ const lineHeight = style.lineHeight || 1.4;
217
+ const lineHeightPx = style.fontSize * lineHeight;
218
+ const textStyles = {
219
+ fontSize: `${style.fontSize}px`,
220
+ fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
221
+ fontWeight: style.fontWeight || "normal",
222
+ fill: style.color,
223
+ ...animStyle.style
224
+ };
225
+ const filterId = style.shadow ? `shadow-${layer.id}` : void 0;
226
+ return /* @__PURE__ */ jsxs("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
227
+ style.shadow && /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsx2("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx2("feDropShadow", { dx: "0", dy: "2", stdDeviation: "3", floodColor: "rgba(0,0,0,0.7)" }) }) }),
228
+ style.background && /* @__PURE__ */ jsx2(
229
+ "rect",
230
+ {
231
+ x: x - (style.padding || 16),
232
+ y: y - style.fontSize - (style.padding || 16),
233
+ width: getTextBoxWidth(lines, style) + (style.padding || 16) * 2,
234
+ height: lines.length * lineHeightPx + (style.padding || 16) * 2,
235
+ fill: style.background,
236
+ rx: 4,
237
+ ry: 4
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsx2(
241
+ "text",
242
+ {
243
+ x,
244
+ y,
245
+ textAnchor,
246
+ dominantBaseline,
247
+ style: textStyles,
248
+ filter: filterId ? `url(#${filterId})` : void 0,
249
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs("tspan", { x, dy: i === 0 ? 0 : lineHeightPx, children: [
250
+ line || "\xA0",
251
+ " "
252
+ ] }, i))
253
+ }
254
+ )
255
+ ] });
256
+ }
257
+ function getTextAnchor(align, anchor) {
258
+ if (align === "center") return "middle";
259
+ if (align === "right") return "end";
260
+ if (align === "left") return "start";
261
+ if (anchor?.includes("right")) return "end";
262
+ if (anchor === "center") return "middle";
263
+ return "start";
264
+ }
265
+ function getDominantBaseline(anchor) {
266
+ if (anchor?.includes("bottom")) return "text-after-edge";
267
+ if (anchor === "center") return "middle";
268
+ return "text-before-edge";
269
+ }
270
+ function getTextBoxWidth(lines, style) {
271
+ const maxLineLength = Math.max(...lines.map((l) => l.length));
272
+ return maxLineLength * style.fontSize * 0.55;
273
+ }
274
+ function wrapText(text, fontSize, maxWidth) {
275
+ if (!text.trim()) return [""];
276
+ const avgCharWidth = fontSize * 0.5;
277
+ const charsPerLine = Math.floor(maxWidth / avgCharWidth);
278
+ if (charsPerLine <= 0) return [text];
279
+ const words = text.split(/\s+/);
280
+ const lines = [];
281
+ let currentLine = "";
282
+ for (const word of words) {
283
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
284
+ if (testLine.length <= charsPerLine) {
285
+ currentLine = testLine;
286
+ } else {
287
+ if (currentLine) {
288
+ lines.push(currentLine);
289
+ }
290
+ if (word.length > charsPerLine) {
291
+ let remaining = word;
292
+ while (remaining.length > charsPerLine) {
293
+ lines.push(remaining.slice(0, charsPerLine));
294
+ remaining = remaining.slice(charsPerLine);
295
+ }
296
+ currentLine = remaining;
297
+ } else {
298
+ currentLine = word;
299
+ }
300
+ }
301
+ }
302
+ if (currentLine) {
303
+ lines.push(currentLine);
304
+ }
305
+ return lines.length > 0 ? lines : [""];
306
+ }
307
+
308
+ // src/layers/ShapeLayer.tsx
309
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
310
+ function ShapeLayer({ layer, viewport, blockTime }) {
311
+ const { content, position, animation } = layer;
312
+ const rawX = resolveValue(position.x, viewport.width);
313
+ const rawY = resolveValue(position.y, viewport.height);
314
+ const width = position.width ? resolveValue(position.width, viewport.width) : 100;
315
+ const height = position.height ? resolveValue(position.height, viewport.height) : 100;
316
+ const anchorOffset = getAnchorOffset(position.anchor, width, height);
317
+ const x = rawX + anchorOffset.x;
318
+ const y = rawY + anchorOffset.y;
319
+ const animStyle = getAnimationStyle(animation, blockTime);
320
+ const fill = content.fill || "none";
321
+ const isCSSGradient = typeof fill === "string" && fill.includes("gradient(");
322
+ if (content.shape === "rect" && isCSSGradient) {
323
+ return /* @__PURE__ */ jsx3(
324
+ "g",
325
+ {
326
+ className: `block-layer block-layer--shape ${animStyle.className}`,
327
+ style: animStyle.style,
328
+ "data-layer-id": layer.id,
329
+ children: /* @__PURE__ */ jsx3("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx3(
330
+ "div",
331
+ {
332
+ style: {
333
+ width: `${width}px`,
334
+ height: `${height}px`,
335
+ background: fill,
336
+ borderRadius: content.borderRadius ? `${content.borderRadius}px` : void 0,
337
+ pointerEvents: "none"
338
+ }
339
+ }
340
+ ) })
341
+ }
342
+ );
343
+ }
344
+ const shapeProps = {
345
+ fill,
346
+ stroke: content.stroke,
347
+ strokeWidth: content.strokeWidth
348
+ };
349
+ return /* @__PURE__ */ jsxs2(
350
+ "g",
351
+ {
352
+ className: `block-layer block-layer--shape ${animStyle.className}`,
353
+ style: animStyle.style,
354
+ "data-layer-id": layer.id,
355
+ children: [
356
+ content.shape === "rect" && /* @__PURE__ */ jsx3(
357
+ "rect",
358
+ {
359
+ x,
360
+ y,
361
+ width,
362
+ height,
363
+ rx: content.borderRadius,
364
+ ry: content.borderRadius,
365
+ ...shapeProps
366
+ }
367
+ ),
368
+ content.shape === "circle" && /* @__PURE__ */ jsx3(
369
+ "circle",
370
+ {
371
+ cx: x + width / 2,
372
+ cy: y + height / 2,
373
+ r: Math.min(width, height) / 2,
374
+ ...shapeProps
375
+ }
376
+ ),
377
+ content.shape === "line" && /* @__PURE__ */ jsx3(
378
+ "line",
379
+ {
380
+ x1: x,
381
+ y1: y,
382
+ x2: x + width,
383
+ y2: y + height,
384
+ stroke: content.stroke || "#ffffff",
385
+ strokeWidth: content.strokeWidth || 2
386
+ }
387
+ )
388
+ ]
389
+ }
390
+ );
391
+ }
392
+
393
+ // src/layers/MapLayer.tsx
394
+ import { useState as useState2, useEffect as useEffect2 } from "react";
395
+
396
+ // src/utils/mapTileUtils.ts
397
+ var TILE_PROVIDERS = {
398
+ terrain: {
399
+ url: "https://tile.opentopomap.org/{z}/{x}/{y}.png",
400
+ attribution: "Map: OpenTopoMap (CC-BY-SA)",
401
+ maxZoom: 17
402
+ },
403
+ road: {
404
+ url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
405
+ attribution: "\xA9 OpenStreetMap contributors",
406
+ maxZoom: 19
407
+ },
408
+ satellite: {
409
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
410
+ attribution: "Imagery: Esri, Maxar, Earthstar",
411
+ maxZoom: 18
412
+ },
413
+ toner: {
414
+ url: "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png",
415
+ attribution: "Map: Stadia Maps, Stamen Design",
416
+ maxZoom: 20
417
+ },
418
+ watercolor: {
419
+ url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
420
+ attribution: "Map: Stadia Maps, Stamen Design",
421
+ maxZoom: 16
422
+ }
423
+ };
424
+ function latLngToTile(lat, lng, zoom) {
425
+ const n = Math.pow(2, zoom);
426
+ const x = Math.floor((lng + 180) / 360 * n);
427
+ const latRad = lat * Math.PI / 180;
428
+ const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);
429
+ return { x, y };
430
+ }
431
+ function getPixelOffset(lat, lng, zoom, tileSize = 256) {
432
+ const n = Math.pow(2, zoom);
433
+ const xTile = (lng + 180) / 360 * n;
434
+ const latRad = lat * Math.PI / 180;
435
+ const yTile = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n;
436
+ return {
437
+ x: (xTile - Math.floor(xTile)) * tileSize,
438
+ y: (yTile - Math.floor(yTile)) * tileSize
439
+ };
440
+ }
441
+ function getTilesForViewport(centerLat, centerLng, zoom, viewportWidth, viewportHeight, tileSize = 256) {
442
+ const centerTile = latLngToTile(centerLat, centerLng, zoom);
443
+ const pixelOffset = getPixelOffset(centerLat, centerLng, zoom, tileSize);
444
+ const tilesX = Math.ceil(viewportWidth / tileSize) + 1;
445
+ const tilesY = Math.ceil(viewportHeight / tileSize) + 1;
446
+ const startX = centerTile.x - Math.floor(tilesX / 2);
447
+ const startY = centerTile.y - Math.floor(tilesY / 2);
448
+ const centerScreenX = viewportWidth / 2 - pixelOffset.x;
449
+ const centerScreenY = viewportHeight / 2 - pixelOffset.y;
450
+ const tiles = [];
451
+ for (let dy = 0; dy < tilesY; dy++) {
452
+ for (let dx = 0; dx < tilesX; dx++) {
453
+ const tileX = startX + dx;
454
+ const tileY = startY + dy;
455
+ const screenX = centerScreenX + (tileX - centerTile.x) * tileSize;
456
+ const screenY = centerScreenY + (tileY - centerTile.y) * tileSize;
457
+ tiles.push({ x: tileX, y: tileY, screenX, screenY });
458
+ }
459
+ }
460
+ return tiles;
461
+ }
462
+ function buildTileUrl(provider, x, y, z) {
463
+ return provider.url.replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
464
+ }
465
+ async function fetchTileImage(url) {
466
+ return new Promise((resolve, reject) => {
467
+ const img = new Image();
468
+ img.crossOrigin = "anonymous";
469
+ img.onload = () => resolve(img);
470
+ img.onerror = () => reject(new Error(`Failed to load tile: ${url}`));
471
+ img.src = url;
472
+ });
473
+ }
474
+ async function composeMapImage(options) {
475
+ const { center, zoom, style, width, height, markers = [], showAttribution = true } = options;
476
+ const provider = TILE_PROVIDERS[style];
477
+ const tileSize = provider.tileSize || 256;
478
+ const clampedZoom = Math.min(zoom, provider.maxZoom);
479
+ const canvas = document.createElement("canvas");
480
+ canvas.width = width;
481
+ canvas.height = height;
482
+ const ctx = canvas.getContext("2d");
483
+ if (!ctx) throw new Error("Failed to get canvas context");
484
+ ctx.fillStyle = style === "toner" ? "#ffffff" : "#e5e7eb";
485
+ ctx.fillRect(0, 0, width, height);
486
+ const tiles = getTilesForViewport(center.lat, center.lng, clampedZoom, width, height, tileSize);
487
+ const tilePromises = tiles.map(async (tile) => {
488
+ const url = buildTileUrl(provider, tile.x, tile.y, clampedZoom);
489
+ try {
490
+ const img = await fetchTileImage(url);
491
+ ctx.drawImage(img, tile.screenX, tile.screenY, tileSize, tileSize);
492
+ } catch (err) {
493
+ console.warn(`Tile load failed: ${url}`, err);
494
+ }
495
+ });
496
+ await Promise.all(tilePromises);
497
+ for (const marker of markers) {
498
+ drawMarker(ctx, marker, center, clampedZoom, width, height, tileSize);
499
+ }
500
+ if (showAttribution) {
501
+ drawAttribution(ctx, provider.attribution, width, height);
502
+ }
503
+ return canvas.toDataURL("image/png");
504
+ }
505
+ function drawMarker(ctx, marker, center, zoom, width, height, tileSize) {
506
+ const centerTile = latLngToTile(center.lat, center.lng, zoom);
507
+ const markerTile = latLngToTile(marker.lat, marker.lng, zoom);
508
+ const centerOffset = getPixelOffset(center.lat, center.lng, zoom, tileSize);
509
+ const markerOffset = getPixelOffset(marker.lat, marker.lng, zoom, tileSize);
510
+ const dx = (markerTile.x - centerTile.x) * tileSize + (markerOffset.x - centerOffset.x);
511
+ const dy = (markerTile.y - centerTile.y) * tileSize + (markerOffset.y - centerOffset.y);
512
+ const screenX = width / 2 + dx;
513
+ const screenY = height / 2 + dy;
514
+ const color = marker.color || "#ef4444";
515
+ const icon = marker.icon || "pin";
516
+ ctx.save();
517
+ if (icon === "pin") {
518
+ ctx.fillStyle = color;
519
+ ctx.beginPath();
520
+ ctx.arc(screenX, screenY - 12, 8, Math.PI, 0, false);
521
+ ctx.lineTo(screenX, screenY);
522
+ ctx.closePath();
523
+ ctx.fill();
524
+ ctx.fillStyle = "#ffffff";
525
+ ctx.beginPath();
526
+ ctx.arc(screenX, screenY - 12, 3, 0, Math.PI * 2);
527
+ ctx.fill();
528
+ } else if (icon === "circle") {
529
+ ctx.fillStyle = color;
530
+ ctx.beginPath();
531
+ ctx.arc(screenX, screenY, 8, 0, Math.PI * 2);
532
+ ctx.fill();
533
+ ctx.strokeStyle = "#ffffff";
534
+ ctx.lineWidth = 2;
535
+ ctx.stroke();
536
+ } else if (icon === "star") {
537
+ ctx.fillStyle = color;
538
+ drawStar(ctx, screenX, screenY, 5, 10, 5);
539
+ ctx.fill();
540
+ }
541
+ if (marker.label) {
542
+ ctx.fillStyle = "#1f2937";
543
+ ctx.font = "bold 12px system-ui, sans-serif";
544
+ ctx.textAlign = "center";
545
+ ctx.fillText(marker.label, screenX, screenY + 20);
546
+ }
547
+ ctx.restore();
548
+ }
549
+ function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
550
+ let rot = Math.PI / 2 * 3;
551
+ const step = Math.PI / spikes;
552
+ ctx.beginPath();
553
+ ctx.moveTo(cx, cy - outerRadius);
554
+ for (let i = 0; i < spikes; i++) {
555
+ ctx.lineTo(cx + Math.cos(rot) * outerRadius, cy + Math.sin(rot) * outerRadius);
556
+ rot += step;
557
+ ctx.lineTo(cx + Math.cos(rot) * innerRadius, cy + Math.sin(rot) * innerRadius);
558
+ rot += step;
559
+ }
560
+ ctx.lineTo(cx, cy - outerRadius);
561
+ ctx.closePath();
562
+ }
563
+ function drawAttribution(ctx, text, width, height) {
564
+ ctx.save();
565
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
566
+ const padding = 4;
567
+ ctx.font = "10px system-ui, sans-serif";
568
+ const textWidth = ctx.measureText(text).width;
569
+ ctx.fillRect(width - textWidth - padding * 2 - 4, height - 16, textWidth + padding * 2, 14);
570
+ ctx.fillStyle = "#374151";
571
+ ctx.textAlign = "right";
572
+ ctx.fillText(text, width - padding - 4, height - 5);
573
+ ctx.restore();
574
+ }
575
+
576
+ // src/layers/MapLayer.tsx
577
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
578
+ function MapLayer({ layer, basePath, viewport, blockTime }) {
579
+ const { content, position, animation } = layer;
580
+ const [mapImageUrl, setMapImageUrl] = useState2(null);
581
+ const [isLoading, setIsLoading] = useState2(true);
582
+ const [error, setError] = useState2(null);
583
+ const x = resolveValue(position.x, viewport.width);
584
+ const y = resolveValue(position.y, viewport.height);
585
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
586
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
587
+ const offset = getAnchorOffset(position.anchor, width, height);
588
+ const finalX = x + offset.x;
589
+ const finalY = y + offset.y;
590
+ useEffect2(() => {
591
+ let cancelled = false;
592
+ if (content.staticSrc) {
593
+ const src = content.staticSrc.startsWith("http") ? content.staticSrc : `${basePath}/${content.staticSrc}`;
594
+ setMapImageUrl(src);
595
+ setIsLoading(false);
596
+ return;
597
+ }
598
+ setIsLoading(true);
599
+ setError(null);
600
+ composeMapImage({
601
+ center: content.center,
602
+ zoom: content.zoom,
603
+ style: content.style,
604
+ width,
605
+ height,
606
+ markers: content.markers,
607
+ showAttribution: content.showAttribution !== false
608
+ }).then((dataUrl) => {
609
+ if (!cancelled) {
610
+ setMapImageUrl(dataUrl);
611
+ setIsLoading(false);
612
+ }
613
+ }).catch((err) => {
614
+ if (!cancelled) {
615
+ console.error("Failed to compose map:", err);
616
+ setError(err instanceof Error ? err.message : String(err));
617
+ setIsLoading(false);
618
+ }
619
+ });
620
+ return () => {
621
+ cancelled = true;
622
+ };
623
+ }, [
624
+ content.center.lat,
625
+ content.center.lng,
626
+ content.zoom,
627
+ content.style,
628
+ content.staticSrc,
629
+ width,
630
+ height,
631
+ basePath
632
+ ]);
633
+ const animStyle = getAnimationStyle(animation, blockTime);
634
+ if (isLoading) {
635
+ return /* @__PURE__ */ jsxs3(
636
+ "g",
637
+ {
638
+ className: `block-layer block-layer--map ${animStyle.className}`,
639
+ style: animStyle.style,
640
+ "data-layer-id": layer.id,
641
+ children: [
642
+ /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height, fill: "#e5e7eb" }),
643
+ /* @__PURE__ */ jsx4(
644
+ "text",
645
+ {
646
+ x: finalX + width / 2,
647
+ y: finalY + height / 2,
648
+ textAnchor: "middle",
649
+ dominantBaseline: "middle",
650
+ fill: "#9ca3af",
651
+ fontSize: "24",
652
+ fontFamily: "system-ui, sans-serif",
653
+ children: "Loading map..."
654
+ }
655
+ )
656
+ ]
657
+ }
658
+ );
659
+ }
660
+ if (error || !mapImageUrl) {
661
+ return /* @__PURE__ */ jsxs3(
662
+ "g",
663
+ {
664
+ className: `block-layer block-layer--map ${animStyle.className}`,
665
+ style: animStyle.style,
666
+ "data-layer-id": layer.id,
667
+ children: [
668
+ /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height, fill: "#fef2f2" }),
669
+ /* @__PURE__ */ jsx4(
670
+ "text",
671
+ {
672
+ x: finalX + width / 2,
673
+ y: finalY + height / 2,
674
+ textAnchor: "middle",
675
+ dominantBaseline: "middle",
676
+ fill: "#dc2626",
677
+ fontSize: "18",
678
+ fontFamily: "system-ui, sans-serif",
679
+ children: "Map failed to load"
680
+ }
681
+ )
682
+ ]
683
+ }
684
+ );
685
+ }
686
+ return /* @__PURE__ */ jsxs3(
687
+ "g",
688
+ {
689
+ className: `block-layer block-layer--map ${animStyle.className}`,
690
+ style: animStyle.style,
691
+ "data-layer-id": layer.id,
692
+ children: [
693
+ /* @__PURE__ */ jsx4("defs", { children: /* @__PURE__ */ jsx4("clipPath", { id: `clip-${layer.id}`, children: /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height }) }) }),
694
+ /* @__PURE__ */ jsx4("g", { clipPath: `url(#clip-${layer.id})`, children: /* @__PURE__ */ jsx4(
695
+ "image",
696
+ {
697
+ href: mapImageUrl,
698
+ x: finalX,
699
+ y: finalY,
700
+ width,
701
+ height,
702
+ preserveAspectRatio: "xMidYMid slice",
703
+ style: { pointerEvents: "none" }
704
+ }
705
+ ) })
706
+ ]
707
+ }
708
+ );
709
+ }
710
+
711
+ // src/layers/VideoLayer.tsx
712
+ import { useRef, useEffect as useEffect3 } from "react";
713
+ import { jsx as jsx5 } from "react/jsx-runtime";
714
+ function VideoLayer({
715
+ layer,
716
+ basePath,
717
+ viewport,
718
+ blockTime: _blockTime,
719
+ isPlaying
720
+ }) {
721
+ const { content, position } = layer;
722
+ const videoRef = useRef(null);
723
+ const hasStartedRef = useRef(false);
724
+ const x = resolveValue(position.x, viewport.width);
725
+ const y = resolveValue(position.y, viewport.height);
726
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
727
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
728
+ const offset = getAnchorOffset(position.anchor, width, height);
729
+ const finalX = x + offset.x;
730
+ const finalY = y + offset.y;
731
+ const src = useMediaUrl(content.src, basePath);
732
+ const resolvedPoster = useMediaUrl(content.posterSrc || "", basePath);
733
+ const posterSrc = content.posterSrc ? resolvedPoster : void 0;
734
+ useEffect3(() => {
735
+ const video = videoRef.current;
736
+ if (!video) return;
737
+ video.currentTime = content.clipStart;
738
+ hasStartedRef.current = true;
739
+ if (isPlaying) {
740
+ const playPromise = video.play();
741
+ if (playPromise) {
742
+ playPromise.catch(() => {
743
+ });
744
+ }
745
+ }
746
+ const handleTimeUpdate = () => {
747
+ if (video.currentTime >= content.clipEnd) {
748
+ video.pause();
749
+ video.currentTime = content.clipEnd;
750
+ }
751
+ };
752
+ video.addEventListener("timeupdate", handleTimeUpdate);
753
+ return () => {
754
+ video.removeEventListener("timeupdate", handleTimeUpdate);
755
+ video.pause();
756
+ };
757
+ }, [content.src, content.clipStart, content.clipEnd]);
758
+ useEffect3(() => {
759
+ const video = videoRef.current;
760
+ if (!video || !hasStartedRef.current) return;
761
+ if (video.currentTime >= content.clipEnd) return;
762
+ if (isPlaying) {
763
+ const playPromise = video.play();
764
+ if (playPromise) {
765
+ playPromise.catch(() => {
766
+ });
767
+ }
768
+ } else {
769
+ video.pause();
770
+ }
771
+ }, [isPlaying, content.clipEnd]);
772
+ return /* @__PURE__ */ jsx5("g", { className: "block-layer block-layer--video", "data-layer-id": layer.id, children: /* @__PURE__ */ jsx5("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx5(
773
+ "video",
774
+ {
775
+ ref: videoRef,
776
+ src,
777
+ poster: posterSrc,
778
+ muted: true,
779
+ playsInline: true,
780
+ preload: "auto",
781
+ "data-clip-start": content.clipStart,
782
+ "data-clip-end": content.clipEnd,
783
+ style: {
784
+ width: `${width}px`,
785
+ height: `${height}px`,
786
+ objectFit: content.fit || "cover",
787
+ objectPosition: "center",
788
+ display: "block",
789
+ pointerEvents: "none"
790
+ }
791
+ }
792
+ ) }) });
793
+ }
794
+
795
+ // src/layers/TableLayer.tsx
796
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
797
+ function TableLayer({ layer, viewport, blockTime }) {
798
+ const { content, position, animation } = layer;
799
+ const { headers, rows, align, style } = content;
800
+ const x = resolveValue(position.x, viewport.width);
801
+ const y = resolveValue(position.y, viewport.height);
802
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
803
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
804
+ const offset = getAnchorOffset(position.anchor, width, height);
805
+ const finalX = x + offset.x;
806
+ const finalY = y + offset.y;
807
+ const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
808
+ const cellAlign = (ci) => {
809
+ const a = align?.[ci];
810
+ return a ? { textAlign: a } : void 0;
811
+ };
812
+ const borderRadius = style.borderRadius ?? 8;
813
+ return /* @__PURE__ */ jsx6("foreignObject", { x: finalX, y: finalY, width, height, style: animStyle, children: /* @__PURE__ */ jsx6(
814
+ "div",
815
+ {
816
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
817
+ style: {
818
+ width: `${width}px`,
819
+ height: `${height}px`,
820
+ display: "flex",
821
+ alignItems: "center",
822
+ justifyContent: "center",
823
+ padding: "16px",
824
+ boxSizing: "border-box"
825
+ },
826
+ children: /* @__PURE__ */ jsxs4(
827
+ "table",
828
+ {
829
+ style: {
830
+ width: "100%",
831
+ borderCollapse: "separate",
832
+ borderSpacing: 0,
833
+ fontSize: `${style.fontSize}px`,
834
+ fontFamily: style.fontFamily ?? "system-ui, sans-serif",
835
+ overflow: "hidden",
836
+ borderRadius: `${borderRadius}px`,
837
+ border: `1px solid ${style.borderColor}`
838
+ },
839
+ children: [
840
+ headers.length > 0 && /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { children: headers.map((header, ci) => /* @__PURE__ */ jsx6(
841
+ "th",
842
+ {
843
+ style: {
844
+ background: style.headerBackground,
845
+ color: style.headerColor,
846
+ fontFamily: style.headerFontFamily ?? style.fontFamily ?? "system-ui, sans-serif",
847
+ fontWeight: 600,
848
+ padding: "12px 16px",
849
+ borderBottom: `2px solid ${style.borderColor}`,
850
+ borderRight: ci < headers.length - 1 ? `1px solid ${style.borderColor}` : void 0,
851
+ ...cellAlign(ci)
852
+ },
853
+ children: header
854
+ },
855
+ ci
856
+ )) }) }),
857
+ rows.length > 0 && /* @__PURE__ */ jsx6("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx6("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx6(
858
+ "td",
859
+ {
860
+ style: {
861
+ background: style.cellBackground,
862
+ color: style.cellColor,
863
+ padding: "10px 16px",
864
+ borderBottom: ri < rows.length - 1 ? `1px solid ${style.borderColor}` : void 0,
865
+ borderRight: ci < row.length - 1 ? `1px solid ${style.borderColor}` : void 0,
866
+ ...cellAlign(ci)
867
+ },
868
+ children: cell
869
+ },
870
+ ci
871
+ )) }, ri)) })
872
+ ]
873
+ }
874
+ )
875
+ }
876
+ ) });
877
+ }
878
+
879
+ // src/BlockRenderer.tsx
880
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
881
+ var VIEWPORT = {
882
+ width: 1920,
883
+ height: 1080
884
+ };
885
+ function BlockRenderer({
886
+ block,
887
+ blockTime,
888
+ basePath,
889
+ isEntering = false,
890
+ isExiting = false,
891
+ viewport = VIEWPORT,
892
+ isPlaying
893
+ }) {
894
+ let transitionClass = "";
895
+ const transitionStyle = {};
896
+ if (block.transition && isEntering) {
897
+ transitionClass = getTransitionClass(block.transition.type, true);
898
+ transitionStyle["--transition-duration"] = `${block.transition.duration}s`;
899
+ } else if (block.transition && isExiting) {
900
+ transitionClass = getTransitionClass(block.transition.type, false);
901
+ transitionStyle["--transition-duration"] = `${block.transition.duration}s`;
902
+ }
903
+ const clipId = `vb-clip-${block.id}`;
904
+ return /* @__PURE__ */ jsxs5(
905
+ "svg",
906
+ {
907
+ className: `block-svg ${transitionClass}`,
908
+ style: transitionStyle,
909
+ viewBox: `0 0 ${viewport.width} ${viewport.height}`,
910
+ preserveAspectRatio: "xMidYMid meet",
911
+ overflow: "hidden",
912
+ "data-block-id": block.id,
913
+ children: [
914
+ /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id: clipId, children: /* @__PURE__ */ jsx7("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
915
+ /* @__PURE__ */ jsx7("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx7(
916
+ LayerRenderer,
917
+ {
918
+ layer,
919
+ basePath,
920
+ viewport,
921
+ blockTime,
922
+ isPlaying
923
+ },
924
+ layer.id
925
+ )) })
926
+ ]
927
+ }
928
+ );
929
+ }
930
+ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
931
+ switch (layer.type) {
932
+ case "image":
933
+ return /* @__PURE__ */ jsx7(ImageLayer, { layer, basePath, viewport, blockTime });
934
+ case "text":
935
+ return /* @__PURE__ */ jsx7(TextLayer, { layer, viewport, blockTime });
936
+ case "shape":
937
+ return /* @__PURE__ */ jsx7(ShapeLayer, { layer, viewport, blockTime });
938
+ case "map":
939
+ return /* @__PURE__ */ jsx7(MapLayer, { layer, basePath, viewport, blockTime });
940
+ case "video":
941
+ return /* @__PURE__ */ jsx7(
942
+ VideoLayer,
943
+ {
944
+ layer,
945
+ basePath,
946
+ viewport,
947
+ blockTime,
948
+ isPlaying
949
+ }
950
+ );
951
+ case "table":
952
+ return /* @__PURE__ */ jsx7(TableLayer, { layer, viewport, blockTime });
953
+ default:
954
+ console.warn(`Unknown layer type: ${layer.type}`);
955
+ return null;
956
+ }
957
+ }
958
+
959
+ // src/CaptionOverlay.tsx
960
+ import { getCaptionAtTime } from "@bendyline/squisq/schemas";
961
+
962
+ // src/SocialCaptionOverlay.tsx
963
+ import { useMemo as useMemo2 } from "react";
964
+ import { resolveFontFamily } from "@bendyline/squisq/schemas";
965
+ import { jsx as jsx8 } from "react/jsx-runtime";
966
+ var TARGET_CHUNK_SIZE = 4;
967
+ var MIN_CHUNK_SIZE = 2;
968
+ var MAX_CHUNK_SIZE = 6;
969
+ function resolvePhraseTiming(phrase) {
970
+ if (phrase.words && phrase.words.length > 0) {
971
+ return phrase.words.map((w) => ({
972
+ text: w.text,
973
+ startTime: w.startTime,
974
+ endTime: w.endTime
975
+ }));
976
+ }
977
+ const rawWords = phrase.text.split(/\s+/).filter((w) => w.length > 0);
978
+ if (rawWords.length === 0) return [];
979
+ const duration = phrase.endTime - phrase.startTime;
980
+ const wordDuration = duration / rawWords.length;
981
+ return rawWords.map((text, i) => ({
982
+ text,
983
+ startTime: phrase.startTime + i * wordDuration,
984
+ endTime: phrase.startTime + (i + 1) * wordDuration
985
+ }));
986
+ }
987
+ function buildWordStream(captions) {
988
+ const allWords = [];
989
+ for (const phrase of captions.phrases) {
990
+ allWords.push(...resolvePhraseTiming(phrase));
991
+ }
992
+ if (allWords.length === 0) return { words: [], chunks: [] };
993
+ let chunkSize = TARGET_CHUNK_SIZE;
994
+ if (allWords.length > chunkSize) {
995
+ const numChunks = Math.ceil(allWords.length / chunkSize);
996
+ chunkSize = Math.ceil(allWords.length / numChunks);
997
+ chunkSize = Math.min(MAX_CHUNK_SIZE, Math.max(MIN_CHUNK_SIZE, chunkSize));
998
+ } else {
999
+ chunkSize = Math.max(MIN_CHUNK_SIZE, allWords.length);
1000
+ }
1001
+ const chunks = [];
1002
+ for (let i = 0; i < allWords.length; i += chunkSize) {
1003
+ const chunkWords = allWords.slice(i, i + chunkSize);
1004
+ chunks.push({
1005
+ words: chunkWords,
1006
+ startTime: chunkWords[0].startTime,
1007
+ endTime: chunkWords[chunkWords.length - 1].endTime
1008
+ });
1009
+ }
1010
+ return { words: allWords, chunks };
1011
+ }
1012
+ function SocialCaptionOverlay({
1013
+ captions,
1014
+ currentTime,
1015
+ enabled = true,
1016
+ theme,
1017
+ viewport
1018
+ }) {
1019
+ const { chunks } = useMemo2(
1020
+ () => captions ? buildWordStream(captions) : { words: [], chunks: [] },
1021
+ [captions]
1022
+ );
1023
+ if (!enabled || chunks.length === 0) {
1024
+ return /* @__PURE__ */ jsx8(
1025
+ "div",
1026
+ {
1027
+ className: "social-caption-overlay",
1028
+ style: {
1029
+ position: "absolute",
1030
+ bottom: "18%",
1031
+ left: 0,
1032
+ right: 0,
1033
+ zIndex: 50,
1034
+ pointerEvents: "none",
1035
+ opacity: 0,
1036
+ transition: "opacity 0.15s ease-in-out"
1037
+ }
1038
+ }
1039
+ );
1040
+ }
1041
+ let activeChunk = null;
1042
+ let activeWordIndex = -1;
1043
+ for (const chunk of chunks) {
1044
+ if (currentTime >= chunk.startTime && currentTime < chunk.endTime) {
1045
+ activeChunk = chunk;
1046
+ break;
1047
+ }
1048
+ }
1049
+ if (!activeChunk) {
1050
+ for (let i = 0; i < chunks.length - 1; i++) {
1051
+ if (currentTime >= chunks[i].endTime && currentTime < chunks[i + 1].startTime) {
1052
+ activeChunk = chunks[i];
1053
+ activeWordIndex = activeChunk.words.length - 1;
1054
+ break;
1055
+ }
1056
+ }
1057
+ if (!activeChunk && chunks.length > 0 && currentTime >= chunks[chunks.length - 1].startTime) {
1058
+ activeChunk = chunks[chunks.length - 1];
1059
+ activeWordIndex = activeChunk.words.length - 1;
1060
+ }
1061
+ }
1062
+ if (!activeChunk) return null;
1063
+ if (activeWordIndex === -1) {
1064
+ for (let i = 0; i < activeChunk.words.length; i++) {
1065
+ const word = activeChunk.words[i];
1066
+ if (currentTime >= word.startTime && currentTime < word.endTime) {
1067
+ activeWordIndex = i;
1068
+ break;
1069
+ }
1070
+ }
1071
+ if (activeWordIndex === -1) {
1072
+ for (let i = activeChunk.words.length - 1; i >= 0; i--) {
1073
+ if (currentTime >= activeChunk.words[i].startTime) {
1074
+ activeWordIndex = i;
1075
+ break;
1076
+ }
1077
+ }
1078
+ }
1079
+ if (activeWordIndex === -1) activeWordIndex = 0;
1080
+ }
1081
+ const primaryColor = theme?.colors?.primary ?? "#5b9bd5";
1082
+ const fontFamily = theme?.typography?.titleFont ? resolveFontFamily(theme.typography.titleFont, '"PT Serif", Georgia, serif') : '"PT Serif", Georgia, serif';
1083
+ const viewportHeight = viewport?.height ?? 720;
1084
+ const baseFontSize = Math.round(viewportHeight * 0.055);
1085
+ const fontSize = Math.max(24, Math.min(72, baseFontSize));
1086
+ return /* @__PURE__ */ jsx8(
1087
+ "div",
1088
+ {
1089
+ className: "social-caption-overlay",
1090
+ style: {
1091
+ position: "absolute",
1092
+ bottom: "18%",
1093
+ left: 0,
1094
+ right: 0,
1095
+ zIndex: 50,
1096
+ pointerEvents: "none",
1097
+ textAlign: "center",
1098
+ padding: "0 8%",
1099
+ boxSizing: "border-box",
1100
+ opacity: 1,
1101
+ transition: "opacity 0.15s ease-in-out"
1102
+ },
1103
+ children: /* @__PURE__ */ jsx8(
1104
+ "div",
1105
+ {
1106
+ style: {
1107
+ display: "inline-block",
1108
+ lineHeight: 1.3
1109
+ },
1110
+ children: activeChunk.words.map((word, i) => {
1111
+ const isActive = i === activeWordIndex;
1112
+ return /* @__PURE__ */ jsx8(
1113
+ "span",
1114
+ {
1115
+ style: {
1116
+ fontFamily,
1117
+ fontSize: `${fontSize}px`,
1118
+ fontWeight: isActive ? 800 : 600,
1119
+ color: isActive ? primaryColor : "rgba(255, 255, 255, 0.9)",
1120
+ textShadow: "0 2px 8px rgba(0,0,0,0.7), 0 0 20px rgba(0,0,0,0.4)",
1121
+ marginRight: i < activeChunk.words.length - 1 ? "0.3em" : void 0,
1122
+ transition: "color 0.1s ease, font-weight 0.1s ease",
1123
+ textTransform: "uppercase"
1124
+ },
1125
+ children: word.text
1126
+ },
1127
+ `${word.startTime}-${i}`
1128
+ );
1129
+ })
1130
+ }
1131
+ )
1132
+ }
1133
+ );
1134
+ }
1135
+
1136
+ // src/CaptionOverlay.tsx
1137
+ import { jsx as jsx9 } from "react/jsx-runtime";
1138
+ function CaptionOverlay({
1139
+ captions,
1140
+ currentTime,
1141
+ enabled = true,
1142
+ fontSize = 16,
1143
+ captionStyle = "standard",
1144
+ theme,
1145
+ viewport
1146
+ }) {
1147
+ if (captionStyle === "social") {
1148
+ return /* @__PURE__ */ jsx9(
1149
+ SocialCaptionOverlay,
1150
+ {
1151
+ captions,
1152
+ currentTime,
1153
+ enabled,
1154
+ theme,
1155
+ viewport
1156
+ }
1157
+ );
1158
+ }
1159
+ const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
1160
+ const captionText = phrase?.text ?? null;
1161
+ return /* @__PURE__ */ jsx9(
1162
+ "div",
1163
+ {
1164
+ className: "caption-overlay",
1165
+ style: {
1166
+ position: "absolute",
1167
+ top: "6px",
1168
+ left: "50%",
1169
+ transform: "translateX(-50%)",
1170
+ zIndex: 50,
1171
+ pointerEvents: "none",
1172
+ maxWidth: "100%",
1173
+ width: "100%",
1174
+ textAlign: "center",
1175
+ opacity: captionText ? 1 : 0,
1176
+ transition: "opacity 0.15s ease-in-out",
1177
+ padding: "0 4px",
1178
+ boxSizing: "border-box"
1179
+ },
1180
+ children: captionText && /* @__PURE__ */ jsx9(
1181
+ "div",
1182
+ {
1183
+ style: {
1184
+ display: "inline-block",
1185
+ padding: "3px 10px",
1186
+ background: "rgba(0, 0, 0, 0.65)",
1187
+ borderRadius: "4px",
1188
+ backdropFilter: "blur(4px)"
1189
+ },
1190
+ children: /* @__PURE__ */ jsx9(
1191
+ "span",
1192
+ {
1193
+ style: {
1194
+ color: "#ffffff",
1195
+ fontSize: `${fontSize}px`,
1196
+ fontFamily: "'Hanken Grotesk', system-ui, sans-serif",
1197
+ fontWeight: 500,
1198
+ lineHeight: 1.25,
1199
+ textShadow: "0 1px 3px rgba(0,0,0,0.5)",
1200
+ whiteSpace: "pre-wrap",
1201
+ wordWrap: "break-word"
1202
+ },
1203
+ children: captionText
1204
+ }
1205
+ )
1206
+ }
1207
+ )
1208
+ }
1209
+ );
1210
+ }
1211
+
1212
+ // src/hooks/useAutoSurface.ts
1213
+ import { useCallback, useMemo as useMemo3, useSyncExternalStore } from "react";
1214
+ import { DARK_SURFACE, LIGHT_SURFACE } from "@bendyline/squisq/schemas";
1215
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
1216
+ var getServerSnapshot = () => LIGHT_SURFACE;
1217
+ function useAutoSurface(enabled) {
1218
+ const mql = useMemo3(
1219
+ () => enabled && typeof window !== "undefined" ? window.matchMedia(DARK_QUERY) : null,
1220
+ [enabled]
1221
+ );
1222
+ const subscribe = useCallback(
1223
+ (cb) => {
1224
+ if (!mql) return () => {
1225
+ };
1226
+ mql.addEventListener("change", cb);
1227
+ return () => mql.removeEventListener("change", cb);
1228
+ },
1229
+ [mql]
1230
+ );
1231
+ const getSnapshot = useCallback(() => mql?.matches ? DARK_SURFACE : LIGHT_SURFACE, [mql]);
1232
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1233
+ }
1234
+
1235
+ // src/hooks/useAudioSync.ts
1236
+ import { useState as useState3, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback2 } from "react";
1237
+ function useAudioSync(audioRef, audioTrack, basePath = "") {
1238
+ const [currentTime, setCurrentTime] = useState3(0);
1239
+ const [isPlaying, setIsPlaying] = useState3(false);
1240
+ const [currentSegment, setCurrentSegment] = useState3(0);
1241
+ const [isEnded, setIsEnded] = useState3(false);
1242
+ const [isAudioReady, setIsAudioReady] = useState3(false);
1243
+ const [totalDuration, setTotalDuration] = useState3(0);
1244
+ const segmentStarts = useRef2([]);
1245
+ const pendingSeekTime = useRef2(null);
1246
+ const shouldPlayAfterLoad = useRef2(false);
1247
+ const blobUrls = useRef2(/* @__PURE__ */ new Map());
1248
+ const loadingPromises = useRef2(/* @__PURE__ */ new Map());
1249
+ const fallbackMode = useRef2(false);
1250
+ useEffect4(() => {
1251
+ if (!audioTrack?.segments) {
1252
+ return;
1253
+ }
1254
+ let time = 0;
1255
+ segmentStarts.current = audioTrack.segments.map((seg) => {
1256
+ const start = time;
1257
+ time += seg.duration;
1258
+ return start;
1259
+ });
1260
+ setTotalDuration(time);
1261
+ }, [audioTrack]);
1262
+ const preloadAudio = useCallback2(
1263
+ async (src) => {
1264
+ const audioUrl = basePath ? `${basePath}/${src}` : src;
1265
+ if (blobUrls.current.has(src)) {
1266
+ return blobUrls.current.get(src);
1267
+ }
1268
+ if (loadingPromises.current.has(src)) {
1269
+ return loadingPromises.current.get(src);
1270
+ }
1271
+ const loadPromise = (async () => {
1272
+ try {
1273
+ const response = await fetch(audioUrl);
1274
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1275
+ const blob = await response.blob();
1276
+ const blobUrl = URL.createObjectURL(blob);
1277
+ blobUrls.current.set(src, blobUrl);
1278
+ return blobUrl;
1279
+ } catch {
1280
+ return audioUrl;
1281
+ } finally {
1282
+ loadingPromises.current.delete(src);
1283
+ }
1284
+ })();
1285
+ loadingPromises.current.set(src, loadPromise);
1286
+ return loadPromise;
1287
+ },
1288
+ [basePath]
1289
+ );
1290
+ useEffect4(() => {
1291
+ if (!audioTrack?.segments) return;
1292
+ audioTrack.segments.forEach((segment) => {
1293
+ preloadAudio(segment.src);
1294
+ });
1295
+ const currentBlobUrls = blobUrls.current;
1296
+ return () => {
1297
+ currentBlobUrls.forEach((url) => {
1298
+ URL.revokeObjectURL(url);
1299
+ });
1300
+ currentBlobUrls.clear();
1301
+ };
1302
+ }, [audioTrack, preloadAudio]);
1303
+ useEffect4(() => {
1304
+ const audio = audioRef.current;
1305
+ if (!audio) return;
1306
+ const handleTimeUpdate = () => {
1307
+ const segmentStart = segmentStarts.current[currentSegment] || 0;
1308
+ const overallTime = segmentStart + audio.currentTime;
1309
+ setCurrentTime(overallTime);
1310
+ };
1311
+ const handlePlay = () => {
1312
+ fallbackMode.current = false;
1313
+ setIsPlaying(true);
1314
+ };
1315
+ const handlePause = () => setIsPlaying(false);
1316
+ const handleError = () => {
1317
+ setIsAudioReady(true);
1318
+ };
1319
+ const handleEnded = () => {
1320
+ if (audioTrack && currentSegment < audioTrack.segments.length - 1) {
1321
+ shouldPlayAfterLoad.current = true;
1322
+ setCurrentSegment((prev) => prev + 1);
1323
+ } else {
1324
+ setIsEnded(true);
1325
+ setIsPlaying(false);
1326
+ }
1327
+ };
1328
+ audio.addEventListener("timeupdate", handleTimeUpdate);
1329
+ audio.addEventListener("play", handlePlay);
1330
+ audio.addEventListener("pause", handlePause);
1331
+ audio.addEventListener("ended", handleEnded);
1332
+ audio.addEventListener("error", handleError);
1333
+ return () => {
1334
+ audio.removeEventListener("timeupdate", handleTimeUpdate);
1335
+ audio.removeEventListener("play", handlePlay);
1336
+ audio.removeEventListener("pause", handlePause);
1337
+ audio.removeEventListener("ended", handleEnded);
1338
+ audio.removeEventListener("error", handleError);
1339
+ };
1340
+ }, [audioRef, currentSegment, audioTrack]);
1341
+ useEffect4(() => {
1342
+ const audio = audioRef.current;
1343
+ if (!audio || !audioTrack?.segments) return;
1344
+ const segment = audioTrack.segments[currentSegment];
1345
+ if (!segment) return;
1346
+ const applyPendingSeek = () => {
1347
+ if (pendingSeekTime.current !== null) {
1348
+ const segmentStart = segmentStarts.current[currentSegment] || 0;
1349
+ const segmentTime = pendingSeekTime.current - segmentStart;
1350
+ audio.currentTime = Math.max(0, segmentTime);
1351
+ setCurrentTime(pendingSeekTime.current);
1352
+ pendingSeekTime.current = null;
1353
+ }
1354
+ if (shouldPlayAfterLoad.current) {
1355
+ audio.play().catch(() => {
1356
+ });
1357
+ shouldPlayAfterLoad.current = false;
1358
+ }
1359
+ };
1360
+ const currentSrc = audio.src;
1361
+ const cachedBlobUrl = blobUrls.current.get(segment.src);
1362
+ const isSameSource = currentSrc && (currentSrc === cachedBlobUrl || currentSrc.endsWith(segment.src));
1363
+ if (!isSameSource) {
1364
+ const loadAndPlay = async () => {
1365
+ const blobUrl = await preloadAudio(segment.src);
1366
+ const handleCanPlay = () => {
1367
+ setIsAudioReady(true);
1368
+ applyPendingSeek();
1369
+ audio.removeEventListener("canplay", handleCanPlay);
1370
+ };
1371
+ audio.addEventListener("canplay", handleCanPlay);
1372
+ audio.src = blobUrl;
1373
+ audio.load();
1374
+ await Promise.resolve();
1375
+ if (audio.readyState >= 3) {
1376
+ audio.removeEventListener("canplay", handleCanPlay);
1377
+ setIsAudioReady(true);
1378
+ applyPendingSeek();
1379
+ }
1380
+ };
1381
+ loadAndPlay();
1382
+ } else {
1383
+ applyPendingSeek();
1384
+ }
1385
+ }, [audioRef, currentSegment, audioTrack, preloadAudio]);
1386
+ const play = useCallback2(() => {
1387
+ const audio = audioRef.current;
1388
+ if (audio) {
1389
+ if (isEnded) {
1390
+ setCurrentSegment(0);
1391
+ setIsEnded(false);
1392
+ }
1393
+ audio.play().then(() => {
1394
+ fallbackMode.current = false;
1395
+ }).catch(() => {
1396
+ fallbackMode.current = true;
1397
+ setIsPlaying(true);
1398
+ });
1399
+ }
1400
+ }, [audioRef, isEnded]);
1401
+ const pause = useCallback2(() => {
1402
+ const audio = audioRef.current;
1403
+ if (audio) {
1404
+ audio.pause();
1405
+ }
1406
+ setIsPlaying(false);
1407
+ }, [audioRef]);
1408
+ const toggle = useCallback2(() => {
1409
+ const audio = audioRef.current;
1410
+ if (!audio) return;
1411
+ if (!isPlaying) {
1412
+ play();
1413
+ } else {
1414
+ pause();
1415
+ }
1416
+ }, [audioRef, isPlaying, play, pause]);
1417
+ const seekTo = useCallback2(
1418
+ (time) => {
1419
+ const audio = audioRef.current;
1420
+ if (!audio || !audioTrack?.segments) return;
1421
+ const clampedTime = totalDuration > 0 ? Math.max(0, Math.min(time, totalDuration)) : Math.max(0, time);
1422
+ let segmentIndex = 0;
1423
+ let segmentStart = 0;
1424
+ for (let i = 0; i < audioTrack.segments.length; i++) {
1425
+ const segEnd = segmentStart + audioTrack.segments[i].duration;
1426
+ if (clampedTime < segEnd) {
1427
+ segmentIndex = i;
1428
+ break;
1429
+ }
1430
+ segmentStart = segEnd;
1431
+ if (i === audioTrack.segments.length - 1) {
1432
+ segmentIndex = i;
1433
+ }
1434
+ }
1435
+ const wasPlaying = !audio.paused;
1436
+ setIsEnded(false);
1437
+ if (segmentIndex !== currentSegment) {
1438
+ pendingSeekTime.current = clampedTime;
1439
+ shouldPlayAfterLoad.current = wasPlaying;
1440
+ setCurrentSegment(segmentIndex);
1441
+ } else {
1442
+ const segmentTime = clampedTime - segmentStart;
1443
+ audio.currentTime = Math.max(0, segmentTime);
1444
+ setCurrentTime(clampedTime);
1445
+ }
1446
+ },
1447
+ [audioRef, audioTrack, currentSegment, totalDuration]
1448
+ );
1449
+ const skipToSegment = useCallback2(
1450
+ (index) => {
1451
+ if (!audioTrack?.segments || index < 0 || index >= audioTrack.segments.length) {
1452
+ return;
1453
+ }
1454
+ setCurrentSegment(index);
1455
+ setIsEnded(false);
1456
+ },
1457
+ [audioTrack]
1458
+ );
1459
+ const restart = useCallback2(async () => {
1460
+ seekTo(0);
1461
+ await new Promise((resolve) => setTimeout(resolve, 50));
1462
+ play();
1463
+ }, [seekTo, play]);
1464
+ useEffect4(() => {
1465
+ if (!isPlaying || !fallbackMode.current || !totalDuration) return;
1466
+ let lastTime = performance.now();
1467
+ let raf;
1468
+ const tick = (now) => {
1469
+ if (!fallbackMode.current) return;
1470
+ const dt = (now - lastTime) / 1e3;
1471
+ lastTime = now;
1472
+ setCurrentTime((prev) => {
1473
+ const next = prev + dt;
1474
+ if (next >= totalDuration) {
1475
+ fallbackMode.current = false;
1476
+ setIsEnded(true);
1477
+ setIsPlaying(false);
1478
+ return totalDuration;
1479
+ }
1480
+ return next;
1481
+ });
1482
+ raf = requestAnimationFrame(tick);
1483
+ };
1484
+ raf = requestAnimationFrame(tick);
1485
+ return () => cancelAnimationFrame(raf);
1486
+ }, [isPlaying, totalDuration]);
1487
+ return {
1488
+ // State
1489
+ currentTime,
1490
+ isPlaying,
1491
+ currentSegment,
1492
+ totalDuration,
1493
+ isEnded,
1494
+ isReady: isAudioReady,
1495
+ isAvailable: true,
1496
+ // HTML5 audio is always available in browsers
1497
+ // Actions
1498
+ play: async () => play(),
1499
+ pause: async () => pause(),
1500
+ toggle: async () => toggle(),
1501
+ seekTo: async (time) => seekTo(time),
1502
+ skipToSegment: async (index) => skipToSegment(index),
1503
+ restart
1504
+ };
1505
+ }
1506
+
1507
+ // src/hooks/useDocPlayback.ts
1508
+ import { useState as useState4, useEffect as useEffect5, useMemo as useMemo4, useCallback as useCallback3, useRef as useRef3 } from "react";
1509
+ import { getBlockAtTime } from "@bendyline/squisq/schemas";
1510
+ import {
1511
+ expandDocBlocks,
1512
+ flattenBlocks,
1513
+ isTemplateBlock,
1514
+ VIEWPORT_PRESETS
1515
+ } from "@bendyline/squisq/doc";
1516
+ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landscape, renderMode = false, theme) {
1517
+ const [transitionState, setTransitionState] = useState4({
1518
+ entering: false,
1519
+ exiting: false,
1520
+ previousBlock: null
1521
+ });
1522
+ const blocks = useMemo4(() => {
1523
+ if (!script?.blocks) {
1524
+ return [];
1525
+ }
1526
+ const hasChildren = script.blocks.some((b) => b.children && b.children.length > 0);
1527
+ const flatBlocks = hasChildren ? flattenBlocks(script.blocks) : script.blocks;
1528
+ const hasTemplates = flatBlocks.some(isTemplateBlock);
1529
+ if (hasTemplates) {
1530
+ const audioSegments = script.audio?.segments?.map((seg) => ({
1531
+ startTime: seg.startTime,
1532
+ duration: seg.duration
1533
+ }));
1534
+ const expanded = expandDocBlocks(flatBlocks, {
1535
+ audioSegments,
1536
+ viewport,
1537
+ persistentLayers: script.persistentLayers,
1538
+ theme
1539
+ });
1540
+ return expanded;
1541
+ }
1542
+ return flatBlocks;
1543
+ }, [script?.blocks, script?.audio?.segments, script?.persistentLayers, viewport, theme]);
1544
+ const currentBlock = useMemo4(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
1545
+ const currentBlockIndex = useMemo4(
1546
+ () => currentBlock ? blocks.indexOf(currentBlock) : -1,
1547
+ [blocks, currentBlock]
1548
+ );
1549
+ const blockTime = useMemo4(() => {
1550
+ if (!currentBlock) return 0;
1551
+ return Math.max(0, currentTime - currentBlock.startTime);
1552
+ }, [currentBlock, currentTime]);
1553
+ const blockProgress = useMemo4(() => {
1554
+ if (!currentBlock || currentBlock.duration === 0) return 0;
1555
+ return Math.min(1, blockTime / currentBlock.duration);
1556
+ }, [currentBlock, blockTime]);
1557
+ const docProgress = useMemo4(() => {
1558
+ if (!script || script.duration === 0) return 0;
1559
+ return Math.min(1, currentTime / script.duration);
1560
+ }, [script, currentTime]);
1561
+ const _prevBlockRef = useMemo4(
1562
+ () => currentBlock,
1563
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally keyed on index, not block reference
1564
+ [currentBlockIndex]
1565
+ );
1566
+ useEffect5(() => {
1567
+ if (!currentBlock || renderMode) return;
1568
+ if (transitionState.previousBlock?.id !== currentBlock.id) {
1569
+ const transition = currentBlock.transition;
1570
+ const transitionDuration = transition?.duration || 0;
1571
+ if (transitionDuration > 0) {
1572
+ setTransitionState({
1573
+ entering: true,
1574
+ exiting: true,
1575
+ previousBlock: transitionState.previousBlock
1576
+ });
1577
+ const timer = setTimeout(() => {
1578
+ setTransitionState({
1579
+ entering: false,
1580
+ exiting: false,
1581
+ previousBlock: currentBlock
1582
+ });
1583
+ }, transitionDuration * 1e3);
1584
+ return () => clearTimeout(timer);
1585
+ } else {
1586
+ setTransitionState({
1587
+ entering: false,
1588
+ exiting: false,
1589
+ previousBlock: currentBlock
1590
+ });
1591
+ }
1592
+ }
1593
+ }, [currentBlock?.id, renderMode]);
1594
+ const renderPrevBlockRef = useRef3(null);
1595
+ useEffect5(() => {
1596
+ if (!renderMode || !currentBlock) return;
1597
+ if (transitionState.previousBlock?.id !== currentBlock.id) {
1598
+ const oldPrev = transitionState.previousBlock;
1599
+ renderPrevBlockRef.current = oldPrev;
1600
+ setTransitionState((prev) => ({
1601
+ ...prev,
1602
+ previousBlock: currentBlock
1603
+ }));
1604
+ }
1605
+ }, [currentBlock?.id, renderMode]);
1606
+ const renderTransitionDuration = currentBlock?.transition?.duration || 0;
1607
+ const renderIsEntering = renderMode && renderTransitionDuration > 0 && blockTime < renderTransitionDuration;
1608
+ const renderIsExiting = renderIsEntering && renderPrevBlockRef.current !== null;
1609
+ const goToBlock = useCallback3(
1610
+ (index) => {
1611
+ if (!script || index < 0 || index >= blocks.length) return;
1612
+ const targetBlock = blocks[index];
1613
+ if (targetBlock) {
1614
+ return targetBlock.startTime;
1615
+ }
1616
+ },
1617
+ [script, blocks]
1618
+ );
1619
+ const nextBlock = useCallback3(() => {
1620
+ if (currentBlockIndex < blocks.length - 1) {
1621
+ return goToBlock(currentBlockIndex + 1);
1622
+ }
1623
+ }, [currentBlockIndex, blocks.length, goToBlock]);
1624
+ const prevBlock = useCallback3(() => {
1625
+ if (currentBlockIndex > 0) {
1626
+ return goToBlock(currentBlockIndex - 1);
1627
+ }
1628
+ }, [currentBlockIndex, goToBlock]);
1629
+ return {
1630
+ currentBlock,
1631
+ currentBlockIndex,
1632
+ previousBlock: renderMode ? renderIsExiting ? renderPrevBlockRef.current : null : transitionState.exiting ? transitionState.previousBlock : null,
1633
+ isEntering: renderMode ? renderIsEntering : transitionState.entering,
1634
+ isExiting: renderMode ? renderIsExiting : transitionState.exiting,
1635
+ blockTime,
1636
+ blockProgress,
1637
+ docProgress,
1638
+ nextBlock,
1639
+ prevBlock,
1640
+ goToBlock,
1641
+ /** Expanded blocks (templates converted to full blocks with layers) */
1642
+ blocks
1643
+ };
1644
+ }
1645
+
1646
+ // src/hooks/useViewportOrientation.ts
1647
+ import { useState as useState5, useEffect as useEffect6, useMemo as useMemo5 } from "react";
1648
+ import {
1649
+ VIEWPORT_PRESETS as VIEWPORT_PRESETS2
1650
+ } from "@bendyline/squisq/doc";
1651
+ function getOrientationFromWindow(width, height) {
1652
+ const ratio = width / height;
1653
+ if (ratio > 1.2) {
1654
+ return "landscape";
1655
+ } else if (ratio < 0.83) {
1656
+ return "portrait";
1657
+ } else {
1658
+ return "landscape";
1659
+ }
1660
+ }
1661
+ function getViewportForOrientation(orientation) {
1662
+ switch (orientation) {
1663
+ case "portrait":
1664
+ return VIEWPORT_PRESETS2.portrait;
1665
+ case "square":
1666
+ return VIEWPORT_PRESETS2.square;
1667
+ case "landscape":
1668
+ default:
1669
+ return VIEWPORT_PRESETS2.landscape;
1670
+ }
1671
+ }
1672
+ function useViewportOrientation() {
1673
+ const [windowSize, setWindowSize] = useState5(() => ({
1674
+ width: typeof window !== "undefined" ? window.innerWidth : 1920,
1675
+ height: typeof window !== "undefined" ? window.innerHeight : 1080
1676
+ }));
1677
+ useEffect6(() => {
1678
+ if (typeof window === "undefined") return;
1679
+ const handleResize = () => {
1680
+ setWindowSize({
1681
+ width: window.innerWidth,
1682
+ height: window.innerHeight
1683
+ });
1684
+ };
1685
+ let timeoutId;
1686
+ const debouncedResize = () => {
1687
+ clearTimeout(timeoutId);
1688
+ timeoutId = setTimeout(handleResize, 100);
1689
+ };
1690
+ window.addEventListener("resize", debouncedResize);
1691
+ return () => {
1692
+ window.removeEventListener("resize", debouncedResize);
1693
+ clearTimeout(timeoutId);
1694
+ };
1695
+ }, []);
1696
+ const orientation = useMemo5(
1697
+ () => getOrientationFromWindow(windowSize.width, windowSize.height),
1698
+ [windowSize.width, windowSize.height]
1699
+ );
1700
+ const viewport = useMemo5(() => getViewportForOrientation(orientation), [orientation]);
1701
+ return {
1702
+ viewport,
1703
+ orientation,
1704
+ windowSize
1705
+ };
1706
+ }
1707
+
1708
+ // src/DocPlayer.tsx
1709
+ import {
1710
+ expandCoverBlock,
1711
+ createTemplateContext,
1712
+ DEFAULT_THEME as DEFAULT_THEME2,
1713
+ VIEWPORT_PRESETS as VIEWPORT_PRESETS4
1714
+ } from "@bendyline/squisq/doc";
1715
+
1716
+ // src/DocProgressBar.tsx
1717
+ import { useRef as useRef4, useState as useState6, useCallback as useCallback4 } from "react";
1718
+
1719
+ // src/types.ts
1720
+ function formatTime(seconds) {
1721
+ const mins = Math.floor(seconds / 60);
1722
+ const secs = Math.floor(seconds % 60);
1723
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1724
+ }
1725
+
1726
+ // src/DocProgressBar.tsx
1727
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1728
+ function DocProgressBar({
1729
+ state,
1730
+ actions,
1731
+ blockMarkers,
1732
+ expandedBlocks,
1733
+ getBlockTitle
1734
+ }) {
1735
+ const progressBarRef = useRef4(null);
1736
+ const [hoverPosition, setHoverPosition] = useState6(null);
1737
+ const handleProgressHover = useCallback4((e) => {
1738
+ const bar = progressBarRef.current;
1739
+ if (!bar) return;
1740
+ const rect = bar.getBoundingClientRect();
1741
+ const x = e.clientX - rect.left;
1742
+ const progress = Math.max(0, Math.min(1, x / rect.width));
1743
+ setHoverPosition(progress);
1744
+ }, []);
1745
+ const handleProgressLeave = useCallback4(() => {
1746
+ setHoverPosition(null);
1747
+ }, []);
1748
+ const getBlockAtTimeLocal = useCallback4(
1749
+ (time) => {
1750
+ for (let i = expandedBlocks.length - 1; i >= 0; i--) {
1751
+ const blk = expandedBlocks[i];
1752
+ if (time >= blk.startTime) {
1753
+ return { block: blk, index: i };
1754
+ }
1755
+ }
1756
+ return expandedBlocks.length > 0 ? { block: expandedBlocks[0], index: 0 } : null;
1757
+ },
1758
+ [expandedBlocks]
1759
+ );
1760
+ return /* @__PURE__ */ jsxs6(
1761
+ "div",
1762
+ {
1763
+ ref: progressBarRef,
1764
+ style: {
1765
+ flex: 1,
1766
+ height: "24px",
1767
+ cursor: "pointer",
1768
+ position: "relative",
1769
+ display: "flex",
1770
+ alignItems: "center"
1771
+ },
1772
+ onClick: (e) => {
1773
+ const rect = e.currentTarget.getBoundingClientRect();
1774
+ const x = e.clientX - rect.left;
1775
+ const progress = x / rect.width;
1776
+ actions.seekTo(progress * state.totalDuration);
1777
+ },
1778
+ onMouseMove: handleProgressHover,
1779
+ onMouseLeave: handleProgressLeave,
1780
+ children: [
1781
+ /* @__PURE__ */ jsx10(
1782
+ "div",
1783
+ {
1784
+ style: {
1785
+ position: "absolute",
1786
+ left: 0,
1787
+ right: 0,
1788
+ height: "6px",
1789
+ background: "rgba(255,255,255,0.2)",
1790
+ borderRadius: "3px"
1791
+ }
1792
+ }
1793
+ ),
1794
+ /* @__PURE__ */ jsx10(
1795
+ "div",
1796
+ {
1797
+ style: {
1798
+ position: "absolute",
1799
+ left: 0,
1800
+ width: `${state.docProgress * 100}%`,
1801
+ height: "6px",
1802
+ background: "#5b9bd5",
1803
+ borderRadius: "3px",
1804
+ transition: "width 0.1s"
1805
+ }
1806
+ }
1807
+ ),
1808
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx10(
1809
+ "div",
1810
+ {
1811
+ style: {
1812
+ position: "absolute",
1813
+ left: `${marker.position}%`,
1814
+ transform: "translateX(-50%)",
1815
+ width: "10px",
1816
+ height: "10px",
1817
+ borderRadius: "50%",
1818
+ background: marker.index === state.currentBlockIndex ? "#ffffff" : "rgba(255,255,255,0.5)",
1819
+ border: "2px solid #5b9bd5",
1820
+ cursor: "pointer",
1821
+ zIndex: 2,
1822
+ transition: "transform 0.15s, background 0.15s"
1823
+ },
1824
+ title: marker.title,
1825
+ onClick: (e) => {
1826
+ e.stopPropagation();
1827
+ actions.seekTo(marker.block.startTime);
1828
+ },
1829
+ onMouseEnter: (e) => {
1830
+ e.currentTarget.style.transform = "translateX(-50%) scale(1.3)";
1831
+ },
1832
+ onMouseLeave: (e) => {
1833
+ e.currentTarget.style.transform = "translateX(-50%)";
1834
+ }
1835
+ },
1836
+ `${marker.block.id}-${i}`
1837
+ )),
1838
+ hoverPosition !== null && /* @__PURE__ */ jsxs6(
1839
+ "div",
1840
+ {
1841
+ style: {
1842
+ position: "absolute",
1843
+ left: `${hoverPosition * 100}%`,
1844
+ bottom: "100%",
1845
+ transform: "translateX(-50%)",
1846
+ marginBottom: "8px",
1847
+ padding: "6px 10px",
1848
+ background: "rgba(0,0,0,0.9)",
1849
+ borderRadius: "4px",
1850
+ whiteSpace: "nowrap",
1851
+ pointerEvents: "none",
1852
+ zIndex: 10
1853
+ },
1854
+ children: [
1855
+ /* @__PURE__ */ jsx10("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
1856
+ (() => {
1857
+ const hoverTime = hoverPosition * state.totalDuration;
1858
+ const slideInfo = getBlockAtTimeLocal(hoverTime);
1859
+ if (slideInfo && getBlockTitle) {
1860
+ return /* @__PURE__ */ jsx10("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
1861
+ }
1862
+ return null;
1863
+ })()
1864
+ ]
1865
+ }
1866
+ ),
1867
+ hoverPosition !== null && /* @__PURE__ */ jsx10(
1868
+ "div",
1869
+ {
1870
+ style: {
1871
+ position: "absolute",
1872
+ left: `${hoverPosition * 100}%`,
1873
+ top: "50%",
1874
+ transform: "translate(-50%, -50%)",
1875
+ width: "2px",
1876
+ height: "16px",
1877
+ background: "rgba(255,255,255,0.6)",
1878
+ pointerEvents: "none",
1879
+ zIndex: 1
1880
+ }
1881
+ }
1882
+ )
1883
+ ]
1884
+ }
1885
+ );
1886
+ }
1887
+
1888
+ // src/DocControlsOverlay.tsx
1889
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1890
+ function DocControlsOverlay({
1891
+ state,
1892
+ actions,
1893
+ blockMarkers,
1894
+ expandedBlocks,
1895
+ getBlockTitle
1896
+ }) {
1897
+ return /* @__PURE__ */ jsxs7(
1898
+ "div",
1899
+ {
1900
+ className: "doc-player__controls",
1901
+ style: {
1902
+ position: "absolute",
1903
+ bottom: 0,
1904
+ left: 0,
1905
+ right: 0,
1906
+ padding: "12px 16px",
1907
+ background: "linear-gradient(transparent, rgba(0,0,0,0.8))",
1908
+ display: "flex",
1909
+ alignItems: "center",
1910
+ gap: "8px",
1911
+ zIndex: 100
1912
+ },
1913
+ children: [
1914
+ /* @__PURE__ */ jsx11(
1915
+ "button",
1916
+ {
1917
+ onClick: actions.restart,
1918
+ style: {
1919
+ background: "none",
1920
+ border: "none",
1921
+ color: "rgba(255,255,255,0.7)",
1922
+ cursor: "pointer",
1923
+ padding: "8px",
1924
+ fontSize: "14px",
1925
+ display: "flex",
1926
+ alignItems: "center"
1927
+ },
1928
+ title: "Restart",
1929
+ "aria-label": "Restart from beginning",
1930
+ children: /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
1931
+ }
1932
+ ),
1933
+ /* @__PURE__ */ jsx11(
1934
+ "button",
1935
+ {
1936
+ onClick: actions.toggle,
1937
+ style: {
1938
+ background: "rgba(255,255,255,0.2)",
1939
+ border: "none",
1940
+ borderRadius: "50%",
1941
+ color: "white",
1942
+ cursor: "pointer",
1943
+ padding: "10px",
1944
+ fontSize: "16px",
1945
+ display: "flex",
1946
+ alignItems: "center",
1947
+ justifyContent: "center",
1948
+ width: "40px",
1949
+ height: "40px"
1950
+ },
1951
+ "aria-label": state.isPlaying ? "Pause" : "Play",
1952
+ children: state.isPlaying ? /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx11("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx11("path", { d: "M8 5v14l11-7z" }) })
1953
+ }
1954
+ ),
1955
+ /* @__PURE__ */ jsxs7("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
1956
+ formatTime(state.currentTime),
1957
+ " / ",
1958
+ formatTime(state.totalDuration)
1959
+ ] }),
1960
+ /* @__PURE__ */ jsx11(
1961
+ DocProgressBar,
1962
+ {
1963
+ state,
1964
+ actions,
1965
+ blockMarkers,
1966
+ expandedBlocks,
1967
+ getBlockTitle
1968
+ }
1969
+ ),
1970
+ /* @__PURE__ */ jsxs7("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
1971
+ state.currentBlockIndex + 1,
1972
+ "/",
1973
+ state.totalBlocks
1974
+ ] }),
1975
+ state.hasCaptions && /* @__PURE__ */ jsxs7(
1976
+ "button",
1977
+ {
1978
+ onClick: () => actions.cycleCaptionMode(),
1979
+ style: {
1980
+ background: state.captionMode !== "off" ? "rgba(255,255,255,0.2)" : "none",
1981
+ border: "none",
1982
+ color: state.captionMode !== "off" ? "white" : "rgba(255,255,255,0.5)",
1983
+ cursor: "pointer",
1984
+ padding: "8px",
1985
+ fontSize: "12px",
1986
+ display: "flex",
1987
+ alignItems: "center",
1988
+ gap: "4px",
1989
+ borderRadius: "4px"
1990
+ },
1991
+ title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
1992
+ "aria-label": "Cycle caption style",
1993
+ children: [
1994
+ /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) }),
1995
+ state.captionMode !== "off" && /* @__PURE__ */ jsx11("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
1996
+ ]
1997
+ }
1998
+ ),
1999
+ actions.toggleFullscreen && /* @__PURE__ */ jsx11(
2000
+ "button",
2001
+ {
2002
+ onClick: actions.toggleFullscreen,
2003
+ style: {
2004
+ background: state.isFullscreen ? "rgba(255,255,255,0.2)" : "none",
2005
+ border: "none",
2006
+ color: state.isFullscreen ? "white" : "rgba(255,255,255,0.5)",
2007
+ cursor: "pointer",
2008
+ padding: "8px",
2009
+ display: "flex",
2010
+ alignItems: "center",
2011
+ borderRadius: "4px"
2012
+ },
2013
+ title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
2014
+ "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
2015
+ children: state.isFullscreen ? /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
2016
+ }
2017
+ )
2018
+ ]
2019
+ }
2020
+ );
2021
+ }
2022
+
2023
+ // src/DocControlsSlideshow.tsx
2024
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2025
+ function DocControlsSlideshow({ state, slideNav }) {
2026
+ const { currentBlockIndex, totalBlocks } = state;
2027
+ const isFirst = currentBlockIndex <= 0;
2028
+ const isLast = currentBlockIndex >= totalBlocks - 1;
2029
+ return /* @__PURE__ */ jsxs8(
2030
+ "div",
2031
+ {
2032
+ className: "doc-controls-slideshow",
2033
+ "data-testid": "slideshow-controls",
2034
+ style: {
2035
+ position: "absolute",
2036
+ bottom: "16px",
2037
+ right: "16px",
2038
+ display: "flex",
2039
+ alignItems: "center",
2040
+ gap: "2px",
2041
+ background: "rgba(0, 0, 0, 0.65)",
2042
+ borderRadius: "8px",
2043
+ padding: "4px 6px",
2044
+ zIndex: 100,
2045
+ userSelect: "none",
2046
+ backdropFilter: "blur(8px)",
2047
+ WebkitBackdropFilter: "blur(8px)"
2048
+ },
2049
+ children: [
2050
+ /* @__PURE__ */ jsx12(
2051
+ "button",
2052
+ {
2053
+ onClick: (e) => {
2054
+ e.stopPropagation();
2055
+ slideNav.prevSlide();
2056
+ },
2057
+ disabled: isFirst,
2058
+ "data-testid": "slide-prev",
2059
+ "aria-label": "Previous slide",
2060
+ title: "Previous slide",
2061
+ style: {
2062
+ background: "none",
2063
+ border: "none",
2064
+ color: isFirst ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.9)",
2065
+ cursor: isFirst ? "default" : "pointer",
2066
+ padding: "6px 8px",
2067
+ display: "flex",
2068
+ alignItems: "center",
2069
+ justifyContent: "center",
2070
+ borderRadius: "4px",
2071
+ transition: "background 0.15s"
2072
+ },
2073
+ onMouseEnter: (e) => {
2074
+ if (!isFirst) e.currentTarget.style.background = "rgba(255,255,255,0.1)";
2075
+ },
2076
+ onMouseLeave: (e) => {
2077
+ e.currentTarget.style.background = "none";
2078
+ },
2079
+ children: /* @__PURE__ */ jsx12("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx12("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
2080
+ }
2081
+ ),
2082
+ /* @__PURE__ */ jsx12(
2083
+ "span",
2084
+ {
2085
+ "data-testid": "slide-counter",
2086
+ style: {
2087
+ color: "rgba(255,255,255,0.9)",
2088
+ fontSize: "13px",
2089
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2090
+ fontVariantNumeric: "tabular-nums",
2091
+ minWidth: "48px",
2092
+ textAlign: "center",
2093
+ padding: "0 4px",
2094
+ letterSpacing: "0.02em"
2095
+ },
2096
+ children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
2097
+ }
2098
+ ),
2099
+ /* @__PURE__ */ jsx12(
2100
+ "button",
2101
+ {
2102
+ onClick: (e) => {
2103
+ e.stopPropagation();
2104
+ slideNav.nextSlide();
2105
+ },
2106
+ disabled: isLast,
2107
+ "data-testid": "slide-next",
2108
+ "aria-label": "Next slide",
2109
+ title: "Next slide",
2110
+ style: {
2111
+ background: "none",
2112
+ border: "none",
2113
+ color: isLast ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.9)",
2114
+ cursor: isLast ? "default" : "pointer",
2115
+ padding: "6px 8px",
2116
+ display: "flex",
2117
+ alignItems: "center",
2118
+ justifyContent: "center",
2119
+ borderRadius: "4px",
2120
+ transition: "background 0.15s"
2121
+ },
2122
+ onMouseEnter: (e) => {
2123
+ if (!isLast) e.currentTarget.style.background = "rgba(255,255,255,0.1)";
2124
+ },
2125
+ onMouseLeave: (e) => {
2126
+ e.currentTarget.style.background = "none";
2127
+ },
2128
+ children: /* @__PURE__ */ jsx12("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx12("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
2129
+ }
2130
+ )
2131
+ ]
2132
+ }
2133
+ );
2134
+ }
2135
+
2136
+ // src/LinearDocView.tsx
2137
+ import { useMemo as useMemo6 } from "react";
2138
+ import {
2139
+ applySurface,
2140
+ resolveFontFamily as resolveFontFamily2
2141
+ } from "@bendyline/squisq/schemas";
2142
+ import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2143
+ import { getLayers, hasTemplate, DEFAULT_THEME } from "@bendyline/squisq/doc";
2144
+ import { extractPlainText } from "@bendyline/squisq/markdown";
2145
+
2146
+ // src/MarkdownRenderer.tsx
2147
+ import { Fragment } from "react";
2148
+
2149
+ // src/InlineVideoPlayer.tsx
2150
+ import { jsx as jsx13 } from "react/jsx-runtime";
2151
+ function InlineVideoPlayer({
2152
+ src,
2153
+ basePath = "",
2154
+ width,
2155
+ height,
2156
+ poster,
2157
+ controls = true,
2158
+ preload = "metadata",
2159
+ className
2160
+ }) {
2161
+ const resolvedSrc = useMediaUrl(src, basePath);
2162
+ const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2163
+ const posterUrl = poster ? resolvedPoster : void 0;
2164
+ if (!resolvedSrc) return null;
2165
+ return /* @__PURE__ */ jsx13("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx13(
2166
+ "video",
2167
+ {
2168
+ src: resolvedSrc,
2169
+ poster: posterUrl,
2170
+ controls,
2171
+ preload,
2172
+ width,
2173
+ height,
2174
+ playsInline: true
2175
+ }
2176
+ ) });
2177
+ }
2178
+
2179
+ // src/InlineAudioPlayer.tsx
2180
+ import { jsx as jsx14 } from "react/jsx-runtime";
2181
+ function InlineAudioPlayer({
2182
+ src,
2183
+ basePath = "",
2184
+ controls = true,
2185
+ preload = "metadata",
2186
+ className
2187
+ }) {
2188
+ const resolvedSrc = useMediaUrl(src, basePath);
2189
+ if (!resolvedSrc) return null;
2190
+ return /* @__PURE__ */ jsx14("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx14("audio", { src: resolvedSrc, controls, preload }) });
2191
+ }
2192
+
2193
+ // src/MarkdownRenderer.tsx
2194
+ import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2195
+ function renderInline(nodes, keyPrefix = "") {
2196
+ return nodes.map((node, i) => {
2197
+ const key = `${keyPrefix}i${i}`;
2198
+ switch (node.type) {
2199
+ case "text": {
2200
+ if (!node.value.includes("\n")) {
2201
+ return /* @__PURE__ */ jsx15(Fragment, { children: node.value }, key);
2202
+ }
2203
+ const parts = node.value.split("\n");
2204
+ return /* @__PURE__ */ jsx15(Fragment, { children: parts.map((part, j) => /* @__PURE__ */ jsxs9(Fragment, { children: [
2205
+ j > 0 && /* @__PURE__ */ jsx15("br", {}),
2206
+ part
2207
+ ] }, j)) }, key);
2208
+ }
2209
+ case "emphasis":
2210
+ return /* @__PURE__ */ jsx15("em", { className: "squisq-md-em", children: renderInline(node.children, key) }, key);
2211
+ case "strong":
2212
+ return /* @__PURE__ */ jsx15("strong", { className: "squisq-md-strong", children: renderInline(node.children, key) }, key);
2213
+ case "delete":
2214
+ return /* @__PURE__ */ jsx15("del", { className: "squisq-md-del", children: renderInline(node.children, key) }, key);
2215
+ case "inlineCode":
2216
+ return /* @__PURE__ */ jsx15("code", { className: "squisq-md-inline-code", children: node.value }, key);
2217
+ case "link":
2218
+ return /* @__PURE__ */ jsx15(
2219
+ "a",
2220
+ {
2221
+ className: "squisq-md-link",
2222
+ href: node.url,
2223
+ title: node.title ?? void 0,
2224
+ target: "_blank",
2225
+ rel: "noopener noreferrer",
2226
+ children: renderInline(node.children, key)
2227
+ },
2228
+ key
2229
+ );
2230
+ case "image":
2231
+ return /* @__PURE__ */ jsx15(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2232
+ case "break":
2233
+ return /* @__PURE__ */ jsx15("br", {}, key);
2234
+ case "inlineMath":
2235
+ return /* @__PURE__ */ jsx15("code", { className: "squisq-md-inline-math", children: node.value }, key);
2236
+ case "htmlInline":
2237
+ if (!containsMediaTag(node.htmlChildren)) {
2238
+ return /* @__PURE__ */ jsx15(
2239
+ "span",
2240
+ {
2241
+ className: "squisq-md-html-inline",
2242
+ dangerouslySetInnerHTML: { __html: node.rawHtml }
2243
+ },
2244
+ key
2245
+ );
2246
+ }
2247
+ return /* @__PURE__ */ jsx15("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(node.htmlChildren, `${key}h`) }, key);
2248
+ case "footnoteReference":
2249
+ return /* @__PURE__ */ jsx15("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs9("a", { href: `#fn-${node.identifier}`, children: [
2250
+ "[",
2251
+ node.label ?? node.identifier,
2252
+ "]"
2253
+ ] }) }, key);
2254
+ case "linkReference":
2255
+ return /* @__PURE__ */ jsx15("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key) }, key);
2256
+ case "imageReference":
2257
+ return /* @__PURE__ */ jsxs9("span", { className: "squisq-md-image-ref", children: [
2258
+ "[",
2259
+ node.alt ?? node.identifier,
2260
+ "]"
2261
+ ] }, key);
2262
+ case "textDirective":
2263
+ return /* @__PURE__ */ jsx15("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key) }, key);
2264
+ case "mention":
2265
+ return /* @__PURE__ */ jsxs9(
2266
+ "span",
2267
+ {
2268
+ className: "squisq-md-mention mention",
2269
+ "data-mention": "true",
2270
+ "data-kind": node.targetKind,
2271
+ "data-id": node.targetId,
2272
+ "data-label": node.displayName,
2273
+ children: [
2274
+ "@",
2275
+ node.displayName
2276
+ ]
2277
+ },
2278
+ key
2279
+ );
2280
+ default:
2281
+ return null;
2282
+ }
2283
+ });
2284
+ }
2285
+ function renderBlock(node, key) {
2286
+ switch (node.type) {
2287
+ case "paragraph":
2288
+ return /* @__PURE__ */ jsx15("p", { className: "squisq-md-p", children: renderInline(node.children, key) }, key);
2289
+ case "heading": {
2290
+ const Tag = `h${node.depth}`;
2291
+ return /* @__PURE__ */ jsx15(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key) }, key);
2292
+ }
2293
+ case "blockquote":
2294
+ return /* @__PURE__ */ jsx15("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key) }, key);
2295
+ case "list":
2296
+ if (node.ordered) {
2297
+ return /* @__PURE__ */ jsx15("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`)) }, key);
2298
+ }
2299
+ return /* @__PURE__ */ jsx15("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`)) }, key);
2300
+ case "code":
2301
+ return /* @__PURE__ */ jsx15("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx15("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
2302
+ case "thematicBreak":
2303
+ return /* @__PURE__ */ jsx15("hr", { className: "squisq-md-hr" }, key);
2304
+ case "table":
2305
+ return renderTable(node.children, node.align, key);
2306
+ case "htmlBlock":
2307
+ if (!containsMediaTag(node.htmlChildren)) {
2308
+ return /* @__PURE__ */ jsx15(
2309
+ "div",
2310
+ {
2311
+ className: "squisq-md-html-block",
2312
+ dangerouslySetInnerHTML: { __html: node.rawHtml }
2313
+ },
2314
+ key
2315
+ );
2316
+ }
2317
+ return /* @__PURE__ */ jsx15("div", { className: "squisq-md-html-block", children: renderHtmlNodes(node.htmlChildren, `${key}h`) }, key);
2318
+ case "math":
2319
+ return /* @__PURE__ */ jsx15("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx15("code", { children: node.value }) }, key);
2320
+ case "definition":
2321
+ return null;
2322
+ case "footnoteDefinition":
2323
+ return /* @__PURE__ */ jsxs9("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
2324
+ /* @__PURE__ */ jsx15("sup", { children: node.label ?? node.identifier }),
2325
+ renderBlocks(node.children, key)
2326
+ ] }, key);
2327
+ case "containerDirective":
2328
+ return /* @__PURE__ */ jsxs9(
2329
+ "div",
2330
+ {
2331
+ className: `squisq-md-directive squisq-md-directive-${node.name}`,
2332
+ "data-directive": node.name,
2333
+ children: [
2334
+ node.label && /* @__PURE__ */ jsx15("div", { className: "squisq-md-directive-label", children: node.label }),
2335
+ renderBlocks(node.children, key)
2336
+ ]
2337
+ },
2338
+ key
2339
+ );
2340
+ case "leafDirective":
2341
+ return /* @__PURE__ */ jsx15(
2342
+ "div",
2343
+ {
2344
+ className: `squisq-md-directive squisq-md-directive-${node.name}`,
2345
+ "data-directive": node.name,
2346
+ children: renderInline(node.children, key)
2347
+ },
2348
+ key
2349
+ );
2350
+ case "definitionList":
2351
+ return /* @__PURE__ */ jsx15("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2352
+ if (child.type === "definitionTerm") {
2353
+ return /* @__PURE__ */ jsx15("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`) }, `${key}dt${i}`);
2354
+ }
2355
+ return /* @__PURE__ */ jsx15("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`) }, `${key}dd${i}`);
2356
+ }) }, key);
2357
+ default:
2358
+ return null;
2359
+ }
2360
+ }
2361
+ function renderListItem(item, key) {
2362
+ const isTask = item.checked !== null && item.checked !== void 0;
2363
+ return /* @__PURE__ */ jsxs9("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
2364
+ isTask && /* @__PURE__ */ jsx15("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
2365
+ renderBlocks(item.children, key)
2366
+ ] }, key);
2367
+ }
2368
+ function renderTable(rows, align, key) {
2369
+ const [headerRow, ...bodyRows] = rows;
2370
+ return /* @__PURE__ */ jsxs9("table", { className: "squisq-md-table", children: [
2371
+ headerRow && /* @__PURE__ */ jsx15("thead", { children: /* @__PURE__ */ jsx15("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx15(
2372
+ "th",
2373
+ {
2374
+ className: "squisq-md-th",
2375
+ style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2376
+ children: renderInline(cell.children, `${key}th${ci}`)
2377
+ },
2378
+ `${key}th${ci}`
2379
+ )) }) }),
2380
+ bodyRows.length > 0 && /* @__PURE__ */ jsx15("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx15("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx15(
2381
+ "td",
2382
+ {
2383
+ className: "squisq-md-td",
2384
+ style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2385
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`)
2386
+ },
2387
+ `${key}td${ri}-${ci}`
2388
+ )) }, `${key}tr${ri}`)) })
2389
+ ] }, key);
2390
+ }
2391
+ function renderBlocks(nodes, keyPrefix = "") {
2392
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`));
2393
+ }
2394
+ function MdImage({ src, alt, title }) {
2395
+ const resolved = useMediaUrl(src, ".");
2396
+ return /* @__PURE__ */ jsx15("img", { className: "squisq-md-image", src: resolved, alt, title });
2397
+ }
2398
+ function containsMediaTag(nodes) {
2399
+ for (const node of nodes) {
2400
+ if (node.type !== "htmlElement") continue;
2401
+ if (node.tagName === "video" || node.tagName === "audio") return true;
2402
+ if (containsMediaTag(node.children)) return true;
2403
+ }
2404
+ return false;
2405
+ }
2406
+ var PASSTHROUGH_ATTRS = {
2407
+ // common
2408
+ class: "className",
2409
+ id: "id",
2410
+ title: "title",
2411
+ style: "style",
2412
+ // media-adjacent (used when video/audio appear inside other wrappers)
2413
+ width: "width",
2414
+ height: "height",
2415
+ // anchor
2416
+ href: "href",
2417
+ target: "target",
2418
+ rel: "rel"
2419
+ };
2420
+ function reactPropsFromAttrs(attrs) {
2421
+ const out = {};
2422
+ for (const [name, value] of Object.entries(attrs)) {
2423
+ const propName = PASSTHROUGH_ATTRS[name];
2424
+ if (!propName) continue;
2425
+ if (propName === "style") {
2426
+ out["data-style"] = value;
2427
+ continue;
2428
+ }
2429
+ out[propName] = value;
2430
+ }
2431
+ return out;
2432
+ }
2433
+ function renderHtmlElement(el, key) {
2434
+ if (el.tagName === "video") {
2435
+ return /* @__PURE__ */ jsx15(
2436
+ InlineVideoPlayer,
2437
+ {
2438
+ src: el.attributes.src ?? "",
2439
+ width: el.attributes.width,
2440
+ height: el.attributes.height,
2441
+ poster: el.attributes.poster,
2442
+ controls: "controls" in el.attributes,
2443
+ preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
2444
+ },
2445
+ key
2446
+ );
2447
+ }
2448
+ if (el.tagName === "audio") {
2449
+ return /* @__PURE__ */ jsx15(
2450
+ InlineAudioPlayer,
2451
+ {
2452
+ src: el.attributes.src ?? "",
2453
+ controls: "controls" in el.attributes,
2454
+ preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
2455
+ },
2456
+ key
2457
+ );
2458
+ }
2459
+ const Tag = el.tagName;
2460
+ const props = reactPropsFromAttrs(el.attributes);
2461
+ if (el.selfClosing) {
2462
+ return /* @__PURE__ */ jsx15(Tag, { ...props }, key);
2463
+ }
2464
+ return /* @__PURE__ */ jsx15(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
2465
+ }
2466
+ function renderHtmlNodes(nodes, keyPrefix) {
2467
+ return nodes.map((node, i) => {
2468
+ const key = `${keyPrefix}${i}`;
2469
+ switch (node.type) {
2470
+ case "htmlElement":
2471
+ return renderHtmlElement(node, key);
2472
+ case "htmlText":
2473
+ return /* @__PURE__ */ jsx15(Fragment, { children: node.value }, key);
2474
+ case "htmlComment":
2475
+ return null;
2476
+ default:
2477
+ return null;
2478
+ }
2479
+ });
2480
+ }
2481
+ function MarkdownRenderer({ nodes, className }) {
2482
+ if (!nodes || nodes.length === 0) return null;
2483
+ return /* @__PURE__ */ jsx15("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes) });
2484
+ }
2485
+
2486
+ // src/LinearDocView.tsx
2487
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
2488
+ function isAnnotatedBlock(block) {
2489
+ const annotation = block.sourceHeading?.templateAnnotation;
2490
+ if (!annotation) return false;
2491
+ return !!annotation.template && hasTemplate(annotation.template);
2492
+ }
2493
+ function countAll(blocks) {
2494
+ let count = 0;
2495
+ for (const b of blocks) {
2496
+ count++;
2497
+ if (b.children) count += countAll(b.children);
2498
+ }
2499
+ return count;
2500
+ }
2501
+ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
2502
+ const isAnnotated = isAnnotatedBlock(block);
2503
+ const visualBlock = useMemo6(() => {
2504
+ if (!isAnnotated) return null;
2505
+ const annotation = block.sourceHeading.templateAnnotation;
2506
+ const headingText = extractPlainText(block.sourceHeading);
2507
+ const bodyText = extractBodyPlainText(block.contents);
2508
+ const templateBlock = {
2509
+ id: block.id,
2510
+ template: annotation.template,
2511
+ startTime: 0,
2512
+ duration: 1,
2513
+ audioSegment: 0,
2514
+ title: headingText,
2515
+ ...getTemplateDefaults(
2516
+ annotation.template ?? "sectionHeader",
2517
+ headingText,
2518
+ bodyText,
2519
+ block.contents
2520
+ ),
2521
+ ...annotation.params,
2522
+ ...block.templateOverrides
2523
+ };
2524
+ const ctx = {
2525
+ ...renderContext,
2526
+ blockIndex
2527
+ };
2528
+ const layers = getLayers(templateBlock, ctx);
2529
+ return {
2530
+ ...block,
2531
+ layers,
2532
+ template: annotation.template
2533
+ };
2534
+ }, [block, isAnnotated, renderContext, blockIndex]);
2535
+ return /* @__PURE__ */ jsxs10(
2536
+ "div",
2537
+ {
2538
+ className: "squisq-linear-section",
2539
+ "data-block-id": block.id,
2540
+ "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
2541
+ children: [
2542
+ block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx16(MarkdownRenderer, { nodes: [block.sourceHeading] }),
2543
+ isAnnotated && visualBlock && /* @__PURE__ */ jsx16("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx16(
2544
+ "div",
2545
+ {
2546
+ className: "squisq-linear-card-svg",
2547
+ style: {
2548
+ width: "100%",
2549
+ aspectRatio: `${viewport.width} / ${viewport.height}`,
2550
+ overflow: "hidden",
2551
+ marginBottom: "1em"
2552
+ },
2553
+ children: /* @__PURE__ */ jsx16(
2554
+ BlockRenderer,
2555
+ {
2556
+ block: visualBlock,
2557
+ blockTime: 0,
2558
+ basePath,
2559
+ viewport
2560
+ }
2561
+ )
2562
+ }
2563
+ ) }),
2564
+ !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx16(MarkdownRenderer, { nodes: block.contents }),
2565
+ block.children && block.children.length > 0 && /* @__PURE__ */ jsx16("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx16(
2566
+ BlockSection,
2567
+ {
2568
+ block: child,
2569
+ basePath,
2570
+ viewport,
2571
+ renderContext,
2572
+ blockIndex: blockIndex + i + 1
2573
+ },
2574
+ child.id
2575
+ )) })
2576
+ ]
2577
+ }
2578
+ );
2579
+ }
2580
+ function extractBodyPlainText(contents) {
2581
+ if (!contents || contents.length === 0) return "";
2582
+ return contents.map((n) => extractPlainText(n)).join("\n").trim();
2583
+ }
2584
+ function extractListItems(contents) {
2585
+ if (!contents) return [];
2586
+ const items = [];
2587
+ for (const node of contents) {
2588
+ if (node.type === "list") {
2589
+ for (const item of node.children) {
2590
+ const text = extractPlainText(item).trim();
2591
+ if (text) items.push(text);
2592
+ }
2593
+ }
2594
+ }
2595
+ return items;
2596
+ }
2597
+ function extractFirstImage(contents) {
2598
+ if (!contents || contents.length === 0) return null;
2599
+ function fromHtml(nodes) {
2600
+ for (const node of nodes) {
2601
+ if (!node || typeof node !== "object") continue;
2602
+ const n = node;
2603
+ if (n.type === "htmlElement" && n.tagName === "img") {
2604
+ const attrs = n.attributes;
2605
+ if (attrs && typeof attrs.src === "string" && attrs.src) {
2606
+ return {
2607
+ src: attrs.src,
2608
+ alt: typeof attrs.alt === "string" ? attrs.alt : "",
2609
+ width: parseDim(attrs.width),
2610
+ height: parseDim(attrs.height)
2611
+ };
2612
+ }
2613
+ }
2614
+ if (Array.isArray(n.children)) {
2615
+ const found = fromHtml(n.children);
2616
+ if (found) return found;
2617
+ }
2618
+ }
2619
+ return null;
2620
+ }
2621
+ function walk(node) {
2622
+ if (!node || typeof node !== "object") return null;
2623
+ const n = node;
2624
+ if (n.type === "image" && typeof n.url === "string" && n.url) {
2625
+ return { src: n.url, alt: typeof n.alt === "string" ? n.alt : "" };
2626
+ }
2627
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
2628
+ const found = fromHtml(n.htmlChildren);
2629
+ if (found) return found;
2630
+ }
2631
+ if (Array.isArray(n.children)) {
2632
+ for (const child of n.children) {
2633
+ const found = walk(child);
2634
+ if (found) return found;
2635
+ }
2636
+ }
2637
+ return null;
2638
+ }
2639
+ for (const node of contents) {
2640
+ const found = walk(node);
2641
+ if (found) return found;
2642
+ }
2643
+ return null;
2644
+ }
2645
+ function parseDim(raw) {
2646
+ if (raw === void 0) return void 0;
2647
+ const n = parseFloat(raw);
2648
+ return Number.isFinite(n) && n > 0 ? n : void 0;
2649
+ }
2650
+ function extractTableData(contents) {
2651
+ if (!contents) return null;
2652
+ for (const node of contents) {
2653
+ if (node.type === "table") {
2654
+ const table = node;
2655
+ const [headerRow, ...bodyRows] = table.children;
2656
+ if (!headerRow) return null;
2657
+ const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
2658
+ const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
2659
+ return { headers, rows, align: table.align };
2660
+ }
2661
+ }
2662
+ return null;
2663
+ }
2664
+ function getTemplateDefaults(templateName, headingText, bodyText, contents) {
2665
+ switch (templateName) {
2666
+ case "statHighlight":
2667
+ return { stat: headingText, description: bodyText || headingText };
2668
+ case "quote":
2669
+ case "fullBleedQuote":
2670
+ case "pullQuote":
2671
+ return { quote: bodyText || headingText };
2672
+ case "factCard":
2673
+ return { fact: headingText, explanation: bodyText || headingText };
2674
+ case "comparisonBar":
2675
+ return { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 };
2676
+ case "list": {
2677
+ const items = extractListItems(contents);
2678
+ return { items: items.length > 0 ? items : ["Item 1", "Item 2", "Item 3"] };
2679
+ }
2680
+ case "definitionCard":
2681
+ return { term: headingText, definition: bodyText || headingText };
2682
+ case "dateEvent":
2683
+ return { date: headingText, description: bodyText || headingText };
2684
+ case "dataTable": {
2685
+ const tableData = extractTableData(contents);
2686
+ return tableData ?? { headers: ["Column"], rows: [["Data"]] };
2687
+ }
2688
+ case "imageWithCaption": {
2689
+ const img = extractFirstImage(contents);
2690
+ if (!img) return { caption: headingText };
2691
+ return {
2692
+ imageSrc: img.src,
2693
+ imageAlt: img.alt || headingText,
2694
+ caption: headingText
2695
+ };
2696
+ }
2697
+ case "leftFeature":
2698
+ case "rightFeature": {
2699
+ const img = extractFirstImage(contents);
2700
+ return {
2701
+ imageSrc: img?.src ?? "",
2702
+ imageAlt: img?.alt || headingText,
2703
+ imageWidth: img?.width,
2704
+ imageHeight: img?.height,
2705
+ title: headingText,
2706
+ body: bodyText
2707
+ };
2708
+ }
2709
+ default:
2710
+ return {};
2711
+ }
2712
+ }
2713
+ function LinearDocView({
2714
+ doc,
2715
+ basePath = "/",
2716
+ viewport,
2717
+ className,
2718
+ theme,
2719
+ surface,
2720
+ thinMargins = false,
2721
+ imageDisplayMode = "inline"
2722
+ }) {
2723
+ const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
2724
+ const totalBlocks = useMemo6(() => countAll(doc.blocks), [doc.blocks]);
2725
+ const autoSurface = useAutoSurface(surface === "auto");
2726
+ const resolvedSurface = surface === "auto" ? autoSurface : surface;
2727
+ const renderContext = useMemo6(() => {
2728
+ const baseTheme = theme ?? DEFAULT_THEME;
2729
+ return {
2730
+ theme: resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme,
2731
+ viewport: activeViewport,
2732
+ totalBlocks
2733
+ };
2734
+ }, [activeViewport, totalBlocks, theme, resolvedSurface]);
2735
+ const activeTheme = renderContext.theme;
2736
+ const bgColor = activeTheme.colors.background;
2737
+ const textColor = activeTheme.colors.text;
2738
+ const mutedColor = activeTheme.colors.textMuted;
2739
+ const primaryColor = activeTheme.colors.primary;
2740
+ const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
2741
+ const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
2742
+ const lineHt = activeTheme.typography.lineHeight ?? 1.7;
2743
+ return /* @__PURE__ */ jsx16(
2744
+ "div",
2745
+ {
2746
+ className: `squisq-linear ${className || ""}`,
2747
+ style: {
2748
+ width: "100%",
2749
+ // Thin-margins mode is the "embedded in someone else's container"
2750
+ // signal (chat bubble, sidebar preview). Fit to content there so
2751
+ // the host's bubble doesn't render a tall empty box when the doc
2752
+ // is short. Standalone mode keeps height:100% for full-viewport
2753
+ // scrolling.
2754
+ height: thinMargins ? "auto" : "100%",
2755
+ overflowY: thinMargins ? "visible" : "auto",
2756
+ overflowX: "hidden",
2757
+ background: bgColor
2758
+ },
2759
+ children: /* @__PURE__ */ jsxs10(
2760
+ "div",
2761
+ {
2762
+ className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
2763
+ style: {
2764
+ // Thin-margins mode drops the 720px reading column + generous
2765
+ // page padding (right for standalone docs) in favor of a tight
2766
+ // layout that hugs its container (right for chat bubbles and
2767
+ // sidebar previews).
2768
+ maxWidth: thinMargins ? "none" : "720px",
2769
+ margin: thinMargins ? "0" : "0 auto",
2770
+ padding: thinMargins ? "0" : "24px 16px",
2771
+ lineHeight: lineHt,
2772
+ fontSize: "16px",
2773
+ fontFamily: bodyFont,
2774
+ color: textColor,
2775
+ // CSS custom properties for MarkdownRenderer / nested elements
2776
+ "--squisq-linear-title-font": titleFont,
2777
+ "--squisq-linear-body-font": bodyFont,
2778
+ "--squisq-linear-text": textColor,
2779
+ "--squisq-linear-muted": mutedColor,
2780
+ "--squisq-linear-primary": primaryColor,
2781
+ "--squisq-linear-bg": bgColor
2782
+ },
2783
+ children: [
2784
+ /* @__PURE__ */ jsx16("style", { children: `
2785
+ .squisq-linear-content h1,
2786
+ .squisq-linear-content h2,
2787
+ .squisq-linear-content h3,
2788
+ .squisq-linear-content h4,
2789
+ .squisq-linear-content h5,
2790
+ .squisq-linear-content h6 {
2791
+ font-family: var(--squisq-linear-title-font);
2792
+ color: var(--squisq-linear-text);
2793
+ margin-top: 1.2em;
2794
+ margin-bottom: 0.4em;
2795
+ }
2796
+ .squisq-linear-content h1 { font-size: 2em; }
2797
+ .squisq-linear-content h2 { font-size: 1.5em; }
2798
+ .squisq-linear-content h3 { font-size: 1.25em; }
2799
+ .squisq-linear-content p {
2800
+ margin-bottom: 0.75em;
2801
+ }
2802
+ .squisq-linear-content ul,
2803
+ .squisq-linear-content ol {
2804
+ padding-left: 2em;
2805
+ margin-bottom: 0.75em;
2806
+ }
2807
+ .squisq-linear-content li {
2808
+ margin-bottom: 0.3em;
2809
+ }
2810
+ .squisq-linear-content a {
2811
+ /* Blend the theme's primary toward the body text color so
2812
+ links stay theme-flavored but always have enough contrast
2813
+ against the background. Some themes (e.g. Gezellig) pick a
2814
+ mid-tone primary that's nearly invisible on a dark page
2815
+ without this lift. */
2816
+ color: color-mix(in srgb, var(--squisq-linear-primary) 65%, var(--squisq-linear-text));
2817
+ text-decoration: underline;
2818
+ text-decoration-thickness: 1px;
2819
+ text-underline-offset: 2px;
2820
+ }
2821
+ .squisq-linear-content a:hover {
2822
+ color: var(--squisq-linear-primary);
2823
+ }
2824
+ .squisq-linear-content code {
2825
+ color: var(--squisq-linear-primary);
2826
+ font-size: 0.9em;
2827
+ padding: 0.15em 0.3em;
2828
+ border-radius: 3px;
2829
+ background: rgba(128, 128, 128, 0.15);
2830
+ }
2831
+ .squisq-linear-content pre {
2832
+ padding: 1em;
2833
+ border-radius: 6px;
2834
+ background: rgba(0, 0, 0, 0.2);
2835
+ overflow-x: auto;
2836
+ margin-bottom: 0.75em;
2837
+ }
2838
+ .squisq-linear-content pre code {
2839
+ padding: 0;
2840
+ background: none;
2841
+ }
2842
+ .squisq-linear-content blockquote {
2843
+ border-left: 3px solid var(--squisq-linear-muted);
2844
+ color: var(--squisq-linear-muted);
2845
+ padding-left: 1em;
2846
+ margin-left: 0;
2847
+ margin-bottom: 0.75em;
2848
+ }
2849
+ .squisq-linear-content hr {
2850
+ border: none;
2851
+ border-top: 1px solid var(--squisq-linear-muted);
2852
+ margin: 1.5em 0;
2853
+ }
2854
+ .squisq-linear-content img {
2855
+ max-width: 100%;
2856
+ height: auto;
2857
+ border-radius: 6px;
2858
+ margin: 0.5em 0;
2859
+ }
2860
+ .squisq-linear-content--thumbnail-images img {
2861
+ max-width: 100px;
2862
+ max-height: 100px;
2863
+ width: auto;
2864
+ height: auto;
2865
+ object-fit: contain;
2866
+ display: block;
2867
+ }
2868
+ .squisq-linear-content strong {
2869
+ font-weight: 700;
2870
+ }
2871
+ .squisq-linear-content em {
2872
+ font-style: italic;
2873
+ }
2874
+ .squisq-linear-content table {
2875
+ width: 100%;
2876
+ border-collapse: collapse;
2877
+ margin: 1em 0;
2878
+ font-size: 0.95em;
2879
+ }
2880
+ .squisq-linear-content thead th {
2881
+ background: var(--squisq-linear-primary);
2882
+ color: var(--squisq-linear-bg);
2883
+ font-family: var(--squisq-linear-title-font);
2884
+ font-weight: 600;
2885
+ padding: 10px 14px;
2886
+ text-align: left;
2887
+ }
2888
+ .squisq-linear-content tbody td {
2889
+ padding: 8px 14px;
2890
+ border-bottom: 1px solid color-mix(in srgb, var(--squisq-linear-muted) 30%, transparent);
2891
+ }
2892
+ .squisq-linear-content tbody tr:hover {
2893
+ background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
2894
+ }
2895
+ ` }),
2896
+ doc.blocks.map((block, i) => /* @__PURE__ */ jsx16(
2897
+ BlockSection,
2898
+ {
2899
+ block,
2900
+ basePath,
2901
+ viewport: activeViewport,
2902
+ renderContext,
2903
+ blockIndex: i
2904
+ },
2905
+ block.id
2906
+ ))
2907
+ ]
2908
+ }
2909
+ )
2910
+ }
2911
+ );
2912
+ }
2913
+
2914
+ // src/DocPlayer.tsx
2915
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2916
+ var SMALL_WORDS = /* @__PURE__ */ new Set([
2917
+ "a",
2918
+ "an",
2919
+ "the",
2920
+ "and",
2921
+ "but",
2922
+ "or",
2923
+ "for",
2924
+ "nor",
2925
+ "on",
2926
+ "at",
2927
+ "to",
2928
+ "in",
2929
+ "of",
2930
+ "by",
2931
+ "is"
2932
+ ]);
2933
+ function buildSegmentTitleMap(script) {
2934
+ const map = /* @__PURE__ */ new Map();
2935
+ for (const block of script.blocks) {
2936
+ if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
2937
+ const segIdx = block.audioSegment;
2938
+ if (!map.has(segIdx)) {
2939
+ map.set(segIdx, block.title);
2940
+ }
2941
+ }
2942
+ }
2943
+ for (let i = 0; i < script.audio.segments.length; i++) {
2944
+ if (!map.has(i)) {
2945
+ const name = script.audio.segments[i].name;
2946
+ if (name === "intro" || name.includes("intro")) {
2947
+ map.set(i, "Introduction");
2948
+ } else if (name === "flight-context" || name.includes("flight-context")) {
2949
+ map.set(i, "Flight Context");
2950
+ } else {
2951
+ const words = name.split("-");
2952
+ const titled = words.map(
2953
+ (w, idx) => idx === 0 || !SMALL_WORDS.has(w) ? w.charAt(0).toUpperCase() + w.slice(1) : w
2954
+ ).join(" ");
2955
+ map.set(i, titled);
2956
+ }
2957
+ }
2958
+ }
2959
+ return map;
2960
+ }
2961
+ function DocPlayer({
2962
+ script,
2963
+ basePath,
2964
+ renderMode = false,
2965
+ autoPlay = false,
2966
+ onEnded,
2967
+ onTimeUpdate,
2968
+ audioProvider: externalAudioProvider,
2969
+ showControls = true,
2970
+ showScrubber = false,
2971
+ muted = false,
2972
+ captionsEnabled: captionsEnabledProp,
2973
+ onCaptionsToggle,
2974
+ onPlaybackStateChange,
2975
+ onControlsReady,
2976
+ isFullscreen = false,
2977
+ onFullscreenToggle,
2978
+ onBlockMarkers,
2979
+ forceViewport,
2980
+ displayMode = "video",
2981
+ theme,
2982
+ surface,
2983
+ captionStyle = "standard"
2984
+ }) {
2985
+ const isSlideshowMode = displayMode === "slideshow";
2986
+ const isLinearMode = displayMode === "linear";
2987
+ const audioRef = useRef5(null);
2988
+ const containerRef = useRef5(null);
2989
+ const [tapFeedback, setTapFeedback] = useState7(null);
2990
+ const tapFeedbackTimer = useRef5();
2991
+ const { viewport, orientation } = useViewportOrientation();
2992
+ const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
2993
+ const isDebugMode = useMemo7(() => {
2994
+ if (typeof window === "undefined") return false;
2995
+ const params = new URLSearchParams(window.location.search);
2996
+ return params.get("debug") === "true";
2997
+ }, []);
2998
+ const internalAudio = useAudioSync(audioRef, script.audio, basePath);
2999
+ const audio = externalAudioProvider || internalAudio;
3000
+ const {
3001
+ currentTime,
3002
+ isPlaying,
3003
+ currentSegment,
3004
+ totalDuration,
3005
+ isEnded,
3006
+ isReady: isAudioReady,
3007
+ isAvailable,
3008
+ unavailableMessage,
3009
+ play,
3010
+ pause,
3011
+ toggle,
3012
+ seekTo,
3013
+ skipToSegment: _skipToSegment,
3014
+ restart
3015
+ } = audio;
3016
+ const currentTimeRef = useRef5(currentTime);
3017
+ currentTimeRef.current = currentTime;
3018
+ const totalDurationRef = useRef5(totalDuration);
3019
+ totalDurationRef.current = totalDuration;
3020
+ const expandedBlocksLenRef = useRef5(0);
3021
+ const handleContainerClick = useCallback5(
3022
+ (e) => {
3023
+ if (renderMode || isSlideshowMode || isLinearMode) return;
3024
+ const target = e.target;
3025
+ if (target.closest(
3026
+ "button, a, input, .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow"
3027
+ ))
3028
+ return;
3029
+ toggle();
3030
+ const nextState = isPlaying ? "play" : "pause";
3031
+ setTapFeedback(nextState);
3032
+ clearTimeout(tapFeedbackTimer.current);
3033
+ tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
3034
+ },
3035
+ [renderMode, toggle, isPlaying, isSlideshowMode, isLinearMode]
3036
+ );
3037
+ const autoSurface = useAutoSurface(surface === "auto");
3038
+ const resolvedSurface = surface === "auto" ? autoSurface : surface;
3039
+ const effectiveTheme = useMemo7(() => {
3040
+ const base = theme ?? DEFAULT_THEME2;
3041
+ return resolvedSurface ? applySurface2(base, resolvedSurface) : base;
3042
+ }, [theme, resolvedSurface]);
3043
+ const {
3044
+ currentBlock,
3045
+ currentBlockIndex,
3046
+ previousBlock,
3047
+ isEntering,
3048
+ isExiting,
3049
+ blockTime,
3050
+ blockProgress: _blockProgress,
3051
+ docProgress,
3052
+ nextBlock: _nextBlock,
3053
+ prevBlock: _prevBlock,
3054
+ blocks: expandedBlocks
3055
+ } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
3056
+ const coverBlock = useMemo7(() => {
3057
+ const startBlockConfig = script.startBlock;
3058
+ if (!startBlockConfig) return null;
3059
+ const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
3060
+ const layers = expandCoverBlock(startBlockConfig, context);
3061
+ return {
3062
+ id: "cover-block",
3063
+ startTime: -1,
3064
+ // Not part of timeline
3065
+ duration: 0,
3066
+ // Static
3067
+ audioSegment: -1,
3068
+ layers
3069
+ };
3070
+ }, [script.startBlock, activeViewport, effectiveTheme]);
3071
+ const [coverForced, setCoverForced] = useState7(false);
3072
+ const [coverGraceActive, setCoverGraceActive] = useState7(false);
3073
+ const coverGraceTimer = useRef5();
3074
+ const coverWasShowing = useRef5(false);
3075
+ const hasPlayedOnce = useRef5(false);
3076
+ const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3077
+ if (atRest) coverWasShowing.current = true;
3078
+ useEffect7(() => {
3079
+ if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
3080
+ coverWasShowing.current = false;
3081
+ hasPlayedOnce.current = true;
3082
+ setCoverGraceActive(true);
3083
+ coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3084
+ }
3085
+ }, [isPlaying, coverBlock, renderMode]);
3086
+ useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
3087
+ const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3088
+ const hasAutoPlayed = useRef5(false);
3089
+ useEffect7(() => {
3090
+ if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3091
+ hasAutoPlayed.current = true;
3092
+ play();
3093
+ }
3094
+ }, [isAudioReady, autoPlay, play]);
3095
+ useEffect7(() => {
3096
+ onTimeUpdate?.(currentTime);
3097
+ }, [currentTime, onTimeUpdate]);
3098
+ useEffect7(() => {
3099
+ if (isEnded) {
3100
+ onEnded?.();
3101
+ }
3102
+ }, [isEnded, onEnded]);
3103
+ useEffect7(() => {
3104
+ if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3105
+ const w = window;
3106
+ w.seekTo = (time) => {
3107
+ seekTo(time);
3108
+ return new Promise((resolve) => {
3109
+ requestAnimationFrame(() => {
3110
+ let blockStartTime = 0;
3111
+ for (let i = expandedBlocks.length - 1; i >= 0; i--) {
3112
+ if (time >= expandedBlocks[i].startTime) {
3113
+ blockStartTime = expandedBlocks[i].startTime;
3114
+ break;
3115
+ }
3116
+ }
3117
+ const elapsedMs = (time - blockStartTime) * 1e3;
3118
+ document.getAnimations().forEach((anim) => {
3119
+ const target = anim.effect?.target;
3120
+ if (!target) return;
3121
+ if (target.closest(".doc-player__block--active")) {
3122
+ anim.currentTime = Math.max(0, elapsedMs);
3123
+ } else if (target.closest(".doc-player__block--previous")) {
3124
+ anim.currentTime = Math.max(0, elapsedMs);
3125
+ }
3126
+ });
3127
+ const blockElapsed = time - blockStartTime;
3128
+ const videoSeekPromises = [];
3129
+ const activeBlockEl = document.querySelector(".doc-player__block--active");
3130
+ if (activeBlockEl) {
3131
+ const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
3132
+ videos.forEach((el) => {
3133
+ const video = el;
3134
+ const clipStart = parseFloat(video.dataset.clipStart || "0");
3135
+ const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3136
+ const targetTime = Math.min(clipStart + Math.max(0, blockElapsed), clipEnd);
3137
+ video.pause();
3138
+ video.currentTime = targetTime;
3139
+ videoSeekPromises.push(
3140
+ new Promise((r) => {
3141
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
3142
+ r();
3143
+ } else {
3144
+ video.addEventListener("seeked", () => r(), { once: true });
3145
+ setTimeout(r, 200);
3146
+ }
3147
+ })
3148
+ );
3149
+ });
3150
+ }
3151
+ Promise.all(videoSeekPromises).then(() => {
3152
+ requestAnimationFrame(() => resolve());
3153
+ });
3154
+ });
3155
+ });
3156
+ };
3157
+ w.getDuration = () => {
3158
+ if (totalDuration > 0) return totalDuration;
3159
+ if (expandedBlocks.length > 0) {
3160
+ const last = expandedBlocks[expandedBlocks.length - 1];
3161
+ return last.startTime + last.duration;
3162
+ }
3163
+ return 0;
3164
+ };
3165
+ w.getBlocks = () => expandedBlocks.map((s) => ({
3166
+ id: s.id,
3167
+ template: s.template ?? "raw",
3168
+ startTime: s.startTime,
3169
+ duration: s.duration
3170
+ }));
3171
+ w.getAudioSegments = () => script.audio.segments.map((seg) => ({
3172
+ src: seg.src,
3173
+ name: seg.name,
3174
+ duration: seg.duration,
3175
+ startTime: seg.startTime
3176
+ }));
3177
+ w.getCaptions = () => script.captions?.phrases?.map((p) => ({
3178
+ text: p.text,
3179
+ startTime: p.startTime,
3180
+ endTime: p.endTime
3181
+ })) || [];
3182
+ w.getChapters = () => {
3183
+ const titleMap = buildSegmentTitleMap(script);
3184
+ return script.audio.segments.map((seg, i) => ({
3185
+ title: titleMap.get(i) || seg.name,
3186
+ startTime: seg.startTime,
3187
+ duration: seg.duration
3188
+ }));
3189
+ };
3190
+ w.showCover = () => {
3191
+ setCoverForced(true);
3192
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3193
+ };
3194
+ w.hideCover = () => {
3195
+ setCoverForced(false);
3196
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3197
+ };
3198
+ w.hasCoverBlock = () => !!coverBlock;
3199
+ }
3200
+ return () => {
3201
+ if (typeof window !== "undefined") {
3202
+ const w = window;
3203
+ delete w.seekTo;
3204
+ delete w.getDuration;
3205
+ delete w.getBlocks;
3206
+ delete w.getAudioSegments;
3207
+ delete w.getCaptions;
3208
+ delete w.getChapters;
3209
+ delete w.showCover;
3210
+ delete w.hideCover;
3211
+ delete w.hasCoverBlock;
3212
+ }
3213
+ };
3214
+ }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
3215
+ const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3216
+ const [captionMode, setCaptionMode] = useState7(defaultMode);
3217
+ const captionsEnabled = captionMode !== "off";
3218
+ const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3219
+ const setCaptionsEnabled = useCallback5(
3220
+ (enabled) => {
3221
+ setCaptionMode(enabled ? captionStyle || "standard" : "off");
3222
+ onCaptionsToggle?.(enabled);
3223
+ },
3224
+ [onCaptionsToggle, captionStyle]
3225
+ );
3226
+ const cycleCaptionMode = useCallback5(() => {
3227
+ setCaptionMode((prev) => {
3228
+ const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3229
+ onCaptionsToggle?.(next !== "off");
3230
+ return next;
3231
+ });
3232
+ }, [onCaptionsToggle]);
3233
+ const hasCaptions = script.captions && script.captions.phrases.length > 0;
3234
+ const segmentTitleMap = useMemo7(() => buildSegmentTitleMap(script), [script]);
3235
+ const playbackState = useMemo7(
3236
+ () => ({
3237
+ isPlaying,
3238
+ currentTime,
3239
+ totalDuration,
3240
+ currentBlockIndex,
3241
+ totalBlocks: expandedBlocks.length,
3242
+ docProgress,
3243
+ hasCaptions: !!hasCaptions,
3244
+ captionsEnabled,
3245
+ captionMode,
3246
+ isFullscreen,
3247
+ currentSegmentIndex: currentSegment,
3248
+ currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
3249
+ currentBlock: currentBlock ?? null
3250
+ }),
3251
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
3252
+ [
3253
+ isPlaying,
3254
+ currentTime,
3255
+ totalDuration,
3256
+ currentBlockIndex,
3257
+ expandedBlocks.length,
3258
+ docProgress,
3259
+ hasCaptions,
3260
+ captionsEnabled,
3261
+ captionMode,
3262
+ isFullscreen,
3263
+ currentSegment,
3264
+ segmentTitleMap,
3265
+ currentBlock
3266
+ ]
3267
+ );
3268
+ const playbackActions = useMemo7(
3269
+ () => ({
3270
+ toggle,
3271
+ restart,
3272
+ seekTo,
3273
+ setCaptionsEnabled,
3274
+ cycleCaptionMode,
3275
+ toggleFullscreen: onFullscreenToggle
3276
+ }),
3277
+ [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]
3278
+ );
3279
+ const slideNavActions = useMemo7(
3280
+ () => ({
3281
+ nextSlide: () => {
3282
+ if (currentBlockIndex < expandedBlocks.length - 1) {
3283
+ const target = expandedBlocks[currentBlockIndex + 1];
3284
+ if (target) {
3285
+ seekTo(target.startTime);
3286
+ pause();
3287
+ }
3288
+ }
3289
+ },
3290
+ prevSlide: () => {
3291
+ if (currentBlockIndex > 0) {
3292
+ const target = expandedBlocks[currentBlockIndex - 1];
3293
+ if (target) {
3294
+ seekTo(target.startTime);
3295
+ pause();
3296
+ }
3297
+ }
3298
+ },
3299
+ goToSlide: (index) => {
3300
+ if (index >= 0 && index < expandedBlocks.length) {
3301
+ const target = expandedBlocks[index];
3302
+ if (target) {
3303
+ seekTo(target.startTime);
3304
+ pause();
3305
+ }
3306
+ }
3307
+ }
3308
+ }),
3309
+ [currentBlockIndex, expandedBlocks, seekTo, pause]
3310
+ );
3311
+ useEffect7(() => {
3312
+ onPlaybackStateChange?.(playbackState);
3313
+ }, [playbackState, onPlaybackStateChange]);
3314
+ useEffect7(() => {
3315
+ onControlsReady?.({ play, pause, ...playbackActions });
3316
+ }, [play, pause, playbackActions, onControlsReady]);
3317
+ const getBlockTitle = useCallback5((block) => {
3318
+ const docBlock = block;
3319
+ if (isTemplateBlock2(docBlock)) {
3320
+ const props = docBlock;
3321
+ if (typeof props.title === "string") return props.title;
3322
+ if (typeof props.stat === "string") return props.stat;
3323
+ if (typeof props.quote === "string") {
3324
+ const firstLine = props.quote.split("\n")[0];
3325
+ if (firstLine.length <= 30) return firstLine;
3326
+ return firstLine.slice(0, 27) + "...";
3327
+ }
3328
+ if (typeof props.date === "string") return props.date;
3329
+ if (typeof props.fact === "string") return props.fact;
3330
+ }
3331
+ if (block.layers && Array.isArray(block.layers)) {
3332
+ const textLayer = block.layers.find((l) => l.type === "text");
3333
+ if (textLayer?.content?.text) {
3334
+ const firstLine = textLayer.content.text.split("\n")[0];
3335
+ if (firstLine.length <= 30) return firstLine;
3336
+ return firstLine.slice(0, 27) + "...";
3337
+ }
3338
+ }
3339
+ return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3340
+ }, []);
3341
+ const blockMarkers = useMemo7(() => {
3342
+ if (!totalDuration || !expandedBlocks.length) return [];
3343
+ let prevSegment = -1;
3344
+ return expandedBlocks.map((block, index) => {
3345
+ const isSectionStart = block.audioSegment !== prevSegment;
3346
+ prevSegment = block.audioSegment;
3347
+ return {
3348
+ block,
3349
+ index,
3350
+ position: block.startTime / totalDuration * 100,
3351
+ title: getBlockTitle(block),
3352
+ isSectionStart
3353
+ };
3354
+ });
3355
+ }, [expandedBlocks, totalDuration, getBlockTitle]);
3356
+ useEffect7(() => {
3357
+ if (blockMarkers.length > 0) {
3358
+ onBlockMarkers?.(blockMarkers);
3359
+ }
3360
+ }, [blockMarkers, onBlockMarkers]);
3361
+ expandedBlocksLenRef.current = expandedBlocks.length;
3362
+ const handleKeyDown = useCallback5(
3363
+ (e) => {
3364
+ const activeEl = document.activeElement;
3365
+ if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
3366
+ return;
3367
+ }
3368
+ if (isLinearMode) return;
3369
+ if (isSlideshowMode) {
3370
+ switch (e.key) {
3371
+ case "ArrowRight":
3372
+ case "ArrowDown":
3373
+ case " ":
3374
+ e.preventDefault();
3375
+ slideNavActions.nextSlide();
3376
+ break;
3377
+ case "ArrowLeft":
3378
+ case "ArrowUp":
3379
+ e.preventDefault();
3380
+ slideNavActions.prevSlide();
3381
+ break;
3382
+ case "Home":
3383
+ e.preventDefault();
3384
+ slideNavActions.goToSlide(0);
3385
+ break;
3386
+ case "End":
3387
+ e.preventDefault();
3388
+ slideNavActions.goToSlide(expandedBlocksLenRef.current - 1);
3389
+ break;
3390
+ }
3391
+ } else {
3392
+ switch (e.key) {
3393
+ case " ":
3394
+ e.preventDefault();
3395
+ toggle();
3396
+ break;
3397
+ case "ArrowRight":
3398
+ seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
3399
+ break;
3400
+ case "ArrowLeft":
3401
+ seekTo(Math.max(currentTimeRef.current - 10, 0));
3402
+ break;
3403
+ }
3404
+ }
3405
+ },
3406
+ [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
3407
+ );
3408
+ useEffect7(() => {
3409
+ if (renderMode) return;
3410
+ window.addEventListener("keydown", handleKeyDown);
3411
+ return () => window.removeEventListener("keydown", handleKeyDown);
3412
+ }, [handleKeyDown, renderMode]);
3413
+ if (isLinearMode) {
3414
+ return /* @__PURE__ */ jsx17(
3415
+ "div",
3416
+ {
3417
+ ref: containerRef,
3418
+ className: "doc-player doc-player--linear",
3419
+ style: {
3420
+ position: "relative",
3421
+ width: "100%",
3422
+ height: "100%",
3423
+ overflow: "hidden"
3424
+ },
3425
+ children: /* @__PURE__ */ jsx17(
3426
+ LinearDocView,
3427
+ {
3428
+ doc: script,
3429
+ basePath,
3430
+ viewport: activeViewport,
3431
+ theme,
3432
+ surface
3433
+ }
3434
+ )
3435
+ }
3436
+ );
3437
+ }
3438
+ return /* @__PURE__ */ jsxs11(
3439
+ "div",
3440
+ {
3441
+ ref: containerRef,
3442
+ className: "doc-player",
3443
+ onClick: handleContainerClick,
3444
+ style: {
3445
+ position: "relative",
3446
+ width: "100%",
3447
+ aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
3448
+ margin: "0 auto",
3449
+ overflow: "hidden",
3450
+ cursor: renderMode ? void 0 : "pointer"
3451
+ },
3452
+ children: [
3453
+ /* @__PURE__ */ jsx17("audio", { ref: audioRef, preload: "auto", muted }),
3454
+ /* @__PURE__ */ jsxs11("div", { className: "doc-player__viewport", children: [
3455
+ showCoverBlock && coverBlock && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx17(
3456
+ BlockRenderer,
3457
+ {
3458
+ block: coverBlock,
3459
+ blockTime: 0,
3460
+ basePath,
3461
+ isEntering: false,
3462
+ viewport: activeViewport
3463
+ }
3464
+ ) }),
3465
+ !showCoverBlock && previousBlock && isExiting && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx17(
3466
+ BlockRenderer,
3467
+ {
3468
+ block: previousBlock,
3469
+ blockTime,
3470
+ basePath,
3471
+ isExiting: true,
3472
+ viewport: activeViewport
3473
+ }
3474
+ ) }),
3475
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx17(
3476
+ BlockRenderer,
3477
+ {
3478
+ block: currentBlock,
3479
+ blockTime,
3480
+ basePath,
3481
+ isEntering,
3482
+ viewport: activeViewport,
3483
+ isPlaying
3484
+ }
3485
+ ) }),
3486
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx17(
3487
+ CaptionOverlay,
3488
+ {
3489
+ captions: script.captions,
3490
+ currentTime,
3491
+ enabled: captionsEnabled && (renderMode || isPlaying || currentTime > 0),
3492
+ fontSize: 16,
3493
+ captionStyle: activeCaptionStyle,
3494
+ theme: effectiveTheme,
3495
+ viewport: activeViewport
3496
+ }
3497
+ ),
3498
+ isDebugMode && /* @__PURE__ */ jsxs11(
3499
+ "div",
3500
+ {
3501
+ className: "doc-player__debug",
3502
+ style: {
3503
+ position: "absolute",
3504
+ top: "8px",
3505
+ right: "8px",
3506
+ padding: "8px 12px",
3507
+ background: "rgba(0, 0, 0, 0.85)",
3508
+ borderRadius: "6px",
3509
+ color: "#00ff00",
3510
+ fontFamily: "monospace",
3511
+ fontSize: "11px",
3512
+ lineHeight: "1.5",
3513
+ zIndex: 200,
3514
+ maxWidth: "280px",
3515
+ pointerEvents: "none",
3516
+ textAlign: "left"
3517
+ },
3518
+ children: [
3519
+ /* @__PURE__ */ jsx17("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
3520
+ /* @__PURE__ */ jsxs11("div", { children: [
3521
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "template:" }),
3522
+ " ",
3523
+ /* @__PURE__ */ jsx17("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
3524
+ ] }),
3525
+ /* @__PURE__ */ jsxs11("div", { children: [
3526
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "block:" }),
3527
+ " ",
3528
+ currentBlockIndex + 1,
3529
+ "/",
3530
+ expandedBlocks.length,
3531
+ " ",
3532
+ /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
3533
+ "(",
3534
+ currentBlock?.id || "none",
3535
+ ")"
3536
+ ] })
3537
+ ] }),
3538
+ /* @__PURE__ */ jsxs11("div", { children: [
3539
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "time:" }),
3540
+ " ",
3541
+ currentTime.toFixed(2),
3542
+ "s /",
3543
+ " ",
3544
+ totalDuration.toFixed(1),
3545
+ "s",
3546
+ " ",
3547
+ /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
3548
+ "(progress: ",
3549
+ (docProgress * 100).toFixed(1),
3550
+ "%, scriptDur:",
3551
+ " ",
3552
+ script.duration.toFixed(1),
3553
+ ")"
3554
+ ] })
3555
+ ] }),
3556
+ /* @__PURE__ */ jsxs11("div", { children: [
3557
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "blockTime:" }),
3558
+ " ",
3559
+ blockTime.toFixed(2),
3560
+ "s /",
3561
+ " ",
3562
+ (currentBlock?.duration || 0).toFixed(1),
3563
+ "s"
3564
+ ] }),
3565
+ /* @__PURE__ */ jsxs11("div", { children: [
3566
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "segment:" }),
3567
+ " ",
3568
+ currentSegment,
3569
+ "/",
3570
+ script.audio.segments.length - 1,
3571
+ " ",
3572
+ /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
3573
+ "(",
3574
+ script.audio.segments[currentSegment]?.name || "none",
3575
+ ")"
3576
+ ] })
3577
+ ] }),
3578
+ /* @__PURE__ */ jsxs11("div", { children: [
3579
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "viewport:" }),
3580
+ " ",
3581
+ activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
3582
+ " ",
3583
+ /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
3584
+ "(",
3585
+ orientation,
3586
+ ")"
3587
+ ] })
3588
+ ] }),
3589
+ /* @__PURE__ */ jsxs11("div", { children: [
3590
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "playing:" }),
3591
+ " ",
3592
+ /* @__PURE__ */ jsx17("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
3593
+ showCoverBlock && /* @__PURE__ */ jsx17("span", { style: { color: "#60a5fa" }, children: " (cover)" })
3594
+ ] }),
3595
+ hasCaptions && (() => {
3596
+ const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
3597
+ const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
3598
+ return /* @__PURE__ */ jsxs11(Fragment2, { children: [
3599
+ /* @__PURE__ */ jsxs11("div", { children: [
3600
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "captions:" }),
3601
+ " ",
3602
+ script.captions?.phrases.length || 0,
3603
+ " phrases",
3604
+ " ",
3605
+ /* @__PURE__ */ jsxs11("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
3606
+ "(",
3607
+ captionsEnabled ? "on" : "off",
3608
+ ")"
3609
+ ] })
3610
+ ] }),
3611
+ /* @__PURE__ */ jsxs11("div", { children: [
3612
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.enabled:" }),
3613
+ " ",
3614
+ /* @__PURE__ */ jsx17("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
3615
+ " ",
3616
+ /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
3617
+ "(playing=",
3618
+ String(isPlaying),
3619
+ " t>0=",
3620
+ String(currentTime > 0),
3621
+ ")"
3622
+ ] })
3623
+ ] }),
3624
+ /* @__PURE__ */ jsxs11("div", { children: [
3625
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.phrase:" }),
3626
+ " ",
3627
+ /* @__PURE__ */ jsx17("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
3628
+ ] }),
3629
+ debugPhrase && /* @__PURE__ */ jsxs11("div", { children: [
3630
+ /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.range:" }),
3631
+ " ",
3632
+ /* @__PURE__ */ jsxs11("span", { style: { color: "#60a5fa" }, children: [
3633
+ debugPhrase.startTime.toFixed(2),
3634
+ "-",
3635
+ debugPhrase.endTime.toFixed(2)
3636
+ ] })
3637
+ ] })
3638
+ ] });
3639
+ })()
3640
+ ]
3641
+ }
3642
+ )
3643
+ ] }),
3644
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs11(
3645
+ "div",
3646
+ {
3647
+ className: "doc-player__unavailable",
3648
+ style: {
3649
+ position: "absolute",
3650
+ top: 0,
3651
+ left: 0,
3652
+ right: 0,
3653
+ bottom: 0,
3654
+ display: "flex",
3655
+ alignItems: "center",
3656
+ justifyContent: "center",
3657
+ flexDirection: "column",
3658
+ gap: "16px",
3659
+ background: "rgba(0, 0, 0, 0.7)",
3660
+ color: "rgba(255, 255, 255, 0.9)",
3661
+ fontSize: "14px",
3662
+ zIndex: 50
3663
+ },
3664
+ children: [
3665
+ /* @__PURE__ */ jsx17("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
3666
+ /* @__PURE__ */ jsx17("span", { children: unavailableMessage })
3667
+ ]
3668
+ }
3669
+ ),
3670
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx17(
3671
+ DocControlsOverlay,
3672
+ {
3673
+ state: playbackState,
3674
+ actions: playbackActions,
3675
+ blockMarkers,
3676
+ expandedBlocks,
3677
+ getBlockTitle
3678
+ }
3679
+ ),
3680
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx17(
3681
+ "div",
3682
+ {
3683
+ className: "doc-player__scrubber",
3684
+ style: {
3685
+ position: "absolute",
3686
+ bottom: 0,
3687
+ left: 0,
3688
+ right: 0,
3689
+ padding: "12px 16px 8px",
3690
+ background: "linear-gradient(transparent, rgba(0,0,0,0.6))",
3691
+ display: "flex",
3692
+ alignItems: "center",
3693
+ zIndex: 100
3694
+ },
3695
+ children: /* @__PURE__ */ jsx17(
3696
+ DocProgressBar,
3697
+ {
3698
+ state: playbackState,
3699
+ actions: playbackActions,
3700
+ blockMarkers,
3701
+ expandedBlocks,
3702
+ getBlockTitle
3703
+ }
3704
+ )
3705
+ }
3706
+ ),
3707
+ !renderMode && isSlideshowMode && /* @__PURE__ */ jsx17(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
3708
+ !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx17("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx17("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx17("path", { d: "M8 5v14l11-7z" }) }) }, Date.now())
3709
+ ]
3710
+ }
3711
+ );
3712
+ }
3713
+
3714
+ // src/DocControlsBottom.tsx
3715
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
3716
+ function DocControlsBottom({
3717
+ state,
3718
+ actions,
3719
+ blockMarkers,
3720
+ expandedBlocks,
3721
+ getBlockTitle
3722
+ }) {
3723
+ return /* @__PURE__ */ jsxs12("div", { className: "doc-controls-bottom", children: [
3724
+ /* @__PURE__ */ jsx18(
3725
+ "button",
3726
+ {
3727
+ className: "bottom-ctrl-btn",
3728
+ onClick: actions.restart,
3729
+ title: "Restart",
3730
+ "aria-label": "Restart from beginning",
3731
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx18("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
3732
+ }
3733
+ ),
3734
+ /* @__PURE__ */ jsx18(
3735
+ "button",
3736
+ {
3737
+ className: "bottom-ctrl-btn bottom-play-btn",
3738
+ onClick: actions.toggle,
3739
+ "aria-label": state.isPlaying ? "Pause" : "Play",
3740
+ children: state.isPlaying ? /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx18("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx18("path", { d: "M8 5v14l11-7z" }) })
3741
+ }
3742
+ ),
3743
+ /* @__PURE__ */ jsxs12("span", { className: "bottom-time", children: [
3744
+ formatTime(state.currentTime),
3745
+ " / ",
3746
+ formatTime(state.totalDuration)
3747
+ ] }),
3748
+ /* @__PURE__ */ jsx18(
3749
+ DocProgressBar,
3750
+ {
3751
+ state,
3752
+ actions,
3753
+ blockMarkers,
3754
+ expandedBlocks,
3755
+ getBlockTitle
3756
+ }
3757
+ ),
3758
+ /* @__PURE__ */ jsxs12("span", { className: "bottom-segment", children: [
3759
+ state.currentBlockIndex + 1,
3760
+ "/",
3761
+ state.totalBlocks
3762
+ ] }),
3763
+ state.hasCaptions && /* @__PURE__ */ jsx18(
3764
+ "button",
3765
+ {
3766
+ className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
3767
+ onClick: () => actions.cycleCaptionMode(),
3768
+ title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
3769
+ "aria-label": "Cycle caption style",
3770
+ children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx18("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
3771
+ }
3772
+ )
3773
+ ] });
3774
+ }
3775
+
3776
+ // src/DocControlsSidebar.tsx
3777
+ import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
3778
+ function DocControlsSidebar({ state, actions }) {
3779
+ return /* @__PURE__ */ jsxs13("div", { className: "doc-controls-sidebar", children: [
3780
+ /* @__PURE__ */ jsx19(
3781
+ "button",
3782
+ {
3783
+ className: "sidebar-ctrl-btn",
3784
+ onClick: actions.restart,
3785
+ title: "Restart",
3786
+ "aria-label": "Restart from beginning",
3787
+ children: /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
3788
+ }
3789
+ ),
3790
+ /* @__PURE__ */ jsx19(
3791
+ "button",
3792
+ {
3793
+ className: "sidebar-ctrl-btn sidebar-play-btn",
3794
+ onClick: actions.toggle,
3795
+ "aria-label": state.isPlaying ? "Pause" : "Play",
3796
+ children: state.isPlaying ? /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx19("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx19("path", { d: "M8 5v14l11-7z" }) })
3797
+ }
3798
+ ),
3799
+ /* @__PURE__ */ jsxs13("div", { className: "sidebar-time", children: [
3800
+ /* @__PURE__ */ jsx19("div", { children: formatTime(state.currentTime) }),
3801
+ /* @__PURE__ */ jsx19("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
3802
+ ] }),
3803
+ /* @__PURE__ */ jsxs13("div", { className: "sidebar-segment", children: [
3804
+ state.currentBlockIndex + 1,
3805
+ "/",
3806
+ state.totalBlocks
3807
+ ] }),
3808
+ state.hasCaptions && /* @__PURE__ */ jsx19(
3809
+ "button",
3810
+ {
3811
+ className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
3812
+ onClick: () => actions.cycleCaptionMode(),
3813
+ title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
3814
+ "aria-label": "Cycle caption style",
3815
+ children: /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
3816
+ }
3817
+ ),
3818
+ actions.toggleFullscreen && /* @__PURE__ */ jsx19(
3819
+ "button",
3820
+ {
3821
+ className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
3822
+ onClick: actions.toggleFullscreen,
3823
+ title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
3824
+ "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
3825
+ children: state.isFullscreen ? /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
3826
+ }
3827
+ )
3828
+ ] });
3829
+ }
3830
+
3831
+ // src/DocPlayerWithSidebar.tsx
3832
+ import { useRef as useRef6, useState as useState8, useCallback as useCallback6, useEffect as useEffect8 } from "react";
3833
+ import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3834
+ var DEFAULT_STATE = {
3835
+ isPlaying: false,
3836
+ currentTime: 0,
3837
+ totalDuration: 0,
3838
+ currentBlockIndex: 0,
3839
+ totalBlocks: 0,
3840
+ docProgress: 0,
3841
+ hasCaptions: false,
3842
+ captionsEnabled: false,
3843
+ captionMode: "off",
3844
+ currentSegmentIndex: 0,
3845
+ currentSegmentName: null,
3846
+ currentBlock: null
3847
+ };
3848
+ function DocPlayerWithSidebar({
3849
+ script,
3850
+ basePath,
3851
+ autoPlay = false,
3852
+ onEnded,
3853
+ onTimeUpdate,
3854
+ audioProvider,
3855
+ muted,
3856
+ captionsEnabled,
3857
+ isFullscreen,
3858
+ onFullscreenToggle,
3859
+ forceViewport,
3860
+ onPlayingChange
3861
+ }) {
3862
+ const stateRef = useRef6(DEFAULT_STATE);
3863
+ const actionsRef = useRef6(null);
3864
+ const wasPlayingRef = useRef6(false);
3865
+ const [, setTick] = useState8(0);
3866
+ const handleStateChange = useCallback6(
3867
+ (state) => {
3868
+ stateRef.current = state;
3869
+ if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
3870
+ wasPlayingRef.current = state.isPlaying;
3871
+ onPlayingChange(state.isPlaying);
3872
+ }
3873
+ },
3874
+ [onPlayingChange]
3875
+ );
3876
+ const handleControlsReady = useCallback6(
3877
+ (controls) => {
3878
+ const isFirst = !actionsRef.current;
3879
+ actionsRef.current = controls;
3880
+ if (isFirst) setTick((t) => t + 1);
3881
+ },
3882
+ []
3883
+ );
3884
+ useEffect8(() => {
3885
+ const interval = setInterval(() => {
3886
+ setTick((t) => t + 1);
3887
+ }, 250);
3888
+ return () => clearInterval(interval);
3889
+ }, []);
3890
+ return /* @__PURE__ */ jsxs14("div", { className: "doc-player-sidebar-layout", children: [
3891
+ /* @__PURE__ */ jsx20("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx20(
3892
+ DocPlayer,
3893
+ {
3894
+ script,
3895
+ basePath,
3896
+ autoPlay,
3897
+ onEnded,
3898
+ onTimeUpdate,
3899
+ audioProvider,
3900
+ muted,
3901
+ captionsEnabled,
3902
+ showControls: isFullscreen,
3903
+ showScrubber: !isFullscreen,
3904
+ onPlaybackStateChange: handleStateChange,
3905
+ onControlsReady: handleControlsReady,
3906
+ isFullscreen,
3907
+ onFullscreenToggle,
3908
+ forceViewport
3909
+ }
3910
+ ) }),
3911
+ actionsRef.current && /* @__PURE__ */ jsx20(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
3912
+ ] });
3913
+ }
3914
+
3915
+ // src/jsonView/useJsonViewTokens.ts
3916
+ import { useMemo as useMemo8 } from "react";
3917
+ import {
3918
+ applySurface as applySurface3,
3919
+ resolveFontFamily as resolveFontFamily3
3920
+ } from "@bendyline/squisq/schemas";
3921
+ import { DEFAULT_THEME as DEFAULT_THEME3 } from "@bendyline/squisq/doc";
3922
+ function useJsonViewTokens(theme, surface) {
3923
+ const auto = useAutoSurface(surface === "auto");
3924
+ const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
3925
+ return useMemo8(() => {
3926
+ const baseTheme = theme ?? DEFAULT_THEME3;
3927
+ const finalTheme = effectiveSurface ? applySurface3(baseTheme, effectiveSurface) : baseTheme;
3928
+ const titleFont = resolveFontFamily3(finalTheme.typography.titleFont, "system-ui, sans-serif");
3929
+ const bodyFont = resolveFontFamily3(finalTheme.typography.bodyFont, "system-ui, sans-serif");
3930
+ const monoFont = resolveFontFamily3(
3931
+ finalTheme.typography.monoFont,
3932
+ "ui-monospace, Consolas, monospace"
3933
+ );
3934
+ const style = {
3935
+ ["--squisq-json-bg"]: finalTheme.colors.background,
3936
+ ["--squisq-json-text"]: finalTheme.colors.text,
3937
+ ["--squisq-json-muted"]: finalTheme.colors.textMuted,
3938
+ ["--squisq-json-primary"]: finalTheme.colors.primary,
3939
+ ["--squisq-json-accent"]: finalTheme.colors.secondary,
3940
+ ["--squisq-json-border"]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,
3941
+ ["--squisq-json-title-font"]: titleFont,
3942
+ ["--squisq-json-body-font"]: bodyFont,
3943
+ ["--squisq-json-mono-font"]: monoFont,
3944
+ ["--squisq-json-radius"]: `${finalTheme.style.borderRadius ?? 8}px`
3945
+ };
3946
+ return { style, theme: finalTheme };
3947
+ }, [theme, effectiveSurface]);
3948
+ }
3949
+
3950
+ // src/jsonView/RenderNode.tsx
3951
+ import {
3952
+ chooseControl,
3953
+ resolveFlag,
3954
+ resolveRef
3955
+ } from "@bendyline/squisq/jsonForm";
3956
+
3957
+ // src/jsonView/viewers.tsx
3958
+ import { Fragment as Fragment3, useMemo as useMemo9 } from "react";
3959
+ import {
3960
+ arrayItemKind
3961
+ } from "@bendyline/squisq/jsonForm";
3962
+ import { parseMarkdown } from "@bendyline/squisq/markdown";
3963
+ import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
3964
+ function TextViewer({ value }) {
3965
+ if (value === void 0 || value === null || value === "") {
3966
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
3967
+ }
3968
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: String(value) });
3969
+ }
3970
+ function MultilineViewer({ value }) {
3971
+ if (value === void 0 || value === null || value === "") {
3972
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
3973
+ }
3974
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
3975
+ }
3976
+ function RichTextViewer({ value }) {
3977
+ const nodes = useMemo9(() => {
3978
+ if (typeof value !== "string" || value === "") return null;
3979
+ try {
3980
+ const doc = parseMarkdown(value);
3981
+ return doc.children;
3982
+ } catch {
3983
+ return null;
3984
+ }
3985
+ }, [value]);
3986
+ if (!nodes) return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
3987
+ return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx21(MarkdownRenderer, { nodes }) });
3988
+ }
3989
+ function NumberViewer({ value }) {
3990
+ if (value === void 0 || value === null) {
3991
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
3992
+ }
3993
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: String(value) });
3994
+ }
3995
+ function BooleanViewer({ value }) {
3996
+ const on = Boolean(value);
3997
+ return /* @__PURE__ */ jsx21("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
3998
+ }
3999
+ function EnumViewer({ value, schema }) {
4000
+ if (value === void 0 || value === null || value === "") {
4001
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4002
+ }
4003
+ const labels = schema.squisq?.enumLabels;
4004
+ const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4005
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: display });
4006
+ }
4007
+ function ColorViewer({ value }) {
4008
+ if (typeof value !== "string" || value === "") {
4009
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4010
+ }
4011
+ return /* @__PURE__ */ jsxs15("span", { className: "squisq-jv-color", children: [
4012
+ /* @__PURE__ */ jsx21("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4013
+ /* @__PURE__ */ jsx21("span", { className: "squisq-jv-color__hex", children: value })
4014
+ ] });
4015
+ }
4016
+ function DateViewer({ value, schema }) {
4017
+ if (typeof value !== "string" || value === "") {
4018
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4019
+ }
4020
+ const fmt = schema.format;
4021
+ let display = value;
4022
+ try {
4023
+ const date = new Date(value);
4024
+ if (!Number.isNaN(date.getTime())) {
4025
+ if (fmt === "time") {
4026
+ display = date.toLocaleTimeString();
4027
+ } else if (fmt === "date") {
4028
+ display = date.toLocaleDateString();
4029
+ } else {
4030
+ display = date.toLocaleString();
4031
+ }
4032
+ }
4033
+ } catch {
4034
+ }
4035
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: display });
4036
+ }
4037
+ function ChipBinViewer({ value, schema }) {
4038
+ if (!Array.isArray(value) || value.length === 0) {
4039
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4040
+ }
4041
+ const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4042
+ const labels = itemSchema?.squisq?.enumLabels;
4043
+ return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4044
+ const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4045
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-chip", children: label }, i);
4046
+ }) });
4047
+ }
4048
+ function CardStackViewer(props) {
4049
+ const { value, schema, rootSchema, rootData, pointer, density } = props;
4050
+ if (!Array.isArray(value) || value.length === 0) {
4051
+ return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "No items" });
4052
+ }
4053
+ const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4054
+ const itemLabel = itemSchema.squisq?.itemLabel;
4055
+ return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4056
+ const title = resolveItemTitle(itemLabel, item, i);
4057
+ return /* @__PURE__ */ jsxs15("div", { className: "squisq-jv-card", children: [
4058
+ title ? /* @__PURE__ */ jsx21("h4", { className: "squisq-jv-card__title", children: title }) : null,
4059
+ /* @__PURE__ */ jsx21(
4060
+ RenderNode,
4061
+ {
4062
+ value: item,
4063
+ schema: itemSchema,
4064
+ rootSchema,
4065
+ rootData,
4066
+ pointer: `${pointer}/${i}`,
4067
+ density,
4068
+ suppressTopGroupTitle: true
4069
+ }
4070
+ )
4071
+ ] }, i);
4072
+ }) });
4073
+ }
4074
+ function resolveItemTitle(spec, item, index) {
4075
+ if (!spec) return `Item ${index + 1}`;
4076
+ if (typeof spec === "string") return spec;
4077
+ if (item && typeof item === "object") {
4078
+ const v = item[spec.fromField];
4079
+ if (typeof v === "string" && v !== "") return v;
4080
+ if (typeof v === "number") return String(v);
4081
+ }
4082
+ return `Item ${index + 1}`;
4083
+ }
4084
+ function GroupViewer(props) {
4085
+ const { value, schema, rootSchema, rootData, pointer, density, suppressTitle } = props;
4086
+ const title = !suppressTitle ? schema.squisq?.label ?? schema.title : void 0;
4087
+ const help = schema.squisq?.help ?? schema.description;
4088
+ const obj = (value && typeof value === "object" ? value : {}) ?? {};
4089
+ const propEntries = Object.entries(schema.properties ?? {});
4090
+ return /* @__PURE__ */ jsxs15("section", { className: "squisq-jv-group", children: [
4091
+ title ? /* @__PURE__ */ jsx21("h3", { className: "squisq-jv-group__title", children: title }) : null,
4092
+ help ? /* @__PURE__ */ jsx21("p", { className: "squisq-jv-group__help", children: help }) : null,
4093
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx21(Fragment3, { children: /* @__PURE__ */ jsx21(
4094
+ RowOrSection,
4095
+ {
4096
+ label: propSchema.squisq?.label ?? propSchema.title ?? key,
4097
+ help: propSchema.squisq?.help ?? propSchema.description,
4098
+ kindHint: propSchema,
4099
+ children: /* @__PURE__ */ jsx21(
4100
+ RenderNode,
4101
+ {
4102
+ value: obj[key],
4103
+ schema: propSchema,
4104
+ rootSchema,
4105
+ rootData,
4106
+ pointer: `${pointer}/${key}`,
4107
+ density,
4108
+ suppressTopGroupTitle: true
4109
+ }
4110
+ )
4111
+ }
4112
+ ) }, key))
4113
+ ] });
4114
+ }
4115
+ function RowOrSection({
4116
+ label,
4117
+ help,
4118
+ kindHint,
4119
+ children
4120
+ }) {
4121
+ const composite = isCompositeKind(kindHint);
4122
+ if (composite) {
4123
+ return /* @__PURE__ */ jsx21(Fragment4, { children });
4124
+ }
4125
+ return /* @__PURE__ */ jsxs15("div", { className: "squisq-jv-row", children: [
4126
+ /* @__PURE__ */ jsx21("div", { className: "squisq-jv-label", title: help, children: label }),
4127
+ /* @__PURE__ */ jsx21("div", { children })
4128
+ ] });
4129
+ }
4130
+ function isCompositeKind(schema) {
4131
+ const t = schema.type;
4132
+ const arrayLike = t === "array" || Array.isArray(t) && t.includes("array");
4133
+ if (arrayLike) {
4134
+ return arrayItemKind(schema) === "object";
4135
+ }
4136
+ const objectLike = t === "object" || Array.isArray(t) && t.includes("object");
4137
+ if (objectLike) return true;
4138
+ if (schema.oneOf || schema.anyOf) return true;
4139
+ if (schema.format === "markdown" || schema.squisq?.control === "richtext") return true;
4140
+ return false;
4141
+ }
4142
+ function TabsViewer(props) {
4143
+ const { value, schema, rootSchema, rootData, pointer, density } = props;
4144
+ const branches = (schema.oneOf ?? schema.anyOf ?? []).slice();
4145
+ const matchedIndex = pickMatchingBranch(branches, value);
4146
+ const branch = branches[matchedIndex];
4147
+ if (!branch) {
4148
+ return /* @__PURE__ */ jsx21(TextViewer, { ...props });
4149
+ }
4150
+ return /* @__PURE__ */ jsxs15("div", { className: "squisq-jv-tabs", children: [
4151
+ /* @__PURE__ */ jsx21("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4152
+ /* @__PURE__ */ jsx21(
4153
+ RenderNode,
4154
+ {
4155
+ value,
4156
+ schema: branch,
4157
+ rootSchema,
4158
+ rootData,
4159
+ pointer,
4160
+ density
4161
+ }
4162
+ )
4163
+ ] });
4164
+ }
4165
+ function pickMatchingBranch(branches, value) {
4166
+ for (let i = 0; i < branches.length; i++) {
4167
+ if (matchesShape(branches[i], value)) return i;
4168
+ }
4169
+ return 0;
4170
+ }
4171
+ function matchesShape(schema, value) {
4172
+ const t = schema.type;
4173
+ const target = Array.isArray(t) ? t.find((x) => x !== "null") : t;
4174
+ if (!target) return true;
4175
+ switch (target) {
4176
+ case "string":
4177
+ return typeof value === "string";
4178
+ case "number":
4179
+ case "integer":
4180
+ return typeof value === "number";
4181
+ case "boolean":
4182
+ return typeof value === "boolean";
4183
+ case "null":
4184
+ return value === null;
4185
+ case "array":
4186
+ return Array.isArray(value);
4187
+ case "object":
4188
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4189
+ default:
4190
+ return false;
4191
+ }
4192
+ }
4193
+ var VIEWERS = {
4194
+ text: TextViewer,
4195
+ multiline: MultilineViewer,
4196
+ richtext: RichTextViewer,
4197
+ color: ColorViewer,
4198
+ date: DateViewer,
4199
+ time: DateViewer,
4200
+ datetime: DateViewer,
4201
+ slider: NumberViewer,
4202
+ stepper: NumberViewer,
4203
+ segmented: EnumViewer,
4204
+ radio: EnumViewer,
4205
+ combobox: EnumViewer,
4206
+ toggle: BooleanViewer,
4207
+ checkbox: BooleanViewer,
4208
+ card: GroupViewer,
4209
+ group: GroupViewer,
4210
+ "card-stack": CardStackViewer,
4211
+ "chip-bin": ChipBinViewer,
4212
+ tabs: TabsViewer
4213
+ };
4214
+
4215
+ // src/jsonView/RenderNode.tsx
4216
+ import { jsx as jsx22 } from "react/jsx-runtime";
4217
+ function RenderNode(props) {
4218
+ const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
4219
+ if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
4220
+ const kind = chooseControl(resolved);
4221
+ const Viewer = VIEWERS[kind];
4222
+ const viewerProps = {
4223
+ value: props.value,
4224
+ schema: resolved,
4225
+ rootSchema: props.rootSchema,
4226
+ rootData: props.rootData,
4227
+ pointer: props.pointer,
4228
+ density: props.density
4229
+ };
4230
+ if (kind === "group" || kind === "card") {
4231
+ const Group = Viewer;
4232
+ return /* @__PURE__ */ jsx22(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4233
+ }
4234
+ return /* @__PURE__ */ jsx22(Viewer, { ...viewerProps });
4235
+ }
4236
+
4237
+ // src/jsonView/JsonView.tsx
4238
+ import { jsx as jsx23 } from "react/jsx-runtime";
4239
+ function JsonView(props) {
4240
+ const { schema, value, theme, surface, density = "comfortable", className } = props;
4241
+ const { style } = useJsonViewTokens(theme, surface);
4242
+ const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
4243
+ return /* @__PURE__ */ jsx23("div", { className: cls, style, children: /* @__PURE__ */ jsx23(
4244
+ RenderNode,
4245
+ {
4246
+ value,
4247
+ schema,
4248
+ rootSchema: schema,
4249
+ rootData: value,
4250
+ pointer: "",
4251
+ density
4252
+ }
4253
+ ) });
4254
+ }
4255
+ export {
4256
+ BlockRenderer,
4257
+ CaptionOverlay,
4258
+ DocControlsBottom,
4259
+ DocControlsOverlay,
4260
+ DocControlsSidebar,
4261
+ DocControlsSlideshow,
4262
+ DocPlayer,
4263
+ DocPlayerWithSidebar,
4264
+ DocProgressBar,
4265
+ ImageLayer,
4266
+ InlineAudioPlayer,
4267
+ InlineVideoPlayer,
4268
+ JsonView,
4269
+ LinearDocView,
4270
+ MapLayer,
4271
+ MarkdownRenderer,
4272
+ MediaContext,
4273
+ ShapeLayer,
4274
+ SocialCaptionOverlay,
4275
+ TableLayer,
4276
+ TextLayer,
4277
+ VIEWPORT,
4278
+ VideoLayer,
4279
+ formatTime,
4280
+ getAnimationStyle,
4281
+ getTransitionClass,
4282
+ useAudioSync,
4283
+ useAutoSurface,
4284
+ useDocPlayback,
4285
+ useMediaProvider,
4286
+ useMediaUrl,
4287
+ useViewportOrientation
4288
+ };
34
4289
  //# sourceMappingURL=index.js.map