@bendyline/squisq 1.5.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
  6. package/dist/chunk-2PWOO4ZE.js.map +1 -0
  7. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  8. package/dist/chunk-3E5F2XMR.js.map +1 -0
  9. package/dist/chunk-4X3JQXNM.js +35 -0
  10. package/dist/chunk-4X3JQXNM.js.map +1 -0
  11. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  12. package/dist/chunk-5DKBFXPY.js.map +1 -0
  13. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  14. package/dist/chunk-5J4HKRJF.js.map +1 -0
  15. package/dist/chunk-5UYI3RJB.js +2503 -0
  16. package/dist/chunk-5UYI3RJB.js.map +1 -0
  17. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  18. package/dist/chunk-66QO7XFB.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-CY2R2VPR.js +884 -0
  22. package/dist/chunk-CY2R2VPR.js.map +1 -0
  23. package/dist/chunk-FVJUUTEM.js +74 -0
  24. package/dist/chunk-FVJUUTEM.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-MMPBOUK7.js} +10 -73
  30. package/dist/chunk-MMPBOUK7.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  36. package/dist/chunk-R2GC6RLD.js.map +1 -0
  37. package/dist/chunk-R2YZTCLO.js +292 -0
  38. package/dist/chunk-R2YZTCLO.js.map +1 -0
  39. package/dist/chunk-S7A5D7ZJ.js +174 -0
  40. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  41. package/dist/chunk-TFJVXUHH.js +2358 -0
  42. package/dist/chunk-TFJVXUHH.js.map +1 -0
  43. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  44. package/dist/chunk-TUWMK62B.js.map +1 -0
  45. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  46. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  47. package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
  48. package/dist/chunk-W4O2TX5I.js.map +1 -0
  49. package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
  50. package/dist/chunk-X6DUONGT.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +246 -80
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +54 -4
  114. package/src/__tests__/markdownSanitize.test.ts +7 -5
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +12 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +70 -2
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +138 -2
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/fallbackBlock.ts +1 -1
  174. package/src/doc/templates/index.ts +134 -279
  175. package/src/doc/templates/inputDescriptors.ts +20 -1
  176. package/src/doc/templates/listBlock.ts +88 -30
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +67 -5
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/index.ts +1 -0
  204. package/src/markdown/stringify.ts +4 -9
  205. package/src/markdown/utils.ts +36 -14
  206. package/src/narration/align.ts +514 -0
  207. package/src/narration/features.ts +231 -0
  208. package/src/narration/index.ts +86 -0
  209. package/src/narration/nuclei.ts +118 -0
  210. package/src/narration/pacing.ts +211 -0
  211. package/src/narration/script.ts +239 -0
  212. package/src/narration/session.ts +100 -0
  213. package/src/narration/sidecar.ts +187 -0
  214. package/src/narration/syllables.ts +85 -0
  215. package/src/narration/trace.ts +50 -0
  216. package/src/narration/types.ts +299 -0
  217. package/src/narration/vad.ts +131 -0
  218. package/src/recommend/numberHighlight.ts +25 -0
  219. package/src/recommend/templates.ts +108 -12
  220. package/src/schemas/BlockTemplates.ts +156 -7
  221. package/src/schemas/Doc.ts +72 -9
  222. package/src/schemas/Media.ts +12 -2
  223. package/src/schemas/Theme.ts +45 -25
  224. package/src/schemas/Viewport.ts +2 -2
  225. package/src/schemas/fontStacks.ts +79 -0
  226. package/src/schemas/themeCompile.ts +15 -0
  227. package/src/schemas/themeConstants.ts +2 -0
  228. package/src/schemas/themeLibrary.ts +25 -19
  229. package/src/schemas/themeValidator.ts +1 -1
  230. package/src/schemas/themes/gezellig.json +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-5TYS5V3G.js.map +0 -1
  247. package/dist/chunk-A4O7GIWE.js.map +0 -1
  248. package/dist/chunk-BEQODJWV.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-GPLTCSXO.js.map +0 -1
  251. package/dist/chunk-HWVFJAAH.js.map +0 -1
  252. package/dist/chunk-IMSCRLLV.js.map +0 -1
  253. package/dist/chunk-JELIRVDP.js +0 -24
  254. package/dist/chunk-JELIRVDP.js.map +0 -1
  255. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  256. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  257. package/dist/chunk-LTN6I7QW.js.map +0 -1
  258. package/dist/chunk-P3K7NLHF.js.map +0 -1
  259. package/dist/chunk-T5UK6YOB.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1801
  266. package/dist/story/index.js +0 -279
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -19,6 +19,43 @@ import {
19
19
  } from '../utils/themeUtils.js';
20
20
  import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
21
21
 
22
+ const LIST_ITEM_LINE_HEIGHT = 1.2;
23
+ const LIST_ITEM_GAP_PX = 18;
24
+
25
+ /**
26
+ * Mirror the renderer's character-based wrapping closely enough to reserve
27
+ * vertical space for each independently positioned text layer.
28
+ */
29
+ function estimateWrappedLineCount(text: string, fontSize: number, maxWidth: number): number {
30
+ if (!text.trim()) return 1;
31
+
32
+ const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
33
+ if (charsPerLine <= 0) return 1;
34
+
35
+ let lineCount = 0;
36
+ let currentLineLength = 0;
37
+
38
+ for (const word of text.split(/\s+/)) {
39
+ const testLineLength = currentLineLength ? currentLineLength + 1 + word.length : word.length;
40
+
41
+ if (testLineLength <= charsPerLine) {
42
+ currentLineLength = testLineLength;
43
+ continue;
44
+ }
45
+
46
+ if (currentLineLength) lineCount += 1;
47
+
48
+ let remainingLength = word.length;
49
+ while (remainingLength > charsPerLine) {
50
+ lineCount += 1;
51
+ remainingLength -= charsPerLine;
52
+ }
53
+ currentLineLength = remainingLength;
54
+ }
55
+
56
+ return Math.max(1, lineCount + (currentLineLength ? 1 : 0));
57
+ }
58
+
22
59
  export function listBlock(input: ListBlockInput, context: TemplateContext): Layer[] {
23
60
  const { title, accentImage } = input;
24
61
  // `items` is required by the schema, but malformed / partially-authored
@@ -66,8 +103,9 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
66
103
  // defensive against future edits to the accent-layout types).
67
104
  const centerX = parseFloat(accentLayout.textCenterX);
68
105
  const widthPct = parseFloat(accentLayout.textWidth);
69
- const leftX =
70
- Number.isFinite(centerX) && Number.isFinite(widthPct) ? `${centerX - widthPct / 2}%` : '8%';
106
+ const leftPct =
107
+ Number.isFinite(centerX) && Number.isFinite(widthPct) ? centerX - widthPct / 2 : 8;
108
+ const leftX = `${leftPct}%`;
71
109
 
72
110
  // Title if provided
73
111
  const startY = title ? 34 : 26;
@@ -95,28 +133,49 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
95
133
  });
96
134
  }
97
135
 
98
- // Stack items with a fixed compact gap rather than stretching them
99
- // across the available band. Distributing items across (startY 80%)
100
- // left big vertical gaps for short lists and made the slide read as a
101
- // sparse menu instead of a tight enumeration; conversely a bare
102
- // line-height gap read as a cramped paragraph — 18px of air keeps each
103
- // entry its own line without breaking the group.
104
- //
105
- // Spacing = item line-height (34px base × 1.2) + 18px gap, expressed
106
- // as % of the 1080px design canvas (~5.4%). Wrapped items push the
107
- // next entry down via their own line-height, so this sets the
108
- // minimum baseline-to-baseline distance for unwrapped items.
109
- const LIST_ITEM_BASE_PX = 34;
110
- const LIST_ITEM_LINE_HEIGHT = 1.2;
111
- const LIST_ITEM_GAP_PX = 18;
112
- const DESIGN_HEIGHT_PX = 1080;
113
- const spacing =
114
- ((LIST_ITEM_BASE_PX * LIST_ITEM_LINE_HEIGHT + LIST_ITEM_GAP_PX) / DESIGN_HEIGHT_PX) * 100;
136
+ // Render the number and body in separate columns. The body is therefore a
137
+ // single text layer whose wrapped lines all share the same x coordinate,
138
+ // producing a hanging indent instead of wrapping underneath the number.
139
+ const textWidthPx = (Number.isFinite(widthPct) ? widthPct / 100 : 0.85) * context.viewport.width;
140
+ const markerText = `${items.length}.`;
141
+ const markerWidthPx = Math.max(itemFontSize, markerText.length * itemFontSize * 0.5);
142
+ const markerGapPx = itemFontSize * 0.35;
143
+ const bodyIndentPx = markerWidthPx + markerGapPx;
144
+ const bodyLeftX = `${leftPct + (bodyIndentPx / context.viewport.width) * 100}%`;
145
+ const bodyWidthPx = Math.max(itemFontSize, textWidthPx - bodyIndentPx);
146
+ let itemY = startY;
115
147
 
116
148
  // List items with staggered animation
117
149
  for (let i = 0; i < items.length; i++) {
118
- const y = startY + spacing * i;
119
- const itemText = `${i + 1}. ${items[i]}`;
150
+ const itemText = items[i]!;
151
+ const lineCount = estimateWrappedLineCount(itemText, itemFontSize, bodyWidthPx);
152
+ const animation = themedEntrance(context, 'text', {
153
+ type: 'fadeIn',
154
+ duration: 0.8,
155
+ delay: 0.3 + 0.3 * i,
156
+ });
157
+
158
+ layers.push({
159
+ type: 'text',
160
+ id: `item-${i}-marker`,
161
+ content: {
162
+ text: `${i + 1}.`,
163
+ style: {
164
+ fontSize: itemFontSize,
165
+ fontFamily: getThemeFont(context, 'body'),
166
+ color: theme.colors.text,
167
+ textAlign: 'right',
168
+ lineHeight: LIST_ITEM_LINE_HEIGHT,
169
+ shadow: shouldUseShadow(context),
170
+ },
171
+ },
172
+ position: {
173
+ x: leftX,
174
+ y: adjustY(`${itemY}%`, accentLayout),
175
+ width: markerWidthPx,
176
+ },
177
+ animation,
178
+ });
120
179
 
121
180
  layers.push({
122
181
  type: 'text',
@@ -128,21 +187,20 @@ export function listBlock(input: ListBlockInput, context: TemplateContext): Laye
128
187
  fontFamily: getThemeFont(context, 'body'),
129
188
  color: theme.colors.text,
130
189
  textAlign: 'left',
131
- lineHeight: 1.2,
190
+ lineHeight: LIST_ITEM_LINE_HEIGHT,
132
191
  shadow: shouldUseShadow(context),
133
192
  },
134
193
  },
135
194
  position: {
136
- x: leftX,
137
- y: adjustY(`${y}%`, accentLayout),
138
- width: accentLayout.textWidth,
195
+ x: bodyLeftX,
196
+ y: adjustY(`${itemY}%`, accentLayout),
197
+ width: bodyWidthPx,
139
198
  },
140
- animation: themedEntrance(context, 'text', {
141
- type: 'fadeIn',
142
- duration: 0.8,
143
- delay: 0.3 + 0.3 * i,
144
- }),
199
+ animation,
145
200
  });
201
+
202
+ const itemHeightPx = lineCount * itemFontSize * LIST_ITEM_LINE_HEIGHT;
203
+ itemY += ((itemHeightPx + LIST_ITEM_GAP_PX) / context.viewport.height) * 100;
146
204
  }
147
205
 
148
206
  return layers;
@@ -108,6 +108,16 @@ export const TEMPLATE_METADATA: Record<string, TemplateMetadata> = {
108
108
  description:
109
109
  'Renders sub-headings as connected nodes — edit visually as a node-and-edge diagram.',
110
110
  },
111
+ tree: {
112
+ label: 'Tree',
113
+ description:
114
+ 'Renders an ASCII file-tree / outline fence as a filesystem-style treeview — edit as an outline.',
115
+ },
116
+ timeline: {
117
+ label: 'Timeline',
118
+ description:
119
+ 'Plots events across one or more horizontal tracks with callouts and optional branching links.',
120
+ },
111
121
  layout: {
112
122
  label: 'Layout',
113
123
  description:
@@ -27,6 +27,7 @@ import type {
27
27
  ProgressIndicatorConfig,
28
28
  } from '../../schemas/BlockTemplates.js';
29
29
  import type { Theme } from '../../schemas/Theme.js';
30
+ import { cloneData } from '../../internal/immutable.js';
30
31
  import { isPersistentLayerTemplate } from '../../schemas/BlockTemplates.js';
31
32
  import { oklchDarken, withAlpha } from '../../schemas/colorUtils.js';
32
33
 
@@ -415,9 +416,9 @@ function expandProgressIndicator(config: ProgressIndicatorConfig): Layer {
415
416
  * Expand a single persistent layer (template or raw) to raw Layer(s).
416
417
  */
417
418
  export function expandPersistentLayer(layer: PersistentLayer, theme?: Theme): Layer[] {
418
- // If already a raw Layer, return as-is
419
+ // If already a raw Layer, return an owned copy.
419
420
  if (!isPersistentLayerTemplate(layer)) {
420
- return [layer as Layer];
421
+ return [cloneData(layer as Layer)];
421
422
  }
422
423
 
423
424
  const template = layer as PersistentLayerTemplate;
@@ -466,128 +467,6 @@ export function expandPersistentLayers(
466
467
  );
467
468
  }
468
469
 
469
- // ============================================
470
- // Style Presets (legacy helper — prefer theme.persistentLayers)
471
- // ============================================
472
-
473
- /** Legacy style preset names. Prefer using `Theme.persistentLayers` directly. */
474
- export type DocStylePreset = 'minimal' | 'documentary' | 'branded' | 'cinematic' | 'clean';
475
-
476
- /**
477
- * Get a PersistentLayerConfig from a style preset.
478
- *
479
- * @param preset - Style preset name
480
- * @param articleTitle - Article title for title caption
481
- * @param heroSrc - Optional hero image for cinematic style
482
- */
483
- export function getDocStyleConfig(
484
- preset: DocStylePreset,
485
- articleTitle: string,
486
- heroSrc?: string,
487
- subtitle?: string,
488
- ): PersistentLayerConfig {
489
- switch (preset) {
490
- case 'minimal':
491
- return {};
492
-
493
- case 'documentary':
494
- return {
495
- bottomLayers: [
496
- {
497
- template: 'gradientBackground',
498
- config: { type: 'gradientBackground', preset: 'dark-vignette' },
499
- },
500
- ],
501
- topLayers: [
502
- {
503
- template: 'titleCaption',
504
- config: {
505
- type: 'titleCaption',
506
- title: articleTitle,
507
- subtitle,
508
- position: 'bottom-left',
509
- fontSize: 24,
510
- showThumbnail: !!heroSrc,
511
- thumbnailSrc: heroSrc,
512
- },
513
- },
514
- ],
515
- };
516
-
517
- case 'branded':
518
- return {
519
- bottomLayers: [
520
- {
521
- template: 'gradientBackground',
522
- config: { type: 'gradientBackground', preset: 'cool-blue' },
523
- },
524
- ],
525
- topLayers: [
526
- {
527
- template: 'titleCaption',
528
- config: {
529
- type: 'titleCaption',
530
- title: articleTitle,
531
- subtitle,
532
- position: 'bottom-left',
533
- fontSize: 26,
534
- showThumbnail: !!heroSrc,
535
- thumbnailSrc: heroSrc,
536
- },
537
- },
538
- ],
539
- };
540
-
541
- case 'cinematic':
542
- return {
543
- bottomLayers: heroSrc
544
- ? [
545
- {
546
- template: 'imageBackground',
547
- config: {
548
- type: 'imageBackground',
549
- src: heroSrc,
550
- blur: 12,
551
- opacity: 0.3,
552
- ambientMotion: 'zoomIn',
553
- },
554
- },
555
- ]
556
- : [
557
- {
558
- template: 'gradientBackground',
559
- config: { type: 'gradientBackground', preset: 'radial-dark' },
560
- },
561
- ],
562
- topLayers: [
563
- {
564
- template: 'titleCaption',
565
- config: {
566
- type: 'titleCaption',
567
- title: articleTitle,
568
- subtitle,
569
- position: 'bottom-right',
570
- fontSize: 22,
571
- },
572
- },
573
- ],
574
- };
575
-
576
- case 'clean':
577
- return {
578
- bottomLayers: [
579
- {
580
- template: 'solidBackground',
581
- config: { type: 'solidBackground', color: '#1a202c' },
582
- },
583
- ],
584
- };
585
-
586
- default:
587
- return {};
588
- }
589
- }
590
-
591
470
  /**
592
471
  * Get persistent layers from a Theme. Returns the theme's baked-in
593
472
  * persistentLayers config, or an empty config when the theme has none.
@@ -625,7 +504,7 @@ export function resolvePersistentLayers(
625
504
  /**
626
505
  * Compose a block's layers between pre-expanded persistent bottom/top
627
506
  * layers, honoring the per-block `useBottomLayer` / `useTopLayer` opt-outs.
628
- * Single shared implementation for `expandDocBlocks` and `getLayers`.
507
+ * Shared persistent-layer primitive used by `materializeBlockLayers`.
629
508
  */
630
509
  export function wrapWithPersistentLayers(
631
510
  layers: Layer[],
@@ -636,5 +515,12 @@ export function wrapWithPersistentLayers(
636
515
  if (bottomLayers.length === 0 && topLayers.length === 0) return layers;
637
516
  const useBottom = block.useBottomLayer !== false;
638
517
  const useTop = block.useTopLayer !== false;
639
- return [...(useBottom ? bottomLayers : []), ...layers, ...(useTop ? topLayers : [])];
518
+ // Each expanded block owns its persistent layers. This prevents a renderer
519
+ // (or a later timing/style pass) from mutating the caller's config or a
520
+ // sibling block through a shared layer reference.
521
+ return [
522
+ ...(useBottom ? bottomLayers.map((layer) => cloneData(layer)) : []),
523
+ ...layers,
524
+ ...(useTop ? topLayers.map((layer) => cloneData(layer)) : []),
525
+ ];
640
526
  }
@@ -0,0 +1,88 @@
1
+ /** Runtime registry composition for built-in and document-scoped templates. */
2
+
3
+ import type { Layer } from '../../schemas/Doc.js';
4
+ import type {
5
+ TemplateBlock,
6
+ TemplateContext,
7
+ TemplateRegistry,
8
+ } from '../../schemas/BlockTemplates.js';
9
+ import type { CustomTemplateDefinition } from '../../schemas/CustomTemplates.js';
10
+ import { makeCustomTemplateFn } from './customTemplate.js';
11
+
12
+ import { titleBlock } from './titleBlock.js';
13
+ import { sectionHeader } from './sectionHeader.js';
14
+ import { statHighlight } from './statHighlight.js';
15
+ import { quoteBlock } from './quoteBlock.js';
16
+ import { factCard } from './factCard.js';
17
+ import { twoColumn } from './twoColumn.js';
18
+ import { dateEvent } from './dateEvent.js';
19
+ import { imageWithCaption } from './imageWithCaption.js';
20
+ import { leftFeature, rightFeature } from './featureBlock.js';
21
+ import { mapBlock } from './mapBlock.js';
22
+ import { fullBleedQuote } from './fullBleedQuote.js';
23
+ import { listBlock } from './listBlock.js';
24
+ import { photoGrid } from './photoGrid.js';
25
+ import { definitionCard } from './definitionCard.js';
26
+ import { comparisonBar } from './comparisonBar.js';
27
+ import { pullQuote } from './pullQuote.js';
28
+ import { videoWithCaption } from './videoWithCaption.js';
29
+ import { videoPullQuote } from './videoPullQuote.js';
30
+ import { dataTable } from './dataTable.js';
31
+ import { diagramBlock } from './diagramBlock.js';
32
+ import { treeBlock } from './treeBlock.js';
33
+ import { timelineBlock } from './timelineBlock.js';
34
+ import { drawingBlock } from './drawingBlock.js';
35
+ import { layoutBlock } from './layoutBlock.js';
36
+
37
+ /** Built-in templates keyed by the ids used in document annotations. */
38
+ export const templateRegistry: TemplateRegistry = {
39
+ title: titleBlock,
40
+ sectionHeader,
41
+ statHighlight,
42
+ quote: quoteBlock,
43
+ factCard,
44
+ twoColumn,
45
+ dateEvent,
46
+ imageWithCaption,
47
+ leftFeature,
48
+ rightFeature,
49
+ map: mapBlock,
50
+ fullBleedQuote,
51
+ list: listBlock,
52
+ photoGrid,
53
+ definitionCard,
54
+ comparisonBar,
55
+ pullQuote,
56
+ videoWithCaption,
57
+ videoPullQuote,
58
+ dataTable,
59
+ diagram: diagramBlock,
60
+ tree: treeBlock,
61
+ timeline: timelineBlock,
62
+ layout: layoutBlock,
63
+ drawing: drawingBlock,
64
+ };
65
+
66
+ /** Broad runtime view used after a template name has selected its function. */
67
+ export type RuntimeTemplateRegistry = Record<
68
+ string,
69
+ (input: TemplateBlock, context: TemplateContext) => Layer[]
70
+ >;
71
+
72
+ /** Merge document-scoped templates without mutating the built-in registry. */
73
+ export function buildRegistry(
74
+ custom?: readonly CustomTemplateDefinition[],
75
+ ): RuntimeTemplateRegistry {
76
+ const merged: RuntimeTemplateRegistry = {
77
+ ...(templateRegistry as unknown as RuntimeTemplateRegistry),
78
+ };
79
+ if (custom) {
80
+ for (const definition of custom) {
81
+ if (merged[definition.name]) continue;
82
+ merged[definition.name] = makeCustomTemplateFn(
83
+ definition,
84
+ ) as unknown as RuntimeTemplateRegistry[string];
85
+ }
86
+ }
87
+ return merged;
88
+ }
@@ -0,0 +1,22 @@
1
+ /** Canonical aliases for historical template ids. Kept dependency-free. */
2
+ export const TEMPLATE_ALIASES: Readonly<Record<string, string>> = Object.freeze({
3
+ titleBlock: 'title',
4
+ quoteBlock: 'quote',
5
+ mapBlock: 'map',
6
+ listBlock: 'list',
7
+ diagramBlock: 'diagram',
8
+ diagramNode: 'diagram',
9
+ });
10
+
11
+ /** Resolve a historical template id to its canonical registry key. */
12
+ export function resolveTemplateName(name: string): string {
13
+ return TEMPLATE_ALIASES[name] ?? name;
14
+ }
15
+
16
+ /** Templates that consume their child headings rather than rendering them. */
17
+ export const CONTAINER_TEMPLATES: ReadonlySet<string> = new Set(['diagram', 'drawing', 'layout']);
18
+
19
+ /** True when `name` (or its alias) consumes child blocks. */
20
+ export function isContainerTemplate(name: string | undefined): boolean {
21
+ return !!name && CONTAINER_TEMPLATES.has(resolveTemplateName(name));
22
+ }