@bendyline/squisq 1.2.2 → 1.4.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 (427) hide show
  1. package/dist/{Doc-BfBhiCNr.d.ts → Doc-BeSeH2iX.d.ts} +141 -33
  2. package/dist/ImageEditDoc-BTIvw0oq.d.ts +84 -0
  3. package/dist/__tests__/colorUtils.test.d.ts +2 -0
  4. package/dist/__tests__/colorUtils.test.d.ts.map +1 -0
  5. package/dist/__tests__/colorUtils.test.js +94 -0
  6. package/dist/__tests__/colorUtils.test.js.map +1 -0
  7. package/dist/__tests__/getLayers.test.js +31 -7
  8. package/dist/__tests__/getLayers.test.js.map +1 -1
  9. package/dist/__tests__/iconResolve.test.d.ts +2 -0
  10. package/dist/__tests__/iconResolve.test.d.ts.map +1 -0
  11. package/dist/__tests__/iconResolve.test.js +99 -0
  12. package/dist/__tests__/iconResolve.test.js.map +1 -0
  13. package/dist/__tests__/imageEditState.test.d.ts +2 -0
  14. package/dist/__tests__/imageEditState.test.d.ts.map +1 -0
  15. package/dist/__tests__/imageEditState.test.js +61 -0
  16. package/dist/__tests__/imageEditState.test.js.map +1 -0
  17. package/dist/__tests__/imageEditVersions.test.d.ts +2 -0
  18. package/dist/__tests__/imageEditVersions.test.d.ts.map +1 -0
  19. package/dist/__tests__/imageEditVersions.test.js +141 -0
  20. package/dist/__tests__/imageEditVersions.test.js.map +1 -0
  21. package/dist/__tests__/inferDocumentTitle.test.d.ts +2 -0
  22. package/dist/__tests__/inferDocumentTitle.test.d.ts.map +1 -0
  23. package/dist/__tests__/inferDocumentTitle.test.js +42 -0
  24. package/dist/__tests__/inferDocumentTitle.test.js.map +1 -0
  25. package/dist/__tests__/jsonForm.chooseControl.test.d.ts +2 -0
  26. package/dist/__tests__/jsonForm.chooseControl.test.d.ts.map +1 -0
  27. package/dist/__tests__/jsonForm.chooseControl.test.js +74 -0
  28. package/dist/__tests__/jsonForm.chooseControl.test.js.map +1 -0
  29. package/dist/__tests__/jsonForm.evaluateWhen.test.d.ts +2 -0
  30. package/dist/__tests__/jsonForm.evaluateWhen.test.d.ts.map +1 -0
  31. package/dist/__tests__/jsonForm.evaluateWhen.test.js +51 -0
  32. package/dist/__tests__/jsonForm.evaluateWhen.test.js.map +1 -0
  33. package/dist/__tests__/jsonForm.inferSchema.test.d.ts +2 -0
  34. package/dist/__tests__/jsonForm.inferSchema.test.d.ts.map +1 -0
  35. package/dist/__tests__/jsonForm.inferSchema.test.js +23 -0
  36. package/dist/__tests__/jsonForm.inferSchema.test.js.map +1 -0
  37. package/dist/__tests__/jsonForm.pointer.test.d.ts +2 -0
  38. package/dist/__tests__/jsonForm.pointer.test.d.ts.map +1 -0
  39. package/dist/__tests__/jsonForm.pointer.test.js +94 -0
  40. package/dist/__tests__/jsonForm.pointer.test.js.map +1 -0
  41. package/dist/__tests__/markdown.test.js +40 -0
  42. package/dist/__tests__/markdown.test.js.map +1 -1
  43. package/dist/__tests__/markdownIcons.test.d.ts +2 -0
  44. package/dist/__tests__/markdownIcons.test.d.ts.map +1 -0
  45. package/dist/__tests__/markdownIcons.test.js +93 -0
  46. package/dist/__tests__/markdownIcons.test.js.map +1 -0
  47. package/dist/__tests__/markdownToDoc.test.js +15 -4
  48. package/dist/__tests__/markdownToDoc.test.js.map +1 -1
  49. package/dist/__tests__/recommendTemplates.test.d.ts +2 -0
  50. package/dist/__tests__/recommendTemplates.test.d.ts.map +1 -0
  51. package/dist/__tests__/recommendTemplates.test.js +158 -0
  52. package/dist/__tests__/recommendTemplates.test.js.map +1 -0
  53. package/dist/__tests__/scopedContentContainer.test.d.ts +2 -0
  54. package/dist/__tests__/scopedContentContainer.test.d.ts.map +1 -0
  55. package/dist/__tests__/scopedContentContainer.test.js +74 -0
  56. package/dist/__tests__/scopedContentContainer.test.js.map +1 -0
  57. package/dist/__tests__/slideshowGenerator.test.js +1 -1
  58. package/dist/__tests__/slideshowGenerator.test.js.map +1 -1
  59. package/dist/__tests__/surface.test.d.ts +2 -0
  60. package/dist/__tests__/surface.test.d.ts.map +1 -0
  61. package/dist/__tests__/surface.test.js +57 -0
  62. package/dist/__tests__/surface.test.js.map +1 -0
  63. package/dist/__tests__/templateAnnotationParse.test.d.ts +2 -0
  64. package/dist/__tests__/templateAnnotationParse.test.d.ts.map +1 -0
  65. package/dist/__tests__/templateAnnotationParse.test.js +42 -0
  66. package/dist/__tests__/templateAnnotationParse.test.js.map +1 -0
  67. package/dist/__tests__/templates.test.js +90 -9
  68. package/dist/__tests__/templates.test.js.map +1 -1
  69. package/dist/__tests__/themeCompile.test.d.ts +2 -0
  70. package/dist/__tests__/themeCompile.test.d.ts.map +1 -0
  71. package/dist/__tests__/themeCompile.test.js +134 -0
  72. package/dist/__tests__/themeCompile.test.js.map +1 -0
  73. package/dist/__tests__/themeValidator.test.d.ts +2 -0
  74. package/dist/__tests__/themeValidator.test.d.ts.map +1 -0
  75. package/dist/__tests__/themeValidator.test.js +107 -0
  76. package/dist/__tests__/themeValidator.test.js.map +1 -0
  77. package/dist/__tests__/versions.test.d.ts +2 -0
  78. package/dist/__tests__/versions.test.d.ts.map +1 -0
  79. package/dist/__tests__/versions.test.js +339 -0
  80. package/dist/__tests__/versions.test.js.map +1 -0
  81. package/dist/{chunk-CYQHG27J.js → chunk-2TMAWB4Y.js} +74 -1
  82. package/dist/chunk-2TMAWB4Y.js.map +1 -0
  83. package/dist/{chunk-LAZELBON.js → chunk-3K5OG6KI.js} +165 -17
  84. package/dist/chunk-3K5OG6KI.js.map +1 -0
  85. package/dist/chunk-57CVQCFX.js +193 -0
  86. package/dist/chunk-57CVQCFX.js.map +1 -0
  87. package/dist/{chunk-QM5PKNPW.js → chunk-5WFRKURX.js} +1 -1
  88. package/dist/chunk-5WFRKURX.js.map +1 -0
  89. package/dist/chunk-B4EEON3N.js +1317 -0
  90. package/dist/chunk-B4EEON3N.js.map +1 -0
  91. package/dist/chunk-DBS6RIFL.js +81 -0
  92. package/dist/chunk-DBS6RIFL.js.map +1 -0
  93. package/dist/{chunk-MIBRIRGJ.js → chunk-FR2RBTKO.js} +3 -3
  94. package/dist/{chunk-MIBRIRGJ.js.map → chunk-FR2RBTKO.js.map} +1 -1
  95. package/dist/chunk-H3AXU4MJ.js +179 -0
  96. package/dist/chunk-H3AXU4MJ.js.map +1 -0
  97. package/dist/chunk-JELIRVDP.js +24 -0
  98. package/dist/chunk-JELIRVDP.js.map +1 -0
  99. package/dist/chunk-K32VJONL.js +193 -0
  100. package/dist/chunk-K32VJONL.js.map +1 -0
  101. package/dist/{chunk-GAQLMORN.js → chunk-KSWJR66U.js} +248 -44
  102. package/dist/chunk-KSWJR66U.js.map +1 -0
  103. package/dist/{chunk-TSG2OSE6.js → chunk-MYIH7FWD.js} +3 -3
  104. package/dist/chunk-MYIH7FWD.js.map +1 -0
  105. package/dist/chunk-VLJJHDAC.js +218 -0
  106. package/dist/chunk-VLJJHDAC.js.map +1 -0
  107. package/dist/chunk-WMOI7FN3.js +182 -0
  108. package/dist/chunk-WMOI7FN3.js.map +1 -0
  109. package/dist/chunk-ZTX4PFFT.js +520 -0
  110. package/dist/chunk-ZTX4PFFT.js.map +1 -0
  111. package/dist/doc/audioMapping.d.ts.map +1 -1
  112. package/dist/doc/audioMapping.js +80 -9
  113. package/dist/doc/audioMapping.js.map +1 -1
  114. package/dist/doc/getLayers.d.ts.map +1 -1
  115. package/dist/doc/getLayers.js +29 -18
  116. package/dist/doc/getLayers.js.map +1 -1
  117. package/dist/doc/markdownToDoc.d.ts.map +1 -1
  118. package/dist/doc/markdownToDoc.js +40 -6
  119. package/dist/doc/markdownToDoc.js.map +1 -1
  120. package/dist/doc/templates/featureBlock.d.ts +18 -0
  121. package/dist/doc/templates/featureBlock.d.ts.map +1 -0
  122. package/dist/doc/templates/featureBlock.js +180 -0
  123. package/dist/doc/templates/featureBlock.js.map +1 -0
  124. package/dist/doc/templates/fullBleedQuote.d.ts +12 -0
  125. package/dist/doc/templates/fullBleedQuote.d.ts.map +1 -1
  126. package/dist/doc/templates/fullBleedQuote.js +12 -0
  127. package/dist/doc/templates/fullBleedQuote.js.map +1 -1
  128. package/dist/doc/templates/index.d.ts +28 -4
  129. package/dist/doc/templates/index.d.ts.map +1 -1
  130. package/dist/doc/templates/index.js +45 -12
  131. package/dist/doc/templates/index.js.map +1 -1
  132. package/dist/doc/templates/listBlock.d.ts.map +1 -1
  133. package/dist/doc/templates/listBlock.js +22 -5
  134. package/dist/doc/templates/listBlock.js.map +1 -1
  135. package/dist/doc/templates/statHighlight.d.ts +12 -0
  136. package/dist/doc/templates/statHighlight.d.ts.map +1 -1
  137. package/dist/doc/templates/statHighlight.js +12 -0
  138. package/dist/doc/templates/statHighlight.js.map +1 -1
  139. package/dist/doc/templates/titleBlock.d.ts +11 -0
  140. package/dist/doc/templates/titleBlock.d.ts.map +1 -1
  141. package/dist/doc/templates/titleBlock.js +8 -0
  142. package/dist/doc/templates/titleBlock.js.map +1 -1
  143. package/dist/doc/utils/themeUtils.d.ts +4 -4
  144. package/dist/doc/utils/themeUtils.d.ts.map +1 -1
  145. package/dist/doc/utils/themeUtils.js +8 -7
  146. package/dist/doc/utils/themeUtils.js.map +1 -1
  147. package/dist/generate/slideshowGenerator.js +1 -1
  148. package/dist/generate/slideshowGenerator.js.map +1 -1
  149. package/dist/generate/templateMapper.js +2 -2
  150. package/dist/generate/templateMapper.js.map +1 -1
  151. package/dist/icons/iconData.d.ts +26 -0
  152. package/dist/icons/iconData.d.ts.map +1 -0
  153. package/dist/icons/iconData.js +12 -0
  154. package/dist/icons/iconData.js.map +1 -0
  155. package/dist/icons/index.d.ts +12 -0
  156. package/dist/icons/index.d.ts.map +1 -0
  157. package/dist/icons/index.js +10 -0
  158. package/dist/icons/index.js.map +1 -0
  159. package/dist/icons/resolve.d.ts +61 -0
  160. package/dist/icons/resolve.d.ts.map +1 -0
  161. package/dist/icons/resolve.js +130 -0
  162. package/dist/icons/resolve.js.map +1 -0
  163. package/dist/imageEdit/ImageEditVersionManager.d.ts +31 -0
  164. package/dist/imageEdit/ImageEditVersionManager.d.ts.map +1 -0
  165. package/dist/imageEdit/ImageEditVersionManager.js +39 -0
  166. package/dist/imageEdit/ImageEditVersionManager.js.map +1 -0
  167. package/dist/imageEdit/export.d.ts +33 -0
  168. package/dist/imageEdit/export.d.ts.map +1 -0
  169. package/dist/imageEdit/export.js +253 -0
  170. package/dist/imageEdit/export.js.map +1 -0
  171. package/dist/imageEdit/index.d.ts +14 -0
  172. package/dist/imageEdit/index.d.ts.map +1 -0
  173. package/dist/imageEdit/index.js +22 -0
  174. package/dist/imageEdit/index.js.map +1 -0
  175. package/dist/imageEdit/persistence.d.ts +15 -0
  176. package/dist/imageEdit/persistence.d.ts.map +1 -0
  177. package/dist/imageEdit/persistence.js +49 -0
  178. package/dist/imageEdit/persistence.js.map +1 -0
  179. package/dist/imageEdit/state.d.ts +42 -0
  180. package/dist/imageEdit/state.d.ts.map +1 -0
  181. package/dist/imageEdit/state.js +98 -0
  182. package/dist/imageEdit/state.js.map +1 -0
  183. package/dist/imageEdit/versionPaths.d.ts +27 -0
  184. package/dist/imageEdit/versionPaths.d.ts.map +1 -0
  185. package/dist/imageEdit/versionPaths.js +57 -0
  186. package/dist/imageEdit/versionPaths.js.map +1 -0
  187. package/dist/imageEdit/versions.d.ts +67 -0
  188. package/dist/imageEdit/versions.d.ts.map +1 -0
  189. package/dist/imageEdit/versions.js +161 -0
  190. package/dist/imageEdit/versions.js.map +1 -0
  191. package/dist/index.d.ts +4 -0
  192. package/dist/index.d.ts.map +1 -1
  193. package/dist/index.js +4 -0
  194. package/dist/index.js.map +1 -1
  195. package/dist/jsonForm/chooseControl.d.ts +20 -0
  196. package/dist/jsonForm/chooseControl.d.ts.map +1 -0
  197. package/dist/jsonForm/chooseControl.js +82 -0
  198. package/dist/jsonForm/chooseControl.js.map +1 -0
  199. package/dist/jsonForm/evaluateWhen.d.ts +12 -0
  200. package/dist/jsonForm/evaluateWhen.d.ts.map +1 -0
  201. package/dist/jsonForm/evaluateWhen.js +65 -0
  202. package/dist/jsonForm/evaluateWhen.js.map +1 -0
  203. package/dist/jsonForm/index.d.ts +14 -0
  204. package/dist/jsonForm/index.d.ts.map +1 -0
  205. package/dist/jsonForm/index.js +12 -0
  206. package/dist/jsonForm/index.js.map +1 -0
  207. package/dist/jsonForm/inferSchema.d.ts +12 -0
  208. package/dist/jsonForm/inferSchema.d.ts.map +1 -0
  209. package/dist/jsonForm/inferSchema.js +15 -0
  210. package/dist/jsonForm/inferSchema.js.map +1 -0
  211. package/dist/jsonForm/pointer.d.ts +26 -0
  212. package/dist/jsonForm/pointer.d.ts.map +1 -0
  213. package/dist/jsonForm/pointer.js +102 -0
  214. package/dist/jsonForm/pointer.js.map +1 -0
  215. package/dist/jsonForm/types.d.ts +120 -0
  216. package/dist/jsonForm/types.d.ts.map +1 -0
  217. package/dist/jsonForm/types.js +10 -0
  218. package/dist/jsonForm/types.js.map +1 -0
  219. package/dist/markdown/convert.d.ts +11 -1
  220. package/dist/markdown/convert.d.ts.map +1 -1
  221. package/dist/markdown/convert.js +235 -15
  222. package/dist/markdown/convert.js.map +1 -1
  223. package/dist/markdown/index.d.ts +2 -2
  224. package/dist/markdown/index.d.ts.map +1 -1
  225. package/dist/markdown/index.js +1 -1
  226. package/dist/markdown/index.js.map +1 -1
  227. package/dist/markdown/types.d.ts +34 -1
  228. package/dist/markdown/types.d.ts.map +1 -1
  229. package/dist/markdown/utils.d.ts +40 -0
  230. package/dist/markdown/utils.d.ts.map +1 -1
  231. package/dist/markdown/utils.js +155 -0
  232. package/dist/markdown/utils.js.map +1 -1
  233. package/dist/recommend/index.d.ts +16 -0
  234. package/dist/recommend/index.d.ts.map +1 -0
  235. package/dist/recommend/index.js +15 -0
  236. package/dist/recommend/index.js.map +1 -0
  237. package/dist/recommend/templates.d.ts +39 -0
  238. package/dist/recommend/templates.d.ts.map +1 -0
  239. package/dist/recommend/templates.js +235 -0
  240. package/dist/recommend/templates.js.map +1 -0
  241. package/dist/schemas/BlockTemplates.d.ts +46 -8
  242. package/dist/schemas/BlockTemplates.d.ts.map +1 -1
  243. package/dist/schemas/BlockTemplates.js +3 -3
  244. package/dist/schemas/BlockTemplates.js.map +1 -1
  245. package/dist/schemas/ImageEditDoc.d.ts +81 -0
  246. package/dist/schemas/ImageEditDoc.d.ts.map +1 -0
  247. package/dist/schemas/ImageEditDoc.js +20 -0
  248. package/dist/schemas/ImageEditDoc.js.map +1 -0
  249. package/dist/schemas/Theme.d.ts +93 -23
  250. package/dist/schemas/Theme.d.ts.map +1 -1
  251. package/dist/schemas/Theme.js +80 -13
  252. package/dist/schemas/Theme.js.map +1 -1
  253. package/dist/schemas/colorUtils.d.ts +51 -0
  254. package/dist/schemas/colorUtils.d.ts.map +1 -0
  255. package/dist/schemas/colorUtils.js +154 -0
  256. package/dist/schemas/colorUtils.js.map +1 -0
  257. package/dist/schemas/fontStacks.d.ts +57 -0
  258. package/dist/schemas/fontStacks.d.ts.map +1 -0
  259. package/dist/schemas/fontStacks.js +227 -0
  260. package/dist/schemas/fontStacks.js.map +1 -0
  261. package/dist/schemas/index.d.ts +5 -0
  262. package/dist/schemas/index.d.ts.map +1 -1
  263. package/dist/schemas/index.js +4 -0
  264. package/dist/schemas/index.js.map +1 -1
  265. package/dist/schemas/themeCompile.d.ts +45 -0
  266. package/dist/schemas/themeCompile.d.ts.map +1 -0
  267. package/dist/schemas/themeCompile.js +160 -0
  268. package/dist/schemas/themeCompile.js.map +1 -0
  269. package/dist/schemas/themeLibrary.d.ts +11 -15
  270. package/dist/schemas/themeLibrary.d.ts.map +1 -1
  271. package/dist/schemas/themeLibrary.js +18 -499
  272. package/dist/schemas/themeLibrary.js.map +1 -1
  273. package/dist/schemas/themeValidator.d.ts +26 -0
  274. package/dist/schemas/themeValidator.d.ts.map +1 -0
  275. package/dist/schemas/themeValidator.js +352 -0
  276. package/dist/schemas/themeValidator.js.map +1 -0
  277. package/dist/schemas/themes/bold.json +48 -0
  278. package/dist/schemas/themes/cinematic.json +54 -0
  279. package/dist/schemas/themes/documentary.json +49 -0
  280. package/dist/schemas/themes/gezellig.json +44 -0
  281. package/dist/schemas/themes/index.d.ts +26 -0
  282. package/dist/schemas/themes/index.d.ts.map +1 -0
  283. package/dist/schemas/themes/index.js +49 -0
  284. package/dist/schemas/themes/index.js.map +1 -0
  285. package/dist/schemas/themes/magazine.json +44 -0
  286. package/dist/schemas/themes/minimalist.json +45 -0
  287. package/dist/schemas/themes/morning-light.json +42 -0
  288. package/dist/schemas/themes/standard-dark.json +50 -0
  289. package/dist/schemas/themes/standard.json +42 -0
  290. package/dist/schemas/themes/tech-dark.json +48 -0
  291. package/dist/schemas/themes/warm-earth.json +50 -0
  292. package/dist/storage/ScopedContentContainer.d.ts +44 -0
  293. package/dist/storage/ScopedContentContainer.d.ts.map +1 -0
  294. package/dist/storage/ScopedContentContainer.js +94 -0
  295. package/dist/storage/ScopedContentContainer.js.map +1 -0
  296. package/dist/storage/index.d.ts +1 -0
  297. package/dist/storage/index.d.ts.map +1 -1
  298. package/dist/storage/index.js +1 -0
  299. package/dist/storage/index.js.map +1 -1
  300. package/dist/story/index.d.ts +53 -13
  301. package/dist/story/index.js +12 -4
  302. package/dist/themeLibrary-BHjJ0Qx6.d.ts +40 -0
  303. package/dist/{types-DYAzisMG.d.ts → types-BdZkdJ3z.d.ts} +35 -2
  304. package/dist/types-D98li1gV.d.ts +93 -0
  305. package/dist/versions/DocumentVersionManager.d.ts +31 -0
  306. package/dist/versions/DocumentVersionManager.d.ts.map +1 -0
  307. package/dist/versions/DocumentVersionManager.js +33 -0
  308. package/dist/versions/DocumentVersionManager.js.map +1 -0
  309. package/dist/versions/index.d.ts +29 -0
  310. package/dist/versions/index.d.ts.map +1 -0
  311. package/dist/versions/index.js +28 -0
  312. package/dist/versions/index.js.map +1 -0
  313. package/dist/versions/operations.d.ts +44 -0
  314. package/dist/versions/operations.d.ts.map +1 -0
  315. package/dist/versions/operations.js +190 -0
  316. package/dist/versions/operations.js.map +1 -0
  317. package/dist/versions/paths.d.ts +29 -0
  318. package/dist/versions/paths.d.ts.map +1 -0
  319. package/dist/versions/paths.js +66 -0
  320. package/dist/versions/paths.js.map +1 -0
  321. package/dist/versions/timestamp.d.ts +22 -0
  322. package/dist/versions/timestamp.d.ts.map +1 -0
  323. package/dist/versions/timestamp.js +44 -0
  324. package/dist/versions/timestamp.js.map +1 -0
  325. package/dist/versions/types.d.ts +92 -0
  326. package/dist/versions/types.d.ts.map +1 -0
  327. package/dist/versions/types.js +11 -0
  328. package/dist/versions/types.js.map +1 -0
  329. package/package.json +28 -1
  330. package/src/__tests__/colorUtils.test.ts +112 -0
  331. package/src/__tests__/getLayers.test.ts +32 -7
  332. package/src/__tests__/iconResolve.test.ts +122 -0
  333. package/src/__tests__/imageEditState.test.ts +74 -0
  334. package/src/__tests__/imageEditVersions.test.ts +169 -0
  335. package/src/__tests__/inferDocumentTitle.test.ts +50 -0
  336. package/src/__tests__/jsonForm.chooseControl.test.ts +86 -0
  337. package/src/__tests__/jsonForm.evaluateWhen.test.ts +59 -0
  338. package/src/__tests__/jsonForm.inferSchema.test.ts +25 -0
  339. package/src/__tests__/jsonForm.pointer.test.ts +103 -0
  340. package/src/__tests__/markdown.test.ts +44 -0
  341. package/src/__tests__/markdownIcons.test.ts +104 -0
  342. package/src/__tests__/markdownToDoc.test.ts +19 -4
  343. package/src/__tests__/recommendTemplates.test.ts +183 -0
  344. package/src/__tests__/scopedContentContainer.test.ts +89 -0
  345. package/src/__tests__/slideshowGenerator.test.ts +1 -1
  346. package/src/__tests__/surface.test.ts +68 -0
  347. package/src/__tests__/templateAnnotationParse.test.ts +42 -0
  348. package/src/__tests__/templates.test.ts +93 -9
  349. package/src/__tests__/themeCompile.test.ts +156 -0
  350. package/src/__tests__/themeValidator.test.ts +125 -0
  351. package/src/__tests__/versions.test.ts +415 -0
  352. package/src/doc/audioMapping.ts +95 -13
  353. package/src/doc/getLayers.ts +32 -21
  354. package/src/doc/markdownToDoc.ts +45 -7
  355. package/src/doc/templates/featureBlock.ts +202 -0
  356. package/src/doc/templates/fullBleedQuote.ts +13 -0
  357. package/src/doc/templates/index.ts +50 -16
  358. package/src/doc/templates/listBlock.ts +24 -5
  359. package/src/doc/templates/statHighlight.ts +13 -0
  360. package/src/doc/templates/titleBlock.ts +9 -0
  361. package/src/doc/utils/themeUtils.ts +8 -7
  362. package/src/generate/slideshowGenerator.ts +1 -1
  363. package/src/generate/templateMapper.ts +2 -2
  364. package/src/icons/iconData.ts +31 -0
  365. package/src/icons/index.ts +18 -0
  366. package/src/icons/resolve.ts +139 -0
  367. package/src/imageEdit/ImageEditVersionManager.ts +73 -0
  368. package/src/imageEdit/export.ts +326 -0
  369. package/src/imageEdit/index.ts +71 -0
  370. package/src/imageEdit/persistence.ts +62 -0
  371. package/src/imageEdit/state.ts +112 -0
  372. package/src/imageEdit/versionPaths.ts +57 -0
  373. package/src/imageEdit/versions.ts +231 -0
  374. package/src/index.ts +4 -0
  375. package/src/jsonForm/chooseControl.ts +88 -0
  376. package/src/jsonForm/evaluateWhen.ts +59 -0
  377. package/src/jsonForm/index.ts +22 -0
  378. package/src/jsonForm/inferSchema.ts +25 -0
  379. package/src/jsonForm/pointer.ts +109 -0
  380. package/src/jsonForm/types.ts +167 -0
  381. package/src/markdown/convert.ts +248 -15
  382. package/src/markdown/index.ts +4 -0
  383. package/src/markdown/types.ts +38 -1
  384. package/src/markdown/utils.ts +159 -0
  385. package/src/recommend/index.ts +16 -0
  386. package/src/recommend/templates.ts +273 -0
  387. package/src/schemas/BlockTemplates.ts +50 -7
  388. package/src/schemas/ImageEditDoc.ts +95 -0
  389. package/src/schemas/Theme.ts +152 -23
  390. package/src/schemas/colorUtils.ts +186 -0
  391. package/src/schemas/fontStacks.ts +257 -0
  392. package/src/schemas/index.ts +12 -0
  393. package/src/schemas/themeCompile.ts +203 -0
  394. package/src/schemas/themeLibrary.ts +18 -524
  395. package/src/schemas/themeValidator.ts +388 -0
  396. package/src/schemas/themes/bold.json +48 -0
  397. package/src/schemas/themes/cinematic.json +54 -0
  398. package/src/schemas/themes/documentary.json +49 -0
  399. package/src/schemas/themes/gezellig.json +44 -0
  400. package/src/schemas/themes/index.ts +53 -0
  401. package/src/schemas/themes/magazine.json +44 -0
  402. package/src/schemas/themes/minimalist.json +45 -0
  403. package/src/schemas/themes/morning-light.json +42 -0
  404. package/src/schemas/themes/standard-dark.json +50 -0
  405. package/src/schemas/themes/standard.json +42 -0
  406. package/src/schemas/themes/tech-dark.json +48 -0
  407. package/src/schemas/themes/warm-earth.json +50 -0
  408. package/src/storage/ScopedContentContainer.ts +113 -0
  409. package/src/storage/index.ts +1 -0
  410. package/src/versions/DocumentVersionManager.ts +70 -0
  411. package/src/versions/index.ts +54 -0
  412. package/src/versions/operations.ts +230 -0
  413. package/src/versions/paths.ts +66 -0
  414. package/src/versions/timestamp.ts +48 -0
  415. package/src/versions/types.ts +91 -0
  416. package/dist/chunk-53TZ2UTL.js +0 -470
  417. package/dist/chunk-53TZ2UTL.js.map +0 -1
  418. package/dist/chunk-7GF6RDED.js +0 -82
  419. package/dist/chunk-7GF6RDED.js.map +0 -1
  420. package/dist/chunk-CYQHG27J.js.map +0 -1
  421. package/dist/chunk-GAQLMORN.js.map +0 -1
  422. package/dist/chunk-LAZELBON.js.map +0 -1
  423. package/dist/chunk-QM5PKNPW.js.map +0 -1
  424. package/dist/chunk-TSG2OSE6.js.map +0 -1
  425. package/dist/chunk-XM4WSOHB.js +0 -92
  426. package/dist/chunk-XM4WSOHB.js.map +0 -1
  427. package/dist/themeLibrary-IP7EdyRq.d.ts +0 -44
@@ -453,6 +453,22 @@ export interface MarkdownInlineMath extends MarkdownNodeBase {
453
453
  value: string;
454
454
  }
455
455
 
456
+ /**
457
+ * Inline FontAwesome icon. Authored as `{[name]}` (bare) or
458
+ * `{[fa-<family>:name]}` (qualified) in markdown source. The parser
459
+ * resolves the token against the FA Free catalog and emits this node;
460
+ * unknown / ambiguous tokens stay as literal text.
461
+ */
462
+ export interface MarkdownInlineIcon extends MarkdownNodeBase {
463
+ type: 'inlineIcon';
464
+ /** Token exactly as authored — e.g. `github` or `fa-solid:user`. */
465
+ token: string;
466
+ /** Resolved icon family. */
467
+ family: 'brands' | 'solid' | 'regular';
468
+ /** Resolved icon name (without the `fa-` prefix). */
469
+ name: string;
470
+ }
471
+
456
472
  /**
457
473
  * A footnote reference [^id]. GFM extension.
458
474
  */
@@ -507,6 +523,25 @@ export interface MarkdownTextDirective extends MarkdownNodeBase {
507
523
  children: MarkdownInlineNode[];
508
524
  }
509
525
 
526
+ /**
527
+ * An @-mention of a named entity such as a user or agent.
528
+ *
529
+ * Wire format: `@[Display Name](scheme:id)` — a regular Markdown link
530
+ * preceded by `@`, with a namespaced URL scheme that identifies what
531
+ * kind of entity is being mentioned. Parsers that don't know about
532
+ * mentions still render it as a link with text "Display Name" pointing
533
+ * at "scheme:id" — graceful fallback.
534
+ */
535
+ export interface MarkdownMention extends MarkdownNodeBase {
536
+ type: 'mention';
537
+ /** Namespace for the mentioned entity (e.g. `"user"`). */
538
+ targetKind: string;
539
+ /** Stable identifier within the namespace. */
540
+ targetId: string;
541
+ /** Text shown to the reader. */
542
+ displayName: string;
543
+ }
544
+
510
545
  // ============================================
511
546
  // Union Types
512
547
  // ============================================
@@ -543,7 +578,9 @@ export type MarkdownInlineNode =
543
578
  | MarkdownFootnoteReference
544
579
  | MarkdownLinkReference
545
580
  | MarkdownImageReference
546
- | MarkdownTextDirective;
581
+ | MarkdownTextDirective
582
+ | MarkdownMention
583
+ | MarkdownInlineIcon;
547
584
 
548
585
  /**
549
586
  * Any node in the markdown tree. Includes structural nodes (listItem,
@@ -157,6 +157,109 @@ export function parseFrontmatter(yaml: string): Record<string, unknown> | null {
157
157
  return Object.keys(result).length > 0 ? result : null;
158
158
  }
159
159
 
160
+ const FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n)?/;
161
+
162
+ /** Quote a frontmatter scalar so it round-trips cleanly through `parseFrontmatter`. */
163
+ function formatFrontmatterValue(value: string | number | boolean): string {
164
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value);
165
+ // Quote when needed: leading/trailing whitespace, leading punctuation that
166
+ // could trigger YAML modes, or values that look like reserved literals.
167
+ const needsQuote =
168
+ /^\s|\s$/.test(value) ||
169
+ /^[!&*?|>%@`]/.test(value) ||
170
+ /[:#]/.test(value) ||
171
+ value === '' ||
172
+ value === 'true' ||
173
+ value === 'false' ||
174
+ /^-?\d+(\.\d+)?$/.test(value);
175
+ if (!needsQuote) return value;
176
+ // Prefer double quotes; escape any embedded double-quote / backslash.
177
+ const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
178
+ return `"${escaped}"`;
179
+ }
180
+
181
+ /**
182
+ * Update a markdown source string's YAML frontmatter block, applying the
183
+ * given key/value updates. A `null` or `undefined` value removes the key.
184
+ * If no frontmatter block exists and any non-null update is supplied, a
185
+ * new block is prepended; if every update is a removal and no block
186
+ * exists, the source is returned unchanged.
187
+ *
188
+ * Existing key order is preserved; new keys are appended in the order
189
+ * they appear in `updates`. The simple parser used here handles plain
190
+ * `key: value` pairs only — comments, multi-line scalars, and nested
191
+ * structures are left untouched (we only rewrite lines whose key matches
192
+ * one in `updates`).
193
+ *
194
+ * @param source - The markdown source string.
195
+ * @param updates - Map of key → new value (or null/undefined to remove).
196
+ * @returns The updated markdown source string.
197
+ */
198
+ export function setFrontmatterValues(
199
+ source: string,
200
+ updates: Record<string, string | number | boolean | null | undefined>,
201
+ ): string {
202
+ const updateKeys = Object.keys(updates);
203
+ if (updateKeys.length === 0) return source;
204
+
205
+ const match = source.match(FRONTMATTER_BLOCK_RE);
206
+ const handled = new Set<string>();
207
+
208
+ // Existing frontmatter block — rewrite line-by-line, preserving order.
209
+ if (match) {
210
+ const inner = match[1];
211
+ const lines = inner.split(/\r?\n/);
212
+ const newLines: string[] = [];
213
+
214
+ for (const line of lines) {
215
+ const trimmed = line.trim();
216
+ if (!trimmed || trimmed.startsWith('#')) {
217
+ newLines.push(line);
218
+ continue;
219
+ }
220
+ const colonIdx = trimmed.indexOf(':');
221
+ if (colonIdx < 1) {
222
+ newLines.push(line);
223
+ continue;
224
+ }
225
+ const key = trimmed.slice(0, colonIdx).trim();
226
+ if (!Object.prototype.hasOwnProperty.call(updates, key)) {
227
+ newLines.push(line);
228
+ continue;
229
+ }
230
+ handled.add(key);
231
+ const next = updates[key];
232
+ if (next === null || next === undefined) continue; // remove
233
+ newLines.push(`${key}: ${formatFrontmatterValue(next)}`);
234
+ }
235
+
236
+ // Append any keys that weren't already present.
237
+ for (const key of updateKeys) {
238
+ if (handled.has(key)) continue;
239
+ const next = updates[key];
240
+ if (next === null || next === undefined) continue;
241
+ newLines.push(`${key}: ${formatFrontmatterValue(next)}`);
242
+ }
243
+
244
+ // If every line got removed, drop the block entirely.
245
+ const nonEmpty = newLines.filter((l) => l.trim().length > 0);
246
+ const rest = source.slice(match[0].length);
247
+ if (nonEmpty.length === 0) return rest;
248
+ return `---\n${newLines.join('\n')}\n---\n${rest}`;
249
+ }
250
+
251
+ // No existing block — only create one if there's at least one non-null
252
+ // value to write.
253
+ const fresh: string[] = [];
254
+ for (const key of updateKeys) {
255
+ const next = updates[key];
256
+ if (next === null || next === undefined) continue;
257
+ fresh.push(`${key}: ${formatFrontmatterValue(next)}`);
258
+ }
259
+ if (fresh.length === 0) return source;
260
+ return `---\n${fresh.join('\n')}\n---\n${source}`;
261
+ }
262
+
160
263
  /**
161
264
  * Create a minimal MarkdownDocument from a list of block nodes.
162
265
  * Convenience function for programmatic document construction.
@@ -167,3 +270,59 @@ export function createDocument(...children: MarkdownDocument['children']): Markd
167
270
  children,
168
271
  };
169
272
  }
273
+
274
+ /**
275
+ * Infer a human-readable title for a markdown document.
276
+ *
277
+ * Resolution order:
278
+ * 1. `title:` value in YAML frontmatter (when present and non-empty)
279
+ * 2. Text of the first heading found, walking top-level depth (H1, then
280
+ * H2, etc.) — the shallowest heading wins regardless of source order
281
+ *
282
+ * Returns `undefined` when no usable title is found.
283
+ */
284
+ export function inferDocumentTitle(doc: MarkdownDocument): string | undefined {
285
+ const fmTitle = doc.frontmatter?.title;
286
+ if (typeof fmTitle === 'string') {
287
+ const trimmed = fmTitle.trim();
288
+ if (trimmed) return trimmed;
289
+ }
290
+
291
+ let best: { depth: number; text: string } | null = null;
292
+ for (const node of doc.children) {
293
+ if (node.type !== 'heading') continue;
294
+ const text = extractPlainText(node).trim();
295
+ if (!text) continue;
296
+ if (best === null || node.depth < best.depth) {
297
+ best = { depth: node.depth, text };
298
+ if (best.depth === 1) break;
299
+ }
300
+ }
301
+
302
+ return best?.text;
303
+ }
304
+
305
+ /**
306
+ * Read a theme id from a markdown document's frontmatter.
307
+ *
308
+ * Checks the editor's canonical `squisq-theme` key first, then the
309
+ * shorter legacy aliases (`themeId`, `theme`). Returns `undefined`
310
+ * when none of them carry a non-empty string. Centralizing the lookup
311
+ * keeps the export pipelines (DOCX, PPTX, HTML, plain-HTML bundle) in
312
+ * sync with whatever the editor writes — adding a new frontmatter
313
+ * spelling later is a one-file change.
314
+ */
315
+ export function readFrontmatterThemeId(
316
+ frontmatter: Record<string, unknown> | undefined,
317
+ ): string | undefined {
318
+ if (!frontmatter) return undefined;
319
+ const keys = ['squisq-theme', 'themeId', 'theme'] as const;
320
+ for (const key of keys) {
321
+ const raw = frontmatter[key];
322
+ if (typeof raw === 'string') {
323
+ const trimmed = raw.trim();
324
+ if (trimmed) return trimmed;
325
+ }
326
+ }
327
+ return undefined;
328
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Template recommendations for the editor's block template picker.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { parseMarkdown } from '@bendyline/squisq/markdown';
7
+ * import { profileBlockContents, recommendTemplatesForBlock } from '@bendyline/squisq/recommend';
8
+ *
9
+ * const doc = parseMarkdown(blockBodySource);
10
+ * const profile = profileBlockContents(doc.children);
11
+ * const { recommended, rest } = recommendTemplatesForBlock(profile, ALL_TEMPLATE_NAMES);
12
+ * ```
13
+ */
14
+
15
+ export type { BlockContentProfile, RecommendationResult } from './templates.js';
16
+ export { profileBlockContents, recommendTemplatesForBlock } from './templates.js';
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Block template recommendations.
3
+ *
4
+ * Given a slice of markdown block nodes (the body underneath a heading),
5
+ * profile the content for image / video / quote / list / table / date /
6
+ * stat signals, then map that profile to the set of templates that make
7
+ * sense for the block. Used by the editor's template picker to surface a
8
+ * "Recommended for this block" section above the full template list.
9
+ */
10
+
11
+ import { extractPlainText, findNodesByType, walkMarkdownTree } from '../markdown/utils.js';
12
+ import type {
13
+ HtmlElement,
14
+ HtmlNode,
15
+ MarkdownBlockNode,
16
+ MarkdownHtmlBlock,
17
+ MarkdownNode,
18
+ } from '../markdown/types.js';
19
+
20
+ export interface BlockContentProfile {
21
+ hasImage: boolean;
22
+ imageCount: number;
23
+ hasVideo: boolean;
24
+ hasBlockquote: boolean;
25
+ hasList: boolean;
26
+ hasTable: boolean;
27
+ hasDate: boolean;
28
+ hasNumberHighlight: boolean;
29
+ wordCount: number;
30
+ }
31
+
32
+ export interface RecommendationResult {
33
+ recommended: string[];
34
+ rest: string[];
35
+ }
36
+
37
+ const EMPTY_PROFILE: BlockContentProfile = Object.freeze({
38
+ hasImage: false,
39
+ imageCount: 0,
40
+ hasVideo: false,
41
+ hasBlockquote: false,
42
+ hasList: false,
43
+ hasTable: false,
44
+ hasDate: false,
45
+ hasNumberHighlight: false,
46
+ wordCount: 0,
47
+ });
48
+
49
+ const DATE_PATTERNS: RegExp[] = [
50
+ /\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2},?\s+)?\d{4}\b/i,
51
+ /\b\d{1,2}\s+(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}\b/i,
52
+ /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\.?\s+\d{1,2},?\s+\d{4}\b/i,
53
+ /\b\d{4}-\d{2}-\d{2}\b/,
54
+ /\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/,
55
+ /\bQ[1-4]\s+\d{4}\b/,
56
+ /\b\d{4}s\b/,
57
+ /\b\d{1,2}(st|nd|rd|th)\s+century\b/i,
58
+ ];
59
+
60
+ /**
61
+ * Matches a "highlight number" — a prominent figure suitable for a Stat
62
+ * Highlight block. Qualifies on any of:
63
+ * - currency prefix: `$2.3M`, `€500`, `¥1,234`
64
+ * - unit suffix: `50%`, `100 years`, `5×`, `2.3M`
65
+ * - standalone large: `1,234`, `5000`, `12,345`
66
+ * Plain small numbers (`42`) intentionally don't qualify.
67
+ */
68
+ const NUMBER_HIGHLIGHT_RE =
69
+ /(?:[$€£¥]\s?\d+(?:[.,]\d+)*(?:\s?(?:[MBK]|million|billion|thousand))?|\d+(?:[.,]\d+)*\s?(?:%|‰|x|×|[MBK]|million|billion|thousand|percent|years?|days?|hours?)|\d{3,}(?:[.,]\d+)*)/i;
70
+
71
+ const VIDEO_HOST_RE = /(youtube\.com|youtu\.be|vimeo\.com|wistia\.|loom\.com)/i;
72
+
73
+ function htmlElementsByTag(root: MarkdownNode, tagNames: Set<string>): HtmlElement[] {
74
+ const blocks = findNodesByType<MarkdownHtmlBlock>(root, 'htmlBlock');
75
+ // Inline HTML (htmlInline) also carries htmlChildren; include both.
76
+ const inline = findNodesByType<MarkdownNode>(root, 'htmlInline') as Array<
77
+ MarkdownNode & { htmlChildren?: HtmlNode[] }
78
+ >;
79
+ const out: HtmlElement[] = [];
80
+ const visit = (n: HtmlNode): void => {
81
+ if (n.type === 'htmlElement') {
82
+ if (tagNames.has(n.tagName.toLowerCase())) out.push(n);
83
+ for (const c of n.children) visit(c);
84
+ }
85
+ };
86
+ for (const b of blocks) for (const c of b.htmlChildren) visit(c);
87
+ for (const i of inline) {
88
+ if (Array.isArray(i.htmlChildren)) for (const c of i.htmlChildren) visit(c);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * Walk a slice of block nodes and summarize what kinds of content it
95
+ * contains. Cheap to run — no markdown parsing, just AST traversal.
96
+ */
97
+ export function profileBlockContents(nodes: MarkdownBlockNode[]): BlockContentProfile {
98
+ if (!nodes || nodes.length === 0) return { ...EMPTY_PROFILE };
99
+
100
+ let imageCount = 0;
101
+ let hasBlockquote = false;
102
+ let hasList = false;
103
+ let hasTable = false;
104
+
105
+ const plainParts: string[] = [];
106
+
107
+ for (const node of nodes) {
108
+ imageCount += findNodesByType(node, 'image').length;
109
+ imageCount += findNodesByType(node, 'imageReference').length;
110
+ if (findNodesByType(node, 'blockquote').length > 0) hasBlockquote = true;
111
+ if (findNodesByType(node, 'list').length > 0) hasList = true;
112
+ if (findNodesByType(node, 'table').length > 0) hasTable = true;
113
+ plainParts.push(extractPlainText(node));
114
+ }
115
+
116
+ // HTML embeds: <img>, <video>, <iframe src=…video host>
117
+ const root: MarkdownNode = { type: 'document', children: nodes };
118
+ const imgTags = htmlElementsByTag(root, new Set(['img']));
119
+ imageCount += imgTags.length;
120
+
121
+ const videoTags = htmlElementsByTag(root, new Set(['video', 'iframe', 'source']));
122
+ let hasVideo = false;
123
+ for (const el of videoTags) {
124
+ if (el.tagName.toLowerCase() === 'video') {
125
+ hasVideo = true;
126
+ break;
127
+ }
128
+ const src = el.attributes.src || el.attributes.href || '';
129
+ if (VIDEO_HOST_RE.test(src)) {
130
+ hasVideo = true;
131
+ break;
132
+ }
133
+ }
134
+
135
+ // Link-only video embeds (markdown link to a known video host).
136
+ if (!hasVideo) {
137
+ const links = findNodesByType<MarkdownNode & { url?: string }>(root, 'link');
138
+ for (const link of links) {
139
+ if (link.url && VIDEO_HOST_RE.test(link.url)) {
140
+ hasVideo = true;
141
+ break;
142
+ }
143
+ }
144
+ }
145
+
146
+ const plainText = plainParts.join(' ').trim();
147
+ const wordCount = plainText ? plainText.split(/\s+/).length : 0;
148
+
149
+ let hasDate = false;
150
+ for (const re of DATE_PATTERNS) {
151
+ if (re.test(plainText)) {
152
+ hasDate = true;
153
+ break;
154
+ }
155
+ }
156
+
157
+ // Number highlight: a paragraph that's mostly a single prominent figure.
158
+ let hasNumberHighlight = false;
159
+ walkMarkdownTree(root, (n) => {
160
+ if (hasNumberHighlight) return true;
161
+ if (n.type !== 'paragraph') return;
162
+ const text = extractPlainText(n).trim();
163
+ if (!text) return true;
164
+ const m = text.match(NUMBER_HIGHLIGHT_RE);
165
+ if (!m) return true;
166
+ // Paragraph qualifies when its word count is small AND a prominent
167
+ // number occupies a meaningful chunk of it. Keeps "the company grew
168
+ // 50% last year" from triggering, but accepts "$2.3M raised".
169
+ const words = text.split(/\s+/).length;
170
+ if (words <= 8) hasNumberHighlight = true;
171
+ return true;
172
+ });
173
+
174
+ return {
175
+ hasImage: imageCount > 0,
176
+ imageCount,
177
+ hasVideo,
178
+ hasBlockquote,
179
+ hasList,
180
+ hasTable,
181
+ hasDate,
182
+ hasNumberHighlight,
183
+ wordCount,
184
+ };
185
+ }
186
+
187
+ // ── Recommendation rules ───────────────────────────────────────────
188
+
189
+ const UNIVERSAL_DEFAULTS = ['title', 'sectionHeader', 'factCard', 'twoColumn'];
190
+
191
+ /**
192
+ * Given a content profile, return the canonical template-name list that
193
+ * should appear in the "Recommended" bucket. Caller is responsible for
194
+ * preserving deduplication / ordering when intersecting with the full
195
+ * template list (see {@link recommendTemplatesForBlock}).
196
+ */
197
+ function recommendedNamesForProfile(profile: BlockContentProfile): string[] {
198
+ const names = new Set<string>();
199
+ let anyContentSignal = false;
200
+
201
+ if (profile.hasImage) {
202
+ anyContentSignal = true;
203
+ names.add('imageWithCaption');
204
+ names.add('leftFeature');
205
+ names.add('rightFeature');
206
+ if (profile.imageCount >= 2) names.add('photoGrid');
207
+ }
208
+
209
+ if (profile.hasVideo) {
210
+ anyContentSignal = true;
211
+ names.add('videoWithCaption');
212
+ if (profile.hasBlockquote) names.add('videoPullQuote');
213
+ }
214
+
215
+ if (profile.hasBlockquote) {
216
+ anyContentSignal = true;
217
+ names.add('quote');
218
+ names.add('pullQuote');
219
+ names.add('fullBleedQuote');
220
+ }
221
+
222
+ if (profile.hasList) {
223
+ anyContentSignal = true;
224
+ names.add('list');
225
+ }
226
+
227
+ if (profile.hasTable) {
228
+ anyContentSignal = true;
229
+ names.add('dataTable');
230
+ names.add('comparisonBar');
231
+ }
232
+
233
+ if (profile.hasDate) {
234
+ anyContentSignal = true;
235
+ names.add('dateEvent');
236
+ }
237
+
238
+ if (profile.hasNumberHighlight) {
239
+ anyContentSignal = true;
240
+ names.add('statHighlight');
241
+ }
242
+
243
+ if (anyContentSignal) {
244
+ // Universal pair only — keep recommended focused.
245
+ names.add('title');
246
+ names.add('sectionHeader');
247
+ } else {
248
+ for (const n of UNIVERSAL_DEFAULTS) names.add(n);
249
+ }
250
+
251
+ return Array.from(names);
252
+ }
253
+
254
+ /**
255
+ * Split a flat list of template names into a "Recommended" bucket
256
+ * (matches the active block's content profile) and a "rest" bucket
257
+ * (everything else). Both buckets preserve the input ordering of
258
+ * `allNames`, so the visual layout stays predictable as templates are
259
+ * added.
260
+ */
261
+ export function recommendTemplatesForBlock(
262
+ profile: BlockContentProfile,
263
+ allNames: readonly string[],
264
+ ): RecommendationResult {
265
+ const desired = new Set(recommendedNamesForProfile(profile));
266
+ const recommended: string[] = [];
267
+ const rest: string[] = [];
268
+ for (const name of allNames) {
269
+ if (desired.has(name)) recommended.push(name);
270
+ else rest.push(name);
271
+ }
272
+ return { recommended, rest };
273
+ }
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * Defines a template system for AI-generated doc blocks. Instead of
5
5
  * specifying low-level layers, positions, and animations, AI can use
6
- * high-level templates like "titleBlock" or "statHighlight" with simple
6
+ * high-level templates like "title" or "statHighlight" with simple
7
7
  * content parameters.
8
8
  *
9
9
  * Templates are expanded into full Block structures at render time,
10
10
  * ensuring consistent styling and reducing AI generation errors.
11
11
  *
12
12
  * Template Types:
13
- * - titleBlock: Doc intro with title + subtitle
13
+ * - title: Doc intro with title + subtitle
14
14
  * - sectionHeader: Colored section divider
15
15
  * - statHighlight: Big number/stat with description
16
- * - quoteBlock: Large centered quote
16
+ * - quote: Large centered quote
17
17
  * - factCard: Key fact with explanation
18
18
  * - twoColumn: Side-by-side comparison
19
19
  * - dateEvent: Timeline-style date + description
@@ -116,7 +116,7 @@ interface BaseTemplateBlock {
116
116
  * Title block - doc intro with large title and subtitle.
117
117
  */
118
118
  export interface TitleBlockInput extends BaseTemplateBlock {
119
- template: 'titleBlock';
119
+ template: 'title';
120
120
  /** Main title text */
121
121
  title: string;
122
122
  /** Subtitle or tagline (supports \n for line breaks) */
@@ -164,7 +164,7 @@ export interface StatHighlightInput extends BaseTemplateBlock {
164
164
  * Quote block - large centered quote text.
165
165
  */
166
166
  export interface QuoteBlockInput extends BaseTemplateBlock {
167
- template: 'quoteBlock';
167
+ template: 'quote';
168
168
  /** The quote text (supports \n for line breaks) */
169
169
  quote: string;
170
170
  /** Attribution (author, source) */
@@ -252,6 +252,47 @@ export interface ImageWithCaptionInput extends BaseTemplateBlock {
252
252
  imageLicense?: string;
253
253
  }
254
254
 
255
+ /**
256
+ * Shared fields for left/right feature blocks. A "feature" pairs an
257
+ * image on one side of the block with a title and body paragraph on
258
+ * the other — a common editorial layout for product highlights,
259
+ * profile callouts, or section intros.
260
+ */
261
+ interface FeatureInputBase extends BaseTemplateBlock {
262
+ /** Path to the feature image. */
263
+ imageSrc: string;
264
+ /** Alt text for accessibility. */
265
+ imageAlt?: string;
266
+ /**
267
+ * Explicit display width of the image, in pixels (typically pulled
268
+ * from a `<img width>` attribute when the WYSIWYG editor resized it).
269
+ * When set, the renderer treats the image as a sized asset and
270
+ * centers it inside its half with padding instead of stretching it
271
+ * to fill the cell.
272
+ */
273
+ imageWidth?: number;
274
+ /** Explicit display height of the image, in pixels. */
275
+ imageHeight?: number;
276
+ /** Heading / title text shown next to the image. */
277
+ title?: string;
278
+ /** Body text shown below the title. */
279
+ body?: string;
280
+ }
281
+
282
+ /**
283
+ * Left feature — image on the left, title + body stacked on the right.
284
+ */
285
+ export interface LeftFeatureInput extends FeatureInputBase {
286
+ template: 'leftFeature';
287
+ }
288
+
289
+ /**
290
+ * Right feature — image on the right, title + body stacked on the left.
291
+ */
292
+ export interface RightFeatureInput extends FeatureInputBase {
293
+ template: 'rightFeature';
294
+ }
295
+
255
296
  /**
256
297
  * Map block - geographic map showing article location.
257
298
  *
@@ -260,7 +301,7 @@ export interface ImageWithCaptionInput extends BaseTemplateBlock {
260
301
  * See docs/MAP_TILES.md for available tile styles and attribution.
261
302
  */
262
303
  export interface MapBlockInput extends BaseTemplateBlock {
263
- template: 'mapBlock';
304
+ template: 'map';
264
305
  /** Map center coordinates */
265
306
  center: {
266
307
  lat: number;
@@ -299,7 +340,7 @@ export interface FullBleedQuoteInput extends BaseTemplateBlock {
299
340
  * Good for enumerations like "things to see" or "key features".
300
341
  */
301
342
  export interface ListBlockInput extends BaseTemplateBlock {
302
- template: 'listBlock';
343
+ template: 'list';
303
344
  /** List items (3-5 recommended) */
304
345
  items: string[];
305
346
  /** Optional header above the list */
@@ -459,6 +500,8 @@ export type TemplateBlock =
459
500
  | TwoColumnInput
460
501
  | DateEventInput
461
502
  | ImageWithCaptionInput
503
+ | LeftFeatureInput
504
+ | RightFeatureInput
462
505
  | MapBlockInput
463
506
  | FullBleedQuoteInput
464
507
  | ListBlockInput