@bendyline/squisq 1.5.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
  6. package/dist/chunk-2PWOO4ZE.js.map +1 -0
  7. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  8. package/dist/chunk-3E5F2XMR.js.map +1 -0
  9. package/dist/chunk-4X3JQXNM.js +35 -0
  10. package/dist/chunk-4X3JQXNM.js.map +1 -0
  11. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  12. package/dist/chunk-5DKBFXPY.js.map +1 -0
  13. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  14. package/dist/chunk-5J4HKRJF.js.map +1 -0
  15. package/dist/chunk-5UYI3RJB.js +2503 -0
  16. package/dist/chunk-5UYI3RJB.js.map +1 -0
  17. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  18. package/dist/chunk-66QO7XFB.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-CY2R2VPR.js +884 -0
  22. package/dist/chunk-CY2R2VPR.js.map +1 -0
  23. package/dist/chunk-FVJUUTEM.js +74 -0
  24. package/dist/chunk-FVJUUTEM.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-MMPBOUK7.js} +10 -73
  30. package/dist/chunk-MMPBOUK7.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  36. package/dist/chunk-R2GC6RLD.js.map +1 -0
  37. package/dist/chunk-R2YZTCLO.js +292 -0
  38. package/dist/chunk-R2YZTCLO.js.map +1 -0
  39. package/dist/chunk-S7A5D7ZJ.js +174 -0
  40. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  41. package/dist/chunk-TFJVXUHH.js +2358 -0
  42. package/dist/chunk-TFJVXUHH.js.map +1 -0
  43. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  44. package/dist/chunk-TUWMK62B.js.map +1 -0
  45. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  46. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  47. package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
  48. package/dist/chunk-W4O2TX5I.js.map +1 -0
  49. package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
  50. package/dist/chunk-X6DUONGT.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +246 -80
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +54 -4
  114. package/src/__tests__/markdownSanitize.test.ts +7 -5
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +12 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +70 -2
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +138 -2
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/fallbackBlock.ts +1 -1
  174. package/src/doc/templates/index.ts +134 -279
  175. package/src/doc/templates/inputDescriptors.ts +20 -1
  176. package/src/doc/templates/listBlock.ts +88 -30
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +67 -5
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/index.ts +1 -0
  204. package/src/markdown/stringify.ts +4 -9
  205. package/src/markdown/utils.ts +36 -14
  206. package/src/narration/align.ts +514 -0
  207. package/src/narration/features.ts +231 -0
  208. package/src/narration/index.ts +86 -0
  209. package/src/narration/nuclei.ts +118 -0
  210. package/src/narration/pacing.ts +211 -0
  211. package/src/narration/script.ts +239 -0
  212. package/src/narration/session.ts +100 -0
  213. package/src/narration/sidecar.ts +187 -0
  214. package/src/narration/syllables.ts +85 -0
  215. package/src/narration/trace.ts +50 -0
  216. package/src/narration/types.ts +299 -0
  217. package/src/narration/vad.ts +131 -0
  218. package/src/recommend/numberHighlight.ts +25 -0
  219. package/src/recommend/templates.ts +108 -12
  220. package/src/schemas/BlockTemplates.ts +156 -7
  221. package/src/schemas/Doc.ts +72 -9
  222. package/src/schemas/Media.ts +12 -2
  223. package/src/schemas/Theme.ts +45 -25
  224. package/src/schemas/Viewport.ts +2 -2
  225. package/src/schemas/fontStacks.ts +79 -0
  226. package/src/schemas/themeCompile.ts +15 -0
  227. package/src/schemas/themeConstants.ts +2 -0
  228. package/src/schemas/themeLibrary.ts +25 -19
  229. package/src/schemas/themeValidator.ts +1 -1
  230. package/src/schemas/themes/gezellig.json +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-5TYS5V3G.js.map +0 -1
  247. package/dist/chunk-A4O7GIWE.js.map +0 -1
  248. package/dist/chunk-BEQODJWV.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-GPLTCSXO.js.map +0 -1
  251. package/dist/chunk-HWVFJAAH.js.map +0 -1
  252. package/dist/chunk-IMSCRLLV.js.map +0 -1
  253. package/dist/chunk-JELIRVDP.js +0 -24
  254. package/dist/chunk-JELIRVDP.js.map +0 -1
  255. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  256. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  257. package/dist/chunk-LTN6I7QW.js.map +0 -1
  258. package/dist/chunk-P3K7NLHF.js.map +0 -1
  259. package/dist/chunk-T5UK6YOB.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1801
  266. package/dist/story/index.js +0 -279
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { getLayers, RenderContext } from '../doc/getLayers.js';
2
+ import type { MaterializeBlockLayersOptions } from '../doc/materializeBlockLayers.js';
3
+ import { materializeLayers } from './materializeTestUtils.js';
3
4
  import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
4
5
  import { TemplateBlock, PersistentLayerConfig } from '../schemas/BlockTemplates.js';
5
6
  import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
@@ -30,7 +31,7 @@ function makeRawBlock(layers: Layer[]): Block {
30
31
  };
31
32
  }
32
33
 
33
- const defaultContext: RenderContext = {
34
+ const defaultContext: MaterializeBlockLayersOptions = {
34
35
  theme: DEFAULT_THEME,
35
36
  viewport: VIEWPORT_PRESETS.landscape,
36
37
  blockIndex: 0,
@@ -41,10 +42,10 @@ const defaultContext: RenderContext = {
41
42
  // Tests
42
43
  // ---------------------------------------------------------------------------
43
44
 
44
- describe('getLayers', () => {
45
+ describe('materializeBlockLayers', () => {
45
46
  it('generates layers for a known template block', () => {
46
47
  const block = makeTemplateBlock({ template: 'title', title: 'Test Title' });
47
- const layers = getLayers(block, defaultContext);
48
+ const layers = materializeLayers(block, defaultContext);
48
49
 
49
50
  expect(Array.isArray(layers)).toBe(true);
50
51
  expect(layers.length).toBeGreaterThan(0);
@@ -61,7 +62,7 @@ describe('getLayers', () => {
61
62
  stat: '42%',
62
63
  description: 'of users',
63
64
  });
64
- const layers = getLayers(block, defaultContext);
65
+ const layers = materializeLayers(block, defaultContext);
65
66
 
66
67
  expect(layers.length).toBeGreaterThan(0);
67
68
  const texts = layers.filter((l) => l.type === 'text');
@@ -78,13 +79,17 @@ describe('getLayers', () => {
78
79
  } as ShapeLayer,
79
80
  ];
80
81
  const block = makeRawBlock(existingLayers);
81
- const layers = getLayers(block, defaultContext);
82
+ const layers = materializeLayers(block, defaultContext);
82
83
 
83
84
  expect(layers).toEqual(existingLayers);
85
+ expect(layers).not.toBe(existingLayers);
86
+ expect(layers[0]).not.toBe(existingLayers[0]);
87
+ (layers[0] as ShapeLayer).content.fill = '#000000';
88
+ expect((existingLayers[0] as ShapeLayer).content.fill).toBe('#ff0000');
84
89
  });
85
90
 
86
91
  it('resolves legacy template aliases through TEMPLATE_ALIASES', () => {
87
- // Regression: `getLayers` previously did `block.template in templateRegistry`
92
+ // Regression: the old resolver used `block.template in templateRegistry`
88
93
  // without alias resolution, so docs authored with the legacy long ids
89
94
  // (`titleBlock`, `quoteBlock`, `mapBlock`, `listBlock`) rendered as
90
95
  // empty SVG cards in the linear/document view — `hasTemplate()` accepts
@@ -103,7 +108,7 @@ describe('getLayers', () => {
103
108
  items: ['a', 'b', 'c'],
104
109
  quote: 'q',
105
110
  } as Partial<TemplateBlock>);
106
- const layers = getLayers(block, defaultContext);
111
+ const layers = materializeLayers(block, defaultContext);
107
112
  expect(layers.length).toBeGreaterThan(0);
108
113
  }
109
114
  });
@@ -115,7 +120,7 @@ describe('getLayers', () => {
115
120
  duration: 5,
116
121
  audioSegment: 0,
117
122
  } as unknown as TemplateBlock;
118
- const layers = getLayers(block, defaultContext);
123
+ const layers = materializeLayers(block, defaultContext);
119
124
 
120
125
  expect(layers.length).toBeGreaterThan(0);
121
126
  const noticeText = layers
@@ -126,14 +131,14 @@ describe('getLayers', () => {
126
131
 
127
132
  it('returns empty array for block with no template and no layers', () => {
128
133
  const block: Block = { id: 'empty-1', duration: 5, startTime: 0, audioSegment: 0 };
129
- const layers = getLayers(block, defaultContext);
134
+ const layers = materializeLayers(block, defaultContext);
130
135
 
131
136
  expect(layers).toEqual([]);
132
137
  });
133
138
 
134
139
  it('uses default context values when context is omitted', () => {
135
140
  const block = makeTemplateBlock({ template: 'title', title: 'Defaults' });
136
- const layers = getLayers(block);
141
+ const layers = materializeLayers(block);
137
142
 
138
143
  expect(Array.isArray(layers)).toBe(true);
139
144
  expect(layers.length).toBeGreaterThan(0);
@@ -144,12 +149,12 @@ describe('getLayers', () => {
144
149
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
145
150
 
146
151
  // comparisonBar requires numeric values; passing invalid data may throw
147
- // but getLayers should catch and return []
152
+ // but materialization should degrade without throwing
148
153
  const block = makeTemplateBlock({
149
154
  template: 'comparisonBar',
150
155
  // Missing required fields
151
156
  });
152
- const layers = getLayers(block, defaultContext);
157
+ const layers = materializeLayers(block, defaultContext);
153
158
 
154
159
  expect(Array.isArray(layers)).toBe(true);
155
160
  consoleSpy.mockRestore();
@@ -177,7 +182,10 @@ describe('getLayers', () => {
177
182
 
178
183
  it('injects persistent bottom and top layers around block layers', () => {
179
184
  const block = makeTemplateBlock({ template: 'title', title: 'With Persistent' });
180
- const layers = getLayers(block, { ...defaultContext, persistentLayers: persistentConfig });
185
+ const layers = materializeLayers(block, {
186
+ ...defaultContext,
187
+ persistentLayers: persistentConfig,
188
+ });
181
189
 
182
190
  // First layer should be the bottom persistent layer
183
191
  expect(layers[0]).toEqual(bottomLayer);
@@ -193,7 +201,10 @@ describe('getLayers', () => {
193
201
  title: 'No Bottom',
194
202
  useBottomLayer: false,
195
203
  });
196
- const layers = getLayers(block, { ...defaultContext, persistentLayers: persistentConfig });
204
+ const layers = materializeLayers(block, {
205
+ ...defaultContext,
206
+ persistentLayers: persistentConfig,
207
+ });
197
208
 
198
209
  // Should NOT start with the bottom persistent layer
199
210
  expect(layers[0]).not.toEqual(bottomLayer);
@@ -207,7 +218,10 @@ describe('getLayers', () => {
207
218
  title: 'No Top',
208
219
  useTopLayer: false,
209
220
  });
210
- const layers = getLayers(block, { ...defaultContext, persistentLayers: persistentConfig });
221
+ const layers = materializeLayers(block, {
222
+ ...defaultContext,
223
+ persistentLayers: persistentConfig,
224
+ });
211
225
 
212
226
  // Should start with the bottom persistent layer
213
227
  expect(layers[0]).toEqual(bottomLayer);
@@ -217,8 +231,8 @@ describe('getLayers', () => {
217
231
 
218
232
  it('skips injection when no persistent layers are configured', () => {
219
233
  const block = makeTemplateBlock({ template: 'title', title: 'No Persistent' });
220
- const withoutPersistent = getLayers(block, defaultContext);
221
- const withEmptyPersistent = getLayers(block, {
234
+ const withoutPersistent = materializeLayers(block, defaultContext);
235
+ const withEmptyPersistent = materializeLayers(block, {
222
236
  ...defaultContext,
223
237
  persistentLayers: { bottomLayers: [], topLayers: [] },
224
238
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, afterEach } from 'vitest';
2
2
  import { parseMarkdown } from '../markdown/parse.js';
3
3
  import { markdownToDoc } from '../doc/markdownToDoc.js';
4
- import { getLayers } from '../doc/getLayers.js';
4
+ import { materializeLayers } from './materializeTestUtils.js';
5
5
  import { templateRegistry } from '../doc/templates/index.js';
6
6
  import type { Block, Layer } from '../schemas/Doc.js';
7
7
 
@@ -15,10 +15,10 @@ function firstBlock(md: string): Block {
15
15
  return markdownToDoc(parseMarkdown(md)).blocks[0];
16
16
  }
17
17
 
18
- describe('getLayers — graceful degradation', () => {
18
+ describe('materializeBlockLayers — graceful degradation', () => {
19
19
  it('renders an unknown template as a plain card with title, body, and notice', () => {
20
20
  const block = firstBlock('## My Section {[photGrid]}\n\nSome body prose here.');
21
- const layers = getLayers(block);
21
+ const layers = materializeLayers(block);
22
22
  expect(layers.length).toBeGreaterThan(0);
23
23
  const text = textOf(layers);
24
24
  expect(text).toContain('My Section');
@@ -39,7 +39,7 @@ describe('getLayers — graceful degradation', () => {
39
39
  template: '__boom',
40
40
  title: 'Exploding block',
41
41
  };
42
- const layers = getLayers(block);
42
+ const layers = materializeLayers(block);
43
43
  expect(layers.length).toBeGreaterThan(0);
44
44
  expect(textOf(layers)).toContain('Template "__boom" failed');
45
45
  expect(textOf(layers)).toContain('Exploding block');
@@ -50,11 +50,11 @@ describe('getLayers — graceful degradation', () => {
50
50
 
51
51
  it('blocks with no template at all still return empty layers', () => {
52
52
  const block: Block = { id: 'raw', startTime: 0, duration: 5, audioSegment: 0 };
53
- expect(getLayers(block)).toEqual([]);
53
+ expect(materializeLayers(block)).toEqual([]);
54
54
  });
55
55
  });
56
56
 
57
- describe('getLayers — structured data feeds templates', () => {
57
+ describe('materializeBlockLayers — structured data feeds templates', () => {
58
58
  afterEach(() => {
59
59
  delete (templateRegistry as Record<string, unknown>)['__capture'];
60
60
  });
@@ -76,7 +76,7 @@ describe('getLayers — structured data feeds templates', () => {
76
76
  templateData: { rows: [['a', 'b']], zoom: 12 },
77
77
  templateOverrides: { zoom: '14' },
78
78
  };
79
- getLayers(block);
79
+ materializeLayers(block);
80
80
  expect(received).not.toBeNull();
81
81
  expect(received!['rows']).toEqual([['a', 'b']]);
82
82
  // String overrides from {[…]} params win last.
@@ -92,7 +92,7 @@ describe('getLayers — structured data feeds templates', () => {
92
92
  '| Alice | 30 |',
93
93
  ].join('\n');
94
94
  const block = firstBlock(md);
95
- const layers = getLayers(block);
95
+ const layers = materializeLayers(block);
96
96
  expect(layers.length).toBeGreaterThan(0);
97
97
  const tableLayer = layers.find((l) => l.type === 'table');
98
98
  expect(tableLayer).toBeDefined();
@@ -137,6 +137,8 @@ describe('imageEdit/versions', () => {
137
137
  expect(result.version?.path).toMatch(/^\.versions\/state\.\d{8}T\d{6}Z\.json$/);
138
138
  // snapshot is inside the sidecar, visible to the parent under the prefix
139
139
  expect(await parent.exists(`image_files/${result.version!.path}`)).toBe(true);
140
+ const gitignore = await parent.readFile('image_files/.gitignore');
141
+ expect(gitignore && new TextDecoder().decode(gitignore)).toBe('.versions/\n');
140
142
  });
141
143
 
142
144
  it('returns no-state when sidecar is empty', async () => {
@@ -1,5 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import {
3
+ escapePointerSegment,
4
+ appendPointer,
3
5
  toPointer,
4
6
  pointerSegments,
5
7
  getByPointer,
@@ -7,6 +9,19 @@ import {
7
9
  resolveRef,
8
10
  } from '../jsonForm/index.js';
9
11
 
12
+ describe('pointer composition', () => {
13
+ it('escapes arbitrary member names and appends them safely', () => {
14
+ expect(escapePointerSegment('a~/b')).toBe('a~0~1b');
15
+ expect(appendPointer('/properties', 'a~/b')).toBe('/properties/a~0~1b');
16
+ expect(appendPointer('', '')).toBe('/');
17
+ expect(appendPointer('/', 'child')).toBe('//child');
18
+ });
19
+
20
+ it('validates an explicit base pointer before appending', () => {
21
+ expect(() => appendPointer('/bad~2base', 'child')).toThrow(/Invalid JSON Pointer/);
22
+ });
23
+ });
24
+
10
25
  describe('toPointer', () => {
11
26
  it('keeps pointer-form paths intact', () => {
12
27
  expect(toPointer('/a/b')).toBe('/a/b');
@@ -18,13 +33,18 @@ describe('toPointer', () => {
18
33
  });
19
34
  it('returns empty for root', () => {
20
35
  expect(toPointer('')).toBe('');
21
- expect(toPointer('/')).toBe('');
36
+ expect(toPointer('/')).toBe('/');
22
37
  });
23
38
  it('escapes ~ and / per RFC 6901', () => {
24
39
  // dotted form does not allow / inside a segment; we test pointer-form decoding instead.
25
40
  expect(pointerSegments('/a~1b')).toEqual(['a/b']);
26
41
  expect(pointerSegments('/a~0b')).toEqual(['a~b']);
27
42
  });
43
+ it('rejects malformed pointer syntax and escape sequences', () => {
44
+ expect(() => pointerSegments('not/a/pointer')).toThrow(/Invalid JSON Pointer/);
45
+ expect(() => pointerSegments('/bad~2escape')).toThrow(/Invalid JSON Pointer/);
46
+ expect(() => pointerSegments('/dangling~')).toThrow(/Invalid JSON Pointer/);
47
+ });
28
48
  });
29
49
 
30
50
  describe('getByPointer', () => {
@@ -43,6 +63,13 @@ describe('getByPointer', () => {
43
63
  it('returns the data itself for empty pointer', () => {
44
64
  expect(getByPointer(data, '')).toBe(data);
45
65
  });
66
+ it('treats "/" as the empty-string member per RFC 6901', () => {
67
+ expect(getByPointer({ '': 'empty key' }, '/')).toBe('empty key');
68
+ });
69
+ it('requires canonical array indices', () => {
70
+ expect(getByPointer({ values: ['a'] }, '/values/00')).toBeUndefined();
71
+ expect(getByPointer({ values: ['a'] }, '/values/-')).toBeUndefined();
72
+ });
46
73
  });
47
74
 
48
75
  describe('setByPointer', () => {
@@ -75,6 +102,25 @@ describe('setByPointer', () => {
75
102
  expect(getByPointer(after, '/a/b/1/c')).toBe(42);
76
103
  expect(getByPointer(after, '/a/b/0/c')).toBe(1);
77
104
  });
105
+ it('writes the empty-string member for "/"', () => {
106
+ expect(setByPointer({}, '/', 'value')).toEqual({ '': 'value' });
107
+ });
108
+ it('rejects non-canonical array indices instead of creating NaN properties', () => {
109
+ expect(() => setByPointer({ items: [] }, '/items/nope', 'x')).toThrow(/array index/i);
110
+ expect(() => setByPointer({ items: [] }, '/items/01', 'x')).toThrow(/array index/i);
111
+ });
112
+ it('writes prototype-shaped keys as safe own data properties', () => {
113
+ const before = {};
114
+ const after = setByPointer(before, '/__proto__/polluted', true) as Record<string, unknown>;
115
+ expect(Object.getPrototypeOf(after)).toBe(Object.prototype);
116
+ expect(Object.prototype).not.toHaveProperty('polluted');
117
+ expect(Object.prototype.hasOwnProperty.call(after, '__proto__')).toBe(true);
118
+ expect(getByPointer(after, '/__proto__/polluted')).toBe(true);
119
+
120
+ const constructorPath = setByPointer({}, '/constructor/prototype/polluted', true);
121
+ expect(getByPointer(constructorPath, '/constructor/prototype/polluted')).toBe(true);
122
+ expect(Object.prototype).not.toHaveProperty('polluted');
123
+ });
78
124
  });
79
125
 
80
126
  describe('resolveRef', () => {
@@ -62,8 +62,8 @@ describe('layout container behavior', () => {
62
62
  expect(isContainerTemplate('layout')).toBe(true);
63
63
  });
64
64
 
65
- it('renders a layout block through the template registry (getLayers)', async () => {
66
- const { getLayers } = await import('../doc/getLayers.js');
65
+ it('renders a layout block through the template registry', async () => {
66
+ const { materializeBlockLayers } = await import('../doc/materializeBlockLayers.js');
67
67
  const parent: Block = {
68
68
  id: 'lay',
69
69
  startTime: 0,
@@ -75,7 +75,7 @@ describe('layout container behavior', () => {
75
75
  child('box-1', 'rectangle', { x: '760', y: '390', width: '400', height: '300' }),
76
76
  ],
77
77
  };
78
- const layers = getLayers(parent, {});
78
+ const layers = materializeBlockLayers(parent, { persistentLayers: false }).layers;
79
79
  const types = new Set(layers.map((l: Layer) => l.type));
80
80
  expect(types.has('text')).toBe(true);
81
81
  expect(types.has('shape')).toBe(true);
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { PathLayer } from '../schemas/Doc';
3
+
4
+ describe('major-version core API types', () => {
5
+ it('requires endpoint markers instead of the removed arrow field', () => {
6
+ const layer: PathLayer = {
7
+ id: 'path',
8
+ type: 'path',
9
+ position: { x: 0, y: 0, width: 10, height: 10 },
10
+ content: {
11
+ d: 'M 0 0 L 10 10',
12
+ // @ts-expect-error Old documents are read tolerantly, but new code uses endMarker.
13
+ arrow: 'end',
14
+ },
15
+ };
16
+ expect(layer.content).toHaveProperty('arrow', 'end');
17
+ });
18
+ });
@@ -155,14 +155,14 @@ describe('parseMarkdown / stringifyMarkdown', () => {
155
155
  });
156
156
 
157
157
  it('parses mentions from @[Name](scheme:id)', () => {
158
- const doc = parse('Hey @[Leo](gezel:leo), take a look.');
158
+ const doc = parse('Hey @[Leo](person:leo), take a look.');
159
159
  const p = doc.children[0] as MarkdownParagraph;
160
160
  const text0 = p.children[0] as MarkdownText;
161
161
  expect(text0).toEqual({ type: 'text', value: 'Hey ' });
162
162
  const mention = p.children[1] as MarkdownMention;
163
163
  expect(mention).toEqual({
164
164
  type: 'mention',
165
- targetKind: 'gezel',
165
+ targetKind: 'person',
166
166
  targetId: 'leo',
167
167
  displayName: 'Leo',
168
168
  });
@@ -176,7 +176,7 @@ describe('parseMarkdown / stringifyMarkdown', () => {
176
176
  // re-parsing `\:` produces the unescaped url, so the round-trip
177
177
  // preserves the mention semantically even if the raw text changes
178
178
  // on the first pass.
179
- const input = 'Hey @[Leo](gezel:leo), ping @[Tess](gezel:tess) too.\n';
179
+ const input = 'Hey @[Leo](person:leo), ping @[Tess](person:tess) too.\n';
180
180
  const once = stringifyMarkdown(parseMarkdown(input));
181
181
  const twice = stringifyMarkdown(parseMarkdown(once));
182
182
  expect(twice).toBe(once);
@@ -184,7 +184,7 @@ describe('parseMarkdown / stringifyMarkdown', () => {
184
184
  const doc = parseMarkdown(once) as MarkdownDocument;
185
185
  const p = doc.children[0] as MarkdownParagraph;
186
186
  const m1 = p.children.find((c) => c.type === 'mention') as MarkdownMention;
187
- expect(m1.targetKind).toBe('gezel');
187
+ expect(m1.targetKind).toBe('person');
188
188
  expect(m1.targetId).toBe('leo');
189
189
  expect(m1.displayName).toBe('Leo');
190
190
  });
@@ -831,6 +831,19 @@ describe('parseFrontmatter', () => {
831
831
  expect(result).toEqual({ title: 'Hello World', name: 'Jane' });
832
832
  });
833
833
 
834
+ it('keeps quoted booleans and numbers as strings and decodes escapes', () => {
835
+ const result = parseFrontmatter(String.raw`boolean: "true"
836
+ number: "1e3"
837
+ path: "C:\\temp\\\"quoted\""
838
+ single: 'it''s'`);
839
+ expect(result).toEqual({
840
+ boolean: 'true',
841
+ number: '1e3',
842
+ path: 'C:\\temp\\"quoted"',
843
+ single: "it's",
844
+ });
845
+ });
846
+
834
847
  it('skips comment lines and blank lines', () => {
835
848
  const result = parseFrontmatter('# comment\n\ntitle: Test');
836
849
  expect(result).toEqual({ title: 'Test' });
@@ -903,6 +916,43 @@ describe('multi-line frontmatter round-trips', () => {
903
916
  expect(md).toContain('document-render-as: portrait');
904
917
  expect(md).not.toContain('|-');
905
918
  });
919
+
920
+ it('losslessly round-trips ambiguous and escaped single-line strings', () => {
921
+ const doc = createDocument();
922
+ doc.frontmatter = {
923
+ booleanLike: 'true',
924
+ numericLike: '1e3',
925
+ padded: ' keep me ',
926
+ quoted: '"literal quotes"',
927
+ path: 'C:\\temp\\"quoted"',
928
+ };
929
+ const md = stringifyMarkdown(doc);
930
+ expect(md).toContain('booleanLike: "true"');
931
+ expect(md).toContain('numericLike: "1e3"');
932
+ expect(parseMarkdown(md).frontmatter).toEqual(doc.frontmatter);
933
+ });
934
+
935
+ it('uses the same escaping rules when updating source frontmatter', () => {
936
+ const out = setFrontmatterValues('# Body\n', {
937
+ booleanLike: 'false',
938
+ padded: ' value ',
939
+ path: 'C:\\temp\\"quoted"',
940
+ });
941
+ expect(parseMarkdown(out).frontmatter).toEqual({
942
+ booleanLike: 'false',
943
+ padded: ' value ',
944
+ path: 'C:\\temp\\"quoted"',
945
+ });
946
+ });
947
+
948
+ it('writes boolean values as unquoted YAML booleans', () => {
949
+ const out = setFrontmatterValues('# Body\n', { enabled: true, disabled: false });
950
+ expect(out).toContain('enabled: true');
951
+ expect(out).toContain('disabled: false');
952
+ expect(out).not.toContain('enabled: "true"');
953
+ expect(out).not.toContain('disabled: "false"');
954
+ expect(parseMarkdown(out).frontmatter).toEqual({ enabled: true, disabled: false });
955
+ });
906
956
  });
907
957
 
908
958
  describe('frontmatter in parseMarkdown', () => {
@@ -18,17 +18,19 @@ describe('markdown HTML sanitization', () => {
18
18
  });
19
19
 
20
20
  it('extraLinkSchemes allows host-app schemes but never executable ones', () => {
21
- expect(sanitizeUrl('gezel-nav:src%2Fa.ts', 'link')).toBeNull();
22
- expect(sanitizeUrl('gezel-nav:src%2Fa.ts', 'link', { extraLinkSchemes: ['gezel-nav'] })).toBe(
23
- 'gezel-nav:src%2Fa.ts',
24
- );
21
+ expect(sanitizeUrl('workspace-nav:src%2Fa.ts', 'link')).toBeNull();
22
+ expect(
23
+ sanitizeUrl('workspace-nav:src%2Fa.ts', 'link', { extraLinkSchemes: ['workspace-nav'] }),
24
+ ).toBe('workspace-nav:src%2Fa.ts');
25
25
  expect(
26
26
  sanitizeUrl('javascript:alert(1)', 'link', { extraLinkSchemes: ['javascript'] }),
27
27
  ).toBeNull();
28
28
  expect(sanitizeUrl('data:text/html,x', 'link', { extraLinkSchemes: ['data'] })).toBeNull();
29
29
  // media URLs ignore the option entirely
30
30
  expect(
31
- sanitizeUrl('gezel-nav:src%2Fa.ts', 'media', { extraLinkSchemes: ['gezel-nav'] }),
31
+ sanitizeUrl('workspace-nav:src%2Fa.ts', 'media', {
32
+ extraLinkSchemes: ['workspace-nav'],
33
+ }),
32
34
  ).toBeNull();
33
35
  });
34
36
 
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { matchFontFamily, guessFontFallback } from '../schemas/fontStacks.js';
3
+ import { accentToColorScheme } from '../schemas/themeCompile.js';
4
+ import { deriveScale, isHex } from '../schemas/colorUtils.js';
5
+
6
+ describe('matchFontFamily', () => {
7
+ it('matches curated stacks by label', () => {
8
+ expect(matchFontFamily('Playfair Display')).toEqual({ stackId: 'playfair' });
9
+ expect(matchFontFamily('JetBrains Mono')).toEqual({ stackId: 'jetbrains-mono' });
10
+ expect(matchFontFamily('IBM Plex Sans')).toEqual({ stackId: 'ibm-plex-sans' });
11
+ expect(matchFontFamily('Inter')).toEqual({ stackId: 'inter' });
12
+ });
13
+
14
+ it('matches case-insensitively and ignores surrounding quotes/whitespace', () => {
15
+ expect(matchFontFamily(' playfair display ')).toEqual({ stackId: 'playfair' });
16
+ expect(matchFontFamily('"Merriweather"')).toEqual({ stackId: 'merriweather' });
17
+ expect(matchFontFamily("'LORA'")).toEqual({ stackId: 'lora' });
18
+ });
19
+
20
+ it('matches explicit aliases and Google family names', () => {
21
+ expect(matchFontFamily('Playfair')).toEqual({ stackId: 'playfair' });
22
+ expect(matchFontFamily('Source Serif Pro')).toEqual({ stackId: 'source-serif' });
23
+ expect(matchFontFamily('Source Serif 4')).toEqual({ stackId: 'source-serif' });
24
+ expect(matchFontFamily('Crimson Pro')).toEqual({ stackId: 'crimson' });
25
+ expect(matchFontFamily('Cormorant')).toEqual({ stackId: 'cormorant' });
26
+ });
27
+
28
+ it('maps web-safe faces onto the system stacks that already anchor them', () => {
29
+ expect(matchFontFamily('Georgia')).toEqual({ stackId: 'system-serif' });
30
+ expect(matchFontFamily('Times New Roman')).toEqual({ stackId: 'system-serif' });
31
+ expect(matchFontFamily('Consolas')).toEqual({ stackId: 'system-mono' });
32
+ expect(matchFontFamily('Courier New')).toEqual({ stackId: 'system-mono' });
33
+ expect(matchFontFamily('Segoe UI')).toEqual({ stackId: 'system-sans' });
34
+ });
35
+
36
+ it('preserves unknown faces as custom fonts with a heuristic fallback', () => {
37
+ expect(matchFontFamily('Calibri')).toEqual({
38
+ custom: { name: 'Calibri', fallback: 'sans-serif' },
39
+ });
40
+ expect(matchFontFamily('Calibri Light')).toEqual({
41
+ custom: { name: 'Calibri Light', fallback: 'sans-serif' },
42
+ });
43
+ expect(matchFontFamily('Cambria')).toEqual({
44
+ custom: { name: 'Cambria', fallback: 'serif' },
45
+ });
46
+ expect(matchFontFamily('Aptos')).toEqual({
47
+ custom: { name: 'Aptos', fallback: 'sans-serif' },
48
+ });
49
+ });
50
+
51
+ it('keeps original casing but strips quotes on custom names', () => {
52
+ expect(matchFontFamily('"Neue Haas Grotesk"')).toEqual({
53
+ custom: { name: 'Neue Haas Grotesk', fallback: 'sans-serif' },
54
+ });
55
+ });
56
+
57
+ it('resolves empty/blank names to system-sans', () => {
58
+ expect(matchFontFamily('')).toEqual({ stackId: 'system-sans' });
59
+ expect(matchFontFamily(' ')).toEqual({ stackId: 'system-sans' });
60
+ });
61
+ });
62
+
63
+ describe('guessFontFallback', () => {
64
+ it('detects monospace faces', () => {
65
+ expect(guessFontFallback('Cascadia Code')).toBe('monospace');
66
+ expect(guessFontFallback('Roboto Mono')).toBe('monospace');
67
+ expect(guessFontFallback('Courier Prime')).toBe('monospace');
68
+ });
69
+
70
+ it('detects serif faces by keyword and by known serif names', () => {
71
+ expect(guessFontFallback('Adobe Caslon Pro')).toBe('serif');
72
+ expect(guessFontFallback('Bodoni Moda')).toBe('serif');
73
+ expect(guessFontFallback('Palatino Linotype')).toBe('serif');
74
+ expect(guessFontFallback('Awesome Serif')).toBe('serif');
75
+ });
76
+
77
+ it('does not treat sans-serif names as serif', () => {
78
+ expect(guessFontFallback('PT Sans Serif Whatever')).toBe('sans-serif');
79
+ expect(guessFontFallback('Open Sans')).toBe('sans-serif');
80
+ });
81
+
82
+ it('defaults to sans-serif and never guesses system-ui', () => {
83
+ expect(guessFontFallback('Neue Haas Grotesk')).toBe('sans-serif');
84
+ expect(guessFontFallback('Futura')).toBe('sans-serif');
85
+ });
86
+ });
87
+
88
+ describe('accentToColorScheme', () => {
89
+ it('derives the same scheme the editor previously derived inline', () => {
90
+ // Parity with the original editor-react schemeFromAccent implementation.
91
+ const accent = '#63b3ed';
92
+ const scale = deriveScale(accent, 0.3);
93
+ expect(accentToColorScheme(accent)).toEqual({
94
+ bg: scale.darker2,
95
+ text: scale.lighter2,
96
+ accent: scale.base,
97
+ });
98
+ });
99
+
100
+ it('returns valid hex colors for every slot', () => {
101
+ const scheme = accentToColorScheme('#c05621');
102
+ expect(isHex(scheme.bg)).toBe(true);
103
+ expect(isHex(scheme.text)).toBe(true);
104
+ expect(isHex(scheme.accent)).toBe(true);
105
+ });
106
+
107
+ it('falls back to a neutral scheme on invalid input', () => {
108
+ expect(accentToColorScheme('not-a-color')).toEqual({
109
+ bg: '#1a202c',
110
+ text: '#e2e8f0',
111
+ accent: '#63b3ed',
112
+ });
113
+ });
114
+ });