@bendyline/squisq 1.5.2 → 2.0.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 (263) 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-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.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-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
  24. package/dist/chunk-GF3BVNLU.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-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.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-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.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__/dataTable.test.ts +25 -15
  98. package/src/__tests__/diagramBlock.test.ts +3 -3
  99. package/src/__tests__/diagramText.test.ts +26 -0
  100. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  101. package/src/__tests__/drawingBlock.test.ts +3 -3
  102. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  103. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  104. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  105. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  106. package/src/__tests__/getLayers.test.ts +32 -18
  107. package/src/__tests__/getLayersFallback.test.ts +8 -8
  108. package/src/__tests__/imageEditVersions.test.ts +2 -0
  109. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  110. package/src/__tests__/layoutBlock.test.ts +3 -3
  111. package/src/__tests__/majorApiTypes.test.ts +18 -0
  112. package/src/__tests__/markdown.test.ts +41 -0
  113. package/src/__tests__/matchFontFamily.test.ts +114 -0
  114. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  115. package/src/__tests__/materializeTestUtils.ts +17 -0
  116. package/src/__tests__/narrationAlign.test.ts +150 -0
  117. package/src/__tests__/narrationFeatures.test.ts +71 -0
  118. package/src/__tests__/narrationNuclei.test.ts +45 -0
  119. package/src/__tests__/narrationPacing.test.ts +189 -0
  120. package/src/__tests__/narrationScript.test.ts +149 -0
  121. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  122. package/src/__tests__/narrationSidecar.test.ts +88 -0
  123. package/src/__tests__/narrationSyllables.test.ts +65 -0
  124. package/src/__tests__/narrationTestSignals.ts +167 -0
  125. package/src/__tests__/narrationVad.test.ts +66 -0
  126. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  127. package/src/__tests__/rootExports.test.ts +18 -0
  128. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  129. package/src/__tests__/shapeGeometry.test.ts +12 -0
  130. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  131. package/src/__tests__/templateInputs.test.ts +69 -0
  132. package/src/__tests__/templateMetadata.test.ts +2 -1
  133. package/src/__tests__/templates.test.ts +143 -17
  134. package/src/__tests__/themeCompile.test.ts +49 -22
  135. package/src/__tests__/transformProvenance.test.ts +160 -0
  136. package/src/__tests__/transformV2.test.ts +40 -14
  137. package/src/__tests__/treeviewBattle.test.ts +76 -0
  138. package/src/__tests__/treeviewDetect.test.ts +168 -0
  139. package/src/__tests__/treeviewParse.test.ts +118 -0
  140. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  141. package/src/__tests__/treeviewRender.test.ts +91 -0
  142. package/src/__tests__/versions.test.ts +44 -0
  143. package/src/doc/applyNarrationTiming.ts +244 -0
  144. package/src/doc/asciiDiagram/chars.ts +210 -0
  145. package/src/doc/asciiDiagram/detect.ts +175 -0
  146. package/src/doc/asciiDiagram/index.ts +38 -0
  147. package/src/doc/asciiDiagram/mapping.ts +225 -0
  148. package/src/doc/asciiDiagram/parse.ts +1095 -0
  149. package/src/doc/asciiDiagram/render.ts +963 -0
  150. package/src/doc/asciiDiagram/repair.ts +400 -0
  151. package/src/doc/asciiDiagram/types.ts +65 -0
  152. package/src/doc/asciiTimeline/detect.ts +138 -0
  153. package/src/doc/asciiTimeline/index.ts +26 -0
  154. package/src/doc/asciiTimeline/mapping.ts +43 -0
  155. package/src/doc/asciiTimeline/parse.ts +870 -0
  156. package/src/doc/asciiTimeline/render.ts +427 -0
  157. package/src/doc/asciiTimeline/types.ts +73 -0
  158. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  159. package/src/doc/audioMapping.ts +60 -46
  160. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  161. package/src/doc/docToMarkdown.ts +70 -2
  162. package/src/doc/index.ts +76 -4
  163. package/src/doc/markdownToDoc.ts +135 -1
  164. package/src/doc/materializeBlockLayers.ts +304 -0
  165. package/src/doc/mediaAnnotations.ts +20 -0
  166. package/src/doc/resolveDocTheme.ts +8 -7
  167. package/src/doc/templateInputs.ts +185 -2
  168. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  169. package/src/doc/templates/diagramBlock.ts +237 -38
  170. package/src/doc/templates/fallbackBlock.ts +1 -1
  171. package/src/doc/templates/index.ts +134 -279
  172. package/src/doc/templates/inputDescriptors.ts +20 -1
  173. package/src/doc/templates/metadata.ts +10 -0
  174. package/src/doc/templates/persistentLayers.ts +12 -126
  175. package/src/doc/templates/registry.ts +88 -0
  176. package/src/doc/templates/templateNames.ts +22 -0
  177. package/src/doc/templates/timelineBlock.ts +613 -0
  178. package/src/doc/templates/treeBlock.ts +131 -0
  179. package/src/doc/treeview/chars.ts +72 -0
  180. package/src/doc/treeview/detect.ts +145 -0
  181. package/src/doc/treeview/index.ts +30 -0
  182. package/src/doc/treeview/mapping.ts +95 -0
  183. package/src/doc/treeview/parse.ts +201 -0
  184. package/src/doc/treeview/render.ts +41 -0
  185. package/src/doc/treeview/types.ts +43 -0
  186. package/src/doc/utils/applyRenderStyle.ts +2 -9
  187. package/src/doc/utils/diagramText.ts +78 -0
  188. package/src/doc/utils/shapeGeometry.ts +67 -5
  189. package/src/doc/validate.ts +1 -1
  190. package/src/generate/templateMapper.ts +7 -0
  191. package/src/imageEdit/export.ts +6 -5
  192. package/src/imageEdit/versionPaths.ts +13 -26
  193. package/src/imageEdit/versions.ts +21 -100
  194. package/src/index.ts +1 -0
  195. package/src/internal/immutable.ts +58 -0
  196. package/src/jsonForm/index.ts +9 -1
  197. package/src/jsonForm/pointer.ts +68 -23
  198. package/src/markdown/attrTokens.ts +7 -5
  199. package/src/markdown/index.ts +1 -0
  200. package/src/markdown/stringify.ts +4 -9
  201. package/src/markdown/utils.ts +36 -14
  202. package/src/narration/align.ts +514 -0
  203. package/src/narration/features.ts +231 -0
  204. package/src/narration/index.ts +86 -0
  205. package/src/narration/nuclei.ts +118 -0
  206. package/src/narration/pacing.ts +211 -0
  207. package/src/narration/script.ts +239 -0
  208. package/src/narration/session.ts +100 -0
  209. package/src/narration/sidecar.ts +187 -0
  210. package/src/narration/syllables.ts +85 -0
  211. package/src/narration/trace.ts +50 -0
  212. package/src/narration/types.ts +299 -0
  213. package/src/narration/vad.ts +131 -0
  214. package/src/recommend/numberHighlight.ts +25 -0
  215. package/src/recommend/templates.ts +108 -12
  216. package/src/schemas/BlockTemplates.ts +156 -7
  217. package/src/schemas/Doc.ts +72 -9
  218. package/src/schemas/Media.ts +12 -2
  219. package/src/schemas/Theme.ts +45 -25
  220. package/src/schemas/Viewport.ts +2 -2
  221. package/src/schemas/fontStacks.ts +79 -0
  222. package/src/schemas/themeCompile.ts +15 -0
  223. package/src/schemas/themeConstants.ts +2 -0
  224. package/src/schemas/themeLibrary.ts +25 -19
  225. package/src/schemas/themeValidator.ts +1 -1
  226. package/src/spatial/Geohash.ts +0 -3
  227. package/src/storage/ContentContainer.ts +17 -9
  228. package/src/storage/ScopedContentContainer.ts +45 -15
  229. package/src/transform/applyTransform.ts +5 -5
  230. package/src/transform/index.ts +3 -1
  231. package/src/transform/registry.ts +169 -49
  232. package/src/transform/templateSelector.ts +21 -2
  233. package/src/transform/timingAllocator.ts +152 -21
  234. package/src/transform/types.ts +14 -1
  235. package/src/versions/gitignore.ts +34 -0
  236. package/src/versions/index.ts +2 -0
  237. package/src/versions/operations.ts +18 -104
  238. package/src/versions/paths.ts +13 -28
  239. package/src/versions/snapshotStore.ts +202 -0
  240. package/src/versions/types.ts +3 -1
  241. package/dist/chunk-5TYS5V3G.js.map +0 -1
  242. package/dist/chunk-A4O7GIWE.js.map +0 -1
  243. package/dist/chunk-BEQODJWV.js.map +0 -1
  244. package/dist/chunk-DLXZMS5K.js.map +0 -1
  245. package/dist/chunk-GPLTCSXO.js.map +0 -1
  246. package/dist/chunk-HWVFJAAH.js.map +0 -1
  247. package/dist/chunk-IMSCRLLV.js.map +0 -1
  248. package/dist/chunk-JELIRVDP.js +0 -24
  249. package/dist/chunk-JELIRVDP.js.map +0 -1
  250. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  251. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  252. package/dist/chunk-LTN6I7QW.js.map +0 -1
  253. package/dist/chunk-P3K7NLHF.js.map +0 -1
  254. package/dist/chunk-T5UK6YOB.js.map +0 -1
  255. package/dist/chunk-UY7KGQ5R.js +0 -192
  256. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  257. package/dist/chunk-VLJJHDAC.js +0 -218
  258. package/dist/chunk-VLJJHDAC.js.map +0 -1
  259. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  260. package/dist/story/index.d.ts +0 -1801
  261. package/dist/story/index.js +0 -279
  262. package/src/doc/getLayers.ts +0 -195
  263. /package/dist/{story → narration}/index.js.map +0 -0
@@ -1,35 +1,175 @@
1
1
  /**
2
2
  * Diagram block template.
3
3
  *
4
- * Renders the parent block's children as a node-and-edge diagram:
5
- * - Each child becomes a rounded-rect node card with its title as label.
6
- * - Each `child.connectsTo` entry becomes a path edge (cubic-bezier curve
7
- * with an arrowhead at the target end).
8
- * - Children with `x`/`y` set are placed at those coordinates; the rest
9
- * are auto-laid out below in a square-ish grid (see `diagramLayout.ts`).
4
+ * Renders a node-and-edge diagram from one of two data sources:
5
+ * 1. Child headings (`context.children`) the legacy authored form; each
6
+ * child becomes a node positioned by `x`/`y` and connected by
7
+ * `connectsTo` (see `diagramLayout.ts` for auto-placement rules).
8
+ * 2. `input.nodes`/`input.edges` (usually via `templateData`) the
9
+ * data-driven form derived from an ASCII-art diagram fence. Nodes may
10
+ * carry per-node sizes and a `container` reference; containers draw
11
+ * behind their children as translucent grouping cards.
10
12
  *
11
- * Layout coordinates from `x=`/`y=` are author-defined units. The
12
- * template computes a bounding box of all nodes and scales it to fit
13
- * the block's viewport with padding, so users don't have to think about
14
- * absolute pixel ranges.
13
+ * Layout coordinates are author-defined units. The template computes a
14
+ * bounding box of all nodes and scales it to fit the block's viewport
15
+ * with padding, so users don't have to think about absolute pixel ranges.
15
16
  */
16
17
 
17
18
  import type { Layer, ShapeLayer, TextLayer, PathLayer, MarkerStyle } from '../../schemas/Doc.js';
18
- import type { DiagramBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
19
+ import type {
20
+ DiagramBlockInput,
21
+ DiagramEdgeAnchor,
22
+ DiagramTemplateEdge,
23
+ DiagramTemplateNode,
24
+ TemplateContext,
25
+ } from '../../schemas/BlockTemplates.js';
19
26
  import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
20
- import { connectorPath, lineStyleDasharray, snapEndpoints } from '../utils/shapeGeometry.js';
21
- import { computeDiagramLayout, type DiagramNodePosition } from './diagramLayout.js';
27
+ import { DIAGRAM_LABEL_LINE_HEIGHT, fitDiagramLabel } from '../utils/diagramText.js';
28
+ import {
29
+ anchorPoint,
30
+ connectorPath,
31
+ lineStyleDasharray,
32
+ snapEndpoints,
33
+ type ConnectorRouting,
34
+ } from '../utils/shapeGeometry.js';
35
+ import { computeDiagramLayout } from './diagramLayout.js';
22
36
 
23
37
  const NODE_WIDTH = 180;
24
38
  const NODE_HEIGHT = 64;
25
39
  const PADDING = 80;
26
40
 
41
+ interface ResolvedDiagramNode {
42
+ id: string;
43
+ label: string;
44
+ x: number;
45
+ y: number;
46
+ w: number;
47
+ h: number;
48
+ /** Containment depth (0 = top level); containers draw depth-ascending. */
49
+ depth: number;
50
+ isContainer: boolean;
51
+ }
52
+
53
+ interface ResolvedDiagramEdge {
54
+ id: string;
55
+ source: string;
56
+ target: string;
57
+ label?: string;
58
+ directed: boolean;
59
+ sourceAnchor?: DiagramEdgeAnchor;
60
+ targetAnchor?: DiagramEdgeAnchor;
61
+ routing?: ConnectorRouting;
62
+ }
63
+
64
+ interface ResolvedDiagram {
65
+ nodes: ResolvedDiagramNode[];
66
+ edges: ResolvedDiagramEdge[];
67
+ }
68
+
69
+ function nodesFromChildren(context: TemplateContext): ResolvedDiagram {
70
+ const layout = computeDiagramLayout(context.children ?? []);
71
+ return {
72
+ nodes: layout.nodes.map((n) => ({
73
+ id: n.id,
74
+ label: n.label,
75
+ x: n.x,
76
+ y: n.y,
77
+ w: NODE_WIDTH,
78
+ h: NODE_HEIGHT,
79
+ depth: 0,
80
+ isContainer: false,
81
+ })),
82
+ edges: layout.edges.map((e) => ({
83
+ id: e.id,
84
+ source: e.source,
85
+ target: e.target,
86
+ ...(e.type ? { label: e.type } : {}),
87
+ directed: true,
88
+ })),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Defensive resolution of `templateData`-supplied nodes/edges: the data is
94
+ * untyped at runtime (it can come from a hand-written `json data` fence),
95
+ * so malformed entries are dropped rather than crashing the template.
96
+ */
97
+ function nodesFromTemplateData(input: DiagramBlockInput): ResolvedDiagram {
98
+ const rawNodes = Array.isArray(input.nodes) ? input.nodes : [];
99
+ const valid = rawNodes.filter(
100
+ (n): n is DiagramTemplateNode =>
101
+ typeof n === 'object' &&
102
+ n !== null &&
103
+ typeof n.id === 'string' &&
104
+ n.id.length > 0 &&
105
+ typeof n.label === 'string' &&
106
+ typeof n.x === 'number' &&
107
+ Number.isFinite(n.x) &&
108
+ typeof n.y === 'number' &&
109
+ Number.isFinite(n.y),
110
+ );
111
+ const ids = new Set(valid.map((n) => n.id));
112
+ const containerIds = new Set(
113
+ valid.map((n) => n.container).filter((c): c is string => typeof c === 'string' && ids.has(c)),
114
+ );
115
+ const depthOf = (node: DiagramTemplateNode): number => {
116
+ let depth = 0;
117
+ let current: DiagramTemplateNode | undefined = node;
118
+ const seen = new Set<string>();
119
+ while (current?.container && ids.has(current.container) && !seen.has(current.id)) {
120
+ seen.add(current.id);
121
+ depth++;
122
+ current = valid.find((n) => n.id === current?.container);
123
+ }
124
+ return depth;
125
+ };
126
+ const nodes: ResolvedDiagramNode[] = valid.map((n) => ({
127
+ id: n.id,
128
+ label: n.label,
129
+ x: n.x,
130
+ y: n.y,
131
+ w: typeof n.w === 'number' && Number.isFinite(n.w) && n.w > 0 ? n.w : NODE_WIDTH,
132
+ h: typeof n.h === 'number' && Number.isFinite(n.h) && n.h > 0 ? n.h : NODE_HEIGHT,
133
+ depth: depthOf(n),
134
+ isContainer: containerIds.has(n.id),
135
+ }));
136
+
137
+ const rawEdges = Array.isArray(input.edges) ? input.edges : [];
138
+ const edges: ResolvedDiagramEdge[] = rawEdges
139
+ .filter(
140
+ (e): e is DiagramTemplateEdge =>
141
+ typeof e === 'object' &&
142
+ e !== null &&
143
+ typeof e.source === 'string' &&
144
+ typeof e.target === 'string' &&
145
+ ids.has(e.source) &&
146
+ ids.has(e.target),
147
+ )
148
+ .map((e, i) => ({
149
+ id: e.label ? `${e.source}->${e.target}:${e.label}` : `${e.source}->${e.target}-${i}`,
150
+ source: e.source,
151
+ target: e.target,
152
+ ...(e.label ? { label: e.label } : {}),
153
+ directed: e.directed !== false,
154
+ ...(e.sourceAnchor ? { sourceAnchor: e.sourceAnchor } : {}),
155
+ ...(e.targetAnchor ? { targetAnchor: e.targetAnchor } : {}),
156
+ ...(e.routing ? { routing: e.routing } : {}),
157
+ }));
158
+ return { nodes, edges };
159
+ }
160
+
27
161
  export function diagramBlock(input: DiagramBlockInput, context: TemplateContext): Layer[] {
28
162
  const { theme, viewport, children = [] } = context;
29
163
  const colors = resolveColorScheme(context, input.colorScheme ?? 'blue');
30
164
 
31
- const layout = computeDiagramLayout(children);
32
- if (layout.nodes.length === 0) {
165
+ const resolved =
166
+ children.length > 0
167
+ ? nodesFromChildren(context)
168
+ : input.nodes && input.nodes.length > 0
169
+ ? nodesFromTemplateData(input)
170
+ : { nodes: [], edges: [] };
171
+
172
+ if (resolved.nodes.length === 0) {
33
173
  // Empty diagram — render a single hint label so the block has visible content.
34
174
  return [
35
175
  {
@@ -50,10 +190,10 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
50
190
  }
51
191
 
52
192
  // Compute the bounding box of all nodes in author coordinates.
53
- const minX = Math.min(...layout.nodes.map((n) => n.x));
54
- const maxX = Math.max(...layout.nodes.map((n) => n.x + NODE_WIDTH));
55
- const minY = Math.min(...layout.nodes.map((n) => n.y));
56
- const maxY = Math.max(...layout.nodes.map((n) => n.y + NODE_HEIGHT));
193
+ const minX = Math.min(...resolved.nodes.map((n) => n.x));
194
+ const maxX = Math.max(...resolved.nodes.map((n) => n.x + n.w));
195
+ const minY = Math.min(...resolved.nodes.map((n) => n.y));
196
+ const maxY = Math.max(...resolved.nodes.map((n) => n.y + n.h));
57
197
 
58
198
  // Available area in viewport pixels (after reserving space for the title, if any).
59
199
  const titleHeight = input.title ? 120 : 0;
@@ -79,11 +219,11 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
79
219
  const groupTop = Math.max(PADDING / 2, (viewport.height - titleHeight - scaledH) / 2);
80
220
  const offsetX = PADDING + (availW - scaledW) / 2;
81
221
  const offsetY = groupTop + titleHeight;
82
- const transform = (n: DiagramNodePosition): { x: number; y: number; w: number; h: number } => ({
222
+ const transform = (n: ResolvedDiagramNode): { x: number; y: number; w: number; h: number } => ({
83
223
  x: offsetX + (n.x - minX) * scale,
84
224
  y: offsetY + (n.y - minY) * scale,
85
- w: NODE_WIDTH * scale,
86
- h: NODE_HEIGHT * scale,
225
+ w: n.w * scale,
226
+ h: n.h * scale,
87
227
  });
88
228
 
89
229
  const layers: Layer[] = [];
@@ -107,29 +247,57 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
107
247
  });
108
248
  }
109
249
 
110
- // Position lookup by id for edge routing.
250
+ const containers = resolved.nodes
251
+ .filter((n) => n.isContainer)
252
+ .sort((a, b) => a.depth - b.depth || a.y - b.y || a.x - b.x);
253
+ const leaves = resolved.nodes.filter((n) => !n.isContainer);
254
+
255
+ // Container cards first — translucent grouping surfaces behind everything.
256
+ for (const node of containers) {
257
+ const t = transform(node);
258
+ const card: ShapeLayer = {
259
+ type: 'shape',
260
+ id: `node-card-${node.id}`,
261
+ content: {
262
+ shape: 'rect',
263
+ fill: colors.bg ?? theme.colors.backgroundLight,
264
+ fillOpacity: 0.25,
265
+ stroke: colors.text ?? theme.colors.primary,
266
+ strokeWidth: Math.max(1, strokeW - 1),
267
+ borderRadius: 10,
268
+ },
269
+ position: { x: t.x, y: t.y, width: t.w, height: t.h },
270
+ };
271
+ layers.push(card);
272
+ }
273
+
274
+ // Position lookup by id for edge routing (containers included — edges may
275
+ // attach to them, e.g. a through-border split from a parsed fence).
111
276
  const positions = new Map<string, { cx: number; cy: number; rx: number; ry: number }>();
112
- for (const node of layout.nodes) {
277
+ for (const node of resolved.nodes) {
113
278
  const t = transform(node);
114
279
  positions.set(node.id, { cx: t.x + t.w / 2, cy: t.y + t.h / 2, rx: t.w / 2, ry: t.h / 2 });
115
280
  }
116
281
 
117
- // Edge styling (applies to all edges; per-edge styling lives on the nodes).
282
+ // Edge styling (applies to all edges; direction comes per-edge).
118
283
  const startMarker: MarkerStyle = input.startStyle ?? 'none';
119
- const endMarker: MarkerStyle = input.endStyle ?? 'arrow';
284
+ const configuredEnd: MarkerStyle = input.endStyle ?? 'arrow';
120
285
  const edgeDash = lineStyleDasharray(input.lineStyle);
121
286
 
122
- // Edges first so they sit behind nodes.
123
- for (const edge of layout.edges) {
287
+ // Edges above containers, behind leaf nodes.
288
+ for (const edge of resolved.edges) {
124
289
  const a = positions.get(edge.source);
125
290
  const b = positions.get(edge.target);
126
291
  if (!a || !b) continue;
127
- const { start, end } = snapEndpoints(a, b);
292
+ const endMarker: MarkerStyle = edge.directed ? configuredEnd : 'none';
293
+ const snapped = snapEndpoints(a, b);
294
+ const start = edge.sourceAnchor ? anchorPoint(a, edge.sourceAnchor) : snapped.start;
295
+ const end = edge.targetAnchor ? anchorPoint(b, edge.targetAnchor) : snapped.end;
128
296
  const pathLayer: PathLayer = {
129
297
  type: 'path',
130
298
  id: `edge-${edge.id}`,
131
299
  content: {
132
- d: connectorPath(input.edgeStyle ?? 'curved', start, end),
300
+ d: connectorPath(edge.routing ?? input.edgeStyle ?? 'curved', start, end),
133
301
  stroke: colors.text ?? theme.colors.primary,
134
302
  strokeWidth: strokeW,
135
303
  fill: 'none',
@@ -141,9 +309,9 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
141
309
  };
142
310
  layers.push(pathLayer);
143
311
 
144
- if (edge.type) {
145
- // Connection-type label, floated just above the edge midpoint so the
146
- // line never strikes through the text.
312
+ if (edge.label) {
313
+ // Edge label, floated just above the edge midpoint so the line never
314
+ // strikes through the text.
147
315
  const mx = (a.cx + b.cx) / 2;
148
316
  const my = (a.cy + b.cy) / 2;
149
317
  const labelFontSize = themedFontSize(Math.round(18 * fontAdj), context, false);
@@ -151,7 +319,7 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
151
319
  type: 'text',
152
320
  id: `edge-label-${edge.id}`,
153
321
  content: {
154
- text: edge.type,
322
+ text: edge.label,
155
323
  style: {
156
324
  fontSize: labelFontSize,
157
325
  fontFamily: getThemeFont(context, 'body'),
@@ -165,8 +333,8 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
165
333
  }
166
334
  }
167
335
 
168
- // Nodes (card + label) on top of edges.
169
- for (const node of layout.nodes) {
336
+ // Leaf nodes (card + label) on top of edges.
337
+ for (const node of leaves) {
170
338
  const t = transform(node);
171
339
  const card: ShapeLayer = {
172
340
  type: 'shape',
@@ -182,20 +350,51 @@ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext)
182
350
  };
183
351
  layers.push(card);
184
352
 
353
+ const preferredFontSize = themedFontSize(Math.round(22 * fontAdj), context, false);
354
+ const fit = fitDiagramLabel(node.label, t.w, t.h, preferredFontSize);
185
355
  const label: TextLayer = {
186
356
  type: 'text',
187
357
  id: `node-label-${node.id}`,
188
358
  content: {
189
359
  text: node.label,
190
360
  style: {
191
- fontSize: themedFontSize(Math.round(22 * fontAdj), context, false),
361
+ fontSize: fit.fontSize,
192
362
  fontFamily: getThemeFont(context, 'body'),
193
363
  fontWeight: 'bold',
194
364
  color: colors.text ?? theme.colors.text,
195
365
  textAlign: 'center',
366
+ lineHeight: DIAGRAM_LABEL_LINE_HEIGHT,
367
+ },
368
+ },
369
+ position: {
370
+ x: t.x + t.w / 2,
371
+ y: t.y + t.h / 2 + fit.firstLineOffset,
372
+ anchor: 'center',
373
+ width: fit.textWidth,
374
+ },
375
+ };
376
+ layers.push(label);
377
+ }
378
+
379
+ // Container titles last — anchored near the container's top edge, above
380
+ // any child content that could otherwise overprint them.
381
+ for (const node of containers) {
382
+ const t = transform(node);
383
+ const fontSize = themedFontSize(Math.round(20 * fontAdj), context, false);
384
+ const label: TextLayer = {
385
+ type: 'text',
386
+ id: `node-label-${node.id}`,
387
+ content: {
388
+ text: node.label.split('\n')[0] ?? '',
389
+ style: {
390
+ fontSize,
391
+ fontFamily: getThemeFont(context, 'body'),
392
+ fontWeight: 'bold',
393
+ color: theme.colors.textMuted,
394
+ textAlign: 'center',
196
395
  },
197
396
  },
198
- position: { x: t.x + t.w / 2, y: t.y + t.h / 2, anchor: 'center', width: t.w },
397
+ position: { x: t.x + t.w / 2, y: t.y + fontSize, anchor: 'center', width: t.w },
199
398
  };
200
399
  layers.push(label);
201
400
  }
@@ -8,7 +8,7 @@
8
8
  * agents reviewing screenshots) can see *why* the block degraded.
9
9
  *
10
10
  * This is intentionally not a registered template: it can't be requested by
11
- * name, only reached when a requested template fails. See `getLayers()`.
11
+ * name, only reached when a requested template fails. See `materializeBlockLayers()`.
12
12
  */
13
13
 
14
14
  import type { Layer } from '../../schemas/Doc.js';