@bendyline/squisq 1.4.1 → 1.5.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 (254) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
  4. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
  5. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  6. package/dist/chunk-2OIBZYKP.js.map +1 -0
  7. package/dist/chunk-2UQOHYE2.js +45 -0
  8. package/dist/chunk-2UQOHYE2.js.map +1 -0
  9. package/dist/chunk-6ATE2PSM.js +344 -0
  10. package/dist/chunk-6ATE2PSM.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
  12. package/dist/chunk-A4O7GIWE.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-ET53IIEP.js +1 -0
  16. package/dist/chunk-ET53IIEP.js.map +1 -0
  17. package/dist/chunk-HWVFJAAH.js +362 -0
  18. package/dist/chunk-HWVFJAAH.js.map +1 -0
  19. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  20. package/dist/chunk-IMSCRLLV.js.map +1 -0
  21. package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
  22. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  23. package/dist/chunk-LDQ2HJIX.js +596 -0
  24. package/dist/chunk-LDQ2HJIX.js.map +1 -0
  25. package/dist/chunk-LH7I6SH7.js +7208 -0
  26. package/dist/chunk-LH7I6SH7.js.map +1 -0
  27. package/dist/chunk-PGGGKYWG.js +1 -0
  28. package/dist/chunk-PGGGKYWG.js.map +1 -0
  29. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  30. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  31. package/dist/chunk-QE7TWGVT.js +178 -0
  32. package/dist/chunk-QE7TWGVT.js.map +1 -0
  33. package/dist/chunk-RUDYOTA6.js +345 -0
  34. package/dist/chunk-RUDYOTA6.js.map +1 -0
  35. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  36. package/dist/chunk-T5UK6YOB.js.map +1 -0
  37. package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
  38. package/dist/chunk-TEEEILMP.js.map +1 -0
  39. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  40. package/dist/chunk-TQWLI6S2.js.map +1 -0
  41. package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
  42. package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
  43. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  44. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  45. package/dist/chunk-ZLQKNOZQ.js +1 -0
  46. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  47. package/dist/doc/index.d.ts +5 -5
  48. package/dist/doc/index.js +135 -8
  49. package/dist/generate/index.d.ts +3 -74
  50. package/dist/generate/index.js +2 -7
  51. package/dist/icons/index.d.ts +3 -26
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/inlineIconMarker.d.ts +1 -0
  54. package/dist/icons/inlineIconMarker.js +15 -0
  55. package/dist/icons/inlineIconMarker.js.map +1 -0
  56. package/dist/imageEdit/index.d.ts +12 -6
  57. package/dist/imageEdit/index.js +4 -1
  58. package/dist/index.d.ts +14 -11
  59. package/dist/index.js +265 -37
  60. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  61. package/dist/jsonForm/index.d.ts +42 -1
  62. package/dist/jsonForm/index.js +9 -1
  63. package/dist/markdown/index.d.ts +228 -3
  64. package/dist/markdown/index.js +42 -7
  65. package/dist/recommend/index.d.ts +1 -1
  66. package/dist/recommend/index.js +3 -2
  67. package/dist/schemas/index.d.ts +28 -8
  68. package/dist/schemas/index.js +36 -8
  69. package/dist/spatial/index.d.ts +9 -4
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.d.ts +2 -2
  72. package/dist/storage/index.js +1 -1
  73. package/dist/story/index.d.ts +1021 -41
  74. package/dist/story/index.js +135 -8
  75. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  76. package/dist/timing/index.js +3 -5
  77. package/dist/transform/index.d.ts +48 -10
  78. package/dist/transform/index.js +17 -481
  79. package/dist/transform/index.js.map +1 -1
  80. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  81. package/dist/versions/index.d.ts +1 -1
  82. package/package.json +6 -1
  83. package/src/__tests__/animationUtils.test.ts +40 -0
  84. package/src/__tests__/annotationCoercion.test.ts +243 -0
  85. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  86. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  87. package/src/__tests__/attrTokens.test.ts +162 -0
  88. package/src/__tests__/contentExtractor.test.ts +60 -0
  89. package/src/__tests__/coverBlock.test.ts +46 -1
  90. package/src/__tests__/customTemplateSample.test.ts +144 -0
  91. package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
  92. package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
  93. package/src/__tests__/diagramBlock.test.ts +122 -0
  94. package/src/__tests__/diagramLayout.test.ts +134 -0
  95. package/src/__tests__/drawing.test.ts +109 -0
  96. package/src/__tests__/drawingBlock.test.ts +124 -0
  97. package/src/__tests__/drawingLayout.test.ts +171 -0
  98. package/src/__tests__/geohash.test.ts +6 -5
  99. package/src/__tests__/getLayers.test.ts +6 -2
  100. package/src/__tests__/getLayersFallback.test.ts +103 -0
  101. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  102. package/src/__tests__/imageEditVersions.test.ts +72 -0
  103. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  104. package/src/__tests__/inputDescriptors.test.ts +162 -0
  105. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  106. package/src/__tests__/layoutBlock.test.ts +83 -0
  107. package/src/__tests__/layoutLayout.test.ts +169 -0
  108. package/src/__tests__/markdown.test.ts +87 -0
  109. package/src/__tests__/markdownSanitize.test.ts +79 -0
  110. package/src/__tests__/markdownToDoc.test.ts +391 -2
  111. package/src/__tests__/mediaAnnotations.test.ts +91 -0
  112. package/src/__tests__/mediaSchedule.test.ts +118 -0
  113. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  114. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  115. package/src/__tests__/rootExports.test.ts +43 -0
  116. package/src/__tests__/shapeGeometry.test.ts +89 -0
  117. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  118. package/src/__tests__/structuredData.test.ts +249 -0
  119. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  120. package/src/__tests__/templateMetadata.test.ts +31 -0
  121. package/src/__tests__/templates.test.ts +257 -4
  122. package/src/__tests__/themeCompile.test.ts +42 -0
  123. package/src/__tests__/themeValidator.test.ts +6 -0
  124. package/src/__tests__/transformV2.test.ts +137 -0
  125. package/src/__tests__/transitionNormalize.test.ts +61 -0
  126. package/src/__tests__/validateDoc.test.ts +260 -0
  127. package/src/doc/annotationBlocks.ts +117 -0
  128. package/src/doc/customTemplatesFrontmatter.ts +314 -0
  129. package/src/doc/customThemesFrontmatter.ts +95 -0
  130. package/src/doc/docToMarkdown.ts +104 -5
  131. package/src/doc/getLayers.ts +49 -17
  132. package/src/doc/index.ts +47 -1
  133. package/src/doc/markdownToDoc.ts +411 -13
  134. package/src/doc/mediaAnnotations.ts +100 -0
  135. package/src/doc/resolveDocTheme.ts +55 -0
  136. package/src/doc/standaloneAnnotation.ts +58 -0
  137. package/src/doc/structuredData.ts +301 -0
  138. package/src/doc/templateInputs.ts +240 -0
  139. package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
  140. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  141. package/src/doc/templates/accentImage.ts +30 -6
  142. package/src/doc/templates/captionUtils.ts +23 -0
  143. package/src/doc/templates/comparisonBar.ts +33 -18
  144. package/src/doc/templates/coverBlock.ts +35 -5
  145. package/src/doc/templates/customTemplate.ts +51 -0
  146. package/src/doc/templates/dataTable.ts +37 -19
  147. package/src/doc/templates/dateEvent.ts +41 -20
  148. package/src/doc/templates/definitionCard.ts +26 -18
  149. package/src/doc/templates/diagramBlock.ts +204 -0
  150. package/src/doc/templates/diagramLayout.ts +159 -0
  151. package/src/doc/templates/drawingBlock.ts +325 -0
  152. package/src/doc/templates/drawingLayout.ts +539 -0
  153. package/src/doc/templates/factCard.ts +54 -26
  154. package/src/doc/templates/fallbackBlock.ts +123 -0
  155. package/src/doc/templates/featureBlock.ts +20 -13
  156. package/src/doc/templates/fullBleedQuote.ts +18 -8
  157. package/src/doc/templates/imageWithCaption.ts +30 -15
  158. package/src/doc/templates/index.ts +225 -46
  159. package/src/doc/templates/inputDescriptors.ts +432 -0
  160. package/src/doc/templates/layoutBlock.ts +43 -0
  161. package/src/doc/templates/layoutLayout.ts +243 -0
  162. package/src/doc/templates/listBlock.ts +56 -30
  163. package/src/doc/templates/mapBlock.ts +15 -12
  164. package/src/doc/templates/metadata.ts +120 -0
  165. package/src/doc/templates/persistentLayers.ts +129 -11
  166. package/src/doc/templates/photoGrid.ts +22 -34
  167. package/src/doc/templates/pullQuote.ts +30 -12
  168. package/src/doc/templates/quoteBlock.ts +54 -29
  169. package/src/doc/templates/sectionHeader.ts +25 -11
  170. package/src/doc/templates/statHighlight.ts +39 -22
  171. package/src/doc/templates/titleBlock.ts +59 -21
  172. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
  173. package/src/doc/templates/tokens/resolveTokens.ts +453 -0
  174. package/src/doc/templates/twoColumn.ts +48 -30
  175. package/src/doc/templates/videoPullQuote.ts +22 -12
  176. package/src/doc/templates/videoWithCaption.ts +21 -15
  177. package/src/doc/utils/animationUtils.ts +267 -3
  178. package/src/doc/utils/applyRenderStyle.ts +113 -0
  179. package/src/doc/utils/imageTreatment.ts +70 -0
  180. package/src/doc/utils/nearest.ts +59 -0
  181. package/src/doc/utils/shapeGeometry.ts +464 -0
  182. package/src/doc/utils/themeUtils.ts +75 -1
  183. package/src/doc/validate.ts +575 -0
  184. package/src/generate/contentExtractor.ts +148 -35
  185. package/src/generate/index.ts +0 -3
  186. package/src/icons/index.ts +8 -0
  187. package/src/icons/inlineIconMarker.ts +73 -0
  188. package/src/imageEdit/export.ts +85 -2
  189. package/src/imageEdit/index.ts +1 -0
  190. package/src/imageEdit/persistence.ts +51 -0
  191. package/src/index.ts +2 -0
  192. package/src/jsonForm/index.ts +2 -0
  193. package/src/jsonForm/tokens.ts +78 -0
  194. package/src/markdown/annotationCoercion.ts +295 -0
  195. package/src/markdown/attrTokens.ts +241 -0
  196. package/src/markdown/convert.ts +229 -26
  197. package/src/markdown/index.ts +37 -0
  198. package/src/markdown/sanitize.ts +318 -0
  199. package/src/markdown/stringify.ts +96 -6
  200. package/src/markdown/types.ts +66 -0
  201. package/src/markdown/utils.ts +111 -2
  202. package/src/recommend/templates.ts +24 -0
  203. package/src/schemas/BlockTemplates.ts +133 -4
  204. package/src/schemas/CustomTemplates.ts +126 -0
  205. package/src/schemas/Doc.ts +387 -65
  206. package/src/schemas/ImageEditDoc.ts +11 -4
  207. package/src/schemas/Media.ts +169 -0
  208. package/src/schemas/Theme.ts +23 -1
  209. package/src/schemas/Transitions.ts +224 -0
  210. package/src/schemas/colorUtils.ts +33 -0
  211. package/src/schemas/fontStacks.ts +5 -2
  212. package/src/schemas/index.ts +3 -0
  213. package/src/schemas/themeCompile.ts +24 -5
  214. package/src/schemas/themeValidator.ts +49 -11
  215. package/src/schemas/themes/cinematic.json +69 -12
  216. package/src/schemas/themes/documentary.json +58 -11
  217. package/src/schemas/themes/gezellig.json +60 -11
  218. package/src/schemas/themes/magazine.json +54 -9
  219. package/src/schemas/themes/morning-light.json +52 -9
  220. package/src/schemas/themes/standard-dark.json +48 -13
  221. package/src/schemas/themes/standard.json +44 -12
  222. package/src/schemas/themes/tech-dark.json +65 -12
  223. package/src/spatial/Geohash.ts +52 -21
  224. package/src/storage/ContentContainer.ts +6 -2
  225. package/src/transform/applyTransform.ts +43 -4
  226. package/src/transform/index.ts +1 -0
  227. package/src/transform/registry.ts +44 -13
  228. package/src/transform/styles/dataDriven.ts +2 -1
  229. package/src/transform/styles/documentary.ts +2 -1
  230. package/src/transform/styles/magazine.ts +3 -1
  231. package/src/transform/styles/minimal.ts +2 -1
  232. package/src/transform/styles/narrative.ts +3 -1
  233. package/src/transform/templateSelector.ts +87 -11
  234. package/src/transform/types.ts +35 -2
  235. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  236. package/dist/chunk-3E5F2XMR.js.map +0 -1
  237. package/dist/chunk-3K5OG6KI.js.map +0 -1
  238. package/dist/chunk-4X3JQXNM.js +0 -35
  239. package/dist/chunk-4X3JQXNM.js.map +0 -1
  240. package/dist/chunk-57CVQCFX.js.map +0 -1
  241. package/dist/chunk-5WFRKURX.js.map +0 -1
  242. package/dist/chunk-7UDSRZKG.js.map +0 -1
  243. package/dist/chunk-B4EEON3N.js.map +0 -1
  244. package/dist/chunk-FR2RBTKO.js.map +0 -1
  245. package/dist/chunk-H3AXU4MJ.js +0 -179
  246. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  247. package/dist/chunk-K32VJONL.js.map +0 -1
  248. package/dist/chunk-KSWJR66U.js +0 -3706
  249. package/dist/chunk-KSWJR66U.js.map +0 -1
  250. package/dist/chunk-MYIH7FWD.js +0 -145
  251. package/dist/chunk-MYIH7FWD.js.map +0 -1
  252. package/dist/chunk-ZTX4PFFT.js.map +0 -1
  253. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  254. package/src/generate/slideshowGenerator.ts +0 -273
@@ -11,24 +11,35 @@
11
11
 
12
12
  import type { Layer } from '../../schemas/Doc.js';
13
13
  import type { DataTableInput, TemplateContext } from '../../schemas/BlockTemplates.js';
14
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
15
- import { getThemeFont, resolveColorScheme } from '../utils/themeUtils.js';
14
+ import {
15
+ getThemeFont,
16
+ resolveColorScheme,
17
+ shouldUseShadow,
18
+ themedFontSize,
19
+ themedSurfaceGradient,
20
+ } from '../utils/themeUtils.js';
21
+ import { pickContrastingText, withAlpha } from '../../schemas/colorUtils.js';
16
22
  import { createBackgroundLayer } from './captionUtils.js';
17
23
 
18
24
  export function dataTable(input: DataTableInput, context: TemplateContext): Layer[] {
19
25
  const { title, headers, rows, align, colorScheme } = input;
20
- const { theme } = context;
26
+ const { theme, viewport } = context;
21
27
 
22
28
  const colors = resolveColorScheme(context, colorScheme);
23
- const titleFontSize = scaledFontSize(48, context, true);
24
- const tableFontSize = scaledFontSize(28, context, false);
29
+ const titleFontSize = themedFontSize(48, context, true);
30
+ const tableFontSize = themedFontSize(28, context, false);
25
31
 
26
- const layers: Layer[] = [
27
- createBackgroundLayer(
28
- 'bg',
29
- `linear-gradient(170deg, ${theme.colors.background} 0%, #0f1520 100%)`,
30
- ),
31
- ];
32
+ const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 170))];
33
+
34
+ // Size the table band from its natural content height (header + rows at
35
+ // ~2. line height for cell padding) and center the title+table as one
36
+ // group — a full-height band left a ~200px orphan gap under the title.
37
+ // `rows` is required by the schema but may be missing on partially-
38
+ // authored blocks in live preview; treat it as empty rather than throwing.
39
+ const rowCount = (Array.isArray(rows) ? rows.length : 0) + 1;
40
+ const naturalTableHPct = Math.min(74, ((rowCount * tableFontSize * 2.4) / viewport.height) * 100);
41
+ const titleBandPct = title ? (titleFontSize * 2.2 * 100) / viewport.height : 0;
42
+ const groupTopPct = Math.max(8, (100 - titleBandPct - naturalTableHPct) / 2);
32
43
 
33
44
  // Optional title above the table
34
45
  if (title) {
@@ -43,15 +54,22 @@ export function dataTable(input: DataTableInput, context: TemplateContext): Laye
43
54
  fontWeight: 'bold',
44
55
  color: theme.colors.text,
45
56
  textAlign: 'center',
46
- shadow: true,
57
+ shadow: shouldUseShadow(context),
47
58
  },
48
59
  },
49
- position: { x: '50%', y: '10%', width: '80%', anchor: 'center' },
60
+ position: {
61
+ x: '50%',
62
+ y: `${groupTopPct + titleBandPct / 2}%`,
63
+ width: '80%',
64
+ anchor: 'center',
65
+ },
50
66
  animation: { type: 'fadeIn', duration: 0.8 },
51
67
  });
52
68
  }
53
69
 
54
- // Table layer
70
+ // Table layer. Header text color is picked for contrast against the
71
+ // header fill — pairing two mid-tones from the same scheme made the
72
+ // header row read as a smudge in most themes.
55
73
  layers.push({
56
74
  type: 'table',
57
75
  id: 'table',
@@ -61,10 +79,10 @@ export function dataTable(input: DataTableInput, context: TemplateContext): Laye
61
79
  align,
62
80
  style: {
63
81
  headerBackground: colors.accent,
64
- headerColor: colors.text,
65
- cellBackground: 'rgba(255,255,255,0.05)',
82
+ headerColor: pickContrastingText(colors.accent),
83
+ cellBackground: withAlpha(theme.colors.text, 0.04),
66
84
  cellColor: theme.colors.text,
67
- borderColor: 'rgba(255,255,255,0.12)',
85
+ borderColor: withAlpha(theme.colors.text, 0.15),
68
86
  fontSize: tableFontSize,
69
87
  fontFamily: getThemeFont(context, 'body'),
70
88
  headerFontFamily: getThemeFont(context, 'title'),
@@ -73,9 +91,9 @@ export function dataTable(input: DataTableInput, context: TemplateContext): Laye
73
91
  },
74
92
  position: {
75
93
  x: '10%',
76
- y: title ? '18%' : '8%',
94
+ y: `${groupTopPct + titleBandPct}%`,
77
95
  width: '80%',
78
- height: title ? '74%' : '84%',
96
+ height: `${naturalTableHPct}%`,
79
97
  },
80
98
  animation: { type: 'fadeIn', duration: 1, delay: title ? 0.4 : 0 },
81
99
  });
@@ -12,37 +12,52 @@
12
12
 
13
13
  import type { Layer } from '../../schemas/Doc.js';
14
14
  import type { DateEventInput, TemplateContext } from '../../schemas/BlockTemplates.js';
15
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
16
- import { getThemeFont } from '../utils/themeUtils.js';
15
+ import {
16
+ getThemeFont,
17
+ shouldUseShadow,
18
+ themedFontSize,
19
+ themedSurfaceGradient,
20
+ themedImageTreatment,
21
+ } from '../utils/themeUtils.js';
22
+ import { oklchDarken, relativeLuminance } from '../../schemas/colorUtils.js';
17
23
  import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
18
24
 
19
- const MOOD_COLORS = {
20
- neutral: { bg: '#1a202c', date: '#63b3ed', text: '#ffffff' },
21
- somber: { bg: '#1a1a2e', date: '#e53e3e', text: '#a0aec0' },
22
- celebratory: { bg: '#1a365d', date: '#68d391', text: '#ffffff' },
25
+ /**
26
+ * Mood accents tint the hero date, not the surface — the surface always
27
+ * comes from the theme. (Hard-coded dark mood panels used to turn light
28
+ * and warm themes into a cold navy slide.)
29
+ */
30
+ const MOOD_ACCENTS = {
31
+ neutral: '#63b3ed',
32
+ somber: '#e53e3e',
33
+ celebratory: '#68d391',
23
34
  };
24
35
 
25
36
  export function dateEvent(input: DateEventInput, context: TemplateContext): Layer[] {
26
37
  const { date, description, footer, mood = 'neutral', accentImage } = input;
27
38
  const { theme } = context;
28
- const colors = MOOD_COLORS[mood];
39
+ // Deepen the mood accent on light surfaces so the hero date keeps contrast.
40
+ const moodAccent = MOOD_ACCENTS[mood];
41
+ const dateColor =
42
+ relativeLuminance(theme.colors.background) > 0.5 ? oklchDarken(moodAccent, 0.25) : moodAccent;
29
43
 
30
44
  // Get layout adjustments if accent image is present
31
45
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
32
46
 
33
47
  // Scale font sizes — date is the hero element
34
- const dateFontSize = scaledFontSize(96, context, true);
35
- const descFontSize = scaledFontSize(30, context, false);
36
- const footerFontSize = scaledFontSize(26, context, false);
48
+ const dateFontSize = themedFontSize(96, context, true);
49
+ const descFontSize = themedFontSize(30, context, false);
50
+ const footerFontSize = themedFontSize(26, context, false);
37
51
 
38
52
  const layers: Layer[] = [
39
- // Background — unique diagonal gradient to differentiate from other dark templates
53
+ // Background — theme surface gradient (diagonal to differentiate
54
+ // from other text templates)
40
55
  {
41
56
  type: 'shape',
42
57
  id: 'bg',
43
58
  content: {
44
59
  shape: 'rect',
45
- fill: `linear-gradient(135deg, ${colors.bg} 0%, #0d1117 100%)`,
60
+ fill: themedSurfaceGradient(context, 135),
46
61
  },
47
62
  position: { x: 0, y: 0, width: '100%', height: '100%' },
48
63
  },
@@ -50,7 +65,13 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
50
65
 
51
66
  // Add accent image layers (behind text, after background)
52
67
  if (accentImage) {
53
- layers.push(...createAccentLayers(accentImage, input.id));
68
+ layers.push(
69
+ ...createAccentLayers(
70
+ accentImage,
71
+ input.id,
72
+ themedImageTreatment(context, input.imageTreatment),
73
+ ),
74
+ );
54
75
  }
55
76
 
56
77
  // Date — hero element, much larger
@@ -63,8 +84,8 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
63
84
  fontSize: dateFontSize,
64
85
  fontFamily: getThemeFont(context, 'title'),
65
86
  fontWeight: 'bold',
66
- color: colors.date,
67
- shadow: !!accentImage,
87
+ color: dateColor,
88
+ shadow: shouldUseShadow(context),
68
89
  },
69
90
  },
70
91
  position: {
@@ -84,10 +105,10 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
84
105
  style: {
85
106
  fontSize: descFontSize,
86
107
  fontFamily: getThemeFont(context, 'body'),
87
- color: colors.text,
108
+ color: theme.colors.text,
88
109
  textAlign: 'center',
89
- lineHeight: 1.8,
90
- shadow: !!accentImage,
110
+ lineHeight: 1.6,
111
+ shadow: shouldUseShadow(context),
91
112
  },
92
113
  },
93
114
  position: {
@@ -110,12 +131,12 @@ export function dateEvent(input: DateEventInput, context: TemplateContext): Laye
110
131
  fontSize: footerFontSize,
111
132
  fontFamily: getThemeFont(context, 'body'),
112
133
  color: theme.colors.textMuted,
113
- shadow: !!accentImage,
134
+ shadow: shouldUseShadow(context),
114
135
  },
115
136
  },
116
137
  position: {
117
138
  x: accentLayout.textCenterX,
118
- y: adjustY('82%', accentLayout),
139
+ y: adjustY('72%', accentLayout),
119
140
  anchor: 'center',
120
141
  },
121
142
  animation: { type: 'fadeIn', duration: 1, delay: 3 },
@@ -10,8 +10,15 @@
10
10
 
11
11
  import type { Layer } from '../../schemas/Doc.js';
12
12
  import type { DefinitionCardInput, TemplateContext } from '../../schemas/BlockTemplates.js';
13
- import { scaledFontSize } from '../../schemas/BlockTemplates.js';
14
- import { resolveColorScheme, getThemeFont } from '../utils/themeUtils.js';
13
+ import {
14
+ resolveColorScheme,
15
+ getThemeFont,
16
+ shouldUseShadow,
17
+ themedEntrance,
18
+ themedFontSize,
19
+ themedSurfaceGradient,
20
+ themedImageTreatment,
21
+ } from '../utils/themeUtils.js';
15
22
  import { createAccentLayers, getAccentLayout, adjustY, DEFAULT_LAYOUT } from './accentImage.js';
16
23
  import { createBackgroundLayer } from './captionUtils.js';
17
24
 
@@ -23,20 +30,21 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
23
30
  // Get layout adjustments if accent image is present
24
31
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
25
32
 
26
- const termFontSize = scaledFontSize(72, context, true);
27
- const defFontSize = scaledFontSize(32, context, false);
28
- const originFontSize = scaledFontSize(22, context, false);
33
+ const termFontSize = themedFontSize(72, context, true);
34
+ const defFontSize = themedFontSize(32, context, false);
35
+ const originFontSize = themedFontSize(22, context, false);
29
36
 
30
- const layers: Layer[] = [
31
- createBackgroundLayer(
32
- 'bg',
33
- `linear-gradient(145deg, #1e2030 0%, ${theme.colors.background} 100%)`,
34
- ),
35
- ];
37
+ const layers: Layer[] = [createBackgroundLayer('bg', themedSurfaceGradient(context, 145))];
36
38
 
37
39
  // Add accent image layers
38
40
  if (accentImage) {
39
- layers.push(...createAccentLayers(accentImage, input.id));
41
+ layers.push(
42
+ ...createAccentLayers(
43
+ accentImage,
44
+ input.id,
45
+ themedImageTreatment(context, input.imageTreatment),
46
+ ),
47
+ );
40
48
  }
41
49
 
42
50
  // Term — large, accent-colored
@@ -51,7 +59,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
51
59
  fontWeight: 'bold',
52
60
  color: colors.text,
53
61
  textAlign: 'center',
54
- shadow: !!accentImage,
62
+ shadow: shouldUseShadow(context),
55
63
  },
56
64
  },
57
65
  position: {
@@ -59,7 +67,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
59
67
  y: adjustY('30%', accentLayout),
60
68
  anchor: 'center',
61
69
  },
62
- animation: { type: 'fadeIn', duration: 1.5 },
70
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1.5 }),
63
71
  });
64
72
 
65
73
  // Horizontal separator line
@@ -92,7 +100,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
92
100
  textAlign: 'center',
93
101
  lineHeight: 1.6,
94
102
  maxLines: 4,
95
- shadow: !!accentImage,
103
+ shadow: shouldUseShadow(context),
96
104
  },
97
105
  },
98
106
  position: {
@@ -101,7 +109,7 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
101
109
  width: accentLayout.textWidth,
102
110
  anchor: 'center',
103
111
  },
104
- animation: { type: 'fadeIn', duration: 1, delay: 0.8 },
112
+ animation: themedEntrance(context, 'text', { type: 'fadeIn', duration: 1, delay: 0.8 }),
105
113
  });
106
114
 
107
115
  // Origin if provided
@@ -116,12 +124,12 @@ export function definitionCard(input: DefinitionCardInput, context: TemplateCont
116
124
  fontFamily: getThemeFont(context, 'body'),
117
125
  color: theme.colors.textMuted,
118
126
  textAlign: 'center',
119
- shadow: !!accentImage,
127
+ shadow: shouldUseShadow(context),
120
128
  },
121
129
  },
122
130
  position: {
123
131
  x: accentLayout.textCenterX,
124
- y: adjustY('78%', accentLayout),
132
+ y: adjustY('70%', accentLayout),
125
133
  anchor: 'center',
126
134
  },
127
135
  animation: { type: 'fadeIn', duration: 0.8, delay: 1.5 },
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Diagram block template.
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`).
10
+ *
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.
15
+ */
16
+
17
+ import type { Layer, ShapeLayer, TextLayer, PathLayer, MarkerStyle } from '../../schemas/Doc.js';
18
+ import type { DiagramBlockInput, TemplateContext } from '../../schemas/BlockTemplates.js';
19
+ import { resolveColorScheme, getThemeFont, themedFontSize } from '../utils/themeUtils.js';
20
+ import { clipEndpoints, connectorPath, lineStyleDasharray } from '../utils/shapeGeometry.js';
21
+ import { computeDiagramLayout, type DiagramNodePosition } from './diagramLayout.js';
22
+
23
+ const NODE_WIDTH = 180;
24
+ const NODE_HEIGHT = 64;
25
+ const PADDING = 80;
26
+
27
+ export function diagramBlock(input: DiagramBlockInput, context: TemplateContext): Layer[] {
28
+ const { theme, viewport, children = [] } = context;
29
+ const colors = resolveColorScheme(context, input.colorScheme ?? 'blue');
30
+
31
+ const layout = computeDiagramLayout(children);
32
+ if (layout.nodes.length === 0) {
33
+ // Empty diagram — render a single hint label so the block has visible content.
34
+ return [
35
+ {
36
+ type: 'text',
37
+ id: 'diagram-empty',
38
+ content: {
39
+ text: input.title ?? 'Empty diagram',
40
+ style: {
41
+ fontSize: themedFontSize(36, context, true),
42
+ fontFamily: getThemeFont(context, 'title'),
43
+ color: theme.colors.textMuted,
44
+ textAlign: 'center',
45
+ },
46
+ },
47
+ position: { x: '50%', y: '50%', anchor: 'center' },
48
+ },
49
+ ];
50
+ }
51
+
52
+ // 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));
57
+
58
+ // Available area in viewport pixels (after reserving space for the title, if any).
59
+ const titleHeight = input.title ? 120 : 0;
60
+ const availW = Math.max(1, viewport.width - PADDING * 2);
61
+ const availH = Math.max(1, viewport.height - PADDING * 2 - titleHeight);
62
+ const contentW = Math.max(1, maxX - minX);
63
+ const contentH = Math.max(1, maxY - minY);
64
+
65
+ // Uniform scale so the diagram fits both dimensions. Small diagrams are
66
+ // allowed to grow (everything here is vector) up to 1.8× so a three-node
67
+ // flow doesn't render as a tiny cluster in an empty canvas; label fonts
68
+ // and strokes scale with the nodes, clamped so text never balloons.
69
+ const scale = Math.min(availW / contentW, availH / contentH, 1.8);
70
+ const fontAdj = Math.min(Math.max(scale, 1), 1.5);
71
+ const strokeW = Math.round(2 * fontAdj);
72
+
73
+ // Compute viewport-pixel coordinates of each node's top-left corner.
74
+ // The title and the scaled diagram are centered together as one group —
75
+ // a top-pinned title over a center-floated diagram split the block into
76
+ // two stranded pieces.
77
+ const scaledW = contentW * scale;
78
+ const scaledH = contentH * scale;
79
+ const groupTop = Math.max(PADDING / 2, (viewport.height - titleHeight - scaledH) / 2);
80
+ const offsetX = PADDING + (availW - scaledW) / 2;
81
+ const offsetY = groupTop + titleHeight;
82
+ const transform = (n: DiagramNodePosition): { x: number; y: number; w: number; h: number } => ({
83
+ x: offsetX + (n.x - minX) * scale,
84
+ y: offsetY + (n.y - minY) * scale,
85
+ w: NODE_WIDTH * scale,
86
+ h: NODE_HEIGHT * scale,
87
+ });
88
+
89
+ const layers: Layer[] = [];
90
+
91
+ // Optional title above the diagram.
92
+ if (input.title) {
93
+ layers.push({
94
+ type: 'text',
95
+ id: 'diagram-title',
96
+ content: {
97
+ text: input.title,
98
+ style: {
99
+ fontSize: themedFontSize(40, context, true),
100
+ fontFamily: getThemeFont(context, 'title'),
101
+ fontWeight: 'bold',
102
+ color: theme.colors.text,
103
+ textAlign: 'center',
104
+ },
105
+ },
106
+ position: { x: '50%', y: groupTop + titleHeight / 2 - 16, anchor: 'center' },
107
+ });
108
+ }
109
+
110
+ // Position lookup by id for edge routing.
111
+ const positions = new Map<string, { cx: number; cy: number; rx: number; ry: number }>();
112
+ for (const node of layout.nodes) {
113
+ const t = transform(node);
114
+ 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
+ }
116
+
117
+ // Edge styling (applies to all edges; per-edge styling lives on the nodes).
118
+ const startMarker: MarkerStyle = input.startStyle ?? 'none';
119
+ const endMarker: MarkerStyle = input.endStyle ?? 'arrow';
120
+ const edgeDash = lineStyleDasharray(input.lineStyle);
121
+
122
+ // Edges first so they sit behind nodes.
123
+ for (const edge of layout.edges) {
124
+ const a = positions.get(edge.source);
125
+ const b = positions.get(edge.target);
126
+ if (!a || !b) continue;
127
+ const { start, end } = clipEndpoints(a, b);
128
+ const pathLayer: PathLayer = {
129
+ type: 'path',
130
+ id: `edge-${edge.id}`,
131
+ content: {
132
+ d: connectorPath(input.edgeStyle ?? 'curved', start, end),
133
+ stroke: colors.text ?? theme.colors.primary,
134
+ strokeWidth: strokeW,
135
+ fill: 'none',
136
+ ...(edgeDash ? { dasharray: edgeDash } : {}),
137
+ ...(startMarker !== 'none' ? { startMarker } : {}),
138
+ ...(endMarker !== 'none' ? { endMarker } : {}),
139
+ },
140
+ position: { x: 0, y: 0, width: '100%', height: '100%' },
141
+ };
142
+ layers.push(pathLayer);
143
+
144
+ if (edge.type) {
145
+ // Connection-type label, floated just above the edge midpoint so the
146
+ // line never strikes through the text.
147
+ const mx = (a.cx + b.cx) / 2;
148
+ const my = (a.cy + b.cy) / 2;
149
+ const labelFontSize = themedFontSize(Math.round(18 * fontAdj), context, false);
150
+ const labelLayer: TextLayer = {
151
+ type: 'text',
152
+ id: `edge-label-${edge.id}`,
153
+ content: {
154
+ text: edge.type,
155
+ style: {
156
+ fontSize: labelFontSize,
157
+ fontFamily: getThemeFont(context, 'body'),
158
+ color: theme.colors.textMuted,
159
+ textAlign: 'center',
160
+ },
161
+ },
162
+ position: { x: mx, y: my - labelFontSize * 0.9, anchor: 'center' },
163
+ };
164
+ layers.push(labelLayer);
165
+ }
166
+ }
167
+
168
+ // Nodes (card + label) on top of edges.
169
+ for (const node of layout.nodes) {
170
+ const t = transform(node);
171
+ const card: ShapeLayer = {
172
+ type: 'shape',
173
+ id: `node-card-${node.id}`,
174
+ content: {
175
+ shape: 'rect',
176
+ fill: colors.bg ?? theme.colors.backgroundLight,
177
+ stroke: colors.text ?? theme.colors.primary,
178
+ strokeWidth: strokeW,
179
+ borderRadius: input.nodeShape === 'pill' ? t.h / 2 : 10,
180
+ },
181
+ position: { x: t.x, y: t.y, width: t.w, height: t.h },
182
+ };
183
+ layers.push(card);
184
+
185
+ const label: TextLayer = {
186
+ type: 'text',
187
+ id: `node-label-${node.id}`,
188
+ content: {
189
+ text: node.label,
190
+ style: {
191
+ fontSize: themedFontSize(Math.round(22 * fontAdj), context, false),
192
+ fontFamily: getThemeFont(context, 'body'),
193
+ fontWeight: 'bold',
194
+ color: colors.text ?? theme.colors.text,
195
+ textAlign: 'center',
196
+ },
197
+ },
198
+ position: { x: t.x + t.w / 2, y: t.y + t.h / 2, anchor: 'center', width: t.w },
199
+ };
200
+ layers.push(label);
201
+ }
202
+
203
+ return layers;
204
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Diagram layout — shared between the read-only SVG render path
3
+ * (`diagramBlock` template) and the interactive editor (React Flow in
4
+ * `@bendyline/squisq-editor-react`). Same children + same options always
5
+ * produce the same node positions and edge list, so previews and the
6
+ * editor stay visually consistent.
7
+ *
8
+ * Layout rules:
9
+ * 1. Children with both `x` and `y` set keep their authored coordinates.
10
+ * 2. Children missing either coordinate are auto-placed in a square-ish
11
+ * grid in declaration order, offset below the bounding box of the
12
+ * explicitly-positioned children.
13
+ * 3. Edges come from each child's `connectsTo`. Targets that don't match
14
+ * any sibling id are dropped (and counted in `warnings`).
15
+ */
16
+
17
+ import type { Block, BlockConnection } from '../../schemas/Doc.js';
18
+
19
+ export interface DiagramNodePosition {
20
+ /** Block id (matches `Block.id`). */
21
+ id: string;
22
+ /** Display label (block title or fallback to id). */
23
+ label: string;
24
+ /** Pixel x coordinate (canvas-relative). */
25
+ x: number;
26
+ /** Pixel y coordinate (canvas-relative). */
27
+ y: number;
28
+ /** True when the position came from `x=`/`y=` attributes; false when auto-laid out. */
29
+ pinned: boolean;
30
+ /** Optional `.class` tokens carried over from the heading. */
31
+ classes?: string[];
32
+ }
33
+
34
+ export interface DiagramEdge {
35
+ /** Synthetic stable edge id. */
36
+ id: string;
37
+ /** Source node id. */
38
+ source: string;
39
+ /** Target node id. */
40
+ target: string;
41
+ /** Connection type from `connectsTo=target:type` (used as edge label). */
42
+ type?: string;
43
+ }
44
+
45
+ export interface DiagramLayout {
46
+ nodes: DiagramNodePosition[];
47
+ edges: DiagramEdge[];
48
+ warnings: string[];
49
+ }
50
+
51
+ export interface DiagramLayoutOptions {
52
+ /** Horizontal grid pitch between auto-laid-out nodes. Default: 260 (node width 180 + 80 air). */
53
+ gapX?: number;
54
+ /** Vertical grid pitch between auto-laid-out nodes. Default: 140. */
55
+ gapY?: number;
56
+ /** Padding around the explicit-positions bounding box before the grid starts. Default: 80. */
57
+ gridPad?: number;
58
+ /** Origin x for the grid when no explicit nodes anchor it. Default: 80. */
59
+ gridOriginX?: number;
60
+ /** Origin y for the grid when no explicit nodes anchor it. Default: 80. */
61
+ gridOriginY?: number;
62
+ }
63
+
64
+ const DEFAULT_OPTS: Required<DiagramLayoutOptions> = {
65
+ gapX: 260,
66
+ gapY: 140,
67
+ gridPad: 80,
68
+ gridOriginX: 80,
69
+ gridOriginY: 80,
70
+ };
71
+
72
+ /**
73
+ * Compute node positions and edges for a diagram from a parent block's
74
+ * children. Pure function — same inputs always produce the same output.
75
+ */
76
+ export function computeDiagramLayout(
77
+ children: readonly Block[],
78
+ options: DiagramLayoutOptions = {},
79
+ ): DiagramLayout {
80
+ const opts = { ...DEFAULT_OPTS, ...options };
81
+ const warnings: string[] = [];
82
+
83
+ // First pass: extract explicit positions, record which children still need a grid slot.
84
+ type Entry = { child: Block; pinned: boolean; x: number; y: number };
85
+ const entries: Entry[] = [];
86
+ const unpositionedIdx: number[] = [];
87
+
88
+ for (let i = 0; i < children.length; i++) {
89
+ const c = children[i];
90
+ const hasX = typeof c.x === 'number';
91
+ const hasY = typeof c.y === 'number';
92
+ if (hasX && hasY) {
93
+ entries.push({ child: c, pinned: true, x: c.x as number, y: c.y as number });
94
+ } else {
95
+ entries.push({ child: c, pinned: false, x: 0, y: 0 });
96
+ unpositionedIdx.push(i);
97
+ }
98
+ }
99
+
100
+ // Second pass: lay out unpositioned children in a square-ish grid.
101
+ if (unpositionedIdx.length > 0) {
102
+ const cols = Math.max(1, Math.ceil(Math.sqrt(unpositionedIdx.length)));
103
+ // Find the bottom-right of the pinned bounding box so the grid sits below it.
104
+ let baseX = opts.gridOriginX;
105
+ let baseY = opts.gridOriginY;
106
+ const pinned = entries.filter((e) => e.pinned);
107
+ if (pinned.length > 0) {
108
+ const minX = Math.min(...pinned.map((e) => e.x));
109
+ const maxY = Math.max(...pinned.map((e) => e.y));
110
+ baseX = minX;
111
+ baseY = maxY + opts.gridPad + opts.gapY;
112
+ }
113
+ unpositionedIdx.forEach((entryIdx, k) => {
114
+ const col = k % cols;
115
+ const row = Math.floor(k / cols);
116
+ entries[entryIdx].x = baseX + col * opts.gapX;
117
+ entries[entryIdx].y = baseY + row * opts.gapY;
118
+ });
119
+ }
120
+
121
+ // Third pass: emit nodes + the id-set used to validate edge targets.
122
+ const nodes: DiagramNodePosition[] = entries.map((e) => ({
123
+ id: e.child.id,
124
+ label: e.child.title ?? e.child.id,
125
+ x: e.x,
126
+ y: e.y,
127
+ pinned: e.pinned,
128
+ ...(e.child.classes && e.child.classes.length > 0 ? { classes: e.child.classes } : {}),
129
+ }));
130
+ const nodeIds = new Set(nodes.map((n) => n.id));
131
+
132
+ // Fourth pass: emit edges from each child's connectsTo, dropping ghosts.
133
+ const edges: DiagramEdge[] = [];
134
+ const seenEdgeIds = new Set<string>();
135
+ for (const child of children) {
136
+ if (!child.connectsTo) continue;
137
+ for (const conn of child.connectsTo) {
138
+ if (!nodeIds.has(conn.target)) {
139
+ warnings.push(`Edge from "${child.id}" to "${conn.target}" dropped: no such sibling node`);
140
+ continue;
141
+ }
142
+ const edgeId = makeEdgeId(child.id, conn);
143
+ if (seenEdgeIds.has(edgeId)) continue;
144
+ seenEdgeIds.add(edgeId);
145
+ edges.push({
146
+ id: edgeId,
147
+ source: child.id,
148
+ target: conn.target,
149
+ ...(conn.type ? { type: conn.type } : {}),
150
+ });
151
+ }
152
+ }
153
+
154
+ return { nodes, edges, warnings };
155
+ }
156
+
157
+ function makeEdgeId(source: string, conn: BlockConnection): string {
158
+ return conn.type ? `${source}->${conn.target}:${conn.type}` : `${source}->${conn.target}`;
159
+ }