@carto/ps-react-ui 6.0.0 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/actions-BWlS5tpP.js +1150 -0
  2. package/dist/actions-BWlS5tpP.js.map +1 -0
  3. package/dist/category-alzjKzvQ.js +839 -0
  4. package/dist/category-alzjKzvQ.js.map +1 -0
  5. package/dist/{download-Vlzn2jzb.js → download-B4sykOFm.js} +94 -83
  6. package/dist/download-B4sykOFm.js.map +1 -0
  7. package/dist/{echart-DjcbtKgi.js → echart-CV8F50gn.js} +6 -5
  8. package/dist/echart-CV8F50gn.js.map +1 -0
  9. package/dist/{formula-CdHWtepq.js → formula-DJS-JLpk.js} +10 -16
  10. package/dist/{formula-CdHWtepq.js.map → formula-DJS-JLpk.js.map} +1 -1
  11. package/dist/legend.js +34 -21
  12. package/dist/legend.js.map +1 -1
  13. package/dist/{markdown-CY-67NRE.js → markdown-Bn2nKCbn.js} +12 -18
  14. package/dist/{markdown-CY-67NRE.js.map → markdown-Bn2nKCbn.js.map} +1 -1
  15. package/dist/{range-DHQSsl2_.js → range-BpU2zBmq.js} +2 -2
  16. package/dist/{range-DHQSsl2_.js.map → range-BpU2zBmq.js.map} +1 -1
  17. package/dist/{spread-DuM5oTI1.js → spread-CQwFIwuz.js} +10 -16
  18. package/dist/{spread-DuM5oTI1.js.map → spread-CQwFIwuz.js.map} +1 -1
  19. package/dist/{stores-B8CHbgw-.js → stores-DcBOZlU3.js} +30 -24
  20. package/dist/stores-DcBOZlU3.js.map +1 -0
  21. package/dist/{table-BE4uO9pp.js → table-DEdW_fDj.js} +7 -13
  22. package/dist/{table-BE4uO9pp.js.map → table-DEdW_fDj.js.map} +1 -1
  23. package/dist/types/legend/stores/types.d.ts +4 -2
  24. package/dist/types/widgets/actions/download/csv-item.d.ts +3 -3
  25. package/dist/types/widgets/actions/download/index.d.ts +2 -1
  26. package/dist/types/widgets/actions/download/png-item.d.ts +2 -2
  27. package/dist/types/widgets/actions/download/types.d.ts +12 -1
  28. package/dist/types/widgets/actions/download/widget-download.d.ts +26 -0
  29. package/dist/types/widgets/actions/fullscreen/fullscreen.d.ts +20 -13
  30. package/dist/types/widgets/actions/index.d.ts +1 -1
  31. package/dist/types/widgets/actions/searcher/filter.d.ts +3 -2
  32. package/dist/types/widgets/actions/searcher/searcher-toggle.d.ts +3 -0
  33. package/dist/types/widgets/bar/download.d.ts +6 -20
  34. package/dist/types/widgets/category/category-ui.d.ts +9 -1
  35. package/dist/types/widgets/category/download.d.ts +5 -10
  36. package/dist/types/widgets/category/helpers.d.ts +10 -0
  37. package/dist/types/widgets/echart/style.d.ts +1 -0
  38. package/dist/types/widgets/formula/download.d.ts +5 -15
  39. package/dist/types/widgets/histogram/download.d.ts +3 -8
  40. package/dist/types/widgets/index.d.ts +1 -1
  41. package/dist/types/widgets/markdown/download.d.ts +5 -11
  42. package/dist/types/widgets/pie/download.d.ts +3 -8
  43. package/dist/types/widgets/scatterplot/download.d.ts +6 -11
  44. package/dist/types/widgets/spread/download.d.ts +6 -12
  45. package/dist/types/widgets/stores/index.d.ts +1 -1
  46. package/dist/types/widgets/stores/widget-store-registry.d.ts +14 -0
  47. package/dist/types/widgets/table/download.d.ts +7 -13
  48. package/dist/types/widgets/timeseries/download.d.ts +7 -12
  49. package/dist/types/widgets/toolbox/toolbox.d.ts +1 -1
  50. package/dist/types/widgets/wrapper/style.d.ts +31 -2
  51. package/dist/types/widgets/wrapper/widget-actions.d.ts +2 -2
  52. package/dist/types/widgets/wrapper/widget-wrapper.d.ts +2 -2
  53. package/dist/{utils-BStJWhZN.js → utils-C-eWwOhE.js} +2 -2
  54. package/dist/{utils-BStJWhZN.js.map → utils-C-eWwOhE.js.map} +1 -1
  55. package/dist/widgets/actions.js +4 -4
  56. package/dist/widgets/bar.js +11 -17
  57. package/dist/widgets/bar.js.map +1 -1
  58. package/dist/widgets/category.js +1 -1
  59. package/dist/widgets/echart.js +1 -1
  60. package/dist/widgets/formula.js +1 -1
  61. package/dist/widgets/histogram.js +14 -20
  62. package/dist/widgets/histogram.js.map +1 -1
  63. package/dist/widgets/markdown.js +1 -1
  64. package/dist/widgets/pie.js +13 -19
  65. package/dist/widgets/pie.js.map +1 -1
  66. package/dist/widgets/range.js +1 -1
  67. package/dist/widgets/scatterplot.js +13 -19
  68. package/dist/widgets/scatterplot.js.map +1 -1
  69. package/dist/widgets/spread.js +1 -1
  70. package/dist/widgets/stores.js +2 -2
  71. package/dist/widgets/table.js +1 -1
  72. package/dist/widgets/timeseries.js +16 -22
  73. package/dist/widgets/timeseries.js.map +1 -1
  74. package/dist/widgets/utils.js +1 -1
  75. package/dist/widgets.js +364 -376
  76. package/dist/widgets.js.map +1 -1
  77. package/dist/{zoom-toggle-D_VuA01l.js → zoom-toggle-MmLjMTEw.js} +2 -2
  78. package/dist/{zoom-toggle-D_VuA01l.js.map → zoom-toggle-MmLjMTEw.js.map} +1 -1
  79. package/package.json +1 -1
  80. package/src/legend/components/legend-item/legend-item-ui.test.tsx +22 -1
  81. package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +22 -6
  82. package/src/legend/stores/types.ts +4 -2
  83. package/src/widgets/actions/download/csv-item.test.tsx +3 -3
  84. package/src/widgets/actions/download/csv-item.tsx +6 -6
  85. package/src/widgets/actions/download/download.tsx +5 -3
  86. package/src/widgets/actions/download/index.ts +2 -1
  87. package/src/widgets/actions/download/png-item.test.tsx +5 -3
  88. package/src/widgets/actions/download/png-item.tsx +4 -4
  89. package/src/widgets/actions/download/types.ts +15 -1
  90. package/src/widgets/actions/download/widget-download.test.ts +30 -0
  91. package/src/widgets/actions/download/widget-download.ts +41 -0
  92. package/src/widgets/actions/fullscreen/fullscreen-store.test.tsx +155 -0
  93. package/src/widgets/actions/fullscreen/fullscreen.test.tsx +39 -41
  94. package/src/widgets/actions/fullscreen/fullscreen.tsx +198 -96
  95. package/src/widgets/actions/index.ts +3 -0
  96. package/src/widgets/actions/searcher/filter.test.ts +28 -0
  97. package/src/widgets/actions/searcher/filter.ts +22 -4
  98. package/src/widgets/actions/searcher/searcher-toggle.tsx +16 -8
  99. package/src/widgets/actions/searcher/searcher.test.tsx +22 -0
  100. package/src/widgets/actions/searcher/searcher.tsx +17 -7
  101. package/src/widgets/bar/download.test.tsx +5 -3
  102. package/src/widgets/bar/download.tsx +11 -35
  103. package/src/widgets/category/category-ui.tsx +26 -14
  104. package/src/widgets/category/category.tsx +37 -2
  105. package/src/widgets/category/components/category-legend.tsx +1 -2
  106. package/src/widgets/category/download.test.ts +4 -2
  107. package/src/widgets/category/download.ts +14 -27
  108. package/src/widgets/category/helpers.test.ts +17 -0
  109. package/src/widgets/category/helpers.ts +16 -0
  110. package/src/widgets/echart/style.ts +6 -0
  111. package/src/widgets/formula/download.test.tsx +3 -1
  112. package/src/widgets/formula/download.tsx +10 -30
  113. package/src/widgets/histogram/download.test.ts +6 -4
  114. package/src/widgets/histogram/download.ts +13 -26
  115. package/src/widgets/index.ts +1 -0
  116. package/src/widgets/markdown/download.test.ts +2 -2
  117. package/src/widgets/markdown/download.tsx +11 -26
  118. package/src/widgets/pie/download.test.ts +7 -3
  119. package/src/widgets/pie/download.ts +12 -25
  120. package/src/widgets/scatterplot/download.test.ts +4 -2
  121. package/src/widgets/scatterplot/download.ts +15 -28
  122. package/src/widgets/spread/download.test.ts +3 -1
  123. package/src/widgets/spread/download.ts +13 -27
  124. package/src/widgets/stores/index.ts +2 -0
  125. package/src/widgets/stores/widget-store-registry.test.ts +25 -0
  126. package/src/widgets/stores/widget-store-registry.ts +22 -0
  127. package/src/widgets/table/download.test.ts +5 -3
  128. package/src/widgets/table/download.ts +15 -29
  129. package/src/widgets/timeseries/download.test.ts +6 -4
  130. package/src/widgets/timeseries/download.ts +16 -29
  131. package/src/widgets/toolbox/toolbox.tsx +1 -1
  132. package/src/widgets/wrapper/style.ts +38 -2
  133. package/src/widgets/wrapper/widget-actions.tsx +2 -2
  134. package/src/widgets/wrapper/widget-wrapper.test.tsx +32 -2
  135. package/src/widgets/wrapper/widget-wrapper.tsx +62 -36
  136. package/dist/actions-KsX1xdDM.js +0 -1024
  137. package/dist/actions-KsX1xdDM.js.map +0 -1
  138. package/dist/category-CsdDqDzo.js +0 -833
  139. package/dist/category-CsdDqDzo.js.map +0 -1
  140. package/dist/download-Vlzn2jzb.js.map +0 -1
  141. package/dist/echart-DjcbtKgi.js.map +0 -1
  142. package/dist/stores-B8CHbgw-.js.map +0 -1
@@ -1,43 +1,29 @@
1
1
  import {
2
2
  DOWNLOAD_ITEM_IDS,
3
3
  MarkdownIcon,
4
- buildPngDownloadItem,
4
+ withPngItem,
5
5
  toCsvString,
6
6
  triggerLinkDownload,
7
7
  type DownloadItem,
8
+ type WidgetDownloadArgs,
8
9
  } from '../actions/download'
9
10
  import type { MarkdownWidgetData } from './types'
10
11
 
11
12
  /**
12
- * Download menu items for the Markdown widget. Always includes a `.md`
13
- * item that saves the markdown source verbatim. When `getCaptureEl` is
14
- * supplied, prepends a PNG item that rasterises the captured element via
13
+ * Download menu items for the Markdown widget. Always includes a `.md` item
14
+ * that saves the markdown source verbatim. When `getCaptureEl` is given,
15
+ * prepends a PNG item that rasterises the captured element via
15
16
  * `html2canvas`.
16
17
  */
17
- export function createMarkdownDownloadConfig(args: {
18
- filename: string
19
- getData: () => MarkdownWidgetData
20
- getCaptureEl?: () => HTMLElement | null
21
- pngPixelRatio?: number
22
- pngBackgroundColor?: string | null
23
- }): DownloadItem[] {
24
- const items: DownloadItem[] = []
25
- if (args.getCaptureEl) {
26
- items.push(
27
- buildPngDownloadItem({
28
- filename: args.filename,
29
- getCaptureEl: args.getCaptureEl,
30
- pixelRatio: args.pngPixelRatio,
31
- backgroundColor: args.pngBackgroundColor,
32
- }),
33
- )
34
- }
35
- items.push({
18
+ export function createMarkdownDownloadConfig(
19
+ args: WidgetDownloadArgs<MarkdownWidgetData>,
20
+ ): DownloadItem[] {
21
+ return withPngItem(args, {
36
22
  id: DOWNLOAD_ITEM_IDS.markdown,
37
23
  label: 'Markdown',
38
24
  icon: <MarkdownIcon fontSize='small' />,
39
- resolve: () => {
40
- const data = args.getData()
25
+ resolve: (ctx) => {
26
+ const data = args.getData(ctx)
41
27
  const blob = new Blob([data.content ?? ''], {
42
28
  type: 'text/markdown;charset=utf-8;',
43
29
  })
@@ -49,7 +35,6 @@ export function createMarkdownDownloadConfig(args: {
49
35
  })
50
36
  },
51
37
  })
52
- return items
53
38
  }
54
39
 
55
40
  // Re-export pure helpers consumers may want when building a custom Download item.
@@ -51,7 +51,9 @@ describe('createPieDownloadConfig', () => {
51
51
  getCaptureEl: () => null,
52
52
  })
53
53
  expect(items.map((i) => i.id)).toEqual(['png', 'csv'])
54
- await expect(items[0]!.resolve()).rejects.toThrow(/No PNG capture element/)
54
+ await expect(items[0]!.resolve({ id: null })).rejects.toThrow(
55
+ /No PNG capture element/,
56
+ )
55
57
  })
56
58
 
57
59
  it('CSV resolve uses default series_<n> when seriesNames is absent', async () => {
@@ -59,7 +61,9 @@ describe('createPieDownloadConfig', () => {
59
61
  filename: 'p',
60
62
  getData: () => multi,
61
63
  })
62
- const handle = await items.find((i) => i.id === 'csv')!.resolve()
64
+ const handle = await items
65
+ .find((i) => i.id === 'csv')!
66
+ .resolve({ id: null })
63
67
  expect(handle.filename).toBe('p.csv')
64
68
  expect(csvText).toBe('series,name,value\nseries_1,a,1\nseries_2,c,3')
65
69
  handle.revoke?.()
@@ -72,7 +76,7 @@ describe('createPieDownloadConfig', () => {
72
76
  getData: () => multi,
73
77
  seriesNames: ['alpha', 'beta'],
74
78
  })
75
- await items.find((i) => i.id === 'csv')!.resolve()
79
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
76
80
  expect(csvText).toBe('series,name,value\nalpha,a,1\nbeta,c,3')
77
81
  })
78
82
  })
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  buildCsvDownloadItem,
3
- buildPngDownloadItem,
3
+ withPngItem,
4
4
  type DownloadItem,
5
+ type WidgetDownloadArgs,
5
6
  } from '../actions/download'
6
7
  import type { PieWidgetData } from './types'
7
8
 
@@ -9,33 +10,20 @@ import type { PieWidgetData } from './types'
9
10
  * Download menu items for the Pie widget. Always includes a CSV item with
10
11
  * `series, name, value` columns (one row per slice; series labels come from
11
12
  * `seriesNames` if provided, else `series_<n>`). When `getCaptureEl` is
12
- * supplied, prepends a PNG item that rasterises the captured element via
13
+ * given, prepends a PNG item that rasterises the captured element via
13
14
  * `html2canvas`.
14
15
  */
15
- export function createPieDownloadConfig(args: {
16
- filename: string
17
- getData: () => PieWidgetData
18
- seriesNames?: readonly string[]
19
- getCaptureEl?: () => HTMLElement | null
20
- pngPixelRatio?: number
21
- pngBackgroundColor?: string | null
22
- }): DownloadItem[] {
23
- const items: DownloadItem[] = []
24
- if (args.getCaptureEl) {
25
- items.push(
26
- buildPngDownloadItem({
27
- filename: args.filename,
28
- getCaptureEl: args.getCaptureEl,
29
- pixelRatio: args.pngPixelRatio,
30
- backgroundColor: args.pngBackgroundColor,
31
- }),
32
- )
33
- }
34
- items.push(
16
+ export function createPieDownloadConfig(
17
+ args: WidgetDownloadArgs<PieWidgetData> & {
18
+ seriesNames?: readonly string[]
19
+ },
20
+ ): DownloadItem[] {
21
+ return withPngItem(
22
+ args,
35
23
  buildCsvDownloadItem({
36
24
  filename: args.filename,
37
- getRows: () => {
38
- const data = args.getData()
25
+ getRows: (ctx) => {
26
+ const data = args.getData(ctx)
39
27
  const rows: unknown[][] = [['series', 'name', 'value']]
40
28
  for (const [i, series] of data.entries()) {
41
29
  const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
@@ -47,5 +35,4 @@ export function createPieDownloadConfig(args: {
47
35
  },
48
36
  }),
49
37
  )
50
- return items
51
38
  }
@@ -52,7 +52,9 @@ describe('createScatterplotDownloadConfig', () => {
52
52
  filename: 's',
53
53
  getData: () => data,
54
54
  })
55
- const handle = await items.find((i) => i.id === 'csv')!.resolve()
55
+ const handle = await items
56
+ .find((i) => i.id === 'csv')!
57
+ .resolve({ id: null })
56
58
  expect(handle.filename).toBe('s.csv')
57
59
  expect(csvText).toBe(
58
60
  'series,x,y\nseries_1,1,10\nseries_1,2,20\nseries_2,3,30',
@@ -65,7 +67,7 @@ describe('createScatterplotDownloadConfig', () => {
65
67
  getData: () => data,
66
68
  seriesNames: ['A', 'B'],
67
69
  })
68
- await items.find((i) => i.id === 'csv')!.resolve()
70
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
69
71
  expect(csvText.split('\n')[1]).toBe('A,1,10')
70
72
  })
71
73
  })
@@ -1,40 +1,28 @@
1
1
  import {
2
2
  buildCsvDownloadItem,
3
- buildPngDownloadItem,
3
+ withPngItem,
4
4
  type DownloadItem,
5
+ type WidgetDownloadArgs,
5
6
  } from '../actions/download'
6
7
  import type { ScatterplotWidgetData } from './types'
7
8
 
8
9
  /**
9
- * Download menu items for the Scatterplot widget. Always includes a CSV
10
- * item with `series, x, y` columns (one row per point). When
11
- * `getCaptureEl` is supplied, prepends a PNG item that rasterises the
12
- * captured element via `html2canvas`.
10
+ * Download menu items for the Scatterplot widget. Always includes a CSV item
11
+ * with `series, x, y` columns (one row per point). When `getCaptureEl` is
12
+ * given, prepends a PNG item that rasterises the captured element via
13
+ * `html2canvas`.
13
14
  */
14
- export function createScatterplotDownloadConfig(args: {
15
- filename: string
16
- getData: () => ScatterplotWidgetData
17
- seriesNames?: readonly string[]
18
- getCaptureEl?: () => HTMLElement | null
19
- pngPixelRatio?: number
20
- pngBackgroundColor?: string | null
21
- }): DownloadItem[] {
22
- const items: DownloadItem[] = []
23
- if (args.getCaptureEl) {
24
- items.push(
25
- buildPngDownloadItem({
26
- filename: args.filename,
27
- getCaptureEl: args.getCaptureEl,
28
- pixelRatio: args.pngPixelRatio,
29
- backgroundColor: args.pngBackgroundColor,
30
- }),
31
- )
32
- }
33
- items.push(
15
+ export function createScatterplotDownloadConfig(
16
+ args: WidgetDownloadArgs<ScatterplotWidgetData> & {
17
+ seriesNames?: readonly string[]
18
+ },
19
+ ): DownloadItem[] {
20
+ return withPngItem(
21
+ args,
34
22
  buildCsvDownloadItem({
35
23
  filename: args.filename,
36
- getRows: () => {
37
- const data = args.getData()
24
+ getRows: (ctx) => {
25
+ const data = args.getData(ctx)
38
26
  const rows: unknown[][] = [['series', 'x', 'y']]
39
27
  for (const [i, series] of data.entries()) {
40
28
  const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
@@ -46,5 +34,4 @@ export function createScatterplotDownloadConfig(args: {
46
34
  },
47
35
  }),
48
36
  )
49
- return items
50
37
  }
@@ -55,7 +55,9 @@ describe('createSpreadDownloadConfig', () => {
55
55
  filename: 's',
56
56
  getData: () => data,
57
57
  })
58
- const handle = await items.find((i) => i.id === 'csv')!.resolve()
58
+ const handle = await items
59
+ .find((i) => i.id === 'csv')!
60
+ .resolve({ id: null })
59
61
  expect(handle.filename).toBe('s.csv')
60
62
  expect(csvText).toBe(
61
63
  'series,prefix,min,max,suffix,note\nRevenue,$,1,9,k,q1\n,,0,10,,',
@@ -1,39 +1,26 @@
1
1
  import {
2
2
  buildCsvDownloadItem,
3
- buildPngDownloadItem,
3
+ withPngItem,
4
4
  type DownloadItem,
5
+ type WidgetDownloadArgs,
5
6
  } from '../actions/download'
6
7
  import type { SpreadWidgetData } from './types'
7
8
 
8
9
  /**
9
- * Download menu items for the Spread widget. Always includes a CSV item
10
- * with `series, prefix, min, max, suffix, note` columns (one row per
11
- * entry). When `getCaptureEl` is supplied, prepends a PNG item that
12
- * rasterises the captured element via `html2canvas`.
10
+ * Download menu items for the Spread widget. Always includes a CSV item with
11
+ * `series, prefix, min, max, suffix, note` columns (one row per entry). When
12
+ * `getCaptureEl` is given, prepends a PNG item that rasterises the captured
13
+ * element via `html2canvas`.
13
14
  */
14
- export function createSpreadDownloadConfig(args: {
15
- filename: string
16
- getData: () => SpreadWidgetData
17
- getCaptureEl?: () => HTMLElement | null
18
- pngPixelRatio?: number
19
- pngBackgroundColor?: string | null
20
- }): DownloadItem[] {
21
- const items: DownloadItem[] = []
22
- if (args.getCaptureEl) {
23
- items.push(
24
- buildPngDownloadItem({
25
- filename: args.filename,
26
- getCaptureEl: args.getCaptureEl,
27
- pixelRatio: args.pngPixelRatio,
28
- backgroundColor: args.pngBackgroundColor,
29
- }),
30
- )
31
- }
32
- items.push(
15
+ export function createSpreadDownloadConfig(
16
+ args: WidgetDownloadArgs<SpreadWidgetData>,
17
+ ): DownloadItem[] {
18
+ return withPngItem(
19
+ args,
33
20
  buildCsvDownloadItem({
34
21
  filename: args.filename,
35
- getRows: () => {
36
- const data = args.getData()
22
+ getRows: (ctx) => {
23
+ const data = args.getData(ctx)
37
24
  const rows: unknown[][] = [
38
25
  ['series', 'prefix', 'min', 'max', 'suffix', 'note'],
39
26
  ]
@@ -51,5 +38,4 @@ export function createSpreadDownloadConfig(args: {
51
38
  },
52
39
  }),
53
40
  )
54
- return items
55
41
  }
@@ -29,6 +29,8 @@ export {
29
29
  useWidgetShallow,
30
30
  setCaptureEl,
31
31
  getCaptureEl,
32
+ widgetStoreData,
33
+ widgetCaptureEl,
32
34
  clearAllCaptureEls,
33
35
  setEchartInstance,
34
36
  getEchartInstance,
@@ -20,6 +20,8 @@ import {
20
20
  setEchartInstance,
21
21
  subscribeEchartInstance,
22
22
  unregisterWidgetStore,
23
+ widgetCaptureEl,
24
+ widgetStoreData,
23
25
  } from './widget-store-registry'
24
26
  import type { ECharts } from 'echarts'
25
27
 
@@ -290,3 +292,26 @@ describe('widget-store-registry', () => {
290
292
  })
291
293
  })
292
294
  })
295
+
296
+ describe('download getters', () => {
297
+ beforeEach(() => clearAllWidgetStores())
298
+ afterEach(() => clearAllWidgetStores())
299
+
300
+ it('read the store the menu was opened from, else the given id', () => {
301
+ registerWidgetStore('card', createWidgetStore('card', { data: 'card' }))
302
+ registerWidgetStore('modal', createWidgetStore('modal', { data: 'modal' }))
303
+ const getData = widgetStoreData<string>('card')
304
+ expect(getData({ id: 'modal' })).toBe('modal')
305
+ expect(getData({ id: null })).toBe('card')
306
+ })
307
+
308
+ it('capture element: same rule', () => {
309
+ const card = document.createElement('div')
310
+ const modal = document.createElement('div')
311
+ setCaptureEl('card', card)
312
+ setCaptureEl('modal', modal)
313
+ const getEl = widgetCaptureEl('card')
314
+ expect(getEl({ id: 'modal' })).toBe(modal)
315
+ expect(getEl({ id: null })).toBe(card)
316
+ })
317
+ })
@@ -6,6 +6,7 @@ import { useShallow } from 'zustand/react/shallow'
6
6
  import { isDev } from '@carto/ps-utils'
7
7
  import { pipelineMiddleware } from './pipeline-middleware'
8
8
  import type { WidgetInit, WidgetState, WidgetStoreApi } from './types'
9
+ import type { DownloadContext } from '../actions/download/types'
9
10
 
10
11
  const widgetStores = new Map<string, WidgetStoreApi>()
11
12
 
@@ -86,6 +87,27 @@ export function getCaptureEl(id: string): HTMLElement | null {
86
87
  return captureEls.get(id) ?? null
87
88
  }
88
89
 
90
+ /**
91
+ * `getData` for a `create*DownloadConfig`: the `data` of the store the
92
+ * download menu was opened from (`ctx.id` — the fullscreen dialog's own
93
+ * while it's open), or of widget `id` when the menu sits outside a
94
+ * `<Widget.Provider>`.
95
+ */
96
+ export function widgetStoreData<T>(id: string): (ctx: DownloadContext) => T {
97
+ return (ctx) => getWidgetStore(ctx.id ?? id).getState().data as T
98
+ }
99
+
100
+ /**
101
+ * `getCaptureEl` for a `create*DownloadConfig`: the capture element of the
102
+ * store the download menu was opened from (`ctx.id`), or of widget `id`
103
+ * outside a `<Widget.Provider>`.
104
+ */
105
+ export function widgetCaptureEl(
106
+ id: string,
107
+ ): (ctx: DownloadContext) => HTMLElement | null {
108
+ return (ctx) => getCaptureEl(ctx.id ?? id)
109
+ }
110
+
89
111
  /** @internal — exposed for tests and debugging only. */
90
112
  export function clearAllCaptureEls(): void {
91
113
  captureEls.clear()
@@ -56,7 +56,9 @@ describe('createTableDownloadConfig', () => {
56
56
  getData: () => data,
57
57
  columns,
58
58
  })
59
- const handle = await items.find((i) => i.id === 'csv')!.resolve()
59
+ const handle = await items
60
+ .find((i) => i.id === 'csv')!
61
+ .resolve({ id: null })
60
62
  expect(handle.filename).toBe('t.csv')
61
63
  expect(csvText).toBe('Name,Score\nAlpha,10\nBeta,20')
62
64
  })
@@ -67,7 +69,7 @@ describe('createTableDownloadConfig', () => {
67
69
  getData: () => [{ id: 1, name: '=HYPERLINK("x")', score: 1 }],
68
70
  columns,
69
71
  })
70
- await items.find((i) => i.id === 'csv')!.resolve()
72
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
71
73
  expect(csvText).toBe('Name,Score\n"\'=HYPERLINK(""x"")",1')
72
74
  })
73
75
 
@@ -79,7 +81,7 @@ describe('createTableDownloadConfig', () => {
79
81
  columns,
80
82
  })
81
83
  snapshot = [{ id: 9, name: 'Gamma', score: 99 }]
82
- await items.find((i) => i.id === 'csv')!.resolve()
84
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
83
85
  expect(csvText).toBe('Name,Score\nGamma,99')
84
86
  })
85
87
  })
@@ -1,43 +1,29 @@
1
1
  import {
2
2
  buildCsvDownloadItem,
3
- buildPngDownloadItem,
3
+ withPngItem,
4
4
  type DownloadItem,
5
+ type WidgetDownloadArgs,
5
6
  } from '../actions/download'
6
7
  import { tableDataToCsv } from './helpers'
7
8
  import type { TableColumn, TableWidgetData } from './types'
8
9
 
9
10
  /**
10
- * Download menu items for the Table widget. Always includes a CSV item
11
- * that pulls the latest data from the widget store via `getData()` (so
12
- * locked / sorted / filtered state is honored); headers come from
13
- * `column.label`, cells from `row[column.id]`. When `getCaptureEl` is
14
- * supplied, prepends a PNG item that rasterises the captured element via
15
- * `html2canvas`.
11
+ * Download menu items for the Table widget. Always includes a CSV item that
12
+ * pulls the latest data from the widget store via `getData()` (so locked /
13
+ * sorted / filtered state is honored); headers come from `column.label`,
14
+ * cells from `row[column.id]`. When `getCaptureEl` is given, prepends a PNG
15
+ * item that rasterises the captured element via `html2canvas`.
16
16
  */
17
- export function createTableDownloadConfig(opts: {
18
- filename: string
19
- getData: () => TableWidgetData
20
- columns: readonly TableColumn[]
21
- getCaptureEl?: () => HTMLElement | null
22
- pngPixelRatio?: number
23
- pngBackgroundColor?: string | null
24
- }): DownloadItem[] {
25
- const items: DownloadItem[] = []
26
- if (opts.getCaptureEl) {
27
- items.push(
28
- buildPngDownloadItem({
29
- filename: opts.filename,
30
- getCaptureEl: opts.getCaptureEl,
31
- pixelRatio: opts.pngPixelRatio,
32
- backgroundColor: opts.pngBackgroundColor,
33
- }),
34
- )
35
- }
36
- items.push(
17
+ export function createTableDownloadConfig(
18
+ opts: WidgetDownloadArgs<TableWidgetData> & {
19
+ columns: readonly TableColumn[]
20
+ },
21
+ ): DownloadItem[] {
22
+ return withPngItem(
23
+ opts,
37
24
  buildCsvDownloadItem({
38
25
  filename: opts.filename,
39
- getCsv: () => tableDataToCsv(opts.getData(), opts.columns),
26
+ getCsv: (ctx) => tableDataToCsv(opts.getData(ctx), opts.columns),
40
27
  }),
41
28
  )
42
- return items
43
29
  }
@@ -53,7 +53,9 @@ describe('createTimeseriesDownloadConfig', () => {
53
53
  filename: 't',
54
54
  getData: () => data,
55
55
  })
56
- const handle = await items.find((i) => i.id === 'csv')!.resolve()
56
+ const handle = await items
57
+ .find((i) => i.id === 'csv')!
58
+ .resolve({ id: null })
57
59
  expect(handle.filename).toBe('t.csv')
58
60
  expect(csvText).toBe(
59
61
  'time,series_1,series_2\n' +
@@ -69,7 +71,7 @@ describe('createTimeseriesDownloadConfig', () => {
69
71
  filename: 't',
70
72
  getData: () => data,
71
73
  })
72
- await items.find((i) => i.id === 'csv')!.resolve()
74
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
73
75
  expect(csvText).toContain('2024-01-01T00:00:00.000Z,7')
74
76
  })
75
77
 
@@ -79,7 +81,7 @@ describe('createTimeseriesDownloadConfig', () => {
79
81
  filename: 't',
80
82
  getData: () => data,
81
83
  })
82
- await items.find((i) => i.id === 'csv')!.resolve()
84
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
83
85
  expect(csvText).toContain('2024-Q1,1')
84
86
  })
85
87
 
@@ -89,7 +91,7 @@ describe('createTimeseriesDownloadConfig', () => {
89
91
  getData: () => [[{ name: '2024', value: 1 }]],
90
92
  seriesNames: ['revenue'],
91
93
  })
92
- await items.find((i) => i.id === 'csv')!.resolve()
94
+ await items.find((i) => i.id === 'csv')!.resolve({ id: null })
93
95
  expect(csvText.split('\n')[0]).toBe('time,revenue')
94
96
  })
95
97
  })
@@ -1,41 +1,29 @@
1
1
  import {
2
2
  buildCsvDownloadItem,
3
- buildPngDownloadItem,
3
+ withPngItem,
4
4
  type DownloadItem,
5
+ type WidgetDownloadArgs,
5
6
  } from '../actions/download'
6
7
  import type { TimeseriesWidgetData } from './types'
7
8
 
8
9
  /**
9
- * Download menu items for the Timeseries widget. Always includes a CSV
10
- * item with `time, series_1, series_2, …` columns (one row per unique time
11
- * across all series; ISO-8601 strings for `Date`/numeric times). When
12
- * `getCaptureEl` is supplied, prepends a PNG item that rasterises the
13
- * captured element via `html2canvas`.
10
+ * Download menu items for the Timeseries widget. Always includes a CSV item
11
+ * with `time, series_1, series_2, …` columns (one row per unique time across
12
+ * all series; ISO-8601 strings for `Date`/numeric times). When
13
+ * `getCaptureEl` is given, prepends a PNG item that rasterises the captured
14
+ * element via `html2canvas`.
14
15
  */
15
- export function createTimeseriesDownloadConfig(args: {
16
- filename: string
17
- getData: () => TimeseriesWidgetData
18
- seriesNames?: readonly string[]
19
- getCaptureEl?: () => HTMLElement | null
20
- pngPixelRatio?: number
21
- pngBackgroundColor?: string | null
22
- }): DownloadItem[] {
23
- const items: DownloadItem[] = []
24
- if (args.getCaptureEl) {
25
- items.push(
26
- buildPngDownloadItem({
27
- filename: args.filename,
28
- getCaptureEl: args.getCaptureEl,
29
- pixelRatio: args.pngPixelRatio,
30
- backgroundColor: args.pngBackgroundColor,
31
- }),
32
- )
33
- }
34
- items.push(
16
+ export function createTimeseriesDownloadConfig(
17
+ args: WidgetDownloadArgs<TimeseriesWidgetData> & {
18
+ seriesNames?: readonly string[]
19
+ },
20
+ ): DownloadItem[] {
21
+ return withPngItem(
22
+ args,
35
23
  buildCsvDownloadItem({
36
24
  filename: args.filename,
37
- getRows: () => {
38
- const data = args.getData()
25
+ getRows: (ctx) => {
26
+ const data = args.getData(ctx)
39
27
  const seriesCount = data.length
40
28
 
41
29
  // Collect every unique time, preserving insertion order.
@@ -72,7 +60,6 @@ export function createTimeseriesDownloadConfig(args: {
72
60
  },
73
61
  }),
74
62
  )
75
- return items
76
63
  }
77
64
 
78
65
  function formatTime(v: Date | number | string): string {
@@ -56,7 +56,7 @@ export interface ToolboxProps {
56
56
  * Smart overflow toolbar with **single-mount** items. Children mount once
57
57
  * into a stable, hidden host element and are *physically reparented* via
58
58
  * `appendChild` between the inline preview area and the overflow `<Paper>`
59
- * as the popper opens / closes — same pattern as `Widget.FullScreen.Slot`.
59
+ * as the popper opens / closes.
60
60
  * Items with no inline space when closed are kept mounted but visually
61
61
  * hidden via `display: none`.
62
62
  *
@@ -57,14 +57,50 @@ export const styles = {
57
57
  height: ({ spacing }: Theme) => spacing(0.35),
58
58
  zIndex: 1,
59
59
  },
60
+ // Title button, then Actions / Options, then the chevron, as siblings.
61
+ // Same spacing as when they all sat inside the summary: its 16px end
62
+ // padding moves to the row, the content's 6px end padding sits before
63
+ // the chevron.
64
+ header: {
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ gap: ({ spacing }: Theme) => spacing(0.5),
68
+ minHeight: ({ spacing }: Theme) => spacing(7),
69
+ paddingInlineEnd: ({ spacing }: Theme) => spacing(2),
70
+ },
60
71
  summary: {
72
+ flex: 1,
73
+ minWidth: 0,
61
74
  minHeight: ({ spacing }: Theme) => spacing(7),
75
+ paddingInlineEnd: 0,
62
76
  '& .MuiAccordionSummary-content': {
63
- gap: ({ spacing }: Theme) => spacing(0.5),
64
- paddingInlineEnd: ({ spacing }: Theme) => spacing(0.75),
65
77
  alignItems: 'center',
78
+ minWidth: 0,
79
+ marginInlineEnd: 0,
80
+ paddingInlineEnd: 0,
66
81
  },
67
82
  },
83
+ // No chevron (disabled): keep the actions where they sat when the
84
+ // chevron's slot was empty — 10px further from the edge.
85
+ headerNoIcon: {
86
+ paddingInlineEnd: ({ spacing }: Theme) => spacing(3.25),
87
+ },
88
+ // Looks like the icon the summary used to render in its
89
+ // `expandIconWrapper`: 18px, secondary text colour, rotated when open.
90
+ expandIcon: {
91
+ display: 'flex',
92
+ marginInlineStart: ({ spacing }: Theme) => spacing(0.75),
93
+ color: 'text.secondary',
94
+ cursor: 'pointer',
95
+ '& svg': { width: 18, height: 18, color: 'inherit' },
96
+ transition: ({ transitions }: Theme) =>
97
+ transitions.create('transform', {
98
+ duration: transitions.duration.shortest,
99
+ }),
100
+ },
101
+ expanded: {
102
+ transform: 'rotate(180deg)',
103
+ },
68
104
  // Disabled state — only the collapse toggle is inert (enforced by the
69
105
  // guarded `onChange` handler, not `pointer-events`). Actions and Options in
70
106
  // the summary row, and the details panel, all keep their events. We just
@@ -14,8 +14,8 @@ export interface ActionsProps {
14
14
  * pointer devices), and stay full-opacity when a descendant is marked with
15
15
  * `.active` — typically applied by toggled actions.
16
16
  *
17
- * The container also stops click propagation so interacting with an action
18
- * doesn't toggle the wrapper's collapse.
17
+ * The container also stops click propagation, so a click on an action never
18
+ * reaches handlers further up the widget.
19
19
  */
20
20
  export function Actions({ sx, children }: ActionsProps) {
21
21
  return (