@bendyline/squisq-react 0.1.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/BlockRenderer.d.ts +37 -0
  2. package/dist/BlockRenderer.d.ts.map +1 -0
  3. package/dist/BlockRenderer.js +51 -0
  4. package/dist/BlockRenderer.js.map +1 -0
  5. package/dist/CaptionOverlay.d.ts +24 -0
  6. package/dist/CaptionOverlay.d.ts.map +1 -0
  7. package/dist/CaptionOverlay.js +40 -0
  8. package/dist/CaptionOverlay.js.map +1 -0
  9. package/dist/DocControlsBottom.d.ts +28 -0
  10. package/dist/DocControlsBottom.d.ts.map +1 -0
  11. package/dist/DocControlsBottom.js +23 -0
  12. package/dist/DocControlsBottom.js.map +1 -0
  13. package/dist/DocControlsOverlay.d.ts +28 -0
  14. package/dist/DocControlsOverlay.d.ts.map +1 -0
  15. package/dist/DocControlsOverlay.js +75 -0
  16. package/dist/DocControlsOverlay.js.map +1 -0
  17. package/dist/DocControlsSidebar.d.ts +27 -0
  18. package/dist/DocControlsSidebar.d.ts.map +1 -0
  19. package/dist/DocControlsSidebar.js +6 -0
  20. package/dist/DocControlsSidebar.js.map +1 -0
  21. package/dist/DocControlsSlideshow.d.ts +20 -0
  22. package/dist/DocControlsSlideshow.d.ts.map +1 -0
  23. package/dist/DocControlsSlideshow.js +69 -0
  24. package/dist/DocControlsSlideshow.js.map +1 -0
  25. package/dist/DocPlayer.d.ts +87 -0
  26. package/dist/DocPlayer.d.ts.map +1 -0
  27. package/dist/DocPlayer.js +604 -0
  28. package/dist/DocPlayer.js.map +1 -0
  29. package/dist/DocPlayerWithSidebar.d.ts +41 -0
  30. package/dist/DocPlayerWithSidebar.d.ts.map +1 -0
  31. package/dist/DocPlayerWithSidebar.js +68 -0
  32. package/dist/DocPlayerWithSidebar.js.map +1 -0
  33. package/dist/DocProgressBar.d.ts +31 -0
  34. package/dist/DocProgressBar.d.ts.map +1 -0
  35. package/dist/DocProgressBar.js +122 -0
  36. package/dist/DocProgressBar.js.map +1 -0
  37. package/dist/LinearDocView.d.ts +47 -0
  38. package/dist/LinearDocView.d.ts.map +1 -0
  39. package/dist/LinearDocView.js +184 -0
  40. package/dist/LinearDocView.js.map +1 -0
  41. package/dist/MarkdownRenderer.d.ts +32 -0
  42. package/dist/MarkdownRenderer.d.ts.map +1 -0
  43. package/dist/MarkdownRenderer.js +133 -0
  44. package/dist/MarkdownRenderer.js.map +1 -0
  45. package/dist/__tests__/BlockRenderer.test.d.ts +2 -0
  46. package/dist/__tests__/BlockRenderer.test.d.ts.map +1 -0
  47. package/dist/__tests__/BlockRenderer.test.js +82 -0
  48. package/dist/__tests__/BlockRenderer.test.js.map +1 -0
  49. package/dist/__tests__/DocControlsSlideshow.test.d.ts +2 -0
  50. package/dist/__tests__/DocControlsSlideshow.test.d.ts.map +1 -0
  51. package/dist/__tests__/DocControlsSlideshow.test.js +88 -0
  52. package/dist/__tests__/DocControlsSlideshow.test.js.map +1 -0
  53. package/dist/__tests__/LinearDocView.test.d.ts +2 -0
  54. package/dist/__tests__/LinearDocView.test.d.ts.map +1 -0
  55. package/dist/__tests__/LinearDocView.test.js +160 -0
  56. package/dist/__tests__/LinearDocView.test.js.map +1 -0
  57. package/dist/__tests__/MarkdownRenderer.test.d.ts +2 -0
  58. package/dist/__tests__/MarkdownRenderer.test.d.ts.map +1 -0
  59. package/dist/__tests__/MarkdownRenderer.test.js +208 -0
  60. package/dist/__tests__/MarkdownRenderer.test.js.map +1 -0
  61. package/dist/__tests__/exports.test.d.ts +2 -0
  62. package/dist/__tests__/exports.test.d.ts.map +1 -0
  63. package/dist/__tests__/exports.test.js +48 -0
  64. package/dist/__tests__/exports.test.js.map +1 -0
  65. package/dist/hooks/AudioProvider.d.ts +71 -0
  66. package/dist/hooks/AudioProvider.d.ts.map +1 -0
  67. package/dist/hooks/AudioProvider.js +52 -0
  68. package/dist/hooks/AudioProvider.js.map +1 -0
  69. package/dist/hooks/MediaContext.d.ts +35 -0
  70. package/dist/hooks/MediaContext.d.ts.map +1 -0
  71. package/dist/hooks/MediaContext.js +66 -0
  72. package/dist/hooks/MediaContext.js.map +1 -0
  73. package/dist/hooks/index.d.ts +6 -0
  74. package/dist/hooks/index.d.ts.map +1 -0
  75. package/dist/hooks/index.js +5 -0
  76. package/dist/hooks/index.js.map +1 -0
  77. package/dist/hooks/useAudioSync.d.ts +18 -0
  78. package/dist/hooks/useAudioSync.d.ts.map +1 -0
  79. package/dist/hooks/useAudioSync.js +338 -0
  80. package/dist/hooks/useAudioSync.js.map +1 -0
  81. package/dist/hooks/useDocPlayback.d.ts +47 -0
  82. package/dist/hooks/useDocPlayback.d.ts.map +1 -0
  83. package/dist/hooks/useDocPlayback.js +176 -0
  84. package/dist/hooks/useDocPlayback.js.map +1 -0
  85. package/dist/hooks/useViewportOrientation.d.ts +31 -0
  86. package/dist/hooks/useViewportOrientation.d.ts.map +1 -0
  87. package/dist/hooks/useViewportOrientation.js +92 -0
  88. package/dist/hooks/useViewportOrientation.js.map +1 -0
  89. package/dist/index.d.ts +25 -563
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +26 -3179
  92. package/dist/index.js.map +1 -1
  93. package/dist/layers/ImageLayer.d.ts +23 -0
  94. package/dist/layers/ImageLayer.d.ts.map +1 -0
  95. package/dist/layers/ImageLayer.js +104 -0
  96. package/dist/layers/ImageLayer.js.map +1 -0
  97. package/dist/layers/MapLayer.d.ts +29 -0
  98. package/dist/layers/MapLayer.d.ts.map +1 -0
  99. package/dist/layers/MapLayer.js +97 -0
  100. package/dist/layers/MapLayer.js.map +1 -0
  101. package/dist/layers/ShapeLayer.d.ts +20 -0
  102. package/dist/layers/ShapeLayer.d.ts.map +1 -0
  103. package/dist/layers/ShapeLayer.js +35 -0
  104. package/dist/layers/ShapeLayer.js.map +1 -0
  105. package/dist/layers/TextLayer.d.ts +23 -0
  106. package/dist/layers/TextLayer.d.ts.map +1 -0
  107. package/dist/layers/TextLayer.js +126 -0
  108. package/dist/layers/TextLayer.js.map +1 -0
  109. package/dist/layers/VideoLayer.d.ts +40 -0
  110. package/dist/layers/VideoLayer.d.ts.map +1 -0
  111. package/dist/layers/VideoLayer.js +105 -0
  112. package/dist/layers/VideoLayer.js.map +1 -0
  113. package/dist/layers/index.d.ts +6 -0
  114. package/dist/layers/index.d.ts.map +1 -0
  115. package/dist/layers/index.js +6 -0
  116. package/dist/layers/index.js.map +1 -0
  117. package/dist/squisq-player.global.js +1 -1
  118. package/dist/standalone-entry.d.ts +61 -0
  119. package/dist/standalone-entry.d.ts.map +1 -0
  120. package/dist/standalone-entry.js +169 -0
  121. package/dist/standalone-entry.js.map +1 -0
  122. package/dist/standalone-source.js +1 -1
  123. package/dist/types.d.ts +131 -0
  124. package/dist/types.d.ts.map +1 -0
  125. package/dist/types.js +21 -0
  126. package/dist/types.js.map +1 -0
  127. package/dist/utils/animationUtils.d.ts +8 -0
  128. package/dist/utils/animationUtils.d.ts.map +1 -0
  129. package/dist/utils/animationUtils.js +8 -0
  130. package/dist/utils/animationUtils.js.map +1 -0
  131. package/dist/utils/layerUtils.d.ts +17 -0
  132. package/dist/utils/layerUtils.d.ts.map +1 -0
  133. package/dist/utils/layerUtils.js +37 -0
  134. package/dist/utils/layerUtils.js.map +1 -0
  135. package/dist/utils/mapTileUtils.d.ts +105 -0
  136. package/dist/utils/mapTileUtils.d.ts.map +1 -0
  137. package/dist/utils/mapTileUtils.js +268 -0
  138. package/dist/utils/mapTileUtils.js.map +1 -0
  139. package/package.json +3 -4
package/dist/index.js CHANGED
@@ -1,3180 +1,27 @@
1
- // src/DocPlayer.tsx
2
- import { Fragment as Fragment2, useRef as useRef5, useState as useState7, useEffect as useEffect7, useCallback as useCallback4, useMemo as useMemo5 } from "react";
3
- import { isTemplateBlock as isTemplateBlock2, getCaptionAtTime as getCaptionAtTime2 } from "@bendyline/squisq/schemas";
4
-
5
- // src/utils/animationUtils.ts
6
- import {
7
- getAnimationStyle,
8
- getDefaultAnimationDuration,
9
- getTransitionClass,
10
- getAnimationProgress
11
- } from "@bendyline/squisq/doc";
12
-
13
- // src/utils/layerUtils.ts
14
- function resolveValue(value, dimension) {
15
- if (typeof value === "number") {
16
- return value;
17
- }
18
- if (value.endsWith("%")) {
19
- const percent = parseFloat(value);
20
- return percent / 100 * dimension;
21
- }
22
- return parseFloat(value);
23
- }
24
- function getAnchorOffset(anchor, width, height) {
25
- switch (anchor) {
26
- case "center":
27
- return { x: -width / 2, y: -height / 2 };
28
- case "top-right":
29
- return { x: -width, y: 0 };
30
- case "bottom-left":
31
- return { x: 0, y: -height };
32
- case "bottom-right":
33
- return { x: -width, y: -height };
34
- case "top-left":
35
- default:
36
- return { x: 0, y: 0 };
37
- }
38
- }
39
-
40
- // src/hooks/MediaContext.tsx
41
- import { createContext, useContext, useState, useEffect, useMemo } from "react";
42
- var MediaContext = createContext(null);
43
- function useMediaProvider() {
44
- return useContext(MediaContext);
45
- }
46
- function useMediaUrl(relativePath, basePath) {
47
- const provider = useMediaProvider();
48
- const isAbsolute = relativePath.startsWith("http") || relativePath.startsWith("/") || relativePath.startsWith("data:") || relativePath.startsWith("blob:");
49
- const fallback = useMemo(
50
- () => isAbsolute ? relativePath : `${basePath}/${relativePath}`,
51
- [isAbsolute, relativePath, basePath]
52
- );
53
- const needsProvider = !isAbsolute && !!provider;
54
- const [url, setUrl] = useState(fallback);
55
- useEffect(() => {
56
- if (!needsProvider) {
57
- setUrl(fallback);
58
- return;
59
- }
60
- let cancelled = false;
61
- provider.resolveUrl(relativePath).then((resolved) => {
62
- if (!cancelled) setUrl(resolved);
63
- });
64
- return () => {
65
- cancelled = true;
66
- };
67
- }, [needsProvider, provider, relativePath, fallback]);
68
- return needsProvider ? url : fallback;
69
- }
70
-
71
- // src/layers/ImageLayer.tsx
72
- import { jsx } from "react/jsx-runtime";
73
- function ImageLayer({ layer, basePath, viewport, blockTime }) {
74
- const { content, position, animation } = layer;
75
- const x = resolveValue(position.x, viewport.width);
76
- const y = resolveValue(position.y, viewport.height);
77
- const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
78
- const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
79
- const offset = getAnchorOffset(position.anchor, width, height);
80
- const finalX = x + offset.x;
81
- const finalY = y + offset.y;
82
- const src = useMediaUrl(content.src, basePath);
83
- const animStyle = getAnimationStyle(animation, blockTime);
84
- const preserveAspectRatio = getPreserveAspectRatio(content.fit);
85
- const isCover = content.fit === "cover";
86
- const isSpatialAnim = animation && SPATIAL_ANIMATION_TYPES.has(animation.type);
87
- if (isCover && isSpatialAnim && animation) {
88
- const kbAnim = remapToKenBurns(animation);
89
- const kbStyle = getAnimationStyle(kbAnim, blockTime);
90
- 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(
91
- "div",
92
- {
93
- style: {
94
- width: "100%",
95
- height: "100%",
96
- overflow: "hidden"
97
- },
98
- children: /* @__PURE__ */ jsx(
99
- "img",
100
- {
101
- src,
102
- alt: content.alt || "",
103
- className: kbStyle.className,
104
- style: {
105
- width: "100%",
106
- height: "100%",
107
- objectFit: "cover",
108
- objectPosition: "center",
109
- display: "block",
110
- pointerEvents: "none",
111
- transformOrigin: "center center",
112
- ...kbStyle.style
113
- }
114
- }
115
- )
116
- }
117
- ) }) });
118
- }
119
- if (isCover) {
120
- return /* @__PURE__ */ jsx(
121
- "g",
122
- {
123
- className: `block-layer block-layer--image ${animStyle.className}`,
124
- style: animStyle.style,
125
- "data-layer-id": layer.id,
126
- children: /* @__PURE__ */ jsx("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx(
127
- "img",
128
- {
129
- src,
130
- alt: content.alt || "",
131
- style: {
132
- width: "100%",
133
- height: "100%",
134
- objectFit: "cover",
135
- objectPosition: "center",
136
- display: "block",
137
- pointerEvents: "none"
138
- }
139
- }
140
- ) })
141
- }
142
- );
143
- }
144
- return /* @__PURE__ */ jsx(
145
- "g",
146
- {
147
- className: `block-layer block-layer--image ${animStyle.className}`,
148
- style: animStyle.style,
149
- "data-layer-id": layer.id,
150
- children: /* @__PURE__ */ jsx(
151
- "image",
152
- {
153
- href: src,
154
- x: finalX,
155
- y: finalY,
156
- width,
157
- height,
158
- preserveAspectRatio,
159
- style: { pointerEvents: "none" }
160
- }
161
- )
162
- }
163
- );
164
- }
165
- function getPreserveAspectRatio(fit) {
166
- switch (fit) {
167
- case "cover":
168
- return "xMidYMid slice";
169
- case "fill":
170
- return "none";
171
- case "contain":
172
- default:
173
- return "xMidYMid meet";
174
- }
175
- }
176
- var SPATIAL_ANIMATION_TYPES = /* @__PURE__ */ new Set(["panLeft", "panRight", "slowZoom", "zoomIn", "zoomOut"]);
177
- function remapToKenBurns(anim) {
178
- switch (anim.type) {
179
- case "panLeft":
180
- return { ...anim, type: "slowZoom", panDirection: "left" };
181
- case "panRight":
182
- return { ...anim, type: "slowZoom", panDirection: "right" };
183
- case "zoomIn":
184
- return { ...anim, type: "slowZoom", direction: "in" };
185
- case "zoomOut":
186
- return { ...anim, type: "slowZoom", direction: "out" };
187
- default:
188
- return anim;
189
- }
190
- }
191
-
192
- // src/layers/TextLayer.tsx
193
- import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
194
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
195
- function TextLayer({ layer, viewport, blockTime }) {
196
- const { content, position, animation } = layer;
197
- const { text, style } = content;
198
- const x = resolveValue(position.x, viewport.width);
199
- const y = resolveValue(position.y, viewport.height);
200
- const maxWidth = position.width ? resolveValue(position.width, viewport.width) : void 0;
201
- const textAnchor = getTextAnchor(style.textAlign, position.anchor);
202
- const dominantBaseline = getDominantBaseline(position.anchor);
203
- const animStyle = getAnimationStyle(animation, blockTime);
204
- const rawLines = text.split("\n");
205
- let lines = maxWidth ? rawLines.reduce(
206
- (acc, line) => acc.concat(wrapText(line, style.fontSize, maxWidth)),
207
- []
208
- ) : rawLines;
209
- if (style.maxLines && lines.length > style.maxLines) {
210
- lines = lines.slice(0, style.maxLines);
211
- const last = lines[lines.length - 1];
212
- lines[lines.length - 1] = last.replace(/\s*$/, "") + "...";
213
- }
214
- const lineHeight = style.lineHeight || 1.4;
215
- const lineHeightPx = style.fontSize * lineHeight;
216
- const textStyles = {
217
- fontSize: `${style.fontSize}px`,
218
- fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
219
- fontWeight: style.fontWeight || "normal",
220
- fill: style.color,
221
- ...animStyle.style
222
- };
223
- const filterId = style.shadow ? `shadow-${layer.id}` : void 0;
224
- return /* @__PURE__ */ jsxs("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
225
- 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)" }) }) }),
226
- style.background && /* @__PURE__ */ jsx2(
227
- "rect",
228
- {
229
- x: x - (style.padding || 16),
230
- y: y - style.fontSize - (style.padding || 16),
231
- width: getTextBoxWidth(lines, style) + (style.padding || 16) * 2,
232
- height: lines.length * lineHeightPx + (style.padding || 16) * 2,
233
- fill: style.background,
234
- rx: 4,
235
- ry: 4
236
- }
237
- ),
238
- /* @__PURE__ */ jsx2(
239
- "text",
240
- {
241
- x,
242
- y,
243
- textAnchor,
244
- dominantBaseline,
245
- style: textStyles,
246
- filter: filterId ? `url(#${filterId})` : void 0,
247
- children: lines.map((line, i) => /* @__PURE__ */ jsxs("tspan", { x, dy: i === 0 ? 0 : lineHeightPx, children: [
248
- line || "\xA0",
249
- " "
250
- ] }, i))
251
- }
252
- )
253
- ] });
254
- }
255
- function getTextAnchor(align, anchor) {
256
- if (align === "center") return "middle";
257
- if (align === "right") return "end";
258
- if (align === "left") return "start";
259
- if (anchor?.includes("right")) return "end";
260
- if (anchor === "center") return "middle";
261
- return "start";
262
- }
263
- function getDominantBaseline(anchor) {
264
- if (anchor?.includes("bottom")) return "text-after-edge";
265
- if (anchor === "center") return "middle";
266
- return "text-before-edge";
267
- }
268
- function getTextBoxWidth(lines, style) {
269
- const maxLineLength = Math.max(...lines.map((l) => l.length));
270
- return maxLineLength * style.fontSize * 0.55;
271
- }
272
- function wrapText(text, fontSize, maxWidth) {
273
- if (!text.trim()) return [""];
274
- const avgCharWidth = fontSize * 0.5;
275
- const charsPerLine = Math.floor(maxWidth / avgCharWidth);
276
- if (charsPerLine <= 0) return [text];
277
- const words = text.split(/\s+/);
278
- const lines = [];
279
- let currentLine = "";
280
- for (const word of words) {
281
- const testLine = currentLine ? `${currentLine} ${word}` : word;
282
- if (testLine.length <= charsPerLine) {
283
- currentLine = testLine;
284
- } else {
285
- if (currentLine) {
286
- lines.push(currentLine);
287
- }
288
- if (word.length > charsPerLine) {
289
- let remaining = word;
290
- while (remaining.length > charsPerLine) {
291
- lines.push(remaining.slice(0, charsPerLine));
292
- remaining = remaining.slice(charsPerLine);
293
- }
294
- currentLine = remaining;
295
- } else {
296
- currentLine = word;
297
- }
298
- }
299
- }
300
- if (currentLine) {
301
- lines.push(currentLine);
302
- }
303
- return lines.length > 0 ? lines : [""];
304
- }
305
-
306
- // src/layers/ShapeLayer.tsx
307
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
308
- function ShapeLayer({ layer, viewport, blockTime }) {
309
- const { content, position, animation } = layer;
310
- const x = resolveValue(position.x, viewport.width);
311
- const y = resolveValue(position.y, viewport.height);
312
- const width = position.width ? resolveValue(position.width, viewport.width) : 100;
313
- const height = position.height ? resolveValue(position.height, viewport.height) : 100;
314
- const animStyle = getAnimationStyle(animation, blockTime);
315
- const fill = content.fill || "none";
316
- const isCSSGradient = typeof fill === "string" && fill.includes("gradient(");
317
- if (content.shape === "rect" && isCSSGradient) {
318
- return /* @__PURE__ */ jsx3(
319
- "g",
320
- {
321
- className: `block-layer block-layer--shape ${animStyle.className}`,
322
- style: animStyle.style,
323
- "data-layer-id": layer.id,
324
- children: /* @__PURE__ */ jsx3("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx3(
325
- "div",
326
- {
327
- style: {
328
- width: "100%",
329
- height: "100%",
330
- background: fill,
331
- borderRadius: content.borderRadius ? `${content.borderRadius}px` : void 0,
332
- pointerEvents: "none"
333
- }
334
- }
335
- ) })
336
- }
337
- );
338
- }
339
- const shapeProps = {
340
- fill,
341
- stroke: content.stroke,
342
- strokeWidth: content.strokeWidth
343
- };
344
- return /* @__PURE__ */ jsxs2(
345
- "g",
346
- {
347
- className: `block-layer block-layer--shape ${animStyle.className}`,
348
- style: animStyle.style,
349
- "data-layer-id": layer.id,
350
- children: [
351
- content.shape === "rect" && /* @__PURE__ */ jsx3(
352
- "rect",
353
- {
354
- x,
355
- y,
356
- width,
357
- height,
358
- rx: content.borderRadius,
359
- ry: content.borderRadius,
360
- ...shapeProps
361
- }
362
- ),
363
- content.shape === "circle" && /* @__PURE__ */ jsx3(
364
- "circle",
365
- {
366
- cx: x + width / 2,
367
- cy: y + height / 2,
368
- r: Math.min(width, height) / 2,
369
- ...shapeProps
370
- }
371
- ),
372
- content.shape === "line" && /* @__PURE__ */ jsx3(
373
- "line",
374
- {
375
- x1: x,
376
- y1: y,
377
- x2: x + width,
378
- y2: y + height,
379
- stroke: content.stroke || "#ffffff",
380
- strokeWidth: content.strokeWidth || 2
381
- }
382
- )
383
- ]
384
- }
385
- );
386
- }
387
-
388
- // src/layers/MapLayer.tsx
389
- import { useState as useState2, useEffect as useEffect2 } from "react";
390
-
391
- // src/utils/mapTileUtils.ts
392
- var TILE_PROVIDERS = {
393
- terrain: {
394
- url: "https://tile.opentopomap.org/{z}/{x}/{y}.png",
395
- attribution: "Map: OpenTopoMap (CC-BY-SA)",
396
- maxZoom: 17
397
- },
398
- road: {
399
- url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
400
- attribution: "\xA9 OpenStreetMap contributors",
401
- maxZoom: 19
402
- },
403
- satellite: {
404
- url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
405
- attribution: "Imagery: Esri, Maxar, Earthstar",
406
- maxZoom: 18
407
- },
408
- toner: {
409
- url: "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png",
410
- attribution: "Map: Stadia Maps, Stamen Design",
411
- maxZoom: 20
412
- },
413
- watercolor: {
414
- url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
415
- attribution: "Map: Stadia Maps, Stamen Design",
416
- maxZoom: 16
417
- }
418
- };
419
- function latLngToTile(lat, lng, zoom) {
420
- const n = Math.pow(2, zoom);
421
- const x = Math.floor((lng + 180) / 360 * n);
422
- const latRad = lat * Math.PI / 180;
423
- const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);
424
- return { x, y };
425
- }
426
- function getPixelOffset(lat, lng, zoom, tileSize = 256) {
427
- const n = Math.pow(2, zoom);
428
- const xTile = (lng + 180) / 360 * n;
429
- const latRad = lat * Math.PI / 180;
430
- const yTile = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n;
431
- return {
432
- x: (xTile - Math.floor(xTile)) * tileSize,
433
- y: (yTile - Math.floor(yTile)) * tileSize
434
- };
435
- }
436
- function getTilesForViewport(centerLat, centerLng, zoom, viewportWidth, viewportHeight, tileSize = 256) {
437
- const centerTile = latLngToTile(centerLat, centerLng, zoom);
438
- const pixelOffset = getPixelOffset(centerLat, centerLng, zoom, tileSize);
439
- const tilesX = Math.ceil(viewportWidth / tileSize) + 1;
440
- const tilesY = Math.ceil(viewportHeight / tileSize) + 1;
441
- const startX = centerTile.x - Math.floor(tilesX / 2);
442
- const startY = centerTile.y - Math.floor(tilesY / 2);
443
- const centerScreenX = viewportWidth / 2 - pixelOffset.x;
444
- const centerScreenY = viewportHeight / 2 - pixelOffset.y;
445
- const tiles = [];
446
- for (let dy = 0; dy < tilesY; dy++) {
447
- for (let dx = 0; dx < tilesX; dx++) {
448
- const tileX = startX + dx;
449
- const tileY = startY + dy;
450
- const screenX = centerScreenX + (tileX - centerTile.x) * tileSize;
451
- const screenY = centerScreenY + (tileY - centerTile.y) * tileSize;
452
- tiles.push({ x: tileX, y: tileY, screenX, screenY });
453
- }
454
- }
455
- return tiles;
456
- }
457
- function buildTileUrl(provider, x, y, z) {
458
- return provider.url.replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
459
- }
460
- async function fetchTileImage(url) {
461
- return new Promise((resolve, reject) => {
462
- const img = new Image();
463
- img.crossOrigin = "anonymous";
464
- img.onload = () => resolve(img);
465
- img.onerror = () => reject(new Error(`Failed to load tile: ${url}`));
466
- img.src = url;
467
- });
468
- }
469
- async function composeMapImage(options) {
470
- const { center, zoom, style, width, height, markers = [], showAttribution = true } = options;
471
- const provider = TILE_PROVIDERS[style];
472
- const tileSize = provider.tileSize || 256;
473
- const clampedZoom = Math.min(zoom, provider.maxZoom);
474
- const canvas = document.createElement("canvas");
475
- canvas.width = width;
476
- canvas.height = height;
477
- const ctx = canvas.getContext("2d");
478
- if (!ctx) throw new Error("Failed to get canvas context");
479
- ctx.fillStyle = style === "toner" ? "#ffffff" : "#e5e7eb";
480
- ctx.fillRect(0, 0, width, height);
481
- const tiles = getTilesForViewport(center.lat, center.lng, clampedZoom, width, height, tileSize);
482
- const tilePromises = tiles.map(async (tile) => {
483
- const url = buildTileUrl(provider, tile.x, tile.y, clampedZoom);
484
- try {
485
- const img = await fetchTileImage(url);
486
- ctx.drawImage(img, tile.screenX, tile.screenY, tileSize, tileSize);
487
- } catch (err) {
488
- console.warn(`Tile load failed: ${url}`, err);
489
- }
490
- });
491
- await Promise.all(tilePromises);
492
- for (const marker of markers) {
493
- drawMarker(ctx, marker, center, clampedZoom, width, height, tileSize);
494
- }
495
- if (showAttribution) {
496
- drawAttribution(ctx, provider.attribution, width, height);
497
- }
498
- return canvas.toDataURL("image/png");
499
- }
500
- function drawMarker(ctx, marker, center, zoom, width, height, tileSize) {
501
- const centerTile = latLngToTile(center.lat, center.lng, zoom);
502
- const markerTile = latLngToTile(marker.lat, marker.lng, zoom);
503
- const centerOffset = getPixelOffset(center.lat, center.lng, zoom, tileSize);
504
- const markerOffset = getPixelOffset(marker.lat, marker.lng, zoom, tileSize);
505
- const dx = (markerTile.x - centerTile.x) * tileSize + (markerOffset.x - centerOffset.x);
506
- const dy = (markerTile.y - centerTile.y) * tileSize + (markerOffset.y - centerOffset.y);
507
- const screenX = width / 2 + dx;
508
- const screenY = height / 2 + dy;
509
- const color = marker.color || "#ef4444";
510
- const icon = marker.icon || "pin";
511
- ctx.save();
512
- if (icon === "pin") {
513
- ctx.fillStyle = color;
514
- ctx.beginPath();
515
- ctx.arc(screenX, screenY - 12, 8, Math.PI, 0, false);
516
- ctx.lineTo(screenX, screenY);
517
- ctx.closePath();
518
- ctx.fill();
519
- ctx.fillStyle = "#ffffff";
520
- ctx.beginPath();
521
- ctx.arc(screenX, screenY - 12, 3, 0, Math.PI * 2);
522
- ctx.fill();
523
- } else if (icon === "circle") {
524
- ctx.fillStyle = color;
525
- ctx.beginPath();
526
- ctx.arc(screenX, screenY, 8, 0, Math.PI * 2);
527
- ctx.fill();
528
- ctx.strokeStyle = "#ffffff";
529
- ctx.lineWidth = 2;
530
- ctx.stroke();
531
- } else if (icon === "star") {
532
- ctx.fillStyle = color;
533
- drawStar(ctx, screenX, screenY, 5, 10, 5);
534
- ctx.fill();
535
- }
536
- if (marker.label) {
537
- ctx.fillStyle = "#1f2937";
538
- ctx.font = "bold 12px system-ui, sans-serif";
539
- ctx.textAlign = "center";
540
- ctx.fillText(marker.label, screenX, screenY + 20);
541
- }
542
- ctx.restore();
543
- }
544
- function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
545
- let rot = Math.PI / 2 * 3;
546
- const step = Math.PI / spikes;
547
- ctx.beginPath();
548
- ctx.moveTo(cx, cy - outerRadius);
549
- for (let i = 0; i < spikes; i++) {
550
- ctx.lineTo(cx + Math.cos(rot) * outerRadius, cy + Math.sin(rot) * outerRadius);
551
- rot += step;
552
- ctx.lineTo(cx + Math.cos(rot) * innerRadius, cy + Math.sin(rot) * innerRadius);
553
- rot += step;
554
- }
555
- ctx.lineTo(cx, cy - outerRadius);
556
- ctx.closePath();
557
- }
558
- function drawAttribution(ctx, text, width, height) {
559
- ctx.save();
560
- ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
561
- const padding = 4;
562
- ctx.font = "10px system-ui, sans-serif";
563
- const textWidth = ctx.measureText(text).width;
564
- ctx.fillRect(width - textWidth - padding * 2 - 4, height - 16, textWidth + padding * 2, 14);
565
- ctx.fillStyle = "#374151";
566
- ctx.textAlign = "right";
567
- ctx.fillText(text, width - padding - 4, height - 5);
568
- ctx.restore();
569
- }
570
-
571
- // src/layers/MapLayer.tsx
572
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
573
- function MapLayer({ layer, basePath, viewport, blockTime }) {
574
- const { content, position, animation } = layer;
575
- const [mapImageUrl, setMapImageUrl] = useState2(null);
576
- const [isLoading, setIsLoading] = useState2(true);
577
- const [error, setError] = useState2(null);
578
- const x = resolveValue(position.x, viewport.width);
579
- const y = resolveValue(position.y, viewport.height);
580
- const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
581
- const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
582
- const offset = getAnchorOffset(position.anchor, width, height);
583
- const finalX = x + offset.x;
584
- const finalY = y + offset.y;
585
- useEffect2(() => {
586
- let cancelled = false;
587
- if (content.staticSrc) {
588
- const src = content.staticSrc.startsWith("http") ? content.staticSrc : `${basePath}/${content.staticSrc}`;
589
- setMapImageUrl(src);
590
- setIsLoading(false);
591
- return;
592
- }
593
- setIsLoading(true);
594
- setError(null);
595
- composeMapImage({
596
- center: content.center,
597
- zoom: content.zoom,
598
- style: content.style,
599
- width,
600
- height,
601
- markers: content.markers,
602
- showAttribution: content.showAttribution !== false
603
- }).then((dataUrl) => {
604
- if (!cancelled) {
605
- setMapImageUrl(dataUrl);
606
- setIsLoading(false);
607
- }
608
- }).catch((err) => {
609
- if (!cancelled) {
610
- console.error("Failed to compose map:", err);
611
- setError(err instanceof Error ? err.message : String(err));
612
- setIsLoading(false);
613
- }
614
- });
615
- return () => {
616
- cancelled = true;
617
- };
618
- }, [
619
- content.center.lat,
620
- content.center.lng,
621
- content.zoom,
622
- content.style,
623
- content.staticSrc,
624
- width,
625
- height,
626
- basePath
627
- ]);
628
- const animStyle = getAnimationStyle(animation, blockTime);
629
- if (isLoading) {
630
- return /* @__PURE__ */ jsxs3(
631
- "g",
632
- {
633
- className: `block-layer block-layer--map ${animStyle.className}`,
634
- style: animStyle.style,
635
- "data-layer-id": layer.id,
636
- children: [
637
- /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height, fill: "#e5e7eb" }),
638
- /* @__PURE__ */ jsx4(
639
- "text",
640
- {
641
- x: finalX + width / 2,
642
- y: finalY + height / 2,
643
- textAnchor: "middle",
644
- dominantBaseline: "middle",
645
- fill: "#9ca3af",
646
- fontSize: "24",
647
- fontFamily: "system-ui, sans-serif",
648
- children: "Loading map..."
649
- }
650
- )
651
- ]
652
- }
653
- );
654
- }
655
- if (error || !mapImageUrl) {
656
- return /* @__PURE__ */ jsxs3(
657
- "g",
658
- {
659
- className: `block-layer block-layer--map ${animStyle.className}`,
660
- style: animStyle.style,
661
- "data-layer-id": layer.id,
662
- children: [
663
- /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height, fill: "#fef2f2" }),
664
- /* @__PURE__ */ jsx4(
665
- "text",
666
- {
667
- x: finalX + width / 2,
668
- y: finalY + height / 2,
669
- textAnchor: "middle",
670
- dominantBaseline: "middle",
671
- fill: "#dc2626",
672
- fontSize: "18",
673
- fontFamily: "system-ui, sans-serif",
674
- children: "Map failed to load"
675
- }
676
- )
677
- ]
678
- }
679
- );
680
- }
681
- return /* @__PURE__ */ jsxs3(
682
- "g",
683
- {
684
- className: `block-layer block-layer--map ${animStyle.className}`,
685
- style: animStyle.style,
686
- "data-layer-id": layer.id,
687
- children: [
688
- /* @__PURE__ */ jsx4("defs", { children: /* @__PURE__ */ jsx4("clipPath", { id: `clip-${layer.id}`, children: /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height }) }) }),
689
- /* @__PURE__ */ jsx4("g", { clipPath: `url(#clip-${layer.id})`, children: /* @__PURE__ */ jsx4(
690
- "image",
691
- {
692
- href: mapImageUrl,
693
- x: finalX,
694
- y: finalY,
695
- width,
696
- height,
697
- preserveAspectRatio: "xMidYMid slice",
698
- style: { pointerEvents: "none" }
699
- }
700
- ) })
701
- ]
702
- }
703
- );
704
- }
705
-
706
- // src/layers/VideoLayer.tsx
707
- import { useRef, useEffect as useEffect3 } from "react";
708
- import { jsx as jsx5 } from "react/jsx-runtime";
709
- function VideoLayer({
710
- layer,
711
- basePath,
712
- viewport,
713
- blockTime: _blockTime,
714
- isPlaying
715
- }) {
716
- const { content, position } = layer;
717
- const videoRef = useRef(null);
718
- const hasStartedRef = useRef(false);
719
- const x = resolveValue(position.x, viewport.width);
720
- const y = resolveValue(position.y, viewport.height);
721
- const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
722
- const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
723
- const offset = getAnchorOffset(position.anchor, width, height);
724
- const finalX = x + offset.x;
725
- const finalY = y + offset.y;
726
- const src = useMediaUrl(content.src, basePath);
727
- const resolvedPoster = useMediaUrl(content.posterSrc || "", basePath);
728
- const posterSrc = content.posterSrc ? resolvedPoster : void 0;
729
- useEffect3(() => {
730
- const video = videoRef.current;
731
- if (!video) return;
732
- video.currentTime = content.clipStart;
733
- hasStartedRef.current = true;
734
- if (isPlaying) {
735
- const playPromise = video.play();
736
- if (playPromise) {
737
- playPromise.catch(() => {
738
- });
739
- }
740
- }
741
- const handleTimeUpdate = () => {
742
- if (video.currentTime >= content.clipEnd) {
743
- video.pause();
744
- video.currentTime = content.clipEnd;
745
- }
746
- };
747
- video.addEventListener("timeupdate", handleTimeUpdate);
748
- return () => {
749
- video.removeEventListener("timeupdate", handleTimeUpdate);
750
- video.pause();
751
- };
752
- }, [content.src, content.clipStart, content.clipEnd]);
753
- useEffect3(() => {
754
- const video = videoRef.current;
755
- if (!video || !hasStartedRef.current) return;
756
- if (video.currentTime >= content.clipEnd) return;
757
- if (isPlaying) {
758
- const playPromise = video.play();
759
- if (playPromise) {
760
- playPromise.catch(() => {
761
- });
762
- }
763
- } else {
764
- video.pause();
765
- }
766
- }, [isPlaying, content.clipEnd]);
767
- 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(
768
- "video",
769
- {
770
- ref: videoRef,
771
- src,
772
- poster: posterSrc,
773
- muted: true,
774
- playsInline: true,
775
- preload: "auto",
776
- "data-clip-start": content.clipStart,
777
- "data-clip-end": content.clipEnd,
778
- style: {
779
- width: "100%",
780
- height: "100%",
781
- objectFit: content.fit || "cover",
782
- objectPosition: "center",
783
- display: "block",
784
- pointerEvents: "none"
785
- }
786
- }
787
- ) }) });
788
- }
789
-
790
- // src/BlockRenderer.tsx
791
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
792
- var VIEWPORT = {
793
- width: 1920,
794
- height: 1080
795
- };
796
- function BlockRenderer({
797
- block,
798
- blockTime,
799
- basePath,
800
- isEntering = false,
801
- isExiting = false,
802
- viewport = VIEWPORT,
803
- isPlaying
804
- }) {
805
- let transitionClass = "";
806
- const transitionStyle = {};
807
- if (block.transition && isEntering) {
808
- transitionClass = getTransitionClass(block.transition.type, true);
809
- transitionStyle["--transition-duration"] = `${block.transition.duration}s`;
810
- } else if (block.transition && isExiting) {
811
- transitionClass = getTransitionClass(block.transition.type, false);
812
- transitionStyle["--transition-duration"] = `${block.transition.duration}s`;
813
- }
814
- const clipId = `vb-clip-${block.id}`;
815
- return /* @__PURE__ */ jsxs4(
816
- "svg",
817
- {
818
- className: `block-svg ${transitionClass}`,
819
- style: transitionStyle,
820
- viewBox: `0 0 ${viewport.width} ${viewport.height}`,
821
- preserveAspectRatio: "xMidYMid meet",
822
- overflow: "hidden",
823
- "data-block-id": block.id,
824
- children: [
825
- /* @__PURE__ */ jsx6("defs", { children: /* @__PURE__ */ jsx6("clipPath", { id: clipId, children: /* @__PURE__ */ jsx6("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
826
- /* @__PURE__ */ jsx6("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx6(
827
- LayerRenderer,
828
- {
829
- layer,
830
- basePath,
831
- viewport,
832
- blockTime,
833
- isPlaying
834
- },
835
- layer.id
836
- )) })
837
- ]
838
- }
839
- );
840
- }
841
- function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
842
- switch (layer.type) {
843
- case "image":
844
- return /* @__PURE__ */ jsx6(ImageLayer, { layer, basePath, viewport, blockTime });
845
- case "text":
846
- return /* @__PURE__ */ jsx6(TextLayer, { layer, viewport, blockTime });
847
- case "shape":
848
- return /* @__PURE__ */ jsx6(ShapeLayer, { layer, viewport, blockTime });
849
- case "map":
850
- return /* @__PURE__ */ jsx6(MapLayer, { layer, basePath, viewport, blockTime });
851
- case "video":
852
- return /* @__PURE__ */ jsx6(
853
- VideoLayer,
854
- {
855
- layer,
856
- basePath,
857
- viewport,
858
- blockTime,
859
- isPlaying
860
- }
861
- );
862
- default:
863
- console.warn(`Unknown layer type: ${layer.type}`);
864
- return null;
865
- }
866
- }
867
-
868
- // src/CaptionOverlay.tsx
869
- import { getCaptionAtTime } from "@bendyline/squisq/schemas";
870
- import { jsx as jsx7 } from "react/jsx-runtime";
871
- function CaptionOverlay({
872
- captions,
873
- currentTime,
874
- enabled = true,
875
- fontSize = 16
876
- }) {
877
- const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
878
- const captionText = phrase?.text ?? null;
879
- return /* @__PURE__ */ jsx7(
880
- "div",
881
- {
882
- className: "caption-overlay",
883
- style: {
884
- position: "absolute",
885
- top: "6px",
886
- left: "50%",
887
- transform: "translateX(-50%)",
888
- zIndex: 50,
889
- pointerEvents: "none",
890
- maxWidth: "100%",
891
- width: "100%",
892
- textAlign: "center",
893
- opacity: captionText ? 1 : 0,
894
- transition: "opacity 0.15s ease-in-out",
895
- padding: "0 4px",
896
- boxSizing: "border-box"
897
- },
898
- children: captionText && /* @__PURE__ */ jsx7(
899
- "div",
900
- {
901
- style: {
902
- display: "inline-block",
903
- padding: "3px 10px",
904
- background: "rgba(0, 0, 0, 0.65)",
905
- borderRadius: "4px",
906
- backdropFilter: "blur(4px)"
907
- },
908
- children: /* @__PURE__ */ jsx7(
909
- "span",
910
- {
911
- style: {
912
- color: "#ffffff",
913
- fontSize: `${fontSize}px`,
914
- fontFamily: "'Hanken Grotesk', system-ui, sans-serif",
915
- fontWeight: 500,
916
- lineHeight: 1.25,
917
- textShadow: "0 1px 3px rgba(0,0,0,0.5)",
918
- whiteSpace: "pre-wrap",
919
- wordWrap: "break-word"
920
- },
921
- children: captionText
922
- }
923
- )
924
- }
925
- )
926
- }
927
- );
928
- }
929
-
930
- // src/hooks/useAudioSync.ts
931
- import { useState as useState3, useEffect as useEffect4, useRef as useRef2, useCallback } from "react";
932
- function useAudioSync(audioRef, audioTrack, basePath = "") {
933
- const [currentTime, setCurrentTime] = useState3(0);
934
- const [isPlaying, setIsPlaying] = useState3(false);
935
- const [currentSegment, setCurrentSegment] = useState3(0);
936
- const [isEnded, setIsEnded] = useState3(false);
937
- const [isAudioReady, setIsAudioReady] = useState3(false);
938
- const [totalDuration, setTotalDuration] = useState3(0);
939
- const segmentStarts = useRef2([]);
940
- const pendingSeekTime = useRef2(null);
941
- const shouldPlayAfterLoad = useRef2(false);
942
- const blobUrls = useRef2(/* @__PURE__ */ new Map());
943
- const loadingPromises = useRef2(/* @__PURE__ */ new Map());
944
- const fallbackMode = useRef2(false);
945
- useEffect4(() => {
946
- if (!audioTrack?.segments) {
947
- return;
948
- }
949
- let time = 0;
950
- segmentStarts.current = audioTrack.segments.map((seg) => {
951
- const start = time;
952
- time += seg.duration;
953
- return start;
954
- });
955
- setTotalDuration(time);
956
- }, [audioTrack]);
957
- const preloadAudio = useCallback(
958
- async (src) => {
959
- const audioUrl = basePath ? `${basePath}/${src}` : src;
960
- if (blobUrls.current.has(src)) {
961
- return blobUrls.current.get(src);
962
- }
963
- if (loadingPromises.current.has(src)) {
964
- return loadingPromises.current.get(src);
965
- }
966
- const loadPromise = (async () => {
967
- try {
968
- const response = await fetch(audioUrl);
969
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
970
- const blob = await response.blob();
971
- const blobUrl = URL.createObjectURL(blob);
972
- blobUrls.current.set(src, blobUrl);
973
- return blobUrl;
974
- } catch {
975
- return audioUrl;
976
- } finally {
977
- loadingPromises.current.delete(src);
978
- }
979
- })();
980
- loadingPromises.current.set(src, loadPromise);
981
- return loadPromise;
982
- },
983
- [basePath]
984
- );
985
- useEffect4(() => {
986
- if (!audioTrack?.segments) return;
987
- audioTrack.segments.forEach((segment) => {
988
- preloadAudio(segment.src);
989
- });
990
- const currentBlobUrls = blobUrls.current;
991
- return () => {
992
- currentBlobUrls.forEach((url) => {
993
- URL.revokeObjectURL(url);
994
- });
995
- currentBlobUrls.clear();
996
- };
997
- }, [audioTrack, preloadAudio]);
998
- useEffect4(() => {
999
- const audio = audioRef.current;
1000
- if (!audio) return;
1001
- const handleTimeUpdate = () => {
1002
- const segmentStart = segmentStarts.current[currentSegment] || 0;
1003
- const overallTime = segmentStart + audio.currentTime;
1004
- setCurrentTime(overallTime);
1005
- };
1006
- const handlePlay = () => {
1007
- fallbackMode.current = false;
1008
- setIsPlaying(true);
1009
- };
1010
- const handlePause = () => setIsPlaying(false);
1011
- const handleError = () => {
1012
- setIsAudioReady(true);
1013
- };
1014
- const handleEnded = () => {
1015
- if (audioTrack && currentSegment < audioTrack.segments.length - 1) {
1016
- shouldPlayAfterLoad.current = true;
1017
- setCurrentSegment((prev) => prev + 1);
1018
- } else {
1019
- setIsEnded(true);
1020
- setIsPlaying(false);
1021
- }
1022
- };
1023
- audio.addEventListener("timeupdate", handleTimeUpdate);
1024
- audio.addEventListener("play", handlePlay);
1025
- audio.addEventListener("pause", handlePause);
1026
- audio.addEventListener("ended", handleEnded);
1027
- audio.addEventListener("error", handleError);
1028
- return () => {
1029
- audio.removeEventListener("timeupdate", handleTimeUpdate);
1030
- audio.removeEventListener("play", handlePlay);
1031
- audio.removeEventListener("pause", handlePause);
1032
- audio.removeEventListener("ended", handleEnded);
1033
- audio.removeEventListener("error", handleError);
1034
- };
1035
- }, [audioRef, currentSegment, audioTrack]);
1036
- useEffect4(() => {
1037
- const audio = audioRef.current;
1038
- if (!audio || !audioTrack?.segments) return;
1039
- const segment = audioTrack.segments[currentSegment];
1040
- if (!segment) return;
1041
- const applyPendingSeek = () => {
1042
- if (pendingSeekTime.current !== null) {
1043
- const segmentStart = segmentStarts.current[currentSegment] || 0;
1044
- const segmentTime = pendingSeekTime.current - segmentStart;
1045
- audio.currentTime = Math.max(0, segmentTime);
1046
- setCurrentTime(pendingSeekTime.current);
1047
- pendingSeekTime.current = null;
1048
- }
1049
- if (shouldPlayAfterLoad.current) {
1050
- audio.play().catch(() => {
1051
- });
1052
- shouldPlayAfterLoad.current = false;
1053
- }
1054
- };
1055
- const currentSrc = audio.src;
1056
- const cachedBlobUrl = blobUrls.current.get(segment.src);
1057
- const isSameSource = currentSrc && (currentSrc === cachedBlobUrl || currentSrc.endsWith(segment.src));
1058
- if (!isSameSource) {
1059
- const loadAndPlay = async () => {
1060
- const blobUrl = await preloadAudio(segment.src);
1061
- const handleCanPlay = () => {
1062
- setIsAudioReady(true);
1063
- applyPendingSeek();
1064
- audio.removeEventListener("canplay", handleCanPlay);
1065
- };
1066
- audio.addEventListener("canplay", handleCanPlay);
1067
- audio.src = blobUrl;
1068
- audio.load();
1069
- await Promise.resolve();
1070
- if (audio.readyState >= 3) {
1071
- audio.removeEventListener("canplay", handleCanPlay);
1072
- setIsAudioReady(true);
1073
- applyPendingSeek();
1074
- }
1075
- };
1076
- loadAndPlay();
1077
- } else {
1078
- applyPendingSeek();
1079
- }
1080
- }, [audioRef, currentSegment, audioTrack, preloadAudio]);
1081
- const play = useCallback(() => {
1082
- const audio = audioRef.current;
1083
- if (audio) {
1084
- if (isEnded) {
1085
- setCurrentSegment(0);
1086
- setIsEnded(false);
1087
- }
1088
- audio.play().then(() => {
1089
- fallbackMode.current = false;
1090
- }).catch(() => {
1091
- fallbackMode.current = true;
1092
- setIsPlaying(true);
1093
- });
1094
- }
1095
- }, [audioRef, isEnded]);
1096
- const pause = useCallback(() => {
1097
- const audio = audioRef.current;
1098
- if (audio) {
1099
- audio.pause();
1100
- }
1101
- setIsPlaying(false);
1102
- }, [audioRef]);
1103
- const toggle = useCallback(() => {
1104
- const audio = audioRef.current;
1105
- if (!audio) return;
1106
- if (!isPlaying) {
1107
- play();
1108
- } else {
1109
- pause();
1110
- }
1111
- }, [audioRef, isPlaying, play, pause]);
1112
- const seekTo = useCallback(
1113
- (time) => {
1114
- const audio = audioRef.current;
1115
- if (!audio || !audioTrack?.segments) return;
1116
- const clampedTime = Math.max(0, Math.min(time, totalDuration));
1117
- let segmentIndex = 0;
1118
- let segmentStart = 0;
1119
- for (let i = 0; i < audioTrack.segments.length; i++) {
1120
- const segEnd = segmentStart + audioTrack.segments[i].duration;
1121
- if (clampedTime < segEnd) {
1122
- segmentIndex = i;
1123
- break;
1124
- }
1125
- segmentStart = segEnd;
1126
- if (i === audioTrack.segments.length - 1) {
1127
- segmentIndex = i;
1128
- }
1129
- }
1130
- const wasPlaying = !audio.paused;
1131
- setIsEnded(false);
1132
- if (segmentIndex !== currentSegment) {
1133
- pendingSeekTime.current = clampedTime;
1134
- shouldPlayAfterLoad.current = wasPlaying;
1135
- setCurrentSegment(segmentIndex);
1136
- } else {
1137
- const segmentTime = clampedTime - segmentStart;
1138
- audio.currentTime = Math.max(0, segmentTime);
1139
- setCurrentTime(clampedTime);
1140
- }
1141
- },
1142
- [audioRef, audioTrack, currentSegment, totalDuration]
1143
- );
1144
- const skipToSegment = useCallback(
1145
- (index) => {
1146
- if (!audioTrack?.segments || index < 0 || index >= audioTrack.segments.length) {
1147
- return;
1148
- }
1149
- setCurrentSegment(index);
1150
- setIsEnded(false);
1151
- },
1152
- [audioTrack]
1153
- );
1154
- const restart = useCallback(async () => {
1155
- seekTo(0);
1156
- await new Promise((resolve) => setTimeout(resolve, 50));
1157
- play();
1158
- }, [seekTo, play]);
1159
- useEffect4(() => {
1160
- if (!isPlaying || !fallbackMode.current || !totalDuration) return;
1161
- let lastTime = performance.now();
1162
- let raf;
1163
- const tick = (now) => {
1164
- if (!fallbackMode.current) return;
1165
- const dt = (now - lastTime) / 1e3;
1166
- lastTime = now;
1167
- setCurrentTime((prev) => {
1168
- const next = prev + dt;
1169
- if (next >= totalDuration) {
1170
- fallbackMode.current = false;
1171
- setIsEnded(true);
1172
- setIsPlaying(false);
1173
- return totalDuration;
1174
- }
1175
- return next;
1176
- });
1177
- raf = requestAnimationFrame(tick);
1178
- };
1179
- raf = requestAnimationFrame(tick);
1180
- return () => cancelAnimationFrame(raf);
1181
- }, [isPlaying, totalDuration]);
1182
- return {
1183
- // State
1184
- currentTime,
1185
- isPlaying,
1186
- currentSegment,
1187
- totalDuration,
1188
- isEnded,
1189
- isReady: isAudioReady,
1190
- isAvailable: true,
1191
- // HTML5 audio is always available in browsers
1192
- // Actions
1193
- play: async () => play(),
1194
- pause: async () => pause(),
1195
- toggle: async () => toggle(),
1196
- seekTo: async (time) => seekTo(time),
1197
- skipToSegment: async (index) => skipToSegment(index),
1198
- restart
1199
- };
1200
- }
1201
-
1202
- // src/hooks/useDocPlayback.ts
1203
- import { useState as useState4, useEffect as useEffect5, useMemo as useMemo2, useCallback as useCallback2, useRef as useRef3 } from "react";
1204
- import { getBlockAtTime } from "@bendyline/squisq/schemas";
1205
- import {
1206
- expandDocBlocks,
1207
- isTemplateBlock,
1208
- VIEWPORT_PRESETS
1209
- } from "@bendyline/squisq/doc";
1210
- function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landscape, renderMode = false, theme) {
1211
- const [transitionState, setTransitionState] = useState4({
1212
- entering: false,
1213
- exiting: false,
1214
- previousBlock: null
1215
- });
1216
- const blocks = useMemo2(() => {
1217
- if (!script?.blocks) {
1218
- return [];
1219
- }
1220
- const hasTemplates = script.blocks.some(isTemplateBlock);
1221
- if (hasTemplates) {
1222
- const audioSegments = script.audio?.segments?.map((seg) => ({
1223
- startTime: seg.startTime,
1224
- duration: seg.duration
1225
- }));
1226
- const expanded = expandDocBlocks(script.blocks, {
1227
- audioSegments,
1228
- viewport,
1229
- persistentLayers: script.persistentLayers,
1230
- theme
1231
- });
1232
- return expanded;
1233
- }
1234
- return script.blocks;
1235
- }, [script?.blocks, script?.audio?.segments, script?.persistentLayers, viewport, theme]);
1236
- const currentBlock = useMemo2(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
1237
- const currentBlockIndex = useMemo2(
1238
- () => currentBlock ? blocks.indexOf(currentBlock) : -1,
1239
- [blocks, currentBlock]
1240
- );
1241
- const blockTime = useMemo2(() => {
1242
- if (!currentBlock) return 0;
1243
- return Math.max(0, currentTime - currentBlock.startTime);
1244
- }, [currentBlock, currentTime]);
1245
- const blockProgress = useMemo2(() => {
1246
- if (!currentBlock || currentBlock.duration === 0) return 0;
1247
- return Math.min(1, blockTime / currentBlock.duration);
1248
- }, [currentBlock, blockTime]);
1249
- const docProgress = useMemo2(() => {
1250
- if (!script || script.duration === 0) return 0;
1251
- return Math.min(1, currentTime / script.duration);
1252
- }, [script, currentTime]);
1253
- const _prevBlockRef = useMemo2(
1254
- () => currentBlock,
1255
- // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally keyed on index, not block reference
1256
- [currentBlockIndex]
1257
- );
1258
- useEffect5(() => {
1259
- if (!currentBlock || renderMode) return;
1260
- if (transitionState.previousBlock?.id !== currentBlock.id) {
1261
- const transition = currentBlock.transition;
1262
- const transitionDuration = transition?.duration || 0;
1263
- if (transitionDuration > 0) {
1264
- setTransitionState({
1265
- entering: true,
1266
- exiting: true,
1267
- previousBlock: transitionState.previousBlock
1268
- });
1269
- const timer = setTimeout(() => {
1270
- setTransitionState({
1271
- entering: false,
1272
- exiting: false,
1273
- previousBlock: currentBlock
1274
- });
1275
- }, transitionDuration * 1e3);
1276
- return () => clearTimeout(timer);
1277
- } else {
1278
- setTransitionState({
1279
- entering: false,
1280
- exiting: false,
1281
- previousBlock: currentBlock
1282
- });
1283
- }
1284
- }
1285
- }, [currentBlock?.id, renderMode]);
1286
- const renderPrevBlockRef = useRef3(null);
1287
- useEffect5(() => {
1288
- if (!renderMode || !currentBlock) return;
1289
- if (transitionState.previousBlock?.id !== currentBlock.id) {
1290
- const oldPrev = transitionState.previousBlock;
1291
- renderPrevBlockRef.current = oldPrev;
1292
- setTransitionState((prev) => ({
1293
- ...prev,
1294
- previousBlock: currentBlock
1295
- }));
1296
- }
1297
- }, [currentBlock?.id, renderMode]);
1298
- const renderTransitionDuration = currentBlock?.transition?.duration || 0;
1299
- const renderIsEntering = renderMode && renderTransitionDuration > 0 && blockTime < renderTransitionDuration;
1300
- const renderIsExiting = renderIsEntering && renderPrevBlockRef.current !== null;
1301
- const goToBlock = useCallback2(
1302
- (index) => {
1303
- if (!script || index < 0 || index >= blocks.length) return;
1304
- const targetBlock = blocks[index];
1305
- if (targetBlock) {
1306
- return targetBlock.startTime;
1307
- }
1308
- },
1309
- [script, blocks]
1310
- );
1311
- const nextBlock = useCallback2(() => {
1312
- if (currentBlockIndex < blocks.length - 1) {
1313
- return goToBlock(currentBlockIndex + 1);
1314
- }
1315
- }, [currentBlockIndex, blocks.length, goToBlock]);
1316
- const prevBlock = useCallback2(() => {
1317
- if (currentBlockIndex > 0) {
1318
- return goToBlock(currentBlockIndex - 1);
1319
- }
1320
- }, [currentBlockIndex, goToBlock]);
1321
- return {
1322
- currentBlock,
1323
- currentBlockIndex,
1324
- previousBlock: renderMode ? renderIsExiting ? renderPrevBlockRef.current : null : transitionState.exiting ? transitionState.previousBlock : null,
1325
- isEntering: renderMode ? renderIsEntering : transitionState.entering,
1326
- isExiting: renderMode ? renderIsExiting : transitionState.exiting,
1327
- blockTime,
1328
- blockProgress,
1329
- docProgress,
1330
- nextBlock,
1331
- prevBlock,
1332
- goToBlock,
1333
- /** Expanded blocks (templates converted to full blocks with layers) */
1334
- blocks
1335
- };
1336
- }
1337
-
1338
- // src/hooks/useViewportOrientation.ts
1339
- import { useState as useState5, useEffect as useEffect6, useMemo as useMemo3 } from "react";
1340
- import {
1341
- VIEWPORT_PRESETS as VIEWPORT_PRESETS2
1342
- } from "@bendyline/squisq/doc";
1343
- function getOrientationFromWindow(width, height) {
1344
- const ratio = width / height;
1345
- if (ratio > 1.2) {
1346
- return "landscape";
1347
- } else if (ratio < 0.83) {
1348
- return "portrait";
1349
- } else {
1350
- return "landscape";
1351
- }
1352
- }
1353
- function getViewportForOrientation(orientation) {
1354
- switch (orientation) {
1355
- case "portrait":
1356
- return VIEWPORT_PRESETS2.portrait;
1357
- case "square":
1358
- return VIEWPORT_PRESETS2.square;
1359
- case "landscape":
1360
- default:
1361
- return VIEWPORT_PRESETS2.landscape;
1362
- }
1363
- }
1364
- function useViewportOrientation() {
1365
- const [windowSize, setWindowSize] = useState5(() => ({
1366
- width: typeof window !== "undefined" ? window.innerWidth : 1920,
1367
- height: typeof window !== "undefined" ? window.innerHeight : 1080
1368
- }));
1369
- useEffect6(() => {
1370
- if (typeof window === "undefined") return;
1371
- const handleResize = () => {
1372
- setWindowSize({
1373
- width: window.innerWidth,
1374
- height: window.innerHeight
1375
- });
1376
- };
1377
- let timeoutId;
1378
- const debouncedResize = () => {
1379
- clearTimeout(timeoutId);
1380
- timeoutId = setTimeout(handleResize, 100);
1381
- };
1382
- window.addEventListener("resize", debouncedResize);
1383
- return () => {
1384
- window.removeEventListener("resize", debouncedResize);
1385
- clearTimeout(timeoutId);
1386
- };
1387
- }, []);
1388
- const orientation = useMemo3(
1389
- () => getOrientationFromWindow(windowSize.width, windowSize.height),
1390
- [windowSize.width, windowSize.height]
1391
- );
1392
- const viewport = useMemo3(() => getViewportForOrientation(orientation), [orientation]);
1393
- return {
1394
- viewport,
1395
- orientation,
1396
- windowSize
1397
- };
1398
- }
1399
-
1400
- // src/DocPlayer.tsx
1401
- import {
1402
- expandCoverBlock,
1403
- createTemplateContext,
1404
- DEFAULT_THEME as DEFAULT_THEME2,
1405
- VIEWPORT_PRESETS as VIEWPORT_PRESETS4
1406
- } from "@bendyline/squisq/doc";
1407
-
1408
- // src/DocProgressBar.tsx
1409
- import { useRef as useRef4, useState as useState6, useCallback as useCallback3 } from "react";
1410
-
1411
- // src/types.ts
1412
- function formatTime(seconds) {
1413
- const mins = Math.floor(seconds / 60);
1414
- const secs = Math.floor(seconds % 60);
1415
- return `${mins}:${secs.toString().padStart(2, "0")}`;
1416
- }
1417
-
1418
- // src/DocProgressBar.tsx
1419
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1420
- function DocProgressBar({
1421
- state,
1422
- actions,
1423
- blockMarkers,
1424
- expandedBlocks,
1425
- getBlockTitle
1426
- }) {
1427
- const progressBarRef = useRef4(null);
1428
- const [hoverPosition, setHoverPosition] = useState6(null);
1429
- const handleProgressHover = useCallback3((e) => {
1430
- const bar = progressBarRef.current;
1431
- if (!bar) return;
1432
- const rect = bar.getBoundingClientRect();
1433
- const x = e.clientX - rect.left;
1434
- const progress = Math.max(0, Math.min(1, x / rect.width));
1435
- setHoverPosition(progress);
1436
- }, []);
1437
- const handleProgressLeave = useCallback3(() => {
1438
- setHoverPosition(null);
1439
- }, []);
1440
- const getBlockAtTimeLocal = useCallback3(
1441
- (time) => {
1442
- for (let i = expandedBlocks.length - 1; i >= 0; i--) {
1443
- const blk = expandedBlocks[i];
1444
- if (time >= blk.startTime) {
1445
- return { block: blk, index: i };
1446
- }
1447
- }
1448
- return expandedBlocks.length > 0 ? { block: expandedBlocks[0], index: 0 } : null;
1449
- },
1450
- [expandedBlocks]
1451
- );
1452
- return /* @__PURE__ */ jsxs5(
1453
- "div",
1454
- {
1455
- ref: progressBarRef,
1456
- style: {
1457
- flex: 1,
1458
- height: "24px",
1459
- cursor: "pointer",
1460
- position: "relative",
1461
- display: "flex",
1462
- alignItems: "center"
1463
- },
1464
- onClick: (e) => {
1465
- const rect = e.currentTarget.getBoundingClientRect();
1466
- const x = e.clientX - rect.left;
1467
- const progress = x / rect.width;
1468
- actions.seekTo(progress * state.totalDuration);
1469
- },
1470
- onMouseMove: handleProgressHover,
1471
- onMouseLeave: handleProgressLeave,
1472
- children: [
1473
- /* @__PURE__ */ jsx8(
1474
- "div",
1475
- {
1476
- style: {
1477
- position: "absolute",
1478
- left: 0,
1479
- right: 0,
1480
- height: "6px",
1481
- background: "rgba(255,255,255,0.2)",
1482
- borderRadius: "3px"
1483
- }
1484
- }
1485
- ),
1486
- /* @__PURE__ */ jsx8(
1487
- "div",
1488
- {
1489
- style: {
1490
- position: "absolute",
1491
- left: 0,
1492
- width: `${state.docProgress * 100}%`,
1493
- height: "6px",
1494
- background: "#3d5a80",
1495
- borderRadius: "3px",
1496
- transition: "width 0.1s"
1497
- }
1498
- }
1499
- ),
1500
- blockMarkers.map((marker, i) => /* @__PURE__ */ jsx8(
1501
- "div",
1502
- {
1503
- style: {
1504
- position: "absolute",
1505
- left: `${marker.position}%`,
1506
- transform: "translateX(-50%)",
1507
- width: "10px",
1508
- height: "10px",
1509
- borderRadius: "50%",
1510
- background: marker.index === state.currentBlockIndex ? "#ffffff" : "rgba(255,255,255,0.5)",
1511
- border: "2px solid #3d5a80",
1512
- cursor: "pointer",
1513
- zIndex: 2,
1514
- transition: "transform 0.15s, background 0.15s"
1515
- },
1516
- title: marker.title,
1517
- onClick: (e) => {
1518
- e.stopPropagation();
1519
- actions.seekTo(marker.block.startTime);
1520
- },
1521
- onMouseEnter: (e) => {
1522
- e.currentTarget.style.transform = "translateX(-50%) scale(1.3)";
1523
- },
1524
- onMouseLeave: (e) => {
1525
- e.currentTarget.style.transform = "translateX(-50%)";
1526
- }
1527
- },
1528
- `${marker.block.id}-${i}`
1529
- )),
1530
- hoverPosition !== null && /* @__PURE__ */ jsxs5(
1531
- "div",
1532
- {
1533
- style: {
1534
- position: "absolute",
1535
- left: `${hoverPosition * 100}%`,
1536
- bottom: "100%",
1537
- transform: "translateX(-50%)",
1538
- marginBottom: "8px",
1539
- padding: "6px 10px",
1540
- background: "rgba(0,0,0,0.9)",
1541
- borderRadius: "4px",
1542
- whiteSpace: "nowrap",
1543
- pointerEvents: "none",
1544
- zIndex: 10
1545
- },
1546
- children: [
1547
- /* @__PURE__ */ jsx8("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
1548
- (() => {
1549
- const hoverTime = hoverPosition * state.totalDuration;
1550
- const slideInfo = getBlockAtTimeLocal(hoverTime);
1551
- if (slideInfo && getBlockTitle) {
1552
- return /* @__PURE__ */ jsx8("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
1553
- }
1554
- return null;
1555
- })()
1556
- ]
1557
- }
1558
- ),
1559
- hoverPosition !== null && /* @__PURE__ */ jsx8(
1560
- "div",
1561
- {
1562
- style: {
1563
- position: "absolute",
1564
- left: `${hoverPosition * 100}%`,
1565
- top: "50%",
1566
- transform: "translate(-50%, -50%)",
1567
- width: "2px",
1568
- height: "16px",
1569
- background: "rgba(255,255,255,0.6)",
1570
- pointerEvents: "none",
1571
- zIndex: 1
1572
- }
1573
- }
1574
- )
1575
- ]
1576
- }
1577
- );
1578
- }
1579
-
1580
- // src/DocControlsOverlay.tsx
1581
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1582
- function DocControlsOverlay({
1583
- state,
1584
- actions,
1585
- blockMarkers,
1586
- expandedBlocks,
1587
- getBlockTitle
1588
- }) {
1589
- return /* @__PURE__ */ jsxs6(
1590
- "div",
1591
- {
1592
- className: "doc-player__controls",
1593
- style: {
1594
- position: "absolute",
1595
- bottom: 0,
1596
- left: 0,
1597
- right: 0,
1598
- padding: "12px 16px",
1599
- background: "linear-gradient(transparent, rgba(0,0,0,0.8))",
1600
- display: "flex",
1601
- alignItems: "center",
1602
- gap: "8px",
1603
- zIndex: 100
1604
- },
1605
- children: [
1606
- /* @__PURE__ */ jsx9(
1607
- "button",
1608
- {
1609
- onClick: actions.restart,
1610
- style: {
1611
- background: "none",
1612
- border: "none",
1613
- color: "rgba(255,255,255,0.7)",
1614
- cursor: "pointer",
1615
- padding: "8px",
1616
- fontSize: "14px",
1617
- display: "flex",
1618
- alignItems: "center"
1619
- },
1620
- title: "Restart",
1621
- "aria-label": "Restart from beginning",
1622
- children: /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx9("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" }) })
1623
- }
1624
- ),
1625
- /* @__PURE__ */ jsx9(
1626
- "button",
1627
- {
1628
- onClick: actions.toggle,
1629
- style: {
1630
- background: "rgba(255,255,255,0.2)",
1631
- border: "none",
1632
- borderRadius: "50%",
1633
- color: "white",
1634
- cursor: "pointer",
1635
- padding: "10px",
1636
- fontSize: "16px",
1637
- display: "flex",
1638
- alignItems: "center",
1639
- justifyContent: "center",
1640
- width: "40px",
1641
- height: "40px"
1642
- },
1643
- "aria-label": state.isPlaying ? "Pause" : "Play",
1644
- children: state.isPlaying ? /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx9("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx9("path", { d: "M8 5v14l11-7z" }) })
1645
- }
1646
- ),
1647
- /* @__PURE__ */ jsxs6("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
1648
- formatTime(state.currentTime),
1649
- " / ",
1650
- formatTime(state.totalDuration)
1651
- ] }),
1652
- /* @__PURE__ */ jsx9(
1653
- DocProgressBar,
1654
- {
1655
- state,
1656
- actions,
1657
- blockMarkers,
1658
- expandedBlocks,
1659
- getBlockTitle
1660
- }
1661
- ),
1662
- /* @__PURE__ */ jsxs6("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
1663
- state.currentBlockIndex + 1,
1664
- "/",
1665
- state.totalBlocks
1666
- ] }),
1667
- state.hasCaptions && /* @__PURE__ */ jsx9(
1668
- "button",
1669
- {
1670
- onClick: () => actions.setCaptionsEnabled(!state.captionsEnabled),
1671
- style: {
1672
- background: state.captionsEnabled ? "rgba(255,255,255,0.2)" : "none",
1673
- border: "none",
1674
- color: state.captionsEnabled ? "white" : "rgba(255,255,255,0.5)",
1675
- cursor: "pointer",
1676
- padding: "8px",
1677
- fontSize: "12px",
1678
- display: "flex",
1679
- alignItems: "center",
1680
- borderRadius: "4px"
1681
- },
1682
- title: state.captionsEnabled ? "Hide captions" : "Show captions",
1683
- "aria-label": state.captionsEnabled ? "Hide captions" : "Show captions",
1684
- children: /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx9("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" }) })
1685
- }
1686
- ),
1687
- actions.toggleFullscreen && /* @__PURE__ */ jsx9(
1688
- "button",
1689
- {
1690
- onClick: actions.toggleFullscreen,
1691
- style: {
1692
- background: state.isFullscreen ? "rgba(255,255,255,0.2)" : "none",
1693
- border: "none",
1694
- color: state.isFullscreen ? "white" : "rgba(255,255,255,0.5)",
1695
- cursor: "pointer",
1696
- padding: "8px",
1697
- display: "flex",
1698
- alignItems: "center",
1699
- borderRadius: "4px"
1700
- },
1701
- title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
1702
- "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
1703
- children: state.isFullscreen ? /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx9("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx9("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
1704
- }
1705
- )
1706
- ]
1707
- }
1708
- );
1709
- }
1710
-
1711
- // src/DocControlsSlideshow.tsx
1712
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1713
- function DocControlsSlideshow({ state, slideNav }) {
1714
- const { currentBlockIndex, totalBlocks } = state;
1715
- const isFirst = currentBlockIndex <= 0;
1716
- const isLast = currentBlockIndex >= totalBlocks - 1;
1717
- return /* @__PURE__ */ jsxs7(
1718
- "div",
1719
- {
1720
- className: "doc-controls-slideshow",
1721
- "data-testid": "slideshow-controls",
1722
- style: {
1723
- position: "absolute",
1724
- bottom: "16px",
1725
- right: "16px",
1726
- display: "flex",
1727
- alignItems: "center",
1728
- gap: "2px",
1729
- background: "rgba(0, 0, 0, 0.65)",
1730
- borderRadius: "8px",
1731
- padding: "4px 6px",
1732
- zIndex: 100,
1733
- userSelect: "none",
1734
- backdropFilter: "blur(8px)",
1735
- WebkitBackdropFilter: "blur(8px)"
1736
- },
1737
- children: [
1738
- /* @__PURE__ */ jsx10(
1739
- "button",
1740
- {
1741
- onClick: (e) => {
1742
- e.stopPropagation();
1743
- slideNav.prevSlide();
1744
- },
1745
- disabled: isFirst,
1746
- "data-testid": "slide-prev",
1747
- "aria-label": "Previous slide",
1748
- title: "Previous slide",
1749
- style: {
1750
- background: "none",
1751
- border: "none",
1752
- color: isFirst ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.9)",
1753
- cursor: isFirst ? "default" : "pointer",
1754
- padding: "6px 8px",
1755
- display: "flex",
1756
- alignItems: "center",
1757
- justifyContent: "center",
1758
- borderRadius: "4px",
1759
- transition: "background 0.15s"
1760
- },
1761
- onMouseEnter: (e) => {
1762
- if (!isFirst) e.currentTarget.style.background = "rgba(255,255,255,0.1)";
1763
- },
1764
- onMouseLeave: (e) => {
1765
- e.currentTarget.style.background = "none";
1766
- },
1767
- children: /* @__PURE__ */ jsx10("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx10("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
1768
- }
1769
- ),
1770
- /* @__PURE__ */ jsx10(
1771
- "span",
1772
- {
1773
- "data-testid": "slide-counter",
1774
- style: {
1775
- color: "rgba(255,255,255,0.9)",
1776
- fontSize: "13px",
1777
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1778
- fontVariantNumeric: "tabular-nums",
1779
- minWidth: "48px",
1780
- textAlign: "center",
1781
- padding: "0 4px",
1782
- letterSpacing: "0.02em"
1783
- },
1784
- children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
1785
- }
1786
- ),
1787
- /* @__PURE__ */ jsx10(
1788
- "button",
1789
- {
1790
- onClick: (e) => {
1791
- e.stopPropagation();
1792
- slideNav.nextSlide();
1793
- },
1794
- disabled: isLast,
1795
- "data-testid": "slide-next",
1796
- "aria-label": "Next slide",
1797
- title: "Next slide",
1798
- style: {
1799
- background: "none",
1800
- border: "none",
1801
- color: isLast ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.9)",
1802
- cursor: isLast ? "default" : "pointer",
1803
- padding: "6px 8px",
1804
- display: "flex",
1805
- alignItems: "center",
1806
- justifyContent: "center",
1807
- borderRadius: "4px",
1808
- transition: "background 0.15s"
1809
- },
1810
- onMouseEnter: (e) => {
1811
- if (!isLast) e.currentTarget.style.background = "rgba(255,255,255,0.1)";
1812
- },
1813
- onMouseLeave: (e) => {
1814
- e.currentTarget.style.background = "none";
1815
- },
1816
- children: /* @__PURE__ */ jsx10("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx10("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
1817
- }
1818
- )
1819
- ]
1820
- }
1821
- );
1822
- }
1823
-
1824
- // src/LinearDocView.tsx
1825
- import { useMemo as useMemo4 } from "react";
1826
- import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
1827
- import { getLayers, hasTemplate, DEFAULT_THEME } from "@bendyline/squisq/doc";
1828
- import { extractPlainText } from "@bendyline/squisq/markdown";
1829
-
1830
- // src/MarkdownRenderer.tsx
1831
- import { Fragment } from "react";
1832
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1833
- function renderInline(nodes, keyPrefix = "") {
1834
- return nodes.map((node, i) => {
1835
- const key = `${keyPrefix}i${i}`;
1836
- switch (node.type) {
1837
- case "text":
1838
- return /* @__PURE__ */ jsx11(Fragment, { children: node.value }, key);
1839
- case "emphasis":
1840
- return /* @__PURE__ */ jsx11("em", { className: "squisq-md-em", children: renderInline(node.children, key) }, key);
1841
- case "strong":
1842
- return /* @__PURE__ */ jsx11("strong", { className: "squisq-md-strong", children: renderInline(node.children, key) }, key);
1843
- case "delete":
1844
- return /* @__PURE__ */ jsx11("del", { className: "squisq-md-del", children: renderInline(node.children, key) }, key);
1845
- case "inlineCode":
1846
- return /* @__PURE__ */ jsx11("code", { className: "squisq-md-inline-code", children: node.value }, key);
1847
- case "link":
1848
- return /* @__PURE__ */ jsx11(
1849
- "a",
1850
- {
1851
- className: "squisq-md-link",
1852
- href: node.url,
1853
- title: node.title ?? void 0,
1854
- target: "_blank",
1855
- rel: "noopener noreferrer",
1856
- children: renderInline(node.children, key)
1857
- },
1858
- key
1859
- );
1860
- case "image":
1861
- return /* @__PURE__ */ jsx11(
1862
- "img",
1863
- {
1864
- className: "squisq-md-image",
1865
- src: node.url,
1866
- alt: node.alt ?? "",
1867
- title: node.title ?? void 0
1868
- },
1869
- key
1870
- );
1871
- case "break":
1872
- return /* @__PURE__ */ jsx11("br", {}, key);
1873
- case "inlineMath":
1874
- return /* @__PURE__ */ jsx11("code", { className: "squisq-md-inline-math", children: node.value }, key);
1875
- case "htmlInline":
1876
- return /* @__PURE__ */ jsx11(
1877
- "span",
1878
- {
1879
- className: "squisq-md-html-inline",
1880
- dangerouslySetInnerHTML: { __html: node.rawHtml }
1881
- },
1882
- key
1883
- );
1884
- case "footnoteReference":
1885
- return /* @__PURE__ */ jsx11("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs8("a", { href: `#fn-${node.identifier}`, children: [
1886
- "[",
1887
- node.label ?? node.identifier,
1888
- "]"
1889
- ] }) }, key);
1890
- case "linkReference":
1891
- return /* @__PURE__ */ jsx11("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key) }, key);
1892
- case "imageReference":
1893
- return /* @__PURE__ */ jsxs8("span", { className: "squisq-md-image-ref", children: [
1894
- "[",
1895
- node.alt ?? node.identifier,
1896
- "]"
1897
- ] }, key);
1898
- case "textDirective":
1899
- return /* @__PURE__ */ jsx11("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key) }, key);
1900
- default:
1901
- return null;
1902
- }
1903
- });
1904
- }
1905
- function renderBlock(node, key) {
1906
- switch (node.type) {
1907
- case "paragraph":
1908
- return /* @__PURE__ */ jsx11("p", { className: "squisq-md-p", children: renderInline(node.children, key) }, key);
1909
- case "heading": {
1910
- const Tag = `h${node.depth}`;
1911
- return /* @__PURE__ */ jsx11(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key) }, key);
1912
- }
1913
- case "blockquote":
1914
- return /* @__PURE__ */ jsx11("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key) }, key);
1915
- case "list":
1916
- if (node.ordered) {
1917
- return /* @__PURE__ */ jsx11("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);
1918
- }
1919
- return /* @__PURE__ */ jsx11("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`)) }, key);
1920
- case "code":
1921
- return /* @__PURE__ */ jsx11("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx11("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
1922
- case "thematicBreak":
1923
- return /* @__PURE__ */ jsx11("hr", { className: "squisq-md-hr" }, key);
1924
- case "table":
1925
- return renderTable(node.children, node.align, key);
1926
- case "htmlBlock":
1927
- return /* @__PURE__ */ jsx11(
1928
- "div",
1929
- {
1930
- className: "squisq-md-html-block",
1931
- dangerouslySetInnerHTML: { __html: node.rawHtml }
1932
- },
1933
- key
1934
- );
1935
- case "math":
1936
- return /* @__PURE__ */ jsx11("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx11("code", { children: node.value }) }, key);
1937
- case "definition":
1938
- return null;
1939
- case "footnoteDefinition":
1940
- return /* @__PURE__ */ jsxs8("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
1941
- /* @__PURE__ */ jsx11("sup", { children: node.label ?? node.identifier }),
1942
- renderBlocks(node.children, key)
1943
- ] }, key);
1944
- case "containerDirective":
1945
- return /* @__PURE__ */ jsxs8(
1946
- "div",
1947
- {
1948
- className: `squisq-md-directive squisq-md-directive-${node.name}`,
1949
- "data-directive": node.name,
1950
- children: [
1951
- node.label && /* @__PURE__ */ jsx11("div", { className: "squisq-md-directive-label", children: node.label }),
1952
- renderBlocks(node.children, key)
1953
- ]
1954
- },
1955
- key
1956
- );
1957
- case "leafDirective":
1958
- return /* @__PURE__ */ jsx11(
1959
- "div",
1960
- {
1961
- className: `squisq-md-directive squisq-md-directive-${node.name}`,
1962
- "data-directive": node.name,
1963
- children: renderInline(node.children, key)
1964
- },
1965
- key
1966
- );
1967
- case "definitionList":
1968
- return /* @__PURE__ */ jsx11("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
1969
- if (child.type === "definitionTerm") {
1970
- return /* @__PURE__ */ jsx11("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`) }, `${key}dt${i}`);
1971
- }
1972
- return /* @__PURE__ */ jsx11("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`) }, `${key}dd${i}`);
1973
- }) }, key);
1974
- default:
1975
- return null;
1976
- }
1977
- }
1978
- function renderListItem(item, key) {
1979
- const isTask = item.checked !== null && item.checked !== void 0;
1980
- return /* @__PURE__ */ jsxs8("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
1981
- isTask && /* @__PURE__ */ jsx11("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
1982
- renderBlocks(item.children, key)
1983
- ] }, key);
1984
- }
1985
- function renderTable(rows, align, key) {
1986
- const [headerRow, ...bodyRows] = rows;
1987
- return /* @__PURE__ */ jsxs8("table", { className: "squisq-md-table", children: [
1988
- headerRow && /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsx11("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx11(
1989
- "th",
1990
- {
1991
- className: "squisq-md-th",
1992
- style: align?.[ci] ? { textAlign: align[ci] } : void 0,
1993
- children: renderInline(cell.children, `${key}th${ci}`)
1994
- },
1995
- `${key}th${ci}`
1996
- )) }) }),
1997
- bodyRows.length > 0 && /* @__PURE__ */ jsx11("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx11("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx11(
1998
- "td",
1999
- {
2000
- className: "squisq-md-td",
2001
- style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2002
- children: renderInline(cell.children, `${key}td${ri}-${ci}`)
2003
- },
2004
- `${key}td${ri}-${ci}`
2005
- )) }, `${key}tr${ri}`)) })
2006
- ] }, key);
2007
- }
2008
- function renderBlocks(nodes, keyPrefix = "") {
2009
- return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`));
2010
- }
2011
- function MarkdownRenderer({ nodes, className }) {
2012
- if (!nodes || nodes.length === 0) return null;
2013
- return /* @__PURE__ */ jsx11("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes) });
2014
- }
2015
-
2016
- // src/LinearDocView.tsx
2017
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2018
- function isAnnotatedBlock(block) {
2019
- const annotation = block.sourceHeading?.templateAnnotation;
2020
- if (!annotation) return false;
2021
- return hasTemplate(annotation.template);
2022
- }
2023
- function countAll(blocks) {
2024
- let count = 0;
2025
- for (const b of blocks) {
2026
- count++;
2027
- if (b.children) count += countAll(b.children);
2028
- }
2029
- return count;
2030
- }
2031
- function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
2032
- const isAnnotated = isAnnotatedBlock(block);
2033
- const visualBlock = useMemo4(() => {
2034
- if (!isAnnotated) return null;
2035
- const annotation = block.sourceHeading.templateAnnotation;
2036
- const headingText = extractPlainText(block.sourceHeading);
2037
- const bodyText = extractBodyPlainText(block.contents);
2038
- const templateBlock = {
2039
- id: block.id,
2040
- template: annotation.template,
2041
- startTime: 0,
2042
- duration: 1,
2043
- audioSegment: 0,
2044
- title: headingText,
2045
- ...getTemplateDefaults(annotation.template, headingText, bodyText, block.contents),
2046
- ...annotation.params,
2047
- ...block.templateOverrides
2048
- };
2049
- const ctx = {
2050
- ...renderContext,
2051
- blockIndex
2052
- };
2053
- const layers = getLayers(templateBlock, ctx);
2054
- return {
2055
- ...block,
2056
- layers,
2057
- template: annotation.template
2058
- };
2059
- }, [block, isAnnotated, renderContext, blockIndex]);
2060
- return /* @__PURE__ */ jsxs9(
2061
- "div",
2062
- {
2063
- className: "squisq-linear-section",
2064
- "data-block-id": block.id,
2065
- "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
2066
- children: [
2067
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx12(MarkdownRenderer, { nodes: [block.sourceHeading] }),
2068
- isAnnotated && visualBlock && /* @__PURE__ */ jsxs9("div", { className: "squisq-linear-card", children: [
2069
- block.sourceHeading && /* @__PURE__ */ jsx12("div", { className: "squisq-linear-card-label squisq-md", children: /* @__PURE__ */ jsx12(MarkdownRenderer, { nodes: [block.sourceHeading] }) }),
2070
- /* @__PURE__ */ jsx12(
2071
- "div",
2072
- {
2073
- className: "squisq-linear-card-svg",
2074
- style: {
2075
- width: "100%",
2076
- aspectRatio: `${viewport.width} / ${viewport.height}`,
2077
- overflow: "hidden",
2078
- borderRadius: "8px",
2079
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.12)",
2080
- marginBottom: "1em"
2081
- },
2082
- children: /* @__PURE__ */ jsx12(
2083
- BlockRenderer,
2084
- {
2085
- block: visualBlock,
2086
- blockTime: 0,
2087
- basePath,
2088
- viewport
2089
- }
2090
- )
2091
- }
2092
- )
2093
- ] }),
2094
- !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx12(MarkdownRenderer, { nodes: block.contents }),
2095
- block.children && block.children.length > 0 && /* @__PURE__ */ jsx12("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx12(
2096
- BlockSection,
2097
- {
2098
- block: child,
2099
- basePath,
2100
- viewport,
2101
- renderContext,
2102
- blockIndex: blockIndex + i + 1
2103
- },
2104
- child.id
2105
- )) })
2106
- ]
2107
- }
2108
- );
2109
- }
2110
- function extractBodyPlainText(contents) {
2111
- if (!contents || contents.length === 0) return "";
2112
- return contents.map((n) => extractPlainText(n)).join("\n").trim();
2113
- }
2114
- function extractListItems(contents) {
2115
- if (!contents) return [];
2116
- const items = [];
2117
- for (const node of contents) {
2118
- if (node.type === "list") {
2119
- for (const item of node.children) {
2120
- const text = extractPlainText(item).trim();
2121
- if (text) items.push(text);
2122
- }
2123
- }
2124
- }
2125
- return items;
2126
- }
2127
- function getTemplateDefaults(templateName, headingText, bodyText, contents) {
2128
- switch (templateName) {
2129
- case "statHighlight":
2130
- return { stat: headingText, description: bodyText || headingText };
2131
- case "quoteBlock":
2132
- case "fullBleedQuote":
2133
- case "pullQuote":
2134
- return { quote: bodyText || headingText };
2135
- case "factCard":
2136
- return { fact: headingText, explanation: bodyText || headingText };
2137
- case "comparisonBar":
2138
- return { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 };
2139
- case "listBlock":
2140
- return { items: extractListItems(contents) || ["Item 1", "Item 2", "Item 3"] };
2141
- case "definitionCard":
2142
- return { term: headingText, definition: bodyText || headingText };
2143
- case "dateEvent":
2144
- return { date: headingText, description: bodyText || headingText };
2145
- default:
2146
- return {};
2147
- }
2148
- }
2149
- function LinearDocView({
2150
- doc,
2151
- basePath = "/",
2152
- viewport,
2153
- className,
2154
- theme
2155
- }) {
2156
- const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
2157
- const totalBlocks = useMemo4(() => countAll(doc.blocks), [doc.blocks]);
2158
- const renderContext = useMemo4(
2159
- () => ({
2160
- theme: theme ?? DEFAULT_THEME,
2161
- viewport: activeViewport,
2162
- totalBlocks
2163
- }),
2164
- [activeViewport, totalBlocks, theme]
2165
- );
2166
- return /* @__PURE__ */ jsx12(
2167
- "div",
2168
- {
2169
- className: `squisq-linear ${className || ""}`,
2170
- style: {
2171
- width: "100%",
2172
- height: "100%",
2173
- overflowY: "auto",
2174
- overflowX: "hidden"
2175
- },
2176
- children: /* @__PURE__ */ jsx12(
2177
- "div",
2178
- {
2179
- className: "squisq-linear-content",
2180
- style: {
2181
- maxWidth: "720px",
2182
- margin: "0 auto",
2183
- padding: "24px 16px",
2184
- lineHeight: 1.7,
2185
- fontSize: "16px",
2186
- color: "var(--squisq-text, #1f2937)"
2187
- },
2188
- children: doc.blocks.map((block, i) => /* @__PURE__ */ jsx12(
2189
- BlockSection,
2190
- {
2191
- block,
2192
- basePath,
2193
- viewport: activeViewport,
2194
- renderContext,
2195
- blockIndex: i
2196
- },
2197
- block.id
2198
- ))
2199
- }
2200
- )
2201
- }
2202
- );
2203
- }
2204
-
2205
- // src/DocPlayer.tsx
2206
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
2207
- var SMALL_WORDS = /* @__PURE__ */ new Set([
2208
- "a",
2209
- "an",
2210
- "the",
2211
- "and",
2212
- "but",
2213
- "or",
2214
- "for",
2215
- "nor",
2216
- "on",
2217
- "at",
2218
- "to",
2219
- "in",
2220
- "of",
2221
- "by",
2222
- "is"
2223
- ]);
2224
- function buildSegmentTitleMap(script) {
2225
- const map = /* @__PURE__ */ new Map();
2226
- for (const block of script.blocks) {
2227
- if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
2228
- const segIdx = block.audioSegment;
2229
- if (!map.has(segIdx)) {
2230
- map.set(segIdx, block.title);
2231
- }
2232
- }
2233
- }
2234
- for (let i = 0; i < script.audio.segments.length; i++) {
2235
- if (!map.has(i)) {
2236
- const name = script.audio.segments[i].name;
2237
- if (name === "intro" || name.includes("intro")) {
2238
- map.set(i, "Introduction");
2239
- } else if (name === "flight-context" || name.includes("flight-context")) {
2240
- map.set(i, "Flight Context");
2241
- } else {
2242
- const words = name.split("-");
2243
- const titled = words.map(
2244
- (w, idx) => idx === 0 || !SMALL_WORDS.has(w) ? w.charAt(0).toUpperCase() + w.slice(1) : w
2245
- ).join(" ");
2246
- map.set(i, titled);
2247
- }
2248
- }
2249
- }
2250
- return map;
2251
- }
2252
- function DocPlayer({
2253
- script,
2254
- basePath,
2255
- renderMode = false,
2256
- autoPlay = false,
2257
- onEnded,
2258
- onTimeUpdate,
2259
- audioProvider: externalAudioProvider,
2260
- showControls = true,
2261
- showScrubber = false,
2262
- muted = false,
2263
- captionsEnabled: captionsEnabledProp,
2264
- onCaptionsToggle,
2265
- onPlaybackStateChange,
2266
- onControlsReady,
2267
- isFullscreen = false,
2268
- onFullscreenToggle,
2269
- onBlockMarkers,
2270
- forceViewport,
2271
- displayMode = "video",
2272
- theme
2273
- }) {
2274
- const isSlideshowMode = displayMode === "slideshow";
2275
- const isLinearMode = displayMode === "linear";
2276
- const audioRef = useRef5(null);
2277
- const containerRef = useRef5(null);
2278
- const [tapFeedback, setTapFeedback] = useState7(null);
2279
- const tapFeedbackTimer = useRef5();
2280
- const { viewport, orientation } = useViewportOrientation();
2281
- const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
2282
- const isDebugMode = useMemo5(() => {
2283
- if (typeof window === "undefined") return false;
2284
- const params = new URLSearchParams(window.location.search);
2285
- return params.get("debug") === "true";
2286
- }, []);
2287
- const internalAudio = useAudioSync(audioRef, script.audio, basePath);
2288
- const audio = externalAudioProvider || internalAudio;
2289
- const {
2290
- currentTime,
2291
- isPlaying,
2292
- currentSegment,
2293
- totalDuration,
2294
- isEnded,
2295
- isReady: isAudioReady,
2296
- isAvailable,
2297
- unavailableMessage,
2298
- play,
2299
- pause,
2300
- toggle,
2301
- seekTo,
2302
- skipToSegment: _skipToSegment,
2303
- restart
2304
- } = audio;
2305
- const currentTimeRef = useRef5(currentTime);
2306
- currentTimeRef.current = currentTime;
2307
- const totalDurationRef = useRef5(totalDuration);
2308
- totalDurationRef.current = totalDuration;
2309
- const expandedBlocksLenRef = useRef5(0);
2310
- const handleContainerClick = useCallback4(
2311
- (e) => {
2312
- if (renderMode || isSlideshowMode || isLinearMode) return;
2313
- const target = e.target;
2314
- if (target.closest(
2315
- "button, a, input, .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow"
2316
- ))
2317
- return;
2318
- toggle();
2319
- const nextState = isPlaying ? "play" : "pause";
2320
- setTapFeedback(nextState);
2321
- clearTimeout(tapFeedbackTimer.current);
2322
- tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
2323
- },
2324
- [renderMode, toggle, isPlaying, isSlideshowMode, isLinearMode]
2325
- );
2326
- const {
2327
- currentBlock,
2328
- currentBlockIndex,
2329
- previousBlock,
2330
- isEntering,
2331
- isExiting,
2332
- blockTime,
2333
- blockProgress: _blockProgress,
2334
- docProgress,
2335
- nextBlock: _nextBlock,
2336
- prevBlock: _prevBlock,
2337
- blocks: expandedBlocks
2338
- } = useDocPlayback(script, currentTime, activeViewport, renderMode, theme);
2339
- const coverBlock = useMemo5(() => {
2340
- const startBlockConfig = script.startBlock;
2341
- if (!startBlockConfig) return null;
2342
- const context = createTemplateContext(theme ?? DEFAULT_THEME2, 0, 1, activeViewport);
2343
- const layers = expandCoverBlock(startBlockConfig, context);
2344
- return {
2345
- id: "cover-block",
2346
- startTime: -1,
2347
- // Not part of timeline
2348
- duration: 0,
2349
- // Static
2350
- audioSegment: -1,
2351
- layers
2352
- };
2353
- }, [script.startBlock, activeViewport, theme]);
2354
- const [coverForced, setCoverForced] = useState7(false);
2355
- const [coverGraceActive, setCoverGraceActive] = useState7(false);
2356
- const coverGraceTimer = useRef5();
2357
- const coverWasShowing = useRef5(false);
2358
- const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !renderMode && !autoPlay);
2359
- if (atRest) coverWasShowing.current = true;
2360
- useEffect7(() => {
2361
- if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
2362
- coverWasShowing.current = false;
2363
- setCoverGraceActive(true);
2364
- coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
2365
- return () => clearTimeout(coverGraceTimer.current);
2366
- }
2367
- }, [isPlaying, coverBlock, renderMode]);
2368
- const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !renderMode && !autoPlay);
2369
- const hasAutoPlayed = useRef5(false);
2370
- useEffect7(() => {
2371
- if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
2372
- hasAutoPlayed.current = true;
2373
- play();
2374
- }
2375
- }, [isAudioReady, autoPlay, play]);
2376
- useEffect7(() => {
2377
- onTimeUpdate?.(currentTime);
2378
- }, [currentTime, onTimeUpdate]);
2379
- useEffect7(() => {
2380
- if (isEnded) {
2381
- onEnded?.();
2382
- }
2383
- }, [isEnded, onEnded]);
2384
- useEffect7(() => {
2385
- if ((renderMode || isDebugMode) && typeof window !== "undefined") {
2386
- const w = window;
2387
- w.seekTo = (time) => {
2388
- seekTo(time);
2389
- return new Promise((resolve) => {
2390
- requestAnimationFrame(() => {
2391
- let blockStartTime = 0;
2392
- for (let i = expandedBlocks.length - 1; i >= 0; i--) {
2393
- if (time >= expandedBlocks[i].startTime) {
2394
- blockStartTime = expandedBlocks[i].startTime;
2395
- break;
2396
- }
2397
- }
2398
- const elapsedMs = (time - blockStartTime) * 1e3;
2399
- document.getAnimations().forEach((anim) => {
2400
- const target = anim.effect?.target;
2401
- if (!target) return;
2402
- if (target.closest(".doc-player__block--active")) {
2403
- anim.currentTime = Math.max(0, elapsedMs);
2404
- } else if (target.closest(".doc-player__block--previous")) {
2405
- anim.currentTime = Math.max(0, elapsedMs);
2406
- }
2407
- });
2408
- const blockElapsed = time - blockStartTime;
2409
- const videoSeekPromises = [];
2410
- const activeBlockEl = document.querySelector(".doc-player__block--active");
2411
- if (activeBlockEl) {
2412
- const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
2413
- videos.forEach((el) => {
2414
- const video = el;
2415
- const clipStart = parseFloat(video.dataset.clipStart || "0");
2416
- const clipEnd = parseFloat(video.dataset.clipEnd || "0");
2417
- const targetTime = Math.min(clipStart + Math.max(0, blockElapsed), clipEnd);
2418
- video.pause();
2419
- video.currentTime = targetTime;
2420
- videoSeekPromises.push(
2421
- new Promise((r) => {
2422
- if (Math.abs(video.currentTime - targetTime) < 0.1) {
2423
- r();
2424
- } else {
2425
- video.addEventListener("seeked", () => r(), { once: true });
2426
- setTimeout(r, 200);
2427
- }
2428
- })
2429
- );
2430
- });
2431
- }
2432
- Promise.all(videoSeekPromises).then(() => {
2433
- requestAnimationFrame(() => resolve());
2434
- });
2435
- });
2436
- });
2437
- };
2438
- w.getDuration = () => totalDuration;
2439
- w.getBlocks = () => expandedBlocks.map((s) => ({
2440
- id: s.id,
2441
- template: s.template ?? "raw",
2442
- startTime: s.startTime,
2443
- duration: s.duration
2444
- }));
2445
- w.getAudioSegments = () => script.audio.segments.map((seg) => ({
2446
- src: seg.src,
2447
- name: seg.name,
2448
- duration: seg.duration,
2449
- startTime: seg.startTime
2450
- }));
2451
- w.getCaptions = () => script.captions?.phrases?.map((p) => ({
2452
- text: p.text,
2453
- startTime: p.startTime,
2454
- endTime: p.endTime
2455
- })) || [];
2456
- w.getChapters = () => {
2457
- const titleMap = buildSegmentTitleMap(script);
2458
- return script.audio.segments.map((seg, i) => ({
2459
- title: titleMap.get(i) || seg.name,
2460
- startTime: seg.startTime,
2461
- duration: seg.duration
2462
- }));
2463
- };
2464
- w.showCover = () => {
2465
- setCoverForced(true);
2466
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
2467
- };
2468
- w.hideCover = () => {
2469
- setCoverForced(false);
2470
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
2471
- };
2472
- w.hasCoverBlock = () => !!coverBlock;
2473
- }
2474
- return () => {
2475
- if (typeof window !== "undefined") {
2476
- const w = window;
2477
- delete w.seekTo;
2478
- delete w.getDuration;
2479
- delete w.getBlocks;
2480
- delete w.getAudioSegments;
2481
- delete w.getCaptions;
2482
- delete w.getChapters;
2483
- delete w.showCover;
2484
- delete w.hideCover;
2485
- delete w.hasCoverBlock;
2486
- }
2487
- };
2488
- }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
2489
- const captionsEnabled = captionsEnabledProp !== void 0 ? captionsEnabledProp : true;
2490
- const setCaptionsEnabled = useCallback4(
2491
- (enabled) => {
2492
- onCaptionsToggle?.(enabled);
2493
- },
2494
- [onCaptionsToggle]
2495
- );
2496
- const hasCaptions = script.captions && script.captions.phrases.length > 0;
2497
- const segmentTitleMap = useMemo5(() => buildSegmentTitleMap(script), [script]);
2498
- const playbackState = useMemo5(
2499
- () => ({
2500
- isPlaying,
2501
- currentTime,
2502
- totalDuration,
2503
- currentBlockIndex,
2504
- totalBlocks: expandedBlocks.length,
2505
- docProgress,
2506
- hasCaptions: !!hasCaptions,
2507
- captionsEnabled,
2508
- isFullscreen,
2509
- currentSegmentIndex: currentSegment,
2510
- currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
2511
- currentBlock: currentBlock ?? null
2512
- }),
2513
- // eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
2514
- [
2515
- isPlaying,
2516
- currentTime,
2517
- totalDuration,
2518
- currentBlockIndex,
2519
- expandedBlocks.length,
2520
- docProgress,
2521
- hasCaptions,
2522
- captionsEnabled,
2523
- isFullscreen,
2524
- currentSegment,
2525
- segmentTitleMap,
2526
- currentBlock
2527
- ]
2528
- );
2529
- const playbackActions = useMemo5(
2530
- () => ({
2531
- toggle,
2532
- restart,
2533
- seekTo,
2534
- setCaptionsEnabled,
2535
- toggleFullscreen: onFullscreenToggle
2536
- }),
2537
- [toggle, restart, seekTo, setCaptionsEnabled, onFullscreenToggle]
2538
- );
2539
- const slideNavActions = useMemo5(
2540
- () => ({
2541
- nextSlide: () => {
2542
- if (currentBlockIndex < expandedBlocks.length - 1) {
2543
- const target = expandedBlocks[currentBlockIndex + 1];
2544
- if (target) {
2545
- seekTo(target.startTime);
2546
- pause();
2547
- }
2548
- }
2549
- },
2550
- prevSlide: () => {
2551
- if (currentBlockIndex > 0) {
2552
- const target = expandedBlocks[currentBlockIndex - 1];
2553
- if (target) {
2554
- seekTo(target.startTime);
2555
- pause();
2556
- }
2557
- }
2558
- },
2559
- goToSlide: (index) => {
2560
- if (index >= 0 && index < expandedBlocks.length) {
2561
- const target = expandedBlocks[index];
2562
- if (target) {
2563
- seekTo(target.startTime);
2564
- pause();
2565
- }
2566
- }
2567
- }
2568
- }),
2569
- [currentBlockIndex, expandedBlocks, seekTo, pause]
2570
- );
2571
- useEffect7(() => {
2572
- onPlaybackStateChange?.(playbackState);
2573
- }, [playbackState, onPlaybackStateChange]);
2574
- useEffect7(() => {
2575
- onControlsReady?.({ play, pause, ...playbackActions });
2576
- }, [play, pause, playbackActions, onControlsReady]);
2577
- const getBlockTitle = useCallback4((block) => {
2578
- const docBlock = block;
2579
- if (isTemplateBlock2(docBlock)) {
2580
- const props = docBlock;
2581
- if (typeof props.title === "string") return props.title;
2582
- if (typeof props.stat === "string") return props.stat;
2583
- if (typeof props.quote === "string") {
2584
- const firstLine = props.quote.split("\n")[0];
2585
- if (firstLine.length <= 30) return firstLine;
2586
- return firstLine.slice(0, 27) + "...";
2587
- }
2588
- if (typeof props.date === "string") return props.date;
2589
- if (typeof props.fact === "string") return props.fact;
2590
- }
2591
- if (block.layers && Array.isArray(block.layers)) {
2592
- const textLayer = block.layers.find((l) => l.type === "text");
2593
- if (textLayer?.content?.text) {
2594
- const firstLine = textLayer.content.text.split("\n")[0];
2595
- if (firstLine.length <= 30) return firstLine;
2596
- return firstLine.slice(0, 27) + "...";
2597
- }
2598
- }
2599
- return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2600
- }, []);
2601
- const blockMarkers = useMemo5(() => {
2602
- if (!totalDuration || !expandedBlocks.length) return [];
2603
- let prevSegment = -1;
2604
- return expandedBlocks.map((block, index) => {
2605
- const isSectionStart = block.audioSegment !== prevSegment;
2606
- prevSegment = block.audioSegment;
2607
- return {
2608
- block,
2609
- index,
2610
- position: block.startTime / totalDuration * 100,
2611
- title: getBlockTitle(block),
2612
- isSectionStart
2613
- };
2614
- });
2615
- }, [expandedBlocks, totalDuration, getBlockTitle]);
2616
- useEffect7(() => {
2617
- if (blockMarkers.length > 0) {
2618
- onBlockMarkers?.(blockMarkers);
2619
- }
2620
- }, [blockMarkers, onBlockMarkers]);
2621
- expandedBlocksLenRef.current = expandedBlocks.length;
2622
- const handleKeyDown = useCallback4(
2623
- (e) => {
2624
- const activeEl = document.activeElement;
2625
- if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
2626
- return;
2627
- }
2628
- if (isLinearMode) return;
2629
- if (isSlideshowMode) {
2630
- switch (e.key) {
2631
- case "ArrowRight":
2632
- case "ArrowDown":
2633
- case " ":
2634
- e.preventDefault();
2635
- slideNavActions.nextSlide();
2636
- break;
2637
- case "ArrowLeft":
2638
- case "ArrowUp":
2639
- e.preventDefault();
2640
- slideNavActions.prevSlide();
2641
- break;
2642
- case "Home":
2643
- e.preventDefault();
2644
- slideNavActions.goToSlide(0);
2645
- break;
2646
- case "End":
2647
- e.preventDefault();
2648
- slideNavActions.goToSlide(expandedBlocksLenRef.current - 1);
2649
- break;
2650
- }
2651
- } else {
2652
- switch (e.key) {
2653
- case " ":
2654
- e.preventDefault();
2655
- toggle();
2656
- break;
2657
- case "ArrowRight":
2658
- seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
2659
- break;
2660
- case "ArrowLeft":
2661
- seekTo(Math.max(currentTimeRef.current - 10, 0));
2662
- break;
2663
- }
2664
- }
2665
- },
2666
- [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
2667
- );
2668
- useEffect7(() => {
2669
- if (renderMode) return;
2670
- window.addEventListener("keydown", handleKeyDown);
2671
- return () => window.removeEventListener("keydown", handleKeyDown);
2672
- }, [handleKeyDown, renderMode]);
2673
- if (isLinearMode) {
2674
- return /* @__PURE__ */ jsx13(
2675
- "div",
2676
- {
2677
- ref: containerRef,
2678
- className: "doc-player doc-player--linear",
2679
- style: {
2680
- position: "relative",
2681
- width: "100%",
2682
- height: "100%",
2683
- overflow: "hidden"
2684
- },
2685
- children: /* @__PURE__ */ jsx13(LinearDocView, { doc: script, basePath, viewport: activeViewport })
2686
- }
2687
- );
2688
- }
2689
- return /* @__PURE__ */ jsxs10(
2690
- "div",
2691
- {
2692
- ref: containerRef,
2693
- className: "doc-player",
2694
- onClick: handleContainerClick,
2695
- style: {
2696
- position: "relative",
2697
- width: "100%",
2698
- aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
2699
- margin: "0 auto",
2700
- overflow: "hidden",
2701
- cursor: renderMode ? void 0 : "pointer"
2702
- },
2703
- children: [
2704
- /* @__PURE__ */ jsx13("audio", { ref: audioRef, preload: "auto", muted }),
2705
- /* @__PURE__ */ jsxs10("div", { className: "doc-player__viewport", children: [
2706
- showCoverBlock && coverBlock && /* @__PURE__ */ jsx13("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx13(
2707
- BlockRenderer,
2708
- {
2709
- block: coverBlock,
2710
- blockTime: 0,
2711
- basePath,
2712
- isEntering: false,
2713
- viewport: activeViewport
2714
- }
2715
- ) }),
2716
- !showCoverBlock && previousBlock && isExiting && /* @__PURE__ */ jsx13("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx13(
2717
- BlockRenderer,
2718
- {
2719
- block: previousBlock,
2720
- blockTime,
2721
- basePath,
2722
- isExiting: true,
2723
- viewport: activeViewport
2724
- }
2725
- ) }),
2726
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx13("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx13(
2727
- BlockRenderer,
2728
- {
2729
- block: currentBlock,
2730
- blockTime,
2731
- basePath,
2732
- isEntering,
2733
- viewport: activeViewport,
2734
- isPlaying
2735
- }
2736
- ) }),
2737
- hasCaptions && !renderMode && /* @__PURE__ */ jsx13(
2738
- CaptionOverlay,
2739
- {
2740
- captions: script.captions,
2741
- currentTime,
2742
- enabled: captionsEnabled && (isPlaying || currentTime > 0),
2743
- fontSize: 16
2744
- }
2745
- ),
2746
- isDebugMode && /* @__PURE__ */ jsxs10(
2747
- "div",
2748
- {
2749
- className: "doc-player__debug",
2750
- style: {
2751
- position: "absolute",
2752
- top: "8px",
2753
- right: "8px",
2754
- padding: "8px 12px",
2755
- background: "rgba(0, 0, 0, 0.85)",
2756
- borderRadius: "6px",
2757
- color: "#00ff00",
2758
- fontFamily: "monospace",
2759
- fontSize: "11px",
2760
- lineHeight: "1.5",
2761
- zIndex: 200,
2762
- maxWidth: "280px",
2763
- pointerEvents: "none",
2764
- textAlign: "left"
2765
- },
2766
- children: [
2767
- /* @__PURE__ */ jsx13("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
2768
- /* @__PURE__ */ jsxs10("div", { children: [
2769
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "template:" }),
2770
- " ",
2771
- /* @__PURE__ */ jsx13("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
2772
- ] }),
2773
- /* @__PURE__ */ jsxs10("div", { children: [
2774
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "block:" }),
2775
- " ",
2776
- currentBlockIndex + 1,
2777
- "/",
2778
- expandedBlocks.length,
2779
- " ",
2780
- /* @__PURE__ */ jsxs10("span", { style: { color: "#666" }, children: [
2781
- "(",
2782
- currentBlock?.id || "none",
2783
- ")"
2784
- ] })
2785
- ] }),
2786
- /* @__PURE__ */ jsxs10("div", { children: [
2787
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "time:" }),
2788
- " ",
2789
- currentTime.toFixed(2),
2790
- "s /",
2791
- " ",
2792
- totalDuration.toFixed(1),
2793
- "s"
2794
- ] }),
2795
- /* @__PURE__ */ jsxs10("div", { children: [
2796
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "blockTime:" }),
2797
- " ",
2798
- blockTime.toFixed(2),
2799
- "s /",
2800
- " ",
2801
- (currentBlock?.duration || 0).toFixed(1),
2802
- "s"
2803
- ] }),
2804
- /* @__PURE__ */ jsxs10("div", { children: [
2805
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "segment:" }),
2806
- " ",
2807
- currentSegment,
2808
- "/",
2809
- script.audio.segments.length - 1,
2810
- " ",
2811
- /* @__PURE__ */ jsxs10("span", { style: { color: "#666" }, children: [
2812
- "(",
2813
- script.audio.segments[currentSegment]?.name || "none",
2814
- ")"
2815
- ] })
2816
- ] }),
2817
- /* @__PURE__ */ jsxs10("div", { children: [
2818
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "viewport:" }),
2819
- " ",
2820
- activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
2821
- " ",
2822
- /* @__PURE__ */ jsxs10("span", { style: { color: "#666" }, children: [
2823
- "(",
2824
- orientation,
2825
- ")"
2826
- ] })
2827
- ] }),
2828
- /* @__PURE__ */ jsxs10("div", { children: [
2829
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "playing:" }),
2830
- " ",
2831
- /* @__PURE__ */ jsx13("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
2832
- showCoverBlock && /* @__PURE__ */ jsx13("span", { style: { color: "#60a5fa" }, children: " (cover)" })
2833
- ] }),
2834
- hasCaptions && (() => {
2835
- const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
2836
- const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
2837
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
2838
- /* @__PURE__ */ jsxs10("div", { children: [
2839
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "captions:" }),
2840
- " ",
2841
- script.captions?.phrases.length || 0,
2842
- " phrases",
2843
- " ",
2844
- /* @__PURE__ */ jsxs10("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
2845
- "(",
2846
- captionsEnabled ? "on" : "off",
2847
- ")"
2848
- ] })
2849
- ] }),
2850
- /* @__PURE__ */ jsxs10("div", { children: [
2851
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "cc.enabled:" }),
2852
- " ",
2853
- /* @__PURE__ */ jsx13("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
2854
- " ",
2855
- /* @__PURE__ */ jsxs10("span", { style: { color: "#666" }, children: [
2856
- "(playing=",
2857
- String(isPlaying),
2858
- " t>0=",
2859
- String(currentTime > 0),
2860
- ")"
2861
- ] })
2862
- ] }),
2863
- /* @__PURE__ */ jsxs10("div", { children: [
2864
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "cc.phrase:" }),
2865
- " ",
2866
- /* @__PURE__ */ jsx13("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
2867
- ] }),
2868
- debugPhrase && /* @__PURE__ */ jsxs10("div", { children: [
2869
- /* @__PURE__ */ jsx13("span", { style: { color: "#888" }, children: "cc.range:" }),
2870
- " ",
2871
- /* @__PURE__ */ jsxs10("span", { style: { color: "#60a5fa" }, children: [
2872
- debugPhrase.startTime.toFixed(2),
2873
- "-",
2874
- debugPhrase.endTime.toFixed(2)
2875
- ] })
2876
- ] })
2877
- ] });
2878
- })()
2879
- ]
2880
- }
2881
- )
2882
- ] }),
2883
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs10(
2884
- "div",
2885
- {
2886
- className: "doc-player__unavailable",
2887
- style: {
2888
- position: "absolute",
2889
- top: 0,
2890
- left: 0,
2891
- right: 0,
2892
- bottom: 0,
2893
- display: "flex",
2894
- alignItems: "center",
2895
- justifyContent: "center",
2896
- flexDirection: "column",
2897
- gap: "16px",
2898
- background: "rgba(0, 0, 0, 0.7)",
2899
- color: "rgba(255, 255, 255, 0.9)",
2900
- fontSize: "14px",
2901
- zIndex: 50
2902
- },
2903
- children: [
2904
- /* @__PURE__ */ jsx13("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
2905
- /* @__PURE__ */ jsx13("span", { children: unavailableMessage })
2906
- ]
2907
- }
2908
- ),
2909
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx13(
2910
- DocControlsOverlay,
2911
- {
2912
- state: playbackState,
2913
- actions: playbackActions,
2914
- blockMarkers,
2915
- expandedBlocks,
2916
- getBlockTitle
2917
- }
2918
- ),
2919
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx13(
2920
- "div",
2921
- {
2922
- className: "doc-player__scrubber",
2923
- style: {
2924
- position: "absolute",
2925
- bottom: 0,
2926
- left: 0,
2927
- right: 0,
2928
- padding: "12px 16px 8px",
2929
- background: "linear-gradient(transparent, rgba(0,0,0,0.6))",
2930
- display: "flex",
2931
- alignItems: "center",
2932
- zIndex: 100
2933
- },
2934
- children: /* @__PURE__ */ jsx13(
2935
- DocProgressBar,
2936
- {
2937
- state: playbackState,
2938
- actions: playbackActions,
2939
- blockMarkers,
2940
- expandedBlocks,
2941
- getBlockTitle
2942
- }
2943
- )
2944
- }
2945
- ),
2946
- !renderMode && isSlideshowMode && /* @__PURE__ */ jsx13(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
2947
- !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx13("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx13("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx13("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx13("path", { d: "M8 5v14l11-7z" }) }) }, Date.now())
2948
- ]
2949
- }
2950
- );
2951
- }
2952
-
2953
- // src/DocControlsBottom.tsx
2954
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
2955
- function DocControlsBottom({
2956
- state,
2957
- actions,
2958
- blockMarkers,
2959
- expandedBlocks,
2960
- getBlockTitle
2961
- }) {
2962
- return /* @__PURE__ */ jsxs11("div", { className: "doc-controls-bottom", children: [
2963
- /* @__PURE__ */ jsx14(
2964
- "button",
2965
- {
2966
- className: "bottom-ctrl-btn",
2967
- onClick: actions.restart,
2968
- title: "Restart",
2969
- "aria-label": "Restart from beginning",
2970
- children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx14("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
2971
- }
2972
- ),
2973
- /* @__PURE__ */ jsx14(
2974
- "button",
2975
- {
2976
- className: "bottom-ctrl-btn bottom-play-btn",
2977
- onClick: actions.toggle,
2978
- "aria-label": state.isPlaying ? "Pause" : "Play",
2979
- children: state.isPlaying ? /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx14("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx14("path", { d: "M8 5v14l11-7z" }) })
2980
- }
2981
- ),
2982
- /* @__PURE__ */ jsxs11("span", { className: "bottom-time", children: [
2983
- formatTime(state.currentTime),
2984
- " / ",
2985
- formatTime(state.totalDuration)
2986
- ] }),
2987
- /* @__PURE__ */ jsx14(
2988
- DocProgressBar,
2989
- {
2990
- state,
2991
- actions,
2992
- blockMarkers,
2993
- expandedBlocks,
2994
- getBlockTitle
2995
- }
2996
- ),
2997
- /* @__PURE__ */ jsxs11("span", { className: "bottom-segment", children: [
2998
- state.currentBlockIndex + 1,
2999
- "/",
3000
- state.totalBlocks
3001
- ] }),
3002
- state.hasCaptions && /* @__PURE__ */ jsx14(
3003
- "button",
3004
- {
3005
- className: `bottom-ctrl-btn ${state.captionsEnabled ? "bottom-ctrl-btn--active" : ""}`,
3006
- onClick: () => actions.setCaptionsEnabled(!state.captionsEnabled),
3007
- title: state.captionsEnabled ? "Hide captions" : "Show captions",
3008
- "aria-label": state.captionsEnabled ? "Hide captions" : "Show captions",
3009
- children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx14("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
3010
- }
3011
- )
3012
- ] });
3013
- }
3014
-
3015
- // src/DocControlsSidebar.tsx
3016
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3017
- function DocControlsSidebar({ state, actions }) {
3018
- return /* @__PURE__ */ jsxs12("div", { className: "doc-controls-sidebar", children: [
3019
- /* @__PURE__ */ jsx15(
3020
- "button",
3021
- {
3022
- className: "sidebar-ctrl-btn",
3023
- onClick: actions.restart,
3024
- title: "Restart",
3025
- "aria-label": "Restart from beginning",
3026
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("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" }) })
3027
- }
3028
- ),
3029
- /* @__PURE__ */ jsx15(
3030
- "button",
3031
- {
3032
- className: "sidebar-ctrl-btn sidebar-play-btn",
3033
- onClick: actions.toggle,
3034
- "aria-label": state.isPlaying ? "Pause" : "Play",
3035
- children: state.isPlaying ? /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx15("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx15("path", { d: "M8 5v14l11-7z" }) })
3036
- }
3037
- ),
3038
- /* @__PURE__ */ jsxs12("div", { className: "sidebar-time", children: [
3039
- /* @__PURE__ */ jsx15("div", { children: formatTime(state.currentTime) }),
3040
- /* @__PURE__ */ jsx15("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
3041
- ] }),
3042
- /* @__PURE__ */ jsxs12("div", { className: "sidebar-segment", children: [
3043
- state.currentBlockIndex + 1,
3044
- "/",
3045
- state.totalBlocks
3046
- ] }),
3047
- state.hasCaptions && /* @__PURE__ */ jsx15(
3048
- "button",
3049
- {
3050
- className: `sidebar-ctrl-btn ${state.captionsEnabled ? "sidebar-ctrl-btn--active" : ""}`,
3051
- onClick: () => actions.setCaptionsEnabled(!state.captionsEnabled),
3052
- title: state.captionsEnabled ? "Hide captions" : "Show captions",
3053
- "aria-label": state.captionsEnabled ? "Hide captions" : "Show captions",
3054
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("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" }) })
3055
- }
3056
- ),
3057
- actions.toggleFullscreen && /* @__PURE__ */ jsx15(
3058
- "button",
3059
- {
3060
- className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
3061
- onClick: actions.toggleFullscreen,
3062
- title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
3063
- "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
3064
- children: state.isFullscreen ? /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
3065
- }
3066
- )
3067
- ] });
3068
- }
3069
-
3070
- // src/DocPlayerWithSidebar.tsx
3071
- import { useRef as useRef6, useState as useState8, useCallback as useCallback5, useEffect as useEffect8 } from "react";
3072
- import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
3073
- var DEFAULT_STATE = {
3074
- isPlaying: false,
3075
- currentTime: 0,
3076
- totalDuration: 0,
3077
- currentBlockIndex: 0,
3078
- totalBlocks: 0,
3079
- docProgress: 0,
3080
- hasCaptions: false,
3081
- captionsEnabled: false,
3082
- currentSegmentIndex: 0,
3083
- currentSegmentName: null,
3084
- currentBlock: null
3085
- };
3086
- function DocPlayerWithSidebar({
3087
- script,
3088
- basePath,
3089
- autoPlay = false,
3090
- onEnded,
3091
- onTimeUpdate,
3092
- audioProvider,
3093
- muted,
3094
- captionsEnabled,
3095
- isFullscreen,
3096
- onFullscreenToggle,
3097
- forceViewport,
3098
- onPlayingChange
3099
- }) {
3100
- const stateRef = useRef6(DEFAULT_STATE);
3101
- const actionsRef = useRef6(null);
3102
- const wasPlayingRef = useRef6(false);
3103
- const [, setTick] = useState8(0);
3104
- const handleStateChange = useCallback5(
3105
- (state) => {
3106
- stateRef.current = state;
3107
- if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
3108
- wasPlayingRef.current = state.isPlaying;
3109
- onPlayingChange(state.isPlaying);
3110
- }
3111
- },
3112
- [onPlayingChange]
3113
- );
3114
- const handleControlsReady = useCallback5(
3115
- (controls) => {
3116
- const isFirst = !actionsRef.current;
3117
- actionsRef.current = controls;
3118
- if (isFirst) setTick((t) => t + 1);
3119
- },
3120
- []
3121
- );
3122
- useEffect8(() => {
3123
- const interval = setInterval(() => {
3124
- setTick((t) => t + 1);
3125
- }, 250);
3126
- return () => clearInterval(interval);
3127
- }, []);
3128
- return /* @__PURE__ */ jsxs13("div", { className: "doc-player-sidebar-layout", children: [
3129
- /* @__PURE__ */ jsx16("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx16(
3130
- DocPlayer,
3131
- {
3132
- script,
3133
- basePath,
3134
- autoPlay,
3135
- onEnded,
3136
- onTimeUpdate,
3137
- audioProvider,
3138
- muted,
3139
- captionsEnabled,
3140
- showControls: isFullscreen,
3141
- showScrubber: !isFullscreen,
3142
- onPlaybackStateChange: handleStateChange,
3143
- onControlsReady: handleControlsReady,
3144
- isFullscreen,
3145
- onFullscreenToggle,
3146
- forceViewport
3147
- }
3148
- ) }),
3149
- actionsRef.current && /* @__PURE__ */ jsx16(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
3150
- ] });
3151
- }
3152
- export {
3153
- BlockRenderer,
3154
- CaptionOverlay,
3155
- DocControlsBottom,
3156
- DocControlsOverlay,
3157
- DocControlsSidebar,
3158
- DocControlsSlideshow,
3159
- DocPlayer,
3160
- DocPlayerWithSidebar,
3161
- DocProgressBar,
3162
- ImageLayer,
3163
- LinearDocView,
3164
- MapLayer,
3165
- MarkdownRenderer,
3166
- MediaContext,
3167
- ShapeLayer,
3168
- TextLayer,
3169
- VIEWPORT,
3170
- VideoLayer,
3171
- formatTime,
3172
- getAnimationStyle,
3173
- getTransitionClass,
3174
- useAudioSync,
3175
- useDocPlayback,
3176
- useMediaProvider,
3177
- useMediaUrl,
3178
- useViewportOrientation
3179
- };
1
+ // Main components
2
+ export { DocPlayer } from './DocPlayer.js';
3
+ export { BlockRenderer, VIEWPORT } from './BlockRenderer.js';
4
+ export { CaptionOverlay } from './CaptionOverlay.js';
5
+ export { DocControlsOverlay } from './DocControlsOverlay.js';
6
+ export { DocControlsBottom } from './DocControlsBottom.js';
7
+ export { DocControlsSidebar } from './DocControlsSidebar.js';
8
+ export { DocControlsSlideshow } from './DocControlsSlideshow.js';
9
+ export { DocPlayerWithSidebar } from './DocPlayerWithSidebar.js';
10
+ export { DocProgressBar } from './DocProgressBar.js';
11
+ export { MarkdownRenderer } from './MarkdownRenderer.js';
12
+ export { LinearDocView } from './LinearDocView.js';
13
+ // Layer components
14
+ export { ImageLayer } from './layers/ImageLayer.js';
15
+ export { TextLayer } from './layers/TextLayer.js';
16
+ export { ShapeLayer } from './layers/ShapeLayer.js';
17
+ export { VideoLayer } from './layers/VideoLayer.js';
18
+ export { MapLayer } from './layers/MapLayer.js';
19
+ // Hooks
20
+ export { useAudioSync } from './hooks/useAudioSync.js';
21
+ export { useDocPlayback } from './hooks/useDocPlayback.js';
22
+ export { useViewportOrientation } from './hooks/useViewportOrientation.js';
23
+ export { MediaContext, useMediaProvider, useMediaUrl } from './hooks/MediaContext.js';
24
+ export { formatTime } from './types.js';
25
+ // Utilities
26
+ export { getAnimationStyle, getTransitionClass } from './utils/animationUtils.js';
3180
27
  //# sourceMappingURL=index.js.map