@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
@@ -0,0 +1,415 @@
1
+ import { describe, expect, it, beforeEach } from 'vitest';
2
+ import { MemoryContentContainer } from '../storage/ContentContainer';
3
+ import {
4
+ DocumentVersionManager,
5
+ VERSIONS_PREFIX,
6
+ buildVersionPath,
7
+ coalesceVersions,
8
+ formatVersionTimestamp,
9
+ getDocBasename,
10
+ listVersions,
11
+ parseVersionPath,
12
+ parseVersionTimestamp,
13
+ pruneVersions,
14
+ readVersion,
15
+ revertToVersion,
16
+ saveVersion,
17
+ } from '../versions';
18
+
19
+ const decoder = new TextDecoder();
20
+
21
+ async function readUtf8(c: MemoryContentContainer, path: string): Promise<string | null> {
22
+ const data = await c.readFile(path);
23
+ return data ? decoder.decode(data) : null;
24
+ }
25
+
26
+ describe('formatVersionTimestamp / parseVersionTimestamp', () => {
27
+ it('round-trips to second precision', () => {
28
+ const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
29
+ const stamp = formatVersionTimestamp(d);
30
+ expect(stamp).toBe('20260430T152030Z');
31
+ const parsed = parseVersionTimestamp(stamp);
32
+ expect(parsed?.getTime()).toBe(d.getTime());
33
+ });
34
+
35
+ it('returns null for invalid input', () => {
36
+ expect(parseVersionTimestamp('not-a-stamp')).toBeNull();
37
+ expect(parseVersionTimestamp('20260430T152030')).toBeNull(); // missing Z
38
+ expect(parseVersionTimestamp('2026-04-30T15:20:30Z')).toBeNull(); // wrong format
39
+ });
40
+
41
+ it('orders chronologically when sorted lexicographically', () => {
42
+ const a = formatVersionTimestamp(new Date(Date.UTC(2026, 0, 1, 0, 0, 0)));
43
+ const b = formatVersionTimestamp(new Date(Date.UTC(2026, 0, 1, 0, 0, 1)));
44
+ const c = formatVersionTimestamp(new Date(Date.UTC(2026, 5, 15, 12, 0, 0)));
45
+ expect([c, a, b].sort()).toEqual([a, b, c]);
46
+ });
47
+ });
48
+
49
+ describe('paths', () => {
50
+ it('getDocBasename strips folder + extension', () => {
51
+ expect(getDocBasename('index.md')).toBe('index');
52
+ expect(getDocBasename('subdir/foo.md')).toBe('foo');
53
+ expect(getDocBasename('a/b/c/longname.md')).toBe('longname');
54
+ expect(getDocBasename('noext')).toBe('noext');
55
+ });
56
+
57
+ it('buildVersionPath round-trips through parseVersionPath', () => {
58
+ const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
59
+ const p = buildVersionPath('index', d);
60
+ expect(p).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
61
+ const parsed = parseVersionPath(p);
62
+ expect(parsed).not.toBeNull();
63
+ expect(parsed!.basename).toBe('index');
64
+ expect(parsed!.timestamp.getTime()).toBe(d.getTime());
65
+ expect(parsed!.collision).toBe(0);
66
+ });
67
+
68
+ it('buildVersionPath / parseVersionPath handle collision suffix', () => {
69
+ const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
70
+ const p1 = buildVersionPath('index', d, 1);
71
+ expect(p1).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-2.md`);
72
+ const parsed = parseVersionPath(p1);
73
+ expect(parsed?.collision).toBe(1);
74
+ });
75
+
76
+ it('parseVersionPath returns null for non-version files', () => {
77
+ expect(parseVersionPath('index.md')).toBeNull();
78
+ expect(parseVersionPath('.versions/notes.txt')).toBeNull();
79
+ expect(parseVersionPath('.versions/index.invalidstamp.md')).toBeNull();
80
+ expect(parseVersionPath('.versions/index.20260430T152030Z-1.md')).toBeNull(); // suffix must be >= 2
81
+ });
82
+ });
83
+
84
+ describe('saveVersion', () => {
85
+ let container: MemoryContentContainer;
86
+
87
+ beforeEach(() => {
88
+ container = new MemoryContentContainer();
89
+ });
90
+
91
+ it('returns no-document when container has no markdown', async () => {
92
+ const result = await saveVersion(container);
93
+ expect(result.saved).toBe(false);
94
+ expect(result.reason).toBe('no-document');
95
+ });
96
+
97
+ it('returns empty when document content is empty', async () => {
98
+ await container.writeDocument('', 'index.md');
99
+ const result = await saveVersion(container);
100
+ expect(result.saved).toBe(false);
101
+ expect(result.reason).toBe('empty');
102
+ });
103
+
104
+ it('writes a snapshot to .versions/ on first save', async () => {
105
+ await container.writeDocument('# hello', 'index.md');
106
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
107
+ const result = await saveVersion(container, { now });
108
+
109
+ expect(result.saved).toBe(true);
110
+ expect(result.reason).toBe('saved');
111
+ expect(result.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
112
+ expect(result.version?.basename).toBe('index');
113
+ expect(await readUtf8(container, result.version!.path)).toBe('# hello');
114
+ });
115
+
116
+ it('returns unchanged on identical second save', async () => {
117
+ await container.writeDocument('# hello', 'index.md');
118
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
119
+ const result = await saveVersion(container, {
120
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)),
121
+ });
122
+ expect(result.saved).toBe(false);
123
+ expect(result.reason).toBe('unchanged');
124
+ const all = await listVersions(container);
125
+ expect(all).toHaveLength(1);
126
+ });
127
+
128
+ it('writes again when content changes', async () => {
129
+ await container.writeDocument('# hello', 'index.md');
130
+ const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
131
+ await container.writeDocument('# hello world', 'index.md');
132
+ const r2 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)) });
133
+
134
+ expect(r1.saved).toBe(true);
135
+ expect(r2.saved).toBe(true);
136
+ expect(r1.version?.path).not.toBe(r2.version?.path);
137
+ expect(await listVersions(container)).toHaveLength(2);
138
+ });
139
+
140
+ it('force: true writes despite equality', async () => {
141
+ await container.writeDocument('# hello', 'index.md');
142
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
143
+ const r = await saveVersion(container, {
144
+ force: true,
145
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)),
146
+ });
147
+ expect(r.saved).toBe(true);
148
+ expect(await listVersions(container)).toHaveLength(2);
149
+ });
150
+
151
+ it('appends a collision suffix when two saves land on the same UTC second', async () => {
152
+ await container.writeDocument('# hello', 'index.md');
153
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
154
+ const r1 = await saveVersion(container, { now });
155
+ // Mutate doc and force-save in the same second.
156
+ await container.writeDocument('# hello v2', 'index.md');
157
+ const r2 = await saveVersion(container, { now });
158
+
159
+ expect(r1.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
160
+ expect(r2.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-2.md`);
161
+ });
162
+
163
+ it('content option overrides container.readDocument()', async () => {
164
+ await container.writeDocument('# stored', 'index.md');
165
+ const r = await saveVersion(container, {
166
+ content: '# override',
167
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)),
168
+ });
169
+ expect(r.saved).toBe(true);
170
+ expect(await readUtf8(container, r.version!.path)).toBe('# override');
171
+ });
172
+
173
+ it('basename option overrides the derived basename', async () => {
174
+ await container.writeDocument('# hi', 'index.md');
175
+ const r = await saveVersion(container, {
176
+ basename: 'custom',
177
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)),
178
+ });
179
+ expect(r.version?.path).toBe(`${VERSIONS_PREFIX}custom.20260430T152030Z.md`);
180
+ });
181
+ });
182
+
183
+ describe('listVersions', () => {
184
+ let container: MemoryContentContainer;
185
+ beforeEach(() => {
186
+ container = new MemoryContentContainer();
187
+ });
188
+
189
+ it('ignores non-matching filenames inside .versions/', async () => {
190
+ await container.writeFile(`${VERSIONS_PREFIX}README.txt`, new TextEncoder().encode('skip me'));
191
+ await container.writeFile(
192
+ `${VERSIONS_PREFIX}index.invalidstamp.md`,
193
+ new TextEncoder().encode('also skip'),
194
+ );
195
+ expect(await listVersions(container)).toEqual([]);
196
+ });
197
+
198
+ it('returns newest-first', async () => {
199
+ await container.writeDocument('a', 'index.md');
200
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
201
+ await container.writeDocument('b', 'index.md');
202
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 11, 0, 0)) });
203
+ await container.writeDocument('c', 'index.md');
204
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 12, 0, 0)) });
205
+
206
+ const versions = await listVersions(container);
207
+ expect(versions.map((v) => v.timestamp.getUTCHours())).toEqual([12, 11, 10]);
208
+ });
209
+
210
+ it('basename filter excludes other docs', async () => {
211
+ await container.writeFile(
212
+ `${VERSIONS_PREFIX}foo.20260430T100000Z.md`,
213
+ new TextEncoder().encode('foo'),
214
+ );
215
+ await container.writeFile(
216
+ `${VERSIONS_PREFIX}bar.20260430T100000Z.md`,
217
+ new TextEncoder().encode('bar'),
218
+ );
219
+ expect((await listVersions(container, 'foo')).map((v) => v.basename)).toEqual(['foo']);
220
+ });
221
+
222
+ it('orders collision-suffix snapshots newest-first within the same UTC second', async () => {
223
+ // Three saves at the same UTC second produce paths with collisions 0, 1, 2.
224
+ // The lex order of the suffix bytes (`-` < `.`) does not match the write
225
+ // order, so the comparator must use the parsed collision number.
226
+ await container.writeDocument('a', 'index.md');
227
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
228
+ await saveVersion(container, { now });
229
+ await container.writeDocument('b', 'index.md');
230
+ await saveVersion(container, { now });
231
+ await container.writeDocument('c', 'index.md');
232
+ await saveVersion(container, { now });
233
+
234
+ const versions = await listVersions(container);
235
+ expect(versions.map((v) => v.collision)).toEqual([2, 1, 0]);
236
+ expect(versions[0]!.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-3.md`);
237
+ expect(versions[2]!.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
238
+ });
239
+
240
+ it('saveVersion dedup compares against the actual newest snapshot after a collision', async () => {
241
+ // Regression: when a collision suffix exists, dedup must read the
242
+ // collision file (the real "latest"), not the earlier no-suffix file.
243
+ await container.writeDocument('first', 'index.md');
244
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
245
+ await saveVersion(container, { now });
246
+ await container.writeDocument('second', 'index.md');
247
+ await saveVersion(container, { now });
248
+
249
+ // Latest content on disk is "second"; saving it again should be a no-op.
250
+ const r = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)) });
251
+ expect(r.saved).toBe(false);
252
+ expect(r.reason).toBe('unchanged');
253
+ });
254
+ });
255
+
256
+ describe('readVersion', () => {
257
+ it('accepts both Version object and string path', async () => {
258
+ const container = new MemoryContentContainer();
259
+ await container.writeDocument('# content', 'index.md');
260
+ const r = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
261
+ expect(await readVersion(container, r.version!)).toBe('# content');
262
+ expect(await readVersion(container, r.version!.path)).toBe('# content');
263
+ });
264
+
265
+ it('returns null for missing snapshot', async () => {
266
+ const container = new MemoryContentContainer();
267
+ expect(await readVersion(container, '.versions/missing.20260430T100000Z.md')).toBeNull();
268
+ });
269
+ });
270
+
271
+ describe('revertToVersion', () => {
272
+ let container: MemoryContentContainer;
273
+ beforeEach(() => {
274
+ container = new MemoryContentContainer();
275
+ });
276
+
277
+ it('writes a snapshot of the current doc before reverting (default)', async () => {
278
+ await container.writeDocument('# v1', 'index.md');
279
+ const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
280
+ await container.writeDocument('# v2 in flight', 'index.md');
281
+
282
+ const result = await revertToVersion(container, r1.version!);
283
+ expect(result.reverted).toBe(true);
284
+ expect(result.snapshotted).not.toBeNull();
285
+ expect(await container.readDocument()).toBe('# v1');
286
+ // We should now have two snapshots: the original + the just-snapshotted v2.
287
+ expect(await listVersions(container)).toHaveLength(2);
288
+ });
289
+
290
+ it('snapshotCurrent: false skips the safety snapshot', async () => {
291
+ await container.writeDocument('# v1', 'index.md');
292
+ const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
293
+ await container.writeDocument('# v2 in flight', 'index.md');
294
+
295
+ const result = await revertToVersion(container, r1.version!, { snapshotCurrent: false });
296
+ expect(result.snapshotted).toBeNull();
297
+ expect(await container.readDocument()).toBe('# v1');
298
+ expect(await listVersions(container)).toHaveLength(1);
299
+ });
300
+
301
+ it('returns reverted: false for missing snapshot', async () => {
302
+ const result = await revertToVersion(container, '.versions/missing.20260430T100000Z.md');
303
+ expect(result.reverted).toBe(false);
304
+ });
305
+ });
306
+
307
+ describe('pruneVersions', () => {
308
+ let container: MemoryContentContainer;
309
+
310
+ async function seed(times: number): Promise<void> {
311
+ for (let i = 0; i < times; i++) {
312
+ await container.writeDocument(`# v${i}`, 'index.md');
313
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10 + i, 0, 0)) });
314
+ }
315
+ }
316
+
317
+ beforeEach(() => {
318
+ container = new MemoryContentContainer();
319
+ });
320
+
321
+ it('keep-last-n keeps the n newest', async () => {
322
+ await seed(5);
323
+ const deleted = await pruneVersions(container, { type: 'keep-last-n', n: 3 });
324
+ expect(deleted).toHaveLength(2);
325
+ const remaining = await listVersions(container);
326
+ expect(remaining).toHaveLength(3);
327
+ expect(remaining.map((v) => v.timestamp.getUTCHours())).toEqual([14, 13, 12]);
328
+ });
329
+
330
+ it('keep-last-n with n: 0 deletes everything', async () => {
331
+ await seed(3);
332
+ await pruneVersions(container, { type: 'keep-last-n', n: 0 });
333
+ expect(await listVersions(container)).toHaveLength(0);
334
+ });
335
+
336
+ it('older-than deletes only items older than the cutoff', async () => {
337
+ await seed(5);
338
+ const cutoff = new Date(Date.UTC(2026, 3, 30, 12, 0, 0));
339
+ const deleted = await pruneVersions(container, { type: 'older-than', date: cutoff });
340
+ // hours 10 and 11 are older than 12 → deleted.
341
+ expect(deleted).toHaveLength(2);
342
+ const remaining = await listVersions(container);
343
+ expect(remaining.map((v) => v.timestamp.getUTCHours())).toEqual([14, 13, 12]);
344
+ });
345
+
346
+ it('predicate keeps items the predicate accepts', async () => {
347
+ await seed(4);
348
+ const deleted = await pruneVersions(container, {
349
+ type: 'predicate',
350
+ keep: (v) => v.timestamp.getUTCHours() % 2 === 0,
351
+ });
352
+ // 10 and 12 are even (kept); 11 and 13 are odd (deleted).
353
+ expect(deleted).toHaveLength(2);
354
+ expect((await listVersions(container)).map((v) => v.timestamp.getUTCHours())).toEqual([12, 10]);
355
+ });
356
+ });
357
+
358
+ describe('coalesceVersions', () => {
359
+ it('collapses snapshots within the window', async () => {
360
+ const container = new MemoryContentContainer();
361
+ // Three saves: t=0, t=30s (within window), t=2min (outside window).
362
+ await container.writeDocument('a', 'index.md');
363
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
364
+ await container.writeDocument('b', 'index.md');
365
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 30)) });
366
+ await container.writeDocument('c', 'index.md');
367
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 2, 0)) });
368
+
369
+ const deleted = await coalesceVersions(container, { windowMs: 60_000 });
370
+ // Walking newest-first: the (10:00:30, 10:00:00) pair is 30s apart
371
+ // (within the window), so 10:00:00 — the older of the two — is dropped.
372
+ // The (10:02:00, 10:00:30) pair is 90s apart, so 10:00:30 survives.
373
+ expect(deleted).toHaveLength(1);
374
+ expect(deleted[0]!.timestamp.getTime()).toBe(Date.UTC(2026, 3, 30, 10, 0, 0));
375
+ });
376
+
377
+ it('anchors to the last kept snapshot instead of chaining adjacent pairs', async () => {
378
+ // Regression: four snapshots 30s apart with windowMs: 60_000 should keep
379
+ // the newest plus anything beyond 60s of the anchor (here, t=0). Chaining
380
+ // through deleted entries would erase everything but the newest.
381
+ const container = new MemoryContentContainer();
382
+ const base = Date.UTC(2026, 3, 30, 10, 0, 0);
383
+ await container.writeDocument('a', 'index.md');
384
+ await saveVersion(container, { now: new Date(base) });
385
+ await container.writeDocument('b', 'index.md');
386
+ await saveVersion(container, { now: new Date(base + 30_000) });
387
+ await container.writeDocument('c', 'index.md');
388
+ await saveVersion(container, { now: new Date(base + 60_000) });
389
+ await container.writeDocument('d', 'index.md');
390
+ await saveVersion(container, { now: new Date(base + 90_000) });
391
+
392
+ const deleted = await coalesceVersions(container, { windowMs: 60_000 });
393
+ // Anchor=t+90 keeps t+30 dropped (60s ≤ window), keeps t=0 (90s > window
394
+ // from anchor → new anchor). t+60 is also within 60s of t+90 → dropped.
395
+ expect(deleted).toHaveLength(2);
396
+ const remaining = await listVersions(container);
397
+ expect(remaining.map((v) => v.timestamp.getTime() - base)).toEqual([90_000, 0]);
398
+ });
399
+ });
400
+
401
+ describe('DocumentVersionManager', () => {
402
+ it('forwards calls and applies the basename override', async () => {
403
+ const container = new MemoryContentContainer();
404
+ await container.writeDocument('# hi', 'index.md');
405
+ const manager = new DocumentVersionManager(container, { basename: 'custom' });
406
+ const r = await manager.saveVersion({ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
407
+ expect(r.version?.path).toBe(`${VERSIONS_PREFIX}custom.20260430T152030Z.md`);
408
+
409
+ const list = await manager.listVersions();
410
+ expect(list).toHaveLength(1);
411
+
412
+ const content = await manager.readVersion(list[0]!);
413
+ expect(content).toBe('# hi');
414
+ });
415
+ });
@@ -112,35 +112,117 @@ function flattenForAudio(blocks: Block[]): Block[] {
112
112
  return result;
113
113
  }
114
114
 
115
- /** Info about an MP3 file found in the container. */
115
+ /**
116
+ * Info about a narration audio file found in the container.
117
+ *
118
+ * Originally limited to MP3, this now also covers WebM/MP4/Ogg/M4A
119
+ * audio files that the browser recorder writes into `audio/`. The type
120
+ * name is retained for backwards compatibility with the surrounding
121
+ * matching logic; the data shape is identical regardless of codec.
122
+ */
116
123
  interface Mp3Info {
117
- /** Path in the container (e.g., "images/c23n-issaquah-highlands-intro.mp3") */
124
+ /** Path in the container (e.g., "audio/narration-2026.webm"). */
118
125
  path: string;
119
- /** Filename without path (e.g., "c23n-issaquah-highlands-intro.mp3") */
126
+ /** Filename without path (e.g., "narration-2026.webm"). */
120
127
  filename: string;
121
- /** Parsed timing data (if .timing.json exists) */
128
+ /** Parsed timing data (if .timing.json exists). */
122
129
  timing: AudioTimingData | null;
123
130
  }
124
131
 
132
+ /**
133
+ * File extensions that unambiguously denote audio anywhere in the
134
+ * container. These predate the recorder feature.
135
+ */
136
+ const AUDIO_ONLY_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a'];
137
+
138
+ /**
139
+ * Extensions that could be audio or video — `.webm` and `.mp4` are the
140
+ * containers `MediaRecorder` produces for browser-side narration, but
141
+ * they're also used for video clips. Only treat them as narration when
142
+ * they live under the `audio/` directory the recorder writes to.
143
+ */
144
+ const AMBIGUOUS_EXTENSIONS = ['.webm', '.mp4'];
145
+
146
+ function isNarrationAudioPath(path: string): boolean {
147
+ const lower = path.toLowerCase();
148
+ if (AUDIO_ONLY_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
149
+ if (AMBIGUOUS_EXTENSIONS.some((ext) => lower.endsWith(ext))) {
150
+ return lower.startsWith('audio/');
151
+ }
152
+ return false;
153
+ }
154
+
155
+ /** Strip a known narration audio extension from a filename. */
156
+ function stripAudioExtension(filename: string): string {
157
+ const lower = filename.toLowerCase();
158
+ for (const ext of [...AUDIO_ONLY_EXTENSIONS, ...AMBIGUOUS_EXTENSIONS]) {
159
+ if (lower.endsWith(ext)) return filename.slice(0, -ext.length);
160
+ }
161
+ return filename;
162
+ }
163
+
125
164
  // ── Core Functions ───────────────────────────────────────────────────
126
165
 
127
166
  /**
128
- * Discover all MP3 files in a container and load their timing data.
167
+ * Parse a consolidated timing.json (version 2) which contains all section
168
+ * timing data in a single file.
169
+ */
170
+ function parseConsolidatedTiming(data: ArrayBuffer): Record<string, AudioTimingData> | null {
171
+ try {
172
+ const text = new TextDecoder().decode(data);
173
+ const parsed = JSON.parse(text);
174
+ if (parsed && parsed.version === 2 && parsed.sections) {
175
+ return parsed.sections as Record<string, AudioTimingData>;
176
+ }
177
+ return null;
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Discover all narration audio files in a container and load their
185
+ * timing data. Covers historical `.mp3`/`.wav`/`.ogg`/`.m4a` files
186
+ * anywhere in the container plus `.webm`/`.mp4` files under `audio/`
187
+ * (the destination the browser recorder writes to).
188
+ *
189
+ * Checks for a consolidated timing.json first, then falls back to
190
+ * per-file `.timing.json` sidecars.
129
191
  */
130
192
  async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
131
193
  const files = await container.listFiles();
132
- const mp3Files = files.filter((f) => f.path.endsWith('.mp3'));
194
+ const audioFiles = files.filter((f) => isNarrationAudioPath(f.path));
133
195
  const results: Mp3Info[] = [];
134
196
 
135
- for (const file of mp3Files) {
197
+ // Try to load consolidated timing.json
198
+ let consolidatedSections: Record<string, AudioTimingData> | null = null;
199
+ const consolidatedData = await container.readFile('timing.json');
200
+ if (consolidatedData) {
201
+ consolidatedSections = parseConsolidatedTiming(consolidatedData);
202
+ }
203
+
204
+ for (const file of audioFiles) {
136
205
  const filename = file.path.split('/').pop() ?? file.path;
137
206
  let timing: AudioTimingData | null = null;
138
207
 
139
- // Try to load timing.json
140
- const timingPath = `${file.path}.timing.json`;
141
- const timingData = await container.readFile(timingPath);
142
- if (timingData) {
143
- timing = parseTimingJson(timingData);
208
+ // Try consolidated timing first: match section name from filename
209
+ if (consolidatedSections) {
210
+ const audioBase = stripAudioExtension(filename);
211
+ for (const [sectionName, sectionTiming] of Object.entries(consolidatedSections)) {
212
+ if (audioBase.endsWith(`-${sectionName}`)) {
213
+ timing = sectionTiming;
214
+ break;
215
+ }
216
+ }
217
+ }
218
+
219
+ // Fall back to per-file .timing.json
220
+ if (!timing) {
221
+ const timingPath = `${file.path}.timing.json`;
222
+ const timingData = await container.readFile(timingPath);
223
+ if (timingData) {
224
+ timing = parseTimingJson(timingData);
225
+ }
144
226
  }
145
227
 
146
228
  results.push({ path: file.path, filename, timing });
@@ -221,7 +303,7 @@ function autoMatchBlocks(
221
303
  // Fallback: filename-based matching
222
304
  if (score < MIN_CONTENT_SIMILARITY && block.title) {
223
305
  const titleSlug = slugify(block.title);
224
- const fileSlug = slugify(mp3.filename.replace(/\.mp3$/, ''));
306
+ const fileSlug = slugify(stripAudioExtension(mp3.filename));
225
307
  // Check if the title slug appears in the filename slug
226
308
  if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
227
309
  score = Math.max(score, MIN_FILENAME_SIMILARITY + 0.1);
@@ -34,7 +34,7 @@ import type { ViewportConfig } from '../schemas/Viewport.js';
34
34
  import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
35
35
  import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
36
36
  import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
37
- import { templateRegistry } from './templates/index.js';
37
+ import { templateRegistry, resolveTemplateName } from './templates/index.js';
38
38
  import { expandPersistentLayers } from './templates/persistentLayers.js';
39
39
 
40
40
  // ============================================
@@ -98,29 +98,40 @@ export function getLayers(block: DocBlock, context: RenderContext = {}): Layer[]
98
98
  return injectPersistentLayers(existingLayers, block, context);
99
99
  }
100
100
 
101
- // 2. Template path: look up and call the template function
102
- if (isTemplateBlock(block) && block.template in templateRegistry) {
103
- const templateName = block.template as keyof typeof templateRegistry;
104
- const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
105
- let layers: Layer[];
106
- try {
107
- // Each registry entry accepts its specific TemplateBlock variant; the
108
- // discriminated union guarantees the shapes match at runtime.
109
- const templateFn = templateRegistry[templateName] as (
110
- input: TemplateBlock,
111
- ctx: TemplateContext,
112
- ) => Layer[];
113
- layers = templateFn(block, templateCtx);
114
- if (!Array.isArray(layers)) {
115
- console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
101
+ // 2. Template path: look up and call the template function.
102
+ //
103
+ // Resolve through TEMPLATE_ALIASES so legacy ids (`titleBlock`,
104
+ // `quoteBlock`, `mapBlock`, `listBlock`) hit the registry by their
105
+ // canonical short names. Without this the block-section path renders
106
+ // an empty SVG card: `hasTemplate()` accepts the alias (so
107
+ // `isAnnotatedBlock` returns true and the card wrapper renders), but
108
+ // a raw `block.template in templateRegistry` check below misses it
109
+ // and the layer list comes back empty.
110
+ if (isTemplateBlock(block)) {
111
+ const resolved = resolveTemplateName(block.template);
112
+ if (resolved in templateRegistry) {
113
+ const templateName = resolved as keyof typeof templateRegistry;
114
+ const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
115
+ let layers: Layer[];
116
+ try {
117
+ // Each registry entry accepts its specific TemplateBlock variant; the
118
+ // discriminated union guarantees the shapes match at runtime.
119
+ const templateFn = templateRegistry[templateName] as (
120
+ input: TemplateBlock,
121
+ ctx: TemplateContext,
122
+ ) => Layer[];
123
+ layers = templateFn(block, templateCtx);
124
+ if (!Array.isArray(layers)) {
125
+ console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
126
+ layers = [];
127
+ }
128
+ } catch (err: unknown) {
129
+ console.error(`Error expanding template ${templateName}:`, err);
116
130
  layers = [];
117
131
  }
118
- } catch (err: unknown) {
119
- console.error(`Error expanding template ${templateName}:`, err);
120
- layers = [];
121
- }
122
132
 
123
- return injectPersistentLayers(layers, block, context);
133
+ return injectPersistentLayers(layers, block, context);
134
+ }
124
135
  }
125
136
 
126
137
  // 3. Fallback: no layers and no known template