@carto/ps-react-ui 4.3.3 → 4.3.5

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 (301) hide show
  1. package/dist/components.js +3 -3
  2. package/dist/components.js.map +1 -1
  3. package/dist/{lasso-tool-BwRzEW7k.js → lasso-tool-wFqOD6wk.js} +13 -13
  4. package/dist/lasso-tool-wFqOD6wk.js.map +1 -0
  5. package/dist/types/components/common-types.d.ts +41 -0
  6. package/dist/types/components/types.d.ts +1 -1
  7. package/dist/types/widgets/echart/echart-ui.d.ts +1 -1
  8. package/dist/types/widgets/echart/types.d.ts +4 -0
  9. package/dist/widgets/actions.js +1 -1
  10. package/dist/widgets/bar.js +1 -1
  11. package/dist/widgets/category.js +1 -1
  12. package/dist/widgets/echart.js +96 -85
  13. package/dist/widgets/echart.js.map +1 -1
  14. package/dist/widgets/formula.js +1 -1
  15. package/dist/widgets/histogram.js +1 -1
  16. package/dist/widgets/markdown.js +1 -1
  17. package/dist/widgets/pie.js +1 -1
  18. package/dist/widgets/scatterplot.js +1 -1
  19. package/dist/widgets/spread.js +1 -1
  20. package/dist/widgets/table.js +1 -1
  21. package/dist/widgets/timeseries.js +1 -1
  22. package/dist/widgets/toolbar-actions.js.map +1 -1
  23. package/dist/widgets/wrapper.js +1 -1
  24. package/package.json +8 -3
  25. package/src/components/basemaps/basemaps.test.tsx +196 -0
  26. package/src/components/basemaps/basemaps.tsx +128 -0
  27. package/src/components/basemaps/const.ts +13 -0
  28. package/src/components/basemaps/group-wrapper.test.tsx +38 -0
  29. package/src/components/basemaps/group-wrapper.tsx +28 -0
  30. package/src/components/basemaps/group.test.tsx +52 -0
  31. package/src/components/basemaps/group.tsx +42 -0
  32. package/src/components/basemaps/header.test.tsx +54 -0
  33. package/src/components/basemaps/header.tsx +36 -0
  34. package/src/components/basemaps/styles.ts +76 -0
  35. package/src/components/basemaps/types.ts +30 -0
  36. package/src/components/common-types.ts +1 -0
  37. package/src/components/geolocation-controls/const.ts +6 -0
  38. package/src/components/geolocation-controls/geolocation-controls.test.tsx +133 -0
  39. package/src/components/geolocation-controls/geolocation-controls.tsx +95 -0
  40. package/src/components/geolocation-controls/types.ts +17 -0
  41. package/src/components/index.ts +64 -0
  42. package/src/components/lasso-tool/chip.tsx +37 -0
  43. package/src/components/lasso-tool/const.tsx +70 -0
  44. package/src/components/lasso-tool/icons.tsx +91 -0
  45. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +168 -0
  46. package/src/components/lasso-tool/lasso-tool-inline.tsx +245 -0
  47. package/src/components/lasso-tool/lasso-tool.test.tsx +212 -0
  48. package/src/components/lasso-tool/lasso-tool.tsx +479 -0
  49. package/src/components/lasso-tool/styles.ts +143 -0
  50. package/src/components/lasso-tool/types.ts +114 -0
  51. package/src/components/list-data/list-data-skeleton.test.tsx +10 -0
  52. package/src/components/list-data/list-data-skeleton.tsx +40 -0
  53. package/src/components/list-data/list-data.test.tsx +94 -0
  54. package/src/components/list-data/list-data.tsx +106 -0
  55. package/src/components/list-data/styles.ts +37 -0
  56. package/src/components/list-data/types.ts +25 -0
  57. package/src/components/measurement-tools/const.tsx +108 -0
  58. package/src/components/measurement-tools/icons.tsx +54 -0
  59. package/src/components/measurement-tools/measurement-tools.test.tsx +165 -0
  60. package/src/components/measurement-tools/measurement-tools.tsx +443 -0
  61. package/src/components/measurement-tools/styles.ts +91 -0
  62. package/src/components/measurement-tools/types.ts +77 -0
  63. package/src/components/no-data-alert/no-data-alert.test.tsx +31 -0
  64. package/src/components/no-data-alert/no-data-alert.tsx +59 -0
  65. package/src/components/responsive-drawer/responsive-drawer.test.tsx +91 -0
  66. package/src/components/responsive-drawer/responsive-drawer.tsx +53 -0
  67. package/src/components/smart-tooltip/smart-tooltip.test.tsx +168 -0
  68. package/src/components/smart-tooltip/smart-tooltip.tsx +40 -0
  69. package/src/components/tooltip/tooltip.test.tsx +86 -0
  70. package/src/components/tooltip/tooltip.tsx +30 -0
  71. package/src/components/types.ts +1 -0
  72. package/src/components/zoom-controls/styles.ts +27 -0
  73. package/src/components/zoom-controls/types.ts +19 -0
  74. package/src/components/zoom-controls/zoom-controls.test.tsx +101 -0
  75. package/src/components/zoom-controls/zoom-controls.tsx +114 -0
  76. package/src/hooks/index.ts +2 -0
  77. package/src/hooks/use-debounce.ts +55 -0
  78. package/src/hooks/use-skeleton.test.tsx +32 -0
  79. package/src/hooks/use-skeleton.ts +19 -0
  80. package/src/hooks/use-widget-ref.ts +33 -0
  81. package/src/widgets/README.md +277 -0
  82. package/src/widgets/_shared/chart-config/config-factory.ts +67 -0
  83. package/src/widgets/_shared/chart-config/csv-modifiers.ts +56 -0
  84. package/src/widgets/_shared/chart-config/index.ts +21 -0
  85. package/src/widgets/_shared/chart-config/option-builders.ts +203 -0
  86. package/src/widgets/_shared/skeleton/index.ts +5 -0
  87. package/src/widgets/_shared/skeleton/styles.ts +20 -0
  88. package/src/widgets/actions/change-column/change-column-icon.tsx +10 -0
  89. package/src/widgets/actions/change-column/change-column.test.tsx +163 -0
  90. package/src/widgets/actions/change-column/change-column.tsx +141 -0
  91. package/src/widgets/actions/change-column/sortable-column-item.tsx +49 -0
  92. package/src/widgets/actions/change-column/types.ts +20 -0
  93. package/src/widgets/actions/download/download.test.tsx +322 -0
  94. package/src/widgets/actions/download/download.tsx +118 -0
  95. package/src/widgets/actions/download/exports.test.tsx +275 -0
  96. package/src/widgets/actions/download/exports.tsx +103 -0
  97. package/src/widgets/actions/download/types.ts +21 -0
  98. package/src/widgets/actions/fullscreen/fullscreen.test.tsx +269 -0
  99. package/src/widgets/actions/fullscreen/fullscreen.tsx +82 -0
  100. package/src/widgets/actions/fullscreen/styles.ts +17 -0
  101. package/src/widgets/actions/fullscreen/types.ts +27 -0
  102. package/src/widgets/actions/index.ts +51 -0
  103. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +186 -0
  104. package/src/widgets/actions/lock-selection/lock-selection.tsx +133 -0
  105. package/src/widgets/actions/lock-selection/types.ts +41 -0
  106. package/src/widgets/actions/relative-data/relative-data.test.tsx +267 -0
  107. package/src/widgets/actions/relative-data/relative-data.tsx +133 -0
  108. package/src/widgets/actions/relative-data/style.ts +9 -0
  109. package/src/widgets/actions/relative-data/types.ts +31 -0
  110. package/src/widgets/actions/relative-data/utils.test.ts +223 -0
  111. package/src/widgets/actions/relative-data/utils.ts +58 -0
  112. package/src/widgets/actions/searcher/searcher-toggle.test.tsx +354 -0
  113. package/src/widgets/actions/searcher/searcher-toggle.tsx +73 -0
  114. package/src/widgets/actions/searcher/searcher.tsx +205 -0
  115. package/src/widgets/actions/searcher/types.ts +72 -0
  116. package/src/widgets/actions/shared/styles.ts +12 -0
  117. package/src/widgets/actions/stack-toggle/grouped-bar-chart-icon.tsx +14 -0
  118. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +270 -0
  119. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +146 -0
  120. package/src/widgets/actions/stack-toggle/types.ts +29 -0
  121. package/src/widgets/actions/zoom-toggle/index.ts +2 -0
  122. package/src/widgets/actions/zoom-toggle/style.ts +14 -0
  123. package/src/widgets/actions/zoom-toggle/types.ts +44 -0
  124. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +186 -0
  125. package/src/widgets/bar/config.ts +122 -0
  126. package/src/widgets/bar/index.ts +9 -0
  127. package/src/widgets/bar/skeleton.tsx +60 -0
  128. package/src/widgets/bar/style.ts +33 -0
  129. package/src/widgets/bar/types.ts +16 -0
  130. package/src/widgets/category/category-ui.test.tsx +399 -0
  131. package/src/widgets/category/category-ui.tsx +156 -0
  132. package/src/widgets/category/components/category-bar.tsx +28 -0
  133. package/src/widgets/category/components/category-legend.tsx +30 -0
  134. package/src/widgets/category/components/category-row-multi.tsx +50 -0
  135. package/src/widgets/category/components/category-row-other.tsx +23 -0
  136. package/src/widgets/category/components/category-row-single.tsx +47 -0
  137. package/src/widgets/category/components/index.ts +14 -0
  138. package/src/widgets/category/config.ts +85 -0
  139. package/src/widgets/category/index.ts +30 -0
  140. package/src/widgets/category/skeleton.tsx +24 -0
  141. package/src/widgets/category/style.ts +133 -0
  142. package/src/widgets/category/types.ts +40 -0
  143. package/src/widgets/echart/const.ts +1 -0
  144. package/src/widgets/echart/echart-ui.test.tsx +537 -0
  145. package/src/widgets/echart/echart-ui.tsx +92 -0
  146. package/src/widgets/echart/echart.test.tsx +562 -0
  147. package/src/widgets/echart/echart.tsx +68 -0
  148. package/src/widgets/echart/index.ts +16 -0
  149. package/src/widgets/echart/options.ts +53 -0
  150. package/src/widgets/echart/types.ts +45 -0
  151. package/src/widgets/echart/utils.ts +169 -0
  152. package/src/widgets/error/error.test.tsx +331 -0
  153. package/src/widgets/error/error.tsx +40 -0
  154. package/src/widgets/error/index.ts +2 -0
  155. package/src/widgets/error/types.ts +14 -0
  156. package/src/widgets/formula/components/item.test.tsx +249 -0
  157. package/src/widgets/formula/components/item.tsx +18 -0
  158. package/src/widgets/formula/components/prefix.test.tsx +341 -0
  159. package/src/widgets/formula/components/prefix.tsx +18 -0
  160. package/src/widgets/formula/components/row.test.tsx +364 -0
  161. package/src/widgets/formula/components/row.tsx +21 -0
  162. package/src/widgets/formula/components/series.tsx +34 -0
  163. package/src/widgets/formula/components/suffix.test.tsx +383 -0
  164. package/src/widgets/formula/components/suffix.tsx +28 -0
  165. package/src/widgets/formula/components/value.test.tsx +329 -0
  166. package/src/widgets/formula/components/value.tsx +29 -0
  167. package/src/widgets/formula/config.ts +27 -0
  168. package/src/widgets/formula/formula-ui.test.tsx +399 -0
  169. package/src/widgets/formula/formula-ui.tsx +27 -0
  170. package/src/widgets/formula/index.ts +24 -0
  171. package/src/widgets/formula/serializer.test.tsx +144 -0
  172. package/src/widgets/formula/serializer.ts +28 -0
  173. package/src/widgets/formula/skeleton.tsx +10 -0
  174. package/src/widgets/formula/style.ts +23 -0
  175. package/src/widgets/formula/types.ts +50 -0
  176. package/src/widgets/histogram/config.ts +143 -0
  177. package/src/widgets/histogram/index.ts +8 -0
  178. package/src/widgets/histogram/skeleton.tsx +52 -0
  179. package/src/widgets/histogram/style.ts +8 -0
  180. package/src/widgets/histogram/types.ts +17 -0
  181. package/src/widgets/index.ts +25 -0
  182. package/src/widgets/loader/index.ts +4 -0
  183. package/src/widgets/loader/loader.tsx +70 -0
  184. package/src/widgets/loader/types.ts +11 -0
  185. package/src/widgets/loader/utils.test.ts +112 -0
  186. package/src/widgets/loader/utils.ts +35 -0
  187. package/src/widgets/markdown/config.ts +18 -0
  188. package/src/widgets/markdown/index.ts +14 -0
  189. package/src/widgets/markdown/markdown-ui.test.tsx +341 -0
  190. package/src/widgets/markdown/markdown-ui.tsx +52 -0
  191. package/src/widgets/markdown/markdown.tsx +20 -0
  192. package/src/widgets/markdown/skeleton.tsx +12 -0
  193. package/src/widgets/markdown/style.ts +28 -0
  194. package/src/widgets/markdown/types.ts +28 -0
  195. package/src/widgets/no-data/index.ts +2 -0
  196. package/src/widgets/no-data/no-data.test.tsx +447 -0
  197. package/src/widgets/no-data/no-data.tsx +116 -0
  198. package/src/widgets/no-data/style.ts +18 -0
  199. package/src/widgets/no-data/types.ts +72 -0
  200. package/src/widgets/note/index.ts +2 -0
  201. package/src/widgets/note/note.test.tsx +391 -0
  202. package/src/widgets/note/note.tsx +114 -0
  203. package/src/widgets/note/style.ts +29 -0
  204. package/src/widgets/note/types.ts +9 -0
  205. package/src/widgets/pie/config.ts +177 -0
  206. package/src/widgets/pie/index.ts +8 -0
  207. package/src/widgets/pie/skeleton.tsx +70 -0
  208. package/src/widgets/pie/style.ts +8 -0
  209. package/src/widgets/pie/types.ts +16 -0
  210. package/src/widgets/range/components/range-item.tsx +213 -0
  211. package/src/widgets/range/config.ts +10 -0
  212. package/src/widgets/range/index.ts +16 -0
  213. package/src/widgets/range/range-ui.test.tsx +203 -0
  214. package/src/widgets/range/range-ui.tsx +11 -0
  215. package/src/widgets/range/serializer.test.ts +70 -0
  216. package/src/widgets/range/serializer.ts +27 -0
  217. package/src/widgets/range/skeleton.tsx +14 -0
  218. package/src/widgets/range/style.ts +37 -0
  219. package/src/widgets/range/types.ts +39 -0
  220. package/src/widgets/scatterplot/config.ts +138 -0
  221. package/src/widgets/scatterplot/index.ts +8 -0
  222. package/src/widgets/scatterplot/skeleton.tsx +59 -0
  223. package/src/widgets/scatterplot/style.ts +21 -0
  224. package/src/widgets/scatterplot/types.ts +17 -0
  225. package/src/widgets/selection-summary/index.ts +6 -0
  226. package/src/widgets/selection-summary/selection-summary.tsx +46 -0
  227. package/src/widgets/selection-summary/style.ts +10 -0
  228. package/src/widgets/selection-summary/types.ts +14 -0
  229. package/src/widgets/skeleton-loader/index.ts +2 -0
  230. package/src/widgets/skeleton-loader/skeleton-loader.test.tsx +139 -0
  231. package/src/widgets/skeleton-loader/skeleton-loader.tsx +28 -0
  232. package/src/widgets/skeleton-loader/types.ts +8 -0
  233. package/src/widgets/spread/components/max-value.tsx +29 -0
  234. package/src/widgets/spread/components/min-value.tsx +29 -0
  235. package/src/widgets/spread/components/separator.tsx +6 -0
  236. package/src/widgets/spread/config.ts +34 -0
  237. package/src/widgets/spread/index.ts +23 -0
  238. package/src/widgets/spread/skeleton.tsx +10 -0
  239. package/src/widgets/spread/spread-ui.test.tsx +368 -0
  240. package/src/widgets/spread/spread-ui.tsx +29 -0
  241. package/src/widgets/spread/style.ts +22 -0
  242. package/src/widgets/spread/types.ts +47 -0
  243. package/src/widgets/stores/index.ts +9 -0
  244. package/src/widgets/stores/types.ts +192 -0
  245. package/src/widgets/stores/widget-store.test.ts +601 -0
  246. package/src/widgets/stores/widget-store.ts +239 -0
  247. package/src/widgets/subheader/index.ts +3 -0
  248. package/src/widgets/subheader/style.ts +20 -0
  249. package/src/widgets/subheader/subheader.test.tsx +45 -0
  250. package/src/widgets/subheader/subheader.tsx +16 -0
  251. package/src/widgets/subheader/types.ts +11 -0
  252. package/src/widgets/table/components/cell-header.tsx +58 -0
  253. package/src/widgets/table/components/cell.tsx +80 -0
  254. package/src/widgets/table/components/index.ts +4 -0
  255. package/src/widgets/table/components/pagination-actions.tsx +67 -0
  256. package/src/widgets/table/components/pagination.tsx +41 -0
  257. package/src/widgets/table/components/row.tsx +60 -0
  258. package/src/widgets/table/config.ts +71 -0
  259. package/src/widgets/table/helpers.test.ts +244 -0
  260. package/src/widgets/table/helpers.ts +107 -0
  261. package/src/widgets/table/hooks/index.ts +7 -0
  262. package/src/widgets/table/hooks/use-pagination.test.ts +294 -0
  263. package/src/widgets/table/hooks/use-pagination.ts +155 -0
  264. package/src/widgets/table/hooks/use-selection.test.ts +504 -0
  265. package/src/widgets/table/hooks/use-selection.ts +189 -0
  266. package/src/widgets/table/hooks/use-sort.test.ts +296 -0
  267. package/src/widgets/table/hooks/use-sort.ts +138 -0
  268. package/src/widgets/table/index.ts +53 -0
  269. package/src/widgets/table/serializer.ts +54 -0
  270. package/src/widgets/table/skeleton.tsx +48 -0
  271. package/src/widgets/table/style.ts +34 -0
  272. package/src/widgets/table/table-ui.tsx +64 -0
  273. package/src/widgets/table/types.ts +223 -0
  274. package/src/widgets/timeseries/config.ts +135 -0
  275. package/src/widgets/timeseries/index.ts +8 -0
  276. package/src/widgets/timeseries/skeleton.tsx +55 -0
  277. package/src/widgets/timeseries/style.ts +36 -0
  278. package/src/widgets/timeseries/types.ts +17 -0
  279. package/src/widgets/toolbar-actions/index.ts +6 -0
  280. package/src/widgets/toolbar-actions/styles.ts +38 -0
  281. package/src/widgets/toolbar-actions/toolbar-actions.test.tsx +691 -0
  282. package/src/widgets/toolbar-actions/toolbar-actions.tsx +145 -0
  283. package/src/widgets/toolbar-actions/types.ts +60 -0
  284. package/src/widgets/wrapper/components/actions.test.tsx +101 -0
  285. package/src/widgets/wrapper/components/actions.tsx +30 -0
  286. package/src/widgets/wrapper/components/options.test.tsx +323 -0
  287. package/src/widgets/wrapper/components/options.tsx +73 -0
  288. package/src/widgets/wrapper/components/title.test.tsx +126 -0
  289. package/src/widgets/wrapper/components/title.tsx +32 -0
  290. package/src/widgets/wrapper/index.ts +16 -0
  291. package/src/widgets/wrapper/styles.ts +98 -0
  292. package/src/widgets/wrapper/types.ts +55 -0
  293. package/src/widgets/wrapper/wrapper-ui.test.tsx +232 -0
  294. package/src/widgets/wrapper/wrapper-ui.tsx +57 -0
  295. package/src/widgets/wrapper/wrapper.test.tsx +365 -0
  296. package/src/widgets/wrapper/wrapper.tsx +50 -0
  297. package/dist/lasso-tool-BwRzEW7k.js.map +0 -1
  298. package/dist/types/common/common.d.ts +0 -3
  299. package/dist/types/common/index.d.ts +0 -26
  300. package/dist/types/common/lasso-tools.d.ts +0 -36
  301. package/dist/types/common/measurement-tools.d.ts +0 -65
@@ -0,0 +1,368 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { SpreadUI } from './spread-ui'
4
+ import { useWidgetStore } from '../stores/widget-store'
5
+ import type { SpreadWidgetState } from './types'
6
+
7
+ describe('SpreadUI', () => {
8
+ beforeEach(() => {
9
+ useWidgetStore.getState().clearWidgets()
10
+ })
11
+
12
+ test('renders empty container when widget does not exist', () => {
13
+ const { container } = render(<SpreadUI id='non-existent' />)
14
+ // Box wrapper is rendered but Row returns null for missing widget
15
+ expect(container.firstChild?.childNodes.length).toBe(0)
16
+ })
17
+
18
+ test('renders with single spread data item', () => {
19
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
20
+ type: 'spread',
21
+ data: [{ min: 2.1, max: 2.23, suffix: '€' }],
22
+ })
23
+
24
+ render(<SpreadUI id='test-spread' />)
25
+
26
+ expect(screen.getByText('2.1')).toBeTruthy()
27
+ expect(screen.getByText('-')).toBeTruthy()
28
+ expect(screen.getByText('2.23')).toBeTruthy()
29
+ expect(screen.getByText('€')).toBeTruthy()
30
+ })
31
+
32
+ test('renders with multiple spread data items', () => {
33
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
34
+ type: 'spread',
35
+ data: [
36
+ { min: 1.5, max: 2.5, prefix: '$' },
37
+ { min: 100, max: 200, suffix: '%' },
38
+ { min: 0, max: 10 },
39
+ ],
40
+ })
41
+
42
+ render(<SpreadUI id='test-spread' />)
43
+
44
+ expect(screen.getByText('$')).toBeTruthy()
45
+ expect(screen.getByText('1.5')).toBeTruthy()
46
+ expect(screen.getByText('2.5')).toBeTruthy()
47
+ expect(screen.getByText('100')).toBeTruthy()
48
+ expect(screen.getByText('200')).toBeTruthy()
49
+ expect(screen.getByText('%')).toBeTruthy()
50
+ expect(screen.getByText('0')).toBeTruthy()
51
+ expect(screen.getByText('10')).toBeTruthy()
52
+ })
53
+
54
+ test('renders prefix for each item', () => {
55
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
56
+ type: 'spread',
57
+ data: [
58
+ { min: 1, max: 10, prefix: 'Range 1:' },
59
+ { min: 20, max: 30, prefix: 'Range 2:' },
60
+ ],
61
+ })
62
+
63
+ render(<SpreadUI id='test-spread' />)
64
+
65
+ expect(screen.getByText('Range 1:')).toBeTruthy()
66
+ expect(screen.getByText('Range 2:')).toBeTruthy()
67
+ })
68
+
69
+ test('renders separator between min and max values', () => {
70
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
71
+ type: 'spread',
72
+ data: [{ min: 5, max: 15 }],
73
+ })
74
+
75
+ render(<SpreadUI id='test-spread' />)
76
+
77
+ // Check for separator "-"
78
+ const separators = screen.getAllByText('-')
79
+ expect(separators.length).toBe(1)
80
+ })
81
+
82
+ test('renders in correct order: Prefix, MinValue, Separator, MaxValue, Suffix', () => {
83
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
84
+ type: 'spread',
85
+ data: [{ min: 10, max: 20, prefix: '$', suffix: 'USD' }],
86
+ })
87
+
88
+ const { container } = render(<SpreadUI id='test-spread' />)
89
+
90
+ const items = container.querySelectorAll('.MuiTypography-root')
91
+ expect(items.length).toBe(5)
92
+ expect(items[0]?.textContent).toBe('$')
93
+ expect(items[1]?.textContent).toBe('10')
94
+ expect(items[2]?.textContent).toBe('-')
95
+ expect(items[3]?.textContent).toBe('20')
96
+ expect(items[4]?.textContent).toBe('USD')
97
+ })
98
+
99
+ test('handles data without prefix', () => {
100
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
101
+ type: 'spread',
102
+ data: [{ min: 5, max: 15, suffix: 'km' }],
103
+ })
104
+
105
+ render(<SpreadUI id='test-spread' />)
106
+
107
+ expect(screen.getByText('5')).toBeTruthy()
108
+ expect(screen.getByText('-')).toBeTruthy()
109
+ expect(screen.getByText('15')).toBeTruthy()
110
+ expect(screen.getByText('km')).toBeTruthy()
111
+ })
112
+
113
+ test('handles data without suffix', () => {
114
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
115
+ type: 'spread',
116
+ data: [{ min: 5, max: 15, prefix: '$' }],
117
+ })
118
+
119
+ render(<SpreadUI id='test-spread' />)
120
+
121
+ expect(screen.getByText('$')).toBeTruthy()
122
+ expect(screen.getByText('5')).toBeTruthy()
123
+ expect(screen.getByText('-')).toBeTruthy()
124
+ expect(screen.getByText('15')).toBeTruthy()
125
+ })
126
+
127
+ test('handles data with only min and max values', () => {
128
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
129
+ type: 'spread',
130
+ data: [{ min: 5, max: 15 }],
131
+ })
132
+
133
+ render(<SpreadUI id='test-spread' />)
134
+
135
+ expect(screen.getByText('5')).toBeTruthy()
136
+ expect(screen.getByText('-')).toBeTruthy()
137
+ expect(screen.getByText('15')).toBeTruthy()
138
+ })
139
+
140
+ test('handles empty data array', () => {
141
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
142
+ type: 'spread',
143
+ data: [],
144
+ })
145
+
146
+ const { container } = render(<SpreadUI id='test-spread' />)
147
+
148
+ // Row component returns null for empty data, Box wrapper still rendered
149
+ expect(container.firstChild?.childNodes.length).toBe(0)
150
+ })
151
+
152
+ test('handles undefined data', () => {
153
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
154
+ type: 'spread',
155
+ })
156
+
157
+ const { container } = render(<SpreadUI id='test-spread' />)
158
+
159
+ // Row component returns null for undefined data, Box wrapper still rendered
160
+ expect(container.firstChild?.childNodes.length).toBe(0)
161
+ })
162
+
163
+ test('renders with data containing colors', () => {
164
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
165
+ type: 'spread',
166
+ data: [
167
+ { min: 1, max: 10, color: '#FF0000' },
168
+ { min: 20, max: 30, color: '#00FF00' },
169
+ ],
170
+ })
171
+
172
+ render(<SpreadUI id='test-spread' />)
173
+
174
+ expect(screen.getByText('1')).toBeTruthy()
175
+ expect(screen.getByText('10')).toBeTruthy()
176
+ expect(screen.getByText('20')).toBeTruthy()
177
+ expect(screen.getByText('30')).toBeTruthy()
178
+ })
179
+
180
+ test('renders with negative min value', () => {
181
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
182
+ type: 'spread',
183
+ data: [{ min: -10, max: 5 }],
184
+ })
185
+
186
+ render(<SpreadUI id='test-spread' />)
187
+
188
+ expect(screen.getByText('-10')).toBeTruthy()
189
+ expect(screen.getByText('-')).toBeTruthy()
190
+ expect(screen.getByText('5')).toBeTruthy()
191
+ })
192
+
193
+ test('renders with zero values', () => {
194
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
195
+ type: 'spread',
196
+ data: [{ min: 0, max: 0 }],
197
+ })
198
+
199
+ render(<SpreadUI id='test-spread' />)
200
+
201
+ const zeros = screen.getAllByText('0')
202
+ expect(zeros.length).toBe(2) // min and max both are 0
203
+ })
204
+
205
+ test('renders with decimal values', () => {
206
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
207
+ type: 'spread',
208
+ data: [{ min: 2.1, max: 2.23, suffix: '€' }],
209
+ })
210
+
211
+ render(<SpreadUI id='test-spread' />)
212
+
213
+ expect(screen.getByText('2.1')).toBeTruthy()
214
+ expect(screen.getByText('2.23')).toBeTruthy()
215
+ expect(screen.getByText('€')).toBeTruthy()
216
+ })
217
+
218
+ test('renders with large values', () => {
219
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
220
+ type: 'spread',
221
+ data: [{ min: 1000000, max: 2000000, prefix: '$' }],
222
+ })
223
+
224
+ render(<SpreadUI id='test-spread' />)
225
+
226
+ expect(screen.getByText('$')).toBeTruthy()
227
+ expect(screen.getByText('1000000')).toBeTruthy()
228
+ expect(screen.getByText('2000000')).toBeTruthy()
229
+ })
230
+
231
+ test('updates when widget data changes', () => {
232
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
233
+ type: 'spread',
234
+ data: [{ min: 1, max: 10 }],
235
+ })
236
+
237
+ const { rerender } = render(<SpreadUI id='test-spread' />)
238
+ expect(screen.getByText('1')).toBeTruthy()
239
+ expect(screen.getByText('10')).toBeTruthy()
240
+
241
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
242
+ data: [{ min: 20, max: 30 }],
243
+ })
244
+
245
+ rerender(<SpreadUI id='test-spread' />)
246
+ expect(screen.queryByText('1')).toBeNull()
247
+ expect(screen.queryByText('10')).toBeNull()
248
+ expect(screen.getByText('20')).toBeTruthy()
249
+ expect(screen.getByText('30')).toBeTruthy()
250
+ })
251
+
252
+ test('handles widget removal gracefully', () => {
253
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
254
+ type: 'spread',
255
+ data: [{ min: 1, max: 10 }],
256
+ })
257
+
258
+ const { container, rerender } = render(<SpreadUI id='test-spread' />)
259
+ expect(screen.getByText('1')).toBeTruthy()
260
+
261
+ useWidgetStore.getState().removeWidget('test-spread')
262
+
263
+ rerender(<SpreadUI id='test-spread' />)
264
+ // Box wrapper still rendered but Row returns null
265
+ expect(container.firstChild?.childNodes.length).toBe(0)
266
+ })
267
+
268
+ test('renders multiple rows for multiple data items', () => {
269
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
270
+ type: 'spread',
271
+ data: [
272
+ { min: 1, max: 10, prefix: 'A' },
273
+ { min: 20, max: 30, prefix: 'B' },
274
+ { min: 40, max: 50, prefix: 'C' },
275
+ ],
276
+ })
277
+
278
+ const { container } = render(<SpreadUI id='test-spread' />)
279
+
280
+ // Each data item should create a row
281
+ const rows = container.querySelectorAll('[class*="MuiBox-root"]')
282
+ expect(rows.length).toBeGreaterThan(0)
283
+ })
284
+
285
+ test('renders with series configuration', () => {
286
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
287
+ type: 'spread',
288
+ data: [{ min: 10, max: 100, suffix: '€' }],
289
+ series: [{ name: 'Revenue', color: '#FF5733' }],
290
+ })
291
+
292
+ render(<SpreadUI id='test-spread' />)
293
+
294
+ // Series should display the first letter of the name
295
+ expect(screen.getByText('R')).toBeTruthy()
296
+ expect(screen.getByText('10')).toBeTruthy()
297
+ expect(screen.getByText('100')).toBeTruthy()
298
+ expect(screen.getByText('€')).toBeTruthy()
299
+ })
300
+
301
+ test('renders without series when not provided', () => {
302
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
303
+ type: 'spread',
304
+ data: [{ min: 5, max: 25 }],
305
+ })
306
+
307
+ const { container } = render(<SpreadUI id='test-spread' />)
308
+
309
+ // Should not have any Avatar elements (series)
310
+ const avatars = container.querySelectorAll('.MuiAvatar-root')
311
+ expect(avatars.length).toBe(0)
312
+ })
313
+
314
+ test('renders series with default color when color not provided', () => {
315
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
316
+ type: 'spread',
317
+ data: [{ min: 0, max: 50 }],
318
+ series: [{ name: 'Test' }],
319
+ })
320
+
321
+ render(<SpreadUI id='test-spread' />)
322
+
323
+ // Series should display the first letter
324
+ expect(screen.getByText('T')).toBeTruthy()
325
+ })
326
+
327
+ test('renders with array of series matching data indices', () => {
328
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
329
+ type: 'spread',
330
+ data: [
331
+ { min: 10, max: 100 },
332
+ { min: 20, max: 200 },
333
+ { min: 30, max: 300 },
334
+ ],
335
+ series: [
336
+ { name: 'Alpha', color: '#FF0000' },
337
+ { name: 'Beta', color: '#00FF00' },
338
+ { name: 'Gamma', color: '#0000FF' },
339
+ ],
340
+ })
341
+
342
+ render(<SpreadUI id='test-spread' />)
343
+
344
+ // Each series should display its first letter
345
+ expect(screen.getByText('A')).toBeTruthy()
346
+ expect(screen.getByText('B')).toBeTruthy()
347
+ expect(screen.getByText('G')).toBeTruthy()
348
+ })
349
+
350
+ test('renders partial series array - only shows series for available indices', () => {
351
+ useWidgetStore.getState().setWidget<SpreadWidgetState>('test-spread', {
352
+ type: 'spread',
353
+ data: [
354
+ { min: 10, max: 100 },
355
+ { min: 20, max: 200 },
356
+ { min: 30, max: 300 },
357
+ ],
358
+ series: [{ name: 'Only First', color: '#FF0000' }],
359
+ })
360
+
361
+ const { container } = render(<SpreadUI id='test-spread' />)
362
+
363
+ // Only one series avatar should be rendered
364
+ const avatars = container.querySelectorAll('.MuiAvatar-root')
365
+ expect(avatars.length).toBe(1)
366
+ expect(screen.getByText('O')).toBeTruthy()
367
+ })
368
+ })
@@ -0,0 +1,29 @@
1
+ import type { SpreadUIProps } from './types'
2
+
3
+ import { Separator } from './components/separator'
4
+ import { MinValue } from './components/min-value'
5
+ import { MaxValue } from './components/max-value'
6
+ import { Prefix, Row, Suffix, Series } from '../formula'
7
+ import { useWidgetRef } from '../../hooks'
8
+ import { Box } from '@mui/material'
9
+
10
+ export function SpreadUI(props: SpreadUIProps) {
11
+ const ref = useWidgetRef(props.id)
12
+
13
+ return (
14
+ <Box ref={ref}>
15
+ <Row id={props.id}>
16
+ {({ index }) => (
17
+ <>
18
+ <Series id={props.id} index={index} />
19
+ <Prefix id={props.id} index={index} />
20
+ <MinValue id={props.id} index={index} />
21
+ <Separator />
22
+ <MaxValue id={props.id} index={index} />
23
+ <Suffix id={props.id} index={index} />
24
+ </>
25
+ )}
26
+ </Row>
27
+ </Box>
28
+ )
29
+ }
@@ -0,0 +1,22 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ root: {
5
+ display: 'flex',
6
+ flexDirection: 'column',
7
+ gap: (theme: Theme) => theme.spacing(2),
8
+ },
9
+ item: {
10
+ '&[data-disabled="true"]': {
11
+ color: (theme: Theme) => theme.palette.text.disabled,
12
+ },
13
+ },
14
+ row: {
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ gap: (theme: Theme) => theme.spacing(0.25),
18
+ '& + &': {
19
+ marginTop: (theme: Theme) => theme.spacing(1),
20
+ },
21
+ },
22
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,47 @@
1
+ import type { TypographyProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState, WidgetsStoreProps } from '../stores/types'
4
+ import type { WrapperState } from '../wrapper/types'
5
+ import type { DownloadItem } from '../actions/download/types'
6
+ import type { SeriesConfig } from '../formula/types'
7
+
8
+ export interface SpreadUIProps {
9
+ id: WidgetsStoreProps['id']
10
+ }
11
+
12
+ export interface SpreadDataItem {
13
+ min: number
14
+ max: number
15
+ prefix?: string | ReactNode
16
+ suffix?: string | ReactNode
17
+ color?: string
18
+ }
19
+
20
+ export interface RowProps {
21
+ id: WidgetsStoreProps['id']
22
+ children: ReactNode | ((props: { index: number }) => ReactNode)
23
+ }
24
+
25
+ export interface ValueProps extends Omit<ItemProps, 'children'> {
26
+ id: WidgetsStoreProps['id']
27
+ index?: number
28
+ }
29
+
30
+ export interface ItemProps {
31
+ children: ReactNode
32
+ disabled?: boolean
33
+ TypographyProps?: TypographyProps
34
+ }
35
+
36
+ export type SpreadWidgetData = SpreadDataItem[]
37
+
38
+ export type SpreadWidgetState = BaseWidgetState<
39
+ WrapperState<SpreadWidgetConfig> & { data: SpreadWidgetData }
40
+ >
41
+
42
+ export interface SpreadWidgetConfig {
43
+ formatter?: (value: number) => string
44
+ series?: SeriesConfig[]
45
+ }
46
+
47
+ export type SpreadDownloadConfig = DownloadItem<SpreadWidgetData>[]
@@ -0,0 +1,9 @@
1
+ export { useWidgetStore } from './widget-store'
2
+ export type {
3
+ BaseWidgetState,
4
+ WidgetsStoreProps,
5
+ WidgetState,
6
+ WidgetStore,
7
+ WidgetStoreActions,
8
+ WidgetStoreState,
9
+ } from './types'
@@ -0,0 +1,192 @@
1
+ import type { RefObject } from 'react'
2
+
3
+ export interface WidgetsStoreProps {
4
+ /** Unique identifier for the widget */
5
+ id: string
6
+ /** Type of widget */
7
+ type: string
8
+ /** Widget data - flexible to accommodate different widget types */
9
+ data: unknown
10
+ /** Loading state */
11
+ isLoading: boolean
12
+ /** Fetching state (e.g., for async data) */
13
+ isFetching: boolean
14
+ /** Error message if any */
15
+ error?: {
16
+ title?: string
17
+ message?: string
18
+ }
19
+ /** Whether widget is visible */
20
+ visible?: boolean
21
+ /** Reference to the widget ui instance */
22
+ refUI?: RefObject<HTMLElement | null>
23
+ /** Registered tools for the widget's transformation pipeline */
24
+ registeredTools?: ToolRegistration[]
25
+ /** Formatter function for widget values */
26
+ formatter?: (value: number) => string
27
+ /** Locale for number formatting (e.g., 'en-US', 'es-ES', 'fr-FR') */
28
+ locale?: string
29
+ }
30
+
31
+ /**
32
+ * Tool transformation function type
33
+ * Can be synchronous or asynchronous to support remote operations
34
+ */
35
+ export type ToolTransformFunction = (
36
+ data: unknown,
37
+ config?: Record<string, unknown>,
38
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
39
+ ) => Promise<unknown> | unknown
40
+
41
+ /**
42
+ * Tool registration for widget pipeline
43
+ *
44
+ * @example Basic tool registration
45
+ * ```typescript
46
+ * registerTool(widgetId, {
47
+ * id: 'searcher',
48
+ * order: 10,
49
+ * enabled: true,
50
+ * fn: (data) => filterData(data),
51
+ * })
52
+ * ```
53
+ *
54
+ * @example Tool with dependencies
55
+ * ```typescript
56
+ * registerTool(widgetId, {
57
+ * id: 'lock-selection',
58
+ * order: 20,
59
+ * enabled: true,
60
+ * fn: (data) => lockData(data),
61
+ * disables: ['searcher', 'relative-data'], // Disable these when active
62
+ * })
63
+ * ```
64
+ *
65
+ * Dependency Management:
66
+ * - Multiple tools can disable the same target (reference counting)
67
+ * - Target is only re-enabled when ALL disabling tools are inactive
68
+ * - Original enabled state is preserved and restored
69
+ * - Circular dependencies are detected and throw errors
70
+ */
71
+ export interface ToolRegistration {
72
+ /** Unique tool identifier (e.g., 'searcher', 'relative-data') */
73
+ id: string
74
+ /** Execution priority - lower numbers execute first */
75
+ order: number
76
+ /** Transformation function */
77
+ fn: ToolTransformFunction
78
+ /** Whether tool is currently enabled */
79
+ enabled: boolean
80
+ /** Tool-specific configuration */
81
+ config?: Record<string, unknown>
82
+ /**
83
+ * Array of tool IDs to disable when this tool is active.
84
+ * During pipeline execution, if this tool is enabled, any tools listed
85
+ * in this array will be excluded from the pipeline.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * disables: ['searcher', 'relative-data']
90
+ * ```
91
+ */
92
+ disables?: string[]
93
+ }
94
+
95
+ /**
96
+ * Base widget state interface
97
+ */
98
+ export type BaseWidgetState<T> = WidgetsStoreProps & T
99
+
100
+ /**
101
+ * Union type for all widget states
102
+ */
103
+ export type WidgetState = BaseWidgetState<unknown>
104
+
105
+ /**
106
+ * Widget store state with tool registration
107
+ */
108
+ export interface WidgetStoreState {
109
+ /** Map of widget id to widget state */
110
+ widgets: Record<string, WidgetState>
111
+ }
112
+
113
+ /**
114
+ * Widget store actions
115
+ */
116
+ export interface WidgetStoreActions {
117
+ /**
118
+ * Add or update a widget in the store
119
+ * @param id - Widget ID
120
+ * @param widget - Widget state properties to merge (accepts any object structure)
121
+ * @template T - Type of the widget state for type-safe access
122
+ */
123
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
+ setWidget: <T = any>(id: WidgetState['id'], widget: Partial<T>) => void
125
+
126
+ /**
127
+ * Remove a widget from the store
128
+ * @param id - Widget ID to remove
129
+ */
130
+ removeWidget: (id: WidgetState['id']) => void
131
+
132
+ /**
133
+ * Clear all widgets
134
+ */
135
+ clearWidgets: () => void
136
+
137
+ /**
138
+ * Get widget by ID
139
+ * @param id - Widget ID
140
+ * @returns Widget state or undefined if not found
141
+ */
142
+ getWidget: <T extends WidgetState = WidgetState>(
143
+ id: WidgetState['id'],
144
+ ) => T | undefined
145
+
146
+ /**
147
+ * Register a tool with the widget's transformation pipeline
148
+ * @param widgetId - Widget ID
149
+ * @param tool - Tool registration object
150
+ */
151
+ registerTool: (widgetId: string, tool: ToolRegistration) => void
152
+
153
+ /**
154
+ * Unregister a tool from the widget's transformation pipeline
155
+ * @param widgetId - Widget ID
156
+ * @param toolId - Tool ID to remove
157
+ */
158
+ unregisterTool: (widgetId: string, toolId: string) => void
159
+
160
+ /**
161
+ * Update tool configuration
162
+ * @param widgetId - Widget ID
163
+ * @param toolId - Tool ID
164
+ * @param config - New configuration to merge
165
+ */
166
+ updateToolConfig: (
167
+ widgetId: string,
168
+ toolId: string,
169
+ config: Record<string, unknown>,
170
+ ) => void
171
+
172
+ /**
173
+ * Set tool enabled state
174
+ * @param widgetId - Widget ID
175
+ * @param toolId - Tool ID
176
+ * @param enabled - Whether tool should be enabled
177
+ */
178
+ setToolEnabled: (widgetId: string, toolId: string, enabled: boolean) => void
179
+
180
+ /**
181
+ * Execute the tool transformation pipeline
182
+ * Supports both synchronous and asynchronous tools
183
+ * @param widgetId - Widget ID
184
+ * @param sourceData - Original data to transform
185
+ */
186
+ executeToolPipeline: (widgetId: string, sourceData: unknown) => Promise<void>
187
+ }
188
+
189
+ /**
190
+ * Complete widget store interface
191
+ */
192
+ export type WidgetStore = WidgetStoreState & WidgetStoreActions