@bendyline/squisq 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/dist/{Doc-BeSeH2iX.d.ts → Doc-BsMkmzH8.d.ts} +838 -268
  2. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-DvoeHMP9.d.ts} +11 -4
  3. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  4. package/dist/chunk-2OIBZYKP.js.map +1 -0
  5. package/dist/chunk-2UQOHYE2.js +45 -0
  6. package/dist/chunk-2UQOHYE2.js.map +1 -0
  7. package/dist/{chunk-B4EEON3N.js → chunk-4AKUQPEE.js} +536 -103
  8. package/dist/chunk-4AKUQPEE.js.map +1 -0
  9. package/dist/chunk-4ZFRZBU2.js +6582 -0
  10. package/dist/chunk-4ZFRZBU2.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-7MQGNR76.js} +79 -3
  12. package/dist/chunk-7MQGNR76.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-MYIH7FWD.js → chunk-FBKP5CAO.js} +1 -1
  16. package/dist/chunk-FBKP5CAO.js.map +1 -0
  17. package/dist/chunk-KZNXZ5SM.js +333 -0
  18. package/dist/chunk-KZNXZ5SM.js.map +1 -0
  19. package/dist/{chunk-WMOI7FN3.js → chunk-LRCV7N7F.js} +12 -2
  20. package/dist/{chunk-WMOI7FN3.js.map → chunk-LRCV7N7F.js.map} +1 -1
  21. package/dist/{chunk-3K5OG6KI.js → chunk-OH43RPTP.js} +403 -187
  22. package/dist/chunk-OH43RPTP.js.map +1 -0
  23. package/dist/{chunk-H3AXU4MJ.js → chunk-OKJOQXK4.js} +149 -2
  24. package/dist/chunk-OKJOQXK4.js.map +1 -0
  25. package/dist/chunk-PGGGKYWG.js +1 -0
  26. package/dist/chunk-PGGGKYWG.js.map +1 -0
  27. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  28. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  29. package/dist/chunk-QE7TWGVT.js +178 -0
  30. package/dist/chunk-QE7TWGVT.js.map +1 -0
  31. package/dist/chunk-RUDYOTA6.js +345 -0
  32. package/dist/chunk-RUDYOTA6.js.map +1 -0
  33. package/dist/chunk-TVSGLUID.js +596 -0
  34. package/dist/chunk-TVSGLUID.js.map +1 -0
  35. package/dist/chunk-ZLQKNOZQ.js +1 -0
  36. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  37. package/dist/doc/index.d.ts +4 -4
  38. package/dist/doc/index.js +124 -6
  39. package/dist/generate/index.d.ts +9 -2
  40. package/dist/generate/index.js +1 -1
  41. package/dist/icons/index.d.ts +3 -26
  42. package/dist/icons/index.js +13 -0
  43. package/dist/icons/inlineIconMarker.d.ts +1 -0
  44. package/dist/icons/inlineIconMarker.js +15 -0
  45. package/dist/icons/inlineIconMarker.js.map +1 -0
  46. package/dist/imageEdit/index.d.ts +11 -5
  47. package/dist/imageEdit/index.js +4 -1
  48. package/dist/index.d.ts +11 -8
  49. package/dist/index.js +243 -24
  50. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  51. package/dist/markdown/index.d.ts +212 -3
  52. package/dist/markdown/index.js +40 -7
  53. package/dist/recommend/index.d.ts +1 -1
  54. package/dist/recommend/index.js +3 -2
  55. package/dist/schemas/index.d.ts +28 -8
  56. package/dist/schemas/index.js +35 -5
  57. package/dist/spatial/index.d.ts +9 -4
  58. package/dist/spatial/index.js +1 -1
  59. package/dist/story/index.d.ts +906 -40
  60. package/dist/story/index.js +124 -6
  61. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-DQQKuRMx.d.ts} +1 -1
  62. package/dist/transform/index.d.ts +48 -10
  63. package/dist/transform/index.js +17 -481
  64. package/dist/transform/index.js.map +1 -1
  65. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  66. package/package.json +6 -1
  67. package/src/__tests__/animationUtils.test.ts +40 -0
  68. package/src/__tests__/annotationCoercion.test.ts +243 -0
  69. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  70. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  71. package/src/__tests__/attrTokens.test.ts +162 -0
  72. package/src/__tests__/customTemplateSample.test.ts +83 -0
  73. package/src/__tests__/customTemplatesFrontmatter.test.ts +205 -0
  74. package/src/__tests__/customThemesFrontmatter.test.ts +118 -0
  75. package/src/__tests__/diagramBlock.test.ts +122 -0
  76. package/src/__tests__/diagramLayout.test.ts +134 -0
  77. package/src/__tests__/drawing.test.ts +109 -0
  78. package/src/__tests__/drawingBlock.test.ts +124 -0
  79. package/src/__tests__/drawingLayout.test.ts +171 -0
  80. package/src/__tests__/geohash.test.ts +6 -5
  81. package/src/__tests__/getLayers.test.ts +6 -2
  82. package/src/__tests__/getLayersFallback.test.ts +103 -0
  83. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  84. package/src/__tests__/imageEditVersions.test.ts +72 -0
  85. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  86. package/src/__tests__/layoutBlock.test.ts +83 -0
  87. package/src/__tests__/layoutLayout.test.ts +169 -0
  88. package/src/__tests__/markdown.test.ts +23 -0
  89. package/src/__tests__/markdownSanitize.test.ts +64 -0
  90. package/src/__tests__/markdownToDoc.test.ts +223 -2
  91. package/src/__tests__/mediaAnnotations.test.ts +64 -0
  92. package/src/__tests__/mediaSchedule.test.ts +118 -0
  93. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  94. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  95. package/src/__tests__/rootExports.test.ts +43 -0
  96. package/src/__tests__/shapeGeometry.test.ts +89 -0
  97. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  98. package/src/__tests__/structuredData.test.ts +179 -0
  99. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  100. package/src/__tests__/templateMetadata.test.ts +21 -0
  101. package/src/__tests__/templates.test.ts +145 -4
  102. package/src/__tests__/themeCompile.test.ts +42 -0
  103. package/src/__tests__/themeValidator.test.ts +6 -0
  104. package/src/__tests__/transformV2.test.ts +137 -0
  105. package/src/__tests__/transitionNormalize.test.ts +61 -0
  106. package/src/__tests__/validateDoc.test.ts +100 -0
  107. package/src/doc/customTemplatesFrontmatter.ts +308 -0
  108. package/src/doc/customThemesFrontmatter.ts +90 -0
  109. package/src/doc/docToMarkdown.ts +73 -4
  110. package/src/doc/getLayers.ts +40 -17
  111. package/src/doc/index.ts +35 -1
  112. package/src/doc/markdownToDoc.ts +333 -12
  113. package/src/doc/mediaAnnotations.ts +118 -0
  114. package/src/doc/resolveDocTheme.ts +55 -0
  115. package/src/doc/structuredData.ts +230 -0
  116. package/src/doc/templateInputs.ts +240 -0
  117. package/src/doc/templates/__tests__/customTemplate.test.ts +151 -0
  118. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  119. package/src/doc/templates/accentImage.ts +30 -6
  120. package/src/doc/templates/captionUtils.ts +23 -0
  121. package/src/doc/templates/comparisonBar.ts +26 -18
  122. package/src/doc/templates/coverBlock.ts +7 -4
  123. package/src/doc/templates/customTemplate.ts +45 -0
  124. package/src/doc/templates/dataTable.ts +37 -19
  125. package/src/doc/templates/dateEvent.ts +41 -20
  126. package/src/doc/templates/definitionCard.ts +26 -18
  127. package/src/doc/templates/diagramBlock.ts +204 -0
  128. package/src/doc/templates/diagramLayout.ts +159 -0
  129. package/src/doc/templates/drawingBlock.ts +325 -0
  130. package/src/doc/templates/drawingLayout.ts +539 -0
  131. package/src/doc/templates/factCard.ts +51 -25
  132. package/src/doc/templates/fallbackBlock.ts +123 -0
  133. package/src/doc/templates/featureBlock.ts +20 -13
  134. package/src/doc/templates/fullBleedQuote.ts +18 -8
  135. package/src/doc/templates/imageWithCaption.ts +22 -9
  136. package/src/doc/templates/index.ts +215 -46
  137. package/src/doc/templates/layoutBlock.ts +43 -0
  138. package/src/doc/templates/layoutLayout.ts +243 -0
  139. package/src/doc/templates/listBlock.ts +56 -30
  140. package/src/doc/templates/mapBlock.ts +15 -12
  141. package/src/doc/templates/metadata.ts +120 -0
  142. package/src/doc/templates/persistentLayers.ts +129 -11
  143. package/src/doc/templates/photoGrid.ts +23 -12
  144. package/src/doc/templates/pullQuote.ts +30 -12
  145. package/src/doc/templates/quoteBlock.ts +54 -29
  146. package/src/doc/templates/sectionHeader.ts +25 -11
  147. package/src/doc/templates/statHighlight.ts +39 -22
  148. package/src/doc/templates/titleBlock.ts +59 -21
  149. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +221 -0
  150. package/src/doc/templates/tokens/resolveTokens.ts +0 -0
  151. package/src/doc/templates/twoColumn.ts +48 -30
  152. package/src/doc/templates/videoPullQuote.ts +22 -12
  153. package/src/doc/templates/videoWithCaption.ts +13 -8
  154. package/src/doc/utils/animationUtils.ts +267 -3
  155. package/src/doc/utils/applyRenderStyle.ts +113 -0
  156. package/src/doc/utils/imageTreatment.ts +70 -0
  157. package/src/doc/utils/shapeGeometry.ts +464 -0
  158. package/src/doc/utils/themeUtils.ts +75 -1
  159. package/src/doc/validate.ts +476 -0
  160. package/src/generate/slideshowGenerator.ts +7 -0
  161. package/src/icons/index.ts +8 -0
  162. package/src/icons/inlineIconMarker.ts +73 -0
  163. package/src/imageEdit/export.ts +85 -2
  164. package/src/imageEdit/index.ts +1 -0
  165. package/src/imageEdit/persistence.ts +51 -0
  166. package/src/index.ts +2 -0
  167. package/src/markdown/annotationCoercion.ts +295 -0
  168. package/src/markdown/attrTokens.ts +222 -0
  169. package/src/markdown/convert.ts +229 -26
  170. package/src/markdown/index.ts +36 -0
  171. package/src/markdown/sanitize.ts +298 -0
  172. package/src/markdown/stringify.ts +84 -3
  173. package/src/markdown/types.ts +66 -0
  174. package/src/markdown/utils.ts +49 -1
  175. package/src/recommend/templates.ts +24 -0
  176. package/src/schemas/BlockTemplates.ts +133 -4
  177. package/src/schemas/CustomTemplates.ts +74 -0
  178. package/src/schemas/Doc.ts +353 -29
  179. package/src/schemas/ImageEditDoc.ts +11 -4
  180. package/src/schemas/Media.ts +169 -0
  181. package/src/schemas/Theme.ts +23 -1
  182. package/src/schemas/Transitions.ts +224 -0
  183. package/src/schemas/colorUtils.ts +33 -0
  184. package/src/schemas/fontStacks.ts +5 -2
  185. package/src/schemas/index.ts +3 -0
  186. package/src/schemas/themeCompile.ts +23 -4
  187. package/src/schemas/themeValidator.ts +49 -11
  188. package/src/schemas/themes/cinematic.json +69 -12
  189. package/src/schemas/themes/documentary.json +58 -11
  190. package/src/schemas/themes/gezellig.json +60 -11
  191. package/src/schemas/themes/magazine.json +54 -9
  192. package/src/schemas/themes/morning-light.json +52 -9
  193. package/src/schemas/themes/standard-dark.json +48 -13
  194. package/src/schemas/themes/standard.json +44 -12
  195. package/src/schemas/themes/tech-dark.json +65 -12
  196. package/src/spatial/Geohash.ts +52 -21
  197. package/src/transform/applyTransform.ts +43 -4
  198. package/src/transform/index.ts +1 -0
  199. package/src/transform/registry.ts +44 -13
  200. package/src/transform/styles/dataDriven.ts +2 -1
  201. package/src/transform/styles/documentary.ts +2 -1
  202. package/src/transform/styles/magazine.ts +3 -1
  203. package/src/transform/styles/minimal.ts +2 -1
  204. package/src/transform/styles/narrative.ts +3 -1
  205. package/src/transform/templateSelector.ts +87 -11
  206. package/src/transform/types.ts +35 -2
  207. package/dist/chunk-3K5OG6KI.js.map +0 -1
  208. package/dist/chunk-57CVQCFX.js.map +0 -1
  209. package/dist/chunk-5WFRKURX.js.map +0 -1
  210. package/dist/chunk-7UDSRZKG.js.map +0 -1
  211. package/dist/chunk-B4EEON3N.js.map +0 -1
  212. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  213. package/dist/chunk-KSWJR66U.js +0 -3706
  214. package/dist/chunk-KSWJR66U.js.map +0 -1
  215. package/dist/chunk-MYIH7FWD.js.map +0 -1
  216. package/dist/chunk-ZTX4PFFT.js.map +0 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Fallback Block Rendering
3
+ *
4
+ * The graceful-degradation guarantee: a block whose template can't render —
5
+ * unknown template name, template function threw, or returned a non-array —
6
+ * still renders its heading and body text as a plain, readable card instead
7
+ * of a blank slide. A small notice line names the problem so authors (and
8
+ * agents reviewing screenshots) can see *why* the block degraded.
9
+ *
10
+ * This is intentionally not a registered template: it can't be requested by
11
+ * name, only reached when a requested template fails. See `getLayers()`.
12
+ */
13
+
14
+ import type { Layer } from '../../schemas/Doc.js';
15
+ import type { Block } from '../../schemas/Doc.js';
16
+ import type { DocBlock, TemplateContext } from '../../schemas/BlockTemplates.js';
17
+ import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
18
+ import { extractPlainText } from '../../markdown/utils.js';
19
+
20
+ /** Cap body text so a long section doesn't overflow the card. */
21
+ const MAX_BODY_CHARS = 600;
22
+
23
+ /**
24
+ * Render a block's heading + body text as plain layers, with a notice line
25
+ * describing why the requested template didn't render.
26
+ *
27
+ * @param block The block that failed to render via its template.
28
+ * @param context Template context (theme, viewport).
29
+ * @param notice Short problem description, e.g. `Unknown template "photGrid"`.
30
+ */
31
+ export function fallbackBlockLayers(
32
+ block: DocBlock,
33
+ context: TemplateContext,
34
+ notice: string,
35
+ ): Layer[] {
36
+ const colors = resolveColorScheme(context, 'blue');
37
+ const { layout } = context;
38
+
39
+ const title = (block as Block).title ?? (block as Block).id ?? '';
40
+ const body = extractBodyText(block as Block);
41
+
42
+ const layers: Layer[] = [
43
+ {
44
+ type: 'shape',
45
+ id: 'fallback-bg',
46
+ content: { shape: 'rect', fill: colors.bg },
47
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
48
+ },
49
+ ];
50
+
51
+ if (title) {
52
+ layers.push({
53
+ type: 'text',
54
+ id: 'fallback-title',
55
+ content: {
56
+ text: title,
57
+ style: {
58
+ fontSize: themedFontSize(56, context, true),
59
+ fontFamily: getThemeFont(context, 'title'),
60
+ fontWeight: 'bold',
61
+ color: colors.text,
62
+ textAlign: 'center',
63
+ },
64
+ },
65
+ position: {
66
+ x: '50%',
67
+ y: body ? '28%' : '50%',
68
+ anchor: 'center',
69
+ width: layout.maxTextWidth,
70
+ },
71
+ });
72
+ }
73
+
74
+ if (body) {
75
+ layers.push({
76
+ type: 'text',
77
+ id: 'fallback-body',
78
+ content: {
79
+ text: body,
80
+ style: {
81
+ fontSize: themedFontSize(28, context),
82
+ fontFamily: getThemeFont(context, 'body'),
83
+ color: colors.text,
84
+ textAlign: 'center',
85
+ },
86
+ },
87
+ position: {
88
+ x: '50%',
89
+ y: '55%',
90
+ anchor: 'center',
91
+ width: layout.maxTextWidth,
92
+ },
93
+ });
94
+ }
95
+
96
+ layers.push({
97
+ type: 'text',
98
+ id: 'fallback-notice',
99
+ content: {
100
+ text: `⚠ ${notice}`,
101
+ style: {
102
+ fontSize: themedFontSize(18, context),
103
+ fontFamily: getThemeFont(context, 'body'),
104
+ color: `${colors.text}99`,
105
+ textAlign: 'center',
106
+ },
107
+ },
108
+ position: { x: '50%', y: '92%', anchor: 'center', width: layout.maxTextWidth },
109
+ });
110
+
111
+ return layers;
112
+ }
113
+
114
+ /** Plain text of a block's body contents, truncated for card display. */
115
+ function extractBodyText(block: Block): string {
116
+ if (!block.contents || block.contents.length === 0) return '';
117
+ const text = block.contents
118
+ .map((node) => extractPlainText(node))
119
+ .join('\n')
120
+ .trim();
121
+ if (text.length <= MAX_BODY_CHARS) return text;
122
+ return `${text.slice(0, MAX_BODY_CHARS).trimEnd()}…`;
123
+ }
@@ -18,8 +18,12 @@ import type {
18
18
  RightFeatureInput,
19
19
  TemplateContext,
20
20
  } from '../../schemas/BlockTemplates.js';
21
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
22
- import { getThemeFont } from '../utils/themeUtils.js';
21
+ import {
22
+ getThemeFont,
23
+ themedEntrance,
24
+ themedFontSize,
25
+ themedImageTreatment,
26
+ } from '../utils/themeUtils.js';
23
27
 
24
28
  type FeatureInput = LeftFeatureInput | RightFeatureInput;
25
29
 
@@ -34,6 +38,8 @@ function buildFeatureLayers(
34
38
  ): Layer[] {
35
39
  const { imageSrc, imageAlt, imageWidth, imageHeight, title, body } = input;
36
40
  const { theme, layout } = context;
41
+
42
+ const treatment = themedImageTreatment(context, input.imageTreatment);
37
43
  // Treat the image as "sized" when the host gave us an explicit width
38
44
  // or height — that's our cue that the user resized the image in the
39
45
  // editor and we should respect that as a sizing hint instead of
@@ -44,8 +50,8 @@ function buildFeatureLayers(
44
50
  // behave consistently on narrow viewports.
45
51
  const stack = layout.stackColumns;
46
52
 
47
- const titleFontSize = scaledFontSize(48, context, true);
48
- const bodyFontSize = scaledFontSize(24, context, false);
53
+ const titleFontSize = themedFontSize(48, context, true);
54
+ const bodyFontSize = themedFontSize(24, context, false);
49
55
 
50
56
  // Image takes the full left or right half. The text column gets the
51
57
  // opposite half, with a comfortable inset so the text doesn't kiss
@@ -90,17 +96,17 @@ function buildFeatureLayers(
90
96
  }
91
97
 
92
98
  // Text-column geometry. `textX` is the LEFT edge of the column when
93
- // the side is "left" (image left, text right), and the RIGHT edge of
94
- // the column when the side is "right" we use top-left / top-right
95
- // anchors below so text reads left-aligned in both cases without
96
- // having to flip the column origin manually.
99
+ // the side is "left" (image left, text right); when the side is
100
+ // "right" the column occupies the left half, inset from the card edge.
101
+ // The mirror lives in the layout only fully right-aligned
102
+ // (ragged-left) running text on rightFeature was genuinely hard to read.
97
103
  const COLUMN_INSET = 4; // % of block width — padding from card edge / divider
98
104
  const textColumnWidth = stack ? 90 : 42;
99
105
  const textX = stack
100
106
  ? `${(100 - textColumnWidth) / 2}%`
101
107
  : side === 'left'
102
108
  ? `${50 + COLUMN_INSET}%` // text column starts just past the divider
103
- : `${50 - COLUMN_INSET}%`; // right edge of left-half text column
109
+ : `${COLUMN_INSET + 2}%`; // left edge of the left-half text column
104
110
 
105
111
  // Stack the title above the body, with the pair vertically centered
106
112
  // in the text column. The title sits 8% above center; body sits 2%
@@ -108,8 +114,8 @@ function buildFeatureLayers(
108
114
  // sizes used by the inline preview gutter without overflowing.
109
115
  const titleY = stack ? '60%' : body ? '42%' : '50%';
110
116
  const bodyY = stack ? '78%' : title ? '55%' : '50%';
111
- const textAnchor = side === 'right' && !stack ? 'top-right' : 'top-left';
112
- const textAlign: 'left' | 'right' = side === 'right' && !stack ? 'right' : 'left';
117
+ const textAnchor = 'top-left';
118
+ const textAlign = 'left' as const;
113
119
 
114
120
  // Paint our own background so the text column has a theme-paired
115
121
  // surface to sit on. The host wrapper's surface isn't always theme-
@@ -136,6 +142,7 @@ function buildFeatureLayers(
136
142
  src: imageSrc,
137
143
  alt: imageAlt ?? title ?? '',
138
144
  fit: imageFit,
145
+ ...(treatment ? { treatment } : {}),
139
146
  },
140
147
  position: { x: imgX, y: imgY, width: imgW, height: imgH },
141
148
  });
@@ -162,7 +169,7 @@ function buildFeatureLayers(
162
169
  anchor: textAnchor,
163
170
  width: `${textColumnWidth}%`,
164
171
  },
165
- animation: { type: 'fadeIn', duration: 0.8 },
172
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 0.8 }),
166
173
  });
167
174
  }
168
175
 
@@ -186,7 +193,7 @@ function buildFeatureLayers(
186
193
  anchor: textAnchor,
187
194
  width: `${textColumnWidth}%`,
188
195
  },
189
- animation: { type: 'fadeIn', duration: 0.8, delay: 0.2 },
196
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 0.8, delay: 0.2 }),
190
197
  });
191
198
  }
192
199
 
@@ -10,8 +10,14 @@
10
10
 
11
11
  import type { Layer } from '../../schemas/Doc.js';
12
12
  import type { FullBleedQuoteInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
- import { resolveColorScheme, getThemeFont } from '../utils/themeUtils.js';
13
+ import {
14
+ resolveColorScheme,
15
+ getTemplateHint,
16
+ getThemeFont,
17
+ shouldUseShadow,
18
+ themedFontSize,
19
+ } from '../utils/themeUtils.js';
20
+ import { oklchDarken } from '../../schemas/colorUtils.js';
15
21
 
16
22
  /**
17
23
  * Hint schema published for the theme validator + future customizer hint UI.
@@ -28,20 +34,21 @@ export const fullBleedQuoteHintSchema = {
28
34
 
29
35
  export function fullBleedQuote(input: FullBleedQuoteInput, context: TemplateContext): Layer[] {
30
36
  const { text, colorScheme = 'blue' } = input;
31
- const { theme } = context;
32
37
  const colors = resolveColorScheme(context, colorScheme);
33
38
 
34
39
  // Massive font for dramatic impact
35
- const textFontSize = scaledFontSize(120, context, true);
40
+ const textFontSize = themedFontSize(120, context, true);
36
41
 
37
42
  return [
38
- // Background — dark radial vignette for cinematic feel
43
+ // Background — radial vignette built from the color scheme's own
44
+ // surface, so the card takes on the scheme (and the theme behind it)
45
+ // instead of vignetting every theme into hard black.
39
46
  {
40
47
  type: 'shape',
41
48
  id: 'bg',
42
49
  content: {
43
50
  shape: 'rect',
44
- fill: `radial-gradient(ellipse at 50% 50%, ${theme.colors.backgroundLight} 0%, #000000 100%)`,
51
+ fill: `radial-gradient(ellipse at 50% 50%, ${colors.bg} 0%, ${oklchDarken(colors.bg, 0.12)} 100%)`,
45
52
  },
46
53
  position: { x: 0, y: 0, width: '100%', height: '100%' },
47
54
  },
@@ -58,7 +65,7 @@ export function fullBleedQuote(input: FullBleedQuoteInput, context: TemplateCont
58
65
  color: colors.text,
59
66
  textAlign: 'center',
60
67
  lineHeight: 1.2,
61
- shadow: true,
68
+ shadow: shouldUseShadow(context),
62
69
  },
63
70
  },
64
71
  position: {
@@ -67,7 +74,10 @@ export function fullBleedQuote(input: FullBleedQuoteInput, context: TemplateCont
67
74
  anchor: 'center',
68
75
  width: '85%',
69
76
  },
70
- animation: { type: 'fadeIn', duration: 1.5 },
77
+ animation:
78
+ getTemplateHint<string>(context, 'fullBleedQuote', 'entrance', 'subtle') === 'dramatic'
79
+ ? { type: 'zoomIn', duration: 0.8 }
80
+ : { type: 'fadeIn', duration: 1.5 },
71
81
  },
72
82
  ];
73
83
  }
@@ -10,8 +10,13 @@
10
10
 
11
11
  import type { Layer } from '../../schemas/Doc.js';
12
12
  import type { ImageWithCaptionInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
- import { getThemeFont } from '../utils/themeUtils.js';
13
+ import {
14
+ getThemeFont,
15
+ shouldUseShadow,
16
+ themedFontSize,
17
+ themedImageTreatment,
18
+ } from '../utils/themeUtils.js';
19
+ import { withAlpha } from '../../schemas/colorUtils.js';
15
20
  import { cleanCaption } from './captionUtils.js';
16
21
  import { mapAmbientMotion } from './accentImage.js';
17
22
 
@@ -30,10 +35,12 @@ export function imageWithCaption(input: ImageWithCaptionInput, context: Template
30
35
  const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
31
36
  const { theme, layout } = context;
32
37
 
38
+ const treatment = themedImageTreatment(context, input.imageTreatment);
39
+
33
40
  // Scale font sizes for viewport
34
- const captionFontSize = scaledFontSize(36, context, false);
35
- const titleFontSize = scaledFontSize(96, context, true);
36
- const subtitleFontSize = scaledFontSize(36, context, false);
41
+ const captionFontSize = themedFontSize(36, context, false);
42
+ const titleFontSize = themedFontSize(96, context, true);
43
+ const subtitleFontSize = themedFontSize(36, context, false);
37
44
 
38
45
  // Determine animation based on ambientMotion setting
39
46
  // Support both new 'ambientMotion' and legacy 'kenBurns' property
@@ -54,12 +61,18 @@ export function imageWithCaption(input: ImageWithCaptionInput, context: Template
54
61
  fit: 'cover',
55
62
  credit: imageCredit,
56
63
  license: imageLicense,
64
+ ...(treatment ? { treatment } : {}),
57
65
  },
58
66
  position: { x: 0, y: 0, width: '100%', height: '100%' },
59
67
  animation: imageAnimation,
60
68
  },
61
69
  ];
62
70
 
71
+ // Scrim gradients are tinted from the theme background so the theme
72
+ // text color painted on top stays readable by construction — a fixed
73
+ // black scrim put dark theme text on a dark band in light themes.
74
+ const bg = theme.colors.background;
75
+
63
76
  // TITLE MODE: Large centered title over hero image (like cover slide)
64
77
  if (isTitle && caption) {
65
78
  // Full gradient overlay for title readability (similar to cover slide)
@@ -68,7 +81,7 @@ export function imageWithCaption(input: ImageWithCaptionInput, context: Template
68
81
  id: 'title-gradient',
69
82
  content: {
70
83
  shape: 'rect',
71
- 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%)',
84
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.45)} 40%, ${withAlpha(bg, 0.1)} 70%, ${withAlpha(bg, 0)} 100%)`,
72
85
  },
73
86
  position: { x: 0, y: 0, width: '100%', height: '100%' },
74
87
  });
@@ -85,7 +98,7 @@ export function imageWithCaption(input: ImageWithCaptionInput, context: Template
85
98
  fontWeight: 'bold',
86
99
  color: theme.colors.text,
87
100
  textAlign: 'center',
88
- shadow: true,
101
+ shadow: shouldUseShadow(context),
89
102
  },
90
103
  },
91
104
  position: {
@@ -133,7 +146,7 @@ export function imageWithCaption(input: ImageWithCaptionInput, context: Template
133
146
  id: 'caption-gradient',
134
147
  content: {
135
148
  shape: 'rect',
136
- fill: 'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 60%, transparent 100%)',
149
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.8)} 0%, ${withAlpha(bg, 0.35)} 60%, ${withAlpha(bg, 0)} 100%)`,
137
150
  },
138
151
  position: {
139
152
  x: 0,
@@ -154,7 +167,7 @@ export function imageWithCaption(input: ImageWithCaptionInput, context: Template
154
167
  fontFamily: getThemeFont(context, 'body'),
155
168
  color: theme.colors.text,
156
169
  textAlign: 'center',
157
- shadow: true,
170
+ shadow: shouldUseShadow(context),
158
171
  },
159
172
  },
160
173
  position: {