@bendyline/squisq 0.1.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 (102) hide show
  1. package/dist/Types-sh2VRxfo.d.ts +15 -0
  2. package/dist/chunk-7UDSRZKG.js +103 -0
  3. package/dist/chunk-7UDSRZKG.js.map +1 -0
  4. package/dist/chunk-O5HCIUAD.js +577 -0
  5. package/dist/chunk-O5HCIUAD.js.map +1 -0
  6. package/dist/chunk-PJ7AODIM.js +3040 -0
  7. package/dist/chunk-PJ7AODIM.js.map +1 -0
  8. package/dist/chunk-QWVRN6I4.js +81 -0
  9. package/dist/chunk-QWVRN6I4.js.map +1 -0
  10. package/dist/chunk-S4F2RY37.js +92 -0
  11. package/dist/chunk-S4F2RY37.js.map +1 -0
  12. package/dist/chunk-URU6QMLY.js +888 -0
  13. package/dist/chunk-URU6QMLY.js.map +1 -0
  14. package/dist/chunk-VJN7UB2Z.js +145 -0
  15. package/dist/chunk-VJN7UB2Z.js.map +1 -0
  16. package/dist/doc/index.d.ts +3 -0
  17. package/dist/doc/index.js +126 -0
  18. package/dist/doc/index.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.js +217 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/markdown/index.d.ts +237 -0
  23. package/dist/markdown/index.js +33 -0
  24. package/dist/markdown/index.js.map +1 -0
  25. package/dist/schemas/index.d.ts +70 -0
  26. package/dist/schemas/index.js +61 -0
  27. package/dist/schemas/index.js.map +1 -0
  28. package/dist/spatial/index.d.ts +111 -0
  29. package/dist/spatial/index.js +25 -0
  30. package/dist/spatial/index.js.map +1 -0
  31. package/dist/storage/index.d.ts +91 -0
  32. package/dist/storage/index.js +11 -0
  33. package/dist/storage/index.js.map +1 -0
  34. package/dist/story/index.d.ts +691 -0
  35. package/dist/story/index.js +126 -0
  36. package/dist/story/index.js.map +1 -0
  37. package/dist/themeLibrary-DySHPcgj.d.ts +1352 -0
  38. package/dist/types-DHiv_Pnm.d.ts +496 -0
  39. package/package.json +91 -0
  40. package/src/__tests__/animationUtils.test.ts +77 -0
  41. package/src/__tests__/geohash.test.ts +109 -0
  42. package/src/__tests__/getLayers.test.ts +200 -0
  43. package/src/__tests__/haversine.test.ts +63 -0
  44. package/src/__tests__/localForageAdapter.test.ts +155 -0
  45. package/src/__tests__/markdown.test.ts +806 -0
  46. package/src/__tests__/markdownToDoc.test.ts +374 -0
  47. package/src/__tests__/storage.test.ts +60 -0
  48. package/src/__tests__/templates.test.ts +247 -0
  49. package/src/doc/docToMarkdown.ts +98 -0
  50. package/src/doc/getLayers.ts +152 -0
  51. package/src/doc/index.ts +8 -0
  52. package/src/doc/markdownToDoc.ts +277 -0
  53. package/src/doc/templates/accentImage.ts +338 -0
  54. package/src/doc/templates/captionUtils.ts +33 -0
  55. package/src/doc/templates/comparisonBar.ts +140 -0
  56. package/src/doc/templates/coverBlock.ts +156 -0
  57. package/src/doc/templates/dateEvent.ts +122 -0
  58. package/src/doc/templates/definitionCard.ts +129 -0
  59. package/src/doc/templates/factCard.ts +114 -0
  60. package/src/doc/templates/fullBleedQuote.ts +59 -0
  61. package/src/doc/templates/imageWithCaption.ts +167 -0
  62. package/src/doc/templates/index.ts +571 -0
  63. package/src/doc/templates/listBlock.ts +101 -0
  64. package/src/doc/templates/mapBlock.ts +132 -0
  65. package/src/doc/templates/persistentLayers.ts +522 -0
  66. package/src/doc/templates/photoGrid.ts +199 -0
  67. package/src/doc/templates/pullQuote.ts +119 -0
  68. package/src/doc/templates/quoteBlock.ts +112 -0
  69. package/src/doc/templates/sectionHeader.ts +105 -0
  70. package/src/doc/templates/statHighlight.ts +113 -0
  71. package/src/doc/templates/titleBlock.ts +100 -0
  72. package/src/doc/templates/twoColumn.ts +239 -0
  73. package/src/doc/templates/videoPullQuote.ts +128 -0
  74. package/src/doc/templates/videoWithCaption.ts +126 -0
  75. package/src/doc/utils/animationUtils.ts +135 -0
  76. package/src/doc/utils/themeUtils.ts +139 -0
  77. package/src/index.ts +5 -0
  78. package/src/markdown/convert.ts +897 -0
  79. package/src/markdown/htmlParse.ts +212 -0
  80. package/src/markdown/index.ts +110 -0
  81. package/src/markdown/parse.ts +103 -0
  82. package/src/markdown/stringify.ts +122 -0
  83. package/src/markdown/types.ts +605 -0
  84. package/src/markdown/utils.ts +165 -0
  85. package/src/ngeohash.d.ts +11 -0
  86. package/src/schemas/BlockTemplates.ts +706 -0
  87. package/src/schemas/Doc.ts +565 -0
  88. package/src/schemas/LayoutStrategy.ts +178 -0
  89. package/src/schemas/MediaProvider.ts +70 -0
  90. package/src/schemas/Theme.ts +235 -0
  91. package/src/schemas/Types.ts +15 -0
  92. package/src/schemas/Viewport.ts +91 -0
  93. package/src/schemas/index.ts +14 -0
  94. package/src/schemas/themeLibrary.ts +492 -0
  95. package/src/spatial/Geohash.ts +168 -0
  96. package/src/spatial/Haversine.ts +59 -0
  97. package/src/spatial/index.ts +2 -0
  98. package/src/storage/LocalForageAdapter.ts +103 -0
  99. package/src/storage/LocalStorageAdapter.ts +57 -0
  100. package/src/storage/MemoryStorageAdapter.ts +37 -0
  101. package/src/storage/Storage.ts +28 -0
  102. package/src/storage/index.ts +5 -0
@@ -0,0 +1,3040 @@
1
+ import {
2
+ DEFAULT_THEME,
3
+ VIEWPORT_PRESETS,
4
+ createTemplateContext,
5
+ getTwoColumnPositions,
6
+ isPersistentLayerTemplate,
7
+ isTemplateBlock,
8
+ scaledFontSize2 as scaledFontSize
9
+ } from "./chunk-O5HCIUAD.js";
10
+ import {
11
+ extractPlainText
12
+ } from "./chunk-QWVRN6I4.js";
13
+
14
+ // src/doc/templates/persistentLayers.ts
15
+ var GRADIENT_PRESETS = {
16
+ "dark-vignette": "radial-gradient(ellipse at center, rgba(26,32,44,0.8) 0%, rgba(0,0,0,0.95) 100%)",
17
+ "radial-dark": "radial-gradient(ellipse at center, #1a202c 0%, #000000 100%)",
18
+ "warm-sunset": "linear-gradient(135deg, rgba(124,58,48,0.9) 0%, rgba(26,32,44,0.95) 100%)",
19
+ "cool-blue": "linear-gradient(135deg, rgba(26,54,93,0.9) 0%, rgba(26,32,44,0.95) 100%)",
20
+ "earth-tones": "linear-gradient(135deg, rgba(68,51,34,0.9) 0%, rgba(26,32,44,0.95) 100%)"
21
+ };
22
+ function expandSolidBackground(config) {
23
+ return {
24
+ type: "shape",
25
+ id: "persistent-bg-solid",
26
+ content: {
27
+ shape: "rect",
28
+ fill: config.color
29
+ },
30
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
31
+ };
32
+ }
33
+ function expandGradientBackground(config) {
34
+ const gradient = config.gradient ?? GRADIENT_PRESETS[config.preset ?? "dark-vignette"];
35
+ return {
36
+ type: "shape",
37
+ id: "persistent-bg-gradient",
38
+ content: {
39
+ shape: "rect",
40
+ fill: gradient
41
+ },
42
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
43
+ };
44
+ }
45
+ function expandImageBackground(config) {
46
+ const layers = [];
47
+ const opacity = config.opacity ?? 0.4;
48
+ const imageLayer = {
49
+ type: "image",
50
+ id: "persistent-bg-image",
51
+ content: {
52
+ src: config.src,
53
+ alt: "Background",
54
+ fit: "cover"
55
+ },
56
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
57
+ animation: config.ambientMotion ? {
58
+ type: config.ambientMotion,
59
+ duration: 30
60
+ // Long ambient motion
61
+ } : void 0
62
+ };
63
+ layers.push(imageLayer);
64
+ layers.push({
65
+ type: "shape",
66
+ id: "persistent-bg-overlay",
67
+ content: {
68
+ shape: "rect",
69
+ fill: `rgba(0,0,0,${0.5 + (1 - opacity) * 0.3})`
70
+ },
71
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
72
+ });
73
+ return layers;
74
+ }
75
+ function expandPatternBackground(config) {
76
+ const opacity = config.opacity ?? 0.1;
77
+ const color = config.color ?? `rgba(255,255,255,${opacity})`;
78
+ return {
79
+ type: "shape",
80
+ id: "persistent-bg-pattern",
81
+ content: {
82
+ shape: "rect",
83
+ fill: color
84
+ },
85
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
86
+ };
87
+ }
88
+ function expandTitleCaption(config) {
89
+ const layers = [];
90
+ const fontSize = config.fontSize ?? 18;
91
+ const hasSubtitle = !!config.subtitle;
92
+ const isBottom = config.position.includes("bottom");
93
+ const isLeft = config.position.includes("left");
94
+ const pad = 3;
95
+ const pillHeight = hasSubtitle ? "15%" : "7%";
96
+ const bgYPos = isBottom ? hasSubtitle ? "78%" : "84%" : "2%";
97
+ const bgXPos = isLeft ? `${pad}%` : "68%";
98
+ const thumbW = hasSubtitle ? "6%" : "5%";
99
+ const thumbH = hasSubtitle ? "10%" : "5%";
100
+ const thumbPad = 1.5;
101
+ const textX = config.showThumbnail && isLeft ? `${pad + thumbPad + 6 + thumbPad}%` : `${pad + thumbPad}%`;
102
+ const textWidth = config.showThumbnail ? "19%" : "24%";
103
+ const titleYPos = isBottom ? hasSubtitle ? "80%" : "86%" : "4%";
104
+ layers.push({
105
+ type: "shape",
106
+ id: "persistent-caption-bg",
107
+ content: {
108
+ shape: "rect",
109
+ fill: "rgba(0,0,0,0.6)",
110
+ borderRadius: 8
111
+ },
112
+ position: {
113
+ x: bgXPos,
114
+ y: bgYPos,
115
+ width: config.showThumbnail ? "30%" : "28%",
116
+ height: pillHeight
117
+ }
118
+ });
119
+ if (config.showThumbnail && config.thumbnailSrc) {
120
+ const thumbX = isLeft ? `${pad + thumbPad}%` : "93%";
121
+ const thumbY = isBottom ? hasSubtitle ? `${78 + thumbPad}%` : "85%" : "3%";
122
+ layers.push({
123
+ type: "image",
124
+ id: "persistent-caption-thumb",
125
+ content: {
126
+ src: config.thumbnailSrc,
127
+ alt: "Article thumbnail",
128
+ fit: "cover"
129
+ },
130
+ position: {
131
+ x: thumbX,
132
+ y: thumbY,
133
+ width: thumbW,
134
+ height: thumbH
135
+ }
136
+ });
137
+ }
138
+ layers.push({
139
+ type: "text",
140
+ id: "persistent-caption-title",
141
+ content: {
142
+ text: config.title,
143
+ style: {
144
+ fontSize,
145
+ fontWeight: "bold",
146
+ color: "rgba(255,255,255,0.9)",
147
+ textAlign: isLeft ? "left" : "right",
148
+ maxLines: 1
149
+ }
150
+ },
151
+ position: {
152
+ x: textX,
153
+ y: titleYPos,
154
+ width: textWidth
155
+ }
156
+ });
157
+ if (hasSubtitle) {
158
+ const subtitleYPos = isBottom ? "85.5%" : "8%";
159
+ layers.push({
160
+ type: "text",
161
+ id: "persistent-caption-subtitle",
162
+ content: {
163
+ text: config.subtitle,
164
+ style: {
165
+ fontSize: 18,
166
+ fontWeight: "normal",
167
+ color: "rgba(255,255,255,0.55)",
168
+ textAlign: isLeft ? "left" : "right"
169
+ }
170
+ },
171
+ position: {
172
+ x: textX,
173
+ y: subtitleYPos,
174
+ width: textWidth
175
+ }
176
+ });
177
+ }
178
+ return layers;
179
+ }
180
+ function expandCornerBranding(config) {
181
+ const isBottom = config.position.includes("bottom");
182
+ const isLeft = config.position.includes("left");
183
+ const xPos = isLeft ? "3%" : "90%";
184
+ const yPos = isBottom ? "94%" : "3%";
185
+ if (config.isImage) {
186
+ return {
187
+ type: "image",
188
+ id: "persistent-branding",
189
+ content: {
190
+ src: config.content,
191
+ alt: "Branding",
192
+ fit: "contain"
193
+ },
194
+ position: {
195
+ x: xPos,
196
+ y: yPos,
197
+ width: "8%",
198
+ height: "4%"
199
+ }
200
+ };
201
+ }
202
+ return {
203
+ type: "text",
204
+ id: "persistent-branding",
205
+ content: {
206
+ text: config.content,
207
+ style: {
208
+ fontSize: 14,
209
+ fontWeight: "normal",
210
+ color: "rgba(255,255,255,0.6)"
211
+ }
212
+ },
213
+ position: {
214
+ x: xPos,
215
+ y: yPos
216
+ }
217
+ };
218
+ }
219
+ function expandProgressIndicator(config) {
220
+ const color = config.color ?? "rgba(255,255,255,0.3)";
221
+ const y = config.position === "top" ? "1%" : "98%";
222
+ return {
223
+ type: "shape",
224
+ id: "persistent-progress",
225
+ content: {
226
+ shape: "rect",
227
+ fill: color,
228
+ borderRadius: 2
229
+ },
230
+ position: {
231
+ x: "5%",
232
+ y,
233
+ width: "90%",
234
+ height: "0.5%"
235
+ }
236
+ };
237
+ }
238
+ function expandPersistentLayer(layer) {
239
+ if (!isPersistentLayerTemplate(layer)) {
240
+ return [layer];
241
+ }
242
+ const template = layer;
243
+ const config = template.config;
244
+ switch (config.type) {
245
+ case "solidBackground":
246
+ return [expandSolidBackground(config)];
247
+ case "gradientBackground":
248
+ return [expandGradientBackground(config)];
249
+ case "imageBackground":
250
+ return expandImageBackground(config);
251
+ case "patternBackground":
252
+ return [expandPatternBackground(config)];
253
+ case "titleCaption":
254
+ return expandTitleCaption(config);
255
+ case "cornerBranding":
256
+ return [expandCornerBranding(config)];
257
+ case "progressIndicator":
258
+ return [expandProgressIndicator(config)];
259
+ default:
260
+ console.warn("Unknown persistent layer template:", config.type);
261
+ return [];
262
+ }
263
+ }
264
+ function expandPersistentLayers(layers) {
265
+ if (!layers || layers.length === 0) {
266
+ return [];
267
+ }
268
+ return layers.reduce((acc, layer) => acc.concat(expandPersistentLayer(layer)), []);
269
+ }
270
+ function getDocStyleConfig(preset, articleTitle, heroSrc, subtitle) {
271
+ switch (preset) {
272
+ case "minimal":
273
+ return {};
274
+ case "documentary":
275
+ return {
276
+ bottomLayers: [
277
+ {
278
+ template: "gradientBackground",
279
+ config: { type: "gradientBackground", preset: "dark-vignette" }
280
+ }
281
+ ],
282
+ topLayers: [
283
+ {
284
+ template: "titleCaption",
285
+ config: {
286
+ type: "titleCaption",
287
+ title: articleTitle,
288
+ subtitle,
289
+ position: "bottom-left",
290
+ fontSize: 24,
291
+ showThumbnail: !!heroSrc,
292
+ thumbnailSrc: heroSrc
293
+ }
294
+ }
295
+ ]
296
+ };
297
+ case "branded":
298
+ return {
299
+ bottomLayers: [
300
+ {
301
+ template: "gradientBackground",
302
+ config: { type: "gradientBackground", preset: "cool-blue" }
303
+ }
304
+ ],
305
+ topLayers: [
306
+ {
307
+ template: "titleCaption",
308
+ config: {
309
+ type: "titleCaption",
310
+ title: articleTitle,
311
+ subtitle,
312
+ position: "bottom-left",
313
+ fontSize: 26,
314
+ showThumbnail: !!heroSrc,
315
+ thumbnailSrc: heroSrc
316
+ }
317
+ }
318
+ ]
319
+ };
320
+ case "cinematic":
321
+ return {
322
+ bottomLayers: heroSrc ? [
323
+ {
324
+ template: "imageBackground",
325
+ config: {
326
+ type: "imageBackground",
327
+ src: heroSrc,
328
+ blur: 12,
329
+ opacity: 0.3,
330
+ ambientMotion: "zoomIn"
331
+ }
332
+ }
333
+ ] : [
334
+ {
335
+ template: "gradientBackground",
336
+ config: { type: "gradientBackground", preset: "radial-dark" }
337
+ }
338
+ ],
339
+ topLayers: [
340
+ {
341
+ template: "titleCaption",
342
+ config: {
343
+ type: "titleCaption",
344
+ title: articleTitle,
345
+ subtitle,
346
+ position: "bottom-right",
347
+ fontSize: 22
348
+ }
349
+ }
350
+ ]
351
+ };
352
+ case "clean":
353
+ return {
354
+ bottomLayers: [
355
+ {
356
+ template: "solidBackground",
357
+ config: { type: "solidBackground", color: "#1a202c" }
358
+ }
359
+ ]
360
+ };
361
+ default:
362
+ return {};
363
+ }
364
+ }
365
+
366
+ // src/doc/templates/titleBlock.ts
367
+ function titleBlock(input, context) {
368
+ const { title, subtitle, backgroundColor } = input;
369
+ const { theme, layout } = context;
370
+ const titleFontSize = scaledFontSize(96, context, true);
371
+ const subtitleFontSize = scaledFontSize(36, context, false);
372
+ const baseBg = backgroundColor || theme.colors.primary;
373
+ const layers = [
374
+ // Background — radial gradient for depth instead of flat color
375
+ {
376
+ type: "shape",
377
+ id: "bg",
378
+ content: {
379
+ shape: "rect",
380
+ fill: `radial-gradient(ellipse at 50% 40%, ${baseBg} 0%, ${theme.colors.background} 100%)`
381
+ },
382
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
383
+ },
384
+ // Subtle decorative line above title
385
+ {
386
+ type: "shape",
387
+ id: "accent-line",
388
+ content: {
389
+ shape: "rect",
390
+ fill: "rgba(255, 255, 255, 0.2)"
391
+ },
392
+ position: {
393
+ x: "40%",
394
+ y: subtitle ? "28%" : "38%",
395
+ width: "20%",
396
+ height: "2px"
397
+ }
398
+ },
399
+ // Title
400
+ {
401
+ type: "text",
402
+ id: "title",
403
+ content: {
404
+ text: title,
405
+ style: {
406
+ fontSize: titleFontSize,
407
+ fontWeight: "bold",
408
+ color: theme.colors.text,
409
+ textAlign: "center",
410
+ shadow: true
411
+ }
412
+ },
413
+ position: {
414
+ x: "50%",
415
+ y: subtitle ? layout.primaryY : "50%",
416
+ anchor: "center",
417
+ width: layout.maxTextWidth
418
+ },
419
+ animation: { type: "fadeIn", duration: 2 }
420
+ }
421
+ ];
422
+ if (subtitle) {
423
+ layers.push({
424
+ type: "text",
425
+ id: "subtitle",
426
+ content: {
427
+ text: subtitle,
428
+ style: {
429
+ fontSize: subtitleFontSize,
430
+ color: theme.colors.textMuted,
431
+ textAlign: "center",
432
+ lineHeight: 1.5
433
+ }
434
+ },
435
+ position: {
436
+ x: "50%",
437
+ y: layout.secondaryY,
438
+ anchor: "center",
439
+ width: layout.maxTextWidth
440
+ },
441
+ animation: { type: "fadeIn", duration: 1.5, delay: 1 }
442
+ });
443
+ }
444
+ return layers;
445
+ }
446
+
447
+ // src/doc/utils/themeUtils.ts
448
+ var FALLBACK_COLOR_SCHEME = {
449
+ bg: "#1a365d",
450
+ text: "#63b3ed",
451
+ accent: "#90cdf4"
452
+ };
453
+ function resolveColorScheme(context, name) {
454
+ const schemes = context.theme.colorSchemes;
455
+ if (name && name in schemes) return schemes[name];
456
+ const keys = Object.keys(schemes);
457
+ if (keys.length > 0) return schemes[keys[0]];
458
+ return FALLBACK_COLOR_SCHEME;
459
+ }
460
+ function themedFontSize(basePx, context, isTitle = false) {
461
+ const { fontScale, layout, theme } = context;
462
+ const layoutScale = isTitle ? layout.titleScale : layout.bodyScale;
463
+ const themeScale = isTitle ? theme.typography.titleScale ?? 1 : theme.typography.bodyScale ?? 1;
464
+ return Math.round(basePx * fontScale * layoutScale * themeScale);
465
+ }
466
+ function scaleAnimationDuration(baseDuration, context) {
467
+ const speed = context.theme.style.animationSpeed ?? 1;
468
+ return baseDuration * speed;
469
+ }
470
+ function getDefaultAnimation(context, layerType) {
471
+ const rs = context.theme.renderStyle;
472
+ return layerType === "text" ? rs.defaultTextAnimation : rs.defaultImageAnimation;
473
+ }
474
+ function shouldUseShadow(context) {
475
+ return context.theme.style.textShadow ?? true;
476
+ }
477
+ function getOverlayOpacity(context) {
478
+ return context.theme.style.overlayOpacity ?? 0.5;
479
+ }
480
+ function getTemplateHint(context, templateName, key, fallback) {
481
+ const hints = context.theme.renderStyle.templateHints;
482
+ if (!hints) return fallback;
483
+ const tplHints = hints[templateName];
484
+ if (!tplHints || !(key in tplHints)) return fallback;
485
+ return tplHints[key];
486
+ }
487
+
488
+ // src/doc/templates/sectionHeader.ts
489
+ function sectionHeader(input, context) {
490
+ const { title, colorScheme = "blue", imageSrc, imageAlt, ambientMotion } = input;
491
+ const { layout } = context;
492
+ const colors = resolveColorScheme(context, colorScheme);
493
+ const titleFontSize = scaledFontSize(72, context, true);
494
+ const layers = [];
495
+ if (imageSrc) {
496
+ layers.push({
497
+ type: "image",
498
+ id: "bg-image",
499
+ content: {
500
+ src: imageSrc,
501
+ alt: imageAlt || title,
502
+ fit: "cover"
503
+ },
504
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
505
+ animation: ambientMotion ? { type: ambientMotion, duration: 8 } : { type: "slowZoom", duration: 8, direction: "in" }
506
+ });
507
+ layers.push({
508
+ type: "shape",
509
+ id: "overlay",
510
+ content: {
511
+ shape: "rect",
512
+ fill: "rgba(0, 0, 0, 0.5)"
513
+ },
514
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
515
+ });
516
+ } else {
517
+ layers.push({
518
+ type: "shape",
519
+ id: "bg",
520
+ content: {
521
+ shape: "rect",
522
+ fill: colors.bg
523
+ },
524
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
525
+ });
526
+ layers.push({
527
+ type: "shape",
528
+ id: "line-top",
529
+ content: { shape: "rect", fill: `${colors.text}33` },
530
+ position: { x: "50%", y: "40%", width: "20%", height: "2px", anchor: "center" }
531
+ });
532
+ layers.push({
533
+ type: "shape",
534
+ id: "line-bottom",
535
+ content: { shape: "rect", fill: `${colors.text}33` },
536
+ position: { x: "50%", y: "60%", width: "20%", height: "2px", anchor: "center" }
537
+ });
538
+ }
539
+ layers.push({
540
+ type: "text",
541
+ id: "title",
542
+ content: {
543
+ text: title,
544
+ style: {
545
+ fontSize: titleFontSize,
546
+ fontWeight: "bold",
547
+ color: imageSrc ? "#ffffff" : colors.text,
548
+ textAlign: "center",
549
+ shadow: true
550
+ }
551
+ },
552
+ position: {
553
+ x: "50%",
554
+ y: "50%",
555
+ anchor: "center",
556
+ width: layout.maxTextWidth
557
+ },
558
+ animation: { type: "fadeIn", duration: 1.5 }
559
+ });
560
+ return layers;
561
+ }
562
+
563
+ // src/doc/templates/accentImage.ts
564
+ var PERCENTAGE_RE = /^(\d+(?:\.\d+)?)\s*%?$/;
565
+ var DEFAULT_LAYOUT = {
566
+ textCenterX: "50%",
567
+ textWidth: "85%",
568
+ textYAdjust: 0,
569
+ adjustY: false
570
+ };
571
+ function mapAmbientMotion(motion, duration) {
572
+ if (!motion) return void 0;
573
+ const base = duration !== void 0 ? { duration } : {};
574
+ switch (motion) {
575
+ case "zoomIn":
576
+ case "in":
577
+ return { type: "slowZoom", direction: "in", ...base };
578
+ case "zoomOut":
579
+ case "out":
580
+ return { type: "slowZoom", direction: "out", ...base };
581
+ case "panLeft":
582
+ case "left":
583
+ return { type: "slowZoom", panDirection: "left", ...base };
584
+ case "panRight":
585
+ case "right":
586
+ return { type: "slowZoom", panDirection: "right", ...base };
587
+ default:
588
+ return void 0;
589
+ }
590
+ }
591
+ var STRIP_SIZE = 35;
592
+ var STRIP_SIZE_PCT = `${STRIP_SIZE}%`;
593
+ function getAccentLayout(position) {
594
+ switch (position) {
595
+ case "left-strip":
596
+ return {
597
+ textCenterX: `${50 + STRIP_SIZE / 2}%`,
598
+ // Shift center right
599
+ textWidth: `${100 - STRIP_SIZE - 8}%`,
600
+ // Leave margin
601
+ textYAdjust: 0,
602
+ adjustY: false
603
+ };
604
+ case "right-strip":
605
+ return {
606
+ textCenterX: `${50 - STRIP_SIZE / 2}%`,
607
+ // Shift center left
608
+ textWidth: `${100 - STRIP_SIZE - 8}%`,
609
+ textYAdjust: 0,
610
+ adjustY: false
611
+ };
612
+ case "bottom-strip":
613
+ return {
614
+ textCenterX: "50%",
615
+ textWidth: "85%",
616
+ textYAdjust: -10,
617
+ // Shift content up by 10%
618
+ adjustY: true
619
+ };
620
+ case "corner-inset":
621
+ return {
622
+ textCenterX: "45%",
623
+ // Slight shift to avoid corner
624
+ textWidth: "70%",
625
+ textYAdjust: 0,
626
+ adjustY: false
627
+ };
628
+ default:
629
+ return DEFAULT_LAYOUT;
630
+ }
631
+ }
632
+ function createAccentLayers(accent, slideId) {
633
+ const layers = [];
634
+ const { src, alt, position, ambientMotion, credit, license } = accent;
635
+ switch (position) {
636
+ case "left-strip":
637
+ layers.push(createStripImage(src, alt, "left", slideId, ambientMotion, credit, license));
638
+ layers.push(createStripGradient("left", slideId));
639
+ break;
640
+ case "right-strip":
641
+ layers.push(createStripImage(src, alt, "right", slideId, ambientMotion, credit, license));
642
+ layers.push(createStripGradient("right", slideId));
643
+ break;
644
+ case "bottom-strip":
645
+ layers.push(createBottomStripImage(src, alt, slideId, ambientMotion, credit, license));
646
+ layers.push(createBottomStripGradient(slideId));
647
+ break;
648
+ case "corner-inset":
649
+ layers.push(createCornerInsetImage(src, alt, slideId, ambientMotion, credit, license));
650
+ layers.push(createCornerVignette(slideId));
651
+ break;
652
+ }
653
+ return layers;
654
+ }
655
+ function createStripImage(src, alt, side, slideId, ambientMotion, credit, license) {
656
+ return {
657
+ type: "image",
658
+ id: `${slideId}-accent-img`,
659
+ content: {
660
+ src,
661
+ alt,
662
+ fit: "cover",
663
+ credit,
664
+ license
665
+ },
666
+ position: {
667
+ x: side === "left" ? 0 : `${100 - STRIP_SIZE}%`,
668
+ y: 0,
669
+ width: STRIP_SIZE_PCT,
670
+ height: "100%"
671
+ },
672
+ animation: mapAmbientMotion(ambientMotion, 15)
673
+ };
674
+ }
675
+ function createStripGradient(side, slideId) {
676
+ const gradientId = side === "left" ? "accent-gradient-left" : "accent-gradient-right";
677
+ return {
678
+ type: "shape",
679
+ id: `${slideId}-accent-fade`,
680
+ content: {
681
+ shape: "rect",
682
+ // SVG gradient will be handled by the renderer
683
+ // Using a semi-transparent overlay as fallback
684
+ fill: `url(#${gradientId})`
685
+ },
686
+ position: {
687
+ x: side === "left" ? `${STRIP_SIZE - 8}%` : `${100 - STRIP_SIZE}%`,
688
+ y: 0,
689
+ width: "10%",
690
+ height: "100%"
691
+ }
692
+ };
693
+ }
694
+ function createBottomStripImage(src, alt, slideId, ambientMotion, credit, license) {
695
+ return {
696
+ type: "image",
697
+ id: `${slideId}-accent-img`,
698
+ content: {
699
+ src,
700
+ alt,
701
+ fit: "cover",
702
+ credit,
703
+ license
704
+ },
705
+ position: {
706
+ x: 0,
707
+ y: `${100 - STRIP_SIZE}%`,
708
+ width: "100%",
709
+ height: STRIP_SIZE_PCT
710
+ },
711
+ animation: mapAmbientMotion(ambientMotion, 15)
712
+ };
713
+ }
714
+ function createBottomStripGradient(slideId) {
715
+ return {
716
+ type: "shape",
717
+ id: `${slideId}-accent-fade`,
718
+ content: {
719
+ shape: "rect",
720
+ fill: "url(#accent-gradient-bottom)"
721
+ },
722
+ position: {
723
+ x: 0,
724
+ y: `${100 - STRIP_SIZE - 8}%`,
725
+ width: "100%",
726
+ height: "12%"
727
+ }
728
+ };
729
+ }
730
+ function createCornerInsetImage(src, alt, slideId, ambientMotion, credit, license) {
731
+ return {
732
+ type: "image",
733
+ id: `${slideId}-accent-img`,
734
+ content: {
735
+ src,
736
+ alt,
737
+ fit: "cover",
738
+ credit,
739
+ license
740
+ },
741
+ position: {
742
+ x: "70%",
743
+ y: "60%",
744
+ width: "28%",
745
+ height: "38%",
746
+ anchor: "top-left"
747
+ },
748
+ animation: mapAmbientMotion(ambientMotion, 15) ?? { type: "fadeIn", duration: 2, delay: 0.5 }
749
+ };
750
+ }
751
+ function createCornerVignette(slideId) {
752
+ return {
753
+ type: "shape",
754
+ id: `${slideId}-accent-vignette`,
755
+ content: {
756
+ shape: "rect",
757
+ fill: "url(#accent-vignette-corner)",
758
+ borderRadius: 8
759
+ },
760
+ position: {
761
+ x: "69%",
762
+ y: "59%",
763
+ width: "30%",
764
+ height: "40%"
765
+ }
766
+ };
767
+ }
768
+ function adjustY(originalY, layout) {
769
+ if (!layout.adjustY || layout.textYAdjust === 0) {
770
+ return typeof originalY === "number" ? `${originalY}%` : originalY;
771
+ }
772
+ const yStr = typeof originalY === "number" ? `${originalY}` : originalY;
773
+ const match = yStr.match(PERCENTAGE_RE);
774
+ if (match) {
775
+ const yValue = parseFloat(match[1]);
776
+ const adjusted = Math.max(5, yValue + layout.textYAdjust);
777
+ return `${adjusted}%`;
778
+ }
779
+ return yStr;
780
+ }
781
+
782
+ // src/doc/templates/captionUtils.ts
783
+ var TRAILING_PUNCT_RE = /[.;:!?]+$/;
784
+ function cleanCaption(text) {
785
+ return text.replace(TRAILING_PUNCT_RE, "").trim();
786
+ }
787
+ function createBackgroundLayer(id, fill) {
788
+ return {
789
+ type: "shape",
790
+ id,
791
+ content: { shape: "rect", fill },
792
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
793
+ };
794
+ }
795
+
796
+ // src/doc/templates/statHighlight.ts
797
+ function statHighlight(input, context) {
798
+ const { stat, description, detail, colorScheme = "blue", accentImage } = input;
799
+ const { theme } = context;
800
+ const colors = resolveColorScheme(context, colorScheme);
801
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
802
+ const statFontSize = scaledFontSize(148, context, true);
803
+ const descFontSize = scaledFontSize(32, context, false);
804
+ const detailFontSize = scaledFontSize(26, context, false);
805
+ const layers = [
806
+ createBackgroundLayer(
807
+ "bg",
808
+ `linear-gradient(180deg, ${theme.colors.background} 0%, #0f1520 100%)`
809
+ )
810
+ ];
811
+ if (accentImage) {
812
+ layers.push(...createAccentLayers(accentImage, input.id));
813
+ }
814
+ layers.push({
815
+ type: "text",
816
+ id: "stat",
817
+ content: {
818
+ text: stat,
819
+ style: {
820
+ fontSize: statFontSize,
821
+ fontWeight: "bold",
822
+ color: colors.text,
823
+ shadow: !!accentImage
824
+ }
825
+ },
826
+ position: {
827
+ x: accentLayout.textCenterX,
828
+ y: adjustY("32%", accentLayout),
829
+ anchor: "center"
830
+ },
831
+ animation: { type: "zoomIn", duration: 0.6 }
832
+ });
833
+ layers.push({
834
+ type: "text",
835
+ id: "description",
836
+ content: {
837
+ text: description,
838
+ style: {
839
+ fontSize: descFontSize,
840
+ color: theme.colors.textMuted,
841
+ textAlign: "center",
842
+ lineHeight: 1.6,
843
+ shadow: !!accentImage
844
+ }
845
+ },
846
+ position: {
847
+ x: accentLayout.textCenterX,
848
+ y: adjustY("58%", accentLayout),
849
+ width: accentLayout.textWidth,
850
+ anchor: "center"
851
+ },
852
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
853
+ });
854
+ if (detail) {
855
+ layers.push({
856
+ type: "text",
857
+ id: "detail",
858
+ content: {
859
+ text: detail,
860
+ style: {
861
+ fontSize: detailFontSize,
862
+ color: colors.accent,
863
+ textAlign: "center",
864
+ shadow: !!accentImage
865
+ }
866
+ },
867
+ position: {
868
+ x: accentLayout.textCenterX,
869
+ y: adjustY("75%", accentLayout),
870
+ anchor: "center"
871
+ },
872
+ animation: { type: "fadeIn", duration: 1, delay: 1 }
873
+ });
874
+ }
875
+ return layers;
876
+ }
877
+
878
+ // src/doc/templates/quoteBlock.ts
879
+ function quoteBlock(input, context) {
880
+ const { quote, attribution, accentImage } = input;
881
+ const { theme, layout } = context;
882
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
883
+ const quoteFontSize = scaledFontSize(48, context, true);
884
+ const attrFontSize = scaledFontSize(24, context, false);
885
+ const decorativeQuoteFontSize = scaledFontSize(280, context, true);
886
+ const layers = [
887
+ createBackgroundLayer(
888
+ "bg",
889
+ `linear-gradient(160deg, ${theme.colors.backgroundLight} 0%, #1e2636 100%)`
890
+ )
891
+ ];
892
+ if (accentImage) {
893
+ layers.push(...createAccentLayers(accentImage, input.id));
894
+ }
895
+ layers.push({
896
+ type: "text",
897
+ id: "deco-quote",
898
+ content: {
899
+ text: "\u201C",
900
+ style: {
901
+ fontSize: decorativeQuoteFontSize,
902
+ color: "rgba(255, 255, 255, 0.06)",
903
+ textAlign: "center"
904
+ }
905
+ },
906
+ position: {
907
+ x: accentLayout.textCenterX,
908
+ y: adjustY("18%", accentLayout),
909
+ anchor: "center"
910
+ }
911
+ });
912
+ const quoteY = attribution ? layout.primaryY : "50%";
913
+ layers.push({
914
+ type: "text",
915
+ id: "quote",
916
+ content: {
917
+ text: quote,
918
+ style: {
919
+ fontSize: quoteFontSize,
920
+ color: theme.colors.text,
921
+ textAlign: "center",
922
+ lineHeight: 1.6,
923
+ shadow: true
924
+ }
925
+ },
926
+ position: {
927
+ x: accentLayout.textCenterX,
928
+ y: adjustY(quoteY, accentLayout),
929
+ anchor: "center",
930
+ width: accentLayout.textWidth
931
+ },
932
+ animation: { type: "fadeIn", duration: 2 }
933
+ });
934
+ if (attribution) {
935
+ layers.push({
936
+ type: "text",
937
+ id: "attribution",
938
+ content: {
939
+ text: `\u2014 ${attribution}`,
940
+ style: {
941
+ fontSize: attrFontSize,
942
+ color: theme.colors.textMuted,
943
+ textAlign: "center",
944
+ shadow: !!accentImage
945
+ }
946
+ },
947
+ position: {
948
+ x: accentLayout.textCenterX,
949
+ y: adjustY(layout.captionY, accentLayout),
950
+ anchor: "center"
951
+ },
952
+ animation: { type: "fadeIn", duration: 1, delay: 1.5 }
953
+ });
954
+ }
955
+ return layers;
956
+ }
957
+
958
+ // src/doc/templates/factCard.ts
959
+ function factCard(input, context) {
960
+ const { fact, explanation, source, accentImage } = input;
961
+ const { theme } = context;
962
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
963
+ const factFontSize = scaledFontSize(56, context, true);
964
+ const explainFontSize = scaledFontSize(32, context, false);
965
+ const sourceFontSize = scaledFontSize(20, context, false);
966
+ const layers = [
967
+ createBackgroundLayer(
968
+ "bg",
969
+ `linear-gradient(170deg, #1e2030 0%, ${theme.colors.background} 100%)`
970
+ )
971
+ ];
972
+ if (accentImage) {
973
+ layers.push(...createAccentLayers(accentImage, input.id));
974
+ }
975
+ layers.push({
976
+ type: "text",
977
+ id: "fact",
978
+ content: {
979
+ text: fact,
980
+ style: {
981
+ fontSize: factFontSize,
982
+ fontWeight: "bold",
983
+ color: theme.colors.text,
984
+ textAlign: "center",
985
+ lineHeight: 1.4,
986
+ shadow: true
987
+ }
988
+ },
989
+ position: {
990
+ x: accentLayout.textCenterX,
991
+ y: adjustY("35%", accentLayout),
992
+ width: accentLayout.textWidth,
993
+ anchor: "center"
994
+ },
995
+ animation: { type: "fadeIn", duration: 1.5 }
996
+ });
997
+ layers.push({
998
+ type: "text",
999
+ id: "explanation",
1000
+ content: {
1001
+ text: explanation,
1002
+ style: {
1003
+ fontSize: explainFontSize,
1004
+ color: theme.colors.textMuted,
1005
+ textAlign: "center",
1006
+ lineHeight: 1.5,
1007
+ shadow: !!accentImage
1008
+ }
1009
+ },
1010
+ position: {
1011
+ x: accentLayout.textCenterX,
1012
+ y: adjustY("60%", accentLayout),
1013
+ width: accentLayout.textWidth,
1014
+ anchor: "center"
1015
+ },
1016
+ animation: { type: "fadeIn", duration: 1, delay: 0.8 }
1017
+ });
1018
+ if (source) {
1019
+ layers.push({
1020
+ type: "text",
1021
+ id: "source",
1022
+ content: {
1023
+ text: source,
1024
+ style: {
1025
+ fontSize: sourceFontSize,
1026
+ color: theme.colors.textMuted,
1027
+ textAlign: "center",
1028
+ shadow: !!accentImage
1029
+ }
1030
+ },
1031
+ position: {
1032
+ x: accentLayout.textCenterX,
1033
+ y: adjustY("85%", accentLayout),
1034
+ anchor: "center"
1035
+ },
1036
+ animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
1037
+ });
1038
+ }
1039
+ return layers;
1040
+ }
1041
+
1042
+ // src/doc/templates/twoColumn.ts
1043
+ function twoColumn(input, context) {
1044
+ const { left, right, header, leftColor = "green", rightColor = "blue" } = input;
1045
+ if (!left?.label || !right?.label) return [];
1046
+ const { theme, layout, orientation } = context;
1047
+ const leftColors = resolveColorScheme(context, leftColor);
1048
+ const rightColors = resolveColorScheme(context, rightColor);
1049
+ const positions = getTwoColumnPositions(orientation);
1050
+ const isStacked = layout.stackColumns;
1051
+ const columnWidth = isStacked ? "85%" : "42%";
1052
+ const headerFontSize = scaledFontSize(32, context, false);
1053
+ const labelFontSize = scaledFontSize(44, context, true);
1054
+ const sublabelFontSize = scaledFontSize(22, context, false);
1055
+ const layers = [
1056
+ // Background — subtle gradient to add depth
1057
+ {
1058
+ type: "shape",
1059
+ id: "bg",
1060
+ content: {
1061
+ shape: "rect",
1062
+ fill: `linear-gradient(135deg, ${theme.colors.background} 0%, #16202e 100%)`
1063
+ },
1064
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1065
+ }
1066
+ ];
1067
+ if (!isStacked) {
1068
+ layers.push({
1069
+ type: "shape",
1070
+ id: "left-panel",
1071
+ content: {
1072
+ shape: "rect",
1073
+ fill: "rgba(255, 255, 255, 0.04)"
1074
+ },
1075
+ position: { x: "3%", y: "15%", width: "44%", height: "70%" }
1076
+ });
1077
+ layers.push({
1078
+ type: "shape",
1079
+ id: "right-panel",
1080
+ content: {
1081
+ shape: "rect",
1082
+ fill: "rgba(255, 255, 255, 0.04)"
1083
+ },
1084
+ position: { x: "53%", y: "15%", width: "44%", height: "70%" }
1085
+ });
1086
+ }
1087
+ const headerY = isStacked ? "12%" : "22%";
1088
+ if (header) {
1089
+ layers.push({
1090
+ type: "text",
1091
+ id: "header",
1092
+ content: {
1093
+ text: header,
1094
+ style: {
1095
+ fontSize: headerFontSize,
1096
+ color: theme.colors.textMuted,
1097
+ textAlign: "center"
1098
+ }
1099
+ },
1100
+ position: { x: "50%", y: headerY, anchor: "center" },
1101
+ animation: { type: "fadeIn", duration: 0.8 }
1102
+ });
1103
+ }
1104
+ let leftY;
1105
+ let leftSublabelY;
1106
+ let rightY;
1107
+ let rightSublabelY;
1108
+ if (isStacked) {
1109
+ leftY = header ? "30%" : "25%";
1110
+ leftSublabelY = header ? "40%" : "35%";
1111
+ rightY = header ? "60%" : "55%";
1112
+ rightSublabelY = header ? "70%" : "65%";
1113
+ } else {
1114
+ const baseY = header ? "47%" : "45%";
1115
+ const sublabelOffset = header ? "58%" : "56%";
1116
+ leftY = baseY;
1117
+ leftSublabelY = sublabelOffset;
1118
+ rightY = baseY;
1119
+ rightSublabelY = sublabelOffset;
1120
+ }
1121
+ layers.push({
1122
+ type: "text",
1123
+ id: "left-label",
1124
+ content: {
1125
+ text: left.label,
1126
+ style: {
1127
+ fontSize: labelFontSize,
1128
+ fontWeight: "bold",
1129
+ color: leftColors.text,
1130
+ textAlign: "center",
1131
+ lineHeight: 1.3
1132
+ }
1133
+ },
1134
+ position: {
1135
+ x: positions.left.x,
1136
+ y: leftY,
1137
+ anchor: "center",
1138
+ width: columnWidth
1139
+ },
1140
+ animation: { type: "fadeIn", duration: 1 }
1141
+ });
1142
+ if (left.sublabel) {
1143
+ layers.push({
1144
+ type: "text",
1145
+ id: "left-sublabel",
1146
+ content: {
1147
+ text: left.sublabel,
1148
+ style: {
1149
+ fontSize: sublabelFontSize,
1150
+ color: theme.colors.textMuted,
1151
+ textAlign: "center",
1152
+ lineHeight: 1.4
1153
+ }
1154
+ },
1155
+ position: {
1156
+ x: positions.left.x,
1157
+ y: leftSublabelY,
1158
+ anchor: "center",
1159
+ width: columnWidth
1160
+ },
1161
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.3 }
1162
+ });
1163
+ }
1164
+ const connectorFontSize = scaledFontSize(120, context, true);
1165
+ const connectorY = isStacked ? "48%" : "50%";
1166
+ const connectorSymbol = isStacked ? "\u2193" : "\u2192";
1167
+ layers.push({
1168
+ type: "text",
1169
+ id: "connector",
1170
+ content: {
1171
+ text: connectorSymbol,
1172
+ style: {
1173
+ fontSize: connectorFontSize,
1174
+ fontWeight: "bold",
1175
+ color: "rgba(255, 255, 255, 0.6)",
1176
+ textAlign: "center"
1177
+ }
1178
+ },
1179
+ position: {
1180
+ x: "50%",
1181
+ y: connectorY,
1182
+ anchor: "center"
1183
+ },
1184
+ animation: { type: "fadeIn", duration: 0.6, delay: 0.4 }
1185
+ });
1186
+ layers.push({
1187
+ type: "text",
1188
+ id: "right-label",
1189
+ content: {
1190
+ text: right.label,
1191
+ style: {
1192
+ fontSize: labelFontSize,
1193
+ fontWeight: "bold",
1194
+ color: rightColors.text,
1195
+ textAlign: "center",
1196
+ lineHeight: 1.3
1197
+ }
1198
+ },
1199
+ position: {
1200
+ x: positions.right.x,
1201
+ y: rightY,
1202
+ anchor: "center",
1203
+ width: columnWidth
1204
+ },
1205
+ animation: { type: "fadeIn", duration: 1, delay: 0.5 }
1206
+ });
1207
+ if (right.sublabel) {
1208
+ layers.push({
1209
+ type: "text",
1210
+ id: "right-sublabel",
1211
+ content: {
1212
+ text: right.sublabel,
1213
+ style: {
1214
+ fontSize: sublabelFontSize,
1215
+ color: theme.colors.textMuted,
1216
+ textAlign: "center",
1217
+ lineHeight: 1.4
1218
+ }
1219
+ },
1220
+ position: {
1221
+ x: positions.right.x,
1222
+ y: rightSublabelY,
1223
+ anchor: "center",
1224
+ width: columnWidth
1225
+ },
1226
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.8 }
1227
+ });
1228
+ }
1229
+ return layers;
1230
+ }
1231
+
1232
+ // src/doc/templates/dateEvent.ts
1233
+ var MOOD_COLORS = {
1234
+ neutral: { bg: "#1a202c", date: "#63b3ed", text: "#ffffff" },
1235
+ somber: { bg: "#1a1a2e", date: "#e53e3e", text: "#a0aec0" },
1236
+ celebratory: { bg: "#1a365d", date: "#68d391", text: "#ffffff" }
1237
+ };
1238
+ function dateEvent(input, context) {
1239
+ const { date, description, footer, mood = "neutral", accentImage } = input;
1240
+ const { theme } = context;
1241
+ const colors = MOOD_COLORS[mood];
1242
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1243
+ const dateFontSize = scaledFontSize(96, context, true);
1244
+ const descFontSize = scaledFontSize(30, context, false);
1245
+ const footerFontSize = scaledFontSize(26, context, false);
1246
+ const layers = [
1247
+ // Background — unique diagonal gradient to differentiate from other dark templates
1248
+ {
1249
+ type: "shape",
1250
+ id: "bg",
1251
+ content: {
1252
+ shape: "rect",
1253
+ fill: `linear-gradient(135deg, ${colors.bg} 0%, #0d1117 100%)`
1254
+ },
1255
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1256
+ }
1257
+ ];
1258
+ if (accentImage) {
1259
+ layers.push(...createAccentLayers(accentImage, input.id));
1260
+ }
1261
+ layers.push({
1262
+ type: "text",
1263
+ id: "date",
1264
+ content: {
1265
+ text: date,
1266
+ style: {
1267
+ fontSize: dateFontSize,
1268
+ fontWeight: "bold",
1269
+ color: colors.date,
1270
+ shadow: !!accentImage
1271
+ }
1272
+ },
1273
+ position: {
1274
+ x: accentLayout.textCenterX,
1275
+ y: adjustY("35%", accentLayout),
1276
+ anchor: "center"
1277
+ },
1278
+ animation: { type: "fadeIn", duration: 1.5 }
1279
+ });
1280
+ layers.push({
1281
+ type: "text",
1282
+ id: "description",
1283
+ content: {
1284
+ text: description,
1285
+ style: {
1286
+ fontSize: descFontSize,
1287
+ color: colors.text,
1288
+ textAlign: "center",
1289
+ lineHeight: 1.8,
1290
+ shadow: !!accentImage
1291
+ }
1292
+ },
1293
+ position: {
1294
+ x: accentLayout.textCenterX,
1295
+ y: adjustY("58%", accentLayout),
1296
+ width: accentLayout.textWidth,
1297
+ anchor: "center"
1298
+ },
1299
+ animation: { type: "fadeIn", duration: 2, delay: 1 }
1300
+ });
1301
+ if (footer) {
1302
+ layers.push({
1303
+ type: "text",
1304
+ id: "footer",
1305
+ content: {
1306
+ text: footer,
1307
+ style: {
1308
+ fontSize: footerFontSize,
1309
+ color: theme.colors.textMuted,
1310
+ shadow: !!accentImage
1311
+ }
1312
+ },
1313
+ position: {
1314
+ x: accentLayout.textCenterX,
1315
+ y: adjustY("82%", accentLayout),
1316
+ anchor: "center"
1317
+ },
1318
+ animation: { type: "fadeIn", duration: 1, delay: 3 }
1319
+ });
1320
+ }
1321
+ return layers;
1322
+ }
1323
+
1324
+ // src/doc/templates/imageWithCaption.ts
1325
+ function imageWithCaption(input, context) {
1326
+ const {
1327
+ imageSrc,
1328
+ imageAlt,
1329
+ caption: rawCaption,
1330
+ captionPosition: _captionPosition = "bottom",
1331
+ ambientMotion,
1332
+ isTitle,
1333
+ subtitle,
1334
+ imageCredit,
1335
+ imageLicense
1336
+ } = input;
1337
+ const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
1338
+ const { theme, layout } = context;
1339
+ const captionFontSize = scaledFontSize(36, context, false);
1340
+ const titleFontSize = scaledFontSize(96, context, true);
1341
+ const subtitleFontSize = scaledFontSize(36, context, false);
1342
+ const legacyKenBurns = input.kenBurns;
1343
+ const motion = ambientMotion || legacyKenBurns;
1344
+ const imageAnimation = mapAmbientMotion(motion);
1345
+ const layers = [
1346
+ // Background image
1347
+ {
1348
+ type: "image",
1349
+ id: "bg-image",
1350
+ content: {
1351
+ src: imageSrc,
1352
+ alt: imageAlt,
1353
+ fit: "cover",
1354
+ credit: imageCredit,
1355
+ license: imageLicense
1356
+ },
1357
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
1358
+ animation: imageAnimation
1359
+ }
1360
+ ];
1361
+ if (isTitle && caption) {
1362
+ layers.push({
1363
+ type: "shape",
1364
+ id: "title-gradient",
1365
+ content: {
1366
+ shape: "rect",
1367
+ fill: "linear-gradient(0deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 40%, rgba(0,0,0,0.1) 70%, transparent 100%)"
1368
+ },
1369
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1370
+ });
1371
+ layers.push({
1372
+ type: "text",
1373
+ id: "title",
1374
+ content: {
1375
+ text: caption,
1376
+ style: {
1377
+ fontSize: titleFontSize,
1378
+ fontWeight: "bold",
1379
+ color: theme.colors.text,
1380
+ textAlign: "center",
1381
+ shadow: true
1382
+ }
1383
+ },
1384
+ position: {
1385
+ x: "50%",
1386
+ y: subtitle ? "70%" : "75%",
1387
+ anchor: "center",
1388
+ width: layout.maxTextWidth
1389
+ },
1390
+ animation: { type: "fadeIn", duration: 0.8 }
1391
+ });
1392
+ if (subtitle) {
1393
+ layers.push({
1394
+ type: "text",
1395
+ id: "subtitle",
1396
+ content: {
1397
+ text: subtitle,
1398
+ style: {
1399
+ fontSize: subtitleFontSize,
1400
+ color: theme.colors.textMuted,
1401
+ textAlign: "center",
1402
+ lineHeight: 1.5
1403
+ }
1404
+ },
1405
+ position: {
1406
+ x: "50%",
1407
+ y: "82%",
1408
+ anchor: "center",
1409
+ width: layout.maxTextWidth
1410
+ },
1411
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
1412
+ });
1413
+ }
1414
+ return layers;
1415
+ }
1416
+ if (caption) {
1417
+ layers.push({
1418
+ type: "shape",
1419
+ id: "caption-gradient",
1420
+ content: {
1421
+ shape: "rect",
1422
+ fill: "linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 60%, transparent 100%)"
1423
+ },
1424
+ position: {
1425
+ x: 0,
1426
+ y: "65%",
1427
+ width: "100%",
1428
+ height: "35%"
1429
+ }
1430
+ });
1431
+ layers.push({
1432
+ type: "text",
1433
+ id: "caption",
1434
+ content: {
1435
+ text: caption,
1436
+ style: {
1437
+ fontSize: captionFontSize,
1438
+ color: theme.colors.text,
1439
+ textAlign: "center",
1440
+ shadow: true
1441
+ }
1442
+ },
1443
+ position: {
1444
+ x: "50%",
1445
+ y: "82%",
1446
+ anchor: "center",
1447
+ width: layout.maxTextWidth
1448
+ },
1449
+ animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
1450
+ });
1451
+ }
1452
+ return layers;
1453
+ }
1454
+
1455
+ // src/doc/templates/mapBlock.ts
1456
+ function mapBlock(input, context) {
1457
+ const {
1458
+ center,
1459
+ zoom,
1460
+ mapStyle = "terrain",
1461
+ title,
1462
+ caption,
1463
+ markers,
1464
+ ambientMotion,
1465
+ staticSrc
1466
+ } = input;
1467
+ const { theme, layout } = context;
1468
+ const titleFontSize = scaledFontSize(64, context, true);
1469
+ const captionFontSize = scaledFontSize(32, context, false);
1470
+ const mapAnimation = mapAmbientMotion(ambientMotion);
1471
+ const layers = [
1472
+ // Map background
1473
+ {
1474
+ type: "map",
1475
+ id: "map-bg",
1476
+ content: {
1477
+ center,
1478
+ zoom,
1479
+ style: mapStyle,
1480
+ markers,
1481
+ showAttribution: true,
1482
+ staticSrc
1483
+ // Use pre-rendered image if available
1484
+ },
1485
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
1486
+ animation: mapAnimation
1487
+ }
1488
+ ];
1489
+ if (title) {
1490
+ layers.push({
1491
+ type: "shape",
1492
+ id: "title-overlay",
1493
+ content: {
1494
+ shape: "rect",
1495
+ fill: "rgba(0,0,0,0.6)"
1496
+ },
1497
+ position: { x: 0, y: 0, width: "100%", height: "18%" }
1498
+ });
1499
+ layers.push({
1500
+ type: "text",
1501
+ id: "title",
1502
+ content: {
1503
+ text: title,
1504
+ style: {
1505
+ fontSize: titleFontSize,
1506
+ fontWeight: "bold",
1507
+ color: theme.colors.text,
1508
+ textAlign: "center",
1509
+ shadow: true
1510
+ }
1511
+ },
1512
+ position: {
1513
+ x: "50%",
1514
+ y: "9%",
1515
+ anchor: "center",
1516
+ width: layout.maxTextWidth
1517
+ },
1518
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
1519
+ });
1520
+ }
1521
+ if (caption) {
1522
+ layers.push({
1523
+ type: "shape",
1524
+ id: "caption-overlay",
1525
+ content: {
1526
+ shape: "rect",
1527
+ fill: "rgba(0,0,0,0.6)"
1528
+ },
1529
+ position: { x: 0, y: "82%", width: "100%", height: "18%" }
1530
+ });
1531
+ layers.push({
1532
+ type: "text",
1533
+ id: "caption",
1534
+ content: {
1535
+ text: caption,
1536
+ style: {
1537
+ fontSize: captionFontSize,
1538
+ color: theme.colors.text,
1539
+ textAlign: "center",
1540
+ shadow: true
1541
+ }
1542
+ },
1543
+ position: {
1544
+ x: "50%",
1545
+ y: layout.captionY,
1546
+ anchor: "center",
1547
+ width: layout.maxTextWidth
1548
+ },
1549
+ animation: { type: "fadeIn", duration: 1, delay: 0.5 }
1550
+ });
1551
+ }
1552
+ return layers;
1553
+ }
1554
+
1555
+ // src/doc/templates/fullBleedQuote.ts
1556
+ function fullBleedQuote(input, context) {
1557
+ const { text, colorScheme = "blue" } = input;
1558
+ const { theme } = context;
1559
+ const colors = resolveColorScheme(context, colorScheme);
1560
+ const textFontSize = scaledFontSize(120, context, true);
1561
+ return [
1562
+ // Background — dark radial vignette for cinematic feel
1563
+ {
1564
+ type: "shape",
1565
+ id: "bg",
1566
+ content: {
1567
+ shape: "rect",
1568
+ fill: `radial-gradient(ellipse at 50% 50%, ${theme.colors.backgroundLight} 0%, #000000 100%)`
1569
+ },
1570
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1571
+ },
1572
+ // The text — massive, centered
1573
+ {
1574
+ type: "text",
1575
+ id: "impact-text",
1576
+ content: {
1577
+ text,
1578
+ style: {
1579
+ fontSize: textFontSize,
1580
+ fontWeight: "bold",
1581
+ color: colors.text,
1582
+ textAlign: "center",
1583
+ lineHeight: 1.2,
1584
+ shadow: true
1585
+ }
1586
+ },
1587
+ position: {
1588
+ x: "50%",
1589
+ y: "50%",
1590
+ anchor: "center",
1591
+ width: "85%"
1592
+ },
1593
+ animation: { type: "fadeIn", duration: 1.5 }
1594
+ }
1595
+ ];
1596
+ }
1597
+
1598
+ // src/doc/templates/listBlock.ts
1599
+ function listBlock(input, context) {
1600
+ const { items, title, accentImage } = input;
1601
+ const { theme } = context;
1602
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1603
+ const titleFontSize = scaledFontSize(44, context, true);
1604
+ const itemFontSize = scaledFontSize(34, context, false);
1605
+ const layers = [
1606
+ // Background — gradient
1607
+ {
1608
+ type: "shape",
1609
+ id: "bg",
1610
+ content: {
1611
+ shape: "rect",
1612
+ fill: `linear-gradient(155deg, ${theme.colors.backgroundLight} 0%, ${theme.colors.background} 100%)`
1613
+ },
1614
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1615
+ }
1616
+ ];
1617
+ if (accentImage) {
1618
+ layers.push(...createAccentLayers(accentImage, input.id));
1619
+ }
1620
+ const startY = title ? 30 : 22;
1621
+ if (title) {
1622
+ layers.push({
1623
+ type: "text",
1624
+ id: "list-title",
1625
+ content: {
1626
+ text: title,
1627
+ style: {
1628
+ fontSize: titleFontSize,
1629
+ fontWeight: "bold",
1630
+ color: theme.colors.text,
1631
+ textAlign: "center",
1632
+ shadow: !!accentImage
1633
+ }
1634
+ },
1635
+ position: {
1636
+ x: accentLayout.textCenterX,
1637
+ y: adjustY("16%", accentLayout),
1638
+ anchor: "center"
1639
+ },
1640
+ animation: { type: "fadeIn", duration: 1 }
1641
+ });
1642
+ }
1643
+ const endY = 80;
1644
+ const spacing = items.length > 1 ? (endY - startY) / (items.length - 1) : 0;
1645
+ for (let i = 0; i < items.length; i++) {
1646
+ const y = startY + spacing * i;
1647
+ const itemText = `${i + 1}. ${items[i]}`;
1648
+ layers.push({
1649
+ type: "text",
1650
+ id: `item-${i}`,
1651
+ content: {
1652
+ text: itemText,
1653
+ style: {
1654
+ fontSize: itemFontSize,
1655
+ color: theme.colors.text,
1656
+ lineHeight: 1.4,
1657
+ shadow: !!accentImage
1658
+ }
1659
+ },
1660
+ position: {
1661
+ x: accentLayout.textCenterX,
1662
+ y: adjustY(`${y}%`, accentLayout),
1663
+ anchor: "center",
1664
+ width: accentLayout.textWidth
1665
+ },
1666
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.3 + 0.3 * i }
1667
+ });
1668
+ }
1669
+ return layers;
1670
+ }
1671
+
1672
+ // src/doc/templates/photoGrid.ts
1673
+ var GAP = 0.5;
1674
+ function photoGrid(input, context) {
1675
+ const { images, caption: rawCaption, ambientMotion } = input;
1676
+ if (!images || images.length === 0) return [];
1677
+ const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
1678
+ const { theme, layout } = context;
1679
+ const layers = [
1680
+ // Black background (visible in gaps between images)
1681
+ {
1682
+ type: "shape",
1683
+ id: "bg",
1684
+ content: { shape: "rect", fill: "#000000" },
1685
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1686
+ }
1687
+ ];
1688
+ const captionHeight = caption ? layout.stackColumns ? 18 : 22 : 0;
1689
+ const gridHeight = 100 - captionHeight;
1690
+ const positions = getGridPositions(images.length, gridHeight, layout.stackColumns);
1691
+ const altFontSize = scaledFontSize(24, context, false);
1692
+ for (let i = 0; i < Math.min(images.length, 4); i++) {
1693
+ const img = images[i];
1694
+ const pos = positions[i];
1695
+ layers.push({
1696
+ type: "shape",
1697
+ id: `grid-placeholder-${i}`,
1698
+ content: { shape: "rect", fill: theme.colors.background },
1699
+ position: { x: `${pos.x}%`, y: `${pos.y}%`, width: `${pos.w}%`, height: `${pos.h}%` }
1700
+ });
1701
+ if (img.alt) {
1702
+ layers.push({
1703
+ type: "text",
1704
+ id: `grid-alt-${i}`,
1705
+ content: {
1706
+ text: img.alt,
1707
+ style: {
1708
+ fontSize: altFontSize,
1709
+ color: theme.colors.textMuted,
1710
+ textAlign: "center"
1711
+ }
1712
+ },
1713
+ position: { x: `${pos.x + pos.w / 2}%`, y: `${pos.y + pos.h / 2}%`, anchor: "center" }
1714
+ });
1715
+ }
1716
+ layers.push({
1717
+ type: "image",
1718
+ id: `grid-img-${i}`,
1719
+ content: {
1720
+ src: img.src,
1721
+ alt: img.alt,
1722
+ fit: "cover",
1723
+ credit: img.credit,
1724
+ license: img.license
1725
+ },
1726
+ position: {
1727
+ x: `${pos.x}%`,
1728
+ y: `${pos.y}%`,
1729
+ width: `${pos.w}%`,
1730
+ height: `${pos.h}%`
1731
+ },
1732
+ // Apply ambient motion to the first (largest) image only
1733
+ animation: i === 0 && ambientMotion ? { type: ambientMotion, duration: 15 } : { type: "fadeIn", duration: 1, delay: 0.2 * i }
1734
+ });
1735
+ }
1736
+ if (caption) {
1737
+ const captionFontSize = scaledFontSize(layout.stackColumns ? 28 : 40, context, false);
1738
+ layers.push({
1739
+ type: "shape",
1740
+ id: "caption-bg",
1741
+ content: {
1742
+ shape: "rect",
1743
+ fill: "linear-gradient(transparent, rgba(0,0,0,0.8))"
1744
+ },
1745
+ position: { x: 0, y: `${gridHeight - 4}%`, width: "100%", height: `${captionHeight + 4}%` }
1746
+ });
1747
+ layers.push({
1748
+ type: "text",
1749
+ id: "caption",
1750
+ content: {
1751
+ text: caption,
1752
+ style: {
1753
+ fontSize: captionFontSize,
1754
+ color: theme.colors.text,
1755
+ textAlign: "center",
1756
+ shadow: true
1757
+ }
1758
+ },
1759
+ position: {
1760
+ x: "50%",
1761
+ y: `${gridHeight + (layout.stackColumns ? captionHeight / 2 : captionHeight * 0.3)}%`,
1762
+ anchor: "center",
1763
+ width: "90%"
1764
+ },
1765
+ animation: { type: "fadeIn", duration: 1, delay: 0.5 }
1766
+ });
1767
+ }
1768
+ return layers;
1769
+ }
1770
+ function getGridPositions(count, gridHeight, stacked) {
1771
+ const halfW = 50 - GAP / 2;
1772
+ const halfH = gridHeight / 2 - GAP / 2;
1773
+ switch (count) {
1774
+ case 2:
1775
+ if (stacked) {
1776
+ return [
1777
+ { x: 0, y: 0, w: 100, h: halfH },
1778
+ { x: 0, y: gridHeight / 2 + GAP / 2, w: 100, h: halfH }
1779
+ ];
1780
+ }
1781
+ return [
1782
+ { x: 0, y: 0, w: halfW, h: gridHeight },
1783
+ { x: 50 + GAP / 2, y: 0, w: halfW, h: gridHeight }
1784
+ ];
1785
+ case 3: {
1786
+ if (stacked) {
1787
+ const thirdH = gridHeight / 3 - GAP * 2 / 3;
1788
+ return [
1789
+ { x: 0, y: 0, w: 100, h: thirdH },
1790
+ { x: 0, y: gridHeight / 3 + GAP / 3, w: 100, h: thirdH },
1791
+ { x: 0, y: gridHeight * 2 / 3 + GAP * 2 / 3, w: 100, h: thirdH }
1792
+ ];
1793
+ }
1794
+ const leftW = 60 - GAP / 2;
1795
+ const rightW = 40 - GAP / 2;
1796
+ return [
1797
+ { x: 0, y: 0, w: leftW, h: gridHeight },
1798
+ { x: 60 + GAP / 2, y: 0, w: rightW, h: halfH },
1799
+ { x: 60 + GAP / 2, y: gridHeight / 2 + GAP / 2, w: rightW, h: halfH }
1800
+ ];
1801
+ }
1802
+ case 4:
1803
+ default:
1804
+ return [
1805
+ { x: 0, y: 0, w: halfW, h: halfH },
1806
+ { x: 50 + GAP / 2, y: 0, w: halfW, h: halfH },
1807
+ { x: 0, y: gridHeight / 2 + GAP / 2, w: halfW, h: halfH },
1808
+ { x: 50 + GAP / 2, y: gridHeight / 2 + GAP / 2, w: halfW, h: halfH }
1809
+ ];
1810
+ }
1811
+ }
1812
+
1813
+ // src/doc/templates/definitionCard.ts
1814
+ function definitionCard(input, context) {
1815
+ const { term, definition, origin, colorScheme = "blue", accentImage } = input;
1816
+ const { theme } = context;
1817
+ const colors = resolveColorScheme(context, colorScheme);
1818
+ const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1819
+ const termFontSize = scaledFontSize(72, context, true);
1820
+ const defFontSize = scaledFontSize(32, context, false);
1821
+ const originFontSize = scaledFontSize(22, context, false);
1822
+ const layers = [
1823
+ createBackgroundLayer(
1824
+ "bg",
1825
+ `linear-gradient(145deg, #1e2030 0%, ${theme.colors.background} 100%)`
1826
+ )
1827
+ ];
1828
+ if (accentImage) {
1829
+ layers.push(...createAccentLayers(accentImage, input.id));
1830
+ }
1831
+ layers.push({
1832
+ type: "text",
1833
+ id: "term",
1834
+ content: {
1835
+ text: term,
1836
+ style: {
1837
+ fontSize: termFontSize,
1838
+ fontWeight: "bold",
1839
+ color: colors.text,
1840
+ textAlign: "center",
1841
+ shadow: !!accentImage
1842
+ }
1843
+ },
1844
+ position: {
1845
+ x: accentLayout.textCenterX,
1846
+ y: adjustY("30%", accentLayout),
1847
+ anchor: "center"
1848
+ },
1849
+ animation: { type: "fadeIn", duration: 1.5 }
1850
+ });
1851
+ layers.push({
1852
+ type: "shape",
1853
+ id: "separator",
1854
+ content: {
1855
+ shape: "rect",
1856
+ fill: `${colors.text}33`
1857
+ // accent color at 20% opacity
1858
+ },
1859
+ position: {
1860
+ x: accentLayout.textCenterX,
1861
+ y: adjustY("42%", accentLayout),
1862
+ width: "30%",
1863
+ height: "2px",
1864
+ anchor: "center"
1865
+ }
1866
+ });
1867
+ layers.push({
1868
+ type: "text",
1869
+ id: "definition",
1870
+ content: {
1871
+ text: definition,
1872
+ style: {
1873
+ fontSize: defFontSize,
1874
+ color: theme.colors.text,
1875
+ textAlign: "center",
1876
+ lineHeight: 1.6,
1877
+ maxLines: 4,
1878
+ shadow: !!accentImage
1879
+ }
1880
+ },
1881
+ position: {
1882
+ x: accentLayout.textCenterX,
1883
+ y: adjustY("55%", accentLayout),
1884
+ width: accentLayout.textWidth,
1885
+ anchor: "center"
1886
+ },
1887
+ animation: { type: "fadeIn", duration: 1, delay: 0.8 }
1888
+ });
1889
+ if (origin) {
1890
+ layers.push({
1891
+ type: "text",
1892
+ id: "origin",
1893
+ content: {
1894
+ text: origin,
1895
+ style: {
1896
+ fontSize: originFontSize,
1897
+ color: theme.colors.textMuted,
1898
+ textAlign: "center",
1899
+ shadow: !!accentImage
1900
+ }
1901
+ },
1902
+ position: {
1903
+ x: accentLayout.textCenterX,
1904
+ y: adjustY("78%", accentLayout),
1905
+ anchor: "center"
1906
+ },
1907
+ animation: { type: "fadeIn", duration: 0.8, delay: 1.5 }
1908
+ });
1909
+ }
1910
+ return layers;
1911
+ }
1912
+
1913
+ // src/doc/templates/comparisonBar.ts
1914
+ function comparisonBar(input, context) {
1915
+ const { leftLabel, leftValue, rightLabel, rightValue, unit, colorScheme = "blue" } = input;
1916
+ const { theme } = context;
1917
+ const colors = resolveColorScheme(context, colorScheme);
1918
+ const labelFontSize = scaledFontSize(28, context, false);
1919
+ const valueFontSize = scaledFontSize(48, context, true);
1920
+ const maxValue = Math.max(leftValue, rightValue, 1);
1921
+ const maxBarWidth = 65;
1922
+ const leftBarWidth = leftValue / maxValue * maxBarWidth;
1923
+ const rightBarWidth = rightValue / maxValue * maxBarWidth;
1924
+ const barStartX = 15;
1925
+ const barHeight = 6;
1926
+ const topBarY = 36;
1927
+ const bottomBarY = 58;
1928
+ const formatValue = (v) => {
1929
+ if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
1930
+ if (v >= 1e3) return `${(v / 1e3).toFixed(v >= 1e4 ? 0 : 1)}K`;
1931
+ return v.toLocaleString();
1932
+ };
1933
+ const leftDisplay = unit ? `${formatValue(leftValue)} ${unit}` : formatValue(leftValue);
1934
+ const rightDisplay = unit ? `${formatValue(rightValue)} ${unit}` : formatValue(rightValue);
1935
+ return [
1936
+ // Background
1937
+ {
1938
+ type: "shape",
1939
+ id: "bg",
1940
+ content: {
1941
+ shape: "rect",
1942
+ fill: `linear-gradient(180deg, ${theme.colors.background} 0%, #0f1520 100%)`
1943
+ },
1944
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
1945
+ },
1946
+ // Top bar label
1947
+ {
1948
+ type: "text",
1949
+ id: "left-label",
1950
+ content: {
1951
+ text: leftLabel,
1952
+ style: { fontSize: labelFontSize, color: theme.colors.textMuted }
1953
+ },
1954
+ position: { x: `${barStartX}%`, y: `${topBarY - 8}%` }
1955
+ },
1956
+ // Top bar value
1957
+ {
1958
+ type: "text",
1959
+ id: "left-value",
1960
+ content: {
1961
+ text: leftDisplay,
1962
+ style: { fontSize: valueFontSize, fontWeight: "bold", color: colors.text }
1963
+ },
1964
+ position: { x: `${barStartX + leftBarWidth + 2}%`, y: `${topBarY - 1}%` },
1965
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.3 }
1966
+ },
1967
+ // Top bar
1968
+ {
1969
+ type: "shape",
1970
+ id: "left-bar",
1971
+ content: {
1972
+ shape: "rect",
1973
+ fill: colors.text,
1974
+ borderRadius: 4
1975
+ },
1976
+ position: {
1977
+ x: `${barStartX}%`,
1978
+ y: `${topBarY}%`,
1979
+ width: `${leftBarWidth}%`,
1980
+ height: `${barHeight}%`
1981
+ },
1982
+ animation: { type: "fadeIn", duration: 1 }
1983
+ },
1984
+ // Bottom bar label
1985
+ {
1986
+ type: "text",
1987
+ id: "right-label",
1988
+ content: {
1989
+ text: rightLabel,
1990
+ style: { fontSize: labelFontSize, color: theme.colors.textMuted }
1991
+ },
1992
+ position: { x: `${barStartX}%`, y: `${bottomBarY - 8}%` }
1993
+ },
1994
+ // Bottom bar value
1995
+ {
1996
+ type: "text",
1997
+ id: "right-value",
1998
+ content: {
1999
+ text: rightDisplay,
2000
+ style: { fontSize: valueFontSize, fontWeight: "bold", color: colors.accent }
2001
+ },
2002
+ position: { x: `${barStartX + rightBarWidth + 2}%`, y: `${bottomBarY - 1}%` },
2003
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.6 }
2004
+ },
2005
+ // Bottom bar
2006
+ {
2007
+ type: "shape",
2008
+ id: "right-bar",
2009
+ content: {
2010
+ shape: "rect",
2011
+ fill: colors.accent,
2012
+ borderRadius: 4
2013
+ },
2014
+ position: {
2015
+ x: `${barStartX}%`,
2016
+ y: `${bottomBarY}%`,
2017
+ width: `${rightBarWidth}%`,
2018
+ height: `${barHeight}%`
2019
+ },
2020
+ animation: { type: "fadeIn", duration: 1, delay: 0.3 }
2021
+ }
2022
+ ];
2023
+ }
2024
+
2025
+ // src/doc/templates/pullQuote.ts
2026
+ function pullQuote(input, context) {
2027
+ const { text, attribution, backgroundImage, ambientMotion } = input;
2028
+ if (!backgroundImage?.src) return [];
2029
+ const quoteFontSize = scaledFontSize(52, context, true);
2030
+ const attrFontSize = scaledFontSize(24, context, false);
2031
+ const decoFontSize = scaledFontSize(200, context, true);
2032
+ const layers = [
2033
+ // Full-bleed background image
2034
+ {
2035
+ type: "image",
2036
+ id: "bg-image",
2037
+ content: {
2038
+ src: backgroundImage.src,
2039
+ alt: backgroundImage.alt,
2040
+ fit: "cover",
2041
+ credit: backgroundImage.credit,
2042
+ license: backgroundImage.license
2043
+ },
2044
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
2045
+ animation: ambientMotion ? { type: ambientMotion, duration: 15 } : void 0
2046
+ },
2047
+ // Dark overlay for text readability
2048
+ {
2049
+ type: "shape",
2050
+ id: "overlay",
2051
+ content: {
2052
+ shape: "rect",
2053
+ fill: "rgba(0, 0, 0, 0.55)"
2054
+ },
2055
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2056
+ },
2057
+ // Decorative quotation mark
2058
+ {
2059
+ type: "text",
2060
+ id: "deco-quote",
2061
+ content: {
2062
+ text: "\u201C",
2063
+ style: {
2064
+ fontSize: decoFontSize,
2065
+ color: "rgba(255, 255, 255, 0.08)",
2066
+ textAlign: "center"
2067
+ }
2068
+ },
2069
+ position: {
2070
+ x: "50%",
2071
+ y: "20%",
2072
+ anchor: "center"
2073
+ }
2074
+ },
2075
+ // Quote text
2076
+ {
2077
+ type: "text",
2078
+ id: "quote-text",
2079
+ content: {
2080
+ text,
2081
+ style: {
2082
+ fontSize: quoteFontSize,
2083
+ color: "#ffffff",
2084
+ textAlign: "center",
2085
+ lineHeight: 1.5,
2086
+ shadow: true
2087
+ }
2088
+ },
2089
+ position: {
2090
+ x: "50%",
2091
+ y: attribution ? "45%" : "50%",
2092
+ anchor: "center",
2093
+ width: "80%"
2094
+ },
2095
+ animation: { type: "fadeIn", duration: 2 }
2096
+ }
2097
+ ];
2098
+ if (attribution) {
2099
+ layers.push({
2100
+ type: "text",
2101
+ id: "attribution",
2102
+ content: {
2103
+ text: `\u2014 ${attribution}`,
2104
+ style: {
2105
+ fontSize: attrFontSize,
2106
+ color: "rgba(255, 255, 255, 0.7)",
2107
+ textAlign: "center",
2108
+ shadow: true
2109
+ }
2110
+ },
2111
+ position: {
2112
+ x: "50%",
2113
+ y: "72%",
2114
+ anchor: "center"
2115
+ },
2116
+ animation: { type: "fadeIn", duration: 1, delay: 1.5 }
2117
+ });
2118
+ }
2119
+ return layers;
2120
+ }
2121
+
2122
+ // src/doc/templates/videoWithCaption.ts
2123
+ function videoWithCaption(input, context) {
2124
+ const {
2125
+ videoSrc,
2126
+ posterSrc,
2127
+ videoAlt,
2128
+ clipStart,
2129
+ clipEnd,
2130
+ sourceDuration,
2131
+ caption: rawCaption,
2132
+ captionPosition = "bottom",
2133
+ videoCredit,
2134
+ videoLicense
2135
+ } = input;
2136
+ const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
2137
+ const { theme, layout } = context;
2138
+ const captionFontSize = scaledFontSize(36, context, false);
2139
+ const creditFontSize = scaledFontSize(16, context, false);
2140
+ const layers = [
2141
+ // Background video clip
2142
+ {
2143
+ type: "video",
2144
+ id: "bg-video",
2145
+ content: {
2146
+ src: videoSrc,
2147
+ posterSrc,
2148
+ alt: videoAlt,
2149
+ fit: "cover",
2150
+ clipStart,
2151
+ clipEnd,
2152
+ sourceDuration,
2153
+ credit: videoCredit,
2154
+ license: videoLicense
2155
+ },
2156
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2157
+ }
2158
+ ];
2159
+ if (caption) {
2160
+ layers.push({
2161
+ type: "shape",
2162
+ id: "caption-gradient",
2163
+ content: {
2164
+ shape: "rect",
2165
+ fill: "linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 60%, transparent 100%)"
2166
+ },
2167
+ position: {
2168
+ x: 0,
2169
+ y: captionPosition === "top" ? 0 : "65%",
2170
+ width: "100%",
2171
+ height: "35%"
2172
+ }
2173
+ });
2174
+ const captionY = captionPosition === "top" ? "15%" : captionPosition === "center" ? "50%" : "82%";
2175
+ layers.push({
2176
+ type: "text",
2177
+ id: "caption",
2178
+ content: {
2179
+ text: caption,
2180
+ style: {
2181
+ fontSize: captionFontSize,
2182
+ color: theme.colors.text,
2183
+ textAlign: "center",
2184
+ shadow: true
2185
+ }
2186
+ },
2187
+ position: {
2188
+ x: "50%",
2189
+ y: captionY,
2190
+ anchor: "center",
2191
+ width: layout.maxTextWidth
2192
+ },
2193
+ animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
2194
+ });
2195
+ }
2196
+ if (videoCredit) {
2197
+ layers.push({
2198
+ type: "text",
2199
+ id: "credit",
2200
+ content: {
2201
+ text: videoCredit,
2202
+ style: {
2203
+ fontSize: creditFontSize,
2204
+ color: "rgba(255, 255, 255, 0.5)",
2205
+ textAlign: "right"
2206
+ }
2207
+ },
2208
+ position: {
2209
+ x: "96%",
2210
+ y: "96%",
2211
+ anchor: "bottom-right"
2212
+ }
2213
+ });
2214
+ }
2215
+ return layers;
2216
+ }
2217
+
2218
+ // src/doc/templates/videoPullQuote.ts
2219
+ function videoPullQuote(input, context) {
2220
+ const { text, attribution, backgroundVideo } = input;
2221
+ if (!backgroundVideo?.src) return [];
2222
+ const quoteFontSize = scaledFontSize(52, context, true);
2223
+ const attrFontSize = scaledFontSize(24, context, false);
2224
+ const decoFontSize = scaledFontSize(200, context, true);
2225
+ const layers = [
2226
+ // Full-bleed background video clip
2227
+ {
2228
+ type: "video",
2229
+ id: "bg-video",
2230
+ content: {
2231
+ src: backgroundVideo.src,
2232
+ posterSrc: backgroundVideo.posterSrc,
2233
+ alt: backgroundVideo.alt,
2234
+ fit: "cover",
2235
+ clipStart: backgroundVideo.clipStart,
2236
+ clipEnd: backgroundVideo.clipEnd,
2237
+ credit: backgroundVideo.credit,
2238
+ license: backgroundVideo.license
2239
+ },
2240
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2241
+ },
2242
+ // Dark overlay for text readability
2243
+ {
2244
+ type: "shape",
2245
+ id: "overlay",
2246
+ content: {
2247
+ shape: "rect",
2248
+ fill: "rgba(0, 0, 0, 0.55)"
2249
+ },
2250
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2251
+ },
2252
+ // Decorative quotation mark
2253
+ {
2254
+ type: "text",
2255
+ id: "deco-quote",
2256
+ content: {
2257
+ text: "\u201C",
2258
+ style: {
2259
+ fontSize: decoFontSize,
2260
+ color: "rgba(255, 255, 255, 0.08)",
2261
+ textAlign: "center"
2262
+ }
2263
+ },
2264
+ position: {
2265
+ x: "50%",
2266
+ y: "20%",
2267
+ anchor: "center"
2268
+ }
2269
+ },
2270
+ // Quote text
2271
+ {
2272
+ type: "text",
2273
+ id: "quote-text",
2274
+ content: {
2275
+ text,
2276
+ style: {
2277
+ fontSize: quoteFontSize,
2278
+ color: "#ffffff",
2279
+ textAlign: "center",
2280
+ lineHeight: 1.5,
2281
+ shadow: true
2282
+ }
2283
+ },
2284
+ position: {
2285
+ x: "50%",
2286
+ y: attribution ? "45%" : "50%",
2287
+ anchor: "center",
2288
+ width: "80%"
2289
+ },
2290
+ animation: { type: "fadeIn", duration: 2 }
2291
+ }
2292
+ ];
2293
+ if (attribution) {
2294
+ layers.push({
2295
+ type: "text",
2296
+ id: "attribution",
2297
+ content: {
2298
+ text: `\u2014 ${attribution}`,
2299
+ style: {
2300
+ fontSize: attrFontSize,
2301
+ color: "rgba(255, 255, 255, 0.7)",
2302
+ textAlign: "center",
2303
+ shadow: true
2304
+ }
2305
+ },
2306
+ position: {
2307
+ x: "50%",
2308
+ y: "72%",
2309
+ anchor: "center"
2310
+ },
2311
+ animation: { type: "fadeIn", duration: 1, delay: 1.5 }
2312
+ });
2313
+ }
2314
+ return layers;
2315
+ }
2316
+
2317
+ // src/doc/templates/coverBlock.ts
2318
+ function coverBlock(input, context) {
2319
+ const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
2320
+ const { theme, layout } = context;
2321
+ const titleFontSize = scaledFontSize(96, context, true);
2322
+ const subtitleFontSize = scaledFontSize(36, context, false);
2323
+ const imageAnimation = mapAmbientMotion(ambientMotion);
2324
+ const layers = [
2325
+ // Full-screen hero image
2326
+ {
2327
+ type: "image",
2328
+ id: "cover-hero",
2329
+ content: {
2330
+ src: heroSrc,
2331
+ alt: heroAlt || title,
2332
+ fit: "cover",
2333
+ credit: heroCredit,
2334
+ license: heroLicense
2335
+ },
2336
+ position: { x: 0, y: 0, width: "100%", height: "100%" },
2337
+ animation: imageAnimation
2338
+ },
2339
+ // Gradient overlay for text readability - from bottom
2340
+ {
2341
+ type: "shape",
2342
+ id: "cover-gradient",
2343
+ content: {
2344
+ shape: "rect",
2345
+ fill: "linear-gradient(0deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 40%, rgba(0,0,0,0.1) 70%, transparent 100%)"
2346
+ },
2347
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
2348
+ },
2349
+ // Title - positioned lower in the frame to work with hero composition
2350
+ {
2351
+ type: "text",
2352
+ id: "cover-title",
2353
+ content: {
2354
+ text: title,
2355
+ style: {
2356
+ fontSize: titleFontSize,
2357
+ fontWeight: "bold",
2358
+ color: theme.colors.text,
2359
+ textAlign: "center",
2360
+ shadow: true
2361
+ }
2362
+ },
2363
+ position: {
2364
+ x: "50%",
2365
+ y: subtitle ? "70%" : "75%",
2366
+ anchor: "center",
2367
+ width: layout.maxTextWidth
2368
+ },
2369
+ // No animation delay - shown immediately at rest
2370
+ animation: { type: "fadeIn", duration: 0.8 }
2371
+ }
2372
+ ];
2373
+ if (subtitle) {
2374
+ layers.push({
2375
+ type: "text",
2376
+ id: "cover-subtitle",
2377
+ content: {
2378
+ text: subtitle,
2379
+ style: {
2380
+ fontSize: subtitleFontSize,
2381
+ color: theme.colors.textMuted,
2382
+ textAlign: "center",
2383
+ lineHeight: 1.5
2384
+ }
2385
+ },
2386
+ position: {
2387
+ x: "50%",
2388
+ y: "82%",
2389
+ anchor: "center",
2390
+ width: layout.maxTextWidth
2391
+ },
2392
+ animation: { type: "fadeIn", duration: 0.8, delay: 0.2 }
2393
+ });
2394
+ }
2395
+ return layers;
2396
+ }
2397
+ function startBlockToCoverInput(config) {
2398
+ return {
2399
+ heroSrc: config.heroSrc,
2400
+ heroAlt: config.heroAlt,
2401
+ title: config.title,
2402
+ subtitle: config.subtitle,
2403
+ ambientMotion: config.ambientMotion,
2404
+ heroCredit: config.heroCredit,
2405
+ heroLicense: config.heroLicense
2406
+ };
2407
+ }
2408
+ function expandCoverBlock(config, context) {
2409
+ const input = startBlockToCoverInput(config);
2410
+ return coverBlock(input, context);
2411
+ }
2412
+
2413
+ // src/doc/templates/index.ts
2414
+ var templateRegistry = {
2415
+ titleBlock,
2416
+ sectionHeader,
2417
+ statHighlight,
2418
+ quoteBlock,
2419
+ factCard,
2420
+ twoColumn,
2421
+ dateEvent,
2422
+ imageWithCaption,
2423
+ mapBlock,
2424
+ fullBleedQuote,
2425
+ listBlock,
2426
+ photoGrid,
2427
+ definitionCard,
2428
+ comparisonBar,
2429
+ pullQuote,
2430
+ videoWithCaption,
2431
+ videoPullQuote
2432
+ };
2433
+ function expandTemplateBlock(templateBlock, context) {
2434
+ const templateFn = templateRegistry[templateBlock.template];
2435
+ if (!templateFn) {
2436
+ console.warn(`Unknown template: ${templateBlock.template}`);
2437
+ return {
2438
+ id: templateBlock.id,
2439
+ startTime: 0,
2440
+ duration: templateBlock.duration,
2441
+ audioSegment: templateBlock.audioSegment,
2442
+ transition: templateBlock.transition,
2443
+ template: templateBlock.template
2444
+ // Preserve for debugging
2445
+ };
2446
+ }
2447
+ let layers;
2448
+ try {
2449
+ layers = templateFn(
2450
+ templateBlock,
2451
+ context
2452
+ );
2453
+ if (!Array.isArray(layers)) {
2454
+ console.error(
2455
+ `Template ${templateBlock.template} did not return an array, got:`,
2456
+ typeof layers
2457
+ );
2458
+ layers = [];
2459
+ }
2460
+ } catch (err) {
2461
+ console.error(`Error expanding template ${templateBlock.template}:`, err);
2462
+ layers = [];
2463
+ }
2464
+ return {
2465
+ id: templateBlock.id,
2466
+ startTime: 0,
2467
+ // Will be calculated later
2468
+ duration: templateBlock.duration,
2469
+ audioSegment: templateBlock.audioSegment,
2470
+ ...layers.length > 0 ? { layers } : {},
2471
+ transition: templateBlock.transition,
2472
+ template: templateBlock.template
2473
+ // Preserve for debugging
2474
+ };
2475
+ }
2476
+ function expandDocBlocks(blocks, options = {}) {
2477
+ const opts = options;
2478
+ const theme = opts.theme ?? DEFAULT_THEME;
2479
+ const viewport = opts.viewport ?? VIEWPORT_PRESETS.landscape;
2480
+ const { persistentLayers, audioSegments } = opts;
2481
+ const totalBlocks = blocks.length;
2482
+ const bottomLayers = expandPersistentLayers(persistentLayers?.bottomLayers);
2483
+ const topLayers = expandPersistentLayers(persistentLayers?.topLayers);
2484
+ if (!audioSegments || audioSegments.length === 0) {
2485
+ let currentTime = 0;
2486
+ return blocks.map((block, index) => {
2487
+ const context = createTemplateContext(theme, index, totalBlocks, viewport);
2488
+ const expandedBlock = isTemplateBlock(block) ? expandTemplateBlock(block, context) : block;
2489
+ const templateBlock = block;
2490
+ const useBottom = templateBlock.useBottomLayer !== false;
2491
+ const useTop = templateBlock.useTopLayer !== false;
2492
+ if (bottomLayers.length > 0 || topLayers.length > 0) {
2493
+ expandedBlock.layers = [
2494
+ ...useBottom ? bottomLayers : [],
2495
+ ...expandedBlock.layers ?? [],
2496
+ ...useTop ? topLayers : []
2497
+ ];
2498
+ }
2499
+ expandedBlock.startTime = currentTime;
2500
+ currentTime += expandedBlock.duration;
2501
+ return expandedBlock;
2502
+ });
2503
+ }
2504
+ const blocksBySegment = /* @__PURE__ */ new Map();
2505
+ blocks.forEach((block, index) => {
2506
+ const segmentIndex = block.audioSegment ?? 0;
2507
+ if (!blocksBySegment.has(segmentIndex)) {
2508
+ blocksBySegment.set(segmentIndex, []);
2509
+ }
2510
+ blocksBySegment.get(segmentIndex).push({ block, originalIndex: index });
2511
+ });
2512
+ const expandedBlocks = new Array(blocks.length);
2513
+ for (const [segmentIndex, segmentBlocks] of blocksBySegment) {
2514
+ const audioSegment = audioSegments[segmentIndex];
2515
+ if (!audioSegment) {
2516
+ let offsetTime = 0;
2517
+ for (const { block, originalIndex } of segmentBlocks) {
2518
+ const context = createTemplateContext(theme, originalIndex, totalBlocks, viewport);
2519
+ const expandedBlock = isTemplateBlock(block) ? expandTemplateBlock(block, context) : block;
2520
+ const templateBlock = block;
2521
+ const useBottom = templateBlock.useBottomLayer !== false;
2522
+ const useTop = templateBlock.useTopLayer !== false;
2523
+ if (bottomLayers.length > 0 || topLayers.length > 0) {
2524
+ expandedBlock.layers = [
2525
+ ...useBottom ? bottomLayers : [],
2526
+ ...expandedBlock.layers ?? [],
2527
+ ...useTop ? topLayers : []
2528
+ ];
2529
+ }
2530
+ expandedBlock.startTime = offsetTime;
2531
+ offsetTime += expandedBlock.duration;
2532
+ expandedBlocks[originalIndex] = expandedBlock;
2533
+ }
2534
+ continue;
2535
+ }
2536
+ const sectionHeaderBlocks = [];
2537
+ const contentBlocks = [];
2538
+ for (const item of segmentBlocks) {
2539
+ const templateBlock = item.block;
2540
+ if (templateBlock.template === "sectionHeader") {
2541
+ sectionHeaderBlocks.push(item);
2542
+ } else {
2543
+ contentBlocks.push(item);
2544
+ }
2545
+ }
2546
+ const fixedDuration = sectionHeaderBlocks.reduce((sum, { block }) => {
2547
+ return sum + (block.duration ?? 0);
2548
+ }, 0);
2549
+ const contentBlockDuration = contentBlocks.reduce((sum, { block }) => {
2550
+ return sum + (block.duration ?? 0);
2551
+ }, 0);
2552
+ const remainingDuration = audioSegment.duration - fixedDuration;
2553
+ const scaleFactor = contentBlockDuration > 0 && remainingDuration > 0 ? remainingDuration / contentBlockDuration : 1;
2554
+ const expandedInfos = [];
2555
+ for (const { block, originalIndex } of segmentBlocks) {
2556
+ const context = createTemplateContext(theme, originalIndex, totalBlocks, viewport);
2557
+ const expandedBlock = isTemplateBlock(block) ? expandTemplateBlock(block, context) : block;
2558
+ const templateBlock = block;
2559
+ const useBottom = templateBlock.useBottomLayer !== false;
2560
+ const useTop = templateBlock.useTopLayer !== false;
2561
+ if (bottomLayers.length > 0 || topLayers.length > 0) {
2562
+ expandedBlock.layers = [
2563
+ ...useBottom ? bottomLayers : [],
2564
+ ...expandedBlock.layers ?? [],
2565
+ ...useTop ? topLayers : []
2566
+ ];
2567
+ }
2568
+ expandedInfos.push({
2569
+ block: expandedBlock,
2570
+ originalIndex,
2571
+ templateBlock,
2572
+ hasSourceTiming: typeof templateBlock.sourceStartTime === "number"
2573
+ });
2574
+ }
2575
+ const hasAnySourceTiming = expandedInfos.some((info) => info.hasSourceTiming);
2576
+ expandedInfos.sort((a, b) => {
2577
+ if (hasAnySourceTiming) {
2578
+ const aIsHeader = a.templateBlock.template === "sectionHeader";
2579
+ const bIsHeader = b.templateBlock.template === "sectionHeader";
2580
+ if (aIsHeader && !bIsHeader) return -1;
2581
+ if (!aIsHeader && bIsHeader) return 1;
2582
+ if (a.hasSourceTiming && b.hasSourceTiming) {
2583
+ return a.templateBlock.sourceStartTime - b.templateBlock.sourceStartTime;
2584
+ }
2585
+ if (a.hasSourceTiming && !b.hasSourceTiming) return -1;
2586
+ if (!a.hasSourceTiming && b.hasSourceTiming) return 1;
2587
+ }
2588
+ return a.originalIndex - b.originalIndex;
2589
+ });
2590
+ let offsetWithinSegment = 0;
2591
+ for (let i = 0; i < expandedInfos.length; i++) {
2592
+ const info = expandedInfos[i];
2593
+ const { block: expandedBlock, templateBlock } = info;
2594
+ if (info.hasSourceTiming) {
2595
+ const sourceStart = templateBlock.sourceStartTime;
2596
+ expandedBlock.startTime = audioSegment.startTime + sourceStart;
2597
+ if (typeof templateBlock.sourceDuration === "number") {
2598
+ expandedBlock.duration = Math.max(5, templateBlock.sourceDuration);
2599
+ }
2600
+ offsetWithinSegment = sourceStart + expandedBlock.duration;
2601
+ } else if (templateBlock.template === "sectionHeader") {
2602
+ expandedBlock.startTime = audioSegment.startTime + offsetWithinSegment;
2603
+ offsetWithinSegment += expandedBlock.duration;
2604
+ } else {
2605
+ expandedBlock.startTime = audioSegment.startTime + offsetWithinSegment;
2606
+ const scaledDuration = expandedBlock.duration * scaleFactor;
2607
+ expandedBlock.duration = scaledDuration;
2608
+ offsetWithinSegment += scaledDuration;
2609
+ }
2610
+ expandedBlocks[info.originalIndex] = expandedBlock;
2611
+ }
2612
+ const segmentExpandedBlocks = expandedInfos.map((info) => expandedBlocks[info.originalIndex]).sort((a, b) => a.startTime - b.startTime);
2613
+ for (let i = 0; i < segmentExpandedBlocks.length - 1; i++) {
2614
+ const current = segmentExpandedBlocks[i];
2615
+ const next = segmentExpandedBlocks[i + 1];
2616
+ const currentEnd = current.startTime + current.duration;
2617
+ if (currentEnd > next.startTime) {
2618
+ current.duration = next.startTime - current.startTime;
2619
+ } else if (currentEnd < next.startTime - 0.5) {
2620
+ current.duration = Math.max(0.1, next.startTime - current.startTime);
2621
+ }
2622
+ }
2623
+ const MIN_TRANSITION_GAP = 5;
2624
+ const segmentEnd = audioSegment.startTime + audioSegment.duration;
2625
+ while (segmentExpandedBlocks.length > 1) {
2626
+ const lastBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 1];
2627
+ const timeFromLastToEnd = segmentEnd - lastBlock.startTime;
2628
+ if (timeFromLastToEnd < MIN_TRANSITION_GAP && lastBlock.template !== "sectionHeader") {
2629
+ const prevBlock = segmentExpandedBlocks[segmentExpandedBlocks.length - 2];
2630
+ prevBlock.duration = segmentEnd - prevBlock.startTime;
2631
+ lastBlock.duration = 0;
2632
+ lastBlock.startTime = segmentEnd;
2633
+ segmentExpandedBlocks.pop();
2634
+ } else {
2635
+ if (lastBlock.startTime + lastBlock.duration < segmentEnd) {
2636
+ lastBlock.duration = segmentEnd - lastBlock.startTime;
2637
+ }
2638
+ break;
2639
+ }
2640
+ }
2641
+ if (segmentExpandedBlocks.length === 1) {
2642
+ const onlyBlock = segmentExpandedBlocks[0];
2643
+ if (onlyBlock.startTime + onlyBlock.duration < segmentEnd) {
2644
+ onlyBlock.duration = segmentEnd - onlyBlock.startTime;
2645
+ }
2646
+ }
2647
+ let changed = true;
2648
+ while (changed) {
2649
+ changed = false;
2650
+ for (let i = segmentExpandedBlocks.length - 1; i >= 1; i--) {
2651
+ const block = segmentExpandedBlocks[i];
2652
+ if (block.duration > 0 && block.duration < MIN_TRANSITION_GAP) {
2653
+ const prev = segmentExpandedBlocks[i - 1];
2654
+ const blockEnd = block.startTime + block.duration;
2655
+ prev.duration = blockEnd - prev.startTime;
2656
+ block.duration = 0;
2657
+ block.startTime = segmentEnd;
2658
+ segmentExpandedBlocks.splice(i, 1);
2659
+ changed = true;
2660
+ break;
2661
+ }
2662
+ }
2663
+ }
2664
+ const MAX_BLOCK_DURATION = 20;
2665
+ for (let i = 0; i < segmentExpandedBlocks.length; i++) {
2666
+ const block = segmentExpandedBlocks[i];
2667
+ if (block.duration > MAX_BLOCK_DURATION) {
2668
+ const numParts = Math.ceil(block.duration / MAX_BLOCK_DURATION);
2669
+ const partDuration = block.duration / numParts;
2670
+ if (partDuration >= MIN_TRANSITION_GAP) {
2671
+ block.duration = partDuration;
2672
+ for (let p = 1; p < numParts; p++) {
2673
+ const splitBlock = {
2674
+ id: `${block.id}-split-${p}`,
2675
+ startTime: block.startTime + p * partDuration,
2676
+ duration: partDuration,
2677
+ audioSegment: block.audioSegment,
2678
+ layers: (block.layers ?? []).map((layer) => ({
2679
+ ...layer,
2680
+ id: `${layer.id}-split-${p}`
2681
+ })),
2682
+ transition: { type: "dissolve", duration: 1 },
2683
+ template: block.template
2684
+ };
2685
+ expandedBlocks.push(splitBlock);
2686
+ }
2687
+ }
2688
+ }
2689
+ }
2690
+ }
2691
+ return expandedBlocks.filter((block) => block && block.duration > 0);
2692
+ }
2693
+ function getAvailableTemplates() {
2694
+ return Object.keys(templateRegistry);
2695
+ }
2696
+ function hasTemplate(name) {
2697
+ return name in templateRegistry;
2698
+ }
2699
+
2700
+ // src/doc/utils/animationUtils.ts
2701
+ function getAnimationStyle(animation, _currentTime = 0) {
2702
+ if (!animation || animation.type === "none") {
2703
+ return { className: "", style: {} };
2704
+ }
2705
+ const className = getAnimationClassName(animation);
2706
+ const style = getAnimationCSSVars(animation);
2707
+ return { className, style };
2708
+ }
2709
+ function getAnimationClassName(animation) {
2710
+ const { type, direction, panDirection } = animation;
2711
+ switch (type) {
2712
+ case "fadeIn":
2713
+ return "anim-fadeIn";
2714
+ case "fadeOut":
2715
+ return "anim-fadeOut";
2716
+ case "slowZoom":
2717
+ if (panDirection === "left") return "anim-slowZoom-panLeft";
2718
+ if (panDirection === "right") return "anim-slowZoom-panRight";
2719
+ if (direction === "out") return "anim-slowZoom-out";
2720
+ return "anim-slowZoom-in";
2721
+ case "zoomIn":
2722
+ return "anim-zoomIn";
2723
+ case "zoomOut":
2724
+ return "anim-zoomOut";
2725
+ case "panLeft":
2726
+ return "anim-panLeft";
2727
+ case "panRight":
2728
+ return "anim-panRight";
2729
+ case "typewriter":
2730
+ return "anim-typewriter";
2731
+ default:
2732
+ return "";
2733
+ }
2734
+ }
2735
+ function getAnimationCSSVars(animation) {
2736
+ const vars = {};
2737
+ if (animation.duration !== void 0) {
2738
+ vars["--anim-duration"] = `${animation.duration}s`;
2739
+ }
2740
+ if (animation.delay !== void 0) {
2741
+ vars["--anim-delay"] = `${animation.delay}s`;
2742
+ }
2743
+ if (animation.easing) {
2744
+ vars["--anim-easing"] = animation.easing;
2745
+ }
2746
+ return vars;
2747
+ }
2748
+ function getDefaultAnimationDuration(type) {
2749
+ switch (type) {
2750
+ case "slowZoom":
2751
+ case "panLeft":
2752
+ case "panRight":
2753
+ return 8;
2754
+ // Long, slow animations
2755
+ case "typewriter":
2756
+ return 3;
2757
+ case "fadeIn":
2758
+ case "fadeOut":
2759
+ return 1;
2760
+ case "zoomIn":
2761
+ case "zoomOut":
2762
+ return 0.5;
2763
+ default:
2764
+ return 1;
2765
+ }
2766
+ }
2767
+ function getTransitionClass(type, entering) {
2768
+ const mode = entering ? "enter" : "exit";
2769
+ return `transition-${type}-${mode}`;
2770
+ }
2771
+ function getAnimationProgress(animation, currentTime, slideDuration) {
2772
+ const delay = animation.delay || 0;
2773
+ const duration = animation.duration || slideDuration;
2774
+ if (currentTime < delay) return 0;
2775
+ if (currentTime >= delay + duration) return 1;
2776
+ return (currentTime - delay) / duration;
2777
+ }
2778
+
2779
+ // src/doc/markdownToDoc.ts
2780
+ var SLUG_NON_WORD_RE = /[^\w\s-]/g;
2781
+ var SLUG_SPACES_RE = /[\s_]+/g;
2782
+ var SLUG_MULTI_HYPHEN_RE = /-+/g;
2783
+ var SLUG_TRIM_HYPHEN_RE = /^-|-$/g;
2784
+ function slugify(text) {
2785
+ return text.toLowerCase().replace(SLUG_NON_WORD_RE, "").replace(SLUG_SPACES_RE, "-").replace(SLUG_MULTI_HYPHEN_RE, "-").replace(SLUG_TRIM_HYPHEN_RE, "") || // Trim leading/trailing hyphens
2786
+ "block";
2787
+ }
2788
+ function createIdGenerator() {
2789
+ const used = /* @__PURE__ */ new Map();
2790
+ return (heading, _index) => {
2791
+ const text = extractPlainText(heading);
2792
+ const base = slugify(text);
2793
+ const count = used.get(base) ?? 0;
2794
+ used.set(base, count + 1);
2795
+ if (count === 0) return base;
2796
+ return `${base}-${count + 1}`;
2797
+ };
2798
+ }
2799
+ function markdownToDoc(markdownDoc, options) {
2800
+ const articleId = options?.articleId ?? "markdown-doc";
2801
+ const defaultTemplate = options?.defaultTemplate ?? "sectionHeader";
2802
+ const defaultDuration = options?.defaultDuration ?? 5;
2803
+ const generateId = options?.generateId ?? createIdGenerator();
2804
+ const rootBlocks = [];
2805
+ let headingIndex = 0;
2806
+ const stack = [];
2807
+ let pendingContents = [];
2808
+ let currentBlock = null;
2809
+ function flushContents() {
2810
+ if (currentBlock && pendingContents.length > 0) {
2811
+ currentBlock.contents = (currentBlock.contents ?? []).concat(pendingContents);
2812
+ pendingContents = [];
2813
+ }
2814
+ }
2815
+ function makeBlock(heading) {
2816
+ const id = heading ? generateId(heading, headingIndex++) : "preamble";
2817
+ const annotation = heading?.templateAnnotation;
2818
+ const template = annotation?.template ?? (heading ? defaultTemplate : void 0);
2819
+ const block = {
2820
+ id,
2821
+ startTime: 0,
2822
+ duration: defaultDuration,
2823
+ audioSegment: 0,
2824
+ template,
2825
+ ...heading ? { sourceHeading: heading } : {}
2826
+ };
2827
+ if (annotation?.params) {
2828
+ block.templateOverrides = annotation.params;
2829
+ }
2830
+ return block;
2831
+ }
2832
+ for (const node of markdownDoc.children) {
2833
+ if (node.type === "heading") {
2834
+ const heading = node;
2835
+ const depth = heading.depth;
2836
+ flushContents();
2837
+ const newBlock = makeBlock(heading);
2838
+ if (stack.length === 0) {
2839
+ if (currentBlock && !currentBlock.sourceHeading) {
2840
+ rootBlocks.push(currentBlock);
2841
+ }
2842
+ rootBlocks.push(newBlock);
2843
+ stack.push({ block: newBlock, depth });
2844
+ } else {
2845
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
2846
+ stack.pop();
2847
+ }
2848
+ if (stack.length === 0) {
2849
+ rootBlocks.push(newBlock);
2850
+ stack.push({ block: newBlock, depth });
2851
+ } else {
2852
+ const parent = stack[stack.length - 1].block;
2853
+ if (!parent.children) parent.children = [];
2854
+ parent.children.push(newBlock);
2855
+ stack.push({ block: newBlock, depth });
2856
+ }
2857
+ }
2858
+ currentBlock = newBlock;
2859
+ } else {
2860
+ if (!currentBlock) {
2861
+ currentBlock = makeBlock(null);
2862
+ }
2863
+ pendingContents.push(node);
2864
+ }
2865
+ }
2866
+ flushContents();
2867
+ if (currentBlock && !currentBlock.sourceHeading && !rootBlocks.includes(currentBlock)) {
2868
+ rootBlocks.push(currentBlock);
2869
+ }
2870
+ const allBlocks = flattenBlocks(rootBlocks);
2871
+ let currentTime = 0;
2872
+ for (const block of allBlocks) {
2873
+ block.startTime = currentTime;
2874
+ currentTime += block.duration;
2875
+ }
2876
+ return {
2877
+ articleId,
2878
+ duration: currentTime,
2879
+ blocks: rootBlocks,
2880
+ audio: {
2881
+ segments: []
2882
+ },
2883
+ ...markdownDoc.frontmatter ? { frontmatter: markdownDoc.frontmatter } : {}
2884
+ };
2885
+ }
2886
+ function flattenBlocks(blocks) {
2887
+ const result = [];
2888
+ for (const block of blocks) {
2889
+ result.push(block);
2890
+ if (block.children) {
2891
+ result.push(...flattenBlocks(block.children));
2892
+ }
2893
+ }
2894
+ return result;
2895
+ }
2896
+ function countBlocks(blocks) {
2897
+ let count = 0;
2898
+ for (const block of blocks) {
2899
+ count += 1;
2900
+ if (block.children) {
2901
+ count += countBlocks(block.children);
2902
+ }
2903
+ }
2904
+ return count;
2905
+ }
2906
+ function getBlockDepth(block) {
2907
+ return block.sourceHeading?.depth ?? 0;
2908
+ }
2909
+
2910
+ // src/doc/docToMarkdown.ts
2911
+ function docToMarkdown(doc) {
2912
+ const children = [];
2913
+ function emitBlock(block) {
2914
+ if (block.sourceHeading) {
2915
+ const heading = ensureAnnotation(block, block.sourceHeading);
2916
+ children.push(heading);
2917
+ }
2918
+ if (block.contents) {
2919
+ children.push(...block.contents);
2920
+ }
2921
+ if (block.children) {
2922
+ for (const child of block.children) {
2923
+ emitBlock(child);
2924
+ }
2925
+ }
2926
+ }
2927
+ for (const block of doc.blocks) {
2928
+ emitBlock(block);
2929
+ }
2930
+ return {
2931
+ type: "document",
2932
+ children
2933
+ };
2934
+ }
2935
+ function ensureAnnotation(block, heading) {
2936
+ if (heading.templateAnnotation) return heading;
2937
+ const hasExplicitTemplate = block.template && block.template !== "sectionHeader";
2938
+ const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
2939
+ if (!hasExplicitTemplate && !hasOverrides) return heading;
2940
+ return {
2941
+ ...heading,
2942
+ children: [...heading.children],
2943
+ templateAnnotation: {
2944
+ template: block.template ?? "sectionHeader",
2945
+ ...hasOverrides ? { params: block.templateOverrides } : {}
2946
+ }
2947
+ };
2948
+ }
2949
+
2950
+ // src/doc/getLayers.ts
2951
+ function getLayers(block, context = {}) {
2952
+ const theme = context.theme ?? DEFAULT_THEME;
2953
+ const viewport = context.viewport ?? VIEWPORT_PRESETS.landscape;
2954
+ const blockIndex = context.blockIndex ?? 0;
2955
+ const totalBlocks = context.totalBlocks ?? 1;
2956
+ const existingLayers = block.layers;
2957
+ if (existingLayers && existingLayers.length > 0 && !isTemplateBlock(block)) {
2958
+ return injectPersistentLayers(existingLayers, block, context);
2959
+ }
2960
+ if (isTemplateBlock(block) && block.template in templateRegistry) {
2961
+ const templateName = block.template;
2962
+ const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
2963
+ let layers;
2964
+ try {
2965
+ const templateFn = templateRegistry[templateName];
2966
+ layers = templateFn(block, templateCtx);
2967
+ if (!Array.isArray(layers)) {
2968
+ console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
2969
+ layers = [];
2970
+ }
2971
+ } catch (err) {
2972
+ console.error(`Error expanding template ${templateName}:`, err);
2973
+ layers = [];
2974
+ }
2975
+ return injectPersistentLayers(layers, block, context);
2976
+ }
2977
+ return injectPersistentLayers([], block, context);
2978
+ }
2979
+ function injectPersistentLayers(layers, block, context) {
2980
+ const { persistentLayers } = context;
2981
+ if (!persistentLayers) return layers;
2982
+ const bottomLayers = expandPersistentLayers(persistentLayers.bottomLayers);
2983
+ const topLayers = expandPersistentLayers(persistentLayers.topLayers);
2984
+ if (bottomLayers.length === 0 && topLayers.length === 0) return layers;
2985
+ const templateBlock = block;
2986
+ const useBottom = templateBlock.useBottomLayer !== false;
2987
+ const useTop = templateBlock.useTopLayer !== false;
2988
+ return [...useBottom ? bottomLayers : [], ...layers, ...useTop ? topLayers : []];
2989
+ }
2990
+
2991
+ export {
2992
+ expandPersistentLayers,
2993
+ getDocStyleConfig,
2994
+ titleBlock,
2995
+ resolveColorScheme,
2996
+ themedFontSize,
2997
+ scaleAnimationDuration,
2998
+ getDefaultAnimation,
2999
+ shouldUseShadow,
3000
+ getOverlayOpacity,
3001
+ getTemplateHint,
3002
+ sectionHeader,
3003
+ DEFAULT_LAYOUT,
3004
+ getAccentLayout,
3005
+ createAccentLayers,
3006
+ adjustY,
3007
+ statHighlight,
3008
+ quoteBlock,
3009
+ factCard,
3010
+ twoColumn,
3011
+ dateEvent,
3012
+ imageWithCaption,
3013
+ mapBlock,
3014
+ fullBleedQuote,
3015
+ listBlock,
3016
+ photoGrid,
3017
+ definitionCard,
3018
+ comparisonBar,
3019
+ pullQuote,
3020
+ videoWithCaption,
3021
+ videoPullQuote,
3022
+ coverBlock,
3023
+ expandCoverBlock,
3024
+ templateRegistry,
3025
+ expandTemplateBlock,
3026
+ expandDocBlocks,
3027
+ getAvailableTemplates,
3028
+ hasTemplate,
3029
+ getAnimationStyle,
3030
+ getDefaultAnimationDuration,
3031
+ getTransitionClass,
3032
+ getAnimationProgress,
3033
+ markdownToDoc,
3034
+ flattenBlocks,
3035
+ countBlocks,
3036
+ getBlockDepth,
3037
+ docToMarkdown,
3038
+ getLayers
3039
+ };
3040
+ //# sourceMappingURL=chunk-PJ7AODIM.js.map