@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
@@ -0,0 +1,82 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ interface UseTypewriterOptions {
4
+ /** Characters revealed per second (default: `500`). */
5
+ speed?: number
6
+ /** When true on mount, skip the animation and reveal the full text immediately. */
7
+ skipAnimation?: boolean
8
+ }
9
+
10
+ interface UseTypewriterResult {
11
+ /** The portion of `fullText` revealed so far. */
12
+ displayedText: string
13
+ /** `true` while characters are still being revealed. */
14
+ isTyping: boolean
15
+ }
16
+
17
+ /**
18
+ * Reveals a string character-by-character at a steady rate via
19
+ * `requestAnimationFrame`. Useful for smoothing out bursty WebSocket-streamed
20
+ * agent message text — pair it with `ChatAgentMessage`.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const { displayedText, isTyping } = useTypewriter(message)
25
+ * return (
26
+ * <ChatAgentMessage>
27
+ * <Markdown>{displayedText}</Markdown>
28
+ * {isTyping ? <Cursor /> : null}
29
+ * </ChatAgentMessage>
30
+ * )
31
+ * ```
32
+ */
33
+ export function useTypewriter(
34
+ fullText: string,
35
+ options: UseTypewriterOptions = {},
36
+ ): UseTypewriterResult {
37
+ const { speed = 500, skipAnimation = false } = options
38
+
39
+ const skipRef = useRef(skipAnimation)
40
+
41
+ const [charIndex, setCharIndex] = useState(() =>
42
+ skipRef.current ? fullText.length : 0,
43
+ )
44
+
45
+ useEffect(() => {
46
+ if (skipRef.current) {
47
+ setCharIndex(fullText.length)
48
+ return
49
+ }
50
+
51
+ if (charIndex >= fullText.length) {
52
+ return
53
+ }
54
+
55
+ const msPerChar = 1000 / speed
56
+ let rafId: number
57
+ let lastTime: number | null = null
58
+
59
+ function tick(timestamp: number) {
60
+ lastTime ??= timestamp
61
+
62
+ const elapsed = timestamp - lastTime
63
+ const charsToAdd = Math.floor(elapsed / msPerChar)
64
+
65
+ if (charsToAdd > 0) {
66
+ lastTime = timestamp
67
+ setCharIndex((prev) => Math.min(prev + charsToAdd, fullText.length))
68
+ }
69
+
70
+ rafId = requestAnimationFrame(tick)
71
+ }
72
+
73
+ rafId = requestAnimationFrame(tick)
74
+
75
+ return () => cancelAnimationFrame(rafId)
76
+ }, [fullText, charIndex, speed])
77
+
78
+ return {
79
+ displayedText: fullText.slice(0, charIndex),
80
+ isTyping: charIndex < fullText.length,
81
+ }
82
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, test, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { CopyButton } from './copy-button'
4
+
5
+ vi.mock('@carto/ps-utils', () => ({
6
+ copy: vi.fn(() => Promise.resolve()),
7
+ }))
8
+
9
+ describe('CopyButton', () => {
10
+ test('renders copy button', () => {
11
+ render(
12
+ <CopyButton copyText='hello' onSuccess={vi.fn()} onError={vi.fn()} />,
13
+ )
14
+ expect(screen.getByRole('button')).toBeTruthy()
15
+ })
16
+
17
+ test('renders with custom aria-label', () => {
18
+ render(
19
+ <CopyButton
20
+ copyText='hello'
21
+ onSuccess={vi.fn()}
22
+ onError={vi.fn()}
23
+ aria-label='Copy response'
24
+ />,
25
+ )
26
+ expect(screen.getByLabelText('Copy response')).toBeTruthy()
27
+ })
28
+
29
+ test('calls copy utility when clicked', async () => {
30
+ const { copy } = await import('@carto/ps-utils')
31
+ render(
32
+ <CopyButton
33
+ copyText='hello world'
34
+ onSuccess={vi.fn()}
35
+ onError={vi.fn()}
36
+ />,
37
+ )
38
+ fireEvent.click(screen.getByRole('button'))
39
+ expect(copy).toHaveBeenCalledWith('hello world')
40
+ })
41
+ })
@@ -0,0 +1,31 @@
1
+ import { IconButton } from '@mui/material'
2
+ import { ContentCopyOutlined } from '@mui/icons-material'
3
+ import type { CopyButtonProps } from './types'
4
+ import { copy } from '@carto/ps-utils'
5
+
6
+ export function CopyButton({
7
+ copyText,
8
+ onSuccess,
9
+ onError,
10
+ 'aria-label': ariaLabel,
11
+ ...props
12
+ }: CopyButtonProps) {
13
+ async function onCopy() {
14
+ try {
15
+ await copy(copyText)
16
+ onSuccess?.()
17
+ } catch (err) {
18
+ onError?.(err as Error)
19
+ }
20
+ }
21
+ return (
22
+ <IconButton
23
+ size='small'
24
+ onClick={() => void onCopy()}
25
+ aria-label={ariaLabel ?? 'Copy'}
26
+ {...props}
27
+ >
28
+ <ContentCopyOutlined fontSize='small' />
29
+ </IconButton>
30
+ )
31
+ }
@@ -0,0 +1,10 @@
1
+ import type { IconButtonProps } from '@mui/material/IconButton'
2
+
3
+ export interface CopyButtonProps extends Omit<
4
+ IconButtonProps,
5
+ 'onError' | 'onClick'
6
+ > {
7
+ copyText: string
8
+ onSuccess?: () => void
9
+ onError?: (err: Error) => void
10
+ }
@@ -62,3 +62,6 @@ export type { BasemapsUIProps } from './basemaps/types'
62
62
 
63
63
  export { Tooltip, setTooltipEnterDelay } from './tooltip/tooltip'
64
64
  export { SmartTooltip } from './smart-tooltip/smart-tooltip'
65
+
66
+ export type { CopyButtonProps } from './copy-button/types'
67
+ export { CopyButton } from './copy-button/copy-button'
@@ -2,7 +2,7 @@ import type { EChartsOption } from 'echarts'
2
2
  import type * as echarts from 'echarts'
3
3
  import type { BaseWidgetState } from '../stores/types'
4
4
  import type { Ref, RefObject } from 'react'
5
- import { theme as CartoTheme } from '../../theme'
5
+ import { theme as CartoTheme } from '@carto/meridian-ds/theme'
6
6
 
7
7
  export type EchartOptionsProps = EChartsOption
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { useEffect, type ComponentType } from 'react'
2
2
  import { IconButton, type SvgIconProps } from '@mui/material'
3
- import HighlightAltIcon from '@mui/icons-material/HighlightAlt'
3
+ import { HighlightAlt as HighlightAltIcon } from '@mui/icons-material'
4
4
  import { Tooltip } from '../../../components'
5
5
  import {
6
6
  useEchartInstance,
@@ -1,7 +1,7 @@
1
1
  import { useSortable } from '@dnd-kit/sortable'
2
2
  import { CSS } from '@dnd-kit/utilities'
3
3
  import { ListItemText, MenuItem } from '@mui/material'
4
- import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
4
+ import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'
5
5
  import type { ComponentType, ReactNode } from 'react'
6
6
  import type { SvgIconProps } from '@mui/material'
7
7
  import { styles } from './style'
@@ -8,7 +8,7 @@ import {
8
8
  MenuItem,
9
9
  type SvgIconProps,
10
10
  } from '@mui/material'
11
- import DownloadIcon from '@mui/icons-material/FileDownload'
11
+ import { FileDownload as DownloadIcon } from '@mui/icons-material'
12
12
  import { Tooltip } from '../../../components'
13
13
  import { triggerLinkDownload } from './exports'
14
14
  import type { DownloadItem } from './types'
@@ -1,5 +1,5 @@
1
1
  import { SvgIcon, type SvgIconProps } from '@mui/material'
2
- import ImageOutlined from '@mui/icons-material/ImageOutlined'
2
+ import { ImageOutlined } from '@mui/icons-material'
3
3
 
4
4
  /**
5
5
  * Generic "image" glyph used for the PNG download item. Wraps MUI's
@@ -15,9 +15,9 @@ import {
15
15
  Typography,
16
16
  type SvgIconProps,
17
17
  } from '@mui/material'
18
- import FullscreenIcon from '@mui/icons-material/Fullscreen'
19
- import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'
20
- import CloseIcon from '@mui/icons-material/Close'
18
+ import { Fullscreen as FullscreenIcon } from '@mui/icons-material'
19
+ import { FullscreenExit as FullscreenExitIcon } from '@mui/icons-material'
20
+ import { Close as CloseIcon } from '@mui/icons-material'
21
21
  import { Tooltip } from '../../../components'
22
22
  import { getWidgetStore, useWidget, useWidgetId } from '../../stores'
23
23
  import { DEFAULT_FULLSCREEN_LABELS, type FullScreenLabels } from './labels'
@@ -15,6 +15,14 @@ export {
15
15
  type StackToggleProps,
16
16
  type StackToggleLabels,
17
17
  } from './stack-toggle'
18
+ export {
19
+ ShowAllToggle,
20
+ setShowAll,
21
+ SHOW_ALL_ID,
22
+ DEFAULT_SHOW_ALL_LABELS,
23
+ type ShowAllToggleProps,
24
+ type ShowAllLabels,
25
+ } from './show-all'
18
26
  export {
19
27
  ZoomToggle,
20
28
  addZoom,
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, type ComponentType } from 'react'
2
2
  import { IconButton, type SvgIconProps } from '@mui/material'
3
- import LockIcon from '@mui/icons-material/Lock'
4
- import LockOpenIcon from '@mui/icons-material/LockOpen'
3
+ import { Lock as LockIcon } from '@mui/icons-material'
4
+ import { LockOpen as LockOpenIcon } from '@mui/icons-material'
5
5
  import { Tooltip } from '../../../components'
6
6
  import { useTransform, useWidgetId } from '../../stores'
7
7
  import type { TransformPair } from '../../stores'
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, type ComponentType } from 'react'
2
2
  import { IconButton, type SvgIconProps } from '@mui/material'
3
- import PercentIcon from '@mui/icons-material/Percent'
3
+ import { Percent as PercentIcon } from '@mui/icons-material'
4
4
  import { Tooltip } from '../../../components'
5
5
  import {
6
6
  getWidgetStore,
@@ -1,6 +1,6 @@
1
1
  import { useCallback, type ComponentType } from 'react'
2
2
  import { IconButton, type SvgIconProps } from '@mui/material'
3
- import SearchIcon from '@mui/icons-material/Search'
3
+ import { Search as SearchIcon } from '@mui/icons-material'
4
4
  import { Tooltip } from '../../../components'
5
5
  import {
6
6
  getWidgetStore,
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useMemo, useRef, useState } from 'react'
2
2
  import { IconButton, InputAdornment, TextField } from '@mui/material'
3
- import SearchIcon from '@mui/icons-material/Search'
4
- import ClearIcon from '@mui/icons-material/Clear'
3
+ import { Search as SearchIcon } from '@mui/icons-material'
4
+ import { Clear as ClearIcon } from '@mui/icons-material'
5
5
  import { debounce } from '@carto/ps-utils'
6
6
  import { getWidgetStore, useWidget, useWidgetId } from '../../stores'
7
7
  import { setSearcherText } from './searcher-toggle'
@@ -0,0 +1,7 @@
1
+ export {
2
+ ShowAllToggle,
3
+ setShowAll,
4
+ SHOW_ALL_ID,
5
+ type ShowAllToggleProps,
6
+ } from './show-all'
7
+ export { DEFAULT_SHOW_ALL_LABELS, type ShowAllLabels } from './labels'
@@ -0,0 +1,8 @@
1
+ export interface ShowAllLabels {
2
+ /** Tooltip + aria-label for the collapse (✕) button shown while expanded. */
3
+ toggle: string
4
+ }
5
+
6
+ export const DEFAULT_SHOW_ALL_LABELS: ShowAllLabels = {
7
+ toggle: 'Show less',
8
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { Provider } from '../../provider/widget-provider'
4
+ import { clearAllWidgetStores, getWidgetStore } from '../../stores'
5
+ import { ShowAllToggle, setShowAll, SHOW_ALL_ID } from './show-all'
6
+
7
+ beforeEach(() => clearAllWidgetStores())
8
+ afterEach(() => clearAllWidgetStores())
9
+
10
+ describe('setShowAll', () => {
11
+ it('writes the flag under transformStates[SHOW_ALL_ID]', () => {
12
+ render(
13
+ <Provider id='sa1' data={[]}>
14
+ <span />
15
+ </Provider>,
16
+ )
17
+ setShowAll('sa1', true)
18
+ expect(
19
+ getWidgetStore('sa1').getState().transformStates[SHOW_ALL_ID]?.enabled,
20
+ ).toBe(true)
21
+ setShowAll('sa1', false)
22
+ expect(
23
+ getWidgetStore('sa1').getState().transformStates[SHOW_ALL_ID]?.enabled,
24
+ ).toBe(false)
25
+ })
26
+ })
27
+
28
+ describe('<ShowAllToggle>', () => {
29
+ it('renders a collapse (✕) button', () => {
30
+ render(
31
+ <Provider id='sa2' data={[]}>
32
+ <ShowAllToggle />
33
+ </Provider>,
34
+ )
35
+ expect(screen.getByRole('button', { name: 'Show less' })).toBeTruthy()
36
+ })
37
+
38
+ it('clears the flag on click (collapses)', () => {
39
+ render(
40
+ <Provider id='sa3' data={[]}>
41
+ <ShowAllToggle />
42
+ </Provider>,
43
+ )
44
+ setShowAll('sa3', true)
45
+ fireEvent.click(screen.getByRole('button', { name: 'Show less' }))
46
+ expect(
47
+ getWidgetStore('sa3').getState().transformStates[SHOW_ALL_ID]?.enabled,
48
+ ).toBe(false)
49
+ })
50
+ })
@@ -0,0 +1,72 @@
1
+ import { type ComponentType } from 'react'
2
+ import { IconButton, type SvgIconProps } from '@mui/material'
3
+ import { Close as CloseIcon } from '@mui/icons-material'
4
+ import { Tooltip } from '../../../components'
5
+ import { getWidgetStore, useWidgetId } from '../../stores'
6
+ import { DEFAULT_SHOW_ALL_LABELS, type ShowAllLabels } from './labels'
7
+ import { styles } from './style'
8
+
9
+ /**
10
+ * The `show-all` flag is a pure UI signal (no data/config transform): the
11
+ * Category overflow row writes it `true` via {@link setShowAll}, the
12
+ * composer reads it with `useTransformEnabled(id, SHOW_ALL_ID)` to drop the
13
+ * row cap, and this button writes it back `false` to collapse. Stored under
14
+ * `transformStates` so `useTransformEnabled` can observe it like any other
15
+ * action flag — but it never registers a pipeline transform, so toggling it
16
+ * never re-runs the data pipeline.
17
+ */
18
+ export const SHOW_ALL_ID = 'show-all'
19
+
20
+ export interface ShowAllToggleProps {
21
+ labels?: Partial<ShowAllLabels>
22
+ icon?: ComponentType<SvgIconProps>
23
+ iconProps?: SvgIconProps
24
+ }
25
+
26
+ /**
27
+ * Collapse (✕) affordance shown while a widget is expanded into its
28
+ * "show all" state. Clicking it clears the `show-all` flag, returning the
29
+ * widget to its capped view. Mount it conditionally (only while the flag is
30
+ * set) so it doesn't occupy a `Widget.Toolbox` visibility-budget slot when
31
+ * the widget is collapsed.
32
+ */
33
+ export function ShowAllToggle({
34
+ labels,
35
+ icon: Icon = CloseIcon,
36
+ iconProps,
37
+ }: ShowAllToggleProps) {
38
+ const id = useWidgetId()
39
+ const _labels = { ...DEFAULT_SHOW_ALL_LABELS, ...labels }
40
+
41
+ return (
42
+ <Tooltip title={_labels.toggle}>
43
+ <IconButton
44
+ size='small'
45
+ aria-label={_labels.toggle}
46
+ aria-pressed
47
+ onClick={() => setShowAll(id, false)}
48
+ sx={styles.toggle}
49
+ >
50
+ <Icon fontSize='small' {...iconProps} />
51
+ </IconButton>
52
+ </Tooltip>
53
+ )
54
+ }
55
+
56
+ /**
57
+ * Imperatively writes the `show-all` flag into the widget store. Mirrors
58
+ * {@link setSearcherText} — used by the Category overflow row (to expand)
59
+ * and the {@link ShowAllToggle} button (to collapse) without routing through
60
+ * a re-rendered subscription.
61
+ */
62
+ export function setShowAll(widgetId: string, value: boolean): void {
63
+ getWidgetStore(widgetId).setState((s) => ({
64
+ transformStates: {
65
+ ...s.transformStates,
66
+ [SHOW_ALL_ID]: {
67
+ ...s.transformStates[SHOW_ALL_ID],
68
+ enabled: value,
69
+ },
70
+ },
71
+ }))
72
+ }
@@ -0,0 +1,8 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ toggle: {
5
+ p: 0.5,
6
+ '& .MuiSvgIcon-root': { fontSize: 20 },
7
+ },
8
+ } satisfies Record<string, SxProps<Theme>>
@@ -1,6 +1,6 @@
1
1
  import { type ComponentType } from 'react'
2
2
  import { IconButton, type SvgIconProps } from '@mui/material'
3
- import StackedBarChartIcon from '@mui/icons-material/StackedBarChart'
3
+ import { StackedBarChart as StackedBarChartIcon } from '@mui/icons-material'
4
4
  import { Tooltip } from '../../../components'
5
5
  import { useSingleTransform, useWidgetId } from '../../stores'
6
6
  import { addStack } from './transforms'
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, type ComponentType } from 'react'
2
2
  import { IconButton, useTheme, type SvgIconProps } from '@mui/material'
3
- import ZoomInIcon from '@mui/icons-material/ZoomIn'
3
+ import { ZoomIn as ZoomInIcon } from '@mui/icons-material'
4
4
  import { Tooltip } from '../../../components'
5
5
  import {
6
6
  getEchartInstance,
@@ -48,15 +48,15 @@ describe('<CategoryUI>', () => {
48
48
  expect(screen.getAllByRole('button')).toHaveLength(12)
49
49
  })
50
50
 
51
- it('default caps at 20 when data exceeds it → "Other (X more)" footer', () => {
51
+ it('default caps at 20 when data exceeds it → "Others <count>" footer', () => {
52
52
  const twentyFive = Array.from({ length: 25 }, (_, i) => ({
53
53
  name: `n${i}`,
54
54
  value: 25 - i,
55
55
  }))
56
56
  render(<CategoryUI data={[twentyFive]} />)
57
57
  expect(screen.getAllByRole('button')).toHaveLength(20)
58
- expect(screen.getByText('Other')).toBeTruthy()
59
- expect(screen.getByText('(5 more)')).toBeTruthy()
58
+ expect(screen.getByText('Others')).toBeTruthy()
59
+ expect(screen.getByText('5')).toBeTruthy()
60
60
  })
61
61
 
62
62
  it('maxItems=null → no cap, no scroll (canonical form)', () => {
@@ -70,7 +70,7 @@ describe('<CategoryUI>', () => {
70
70
  const { container } = render(
71
71
  <CategoryUI data={[twentyFive]} maxItems={null} />,
72
72
  )
73
- expect(screen.queryByText('Other')).toBeNull()
73
+ expect(screen.queryByText('Others')).toBeNull()
74
74
  expect(screen.getAllByRole('button')).toHaveLength(25)
75
75
  const root = container.firstChild as HTMLElement
76
76
  const list = root.firstChild as HTMLElement
@@ -86,15 +86,31 @@ describe('<CategoryUI>', () => {
86
86
  render(
87
87
  <CategoryUI data={[twentyFive]} maxItems={Number.POSITIVE_INFINITY} />,
88
88
  )
89
- expect(screen.queryByText('Other')).toBeNull()
89
+ expect(screen.queryByText('Others')).toBeNull()
90
90
  expect(screen.getAllByRole('button')).toHaveLength(25)
91
91
  })
92
92
 
93
- it('slices to maxItems and shows "Other (X more)" footer', () => {
93
+ it('slices to maxItems and shows "Others <count>" footer', () => {
94
94
  render(<CategoryUI data={[oneToTwelve()]} maxItems={5} />)
95
95
  expect(screen.getAllByRole('button')).toHaveLength(5)
96
- expect(screen.getByText('Other')).toBeTruthy()
97
- expect(screen.getByText('(7 more)')).toBeTruthy()
96
+ expect(screen.getByText('Others')).toBeTruthy()
97
+ expect(screen.getByText('7')).toBeTruthy()
98
+ })
99
+
100
+ it('onShowAll makes the Other row a button that fires the callback', () => {
101
+ const onShowAll = vi.fn()
102
+ render(
103
+ <CategoryUI
104
+ data={[oneToTwelve()]}
105
+ maxItems={5}
106
+ onShowAll={onShowAll}
107
+ />,
108
+ )
109
+ // 5 category rows + the now-interactive "Others" row = 6 buttons.
110
+ const buttons = screen.getAllByRole('button')
111
+ expect(buttons).toHaveLength(6)
112
+ fireEvent.click(screen.getByText('Others'))
113
+ expect(onShowAll).toHaveBeenCalledTimes(1)
98
114
  })
99
115
 
100
116
  it('maxItems=0 is the no-cap-with-scroll sentinel: every row renders inside a fixed scroll viewport', () => {
@@ -107,7 +123,7 @@ describe('<CategoryUI>', () => {
107
123
  <CategoryUI data={[oneToTwelve()]} maxItems={0} />,
108
124
  )
109
125
  expect(screen.getAllByRole('button')).toHaveLength(12)
110
- expect(screen.queryByText('Other')).toBeNull()
126
+ expect(screen.queryByText('Others')).toBeNull()
111
127
  expect(screen.queryByText(/more/)).toBeNull()
112
128
  // List has the scroll viewport inline style applied.
113
129
  const root = container.firstChild as HTMLElement
@@ -217,7 +233,7 @@ describe('<CategoryUI>', () => {
217
233
  />,
218
234
  )
219
235
  expect(screen.getByText('Others')).toBeTruthy()
220
- expect(screen.getByText('(+9 hidden)')).toBeTruthy()
236
+ expect(screen.getByText('+9 hidden')).toBeTruthy()
221
237
  })
222
238
 
223
239
  it('large maxItems = no Other row', () => {
@@ -50,7 +50,7 @@ export interface CategoryUIProps {
50
50
  * Caps the number of visible category rows.
51
51
  *
52
52
  * - `undefined` (omitted) — caps at {@link DEFAULT_MAX_ITEMS} (20).
53
- * Surplus rows fold into a single italic "Other (X more)" footer.
53
+ * Surplus rows fold into a single "Others <count>" footer row.
54
54
  * `undefined` cannot mean "no cap" because it's consumed by the
55
55
  * default-parameter syntax — pass `null` instead.
56
56
  * - positive finite N — same as the default but with a custom cap.
@@ -71,6 +71,13 @@ export interface CategoryUIProps {
71
71
  maxItems?: number | null
72
72
  /** Labels for the "Other" overflow row. `{count}` placeholder is replaced. */
73
73
  labels?: CategoryLabels
74
+ /**
75
+ * When provided, the "Other" overflow row becomes a button — clicking it
76
+ * fires `onShowAll`, which a composer typically wires to expand the widget
77
+ * (drop the row cap) so every category becomes reachable. When omitted the
78
+ * overflow row stays a static summary.
79
+ */
80
+ onShowAll?: () => void
74
81
  /** Manual override for the bar-width denominator. */
75
82
  maxOverride?: number
76
83
  /**
@@ -156,6 +163,7 @@ export function CategoryUI({
156
163
  series,
157
164
  maxItems = DEFAULT_MAX_ITEMS,
158
165
  labels,
166
+ onShowAll,
159
167
  maxOverride,
160
168
  size = 'small',
161
169
  stacked = false,
@@ -224,7 +232,7 @@ export function CategoryUI({
224
232
 
225
233
  // Three modes:
226
234
  // * positive finite N (default `DEFAULT_MAX_ITEMS = 20` when prop
227
- // omitted): cap at N rows; surplus folds into "Other (X more)".
235
+ // omitted): cap at N rows; surplus folds into "Others <count>".
228
236
  // * `0`: no cap WITH scroll — every row renders, the list locks to a
229
237
  // fixed viewport and scrolls internally. Composers use this to
230
238
  // bypass pagination while the user is searching:
@@ -296,7 +304,7 @@ export function CategoryUI({
296
304
  <Box sx={styles.root}>
297
305
  <Box
298
306
  ref={listRefCallback}
299
- sx={styles.list}
307
+ sx={scrollMode ? { ...styles.list, ...styles.listScroll } : styles.list}
300
308
  style={
301
309
  scrollMaxHeight !== undefined
302
310
  ? { maxHeight: scrollMaxHeight, overflowY: 'auto' }
@@ -378,6 +386,8 @@ export function CategoryUI({
378
386
  hiddenCount={hiddenCount}
379
387
  otherLabel={labels?.other}
380
388
  otherCountLabel={labels?.otherCount}
389
+ showAllLabel={labels?.showAll}
390
+ onShowAll={onShowAll}
381
391
  />
382
392
  )}
383
393
  </Box>
@@ -96,7 +96,7 @@ describe('<Category> bridge', () => {
96
96
  // 3 visible rows + Other footer.
97
97
  expect(screen.getAllByRole('button')).toHaveLength(3)
98
98
  expect(screen.getByText('Rest')).toBeTruthy()
99
- expect(screen.getByText('((3 hidden))')).toBeTruthy()
99
+ expect(screen.getByText('(3 hidden)')).toBeTruthy()
100
100
  })
101
101
 
102
102
  it('does NOT read `transformStates.searcher.enabled` from the store (composer-mediation contract)', () => {
@@ -119,7 +119,7 @@ describe('<Category> bridge', () => {
119
119
  </Provider>,
120
120
  )
121
121
  expect(screen.getAllByRole('button')).toHaveLength(3)
122
- expect(screen.getByText('Other')).toBeTruthy()
122
+ expect(screen.getByText('Others')).toBeTruthy()
123
123
 
124
124
  // Flip the searcher flag in the store — bridge should NOT react.
125
125
  act(() => {
@@ -134,7 +134,7 @@ describe('<Category> bridge', () => {
134
134
 
135
135
  // Still capped, Other still there.
136
136
  expect(screen.getAllByRole('button')).toHaveLength(3)
137
- expect(screen.getByText('Other')).toBeTruthy()
137
+ expect(screen.getByText('Others')).toBeTruthy()
138
138
  })
139
139
 
140
140
  it('respects maxItems=0 (no-cap sentinel) from the consumer', () => {
@@ -153,7 +153,7 @@ describe('<Category> bridge', () => {
153
153
  )
154
154
  // No cap → all rows render, Other footer is suppressed.
155
155
  expect(screen.getAllByRole('button')).toHaveLength(10)
156
- expect(screen.queryByText('Other')).toBeNull()
156
+ expect(screen.queryByText('Others')).toBeNull()
157
157
  })
158
158
 
159
159
  it('forwards size="medium" through to the rendered bars', () => {