@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -0,0 +1,589 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { ChartGradient, SceneNode } from '@tanstack/charts/types';
3
+ import {
4
+ type ChartLabel,
5
+ chartLabelStyle,
6
+ collectChartLabels,
7
+ createLynxTextMeasurer,
8
+ renderChartSvg,
9
+ } from './chart-scene';
10
+
11
+ const options = {
12
+ width: 100,
13
+ height: 50,
14
+ resolvePaint: (paint: string) =>
15
+ paint === 'var(--primary)' ? '#111' : paint,
16
+ idPrefix: 'c1',
17
+ };
18
+
19
+ /**
20
+ * The SVG body without the root element, for compact assertions.
21
+ */
22
+ const body = (
23
+ nodes: SceneNode[],
24
+ extra: Partial<typeof options> & { gradients?: ChartGradient[] } = {},
25
+ ) =>
26
+ renderChartSvg(nodes, { ...options, ...extra })
27
+ .replace(/^<svg[^>]*>/, '')
28
+ .replace(/<\/svg>$/, '');
29
+
30
+ describe('renderChartSvg', () => {
31
+ it('returns nothing when no shape would be drawn', () => {
32
+ expect(renderChartSvg([], options)).toBe('');
33
+ expect(
34
+ renderChartSvg(
35
+ [
36
+ { kind: 'label', key: 'l', x: 0, y: 0, text: 'a' },
37
+ { kind: 'group', key: 'g', children: [] },
38
+ ],
39
+ options,
40
+ ),
41
+ ).toBe('');
42
+ });
43
+
44
+ it('writes a standalone SVG the size of the chart', () => {
45
+ expect(
46
+ renderChartSvg(
47
+ [{ kind: 'dot', key: 'd', x: 1, y: 2, radius: 3 }],
48
+ options,
49
+ ),
50
+ ).toBe(
51
+ '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50" viewBox="0 0 100 50"><circle cx="1" cy="2" r="3"/></svg>',
52
+ );
53
+ });
54
+
55
+ it('draws rules as paths, which ServalSVG strokes when inherited', () => {
56
+ expect(
57
+ body([
58
+ {
59
+ kind: 'rule',
60
+ key: 'r',
61
+ x1: 0,
62
+ y1: 1.006,
63
+ x2: 10,
64
+ y2: 1,
65
+ style: { stroke: 'var(--primary)', strokeDasharray: 'none' },
66
+ },
67
+ ]),
68
+ ).toBe('<path d="M0 1.01L10 1" stroke="#111"/>');
69
+ });
70
+
71
+ it('pushes inherited paint down onto every shape', () => {
72
+ expect(
73
+ body([
74
+ {
75
+ kind: 'group',
76
+ key: 'g',
77
+ style: {
78
+ fill: 'red',
79
+ stroke: 'blue',
80
+ strokeWidth: 2,
81
+ opacity: 0.5,
82
+ lineCap: 'round',
83
+ lineJoin: 'arcs',
84
+ },
85
+ children: [
86
+ {
87
+ kind: 'rect',
88
+ key: 'r',
89
+ x: 0,
90
+ y: 0,
91
+ width: 4,
92
+ height: 5,
93
+ style: {
94
+ fill: 'green',
95
+ fillOpacity: 0.3,
96
+ lineJoin: 'miter-clip',
97
+ },
98
+ },
99
+ {
100
+ kind: 'polyline',
101
+ key: 'p',
102
+ points: [
103
+ [0, 0],
104
+ [1, 1],
105
+ ],
106
+ style: { strokeDasharray: '4 2', strokeOpacity: 0.4 },
107
+ },
108
+ ],
109
+ },
110
+ ]),
111
+ ).toBe(
112
+ '<g opacity="0.5">' +
113
+ '<rect x="0" y="0" width="4" height="5" fill="green" fill-opacity="0.3" stroke="blue" stroke-width="2" stroke-linecap="round" stroke-linejoin="miter"/>' +
114
+ '<path d="M0 0L1 1" fill="red" stroke="blue" stroke-opacity="0.4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="4 2"/>' +
115
+ '</g>',
116
+ );
117
+ });
118
+
119
+ it('translates and clips groups, with clip paths in the root defs', () => {
120
+ const svg = renderChartSvg(
121
+ [
122
+ {
123
+ kind: 'group',
124
+ key: 'g',
125
+ translateX: 5,
126
+ translateY: 0,
127
+ clip: { x: 0, y: 0, width: 10, height: 20 },
128
+ children: [{ kind: 'dot', key: 'd', x: 0, y: 0, radius: 1 }],
129
+ },
130
+ {
131
+ kind: 'group',
132
+ key: 'h',
133
+ translateX: 0,
134
+ children: [{ kind: 'dot', key: 'e', x: 0, y: 0, radius: 1 }],
135
+ },
136
+ ],
137
+ options,
138
+ );
139
+ expect(svg).toContain(
140
+ '<defs><clipPath id="c1-clip-0"><rect x="0" y="0" width="10" height="20"/></clipPath></defs>',
141
+ );
142
+ expect(svg).toContain(
143
+ '<g transform="translate(5 0)" clip-path="url(#c1-clip-0)"><circle',
144
+ );
145
+ // A zero translation writes no transform.
146
+ expect(svg).toContain('<g><circle cx="0" cy="0" r="1"/></g>');
147
+ expect(
148
+ body([
149
+ {
150
+ kind: 'group',
151
+ key: 'y',
152
+ translateY: 3,
153
+ children: [{ kind: 'dot', key: 'd', x: 0, y: 0, radius: 1 }],
154
+ },
155
+ ]),
156
+ ).toBe('<g transform="translate(0 3)"><circle cx="0" cy="0" r="1"/></g>');
157
+ expect(
158
+ body([
159
+ {
160
+ kind: 'group',
161
+ key: 'x',
162
+ translateX: 2,
163
+ children: [{ kind: 'dot', key: 'd', x: 0, y: 0, radius: 1 }],
164
+ },
165
+ ]),
166
+ ).toBe('<g transform="translate(2 0)"><circle cx="0" cy="0" r="1"/></g>');
167
+ });
168
+
169
+ it('leaves out focus subtrees and labels', () => {
170
+ expect(
171
+ body([
172
+ {
173
+ kind: 'group',
174
+ key: 'f',
175
+ focus: { match: 'x', points: [], placement: 'over' },
176
+ children: [{ kind: 'dot', key: 'd', x: 0, y: 0, radius: 1 }],
177
+ } as unknown as SceneNode,
178
+ { kind: 'label', key: 'l', x: 0, y: 0, text: 'a' },
179
+ { kind: 'dot', key: 'e', x: 0, y: 0, radius: 2 },
180
+ ]),
181
+ ).toBe('<circle cx="0" cy="0" r="2"/>');
182
+ });
183
+
184
+ it('draws polylines and areas from their path or points', () => {
185
+ expect(
186
+ body([
187
+ { kind: 'polyline', key: 'a', points: [], path: 'M0,0C1,1 2,2 3,3' },
188
+ {
189
+ kind: 'area',
190
+ key: 'b',
191
+ points: [
192
+ [0, 0],
193
+ [1, 0],
194
+ [1, 1],
195
+ ],
196
+ },
197
+ { kind: 'area', key: 'c', points: [], path: 'M0 0Z' },
198
+ { kind: 'area', key: 'e', points: [] },
199
+ ]),
200
+ ).toBe(
201
+ '<path d="M0,0C1,1 2,2 3,3"/>' +
202
+ '<path d="M0 0L1 0L1 1Z"/>' +
203
+ '<path d="M0 0Z"/>' +
204
+ '<path d=""/>',
205
+ );
206
+ });
207
+
208
+ it('joins area polygons and their holes with the even-odd rule', () => {
209
+ expect(
210
+ body([
211
+ {
212
+ kind: 'area',
213
+ key: 'a',
214
+ points: [],
215
+ polygons: [
216
+ [
217
+ [
218
+ [0, 0],
219
+ [4, 0],
220
+ [4, 4],
221
+ ],
222
+ [],
223
+ ],
224
+ ],
225
+ },
226
+ ]),
227
+ ).toBe('<path d="M0 0L4 0L4 4Z" fill-rule="evenodd"/>');
228
+ });
229
+
230
+ it('draws rects with a radius or per-corner radii', () => {
231
+ const svg = body([
232
+ { kind: 'rect', key: 'a', x: 0, y: 0, width: 4, height: 4, radius: 2 },
233
+ { kind: 'rect', key: 'b', x: 0, y: 0, width: 4, height: 4, radius: 0 },
234
+ {
235
+ kind: 'rect',
236
+ key: 'c',
237
+ x: 0,
238
+ y: 0,
239
+ width: 4,
240
+ height: 4,
241
+ cornerRadii: [2, 2, 0, 0],
242
+ },
243
+ ]);
244
+ expect(svg).toContain('<rect x="0" y="0" width="4" height="4" rx="2"/>');
245
+ expect(svg).toContain('<rect x="0" y="0" width="4" height="4"/>');
246
+ expect(svg).toMatch(/<path d="M[^"]+"\/>$/);
247
+ });
248
+
249
+ it('writes gradients and points fills at them', () => {
250
+ const gradients: ChartGradient[] = [
251
+ {
252
+ id: 'fade',
253
+ stops: [
254
+ { offset: 0.5, color: 'var(--primary)', opacity: 0.4 },
255
+ { offset: 0.2, color: 'red' },
256
+ { offset: Number.NaN, color: 'blue' },
257
+ ],
258
+ },
259
+ { id: 'glow', type: 'radial', stops: [{ offset: 2, color: 'red' }] },
260
+ ];
261
+ const svg = renderChartSvg(
262
+ [
263
+ {
264
+ kind: 'rect',
265
+ key: 'r',
266
+ x: 0,
267
+ y: 0,
268
+ width: 1,
269
+ height: 1,
270
+ style: { fill: 'url(#fade)', stroke: 'url(#other)' },
271
+ },
272
+ ],
273
+ { ...options, gradients },
274
+ );
275
+ expect(svg).toContain(
276
+ '<linearGradient id="c1-gradient-fade" x1="0%" y1="100%" x2="0%" y2="0%">' +
277
+ '<stop offset="50%" stop-color="#111" stop-opacity="0.4"/>' +
278
+ // Offsets never go backwards, and NaN counts as 0.
279
+ '<stop offset="50%" stop-color="red"/>' +
280
+ '<stop offset="50%" stop-color="blue"/></linearGradient>',
281
+ );
282
+ expect(svg).toContain(
283
+ '<radialGradient id="c1-gradient-glow" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"><stop offset="100%" stop-color="red"/></radialGradient>',
284
+ );
285
+ expect(svg).toContain('fill="url(#c1-gradient-fade)"');
286
+ // Not one of the scene's gradients: left to the paint resolver.
287
+ expect(svg).toContain('stroke="url(#other)"');
288
+ });
289
+
290
+ it('escapes attribute values and writes non-finite numbers as 0', () => {
291
+ expect(
292
+ body([
293
+ {
294
+ kind: 'dot',
295
+ key: 'd',
296
+ x: Number.NaN,
297
+ y: Number.POSITIVE_INFINITY,
298
+ radius: -0,
299
+ style: { fill: 'a"<&>' },
300
+ },
301
+ ]),
302
+ ).toBe('<circle cx="0" cy="0" r="0" fill="a&quot;&lt;&amp;&gt;"/>');
303
+ });
304
+
305
+ it('scopes ids with a default prefix and sanitizes them', () => {
306
+ const svg = renderChartSvg(
307
+ [
308
+ {
309
+ kind: 'group',
310
+ key: 'g',
311
+ clip: { x: 0, y: 0, width: 1, height: 1 },
312
+ children: [
313
+ {
314
+ kind: 'dot',
315
+ key: 'd',
316
+ x: 0,
317
+ y: 0,
318
+ radius: 1,
319
+ style: { fill: 'url(#a b)' },
320
+ },
321
+ ],
322
+ },
323
+ ],
324
+ {
325
+ width: 1,
326
+ height: 1,
327
+ resolvePaint: (p) => p,
328
+ gradients: [{ id: 'a b', stops: [] }],
329
+ },
330
+ );
331
+ expect(svg).toContain('id="chart-clip-0"');
332
+ expect(svg).toContain('fill="url(#chart-gradient-a_b)"');
333
+ });
334
+ });
335
+
336
+ describe('collectChartLabels', () => {
337
+ it('resolves labels to absolute positions and final paint', () => {
338
+ const labels = collectChartLabels(
339
+ [
340
+ {
341
+ kind: 'group',
342
+ key: 'axis',
343
+ translateX: 10,
344
+ translateY: 20,
345
+ style: {
346
+ fill: 'var(--muted-foreground)',
347
+ fillOpacity: 0.5,
348
+ opacity: 0.8,
349
+ },
350
+ children: [
351
+ {
352
+ kind: 'label',
353
+ key: 'tick',
354
+ x: 1,
355
+ y: 2,
356
+ text: '42',
357
+ anchor: 'end',
358
+ baseline: 'middle',
359
+ rotate: -90,
360
+ fontSize: 10,
361
+ fontWeight: 600,
362
+ style: { opacity: 0.5 },
363
+ },
364
+ ],
365
+ },
366
+ { kind: 'label', key: 'title', x: 0, y: 0, text: 'T' },
367
+ { kind: 'dot', key: 'd', x: 0, y: 0, radius: 1 },
368
+ ],
369
+ { defaultColor: 'var(--foreground)' },
370
+ );
371
+ expect(labels).toEqual<ChartLabel[]>([
372
+ {
373
+ key: 'tick',
374
+ text: '42',
375
+ x: 11,
376
+ y: 22,
377
+ anchor: 'end',
378
+ baseline: 'middle',
379
+ rotate: -90,
380
+ fontSize: 10,
381
+ fontWeight: 600,
382
+ color: 'var(--muted-foreground)',
383
+ opacity: 0.8 * 0.5 * 0.5,
384
+ },
385
+ {
386
+ key: 'title',
387
+ text: 'T',
388
+ x: 0,
389
+ y: 0,
390
+ anchor: 'start',
391
+ baseline: 'auto',
392
+ rotate: 0,
393
+ fontSize: 16,
394
+ fontWeight: 400,
395
+ color: 'var(--foreground)',
396
+ opacity: 1,
397
+ },
398
+ ]);
399
+ });
400
+
401
+ it('gives currentColor labels the default color', () => {
402
+ const [label] = collectChartLabels(
403
+ [
404
+ {
405
+ kind: 'label',
406
+ key: 'l',
407
+ x: 0,
408
+ y: 0,
409
+ text: 'a',
410
+ style: { fill: 'currentColor', fillOpacity: 0.5 },
411
+ },
412
+ ],
413
+ { defaultColor: 'black' },
414
+ );
415
+ expect(label).toMatchObject({ color: 'black', opacity: 0.5 });
416
+ });
417
+
418
+ it('drops labels outside their clip, and skips focus subtrees', () => {
419
+ const labels = collectChartLabels(
420
+ [
421
+ {
422
+ kind: 'group',
423
+ key: 'plot',
424
+ translateX: 10,
425
+ clip: { x: 0, y: 0, width: 20, height: 20 },
426
+ children: [
427
+ {
428
+ kind: 'group',
429
+ key: 'inner',
430
+ children: [
431
+ { kind: 'label', key: 'in', x: 5, y: 5, text: 'in' },
432
+ { kind: 'label', key: 'right', x: 25, y: 5, text: 'out' },
433
+ { kind: 'label', key: 'left', x: -5, y: 5, text: 'out' },
434
+ { kind: 'label', key: 'above', x: 5, y: -5, text: 'out' },
435
+ { kind: 'label', key: 'below', x: 5, y: 25, text: 'out' },
436
+ ],
437
+ },
438
+ ],
439
+ },
440
+ {
441
+ kind: 'group',
442
+ key: 'focus',
443
+ focus: { match: 'x', points: [], placement: 'over' },
444
+ children: [{ kind: 'label', key: 'hidden', x: 0, y: 0, text: 'x' }],
445
+ } as unknown as SceneNode,
446
+ ],
447
+ { defaultColor: 'black' },
448
+ );
449
+ expect(labels.map((l) => l.key)).toEqual(['in']);
450
+ });
451
+
452
+ it('applies focus-driven opacity', () => {
453
+ const focusOpacity = vi.fn(() => 0.25);
454
+ const [label] = collectChartLabels(
455
+ [{ kind: 'label', key: 'l', x: 0, y: 0, text: 'a', focusOpacity }],
456
+ { defaultColor: 'black', pointer: { x: 1, y: 2 } },
457
+ );
458
+ expect(label!.opacity).toBe(0.25);
459
+ expect(focusOpacity).toHaveBeenCalledWith({
460
+ focus: null,
461
+ pointer: { x: 1, y: 2 },
462
+ });
463
+
464
+ const [unfocused] = collectChartLabels(
465
+ [
466
+ {
467
+ kind: 'label',
468
+ key: 'l',
469
+ x: 0,
470
+ y: 0,
471
+ text: 'a',
472
+ focusOpacity: () => undefined,
473
+ },
474
+ ],
475
+ { defaultColor: 'black' },
476
+ );
477
+ expect(unfocused!.opacity).toBe(1);
478
+ });
479
+ });
480
+
481
+ describe('chartLabelStyle', () => {
482
+ const label: ChartLabel = {
483
+ key: 'k',
484
+ text: 't',
485
+ x: 1.234,
486
+ y: 5,
487
+ anchor: 'start',
488
+ baseline: 'hanging',
489
+ rotate: 0,
490
+ fontSize: 10,
491
+ fontWeight: 400,
492
+ color: 'red',
493
+ opacity: 1,
494
+ };
495
+
496
+ it('positions the anchor point and sizes the line box', () => {
497
+ expect(chartLabelStyle(label)).toBe(
498
+ 'position: absolute; left: 1.23px; top: 5px; font-size: 10px; line-height: 10px; ' +
499
+ 'font-weight: 400; color: red; opacity: 1; white-space: nowrap; ' +
500
+ 'transform-origin: 0 0; transform: translate(0%, 0%);',
501
+ );
502
+ });
503
+
504
+ it('shifts for the anchor and baseline, and rotates about the anchor', () => {
505
+ expect(
506
+ chartLabelStyle({
507
+ ...label,
508
+ anchor: 'middle',
509
+ baseline: 'middle',
510
+ rotate: -90,
511
+ }),
512
+ ).toContain('transform: rotate(-90deg) translate(-50%, -50%);');
513
+ expect(
514
+ chartLabelStyle({ ...label, anchor: 'end', baseline: 'auto' }),
515
+ ).toContain('transform: translate(-100%, -80%);');
516
+ });
517
+ });
518
+
519
+ describe('createLynxTextMeasurer', () => {
520
+ const base = {
521
+ fontSize: 10,
522
+ fontFamily: 'sans-serif',
523
+ fontStyle: 'normal',
524
+ fontStretch: 'normal',
525
+ letterSpacing: 0,
526
+ direction: 'ltr' as const,
527
+ fontScale: 1,
528
+ anchor: 'start' as const,
529
+ baseline: 'auto' as const,
530
+ };
531
+
532
+ it('measures through getTextInfo, once per text and size', () => {
533
+ const getTextInfo = vi.fn(() => ({ width: 30 }));
534
+ const measure = createLynxTextMeasurer(getTextInfo);
535
+ expect(measure('abc', base)).toEqual({
536
+ x: 0,
537
+ y: -8,
538
+ width: 30,
539
+ height: 10,
540
+ });
541
+ measure('abc', base);
542
+ expect(getTextInfo).toHaveBeenCalledTimes(1);
543
+ expect(getTextInfo).toHaveBeenCalledWith('abc', { fontSize: '10px' });
544
+ });
545
+
546
+ it('offsets the box for the anchor and baseline', () => {
547
+ const measure = createLynxTextMeasurer(() => ({ width: 20 }));
548
+ expect(
549
+ measure('a', { ...base, anchor: 'middle', baseline: 'middle' }),
550
+ ).toEqual({
551
+ x: -10,
552
+ y: -5,
553
+ width: 20,
554
+ height: 10,
555
+ });
556
+ expect(
557
+ measure('a', { ...base, anchor: 'end', baseline: 'hanging' }),
558
+ ).toEqual({
559
+ x: -20,
560
+ y: 0,
561
+ width: 20,
562
+ height: 10,
563
+ });
564
+ });
565
+
566
+ it('scales the font size and widens bold text', () => {
567
+ const getTextInfo = vi.fn(() => ({ width: 100 }));
568
+ const measure = createLynxTextMeasurer(getTextInfo);
569
+ const metrics = measure('a', { ...base, fontScale: 2, fontWeight: 900 });
570
+ expect(getTextInfo).toHaveBeenCalledWith('a', { fontSize: '20px' });
571
+ expect(metrics.width).toBeCloseTo(104);
572
+ // A missing scale counts as 1.
573
+ measure('a', { ...base, fontScale: 0 });
574
+ expect(getTextInfo).toHaveBeenLastCalledWith('a', { fontSize: '10px' });
575
+ });
576
+
577
+ it('measures empty text and zero sizes as nothing', () => {
578
+ const getTextInfo = vi.fn(() => ({ width: 5 }));
579
+ const measure = createLynxTextMeasurer(getTextInfo);
580
+ expect(measure('', base)).toEqual({ x: 0, y: 0, width: 0, height: 0 });
581
+ expect(measure('a', { ...base, fontSize: 0 })).toEqual({
582
+ x: 0,
583
+ y: 0,
584
+ width: 0,
585
+ height: 0,
586
+ });
587
+ expect(getTextInfo).not.toHaveBeenCalled();
588
+ });
589
+ });