@carto/ps-react-ui 4.9.1 → 4.11.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 (207) hide show
  1. package/dist/category-Dnd2_j0x.js +719 -0
  2. package/dist/category-Dnd2_j0x.js.map +1 -0
  3. package/dist/change-column-DjjwoPt1.js +1143 -0
  4. package/dist/change-column-DjjwoPt1.js.map +1 -0
  5. package/dist/chat.js +1507 -0
  6. package/dist/chat.js.map +1 -0
  7. package/dist/components.js +122 -120
  8. package/dist/components.js.map +1 -1
  9. package/dist/copy-button-DGL1tyli.js +26 -0
  10. package/dist/copy-button-DGL1tyli.js.map +1 -0
  11. package/dist/{data-zoom-layout-0QSptXG_.js → data-zoom-layout-CkVnm6ej.js} +3 -3
  12. package/dist/{data-zoom-layout-0QSptXG_.js.map → data-zoom-layout-CkVnm6ej.js.map} +1 -1
  13. package/dist/{download-config-CzmjOT2T.js → download-config-oJIFZ2WC.js} +9 -8
  14. package/dist/{download-config-CzmjOT2T.js.map → download-config-oJIFZ2WC.js.map} +1 -1
  15. package/dist/{png-item-CS4z1iSH.js → png-item-BE9uEqlD.js} +2 -2
  16. package/dist/png-item-BE9uEqlD.js.map +1 -0
  17. package/dist/{spread-Y9R1f5dm.js → spread-DYNpzgh_.js} +10 -11
  18. package/dist/{spread-Y9R1f5dm.js.map → spread-DYNpzgh_.js.map} +1 -1
  19. package/dist/{table-CQCAnDLb.js → table-C9IMbTr0.js} +50 -53
  20. package/dist/table-C9IMbTr0.js.map +1 -0
  21. package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
  22. package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
  23. package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
  24. package/dist/types/chat/bubbles/index.d.ts +4 -0
  25. package/dist/types/chat/const.d.ts +4 -0
  26. package/dist/types/chat/containers/chat-content.d.ts +2 -0
  27. package/dist/types/chat/containers/chat-footer.d.ts +2 -0
  28. package/dist/types/chat/containers/chat-header.d.ts +2 -0
  29. package/dist/types/chat/containers/chat-starter.d.ts +2 -0
  30. package/dist/types/chat/containers/index.d.ts +4 -0
  31. package/dist/types/chat/containers/styles.d.ts +93 -0
  32. package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
  33. package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
  34. package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
  35. package/dist/types/chat/feedback/chat-tool-code-area.d.ts +2 -0
  36. package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
  37. package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
  38. package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
  39. package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
  40. package/dist/types/chat/feedback/index.d.ts +8 -0
  41. package/dist/types/chat/feedback/styles.d.ts +211 -0
  42. package/dist/types/chat/index.d.ts +20 -0
  43. package/dist/types/chat/types.d.ts +184 -0
  44. package/dist/types/chat/use-typewriter.d.ts +30 -0
  45. package/dist/types/components/copy-button/copy-button.d.ts +2 -0
  46. package/dist/types/components/copy-button/types.d.ts +6 -0
  47. package/dist/types/components/index.d.ts +2 -0
  48. package/dist/types/widgets/actions/brush-toggle/style.d.ts +1 -1
  49. package/dist/types/widgets/actions/shared/styles.d.ts +1 -1
  50. package/dist/types/widgets/actions/zoom-toggle/style.d.ts +1 -1
  51. package/dist/types/widgets/echart/types.d.ts +1 -1
  52. package/dist/types/widgets/toolbar-actions/styles.d.ts +1 -1
  53. package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +1 -1
  54. package/dist/types/widgets-v2/actions/change-column/style.d.ts +1 -1
  55. package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +1 -1
  56. package/dist/types/widgets-v2/actions/index.d.ts +1 -0
  57. package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +1 -1
  58. package/dist/types/widgets-v2/actions/relative-data/style.d.ts +1 -1
  59. package/dist/types/widgets-v2/actions/searcher/style.d.ts +1 -1
  60. package/dist/types/widgets-v2/actions/show-all/index.d.ts +2 -0
  61. package/dist/types/widgets-v2/actions/show-all/labels.d.ts +5 -0
  62. package/dist/types/widgets-v2/actions/show-all/show-all.d.ts +33 -0
  63. package/dist/types/widgets-v2/actions/show-all/style.d.ts +8 -0
  64. package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +1 -1
  65. package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +1 -1
  66. package/dist/types/widgets-v2/category/category-ui.d.ts +9 -2
  67. package/dist/types/widgets-v2/category/category.d.ts +9 -2
  68. package/dist/types/widgets-v2/category/components/category-row-other.d.ts +19 -6
  69. package/dist/types/widgets-v2/category/style.d.ts +21 -2
  70. package/dist/types/widgets-v2/category/types.d.ts +2 -0
  71. package/dist/types/widgets-v2/index.d.ts +3 -2
  72. package/dist/types/widgets-v2/selection-summary/labels.d.ts +7 -2
  73. package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +13 -6
  74. package/dist/types/widgets-v2/selection-summary/style.d.ts +15 -0
  75. package/dist/widgets/actions.js +115 -114
  76. package/dist/widgets/actions.js.map +1 -1
  77. package/dist/widgets/bar.js +1 -1
  78. package/dist/widgets/category.js +9 -8
  79. package/dist/widgets/category.js.map +1 -1
  80. package/dist/widgets/formula.js +11 -10
  81. package/dist/widgets/formula.js.map +1 -1
  82. package/dist/widgets/histogram.js +7 -6
  83. package/dist/widgets/histogram.js.map +1 -1
  84. package/dist/widgets/markdown.js +9 -8
  85. package/dist/widgets/markdown.js.map +1 -1
  86. package/dist/widgets/pie.js +1 -1
  87. package/dist/widgets/scatterplot.js +1 -1
  88. package/dist/widgets/spread.js +9 -8
  89. package/dist/widgets/spread.js.map +1 -1
  90. package/dist/widgets/table.js +17 -16
  91. package/dist/widgets/table.js.map +1 -1
  92. package/dist/widgets/timeseries.js +1 -1
  93. package/dist/widgets/utils.js +1 -1
  94. package/dist/widgets/wrapper.js +3 -2
  95. package/dist/widgets/wrapper.js.map +1 -1
  96. package/dist/widgets-v2/actions.js +41 -37
  97. package/dist/widgets-v2/bar.js +9 -10
  98. package/dist/widgets-v2/bar.js.map +1 -1
  99. package/dist/widgets-v2/category.js +25 -26
  100. package/dist/widgets-v2/category.js.map +1 -1
  101. package/dist/widgets-v2/formula.js +3 -3
  102. package/dist/widgets-v2/histogram.js +11 -13
  103. package/dist/widgets-v2/histogram.js.map +1 -1
  104. package/dist/widgets-v2/markdown.js +26 -27
  105. package/dist/widgets-v2/markdown.js.map +1 -1
  106. package/dist/widgets-v2/pie.js +8 -10
  107. package/dist/widgets-v2/pie.js.map +1 -1
  108. package/dist/widgets-v2/scatterplot.js +10 -12
  109. package/dist/widgets-v2/scatterplot.js.map +1 -1
  110. package/dist/widgets-v2/spread.js +15 -16
  111. package/dist/widgets-v2/spread.js.map +1 -1
  112. package/dist/widgets-v2/table.js +39 -40
  113. package/dist/widgets-v2/table.js.map +1 -1
  114. package/dist/widgets-v2/timeseries.js +9 -11
  115. package/dist/widgets-v2/timeseries.js.map +1 -1
  116. package/dist/widgets-v2/utils.js +1 -1
  117. package/dist/widgets-v2.js +284 -282
  118. package/dist/widgets-v2.js.map +1 -1
  119. package/package.json +5 -1
  120. package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
  121. package/src/chat/bubbles/chat-agent-message.tsx +11 -0
  122. package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
  123. package/src/chat/bubbles/chat-error-message.tsx +47 -0
  124. package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
  125. package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
  126. package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
  127. package/src/chat/bubbles/chat-user-message.tsx +27 -0
  128. package/src/chat/bubbles/index.ts +4 -0
  129. package/src/chat/bubbles/styles.ts +148 -0
  130. package/src/chat/const.ts +4 -0
  131. package/src/chat/containers/chat-content.test.tsx +269 -0
  132. package/src/chat/containers/chat-content.tsx +142 -0
  133. package/src/chat/containers/chat-footer.test.tsx +34 -0
  134. package/src/chat/containers/chat-footer.tsx +78 -0
  135. package/src/chat/containers/chat-header.test.tsx +28 -0
  136. package/src/chat/containers/chat-header.tsx +29 -0
  137. package/src/chat/containers/chat-starter.test.tsx +32 -0
  138. package/src/chat/containers/chat-starter.tsx +75 -0
  139. package/src/chat/containers/index.ts +4 -0
  140. package/src/chat/containers/styles.ts +96 -0
  141. package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
  142. package/src/chat/feedback/chat-actions-container.tsx +7 -0
  143. package/src/chat/feedback/chat-loader.test.tsx +10 -0
  144. package/src/chat/feedback/chat-loader.tsx +31 -0
  145. package/src/chat/feedback/chat-rating-action.tsx +43 -0
  146. package/src/chat/feedback/chat-thinking.test.tsx +15 -0
  147. package/src/chat/feedback/chat-thinking.tsx +23 -0
  148. package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
  149. package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
  150. package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
  151. package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
  152. package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
  153. package/src/chat/feedback/chat-tool-group.tsx +156 -0
  154. package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
  155. package/src/chat/feedback/chat-tool-trace.tsx +192 -0
  156. package/src/chat/feedback/get-tool-label.test.tsx +91 -0
  157. package/src/chat/feedback/get-tool-label.ts +13 -0
  158. package/src/chat/feedback/index.ts +8 -0
  159. package/src/chat/feedback/styles.ts +229 -0
  160. package/src/chat/index.ts +59 -0
  161. package/src/chat/types.ts +215 -0
  162. package/src/chat/use-typewriter.test.tsx +38 -0
  163. package/src/chat/use-typewriter.ts +82 -0
  164. package/src/components/copy-button/copy-button.test.tsx +41 -0
  165. package/src/components/copy-button/copy-button.tsx +31 -0
  166. package/src/components/copy-button/types.ts +10 -0
  167. package/src/components/index.ts +3 -0
  168. package/src/widgets/echart/types.ts +1 -1
  169. package/src/widgets-v2/actions/brush-toggle/brush-toggle.tsx +1 -1
  170. package/src/widgets-v2/actions/change-column/sortable-column-item.tsx +1 -1
  171. package/src/widgets-v2/actions/download/download.tsx +1 -1
  172. package/src/widgets-v2/actions/download/icons.tsx +1 -1
  173. package/src/widgets-v2/actions/fullscreen/fullscreen.tsx +3 -3
  174. package/src/widgets-v2/actions/index.ts +8 -0
  175. package/src/widgets-v2/actions/lock-selection/lock-selection.tsx +2 -2
  176. package/src/widgets-v2/actions/relative-data/relative-data.tsx +1 -1
  177. package/src/widgets-v2/actions/searcher/searcher-toggle.tsx +1 -1
  178. package/src/widgets-v2/actions/searcher/searcher.tsx +2 -2
  179. package/src/widgets-v2/actions/show-all/index.ts +7 -0
  180. package/src/widgets-v2/actions/show-all/labels.ts +8 -0
  181. package/src/widgets-v2/actions/show-all/show-all.test.tsx +50 -0
  182. package/src/widgets-v2/actions/show-all/show-all.tsx +72 -0
  183. package/src/widgets-v2/actions/show-all/style.ts +8 -0
  184. package/src/widgets-v2/actions/stack-toggle/stack-toggle.tsx +1 -1
  185. package/src/widgets-v2/actions/zoom-toggle/zoom-toggle.tsx +1 -1
  186. package/src/widgets-v2/category/category-ui.test.tsx +26 -10
  187. package/src/widgets-v2/category/category-ui.tsx +13 -3
  188. package/src/widgets-v2/category/category.test.tsx +4 -4
  189. package/src/widgets-v2/category/category.tsx +10 -1
  190. package/src/widgets-v2/category/components/category-row-other.test.tsx +36 -7
  191. package/src/widgets-v2/category/components/category-row-other.tsx +64 -13
  192. package/src/widgets-v2/category/style.ts +35 -4
  193. package/src/widgets-v2/category/types.ts +2 -0
  194. package/src/widgets-v2/index.ts +3 -0
  195. package/src/widgets-v2/selection-summary/labels.ts +8 -4
  196. package/src/widgets-v2/selection-summary/selection-summary.test.tsx +15 -9
  197. package/src/widgets-v2/selection-summary/selection-summary.tsx +42 -22
  198. package/src/widgets-v2/selection-summary/style.ts +15 -0
  199. package/src/widgets-v2/table/table-ui.tsx +4 -4
  200. package/src/widgets-v2/toolbox/toolbox.tsx +1 -1
  201. package/src/widgets-v2/wrapper/widget-wrapper.tsx +1 -1
  202. package/dist/category-DwaeYjpX.js +0 -656
  203. package/dist/category-DwaeYjpX.js.map +0 -1
  204. package/dist/change-column-B4IT0rh6.js +0 -1110
  205. package/dist/change-column-B4IT0rh6.js.map +0 -1
  206. package/dist/png-item-CS4z1iSH.js.map +0 -1
  207. package/dist/table-CQCAnDLb.js.map +0 -1
@@ -40,7 +40,7 @@ export interface CategoryProps {
40
40
  /** Per-series metadata. Enables the legend + overrides palette per index. */
41
41
  series?: readonly CategorySeriesConfig[]
42
42
  /**
43
- * Cap visible rows; overflow folds into "Other (X more)". Default 20
43
+ * Cap visible rows; overflow folds into an "Others <count>" row. Default 20
44
44
  * (when omitted). Pass `0` to swap the cap for a scrollable viewport
45
45
  * (composers use this when the user opens the SearcherToggle —
46
46
  * `maxItems = searcherOpen ? 0 : userMaxItems`). Pass `null` to disable
@@ -49,6 +49,13 @@ export interface CategoryProps {
49
49
  maxItems?: number | null
50
50
  /** Labels for the "Other" overflow row. */
51
51
  labels?: CategoryLabels
52
+ /**
53
+ * When provided, the "Other" overflow row becomes a button that fires this
54
+ * callback. Composers wire it to expand the widget (e.g. flip the
55
+ * `show-all` flag) so every category is shown. Forwarded to
56
+ * {@link CategoryUI}.
57
+ */
58
+ onShowAll?: () => void
52
59
  /**
53
60
  * Manual override for the bar-width denominator. When omitted, the
54
61
  * bridge auto-fills from the widget store's `rawData` so bar widths
@@ -85,6 +92,7 @@ export function Category({
85
92
  series,
86
93
  maxItems,
87
94
  labels,
95
+ onShowAll,
88
96
  maxOverride,
89
97
  size,
90
98
  stacked,
@@ -113,6 +121,7 @@ export function Category({
113
121
  series={series}
114
122
  maxItems={maxItems}
115
123
  labels={labels}
124
+ onShowAll={onShowAll}
116
125
  maxOverride={effectiveMaxOverride}
117
126
  size={size}
118
127
  stacked={stacked}
@@ -1,12 +1,13 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render, screen } from '@testing-library/react'
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
3
  import { CategoryRowOther } from './category-row-other'
4
4
 
5
5
  describe('<CategoryRowOther>', () => {
6
- it('renders the default labels with substituted count', () => {
6
+ it('renders the default label + bare count (no parentheses)', () => {
7
7
  render(<CategoryRowOther hiddenCount={7} />)
8
- expect(screen.getByText('Other')).toBeTruthy()
9
- expect(screen.getByText('(7 more)')).toBeTruthy()
8
+ expect(screen.getByText('Others')).toBeTruthy()
9
+ expect(screen.getByText('7')).toBeTruthy()
10
+ expect(screen.queryByText('(7 more)')).toBeNull()
10
11
  })
11
12
 
12
13
  it('honors custom labels with {count} placeholder', () => {
@@ -18,11 +19,39 @@ describe('<CategoryRowOther>', () => {
18
19
  />,
19
20
  )
20
21
  expect(screen.getByText('Hidden')).toBeTruthy()
21
- expect(screen.getByText('(+3 not shown)')).toBeTruthy()
22
+ expect(screen.getByText('+3 not shown')).toBeTruthy()
22
23
  })
23
24
 
24
- it('is not a button (non-interactive)', () => {
25
+ it('is not a button when onShowAll is omitted (static fallback)', () => {
25
26
  const { container } = render(<CategoryRowOther hiddenCount={1} />)
26
27
  expect(container.querySelectorAll('[role="button"]').length).toBe(0)
27
28
  })
29
+
30
+ describe('when onShowAll is provided', () => {
31
+ it('renders as a button', () => {
32
+ render(<CategoryRowOther hiddenCount={4} onShowAll={vi.fn()} />)
33
+ expect(screen.getByRole('button')).toBeTruthy()
34
+ })
35
+
36
+ it('fires onShowAll on click', () => {
37
+ const onShowAll = vi.fn()
38
+ render(<CategoryRowOther hiddenCount={4} onShowAll={onShowAll} />)
39
+ fireEvent.click(screen.getByRole('button'))
40
+ expect(onShowAll).toHaveBeenCalledTimes(1)
41
+ })
42
+
43
+ it('fires onShowAll on Enter and Space', () => {
44
+ const onShowAll = vi.fn()
45
+ render(<CategoryRowOther hiddenCount={4} onShowAll={onShowAll} />)
46
+ const btn = screen.getByRole('button')
47
+ fireEvent.keyDown(btn, { key: 'Enter' })
48
+ fireEvent.keyDown(btn, { key: ' ' })
49
+ expect(onShowAll).toHaveBeenCalledTimes(2)
50
+ })
51
+
52
+ it('is keyboard-focusable', () => {
53
+ render(<CategoryRowOther hiddenCount={4} onShowAll={vi.fn()} />)
54
+ expect(screen.getByRole('button').getAttribute('tabindex')).toBe('0')
55
+ })
56
+ })
28
57
  })
@@ -1,33 +1,84 @@
1
+ import type { KeyboardEvent } from 'react'
1
2
  import { Box, Typography } from '@mui/material'
3
+ import { Tooltip } from '../../../components'
2
4
  import { styles } from '../style'
3
5
 
4
6
  export interface CategoryRowOtherProps {
5
7
  hiddenCount: number
6
- /** Label for the overflow row. Defaults to `'Other'`. */
8
+ /** Label for the overflow row. Defaults to `'Others'`. */
7
9
  otherLabel?: string
8
- /** Count text with `{count}` placeholder. Defaults to `'{count} more'`. */
10
+ /** Count text with `{count}` placeholder. Defaults to `'{count}'`. */
9
11
  otherCountLabel?: string
12
+ /**
13
+ * When provided, the row becomes a button: clicking it (or Enter/Space)
14
+ * fires `onShowAll`, expanding the widget to reveal every category. When
15
+ * omitted the row renders as a static, non-interactive summary.
16
+ */
17
+ onShowAll?: () => void
18
+ /** Tooltip shown over the button form. Defaults to `'Show all'`. */
19
+ showAllLabel?: string
10
20
  }
11
21
 
12
22
  /**
13
- * Static "Other (X more)" overflow summary row. Rendered after the last
14
- * visible category row when `data.length > maxItems`. Non-interactive
15
- * no selection, no keyboard target.
23
+ * Overflow summary row rendered after the last visible category row when
24
+ * `data.length > maxItems`. Shows `Others <count>` (count muted, no
25
+ * parentheses).
26
+ *
27
+ * When `onShowAll` is supplied the row is an interactive button — clicking
28
+ * it (or Enter/Space) expands the widget to show all categories, mirroring
29
+ * the Searcher's full-list view. Without `onShowAll` it stays a static,
30
+ * non-interactive summary (backward-compatible default).
16
31
  */
17
32
  export function CategoryRowOther({
18
33
  hiddenCount,
19
- otherLabel = 'Other',
20
- otherCountLabel = '{count} more',
34
+ otherLabel = 'Others',
35
+ otherCountLabel = '{count}',
36
+ onShowAll,
37
+ showAllLabel = 'Show all',
21
38
  }: CategoryRowOtherProps) {
22
39
  const countText = otherCountLabel.replace('{count}', String(hiddenCount))
40
+
41
+ const count = (
42
+ <Typography variant='body2' sx={styles.otherCount}>
43
+ {countText}
44
+ </Typography>
45
+ )
46
+
47
+ if (!onShowAll) {
48
+ return (
49
+ <Box sx={styles.otherRow}>
50
+ <Typography variant='body2' sx={styles.otherLabel}>
51
+ {otherLabel}
52
+ </Typography>
53
+ {count}
54
+ </Box>
55
+ )
56
+ }
57
+
58
+ const handleKey = (e: KeyboardEvent<HTMLSpanElement>): void => {
59
+ if (e.key === 'Enter' || e.key === ' ') {
60
+ e.preventDefault()
61
+ onShowAll()
62
+ }
63
+ }
64
+
65
+ // Only the label is the click target; the count stays static beside it.
23
66
  return (
24
67
  <Box sx={styles.otherRow}>
25
- <Typography variant='body2' sx={styles.otherLabel}>
26
- {otherLabel}
27
- </Typography>
28
- <Typography variant='body2' sx={styles.otherCount}>
29
- ({countText})
30
- </Typography>
68
+ <Tooltip title={showAllLabel}>
69
+ <Typography
70
+ variant='body2'
71
+ component='span'
72
+ role='button'
73
+ tabIndex={0}
74
+ onClick={onShowAll}
75
+ onKeyDown={handleKey}
76
+ sx={{ ...styles.otherLabel, ...styles.otherLabelButton }}
77
+ >
78
+ {otherLabel}
79
+ </Typography>
80
+ </Tooltip>
81
+ {count}
31
82
  </Box>
32
83
  )
33
84
  }
@@ -78,6 +78,13 @@ export const styles = {
78
78
  gap: 1,
79
79
  py: 0.5,
80
80
  },
81
+ // Layered on `list` only in scroll mode (`maxItems === 0`): right padding
82
+ // so the bars + right-aligned values clear the scrollbar. Right side only
83
+ // — the left edge stays at 0 so content stays aligned with the capped
84
+ // view. `overflowY` + the dynamic `maxHeight` remain on the inline style.
85
+ listScroll: {
86
+ pr: 1,
87
+ },
81
88
 
82
89
  // ── Single-series row ─────────────────────────────────────────────
83
90
  // v1 layout: label + value share a top flex row (justify-between); the
@@ -266,20 +273,44 @@ export const styles = {
266
273
  },
267
274
 
268
275
  // ── "Other" overflow row ──────────────────────────────────────────
276
+ // Left-aligned `Others <count>` — the label sits next to a muted count
277
+ // (no parentheses). Only the label is the click target when interactive;
278
+ // the count is always static text beside it.
269
279
  otherRow: {
270
280
  display: 'flex',
271
- justifyContent: 'space-between',
272
281
  alignItems: 'center',
282
+ gap: 1,
273
283
  py: 0.5,
274
284
  px: 0.5,
275
285
  },
286
+ // Interactive affordances layered onto the "Others" label only (cursor,
287
+ // hover tint, focus ring). A negative LEFT margin + matching padding keep
288
+ // the hover pill breathing without shifting the label's text edge (it
289
+ // stays aligned with the category rows above). No right margin, so the
290
+ // row `gap` is preserved between the pill and the count.
291
+ otherLabelButton: {
292
+ cursor: 'pointer',
293
+ userSelect: 'none',
294
+ borderRadius: 1,
295
+ px: 0.5,
296
+ ml: -0.5,
297
+ transition: 'background-color 80ms ease',
298
+ '&:hover': {
299
+ bgcolor: 'action.hover',
300
+ },
301
+ '&:focus-visible': {
302
+ outline: '2px solid',
303
+ outlineColor: 'primary.main',
304
+ outlineOffset: 2,
305
+ },
306
+ },
276
307
  otherLabel: {
277
- fontStyle: 'italic',
278
- color: 'text.secondary',
308
+ color: 'text.primary',
279
309
  fontWeight: 'medium',
280
310
  },
281
311
  otherCount: {
282
- color: 'text.disabled',
312
+ color: 'text.secondary',
313
+ fontVariantNumeric: 'tabular-nums',
283
314
  },
284
315
  // `bar` / `barFill` are size-keyed (`Record<CategorySize, SxProps>`);
285
316
  // every other entry is a single `SxProps<Theme>`. The mixed shape means
@@ -47,6 +47,8 @@ export type CategorySeriesConfig = WidgetSeries
47
47
  export interface CategoryLabels {
48
48
  other?: string
49
49
  otherCount?: string
50
+ /** Tooltip shown over the clickable "Others" row. Defaults to `'Show all'`. */
51
+ showAll?: string
50
52
  }
51
53
 
52
54
  /**
@@ -22,6 +22,7 @@ import {
22
22
  Searcher,
23
23
  SearcherToggle,
24
24
  StackToggle,
25
+ ShowAllToggle,
25
26
  ZoomToggle,
26
27
  BrushToggle,
27
28
  RelativeData,
@@ -83,6 +84,7 @@ export const Widget = {
83
84
  Searcher,
84
85
  SearcherToggle,
85
86
  StackToggle,
87
+ ShowAllToggle,
86
88
  ZoomToggle,
87
89
  BrushToggle,
88
90
  RelativeData,
@@ -149,6 +151,7 @@ export type {
149
151
  SearcherProps,
150
152
  SearcherToggleProps,
151
153
  StackToggleProps,
154
+ ShowAllToggleProps,
152
155
  ZoomToggleProps,
153
156
  BrushToggleProps,
154
157
  RelativeDataProps,
@@ -1,11 +1,15 @@
1
+ import type { ReactNode } from 'react'
2
+
1
3
  export interface SelectionSummaryLabels {
2
- allSelected: string
3
- selections: (count: number) => string
4
+ /**
5
+ * Optional custom renderer for the count. Defaults to the built-in two-tone
6
+ * `selected / total` rendering. Receives the already-resolved displayed
7
+ * number (which equals `total` when nothing is selected).
8
+ */
9
+ summary?: (selected: number, total: number) => ReactNode
4
10
  clear: string
5
11
  }
6
12
 
7
13
  export const DEFAULT_SELECTION_SUMMARY_LABELS: SelectionSummaryLabels = {
8
- allSelected: 'All selected',
9
- selections: (count) => `${count} selected`,
10
14
  clear: 'Clear',
11
15
  }
@@ -9,15 +9,22 @@ describe('<SelectionSummary>', () => {
9
9
  expect(container.textContent).toBe('')
10
10
  })
11
11
 
12
- it('renders the "All selected" label when count === 0 and total > 0 (R34)', () => {
12
+ it('renders nothing when total is omitted (undefined)', () => {
13
+ const { container } = render(<SelectionSummary count={0} />)
14
+ expect(container.textContent).toBe('')
15
+ })
16
+
17
+ it('renders total / total when nothing is selected (count === 0)', () => {
13
18
  render(<SelectionSummary count={0} total={10} />)
14
- expect(screen.getByText('All selected')).toBeTruthy()
19
+ expect(screen.getByLabelText('10 / 10')).toBeTruthy()
20
+ // No Clear button when nothing is selected.
21
+ expect(screen.queryByText('Clear')).toBeNull()
15
22
  })
16
23
 
17
- it('renders the count + Clear link when count > 0', () => {
24
+ it('renders count / total + Clear when count > 0', () => {
18
25
  const onClear = vi.fn()
19
26
  render(<SelectionSummary count={3} total={10} onClear={onClear} />)
20
- expect(screen.getByText(/3/)).toBeTruthy()
27
+ expect(screen.getByLabelText('3 / 10')).toBeTruthy()
21
28
  fireEvent.click(screen.getByText('Clear'))
22
29
  expect(onClear).toHaveBeenCalledTimes(1)
23
30
  })
@@ -27,24 +34,23 @@ describe('<SelectionSummary>', () => {
27
34
  expect(screen.queryByText('Clear')).toBeNull()
28
35
  })
29
36
 
30
- it('honors custom labels for empty + populated + clear', () => {
37
+ it('honors a custom summary renderer and clear label', () => {
31
38
  render(
32
39
  <SelectionSummary
33
40
  count={3}
34
41
  total={10}
35
42
  labels={{
36
- allSelected: 'Showing all',
37
- selections: (n) => `${n} pinned`,
43
+ summary: (selected, total) => `${selected} of ${total} pinned`,
38
44
  clear: 'Reset',
39
45
  }}
40
46
  onClear={() => undefined}
41
47
  />,
42
48
  )
43
- expect(screen.getByText('3 pinned')).toBeTruthy()
49
+ expect(screen.getByText('3 of 10 pinned')).toBeTruthy()
44
50
  expect(screen.getByText('Reset')).toBeTruthy()
45
51
  })
46
52
 
47
- it('renders the custom icon component when provided in the empty state', () => {
53
+ it('renders the custom icon component when provided', () => {
48
54
  const { container } = render(
49
55
  <SelectionSummary count={0} total={10} icon={StarIcon} />,
50
56
  )
@@ -9,9 +9,9 @@ import { styles } from './style'
9
9
  export interface SelectionSummaryProps {
10
10
  /** Number of currently selected items. */
11
11
  count: number
12
- /** Optional total — when 0 the component renders nothing (no data to summarize). */
12
+ /** Optional total — when `undefined` or `0` the component renders nothing (no data). */
13
13
  total?: number
14
- /** Clear callback. Shown only when count > 0. */
14
+ /** Clear callback. The Clear control is shown only when `count > 0` and `onClear` is provided. */
15
15
  onClear?: () => void
16
16
  labels?: Partial<SelectionSummaryLabels>
17
17
  icon?: ComponentType<SvgIconProps>
@@ -19,11 +19,18 @@ export interface SelectionSummaryProps {
19
19
  }
20
20
 
21
21
  /**
22
+ * Renders a `selected / total` count.
23
+ *
22
24
  * Render rules:
23
- * - `total === 0` → render nothing.
24
- * - `count === 0` → render `labels.allSelected` (matches v1 default),
25
- * prefixed with the optional `icon` glyph when provided.
26
- * - `count > 0` render `labels.selections(count)` + Clear (when `onClear`).
25
+ * - `!total` (undefined or 0) → render nothing (no data to summarize).
26
+ * - `count === 0` → display `total / total` (everything is implicitly in
27
+ * scope); no Clear button.
28
+ * - `count > 0` display `count / total` + Clear (when `onClear` is given).
29
+ *
30
+ * The count is two-tone by default (selected number bold/`text.primary`,
31
+ * ` / total` in `text.secondary`). Pass `labels.summary` to render a custom
32
+ * node instead — it receives the already-resolved displayed number (which
33
+ * equals `total` when nothing is selected).
27
34
  */
28
35
  export function SelectionSummary({
29
36
  count,
@@ -33,26 +40,39 @@ export function SelectionSummary({
33
40
  icon: Icon,
34
41
  iconProps,
35
42
  }: SelectionSummaryProps) {
36
- if (total === 0) return null
43
+ if (!total) return null
37
44
  const _labels = { ...DEFAULT_SELECTION_SUMMARY_LABELS, ...labels }
38
-
39
- if (count === 0) {
40
- return (
41
- <Box sx={styles.root}>
42
- {Icon ? <Icon fontSize='small' {...iconProps} /> : null}
43
- <Typography variant='caption' sx={styles.label}>
44
- {_labels.allSelected}
45
- </Typography>
46
- </Box>
47
- )
48
- }
45
+ const shown = count === 0 ? total : count
49
46
 
50
47
  return (
51
48
  <Box sx={styles.root}>
52
- <Typography variant='caption' sx={styles.label}>
53
- {_labels.selections(count)}
54
- </Typography>
55
- {onClear && (
49
+ {Icon ? <Icon fontSize='small' {...iconProps} /> : null}
50
+ <Box
51
+ component='span'
52
+ sx={styles.count}
53
+ aria-label={`${shown} / ${total}`}
54
+ >
55
+ {_labels.summary ? (
56
+ _labels.summary(shown, total)
57
+ ) : (
58
+ <>
59
+ <Typography component='span' variant='caption' sx={styles.selected}>
60
+ {shown}
61
+ </Typography>
62
+ <Typography
63
+ component='span'
64
+ variant='caption'
65
+ sx={styles.separator}
66
+ >
67
+ /
68
+ </Typography>
69
+ <Typography component='span' variant='caption' sx={styles.total}>
70
+ {total}
71
+ </Typography>
72
+ </>
73
+ )}
74
+ </Box>
75
+ {count > 0 && onClear && (
56
76
  <Button size='small' variant='text' onClick={onClear}>
57
77
  {_labels.clear}
58
78
  </Button>
@@ -10,6 +10,21 @@ export const styles = {
10
10
  label: {
11
11
  color: 'text.secondary',
12
12
  },
13
+ count: {
14
+ display: 'inline-flex',
15
+ alignItems: 'baseline',
16
+ gap: 0.5,
17
+ },
18
+ selected: {
19
+ color: 'text.primary',
20
+ fontWeight: 600,
21
+ },
22
+ separator: {
23
+ color: 'text.secondary',
24
+ },
25
+ total: {
26
+ color: 'text.secondary',
27
+ },
13
28
  clear: {
14
29
  color: 'primary.main',
15
30
  cursor: 'pointer',
@@ -14,10 +14,10 @@ import {
14
14
  type TableProps as MuiTableProps,
15
15
  } from '@mui/material'
16
16
  import type { TablePaginationActionsProps } from '@mui/material/TablePagination/TablePaginationActions'
17
- import FirstPageIcon from '@mui/icons-material/FirstPage'
18
- import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'
19
- import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'
20
- import LastPageIcon from '@mui/icons-material/LastPage'
17
+ import { FirstPage as FirstPageIcon } from '@mui/icons-material'
18
+ import { KeyboardArrowLeft } from '@mui/icons-material'
19
+ import { KeyboardArrowRight } from '@mui/icons-material'
20
+ import { LastPage as LastPageIcon } from '@mui/icons-material'
21
21
  import { DEFAULT_TABLE_LABELS, type TableLabels } from './labels'
22
22
  import {
23
23
  DEFAULT_TABLE_PAGE_SIZE_OPTIONS,
@@ -22,7 +22,7 @@ import {
22
22
  type SxProps,
23
23
  type Theme,
24
24
  } from '@mui/material'
25
- import CloseIcon from '@mui/icons-material/Close'
25
+ import { Close as CloseIcon } from '@mui/icons-material'
26
26
  import { WidgetOptions } from '@carto/meridian-ds/custom-icons'
27
27
  import { Tooltip } from '../../components'
28
28
  import { DEFAULT_TOOLBOX_LABELS, type ToolboxLabels } from './labels'
@@ -18,7 +18,7 @@ import {
18
18
  type SxProps,
19
19
  type Theme,
20
20
  } from '@mui/material'
21
- import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
21
+ import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material'
22
22
  import { SmartTooltip } from '../../components/smart-tooltip/smart-tooltip'
23
23
  import { useWidget, useWidgetId } from '../stores'
24
24
  import { Actions, Options } from './widget-actions'