@bendyline/squisq 1.4.1 → 1.5.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 (254) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
  4. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
  5. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  6. package/dist/chunk-2OIBZYKP.js.map +1 -0
  7. package/dist/chunk-2UQOHYE2.js +45 -0
  8. package/dist/chunk-2UQOHYE2.js.map +1 -0
  9. package/dist/chunk-6ATE2PSM.js +344 -0
  10. package/dist/chunk-6ATE2PSM.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
  12. package/dist/chunk-A4O7GIWE.js.map +1 -0
  13. package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
  14. package/dist/chunk-DLXZMS5K.js.map +1 -0
  15. package/dist/chunk-ET53IIEP.js +1 -0
  16. package/dist/chunk-ET53IIEP.js.map +1 -0
  17. package/dist/chunk-HWVFJAAH.js +362 -0
  18. package/dist/chunk-HWVFJAAH.js.map +1 -0
  19. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  20. package/dist/chunk-IMSCRLLV.js.map +1 -0
  21. package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
  22. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  23. package/dist/chunk-LDQ2HJIX.js +596 -0
  24. package/dist/chunk-LDQ2HJIX.js.map +1 -0
  25. package/dist/chunk-LH7I6SH7.js +7208 -0
  26. package/dist/chunk-LH7I6SH7.js.map +1 -0
  27. package/dist/chunk-PGGGKYWG.js +1 -0
  28. package/dist/chunk-PGGGKYWG.js.map +1 -0
  29. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  30. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  31. package/dist/chunk-QE7TWGVT.js +178 -0
  32. package/dist/chunk-QE7TWGVT.js.map +1 -0
  33. package/dist/chunk-RUDYOTA6.js +345 -0
  34. package/dist/chunk-RUDYOTA6.js.map +1 -0
  35. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  36. package/dist/chunk-T5UK6YOB.js.map +1 -0
  37. package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
  38. package/dist/chunk-TEEEILMP.js.map +1 -0
  39. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  40. package/dist/chunk-TQWLI6S2.js.map +1 -0
  41. package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
  42. package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
  43. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  44. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  45. package/dist/chunk-ZLQKNOZQ.js +1 -0
  46. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  47. package/dist/doc/index.d.ts +5 -5
  48. package/dist/doc/index.js +135 -8
  49. package/dist/generate/index.d.ts +3 -74
  50. package/dist/generate/index.js +2 -7
  51. package/dist/icons/index.d.ts +3 -26
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/inlineIconMarker.d.ts +1 -0
  54. package/dist/icons/inlineIconMarker.js +15 -0
  55. package/dist/icons/inlineIconMarker.js.map +1 -0
  56. package/dist/imageEdit/index.d.ts +12 -6
  57. package/dist/imageEdit/index.js +4 -1
  58. package/dist/index.d.ts +14 -11
  59. package/dist/index.js +265 -37
  60. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  61. package/dist/jsonForm/index.d.ts +42 -1
  62. package/dist/jsonForm/index.js +9 -1
  63. package/dist/markdown/index.d.ts +228 -3
  64. package/dist/markdown/index.js +42 -7
  65. package/dist/recommend/index.d.ts +1 -1
  66. package/dist/recommend/index.js +3 -2
  67. package/dist/schemas/index.d.ts +28 -8
  68. package/dist/schemas/index.js +36 -8
  69. package/dist/spatial/index.d.ts +9 -4
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.d.ts +2 -2
  72. package/dist/storage/index.js +1 -1
  73. package/dist/story/index.d.ts +1021 -41
  74. package/dist/story/index.js +135 -8
  75. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  76. package/dist/timing/index.js +3 -5
  77. package/dist/transform/index.d.ts +48 -10
  78. package/dist/transform/index.js +17 -481
  79. package/dist/transform/index.js.map +1 -1
  80. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  81. package/dist/versions/index.d.ts +1 -1
  82. package/package.json +6 -1
  83. package/src/__tests__/animationUtils.test.ts +40 -0
  84. package/src/__tests__/annotationCoercion.test.ts +243 -0
  85. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  86. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  87. package/src/__tests__/attrTokens.test.ts +162 -0
  88. package/src/__tests__/contentExtractor.test.ts +60 -0
  89. package/src/__tests__/coverBlock.test.ts +46 -1
  90. package/src/__tests__/customTemplateSample.test.ts +144 -0
  91. package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
  92. package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
  93. package/src/__tests__/diagramBlock.test.ts +122 -0
  94. package/src/__tests__/diagramLayout.test.ts +134 -0
  95. package/src/__tests__/drawing.test.ts +109 -0
  96. package/src/__tests__/drawingBlock.test.ts +124 -0
  97. package/src/__tests__/drawingLayout.test.ts +171 -0
  98. package/src/__tests__/geohash.test.ts +6 -5
  99. package/src/__tests__/getLayers.test.ts +6 -2
  100. package/src/__tests__/getLayersFallback.test.ts +103 -0
  101. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  102. package/src/__tests__/imageEditVersions.test.ts +72 -0
  103. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  104. package/src/__tests__/inputDescriptors.test.ts +162 -0
  105. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  106. package/src/__tests__/layoutBlock.test.ts +83 -0
  107. package/src/__tests__/layoutLayout.test.ts +169 -0
  108. package/src/__tests__/markdown.test.ts +87 -0
  109. package/src/__tests__/markdownSanitize.test.ts +79 -0
  110. package/src/__tests__/markdownToDoc.test.ts +391 -2
  111. package/src/__tests__/mediaAnnotations.test.ts +91 -0
  112. package/src/__tests__/mediaSchedule.test.ts +118 -0
  113. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  114. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  115. package/src/__tests__/rootExports.test.ts +43 -0
  116. package/src/__tests__/shapeGeometry.test.ts +89 -0
  117. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  118. package/src/__tests__/structuredData.test.ts +249 -0
  119. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  120. package/src/__tests__/templateMetadata.test.ts +31 -0
  121. package/src/__tests__/templates.test.ts +257 -4
  122. package/src/__tests__/themeCompile.test.ts +42 -0
  123. package/src/__tests__/themeValidator.test.ts +6 -0
  124. package/src/__tests__/transformV2.test.ts +137 -0
  125. package/src/__tests__/transitionNormalize.test.ts +61 -0
  126. package/src/__tests__/validateDoc.test.ts +260 -0
  127. package/src/doc/annotationBlocks.ts +117 -0
  128. package/src/doc/customTemplatesFrontmatter.ts +314 -0
  129. package/src/doc/customThemesFrontmatter.ts +95 -0
  130. package/src/doc/docToMarkdown.ts +104 -5
  131. package/src/doc/getLayers.ts +49 -17
  132. package/src/doc/index.ts +47 -1
  133. package/src/doc/markdownToDoc.ts +411 -13
  134. package/src/doc/mediaAnnotations.ts +100 -0
  135. package/src/doc/resolveDocTheme.ts +55 -0
  136. package/src/doc/standaloneAnnotation.ts +58 -0
  137. package/src/doc/structuredData.ts +301 -0
  138. package/src/doc/templateInputs.ts +240 -0
  139. package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
  140. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  141. package/src/doc/templates/accentImage.ts +30 -6
  142. package/src/doc/templates/captionUtils.ts +23 -0
  143. package/src/doc/templates/comparisonBar.ts +33 -18
  144. package/src/doc/templates/coverBlock.ts +35 -5
  145. package/src/doc/templates/customTemplate.ts +51 -0
  146. package/src/doc/templates/dataTable.ts +37 -19
  147. package/src/doc/templates/dateEvent.ts +41 -20
  148. package/src/doc/templates/definitionCard.ts +26 -18
  149. package/src/doc/templates/diagramBlock.ts +204 -0
  150. package/src/doc/templates/diagramLayout.ts +159 -0
  151. package/src/doc/templates/drawingBlock.ts +325 -0
  152. package/src/doc/templates/drawingLayout.ts +539 -0
  153. package/src/doc/templates/factCard.ts +54 -26
  154. package/src/doc/templates/fallbackBlock.ts +123 -0
  155. package/src/doc/templates/featureBlock.ts +20 -13
  156. package/src/doc/templates/fullBleedQuote.ts +18 -8
  157. package/src/doc/templates/imageWithCaption.ts +30 -15
  158. package/src/doc/templates/index.ts +225 -46
  159. package/src/doc/templates/inputDescriptors.ts +432 -0
  160. package/src/doc/templates/layoutBlock.ts +43 -0
  161. package/src/doc/templates/layoutLayout.ts +243 -0
  162. package/src/doc/templates/listBlock.ts +56 -30
  163. package/src/doc/templates/mapBlock.ts +15 -12
  164. package/src/doc/templates/metadata.ts +120 -0
  165. package/src/doc/templates/persistentLayers.ts +129 -11
  166. package/src/doc/templates/photoGrid.ts +22 -34
  167. package/src/doc/templates/pullQuote.ts +30 -12
  168. package/src/doc/templates/quoteBlock.ts +54 -29
  169. package/src/doc/templates/sectionHeader.ts +25 -11
  170. package/src/doc/templates/statHighlight.ts +39 -22
  171. package/src/doc/templates/titleBlock.ts +59 -21
  172. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
  173. package/src/doc/templates/tokens/resolveTokens.ts +453 -0
  174. package/src/doc/templates/twoColumn.ts +48 -30
  175. package/src/doc/templates/videoPullQuote.ts +22 -12
  176. package/src/doc/templates/videoWithCaption.ts +21 -15
  177. package/src/doc/utils/animationUtils.ts +267 -3
  178. package/src/doc/utils/applyRenderStyle.ts +113 -0
  179. package/src/doc/utils/imageTreatment.ts +70 -0
  180. package/src/doc/utils/nearest.ts +59 -0
  181. package/src/doc/utils/shapeGeometry.ts +464 -0
  182. package/src/doc/utils/themeUtils.ts +75 -1
  183. package/src/doc/validate.ts +575 -0
  184. package/src/generate/contentExtractor.ts +148 -35
  185. package/src/generate/index.ts +0 -3
  186. package/src/icons/index.ts +8 -0
  187. package/src/icons/inlineIconMarker.ts +73 -0
  188. package/src/imageEdit/export.ts +85 -2
  189. package/src/imageEdit/index.ts +1 -0
  190. package/src/imageEdit/persistence.ts +51 -0
  191. package/src/index.ts +2 -0
  192. package/src/jsonForm/index.ts +2 -0
  193. package/src/jsonForm/tokens.ts +78 -0
  194. package/src/markdown/annotationCoercion.ts +295 -0
  195. package/src/markdown/attrTokens.ts +241 -0
  196. package/src/markdown/convert.ts +229 -26
  197. package/src/markdown/index.ts +37 -0
  198. package/src/markdown/sanitize.ts +318 -0
  199. package/src/markdown/stringify.ts +96 -6
  200. package/src/markdown/types.ts +66 -0
  201. package/src/markdown/utils.ts +111 -2
  202. package/src/recommend/templates.ts +24 -0
  203. package/src/schemas/BlockTemplates.ts +133 -4
  204. package/src/schemas/CustomTemplates.ts +126 -0
  205. package/src/schemas/Doc.ts +387 -65
  206. package/src/schemas/ImageEditDoc.ts +11 -4
  207. package/src/schemas/Media.ts +169 -0
  208. package/src/schemas/Theme.ts +23 -1
  209. package/src/schemas/Transitions.ts +224 -0
  210. package/src/schemas/colorUtils.ts +33 -0
  211. package/src/schemas/fontStacks.ts +5 -2
  212. package/src/schemas/index.ts +3 -0
  213. package/src/schemas/themeCompile.ts +24 -5
  214. package/src/schemas/themeValidator.ts +49 -11
  215. package/src/schemas/themes/cinematic.json +69 -12
  216. package/src/schemas/themes/documentary.json +58 -11
  217. package/src/schemas/themes/gezellig.json +60 -11
  218. package/src/schemas/themes/magazine.json +54 -9
  219. package/src/schemas/themes/morning-light.json +52 -9
  220. package/src/schemas/themes/standard-dark.json +48 -13
  221. package/src/schemas/themes/standard.json +44 -12
  222. package/src/schemas/themes/tech-dark.json +65 -12
  223. package/src/spatial/Geohash.ts +52 -21
  224. package/src/storage/ContentContainer.ts +6 -2
  225. package/src/transform/applyTransform.ts +43 -4
  226. package/src/transform/index.ts +1 -0
  227. package/src/transform/registry.ts +44 -13
  228. package/src/transform/styles/dataDriven.ts +2 -1
  229. package/src/transform/styles/documentary.ts +2 -1
  230. package/src/transform/styles/magazine.ts +3 -1
  231. package/src/transform/styles/minimal.ts +2 -1
  232. package/src/transform/styles/narrative.ts +3 -1
  233. package/src/transform/templateSelector.ts +87 -11
  234. package/src/transform/types.ts +35 -2
  235. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  236. package/dist/chunk-3E5F2XMR.js.map +0 -1
  237. package/dist/chunk-3K5OG6KI.js.map +0 -1
  238. package/dist/chunk-4X3JQXNM.js +0 -35
  239. package/dist/chunk-4X3JQXNM.js.map +0 -1
  240. package/dist/chunk-57CVQCFX.js.map +0 -1
  241. package/dist/chunk-5WFRKURX.js.map +0 -1
  242. package/dist/chunk-7UDSRZKG.js.map +0 -1
  243. package/dist/chunk-B4EEON3N.js.map +0 -1
  244. package/dist/chunk-FR2RBTKO.js.map +0 -1
  245. package/dist/chunk-H3AXU4MJ.js +0 -179
  246. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  247. package/dist/chunk-K32VJONL.js.map +0 -1
  248. package/dist/chunk-KSWJR66U.js +0 -3706
  249. package/dist/chunk-KSWJR66U.js.map +0 -1
  250. package/dist/chunk-MYIH7FWD.js +0 -145
  251. package/dist/chunk-MYIH7FWD.js.map +0 -1
  252. package/dist/chunk-ZTX4PFFT.js.map +0 -1
  253. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  254. package/src/generate/slideshowGenerator.ts +0 -273
@@ -11,16 +11,27 @@
11
11
 
12
12
  import type { Layer, AnimationType } from '../../schemas/Doc.js';
13
13
  import type { SectionHeaderInput, TemplateContext } from '../../schemas/BlockTemplates.js';
14
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
15
- import { resolveColorScheme, getThemeFont } from '../utils/themeUtils.js';
14
+ import {
15
+ resolveColorScheme,
16
+ getThemeFont,
17
+ shouldUseShadow,
18
+ themedEntrance,
19
+ themedFontSize,
20
+ themedScrim,
21
+ themedImageTreatment,
22
+ } from '../utils/themeUtils.js';
16
23
 
17
24
  export function sectionHeader(input: SectionHeaderInput, context: TemplateContext): Layer[] {
18
25
  const { title = '', colorScheme = 'blue', imageSrc, imageAlt, ambientMotion } = input;
19
- const { layout } = context;
26
+ const { theme, layout } = context;
27
+
28
+ const treatment = themedImageTreatment(context, input.imageTreatment);
20
29
  const colors = resolveColorScheme(context, colorScheme);
21
30
 
22
- // Scale font sizes for viewport
23
- const titleFontSize = scaledFontSize(72, context, true);
31
+ // Scale font sizes for viewport. Section dividers are the loudest
32
+ // interstitial in a doc, so the title sits a step above ordinary
33
+ // content-block headings.
34
+ const titleFontSize = themedFontSize(84, context, true);
24
35
 
25
36
  const layers: Layer[] = [];
26
37
 
@@ -34,6 +45,7 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
34
45
  src: imageSrc,
35
46
  alt: imageAlt || title,
36
47
  fit: 'cover',
48
+ ...(treatment ? { treatment } : {}),
37
49
  },
38
50
  position: { x: 0, y: 0, width: '100%', height: '100%' },
39
51
  animation: ambientMotion
@@ -41,13 +53,14 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
41
53
  : { type: 'slowZoom', duration: 8, direction: 'in' },
42
54
  });
43
55
 
44
- // Dark overlay for text readability
56
+ // Theme-tinted overlay for text readability — light themes get a
57
+ // light scrim with dark theme text, dark themes the familiar dark one.
45
58
  layers.push({
46
59
  type: 'shape',
47
60
  id: 'overlay',
48
61
  content: {
49
62
  shape: 'rect',
50
- fill: 'rgba(0, 0, 0, 0.5)',
63
+ fill: themedScrim(context),
51
64
  },
52
65
  position: { x: 0, y: 0, width: '100%', height: '100%' },
53
66
  });
@@ -78,7 +91,8 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
78
91
  });
79
92
  }
80
93
 
81
- // Section title - white text with shadow for readability over images
94
+ // Section title theme text over the theme-tinted scrim (readable by
95
+ // construction), scheme text on the solid-color fallback
82
96
  layers.push({
83
97
  type: 'text',
84
98
  id: 'title',
@@ -88,9 +102,9 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
88
102
  fontSize: titleFontSize,
89
103
  fontFamily: getThemeFont(context, 'title'),
90
104
  fontWeight: 'bold',
91
- color: imageSrc ? '#ffffff' : colors.text,
105
+ color: imageSrc ? theme.colors.text : colors.text,
92
106
  textAlign: 'center',
93
- shadow: true,
107
+ shadow: shouldUseShadow(context),
94
108
  },
95
109
  },
96
110
  position: {
@@ -99,7 +113,7 @@ export function sectionHeader(input: SectionHeaderInput, context: TemplateContex
99
113
  anchor: 'center',
100
114
  width: layout.maxTextWidth,
101
115
  },
102
- animation: { type: 'fadeIn', duration: 1.5 },
116
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1.5 }),
103
117
  });
104
118
 
105
119
  return layers;
@@ -12,8 +12,15 @@
12
12
 
13
13
  import type { Layer } from '../../schemas/Doc.js';
14
14
  import type { StatHighlightInput, TemplateContext } from '../../schemas/BlockTemplates.js';
15
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
16
- import { resolveColorScheme, getThemeFont } from '../utils/themeUtils.js';
15
+ import {
16
+ resolveColorScheme,
17
+ getTemplateHint,
18
+ getThemeFont,
19
+ shouldUseShadow,
20
+ themedFontSize,
21
+ themedSurfaceGradient,
22
+ themedImageTreatment,
23
+ } from '../utils/themeUtils.js';
17
24
  import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
18
25
  import { createBackgroundLayer } from './captionUtils.js';
19
26
 
@@ -39,20 +46,21 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
39
46
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
40
47
 
41
48
  // Scale font sizes — stat is dramatically large, description is understated
42
- const statFontSize = scaledFontSize(148, context, true);
43
- const descFontSize = scaledFontSize(32, context, false);
44
- const detailFontSize = scaledFontSize(26, context, false);
49
+ const statFontSize = themedFontSize(148, context, true);
50
+ const descFontSize = themedFontSize(32, context, false);
51
+ const detailFontSize = themedFontSize(26, context, false);
45
52
 
46
- const layers: Layer[] = [
47
- createBackgroundLayer(
48
- 'bg',
49
- `linear-gradient(180deg, ${theme.colors.background} 0%, #0f1520 100%)`,
50
- ),
51
- ];
53
+ const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 180))];
52
54
 
53
55
  // Add accent image layers (behind text, after background)
54
56
  if (accentImage) {
55
- layers.push(...createAccentLayers(accentImage, input.id));
57
+ layers.push(
58
+ ...createAccentLayers(
59
+ accentImage,
60
+ input.id,
61
+ themedImageTreatment(context, input.imageTreatment),
62
+ ),
63
+ );
56
64
  }
57
65
 
58
66
  // Big stat — hero element, dominates the slide
@@ -66,18 +74,23 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
66
74
  fontFamily: getThemeFont(context, 'title'),
67
75
  fontWeight: 'bold',
68
76
  color: colors.text,
69
- shadow: !!accentImage,
77
+ shadow: shouldUseShadow(context),
70
78
  },
71
79
  },
72
80
  position: {
73
81
  x: accentLayout.textCenterX,
74
- y: adjustY('32%', accentLayout),
82
+ y: adjustY('36%', accentLayout),
75
83
  anchor: 'center',
76
84
  },
77
- animation: { type: 'zoomIn', duration: 0.6 },
85
+ animation:
86
+ getTemplateHint<string>(context, 'statHighlight', 'entrance', 'subtle') === 'dramatic'
87
+ ? { type: 'zoomIn', duration: 0.4 }
88
+ : { type: 'zoomIn', duration: 0.6 },
78
89
  });
79
90
 
80
- // Description — smaller and understated beneath the stat
91
+ // Description — smaller and understated beneath the stat. Sits close
92
+ // under the stat (the ~26% fixed gap it used to get read as two
93
+ // unrelated elements floating in the panel).
81
94
  layers.push({
82
95
  type: 'text',
83
96
  id: 'description',
@@ -88,13 +101,13 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
88
101
  fontFamily: getThemeFont(context, 'body'),
89
102
  color: theme.colors.textMuted,
90
103
  textAlign: 'center',
91
- lineHeight: 1.6,
92
- shadow: !!accentImage,
104
+ lineHeight: 1.5,
105
+ shadow: shouldUseShadow(context),
93
106
  },
94
107
  },
95
108
  position: {
96
109
  x: accentLayout.textCenterX,
97
- y: adjustY('58%', accentLayout),
110
+ y: adjustY('54%', accentLayout),
98
111
  width: accentLayout.textWidth,
99
112
  anchor: 'center',
100
113
  },
@@ -111,14 +124,18 @@ export function statHighlight(input: StatHighlightInput, context: TemplateContex
111
124
  style: {
112
125
  fontSize: detailFontSize,
113
126
  fontFamily: getThemeFont(context, 'body'),
114
- color: colors.accent,
127
+ // Scheme *text* rather than *accent*: accents in the built-in
128
+ // schemes are tuned as chart/fill colors and drop below legible
129
+ // contrast as body copy on light surfaces.
130
+ color: colors.text,
115
131
  textAlign: 'center',
116
- shadow: !!accentImage,
132
+ shadow: shouldUseShadow(context),
117
133
  },
118
134
  },
119
135
  position: {
120
136
  x: accentLayout.textCenterX,
121
- y: adjustY('75%', accentLayout),
137
+ y: adjustY('66%', accentLayout),
138
+ width: accentLayout.textWidth,
122
139
  anchor: 'center',
123
140
  },
124
141
  animation: { type: 'fadeIn', duration: 1, delay: 1 },
@@ -2,7 +2,9 @@
2
2
  * Title Block Template
3
3
  *
4
4
  * Large title with optional subtitle for doc intros.
5
- * Centered text with fade-in animations.
5
+ * Centered text with fade-in animations, composed as one lockup:
6
+ * accent rule, title, and subtitle are spaced from the title's
7
+ * estimated height instead of fixed slots that collide on wrap.
6
8
  * Adapts font sizes and positioning for different viewports.
7
9
  *
8
10
  * This is shared code used by both site and efb-app doc renderers.
@@ -10,8 +12,15 @@
10
12
 
11
13
  import type { Layer } from '../../schemas/Doc.js';
12
14
  import type { TitleBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
- import { getThemeFont } from '../utils/themeUtils.js';
15
+ import {
16
+ getTemplateHint,
17
+ getThemeFont,
18
+ shouldUseShadow,
19
+ themedEntrance,
20
+ themedFontSize,
21
+ } from '../utils/themeUtils.js';
22
+ import { withAlpha } from '../../schemas/colorUtils.js';
23
+ import { estimateTextHeight } from './captionUtils.js';
15
24
 
16
25
  /**
17
26
  * Hint schema published for the theme validator + future customizer hint UI.
@@ -24,40 +33,68 @@ export const titleBlockHintSchema = {
24
33
 
25
34
  export function titleBlock(input: TitleBlockInput, context: TemplateContext): Layer[] {
26
35
  const { title, subtitle, backgroundColor } = input;
27
- const { theme, layout } = context;
36
+ const { theme, layout, viewport } = context;
28
37
 
29
38
  // Scale font sizes for viewport
30
- const titleFontSize = scaledFontSize(96, context, true);
31
- const subtitleFontSize = scaledFontSize(36, context, false);
39
+ const titleFontSize = themedFontSize(96, context, true);
40
+ const subtitleFontSize = themedFontSize(36, context, false);
32
41
 
33
42
  const baseBg = backgroundColor || theme.colors.primary;
34
43
 
44
+ // Estimate the title's rendered height so the rule and subtitle key off
45
+ // its real extent — fixed slots let the rule overlap the ascenders and
46
+ // stranded the subtitle when the title wrapped.
47
+ const maxWidthPx = (parseFloat(layout.maxTextWidth) / 100) * viewport.width;
48
+ const titleH = estimateTextHeight(title, titleFontSize, maxWidthPx, 1.15);
49
+ const titleYPct = subtitle ? 42 : 48;
50
+ const px = (v: number) => (v / viewport.height) * 100;
51
+
35
52
  const layers: Layer[] = [
36
- // Background — radial gradient for depth instead of flat color
53
+ // Background — theme surface with a soft radial accent tint. The tint
54
+ // is translucent so the theme's text color keeps its contrast with the
55
+ // surface; a full-strength `primary` hotspot behind the title made
56
+ // dark-primary light themes unreadable.
37
57
  {
38
58
  type: 'shape',
39
59
  id: 'bg',
40
60
  content: {
41
61
  shape: 'rect',
42
- fill: `radial-gradient(ellipse at 50% 40%, ${baseBg} 0%, ${theme.colors.background} 100%)`,
62
+ fill: theme.colors.background,
43
63
  },
44
64
  position: { x: 0, y: 0, width: '100%', height: '100%' },
45
65
  },
46
- // Subtle decorative line above title
47
66
  {
67
+ type: 'shape',
68
+ id: 'bg-tint',
69
+ content: {
70
+ shape: 'rect',
71
+ fill: `radial-gradient(ellipse at 50% 40%, ${withAlpha(baseBg, 0.32)} 0%, ${withAlpha(baseBg, 0)} 75%)`,
72
+ },
73
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
74
+ },
75
+ ];
76
+
77
+ // Subtle decorative line, clear above the title's ascenders. Themes can
78
+ // hide it via `templateHints.title.showAccentLine: false` (the line is
79
+ // decorative; title/subtitle positions don't depend on it).
80
+ if (getTemplateHint(context, 'title', 'showAccentLine', true)) {
81
+ layers.push({
48
82
  type: 'shape',
49
83
  id: 'accent-line',
50
84
  content: {
51
85
  shape: 'rect',
52
- fill: 'rgba(255, 255, 255, 0.2)',
86
+ fill: withAlpha(theme.colors.text, 0.3),
53
87
  },
54
88
  position: {
55
- x: '40%',
56
- y: subtitle ? '28%' : '38%',
57
- width: '20%',
58
- height: '2px',
89
+ x: '44%',
90
+ y: `${titleYPct - px(titleH / 2 + 56)}%`,
91
+ width: '12%',
92
+ height: '3px',
59
93
  },
60
- },
94
+ });
95
+ }
96
+
97
+ layers.push(
61
98
  // Title
62
99
  {
63
100
  type: 'text',
@@ -70,21 +107,22 @@ export function titleBlock(input: TitleBlockInput, context: TemplateContext): La
70
107
  fontWeight: 'bold',
71
108
  color: theme.colors.text,
72
109
  textAlign: 'center',
73
- shadow: true,
110
+ shadow: shouldUseShadow(context),
74
111
  },
75
112
  },
76
113
  position: {
77
114
  x: '50%',
78
- y: subtitle ? layout.primaryY : '50%',
115
+ y: `${titleYPct}%`,
79
116
  anchor: 'center',
80
117
  width: layout.maxTextWidth,
81
118
  },
82
- animation: { type: 'fadeIn', duration: 2 },
119
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 2 }),
83
120
  },
84
- ];
121
+ );
85
122
 
86
- // Add subtitle if provided
123
+ // Add subtitle if provided — hangs from the title's estimated bottom edge
87
124
  if (subtitle) {
125
+ const subH = estimateTextHeight(subtitle, subtitleFontSize, maxWidthPx, 1.5);
88
126
  layers.push({
89
127
  type: 'text',
90
128
  id: 'subtitle',
@@ -100,7 +138,7 @@ export function titleBlock(input: TitleBlockInput, context: TemplateContext): La
100
138
  },
101
139
  position: {
102
140
  x: '50%',
103
- y: layout.secondaryY,
141
+ y: `${titleYPct + px(titleH / 2 + 56 + subH / 2)}%`,
104
142
  anchor: 'center',
105
143
  width: layout.maxTextWidth,
106
144
  },