@carto/ps-react-ui 4.3.2 → 4.3.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 (299) 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/toolbar-actions/toolbar-actions.d.ts +6 -3
  8. package/dist/widgets/actions.js +1 -1
  9. package/dist/widgets/bar.js +1 -1
  10. package/dist/widgets/category.js +1 -1
  11. package/dist/widgets/formula.js +1 -1
  12. package/dist/widgets/histogram.js +1 -1
  13. package/dist/widgets/markdown.js +1 -1
  14. package/dist/widgets/pie.js +1 -1
  15. package/dist/widgets/scatterplot.js +1 -1
  16. package/dist/widgets/spread.js +1 -1
  17. package/dist/widgets/table.js +1 -1
  18. package/dist/widgets/timeseries.js +1 -1
  19. package/dist/widgets/toolbar-actions.js +1343 -1332
  20. package/dist/widgets/toolbar-actions.js.map +1 -1
  21. package/dist/widgets/wrapper.js +1 -1
  22. package/package.json +8 -3
  23. package/src/components/basemaps/basemaps.test.tsx +196 -0
  24. package/src/components/basemaps/basemaps.tsx +128 -0
  25. package/src/components/basemaps/const.ts +13 -0
  26. package/src/components/basemaps/group-wrapper.test.tsx +38 -0
  27. package/src/components/basemaps/group-wrapper.tsx +28 -0
  28. package/src/components/basemaps/group.test.tsx +52 -0
  29. package/src/components/basemaps/group.tsx +42 -0
  30. package/src/components/basemaps/header.test.tsx +54 -0
  31. package/src/components/basemaps/header.tsx +36 -0
  32. package/src/components/basemaps/styles.ts +76 -0
  33. package/src/components/basemaps/types.ts +30 -0
  34. package/src/components/common-types.ts +1 -0
  35. package/src/components/geolocation-controls/const.ts +6 -0
  36. package/src/components/geolocation-controls/geolocation-controls.test.tsx +133 -0
  37. package/src/components/geolocation-controls/geolocation-controls.tsx +95 -0
  38. package/src/components/geolocation-controls/types.ts +17 -0
  39. package/src/components/index.ts +64 -0
  40. package/src/components/lasso-tool/chip.tsx +37 -0
  41. package/src/components/lasso-tool/const.tsx +70 -0
  42. package/src/components/lasso-tool/icons.tsx +91 -0
  43. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +168 -0
  44. package/src/components/lasso-tool/lasso-tool-inline.tsx +245 -0
  45. package/src/components/lasso-tool/lasso-tool.test.tsx +212 -0
  46. package/src/components/lasso-tool/lasso-tool.tsx +479 -0
  47. package/src/components/lasso-tool/styles.ts +143 -0
  48. package/src/components/lasso-tool/types.ts +114 -0
  49. package/src/components/list-data/list-data-skeleton.test.tsx +10 -0
  50. package/src/components/list-data/list-data-skeleton.tsx +40 -0
  51. package/src/components/list-data/list-data.test.tsx +94 -0
  52. package/src/components/list-data/list-data.tsx +106 -0
  53. package/src/components/list-data/styles.ts +37 -0
  54. package/src/components/list-data/types.ts +25 -0
  55. package/src/components/measurement-tools/const.tsx +108 -0
  56. package/src/components/measurement-tools/icons.tsx +54 -0
  57. package/src/components/measurement-tools/measurement-tools.test.tsx +165 -0
  58. package/src/components/measurement-tools/measurement-tools.tsx +443 -0
  59. package/src/components/measurement-tools/styles.ts +91 -0
  60. package/src/components/measurement-tools/types.ts +77 -0
  61. package/src/components/no-data-alert/no-data-alert.test.tsx +31 -0
  62. package/src/components/no-data-alert/no-data-alert.tsx +59 -0
  63. package/src/components/responsive-drawer/responsive-drawer.test.tsx +91 -0
  64. package/src/components/responsive-drawer/responsive-drawer.tsx +53 -0
  65. package/src/components/smart-tooltip/smart-tooltip.test.tsx +168 -0
  66. package/src/components/smart-tooltip/smart-tooltip.tsx +40 -0
  67. package/src/components/tooltip/tooltip.test.tsx +86 -0
  68. package/src/components/tooltip/tooltip.tsx +30 -0
  69. package/src/components/types.ts +1 -0
  70. package/src/components/zoom-controls/styles.ts +27 -0
  71. package/src/components/zoom-controls/types.ts +19 -0
  72. package/src/components/zoom-controls/zoom-controls.test.tsx +101 -0
  73. package/src/components/zoom-controls/zoom-controls.tsx +114 -0
  74. package/src/hooks/index.ts +2 -0
  75. package/src/hooks/use-debounce.ts +55 -0
  76. package/src/hooks/use-skeleton.test.tsx +32 -0
  77. package/src/hooks/use-skeleton.ts +19 -0
  78. package/src/hooks/use-widget-ref.ts +33 -0
  79. package/src/widgets/README.md +277 -0
  80. package/src/widgets/_shared/chart-config/config-factory.ts +67 -0
  81. package/src/widgets/_shared/chart-config/csv-modifiers.ts +56 -0
  82. package/src/widgets/_shared/chart-config/index.ts +21 -0
  83. package/src/widgets/_shared/chart-config/option-builders.ts +203 -0
  84. package/src/widgets/_shared/skeleton/index.ts +5 -0
  85. package/src/widgets/_shared/skeleton/styles.ts +20 -0
  86. package/src/widgets/actions/change-column/change-column-icon.tsx +10 -0
  87. package/src/widgets/actions/change-column/change-column.test.tsx +163 -0
  88. package/src/widgets/actions/change-column/change-column.tsx +141 -0
  89. package/src/widgets/actions/change-column/sortable-column-item.tsx +49 -0
  90. package/src/widgets/actions/change-column/types.ts +20 -0
  91. package/src/widgets/actions/download/download.test.tsx +322 -0
  92. package/src/widgets/actions/download/download.tsx +118 -0
  93. package/src/widgets/actions/download/exports.test.tsx +275 -0
  94. package/src/widgets/actions/download/exports.tsx +103 -0
  95. package/src/widgets/actions/download/types.ts +21 -0
  96. package/src/widgets/actions/fullscreen/fullscreen.test.tsx +269 -0
  97. package/src/widgets/actions/fullscreen/fullscreen.tsx +82 -0
  98. package/src/widgets/actions/fullscreen/styles.ts +17 -0
  99. package/src/widgets/actions/fullscreen/types.ts +27 -0
  100. package/src/widgets/actions/index.ts +51 -0
  101. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +186 -0
  102. package/src/widgets/actions/lock-selection/lock-selection.tsx +133 -0
  103. package/src/widgets/actions/lock-selection/types.ts +41 -0
  104. package/src/widgets/actions/relative-data/relative-data.test.tsx +267 -0
  105. package/src/widgets/actions/relative-data/relative-data.tsx +133 -0
  106. package/src/widgets/actions/relative-data/style.ts +9 -0
  107. package/src/widgets/actions/relative-data/types.ts +31 -0
  108. package/src/widgets/actions/relative-data/utils.test.ts +223 -0
  109. package/src/widgets/actions/relative-data/utils.ts +58 -0
  110. package/src/widgets/actions/searcher/searcher-toggle.test.tsx +354 -0
  111. package/src/widgets/actions/searcher/searcher-toggle.tsx +73 -0
  112. package/src/widgets/actions/searcher/searcher.tsx +205 -0
  113. package/src/widgets/actions/searcher/types.ts +72 -0
  114. package/src/widgets/actions/shared/styles.ts +12 -0
  115. package/src/widgets/actions/stack-toggle/grouped-bar-chart-icon.tsx +14 -0
  116. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +270 -0
  117. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +146 -0
  118. package/src/widgets/actions/stack-toggle/types.ts +29 -0
  119. package/src/widgets/actions/zoom-toggle/index.ts +2 -0
  120. package/src/widgets/actions/zoom-toggle/style.ts +14 -0
  121. package/src/widgets/actions/zoom-toggle/types.ts +44 -0
  122. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +186 -0
  123. package/src/widgets/bar/config.ts +122 -0
  124. package/src/widgets/bar/index.ts +9 -0
  125. package/src/widgets/bar/skeleton.tsx +60 -0
  126. package/src/widgets/bar/style.ts +33 -0
  127. package/src/widgets/bar/types.ts +16 -0
  128. package/src/widgets/category/category-ui.test.tsx +399 -0
  129. package/src/widgets/category/category-ui.tsx +156 -0
  130. package/src/widgets/category/components/category-bar.tsx +28 -0
  131. package/src/widgets/category/components/category-legend.tsx +30 -0
  132. package/src/widgets/category/components/category-row-multi.tsx +50 -0
  133. package/src/widgets/category/components/category-row-other.tsx +23 -0
  134. package/src/widgets/category/components/category-row-single.tsx +47 -0
  135. package/src/widgets/category/components/index.ts +14 -0
  136. package/src/widgets/category/config.ts +85 -0
  137. package/src/widgets/category/index.ts +30 -0
  138. package/src/widgets/category/skeleton.tsx +24 -0
  139. package/src/widgets/category/style.ts +133 -0
  140. package/src/widgets/category/types.ts +40 -0
  141. package/src/widgets/echart/const.ts +1 -0
  142. package/src/widgets/echart/echart-ui.test.tsx +519 -0
  143. package/src/widgets/echart/echart-ui.tsx +80 -0
  144. package/src/widgets/echart/echart.test.tsx +537 -0
  145. package/src/widgets/echart/echart.tsx +60 -0
  146. package/src/widgets/echart/index.ts +16 -0
  147. package/src/widgets/echart/options.ts +53 -0
  148. package/src/widgets/echart/types.ts +41 -0
  149. package/src/widgets/echart/utils.ts +169 -0
  150. package/src/widgets/error/error.test.tsx +331 -0
  151. package/src/widgets/error/error.tsx +40 -0
  152. package/src/widgets/error/index.ts +2 -0
  153. package/src/widgets/error/types.ts +14 -0
  154. package/src/widgets/formula/components/item.test.tsx +249 -0
  155. package/src/widgets/formula/components/item.tsx +18 -0
  156. package/src/widgets/formula/components/prefix.test.tsx +341 -0
  157. package/src/widgets/formula/components/prefix.tsx +18 -0
  158. package/src/widgets/formula/components/row.test.tsx +364 -0
  159. package/src/widgets/formula/components/row.tsx +21 -0
  160. package/src/widgets/formula/components/series.tsx +34 -0
  161. package/src/widgets/formula/components/suffix.test.tsx +383 -0
  162. package/src/widgets/formula/components/suffix.tsx +28 -0
  163. package/src/widgets/formula/components/value.test.tsx +329 -0
  164. package/src/widgets/formula/components/value.tsx +29 -0
  165. package/src/widgets/formula/config.ts +27 -0
  166. package/src/widgets/formula/formula-ui.test.tsx +399 -0
  167. package/src/widgets/formula/formula-ui.tsx +27 -0
  168. package/src/widgets/formula/index.ts +24 -0
  169. package/src/widgets/formula/serializer.test.tsx +144 -0
  170. package/src/widgets/formula/serializer.ts +28 -0
  171. package/src/widgets/formula/skeleton.tsx +10 -0
  172. package/src/widgets/formula/style.ts +23 -0
  173. package/src/widgets/formula/types.ts +50 -0
  174. package/src/widgets/histogram/config.ts +143 -0
  175. package/src/widgets/histogram/index.ts +8 -0
  176. package/src/widgets/histogram/skeleton.tsx +52 -0
  177. package/src/widgets/histogram/style.ts +8 -0
  178. package/src/widgets/histogram/types.ts +17 -0
  179. package/src/widgets/index.ts +25 -0
  180. package/src/widgets/loader/index.ts +4 -0
  181. package/src/widgets/loader/loader.tsx +70 -0
  182. package/src/widgets/loader/types.ts +11 -0
  183. package/src/widgets/loader/utils.test.ts +112 -0
  184. package/src/widgets/loader/utils.ts +35 -0
  185. package/src/widgets/markdown/config.ts +18 -0
  186. package/src/widgets/markdown/index.ts +14 -0
  187. package/src/widgets/markdown/markdown-ui.test.tsx +341 -0
  188. package/src/widgets/markdown/markdown-ui.tsx +52 -0
  189. package/src/widgets/markdown/markdown.tsx +20 -0
  190. package/src/widgets/markdown/skeleton.tsx +12 -0
  191. package/src/widgets/markdown/style.ts +28 -0
  192. package/src/widgets/markdown/types.ts +28 -0
  193. package/src/widgets/no-data/index.ts +2 -0
  194. package/src/widgets/no-data/no-data.test.tsx +447 -0
  195. package/src/widgets/no-data/no-data.tsx +116 -0
  196. package/src/widgets/no-data/style.ts +18 -0
  197. package/src/widgets/no-data/types.ts +72 -0
  198. package/src/widgets/note/index.ts +2 -0
  199. package/src/widgets/note/note.test.tsx +391 -0
  200. package/src/widgets/note/note.tsx +114 -0
  201. package/src/widgets/note/style.ts +29 -0
  202. package/src/widgets/note/types.ts +9 -0
  203. package/src/widgets/pie/config.ts +177 -0
  204. package/src/widgets/pie/index.ts +8 -0
  205. package/src/widgets/pie/skeleton.tsx +70 -0
  206. package/src/widgets/pie/style.ts +8 -0
  207. package/src/widgets/pie/types.ts +16 -0
  208. package/src/widgets/range/components/range-item.tsx +213 -0
  209. package/src/widgets/range/config.ts +10 -0
  210. package/src/widgets/range/index.ts +16 -0
  211. package/src/widgets/range/range-ui.test.tsx +203 -0
  212. package/src/widgets/range/range-ui.tsx +11 -0
  213. package/src/widgets/range/serializer.test.ts +70 -0
  214. package/src/widgets/range/serializer.ts +27 -0
  215. package/src/widgets/range/skeleton.tsx +14 -0
  216. package/src/widgets/range/style.ts +37 -0
  217. package/src/widgets/range/types.ts +39 -0
  218. package/src/widgets/scatterplot/config.ts +138 -0
  219. package/src/widgets/scatterplot/index.ts +8 -0
  220. package/src/widgets/scatterplot/skeleton.tsx +59 -0
  221. package/src/widgets/scatterplot/style.ts +21 -0
  222. package/src/widgets/scatterplot/types.ts +17 -0
  223. package/src/widgets/selection-summary/index.ts +6 -0
  224. package/src/widgets/selection-summary/selection-summary.tsx +46 -0
  225. package/src/widgets/selection-summary/style.ts +10 -0
  226. package/src/widgets/selection-summary/types.ts +14 -0
  227. package/src/widgets/skeleton-loader/index.ts +2 -0
  228. package/src/widgets/skeleton-loader/skeleton-loader.test.tsx +139 -0
  229. package/src/widgets/skeleton-loader/skeleton-loader.tsx +28 -0
  230. package/src/widgets/skeleton-loader/types.ts +8 -0
  231. package/src/widgets/spread/components/max-value.tsx +29 -0
  232. package/src/widgets/spread/components/min-value.tsx +29 -0
  233. package/src/widgets/spread/components/separator.tsx +6 -0
  234. package/src/widgets/spread/config.ts +34 -0
  235. package/src/widgets/spread/index.ts +23 -0
  236. package/src/widgets/spread/skeleton.tsx +10 -0
  237. package/src/widgets/spread/spread-ui.test.tsx +368 -0
  238. package/src/widgets/spread/spread-ui.tsx +29 -0
  239. package/src/widgets/spread/style.ts +22 -0
  240. package/src/widgets/spread/types.ts +47 -0
  241. package/src/widgets/stores/index.ts +9 -0
  242. package/src/widgets/stores/types.ts +192 -0
  243. package/src/widgets/stores/widget-store.test.ts +601 -0
  244. package/src/widgets/stores/widget-store.ts +239 -0
  245. package/src/widgets/subheader/index.ts +3 -0
  246. package/src/widgets/subheader/style.ts +20 -0
  247. package/src/widgets/subheader/subheader.test.tsx +45 -0
  248. package/src/widgets/subheader/subheader.tsx +16 -0
  249. package/src/widgets/subheader/types.ts +11 -0
  250. package/src/widgets/table/components/cell-header.tsx +58 -0
  251. package/src/widgets/table/components/cell.tsx +80 -0
  252. package/src/widgets/table/components/index.ts +4 -0
  253. package/src/widgets/table/components/pagination-actions.tsx +67 -0
  254. package/src/widgets/table/components/pagination.tsx +41 -0
  255. package/src/widgets/table/components/row.tsx +60 -0
  256. package/src/widgets/table/config.ts +71 -0
  257. package/src/widgets/table/helpers.test.ts +244 -0
  258. package/src/widgets/table/helpers.ts +107 -0
  259. package/src/widgets/table/hooks/index.ts +7 -0
  260. package/src/widgets/table/hooks/use-pagination.test.ts +294 -0
  261. package/src/widgets/table/hooks/use-pagination.ts +155 -0
  262. package/src/widgets/table/hooks/use-selection.test.ts +504 -0
  263. package/src/widgets/table/hooks/use-selection.ts +189 -0
  264. package/src/widgets/table/hooks/use-sort.test.ts +296 -0
  265. package/src/widgets/table/hooks/use-sort.ts +138 -0
  266. package/src/widgets/table/index.ts +53 -0
  267. package/src/widgets/table/serializer.ts +54 -0
  268. package/src/widgets/table/skeleton.tsx +48 -0
  269. package/src/widgets/table/style.ts +34 -0
  270. package/src/widgets/table/table-ui.tsx +64 -0
  271. package/src/widgets/table/types.ts +223 -0
  272. package/src/widgets/timeseries/config.ts +135 -0
  273. package/src/widgets/timeseries/index.ts +8 -0
  274. package/src/widgets/timeseries/skeleton.tsx +55 -0
  275. package/src/widgets/timeseries/style.ts +36 -0
  276. package/src/widgets/timeseries/types.ts +17 -0
  277. package/src/widgets/toolbar-actions/index.ts +6 -0
  278. package/src/widgets/toolbar-actions/styles.ts +38 -0
  279. package/src/widgets/toolbar-actions/toolbar-actions.test.tsx +691 -0
  280. package/src/widgets/toolbar-actions/toolbar-actions.tsx +145 -0
  281. package/src/widgets/toolbar-actions/types.ts +60 -0
  282. package/src/widgets/wrapper/components/actions.test.tsx +101 -0
  283. package/src/widgets/wrapper/components/actions.tsx +30 -0
  284. package/src/widgets/wrapper/components/options.test.tsx +323 -0
  285. package/src/widgets/wrapper/components/options.tsx +73 -0
  286. package/src/widgets/wrapper/components/title.test.tsx +126 -0
  287. package/src/widgets/wrapper/components/title.tsx +32 -0
  288. package/src/widgets/wrapper/index.ts +16 -0
  289. package/src/widgets/wrapper/styles.ts +98 -0
  290. package/src/widgets/wrapper/types.ts +55 -0
  291. package/src/widgets/wrapper/wrapper-ui.test.tsx +232 -0
  292. package/src/widgets/wrapper/wrapper-ui.tsx +57 -0
  293. package/src/widgets/wrapper/wrapper.test.tsx +365 -0
  294. package/src/widgets/wrapper/wrapper.tsx +50 -0
  295. package/dist/lasso-tool-BwRzEW7k.js.map +0 -1
  296. package/dist/types/common/common.d.ts +0 -3
  297. package/dist/types/common/index.d.ts +0 -26
  298. package/dist/types/common/lasso-tools.d.ts +0 -36
  299. package/dist/types/common/measurement-tools.d.ts +0 -65
@@ -0,0 +1,103 @@
1
+ import type { Ref } from 'react'
2
+ import type { DownloadItem } from './types'
3
+ import html2canvas from 'html2canvas'
4
+ import { ImageOutlined } from '@mui/icons-material'
5
+ import { SvgIcon } from '@mui/material'
6
+
7
+ // Helper to escape CSV cell values
8
+ function escapeCSVCell<D>(value: D): string {
9
+ const str =
10
+ value == null
11
+ ? ''
12
+ : typeof value === 'object'
13
+ ? JSON.stringify(value)
14
+ : String(value)
15
+
16
+ // If the value contains a comma, quote, or newline, wrap in quotes and escape quotes
17
+ if (/[",\n]/.test(str)) {
18
+ return `"${str.replace(/"/g, '""')}"`
19
+ }
20
+ return str
21
+ }
22
+
23
+ async function downloadFileToCSV<D>(data: D[][]) {
24
+ const csvContent = data
25
+ .map((row) => row.map(escapeCSVCell<D>).join(','))
26
+ .join('\n')
27
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
28
+ return Promise.resolve(URL.createObjectURL(blob))
29
+ }
30
+
31
+ export const downloadToCSV: DownloadItem<unknown[][]> = {
32
+ id: 'csv',
33
+ label: 'CSV',
34
+ icon: (
35
+ <SvgIcon>
36
+ <svg
37
+ xmlns='http://www.w3.org/2000/svg'
38
+ width='18'
39
+ height='18'
40
+ fill='none'
41
+ viewBox='0 0 18 18'
42
+ >
43
+ <path
44
+ fill='currentColor'
45
+ d='M4.313 11.25h2.25v-1.125H4.688v-2.25h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v3c0 .213.072.39.216.534a.726.726 0 0 0 .534.216Zm2.925 0h2.25c.212 0 .39-.072.534-.216a.726.726 0 0 0 .216-.534V9.375a.931.931 0 0 0-.216-.59.658.658 0 0 0-.534-.273H8.363v-.637h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v1.125c0 .213.072.403.216.572a.675.675 0 0 0 .534.253h1.126v.675H7.238v1.125Zm4.95 0h1.124l1.313-4.5H13.5l-.75 2.588L12 6.75h-1.125l1.313 4.5ZM3 15c-.413 0-.766-.147-1.06-.44a1.445 1.445 0 0 1-.44-1.06v-9c0-.412.147-.766.44-1.06C2.235 3.148 2.588 3 3 3h12c.412 0 .766.147 1.06.44.293.294.44.648.44 1.06v9c0 .412-.147.766-.44 1.06-.294.293-.647.44-1.06.44H3Zm0-1.5h12v-9H3v9Z'
46
+ />
47
+ </svg>
48
+ </SvgIcon>
49
+ ),
50
+ modifier: downloadFileToCSV,
51
+ callback: (data) => {
52
+ URL.revokeObjectURL(data)
53
+ },
54
+ }
55
+
56
+ async function downloadFileToPNG(ref: Ref<HTMLElement | null> | undefined) {
57
+ if (!ref || typeof ref === 'function' || !ref.current) {
58
+ // eslint-disable-next-line no-console
59
+ console.warn(
60
+ '[CARTO downloadFileToPNG] Invalid ref passed to downloadFileToPNG. ' +
61
+ 'Expected a React ref object with a .current property pointing to an HTMLElement. ' +
62
+ 'Download aborted.',
63
+ )
64
+ return
65
+ }
66
+
67
+ const element = ref.current
68
+ const clone = element.cloneNode(true) as HTMLElement
69
+
70
+ clone.querySelector('.widget-toolbar-container')?.remove()
71
+ clone.querySelector('.widget-wrapper-actions')?.remove()
72
+
73
+ document.body.appendChild(clone)
74
+
75
+ const rect = clone.getBoundingClientRect()
76
+
77
+ const opts = {
78
+ useCORS: true,
79
+ scale: 2,
80
+ backgroundColor: '#fff',
81
+ width: rect.width,
82
+ height: rect.height,
83
+ }
84
+
85
+ const canvasResult: HTMLCanvasElement = await html2canvas(clone, opts)
86
+
87
+ const result = canvasResult.toDataURL('image/png')
88
+
89
+ document.body.removeChild(clone)
90
+
91
+ return Promise.resolve(result)
92
+ }
93
+
94
+ export const downloadToPNG: Omit<DownloadItem, 'modifier'> & {
95
+ modifier: (
96
+ ref: Ref<HTMLElement | null> | undefined,
97
+ ) => Promise<string | undefined>
98
+ } = {
99
+ id: 'png',
100
+ label: 'PNG',
101
+ icon: <ImageOutlined />,
102
+ modifier: downloadFileToPNG,
103
+ }
@@ -0,0 +1,21 @@
1
+ import type { JSX, ReactNode } from 'react'
2
+ import type { IconButtonProps } from '@mui/material'
3
+ import type { WidgetState } from '../../stores/types'
4
+
5
+ export interface DownloadItem<D = unknown> {
6
+ id: string
7
+ label: string
8
+ icon?: JSX.Element
9
+ disabled?: boolean
10
+ filename?: string
11
+ modifier: (data: D) => Promise<string | undefined>
12
+ callback?: (data: string) => void
13
+ }
14
+
15
+ export interface DownloadProps {
16
+ id: WidgetState['id']
17
+ items: DownloadItem[]
18
+ labels?: { ariaLabel?: string }
19
+ IconButtonProps?: IconButtonProps
20
+ Icon?: ReactNode
21
+ }
@@ -0,0 +1,269 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest'
2
+ import { render, screen, fireEvent, within } from '@testing-library/react'
3
+ import { FullScreen } from './fullscreen'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { FullScreenState } from './types'
6
+
7
+ describe('FullScreen', () => {
8
+ const widgetId = 'test-widget'
9
+
10
+ beforeEach(() => {
11
+ // Clear store before each test
12
+ useWidgetStore.getState().clearWidgets()
13
+ // Initialize widget with title
14
+ useWidgetStore.getState().setWidget<FullScreenState>(widgetId, {
15
+ title: 'Test Widget',
16
+ })
17
+ })
18
+
19
+ test('renders fullscreen button', () => {
20
+ render(
21
+ <FullScreen id={widgetId}>
22
+ <div>Widget Content</div>
23
+ </FullScreen>,
24
+ )
25
+
26
+ const button = screen.getByRole('button')
27
+ expect(button).toBeTruthy()
28
+ })
29
+
30
+ test('opens dialog when fullscreen button is clicked', () => {
31
+ render(
32
+ <FullScreen id={widgetId}>
33
+ <div>Widget Content</div>
34
+ </FullScreen>,
35
+ )
36
+
37
+ const button = screen.getByRole('button')
38
+ fireEvent.click(button)
39
+
40
+ // Check if dialog is opened
41
+ expect(screen.getByText('Test Widget')).toBeTruthy()
42
+ expect(screen.getByText('Widget Content')).toBeTruthy()
43
+ })
44
+
45
+ test('closes dialog when close button is clicked', () => {
46
+ render(
47
+ <FullScreen id={widgetId}>
48
+ <div>Widget Content</div>
49
+ </FullScreen>,
50
+ )
51
+
52
+ // Open dialog
53
+ const openButton = screen.getByRole('button')
54
+ fireEvent.click(openButton)
55
+
56
+ // Find and click close button (inside the dialog)
57
+ const dialog = screen.getByRole('dialog')
58
+ const closeButton = within(dialog).getAllByRole('button')[0]
59
+
60
+ fireEvent.click(closeButton!)
61
+
62
+ // Store should be updated
63
+ const widget = useWidgetStore
64
+ .getState()
65
+ .getWidget<FullScreenState>(widgetId)
66
+ expect(widget?.isFullScreen).toBe(false)
67
+ })
68
+
69
+ test('uses custom aria label when provided', () => {
70
+ const customLabels = {
71
+ ariaLabel: 'custom-fullscreen-dialog',
72
+ }
73
+
74
+ render(
75
+ <FullScreen id={widgetId} labels={customLabels}>
76
+ <div>Widget Content</div>
77
+ </FullScreen>,
78
+ )
79
+
80
+ const button = screen.getByRole('button')
81
+ fireEvent.click(button)
82
+
83
+ const dialog = screen.getByRole('dialog')
84
+ expect(dialog.getAttribute('aria-labelledby')).toBe(
85
+ 'custom-fullscreen-dialog',
86
+ )
87
+ })
88
+
89
+ test('uses default aria label when not provided', () => {
90
+ render(
91
+ <FullScreen id={widgetId}>
92
+ <div>Widget Content</div>
93
+ </FullScreen>,
94
+ )
95
+
96
+ const button = screen.getByRole('button')
97
+ fireEvent.click(button)
98
+
99
+ const dialog = screen.getByRole('dialog')
100
+ expect(dialog.getAttribute('aria-labelledby')).toBe(
101
+ `fullscreen-dialog-title-${widgetId}`,
102
+ )
103
+ })
104
+
105
+ test('renders children inside dialog content', () => {
106
+ const TestChild = () => (
107
+ <div>
108
+ <p>First paragraph</p>
109
+ <p>Second paragraph</p>
110
+ </div>
111
+ )
112
+
113
+ render(
114
+ <FullScreen id={widgetId}>
115
+ <TestChild />
116
+ </FullScreen>,
117
+ )
118
+
119
+ const button = screen.getByRole('button')
120
+ fireEvent.click(button)
121
+
122
+ expect(screen.getByText('First paragraph')).toBeTruthy()
123
+ expect(screen.getByText('Second paragraph')).toBeTruthy()
124
+ })
125
+
126
+ test('dialog is not visible initially', () => {
127
+ render(
128
+ <FullScreen id={widgetId}>
129
+ <div>Widget Content</div>
130
+ </FullScreen>,
131
+ )
132
+
133
+ // Dialog should not be rendered until button is clicked
134
+ expect(screen.queryByRole('dialog')).toBeNull()
135
+ })
136
+
137
+ test('updates widget store when opening fullscreen', () => {
138
+ render(
139
+ <FullScreen id={widgetId}>
140
+ <div>Widget Content</div>
141
+ </FullScreen>,
142
+ )
143
+
144
+ const button = screen.getByRole('button')
145
+ fireEvent.click(button)
146
+
147
+ const widget = useWidgetStore
148
+ .getState()
149
+ .getWidget<FullScreenState>(widgetId)
150
+ expect(widget?.isFullScreen).toBe(true)
151
+ })
152
+
153
+ test('updates widget store when closing fullscreen', () => {
154
+ // Pre-set the widget to fullscreen
155
+ useWidgetStore.getState().setWidget<FullScreenState>(widgetId, {
156
+ title: 'Test Widget',
157
+ isFullScreen: true,
158
+ })
159
+
160
+ render(
161
+ <FullScreen id={widgetId}>
162
+ <div>Widget Content</div>
163
+ </FullScreen>,
164
+ )
165
+
166
+ // Find and click close button
167
+ const dialog = screen.getByRole('dialog')
168
+ const closeButton = within(dialog).getAllByRole('button')[0]
169
+
170
+ fireEvent.click(closeButton!)
171
+
172
+ const widget = useWidgetStore
173
+ .getState()
174
+ .getWidget<FullScreenState>(widgetId)
175
+ expect(widget?.isFullScreen).toBe(false)
176
+ })
177
+
178
+ test('handles multiple fullscreen widgets independently', () => {
179
+ const widget1Id = 'widget-1'
180
+ const widget2Id = 'widget-2'
181
+
182
+ // Initialize both widgets with titles
183
+ useWidgetStore.getState().setWidget<FullScreenState>(widget1Id, {
184
+ title: 'Widget 1',
185
+ })
186
+ useWidgetStore.getState().setWidget<FullScreenState>(widget2Id, {
187
+ title: 'Widget 2',
188
+ })
189
+
190
+ render(
191
+ <>
192
+ <FullScreen id={widget1Id}>
193
+ <div>Content 1</div>
194
+ </FullScreen>
195
+ <FullScreen id={widget2Id}>
196
+ <div>Content 2</div>
197
+ </FullScreen>
198
+ </>,
199
+ )
200
+
201
+ const buttons = screen.getAllByRole('button')
202
+
203
+ // Open first widget
204
+ fireEvent.click(buttons[0]!)
205
+ let widget1 = useWidgetStore
206
+ .getState()
207
+ .getWidget<FullScreenState>(widget1Id)
208
+ let widget2 = useWidgetStore
209
+ .getState()
210
+ .getWidget<FullScreenState>(widget2Id)
211
+
212
+ expect(widget1?.isFullScreen).toBe(true)
213
+ expect(widget2?.isFullScreen).toBeUndefined()
214
+
215
+ // Verify widget 1 content is displayed
216
+ expect(screen.getByText('Content 1')).toBeTruthy()
217
+
218
+ // Close first widget via store (simpler than clicking)
219
+ useWidgetStore.getState().setWidget<FullScreenState>(widget1Id, {
220
+ title: 'Widget 1',
221
+ isFullScreen: false,
222
+ })
223
+
224
+ // Open second widget
225
+ fireEvent.click(buttons[1]!)
226
+
227
+ widget1 = useWidgetStore.getState().getWidget<FullScreenState>(widget1Id)
228
+ widget2 = useWidgetStore.getState().getWidget<FullScreenState>(widget2Id)
229
+
230
+ expect(widget1?.isFullScreen).toBe(false)
231
+ expect(widget2?.isFullScreen).toBe(true)
232
+
233
+ // Verify widget 2 content is displayed
234
+ expect(screen.getByText('Content 2')).toBeTruthy()
235
+ })
236
+
237
+ test('renders dialog title with correct typography', () => {
238
+ render(
239
+ <FullScreen id={widgetId}>
240
+ <div>Widget Content</div>
241
+ </FullScreen>,
242
+ )
243
+
244
+ const button = screen.getByRole('button')
245
+ fireEvent.click(button)
246
+
247
+ const title = screen.getByText('Test Widget')
248
+ expect(title).toBeTruthy()
249
+ expect(title.tagName).toBe('SPAN')
250
+ })
251
+
252
+ test('dialog respects isFullScreen state from store', () => {
253
+ // Set initial state to fullscreen
254
+ useWidgetStore.getState().setWidget<FullScreenState>(widgetId, {
255
+ title: 'Test Widget',
256
+ isFullScreen: true,
257
+ })
258
+
259
+ render(
260
+ <FullScreen id={widgetId}>
261
+ <div>Widget Content</div>
262
+ </FullScreen>,
263
+ )
264
+
265
+ // Dialog should be visible immediately
266
+ expect(screen.getByRole('dialog')).toBeTruthy()
267
+ expect(screen.getByText('Widget Content')).toBeTruthy()
268
+ })
269
+ })
@@ -0,0 +1,82 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogTitle,
5
+ IconButton,
6
+ Typography,
7
+ } from '@mui/material'
8
+ import { useWidgetStore } from '../../stores/widget-store'
9
+ import { Close, FullscreenOutlined } from '@mui/icons-material'
10
+ import type {
11
+ FullScreenConfig,
12
+ FullScreenProps,
13
+ FullScreenState,
14
+ } from './types'
15
+ import { styles } from './styles'
16
+ import { useShallow } from 'zustand/shallow'
17
+
18
+ export function FullScreen({
19
+ id,
20
+ labels,
21
+ children,
22
+ Icon,
23
+ IconButtonProps,
24
+ DialogContentProps: { sx, ...DialogContentProps } = {},
25
+ }: FullScreenProps) {
26
+ const { isFullScreen, title } = useWidgetStore(
27
+ useShallow((state) => {
28
+ const widget = state.getWidget<FullScreenState>(id)
29
+ return {
30
+ isFullScreen: widget?.isFullScreen,
31
+ title: widget?.title,
32
+ }
33
+ }),
34
+ )
35
+ const setWidget = useWidgetStore((state) => state.setWidget)
36
+
37
+ const updateFullScreenConfig = (updates: Partial<FullScreenConfig>) => {
38
+ setWidget<FullScreenState>(id, {
39
+ isFullScreen: updates.isFullScreen,
40
+ })
41
+ }
42
+
43
+ return (
44
+ <>
45
+ <IconButton
46
+ {...IconButtonProps}
47
+ onClick={() => updateFullScreenConfig({ isFullScreen: true })}
48
+ >
49
+ {Icon ?? <FullscreenOutlined />}
50
+ </IconButton>
51
+ <Dialog
52
+ maxWidth={false}
53
+ open={!!isFullScreen}
54
+ aria-labelledby={labels?.ariaLabel ?? `fullscreen-dialog-title-${id}`}
55
+ onClose={() => updateFullScreenConfig({ isFullScreen: false })}
56
+ >
57
+ <DialogTitle
58
+ id={`fullscreen-dialog-title-${id}`}
59
+ sx={styles.title.container}
60
+ >
61
+ <Typography variant='subtitle1' component='span'>
62
+ {title}
63
+ </Typography>
64
+ <IconButton
65
+ onClick={() => updateFullScreenConfig({ isFullScreen: false })}
66
+ >
67
+ <Close />
68
+ </IconButton>
69
+ </DialogTitle>
70
+ <DialogContent
71
+ sx={{
72
+ ...styles.content,
73
+ ...sx,
74
+ }}
75
+ {...DialogContentProps}
76
+ >
77
+ {children}
78
+ </DialogContent>
79
+ </Dialog>
80
+ </>
81
+ )
82
+ }
@@ -0,0 +1,17 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ title: {
5
+ container: {
6
+ display: 'flex',
7
+ alignItems: 'center',
8
+ justifyContent: 'space-between',
9
+ gap: 1,
10
+ },
11
+ },
12
+ content: {
13
+ display: 'flex',
14
+ flexDirection: 'column',
15
+ gap: ({ spacing }) => spacing(1),
16
+ },
17
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,27 @@
1
+ import type { DialogContentProps, IconButtonProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../../widgets/stores/types'
4
+
5
+ export type FullScreenState<T = unknown> = BaseWidgetState<
6
+ T &
7
+ FullScreenConfigProps & {
8
+ title: string
9
+ }
10
+ >
11
+
12
+ export interface FullScreenConfig {
13
+ isFullScreen?: boolean
14
+ }
15
+
16
+ export type FullScreenConfigProps = FullScreenConfig
17
+
18
+ export interface FullScreenProps {
19
+ id: FullScreenState['id']
20
+ labels?: {
21
+ ariaLabel?: string
22
+ }
23
+ children: ReactNode
24
+ DialogContentProps?: DialogContentProps
25
+ IconButtonProps?: IconButtonProps
26
+ Icon?: ReactNode
27
+ }
@@ -0,0 +1,51 @@
1
+ /* Fullscreen Widget */
2
+ export { FullScreen } from './fullscreen/fullscreen'
3
+ export type { FullScreenState, FullScreenConfig } from './fullscreen/types'
4
+
5
+ /* Download Widget */
6
+ export { Download } from './download/download'
7
+ export type { DownloadItem, DownloadProps } from './download/types'
8
+ export { downloadToCSV, downloadToPNG } from './download/exports'
9
+
10
+ /* Relative Data Widget */
11
+ export {
12
+ RelativeData,
13
+ RELATIVE_DATA_TOOL_ID,
14
+ } from './relative-data/relative-data'
15
+ export type { RelativeDataProps } from './relative-data/types'
16
+
17
+ /* Zoom Toggle Widget */
18
+ export { ZoomToggle } from './zoom-toggle/zoom-toggle'
19
+ export type {
20
+ ZoomToggleProps,
21
+ ZoomState,
22
+ ZoomConfig,
23
+ } from './zoom-toggle/types'
24
+
25
+ /* Stack Toggle Widget */
26
+ export { StackToggle } from './stack-toggle/stack-toggle'
27
+ export type { StackToggleProps, StackToggleState } from './stack-toggle/types'
28
+
29
+ /* Searcher Toggle Widget */
30
+ export { Searcher, SEARCHER_TOOL_ID } from './searcher/searcher'
31
+ export { SearcherToggle } from './searcher/searcher-toggle'
32
+ export type {
33
+ SearcherToggleProps,
34
+ SearcherProps,
35
+ SearcherFilterFn,
36
+ SearcherState,
37
+ } from './searcher/types'
38
+
39
+ /* Change Column Widget */
40
+ export { ChangeColumn } from './change-column/change-column'
41
+ export type { ChangeColumnProps } from './change-column/types'
42
+
43
+ /* Lock Selection Widget */
44
+ export {
45
+ LockSelection,
46
+ LOCK_SELECTION_TOOL_ID,
47
+ } from './lock-selection/lock-selection'
48
+ export type {
49
+ LockSelectionProps,
50
+ LockSelectionState,
51
+ } from './lock-selection/types'