@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
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Layout layout — pure derivation of positioned layers from a `layout`
3
+ * block's child sub-blocks. The layout counterpart to `drawingLayout.ts`,
4
+ * with two deliberate differences:
5
+ *
6
+ * - **Absolute** positioning. Each child's `x`/`y`/`width`/`height` are
7
+ * used as-is in viewport units (no auto-grid, no fit-to-viewport
8
+ * scaling). A layout is a free-form canvas; authors place things where
9
+ * they want them.
10
+ * - **Text content lives in the child body.** A `{[text …]}` child's
11
+ * paragraphs/lists/marks under its heading become the text — readable
12
+ * markdown rather than an encoded blob. (`content.html` is filled by the
13
+ * editor for rich on-canvas rendering; SSR uses the plain `content.text`.)
14
+ *
15
+ * Each child heading is one layer: `### {#id} {[<type> x=.. y=.. …]}`.
16
+ * Children are emitted in document order, so the first child is the
17
+ * back-most layer (painters' order).
18
+ */
19
+
20
+ import type { Layer, ShapeLayer, TextLayer, PathLayer, ImageLayer } from '../../schemas/Doc.js';
21
+ import { extractPlainText } from '../../markdown/utils.js';
22
+ import { normalizeShapeKind } from './drawingLayout.js';
23
+ import { shapePath } from '../utils/shapeGeometry.js';
24
+
25
+ export interface LayoutLayersResult {
26
+ layers: Layer[];
27
+ warnings: string[];
28
+ }
29
+
30
+ export interface LayoutLayerDefaults {
31
+ /**
32
+ * Default ink — text color and path/line stroke — used when the author
33
+ * omits `color=`/`stroke=`. `layoutBlock` passes the theme text color so
34
+ * un-styled text stays legible on dark themes; the editor's scene canvas
35
+ * omits it and gets the dark-slate default that suits its fixed light
36
+ * surface.
37
+ */
38
+ ink?: string;
39
+ }
40
+
41
+ /** Fallback ink for callers that don't supply defaults (editor scene canvas). */
42
+ const DEFAULT_INK = '#1e293b';
43
+
44
+ /** Minimal Block shape this derivation reads (subset of `Block`). */
45
+ interface LayoutChildBlock {
46
+ id: string;
47
+ template?: string;
48
+ templateOverrides?: Record<string, string>;
49
+ title?: string;
50
+ contents?: import('../../markdown/types.js').MarkdownNode[];
51
+ }
52
+
53
+ const DEFAULT_TEXT = { w: 300, h: 80 };
54
+ const DEFAULT_SHAPE = { w: 160, h: 90 };
55
+ const DEFAULT_IMAGE = { w: 320, h: 240 };
56
+
57
+ /**
58
+ * Map a layout block's children → positioned layers. Pure: same children
59
+ * always produce the same layers.
60
+ */
61
+ export function computeLayoutLayers(
62
+ children: readonly LayoutChildBlock[],
63
+ _viewport: { width: number; height: number },
64
+ defaults: LayoutLayerDefaults = {},
65
+ ): LayoutLayersResult {
66
+ const ink = defaults.ink ?? DEFAULT_INK;
67
+ const layers: Layer[] = [];
68
+ const warnings: string[] = [];
69
+
70
+ for (const child of children) {
71
+ const params = child.templateOverrides ?? {};
72
+ const annotation = (child.template ?? '').toLowerCase();
73
+ const x = numParam(params, 'x') ?? 0;
74
+ const y = numParam(params, 'y') ?? 0;
75
+
76
+ if (annotation === 'image' || annotation === 'img') {
77
+ const src = strParam(params, 'src');
78
+ if (!src) {
79
+ warnings.push(`Layout image "${child.id}" has no src= and was skipped`);
80
+ continue;
81
+ }
82
+ const layer: ImageLayer = {
83
+ id: child.id,
84
+ type: 'image',
85
+ position: {
86
+ x,
87
+ y,
88
+ width: numParam(params, 'width') ?? DEFAULT_IMAGE.w,
89
+ height: numParam(params, 'height') ?? DEFAULT_IMAGE.h,
90
+ },
91
+ content: {
92
+ src,
93
+ alt: strParam(params, 'alt') ?? '',
94
+ ...(strParam(params, 'fit')
95
+ ? { fit: strParam(params, 'fit') as 'cover' | 'contain' | 'fill' }
96
+ : {}),
97
+ },
98
+ };
99
+ layers.push(layer);
100
+ continue;
101
+ }
102
+
103
+ const kind = normalizeShapeKind(child.template);
104
+ if (!kind) {
105
+ warnings.push(
106
+ `Layout child "${child.id}" is not a layer (annotation "${child.template ?? '—'}") and was skipped`,
107
+ );
108
+ continue;
109
+ }
110
+
111
+ if (kind === 'text') {
112
+ const text = bodyText(child) || child.title || '';
113
+ const layer: TextLayer = {
114
+ id: child.id,
115
+ type: 'text',
116
+ position: {
117
+ x,
118
+ y,
119
+ width: numParam(params, 'width') ?? DEFAULT_TEXT.w,
120
+ height: numParam(params, 'height') ?? DEFAULT_TEXT.h,
121
+ },
122
+ content: { text, style: textStyleFromParams(params, ink) },
123
+ };
124
+ layers.push(layer);
125
+ continue;
126
+ }
127
+
128
+ const width = numParam(params, 'width') ?? numParam(params, 'w') ?? DEFAULT_SHAPE.w;
129
+ const height = numParam(params, 'height') ?? numParam(params, 'h') ?? DEFAULT_SHAPE.h;
130
+
131
+ if (kind === 'rect' || kind === 'circle') {
132
+ const shape: ShapeLayer = {
133
+ id: child.id,
134
+ type: 'shape',
135
+ position: { x, y, width, height },
136
+ content: {
137
+ shape: kind,
138
+ ...(strParam(params, 'fill') ? { fill: strParam(params, 'fill') } : {}),
139
+ ...(strParam(params, 'stroke') ? { stroke: strParam(params, 'stroke') } : {}),
140
+ ...(numParam(params, 'strokeWidth') != null
141
+ ? { strokeWidth: numParam(params, 'strokeWidth') }
142
+ : {}),
143
+ ...(strParam(params, 'borderStyle')
144
+ ? { borderStyle: strParam(params, 'borderStyle') as 'solid' | 'dashed' | 'dotted' }
145
+ : {}),
146
+ ...(kind === 'rect' && numParam(params, 'borderRadius') != null
147
+ ? { borderRadius: numParam(params, 'borderRadius') }
148
+ : {}),
149
+ },
150
+ };
151
+ layers.push(shape);
152
+ continue;
153
+ }
154
+
155
+ // line / arrow / path / named polygon → PathLayer (absolute geometry).
156
+ const diagonal = `M ${round(x)} ${round(y)} L ${round(x + width)} ${round(y + height)}`;
157
+ let d = diagonal;
158
+ let pathFill = 'none';
159
+ let endMarker: 'none' | 'arrow' = 'none';
160
+ if (kind === 'path') {
161
+ d = strParam(params, 'd') ?? diagonal;
162
+ pathFill = strParam(params, 'fill') ?? 'none';
163
+ } else if (kind === 'arrow') {
164
+ endMarker = 'arrow';
165
+ } else if (kind !== 'line') {
166
+ d = shapePath(kind, x, y, width, height) ?? diagonal;
167
+ pathFill = strParam(params, 'fill') ?? 'none';
168
+ }
169
+ const path: PathLayer = {
170
+ id: child.id,
171
+ type: 'path',
172
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
173
+ content: {
174
+ d,
175
+ stroke: strParam(params, 'stroke') ?? ink,
176
+ strokeWidth: numParam(params, 'strokeWidth') ?? 2,
177
+ fill: pathFill,
178
+ ...(strParam(params, 'dasharray') ? { dasharray: strParam(params, 'dasharray') } : {}),
179
+ ...(endMarker !== 'none' ? { endMarker } : {}),
180
+ },
181
+ };
182
+ layers.push(path);
183
+ }
184
+
185
+ return { layers, warnings };
186
+ }
187
+
188
+ /** Build a TextStyle from a text layer's annotation params. */
189
+ function textStyleFromParams(
190
+ params: Record<string, string>,
191
+ ink: string,
192
+ ): TextLayer['content']['style'] {
193
+ const style: TextLayer['content']['style'] = {
194
+ fontSize: numParam(params, 'fontSize') ?? 32,
195
+ color: strParam(params, 'color') ?? ink,
196
+ };
197
+ const fontFamily = strParam(params, 'fontFamily');
198
+ if (fontFamily) style.fontFamily = fontFamily;
199
+ const fontWeight = strParam(params, 'fontWeight');
200
+ if (fontWeight === 'bold' || fontWeight === 'normal') style.fontWeight = fontWeight;
201
+ const fontStyle = strParam(params, 'fontStyle');
202
+ if (fontStyle === 'italic' || fontStyle === 'normal') style.fontStyle = fontStyle;
203
+ const align = strParam(params, 'align');
204
+ if (align === 'left' || align === 'center' || align === 'right') style.textAlign = align;
205
+ const valign = strParam(params, 'valign');
206
+ if (valign === 'top' || valign === 'middle' || valign === 'bottom') style.verticalAlign = valign;
207
+ const lineHeight = numParam(params, 'lineHeight');
208
+ if (lineHeight != null) style.lineHeight = lineHeight;
209
+ const padding = numParam(params, 'padding');
210
+ if (padding != null) style.padding = padding;
211
+ const background = strParam(params, 'background');
212
+ if (background) style.background = background;
213
+ return style;
214
+ }
215
+
216
+ /** Read a raw string param (trims a trailing comma — `x=21, y=25` tolerated). */
217
+ function strParam(params: Record<string, string>, key: string): string | undefined {
218
+ const raw = params[key];
219
+ if (raw == null) return undefined;
220
+ const v = raw.replace(/,\s*$/, '').trim();
221
+ return v.length > 0 ? v : undefined;
222
+ }
223
+
224
+ /** Coerce a numeric param, tolerating a trailing comma. Undefined on failure. */
225
+ function numParam(params: Record<string, string>, key: string): number | undefined {
226
+ const v = strParam(params, key);
227
+ if (v == null) return undefined;
228
+ const n = Number(v);
229
+ return Number.isFinite(n) ? n : undefined;
230
+ }
231
+
232
+ /** Plain text of a block's body content. */
233
+ function bodyText(block: LayoutChildBlock): string {
234
+ if (!block.contents || block.contents.length === 0) return '';
235
+ return block.contents
236
+ .map((node) => extractPlainText(node))
237
+ .join('\n')
238
+ .trim();
239
+ }
240
+
241
+ function round(n: number): number {
242
+ return Math.round(n * 100) / 100;
243
+ }
@@ -10,8 +10,13 @@
10
10
 
11
11
  import type { Layer } from '../../schemas/Doc.js';
12
12
  import type { ListBlockInput, 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
+ themedEntrance,
17
+ themedFontSize,
18
+ themedImageTreatment,
19
+ } from '../utils/themeUtils.js';
15
20
  import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
16
21
 
17
22
  export function listBlock(input: ListBlockInput, context: TemplateContext): Layer[] {
@@ -27,8 +32,8 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
27
32
  // Get layout adjustments if accent image is present
28
33
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
29
34
 
30
- const titleFontSize = scaledFontSize(44, context, true);
31
- const itemFontSize = scaledFontSize(34, context, false);
35
+ const titleFontSize = themedFontSize(44, context, true);
36
+ const itemFontSize = themedFontSize(34, context, false);
32
37
 
33
38
  const layers: Layer[] = [
34
39
  // Background — gradient
@@ -45,11 +50,27 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
45
50
 
46
51
  // Add accent image layers
47
52
  if (accentImage) {
48
- layers.push(...createAccentLayers(accentImage, input.id));
53
+ layers.push(
54
+ ...createAccentLayers(
55
+ accentImage,
56
+ input.id,
57
+ themedImageTreatment(context, input.imageTreatment),
58
+ ),
59
+ );
49
60
  }
50
61
 
62
+ // Left edge of the text column — derived from the center+width pair so
63
+ // the title and items share one left axis instead of a centered title
64
+ // floating over a left-aligned column. Falls back to a safe 8% if the
65
+ // layout strings ever turn out unparseable (they're authored, but
66
+ // defensive against future edits to the accent-layout types).
67
+ const centerX = parseFloat(accentLayout.textCenterX);
68
+ const widthPct = parseFloat(accentLayout.textWidth);
69
+ const leftX =
70
+ Number.isFinite(centerX) && Number.isFinite(widthPct) ? `${centerX - widthPct / 2}%` : '8%';
71
+
51
72
  // Title if provided
52
- const startY = title ? 30 : 22;
73
+ const startY = title ? 34 : 26;
53
74
  if (title) {
54
75
  layers.push({
55
76
  type: 'text',
@@ -61,35 +82,36 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
61
82
  fontFamily: getThemeFont(context, 'title'),
62
83
  fontWeight: 'bold',
63
84
  color: theme.colors.text,
64
- textAlign: 'center',
65
- shadow: !!accentImage,
85
+ textAlign: 'left',
86
+ shadow: shouldUseShadow(context),
66
87
  },
67
88
  },
68
89
  position: {
69
- x: accentLayout.textCenterX,
70
- y: adjustY('16%', accentLayout),
71
- anchor: 'center',
90
+ x: leftX,
91
+ y: adjustY('20%', accentLayout),
92
+ width: accentLayout.textWidth,
72
93
  },
73
- animation: { type: 'fadeIn', duration: 1 },
94
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1 }),
74
95
  });
75
96
  }
76
97
 
77
- // Left edge of the text column derived from the center+width pair so
78
- // items sit flush with the column's left margin instead of being
79
- // centered. Falls back to a safe 8% if the layout strings ever turn
80
- // out unparseable (they're authored, but defensive against future
81
- // edits to the accent-layout types).
82
- const centerX = parseFloat(accentLayout.textCenterX);
83
- const widthPct = parseFloat(accentLayout.textWidth);
84
- const leftX =
85
- Number.isFinite(centerX) && Number.isFinite(widthPct) ? `${centerX - widthPct / 2}%` : '8%';
86
-
87
- // Calculate vertical spacing for items. We keep the available band
88
- // (startY endY) but use a tighter line-height (1.2) so wrapped
89
- // items don't bleed into the next entry as aggressively as the old
90
- // 1.4 value did.
91
- const endY = 80;
92
- const spacing = items.length > 1 ? (endY - startY) / (items.length - 1) : 0;
98
+ // Stack items with a fixed compact gap rather than stretching them
99
+ // across the available band. Distributing items across (startY 80%)
100
+ // left big vertical gaps for short lists and made the slide read as a
101
+ // sparse menu instead of a tight enumeration; conversely a bare
102
+ // line-height gap read as a cramped paragraph — 18px of air keeps each
103
+ // entry its own line without breaking the group.
104
+ //
105
+ // Spacing = item line-height (34px base × 1.2) + 18px gap, expressed
106
+ // as % of the 1080px design canvas (~5.4%). Wrapped items push the
107
+ // next entry down via their own line-height, so this sets the
108
+ // minimum baseline-to-baseline distance for unwrapped items.
109
+ const LIST_ITEM_BASE_PX = 34;
110
+ const LIST_ITEM_LINE_HEIGHT = 1.2;
111
+ const LIST_ITEM_GAP_PX = 18;
112
+ const DESIGN_HEIGHT_PX = 1080;
113
+ const spacing =
114
+ ((LIST_ITEM_BASE_PX * LIST_ITEM_LINE_HEIGHT + LIST_ITEM_GAP_PX) / DESIGN_HEIGHT_PX) * 100;
93
115
 
94
116
  // List items with staggered animation
95
117
  for (let i = 0; i < items.length; i++) {
@@ -107,7 +129,7 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
107
129
  color: theme.colors.text,
108
130
  textAlign: 'left',
109
131
  lineHeight: 1.2,
110
- shadow: !!accentImage,
132
+ shadow: shouldUseShadow(context),
111
133
  },
112
134
  },
113
135
  position: {
@@ -115,7 +137,11 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
115
137
  y: adjustY(`${y}%`, accentLayout),
116
138
  width: accentLayout.textWidth,
117
139
  },
118
- animation: { type: 'fadeIn', duration: 0.8, delay: 0.3 + 0.3 * i },
140
+ animation: themedEntrance(context, 'text', {
141
+ type: 'fadeIn',
142
+ duration: 0.8,
143
+ delay: 0.3 + 0.3 * i,
144
+ }),
119
145
  });
120
146
  }
121
147
 
@@ -14,8 +14,8 @@
14
14
 
15
15
  import type { Layer } from '../../schemas/Doc.js';
16
16
  import type { MapBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
17
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
18
- import { getThemeFont } from '../utils/themeUtils.js';
17
+ import { getThemeFont, shouldUseShadow, themedFontSize } from '../utils/themeUtils.js';
18
+ import { withAlpha } from '../../schemas/colorUtils.js';
19
19
  import { mapAmbientMotion } from './accentImage.js';
20
20
 
21
21
  export function mapBlock(input: MapBlockInput, context: TemplateContext): Layer[] {
@@ -32,8 +32,8 @@ export function mapBlock(input: MapBlockInput, context: TemplateContext): Layer[
32
32
  const { theme, layout } = context;
33
33
 
34
34
  // Scale font sizes for viewport
35
- const titleFontSize = scaledFontSize(64, context, true);
36
- const captionFontSize = scaledFontSize(32, context, false);
35
+ const titleFontSize = themedFontSize(64, context, true);
36
+ const captionFontSize = themedFontSize(32, context, false);
37
37
 
38
38
  // Determine animation based on ambientMotion setting
39
39
  const mapAnimation = mapAmbientMotion(ambientMotion);
@@ -56,17 +56,21 @@ export function mapBlock(input: MapBlockInput, context: TemplateContext): Layer[
56
56
  },
57
57
  ];
58
58
 
59
+ // Overlay bands are tinted from the theme background so the theme text
60
+ // color on them stays readable in light and dark themes alike; they
61
+ // fade out toward the map so the band doesn't read as a hard seam.
62
+ const bg = theme.colors.background;
63
+
59
64
  // Add title overlay if provided
60
65
  if (title) {
61
- // Gradient overlay at top for title readability
62
66
  layers.push({
63
67
  type: 'shape',
64
68
  id: 'title-overlay',
65
69
  content: {
66
70
  shape: 'rect',
67
- fill: 'rgba(0,0,0,0.6)',
71
+ fill: `linear-gradient(180deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`,
68
72
  },
69
- position: { x: 0, y: 0, width: '100%', height: '18%' },
73
+ position: { x: 0, y: 0, width: '100%', height: '20%' },
70
74
  });
71
75
 
72
76
  // Title text - positioned at top of slide
@@ -81,7 +85,7 @@ export function mapBlock(input: MapBlockInput, context: TemplateContext): Layer[
81
85
  fontWeight: 'bold',
82
86
  color: theme.colors.text,
83
87
  textAlign: 'center',
84
- shadow: true,
88
+ shadow: shouldUseShadow(context),
85
89
  },
86
90
  },
87
91
  position: {
@@ -96,15 +100,14 @@ export function mapBlock(input: MapBlockInput, context: TemplateContext): Layer[
96
100
 
97
101
  // Add caption if provided
98
102
  if (caption) {
99
- // Gradient overlay at bottom for caption readability
100
103
  layers.push({
101
104
  type: 'shape',
102
105
  id: 'caption-overlay',
103
106
  content: {
104
107
  shape: 'rect',
105
- fill: 'rgba(0,0,0,0.6)',
108
+ fill: `linear-gradient(0deg, ${withAlpha(bg, 0.85)} 0%, ${withAlpha(bg, 0.6)} 70%, ${withAlpha(bg, 0)} 100%)`,
106
109
  },
107
- position: { x: 0, y: '82%', width: '100%', height: '18%' },
110
+ position: { x: 0, y: '80%', width: '100%', height: '20%' },
108
111
  });
109
112
 
110
113
  // Caption text
@@ -118,7 +121,7 @@ export function mapBlock(input: MapBlockInput, context: TemplateContext): Layer[
118
121
  fontFamily: getThemeFont(context, 'body'),
119
122
  color: theme.colors.text,
120
123
  textAlign: 'center',
121
- shadow: true,
124
+ shadow: shouldUseShadow(context),
122
125
  },
123
126
  },
124
127
  position: {
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Canonical, framework-free UI metadata for every built-in block template.
3
+ *
4
+ * This is the single source of truth for the human-readable label and the
5
+ * one-sentence description of each template id in {@link templateRegistry}.
6
+ * The editor's `TemplatePicker` renders its gallery from this map (joining
7
+ * each id with a locally-defined SVG icon, which can't live in this
8
+ * framework-free package), so a template only has to be added here and to
9
+ * `templateRegistry` for it to surface in the editor.
10
+ *
11
+ * `templateMetadata.test.ts` asserts this map and `templateRegistry` stay
12
+ * exactly 1:1 — adding a template to one without the other fails the build.
13
+ *
14
+ * Order matters: pickers iterate this object's insertion order, so it doubles
15
+ * as the canonical gallery order.
16
+ */
17
+
18
+ export interface TemplateMetadata {
19
+ /** Human-readable label shown in pickers (e.g. "Image with Caption"). */
20
+ label: string;
21
+ /** One-sentence description of when to reach for this template. */
22
+ description: string;
23
+ }
24
+
25
+ export const TEMPLATE_METADATA: Record<string, TemplateMetadata> = {
26
+ title: {
27
+ label: 'Title',
28
+ description: 'A bold opening slide with large title text, perfect for covers and chapters.',
29
+ },
30
+ sectionHeader: {
31
+ label: 'Section Header',
32
+ description: 'A clean section break with a prominent title and optional subtitle.',
33
+ },
34
+ statHighlight: {
35
+ label: 'Stat Highlight',
36
+ description: 'Showcases a single key number or metric with supporting context.',
37
+ },
38
+ quote: {
39
+ label: 'Quote',
40
+ description: 'Displays a stylized pull quote with decorative marks and attribution.',
41
+ },
42
+ factCard: {
43
+ label: 'Fact Card',
44
+ description: 'Presents a focused fact or insight with a labeled header and body text.',
45
+ },
46
+ twoColumn: {
47
+ label: 'Two Column',
48
+ description: 'Divides the slide into two equal side-by-side content columns.',
49
+ },
50
+ dateEvent: {
51
+ label: 'Date Event',
52
+ description: 'Highlights a date-based event or milestone with a prominent date display.',
53
+ },
54
+ imageWithCaption: {
55
+ label: 'Image with Caption',
56
+ description: 'Displays a full-bleed image with an optional caption below.',
57
+ },
58
+ leftFeature: {
59
+ label: 'Left Feature',
60
+ description: 'Image on the left, title and body text stacked on the right.',
61
+ },
62
+ rightFeature: {
63
+ label: 'Right Feature',
64
+ description: 'Image on the right, title and body text stacked on the left.',
65
+ },
66
+ map: {
67
+ label: 'Map',
68
+ description: 'Embeds an interactive map centered on a geographic location.',
69
+ },
70
+ fullBleedQuote: {
71
+ label: 'Full Bleed Quote',
72
+ description: 'A full-width quote that spans the entire slide for maximum impact.',
73
+ },
74
+ list: {
75
+ label: 'List',
76
+ description: 'Renders a bulleted or numbered list in a clean, card-style layout.',
77
+ },
78
+ photoGrid: {
79
+ label: 'Photo Grid',
80
+ description: 'Arranges multiple photos in a 2×2 or 3×3 mosaic grid.',
81
+ },
82
+ definitionCard: {
83
+ label: 'Definition Card',
84
+ description: 'Shows a term and its definition in a structured, dictionary-style card.',
85
+ },
86
+ comparisonBar: {
87
+ label: 'Comparison Bar',
88
+ description: 'Visualizes two or more values side-by-side with labeled horizontal bars.',
89
+ },
90
+ pullQuote: {
91
+ label: 'Pull Quote',
92
+ description: 'A stylized pull quote with large decorative marks and centered text.',
93
+ },
94
+ videoWithCaption: {
95
+ label: 'Video with Caption',
96
+ description: 'Embeds a video player with an optional caption below.',
97
+ },
98
+ videoPullQuote: {
99
+ label: 'Video Pull Quote',
100
+ description: 'Combines a video panel with a highlighted pull quote side-by-side.',
101
+ },
102
+ dataTable: {
103
+ label: 'Data Table',
104
+ description: 'Renders tabular data in a clean, styled table with a header row.',
105
+ },
106
+ diagram: {
107
+ label: 'Diagram',
108
+ description:
109
+ 'Renders sub-headings as connected nodes — edit visually as a node-and-edge diagram.',
110
+ },
111
+ layout: {
112
+ label: 'Layout',
113
+ description:
114
+ "Free-form 2D canvas — drag layers into place. Use for one-off block layouts that don't fit a template.",
115
+ },
116
+ drawing: {
117
+ label: 'Drawing',
118
+ description: 'Free-form sketches — draw shapes, paths, and text directly on a 2D surface.',
119
+ },
120
+ };