@bendyline/squisq 1.4.1 → 1.5.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 (216) hide show
  1. package/dist/{Doc-BeSeH2iX.d.ts → Doc-BsMkmzH8.d.ts} +838 -268
  2. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-DvoeHMP9.d.ts} +11 -4
  3. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  4. package/dist/chunk-2OIBZYKP.js.map +1 -0
  5. package/dist/chunk-2UQOHYE2.js +45 -0
  6. package/dist/chunk-2UQOHYE2.js.map +1 -0
  7. package/dist/{chunk-B4EEON3N.js → chunk-4AKUQPEE.js} +536 -103
  8. package/dist/chunk-4AKUQPEE.js.map +1 -0
  9. package/dist/chunk-4ZFRZBU2.js +6582 -0
  10. package/dist/chunk-4ZFRZBU2.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-7MQGNR76.js} +79 -3
  12. package/dist/chunk-7MQGNR76.js.map +1 -0
  13. package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
  14. package/dist/chunk-DLXZMS5K.js.map +1 -0
  15. package/dist/{chunk-MYIH7FWD.js → chunk-FBKP5CAO.js} +1 -1
  16. package/dist/chunk-FBKP5CAO.js.map +1 -0
  17. package/dist/chunk-KZNXZ5SM.js +333 -0
  18. package/dist/chunk-KZNXZ5SM.js.map +1 -0
  19. package/dist/{chunk-WMOI7FN3.js → chunk-LRCV7N7F.js} +12 -2
  20. package/dist/{chunk-WMOI7FN3.js.map → chunk-LRCV7N7F.js.map} +1 -1
  21. package/dist/{chunk-3K5OG6KI.js → chunk-OH43RPTP.js} +403 -187
  22. package/dist/chunk-OH43RPTP.js.map +1 -0
  23. package/dist/{chunk-H3AXU4MJ.js → chunk-OKJOQXK4.js} +149 -2
  24. package/dist/chunk-OKJOQXK4.js.map +1 -0
  25. package/dist/chunk-PGGGKYWG.js +1 -0
  26. package/dist/chunk-PGGGKYWG.js.map +1 -0
  27. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  28. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  29. package/dist/chunk-QE7TWGVT.js +178 -0
  30. package/dist/chunk-QE7TWGVT.js.map +1 -0
  31. package/dist/chunk-RUDYOTA6.js +345 -0
  32. package/dist/chunk-RUDYOTA6.js.map +1 -0
  33. package/dist/chunk-TVSGLUID.js +596 -0
  34. package/dist/chunk-TVSGLUID.js.map +1 -0
  35. package/dist/chunk-ZLQKNOZQ.js +1 -0
  36. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  37. package/dist/doc/index.d.ts +4 -4
  38. package/dist/doc/index.js +124 -6
  39. package/dist/generate/index.d.ts +9 -2
  40. package/dist/generate/index.js +1 -1
  41. package/dist/icons/index.d.ts +3 -26
  42. package/dist/icons/index.js +13 -0
  43. package/dist/icons/inlineIconMarker.d.ts +1 -0
  44. package/dist/icons/inlineIconMarker.js +15 -0
  45. package/dist/icons/inlineIconMarker.js.map +1 -0
  46. package/dist/imageEdit/index.d.ts +11 -5
  47. package/dist/imageEdit/index.js +4 -1
  48. package/dist/index.d.ts +11 -8
  49. package/dist/index.js +243 -24
  50. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  51. package/dist/markdown/index.d.ts +212 -3
  52. package/dist/markdown/index.js +40 -7
  53. package/dist/recommend/index.d.ts +1 -1
  54. package/dist/recommend/index.js +3 -2
  55. package/dist/schemas/index.d.ts +28 -8
  56. package/dist/schemas/index.js +35 -5
  57. package/dist/spatial/index.d.ts +9 -4
  58. package/dist/spatial/index.js +1 -1
  59. package/dist/story/index.d.ts +906 -40
  60. package/dist/story/index.js +124 -6
  61. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-DQQKuRMx.d.ts} +1 -1
  62. package/dist/transform/index.d.ts +48 -10
  63. package/dist/transform/index.js +17 -481
  64. package/dist/transform/index.js.map +1 -1
  65. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  66. package/package.json +6 -1
  67. package/src/__tests__/animationUtils.test.ts +40 -0
  68. package/src/__tests__/annotationCoercion.test.ts +243 -0
  69. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  70. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  71. package/src/__tests__/attrTokens.test.ts +162 -0
  72. package/src/__tests__/customTemplateSample.test.ts +83 -0
  73. package/src/__tests__/customTemplatesFrontmatter.test.ts +205 -0
  74. package/src/__tests__/customThemesFrontmatter.test.ts +118 -0
  75. package/src/__tests__/diagramBlock.test.ts +122 -0
  76. package/src/__tests__/diagramLayout.test.ts +134 -0
  77. package/src/__tests__/drawing.test.ts +109 -0
  78. package/src/__tests__/drawingBlock.test.ts +124 -0
  79. package/src/__tests__/drawingLayout.test.ts +171 -0
  80. package/src/__tests__/geohash.test.ts +6 -5
  81. package/src/__tests__/getLayers.test.ts +6 -2
  82. package/src/__tests__/getLayersFallback.test.ts +103 -0
  83. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  84. package/src/__tests__/imageEditVersions.test.ts +72 -0
  85. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  86. package/src/__tests__/layoutBlock.test.ts +83 -0
  87. package/src/__tests__/layoutLayout.test.ts +169 -0
  88. package/src/__tests__/markdown.test.ts +23 -0
  89. package/src/__tests__/markdownSanitize.test.ts +64 -0
  90. package/src/__tests__/markdownToDoc.test.ts +223 -2
  91. package/src/__tests__/mediaAnnotations.test.ts +64 -0
  92. package/src/__tests__/mediaSchedule.test.ts +118 -0
  93. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  94. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  95. package/src/__tests__/rootExports.test.ts +43 -0
  96. package/src/__tests__/shapeGeometry.test.ts +89 -0
  97. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  98. package/src/__tests__/structuredData.test.ts +179 -0
  99. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  100. package/src/__tests__/templateMetadata.test.ts +21 -0
  101. package/src/__tests__/templates.test.ts +145 -4
  102. package/src/__tests__/themeCompile.test.ts +42 -0
  103. package/src/__tests__/themeValidator.test.ts +6 -0
  104. package/src/__tests__/transformV2.test.ts +137 -0
  105. package/src/__tests__/transitionNormalize.test.ts +61 -0
  106. package/src/__tests__/validateDoc.test.ts +100 -0
  107. package/src/doc/customTemplatesFrontmatter.ts +308 -0
  108. package/src/doc/customThemesFrontmatter.ts +90 -0
  109. package/src/doc/docToMarkdown.ts +73 -4
  110. package/src/doc/getLayers.ts +40 -17
  111. package/src/doc/index.ts +35 -1
  112. package/src/doc/markdownToDoc.ts +333 -12
  113. package/src/doc/mediaAnnotations.ts +118 -0
  114. package/src/doc/resolveDocTheme.ts +55 -0
  115. package/src/doc/structuredData.ts +230 -0
  116. package/src/doc/templateInputs.ts +240 -0
  117. package/src/doc/templates/__tests__/customTemplate.test.ts +151 -0
  118. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  119. package/src/doc/templates/accentImage.ts +30 -6
  120. package/src/doc/templates/captionUtils.ts +23 -0
  121. package/src/doc/templates/comparisonBar.ts +26 -18
  122. package/src/doc/templates/coverBlock.ts +7 -4
  123. package/src/doc/templates/customTemplate.ts +45 -0
  124. package/src/doc/templates/dataTable.ts +37 -19
  125. package/src/doc/templates/dateEvent.ts +41 -20
  126. package/src/doc/templates/definitionCard.ts +26 -18
  127. package/src/doc/templates/diagramBlock.ts +204 -0
  128. package/src/doc/templates/diagramLayout.ts +159 -0
  129. package/src/doc/templates/drawingBlock.ts +325 -0
  130. package/src/doc/templates/drawingLayout.ts +539 -0
  131. package/src/doc/templates/factCard.ts +51 -25
  132. package/src/doc/templates/fallbackBlock.ts +123 -0
  133. package/src/doc/templates/featureBlock.ts +20 -13
  134. package/src/doc/templates/fullBleedQuote.ts +18 -8
  135. package/src/doc/templates/imageWithCaption.ts +22 -9
  136. package/src/doc/templates/index.ts +215 -46
  137. package/src/doc/templates/layoutBlock.ts +43 -0
  138. package/src/doc/templates/layoutLayout.ts +243 -0
  139. package/src/doc/templates/listBlock.ts +56 -30
  140. package/src/doc/templates/mapBlock.ts +15 -12
  141. package/src/doc/templates/metadata.ts +120 -0
  142. package/src/doc/templates/persistentLayers.ts +129 -11
  143. package/src/doc/templates/photoGrid.ts +23 -12
  144. package/src/doc/templates/pullQuote.ts +30 -12
  145. package/src/doc/templates/quoteBlock.ts +54 -29
  146. package/src/doc/templates/sectionHeader.ts +25 -11
  147. package/src/doc/templates/statHighlight.ts +39 -22
  148. package/src/doc/templates/titleBlock.ts +59 -21
  149. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +221 -0
  150. package/src/doc/templates/tokens/resolveTokens.ts +0 -0
  151. package/src/doc/templates/twoColumn.ts +48 -30
  152. package/src/doc/templates/videoPullQuote.ts +22 -12
  153. package/src/doc/templates/videoWithCaption.ts +13 -8
  154. package/src/doc/utils/animationUtils.ts +267 -3
  155. package/src/doc/utils/applyRenderStyle.ts +113 -0
  156. package/src/doc/utils/imageTreatment.ts +70 -0
  157. package/src/doc/utils/shapeGeometry.ts +464 -0
  158. package/src/doc/utils/themeUtils.ts +75 -1
  159. package/src/doc/validate.ts +476 -0
  160. package/src/generate/slideshowGenerator.ts +7 -0
  161. package/src/icons/index.ts +8 -0
  162. package/src/icons/inlineIconMarker.ts +73 -0
  163. package/src/imageEdit/export.ts +85 -2
  164. package/src/imageEdit/index.ts +1 -0
  165. package/src/imageEdit/persistence.ts +51 -0
  166. package/src/index.ts +2 -0
  167. package/src/markdown/annotationCoercion.ts +295 -0
  168. package/src/markdown/attrTokens.ts +222 -0
  169. package/src/markdown/convert.ts +229 -26
  170. package/src/markdown/index.ts +36 -0
  171. package/src/markdown/sanitize.ts +298 -0
  172. package/src/markdown/stringify.ts +84 -3
  173. package/src/markdown/types.ts +66 -0
  174. package/src/markdown/utils.ts +49 -1
  175. package/src/recommend/templates.ts +24 -0
  176. package/src/schemas/BlockTemplates.ts +133 -4
  177. package/src/schemas/CustomTemplates.ts +74 -0
  178. package/src/schemas/Doc.ts +353 -29
  179. package/src/schemas/ImageEditDoc.ts +11 -4
  180. package/src/schemas/Media.ts +169 -0
  181. package/src/schemas/Theme.ts +23 -1
  182. package/src/schemas/Transitions.ts +224 -0
  183. package/src/schemas/colorUtils.ts +33 -0
  184. package/src/schemas/fontStacks.ts +5 -2
  185. package/src/schemas/index.ts +3 -0
  186. package/src/schemas/themeCompile.ts +23 -4
  187. package/src/schemas/themeValidator.ts +49 -11
  188. package/src/schemas/themes/cinematic.json +69 -12
  189. package/src/schemas/themes/documentary.json +58 -11
  190. package/src/schemas/themes/gezellig.json +60 -11
  191. package/src/schemas/themes/magazine.json +54 -9
  192. package/src/schemas/themes/morning-light.json +52 -9
  193. package/src/schemas/themes/standard-dark.json +48 -13
  194. package/src/schemas/themes/standard.json +44 -12
  195. package/src/schemas/themes/tech-dark.json +65 -12
  196. package/src/spatial/Geohash.ts +52 -21
  197. package/src/transform/applyTransform.ts +43 -4
  198. package/src/transform/index.ts +1 -0
  199. package/src/transform/registry.ts +44 -13
  200. package/src/transform/styles/dataDriven.ts +2 -1
  201. package/src/transform/styles/documentary.ts +2 -1
  202. package/src/transform/styles/magazine.ts +3 -1
  203. package/src/transform/styles/minimal.ts +2 -1
  204. package/src/transform/styles/narrative.ts +3 -1
  205. package/src/transform/templateSelector.ts +87 -11
  206. package/src/transform/types.ts +35 -2
  207. package/dist/chunk-3K5OG6KI.js.map +0 -1
  208. package/dist/chunk-57CVQCFX.js.map +0 -1
  209. package/dist/chunk-5WFRKURX.js.map +0 -1
  210. package/dist/chunk-7UDSRZKG.js.map +0 -1
  211. package/dist/chunk-B4EEON3N.js.map +0 -1
  212. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  213. package/dist/chunk-KSWJR66U.js +0 -3706
  214. package/dist/chunk-KSWJR66U.js.map +0 -1
  215. package/dist/chunk-MYIH7FWD.js.map +0 -1
  216. package/dist/chunk-ZTX4PFFT.js.map +0 -1
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Drawing block template.
3
+ *
4
+ * Renders the parent block's children as free-form shapes on a canvas:
5
+ * - Each child becomes a shape (`rect`/`circle` → ShapeLayer, `line`/
6
+ * `arrow`/`path` → PathLayer, `text` → TextLayer) with its title as a
7
+ * centered label and its body text as an optional sublabel.
8
+ * - `line`/`arrow` children with `from`/`to` (and any child's
9
+ * `connectsTo`) become connectors clipped to the shapes they join.
10
+ *
11
+ * Coordinates from `x`/`y`/`width`/`height` are author-defined units; the
12
+ * template fits the bounding box of all shapes to the viewport with
13
+ * padding (same approach as `diagramBlock`), so authors don't think in
14
+ * absolute pixels.
15
+ */
16
+
17
+ import type { Layer, ShapeLayer, TextLayer, PathLayer } from '../../schemas/Doc.js';
18
+ import type { DrawingBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
19
+ import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
20
+ import { computeDrawingLayout, type DrawingShape, type DrawingConnector } from './drawingLayout.js';
21
+ import { shapePath, connectorPath, clipEndpoints } from '../utils/shapeGeometry.js';
22
+
23
+ const PADDING = 80;
24
+
25
+ export function drawingBlock(input: DrawingBlockInput, context: TemplateContext): Layer[] {
26
+ const { theme, viewport, children = [] } = context;
27
+ const colors = resolveColorScheme(context, input.colorScheme);
28
+ const defaultStroke = input.stroke ?? colors.text ?? theme.colors.text;
29
+ const defaultFill = input.fill ?? 'none';
30
+
31
+ const layout = computeDrawingLayout(children);
32
+
33
+ // No shapes authored as children → empty drawing hint.
34
+ if (layout.shapes.length === 0 && layout.connectors.length === 0) {
35
+ return [emptyHint(input, context)];
36
+ }
37
+
38
+ // Bounding box of all shapes in author coordinates.
39
+ const minX = Math.min(...layout.shapes.map((s) => s.x));
40
+ const maxX = Math.max(...layout.shapes.map((s) => s.x + s.width));
41
+ const minY = Math.min(...layout.shapes.map((s) => s.y));
42
+ const maxY = Math.max(...layout.shapes.map((s) => s.y + s.height));
43
+
44
+ const titleHeight = input.title ? 120 : 0;
45
+ const availW = Math.max(1, viewport.width - PADDING * 2);
46
+ const availH = Math.max(1, viewport.height - PADDING * 2 - titleHeight);
47
+ const contentW = Math.max(1, maxX - minX);
48
+ const contentH = Math.max(1, maxY - minY);
49
+
50
+ // Uniform scale that fits both dimensions. Small drawings may grow
51
+ // (shapes are vector) up to 1.8× so a three-shape sketch doesn't render
52
+ // as a tiny off-center cluster; the title and the scaled drawing are
53
+ // centered together as one group.
54
+ const scale = Math.min(availW / contentW, availH / contentH, 1.8);
55
+ const scaledW = contentW * scale;
56
+ const scaledH = contentH * scale;
57
+ const groupTop = Math.max(PADDING / 2, (viewport.height - titleHeight - scaledH) / 2);
58
+ const offsetX = PADDING + (availW - scaledW) / 2;
59
+ const offsetY = groupTop + titleHeight;
60
+
61
+ const tx = (x: number) => offsetX + (x - minX) * scale;
62
+ const ty = (y: number) => offsetY + (y - minY) * scale;
63
+ const fontAdj = Math.min(Math.max(scale, 1), 1.5);
64
+
65
+ const layers: Layer[] = [];
66
+
67
+ if (input.title) {
68
+ layers.push({
69
+ type: 'text',
70
+ id: 'drawing-title',
71
+ content: {
72
+ text: input.title,
73
+ style: {
74
+ fontSize: themedFontSize(40, context, true),
75
+ fontFamily: getThemeFont(context, 'title'),
76
+ fontWeight: 'bold',
77
+ color: theme.colors.text,
78
+ textAlign: 'center',
79
+ },
80
+ },
81
+ position: { x: '50%', y: groupTop + titleHeight / 2 - 16, anchor: 'center' },
82
+ });
83
+ }
84
+
85
+ // Center lookup so connectors can clip to the shapes they join.
86
+ const boxes = new Map<string, { cx: number; cy: number; rx: number; ry: number }>();
87
+ for (const s of layout.shapes) {
88
+ const x = tx(s.x);
89
+ const y = ty(s.y);
90
+ const w = s.width * scale;
91
+ const h = s.height * scale;
92
+ boxes.set(s.id, {
93
+ cx: x + w / 2,
94
+ cy: y + h / 2,
95
+ rx: Math.max(1, w / 2),
96
+ ry: Math.max(1, h / 2),
97
+ });
98
+ }
99
+
100
+ // Connectors first so they sit behind shapes.
101
+ for (const conn of layout.connectors) {
102
+ const a = boxes.get(conn.from);
103
+ const b = boxes.get(conn.to);
104
+ if (!a || !b) continue;
105
+ const { start, end } = clipEndpoints(a, b);
106
+ const path: PathLayer = {
107
+ type: 'path',
108
+ id: `connector-${conn.id}`,
109
+ content: {
110
+ d: connectorPath(conn.routing, start, end),
111
+ stroke: conn.stroke ?? defaultStroke,
112
+ strokeWidth: conn.strokeWidth ?? Math.round(2 * fontAdj),
113
+ fill: 'none',
114
+ ...(conn.dasharray ? { dasharray: conn.dasharray } : {}),
115
+ ...(conn.startMarker !== 'none' ? { startMarker: conn.startMarker } : {}),
116
+ ...(conn.endMarker !== 'none' ? { endMarker: conn.endMarker } : {}),
117
+ },
118
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
119
+ };
120
+ layers.push(path);
121
+ if (conn.label) layers.push(connectorLabel(conn, a, b, context, fontAdj));
122
+ }
123
+
124
+ // Shapes (and their labels) on top.
125
+ for (const s of layout.shapes) {
126
+ const x = tx(s.x);
127
+ const y = ty(s.y);
128
+ const w = s.width * scale;
129
+ const h = s.height * scale;
130
+ layers.push(...shapeLayers(s, x, y, w, h, context, defaultStroke, defaultFill, fontAdj));
131
+ }
132
+
133
+ return layers;
134
+ }
135
+
136
+ // ============================================
137
+ // Per-shape layer emission
138
+ // ============================================
139
+
140
+ function shapeLayers(
141
+ s: DrawingShape,
142
+ x: number,
143
+ y: number,
144
+ w: number,
145
+ h: number,
146
+ context: TemplateContext,
147
+ defaultStroke: string,
148
+ defaultFill: string,
149
+ fontAdj: number,
150
+ ): Layer[] {
151
+ const out: Layer[] = [];
152
+ const stroke = s.stroke ?? defaultStroke;
153
+ const fill = s.fill ?? defaultFill;
154
+
155
+ if (s.kind === 'text') {
156
+ out.push(
157
+ textLayer(
158
+ `shape-${s.id}`,
159
+ s.text ?? s.label ?? '',
160
+ x + w / 2,
161
+ y + h / 2,
162
+ w,
163
+ context,
164
+ s.stroke ?? context.theme.colors.text,
165
+ true,
166
+ fontAdj,
167
+ ),
168
+ );
169
+ return out;
170
+ }
171
+
172
+ if (s.kind === 'rect' || s.kind === 'circle') {
173
+ const shape: ShapeLayer = {
174
+ type: 'shape',
175
+ id: `shape-${s.id}`,
176
+ content: {
177
+ shape: s.kind,
178
+ fill,
179
+ stroke,
180
+ strokeWidth: s.strokeWidth ?? Math.round(2 * fontAdj),
181
+ ...(s.kind === 'rect' && s.borderRadius != null ? { borderRadius: s.borderRadius } : {}),
182
+ },
183
+ position: { x, y, width: w, height: h },
184
+ };
185
+ out.push(shape);
186
+ } else {
187
+ // line / arrow / path / polygon shapes → PathLayer.
188
+ const diagonal = `M ${round(x)} ${round(y)} L ${round(x + w)} ${round(y + h)}`;
189
+ let d = diagonal;
190
+ let pathFill = 'none';
191
+ let endMarker: 'none' | 'arrow' = 'none';
192
+ if (s.kind === 'path') {
193
+ d = s.d ?? diagonal;
194
+ pathFill = fill;
195
+ } else if (s.kind === 'arrow') {
196
+ endMarker = 'arrow';
197
+ } else if (s.kind !== 'line') {
198
+ // Named polygon / curved shape → computed filled path.
199
+ d = shapePath(s.kind, x, y, w, h) ?? diagonal;
200
+ pathFill = fill;
201
+ }
202
+ const path: PathLayer = {
203
+ type: 'path',
204
+ id: `shape-${s.id}`,
205
+ content: {
206
+ d,
207
+ stroke,
208
+ strokeWidth: s.strokeWidth ?? Math.round(2 * fontAdj),
209
+ fill: pathFill,
210
+ ...(s.dasharray ? { dasharray: s.dasharray } : {}),
211
+ ...(endMarker !== 'none' ? { endMarker } : {}),
212
+ },
213
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
214
+ };
215
+ out.push(path);
216
+ }
217
+
218
+ // Label (heading text) centered on the shape; sublabel (body) below it.
219
+ if (s.label) {
220
+ out.push(
221
+ textLayer(
222
+ `shape-label-${s.id}`,
223
+ s.label,
224
+ x + w / 2,
225
+ y + h / 2,
226
+ w,
227
+ context,
228
+ s.stroke ?? context.theme.colors.text,
229
+ true,
230
+ fontAdj,
231
+ ),
232
+ );
233
+ }
234
+ if (s.sublabel) {
235
+ out.push(
236
+ textLayer(
237
+ `shape-sublabel-${s.id}`,
238
+ s.sublabel,
239
+ x + w / 2,
240
+ y + h + 14 * fontAdj,
241
+ w,
242
+ context,
243
+ context.theme.colors.textMuted,
244
+ false,
245
+ fontAdj,
246
+ ),
247
+ );
248
+ }
249
+ return out;
250
+ }
251
+
252
+ function textLayer(
253
+ id: string,
254
+ text: string,
255
+ cx: number,
256
+ cy: number,
257
+ width: number,
258
+ context: TemplateContext,
259
+ color: string,
260
+ bold: boolean,
261
+ fontAdj: number,
262
+ ): TextLayer {
263
+ return {
264
+ type: 'text',
265
+ id,
266
+ content: {
267
+ text,
268
+ style: {
269
+ fontSize: themedFontSize(Math.round((bold ? 22 : 18) * fontAdj), context, false),
270
+ fontFamily: getThemeFont(context, 'body'),
271
+ ...(bold ? { fontWeight: 'bold' as const } : {}),
272
+ color,
273
+ textAlign: 'center',
274
+ },
275
+ },
276
+ position: { x: cx, y: cy, anchor: 'center', width },
277
+ };
278
+ }
279
+
280
+ function connectorLabel(
281
+ conn: DrawingConnector,
282
+ a: { cx: number; cy: number },
283
+ b: { cx: number; cy: number },
284
+ context: TemplateContext,
285
+ fontAdj: number,
286
+ ): TextLayer {
287
+ const fontSize = themedFontSize(Math.round(18 * fontAdj), context, false);
288
+ return {
289
+ type: 'text',
290
+ id: `connector-label-${conn.id}`,
291
+ content: {
292
+ text: conn.label ?? '',
293
+ style: {
294
+ fontSize,
295
+ fontFamily: getThemeFont(context, 'body'),
296
+ color: context.theme.colors.textMuted,
297
+ textAlign: 'center',
298
+ },
299
+ },
300
+ // Floated just above the connector midpoint so the line never
301
+ // strikes through the label text.
302
+ position: { x: (a.cx + b.cx) / 2, y: (a.cy + b.cy) / 2 - fontSize * 0.9, anchor: 'center' },
303
+ };
304
+ }
305
+
306
+ function emptyHint(input: DrawingBlockInput, context: TemplateContext): TextLayer {
307
+ return {
308
+ type: 'text',
309
+ id: 'drawing-empty',
310
+ content: {
311
+ text: input.title ?? 'Empty drawing',
312
+ style: {
313
+ fontSize: themedFontSize(36, context, true),
314
+ fontFamily: getThemeFont(context, 'title'),
315
+ color: context.theme.colors.textMuted,
316
+ textAlign: 'center',
317
+ },
318
+ },
319
+ position: { x: '50%', y: '50%', anchor: 'center' },
320
+ };
321
+ }
322
+
323
+ function round(n: number): number {
324
+ return Math.round(n * 100) / 100;
325
+ }