@carto/ps-react-ui 4.9.0 → 4.11.0

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 (186) 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-BiuuHCDN.js +1156 -0
  4. package/dist/change-column-BiuuHCDN.js.map +1 -0
  5. package/dist/chat.js +1507 -0
  6. package/dist/chat.js.map +1 -0
  7. package/dist/components.js +125 -122
  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-BH0LPwSy.js → data-zoom-layout--YiY6ko_.js} +5 -4
  12. package/dist/{data-zoom-layout-BH0LPwSy.js.map → data-zoom-layout--YiY6ko_.js.map} +1 -1
  13. package/dist/{download-config-DNLkypdN.js → download-config-oJIFZ2WC.js} +10 -9
  14. package/dist/{download-config-DNLkypdN.js.map → download-config-oJIFZ2WC.js.map} +1 -1
  15. package/dist/{lasso-tool-BYbxrJ-7.js → lasso-tool-CDFj4zKY.js} +2 -1
  16. package/dist/lasso-tool-CDFj4zKY.js.map +1 -0
  17. package/dist/{spread-CTuIXZSM.js → spread-CPis22AE.js} +5 -4
  18. package/dist/{spread-CTuIXZSM.js.map → spread-CPis22AE.js.map} +1 -1
  19. package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
  20. package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
  21. package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
  22. package/dist/types/chat/bubbles/index.d.ts +4 -0
  23. package/dist/types/chat/const.d.ts +4 -0
  24. package/dist/types/chat/containers/chat-content.d.ts +2 -0
  25. package/dist/types/chat/containers/chat-footer.d.ts +2 -0
  26. package/dist/types/chat/containers/chat-header.d.ts +2 -0
  27. package/dist/types/chat/containers/chat-starter.d.ts +2 -0
  28. package/dist/types/chat/containers/index.d.ts +4 -0
  29. package/dist/types/chat/containers/styles.d.ts +93 -0
  30. package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
  31. package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
  32. package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
  33. package/dist/types/chat/feedback/chat-tool-code-area.d.ts +2 -0
  34. package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
  35. package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
  36. package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
  37. package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
  38. package/dist/types/chat/feedback/index.d.ts +8 -0
  39. package/dist/types/chat/feedback/styles.d.ts +211 -0
  40. package/dist/types/chat/index.d.ts +20 -0
  41. package/dist/types/chat/types.d.ts +184 -0
  42. package/dist/types/chat/use-typewriter.d.ts +30 -0
  43. package/dist/types/components/copy-button/copy-button.d.ts +2 -0
  44. package/dist/types/components/copy-button/types.d.ts +6 -0
  45. package/dist/types/components/index.d.ts +2 -0
  46. package/dist/types/components/lasso-tool/styles.d.ts +1 -0
  47. package/dist/types/components/measurement-tools/styles.d.ts +1 -0
  48. package/dist/types/widgets-v2/actions/index.d.ts +1 -0
  49. package/dist/types/widgets-v2/actions/show-all/index.d.ts +2 -0
  50. package/dist/types/widgets-v2/actions/show-all/labels.d.ts +5 -0
  51. package/dist/types/widgets-v2/actions/show-all/show-all.d.ts +33 -0
  52. package/dist/types/widgets-v2/actions/show-all/style.d.ts +8 -0
  53. package/dist/types/widgets-v2/category/category-ui.d.ts +9 -2
  54. package/dist/types/widgets-v2/category/category.d.ts +9 -2
  55. package/dist/types/widgets-v2/category/components/category-row-other.d.ts +19 -6
  56. package/dist/types/widgets-v2/category/style.d.ts +21 -2
  57. package/dist/types/widgets-v2/category/types.d.ts +2 -0
  58. package/dist/types/widgets-v2/index.d.ts +3 -2
  59. package/dist/types/widgets-v2/selection-summary/labels.d.ts +7 -2
  60. package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +13 -6
  61. package/dist/types/widgets-v2/selection-summary/style.d.ts +15 -0
  62. package/dist/types/widgets-v2/wrapper/style.d.ts +1 -2
  63. package/dist/types/widgets-v2/wrapper/widget-wrapper.d.ts +6 -1
  64. package/dist/widgets/actions.js +116 -115
  65. package/dist/widgets/actions.js.map +1 -1
  66. package/dist/widgets/bar.js +1 -1
  67. package/dist/widgets/category.js +10 -9
  68. package/dist/widgets/category.js.map +1 -1
  69. package/dist/widgets/formula.js +12 -11
  70. package/dist/widgets/formula.js.map +1 -1
  71. package/dist/widgets/histogram.js +8 -7
  72. package/dist/widgets/histogram.js.map +1 -1
  73. package/dist/widgets/markdown.js +10 -9
  74. package/dist/widgets/markdown.js.map +1 -1
  75. package/dist/widgets/pie.js +1 -1
  76. package/dist/widgets/scatterplot.js +1 -1
  77. package/dist/widgets/spread.js +10 -9
  78. package/dist/widgets/spread.js.map +1 -1
  79. package/dist/widgets/table.js +18 -17
  80. package/dist/widgets/table.js.map +1 -1
  81. package/dist/widgets/timeseries.js +1 -1
  82. package/dist/widgets/utils.js +1 -1
  83. package/dist/widgets/wrapper.js +4 -3
  84. package/dist/widgets/wrapper.js.map +1 -1
  85. package/dist/widgets-v2/actions.js +41 -37
  86. package/dist/widgets-v2/bar.js +9 -8
  87. package/dist/widgets-v2/bar.js.map +1 -1
  88. package/dist/widgets-v2/category.js +23 -22
  89. package/dist/widgets-v2/category.js.map +1 -1
  90. package/dist/widgets-v2/formula.js +24 -23
  91. package/dist/widgets-v2/formula.js.map +1 -1
  92. package/dist/widgets-v2/histogram.js +11 -10
  93. package/dist/widgets-v2/histogram.js.map +1 -1
  94. package/dist/widgets-v2/markdown.js +10 -9
  95. package/dist/widgets-v2/markdown.js.map +1 -1
  96. package/dist/widgets-v2/pie.js +8 -7
  97. package/dist/widgets-v2/pie.js.map +1 -1
  98. package/dist/widgets-v2/scatterplot.js +10 -9
  99. package/dist/widgets-v2/scatterplot.js.map +1 -1
  100. package/dist/widgets-v2/spread.js +10 -9
  101. package/dist/widgets-v2/spread.js.map +1 -1
  102. package/dist/widgets-v2/table.js +17 -16
  103. package/dist/widgets-v2/table.js.map +1 -1
  104. package/dist/widgets-v2/timeseries.js +9 -8
  105. package/dist/widgets-v2/timeseries.js.map +1 -1
  106. package/dist/widgets-v2/utils.js +1 -1
  107. package/dist/widgets-v2.js +343 -338
  108. package/dist/widgets-v2.js.map +1 -1
  109. package/package.json +9 -3
  110. package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
  111. package/src/chat/bubbles/chat-agent-message.tsx +11 -0
  112. package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
  113. package/src/chat/bubbles/chat-error-message.tsx +47 -0
  114. package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
  115. package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
  116. package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
  117. package/src/chat/bubbles/chat-user-message.tsx +27 -0
  118. package/src/chat/bubbles/index.ts +4 -0
  119. package/src/chat/bubbles/styles.ts +148 -0
  120. package/src/chat/const.ts +4 -0
  121. package/src/chat/containers/chat-content.test.tsx +269 -0
  122. package/src/chat/containers/chat-content.tsx +142 -0
  123. package/src/chat/containers/chat-footer.test.tsx +34 -0
  124. package/src/chat/containers/chat-footer.tsx +78 -0
  125. package/src/chat/containers/chat-header.test.tsx +28 -0
  126. package/src/chat/containers/chat-header.tsx +29 -0
  127. package/src/chat/containers/chat-starter.test.tsx +32 -0
  128. package/src/chat/containers/chat-starter.tsx +75 -0
  129. package/src/chat/containers/index.ts +4 -0
  130. package/src/chat/containers/styles.ts +96 -0
  131. package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
  132. package/src/chat/feedback/chat-actions-container.tsx +7 -0
  133. package/src/chat/feedback/chat-loader.test.tsx +10 -0
  134. package/src/chat/feedback/chat-loader.tsx +31 -0
  135. package/src/chat/feedback/chat-rating-action.tsx +43 -0
  136. package/src/chat/feedback/chat-thinking.test.tsx +15 -0
  137. package/src/chat/feedback/chat-thinking.tsx +23 -0
  138. package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
  139. package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
  140. package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
  141. package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
  142. package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
  143. package/src/chat/feedback/chat-tool-group.tsx +156 -0
  144. package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
  145. package/src/chat/feedback/chat-tool-trace.tsx +192 -0
  146. package/src/chat/feedback/get-tool-label.test.tsx +91 -0
  147. package/src/chat/feedback/get-tool-label.ts +13 -0
  148. package/src/chat/feedback/index.ts +8 -0
  149. package/src/chat/feedback/styles.ts +229 -0
  150. package/src/chat/index.ts +59 -0
  151. package/src/chat/types.ts +215 -0
  152. package/src/chat/use-typewriter.test.tsx +38 -0
  153. package/src/chat/use-typewriter.ts +82 -0
  154. package/src/components/copy-button/copy-button.test.tsx +41 -0
  155. package/src/components/copy-button/copy-button.tsx +31 -0
  156. package/src/components/copy-button/types.ts +10 -0
  157. package/src/components/index.ts +3 -0
  158. package/src/components/lasso-tool/styles.ts +1 -0
  159. package/src/components/measurement-tools/styles.ts +1 -0
  160. package/src/widgets-v2/actions/index.ts +8 -0
  161. package/src/widgets-v2/actions/show-all/index.ts +7 -0
  162. package/src/widgets-v2/actions/show-all/labels.ts +8 -0
  163. package/src/widgets-v2/actions/show-all/show-all.test.tsx +50 -0
  164. package/src/widgets-v2/actions/show-all/show-all.tsx +72 -0
  165. package/src/widgets-v2/actions/show-all/style.ts +8 -0
  166. package/src/widgets-v2/category/category-ui.test.tsx +26 -10
  167. package/src/widgets-v2/category/category-ui.tsx +13 -3
  168. package/src/widgets-v2/category/category.test.tsx +4 -4
  169. package/src/widgets-v2/category/category.tsx +10 -1
  170. package/src/widgets-v2/category/components/category-row-other.test.tsx +36 -7
  171. package/src/widgets-v2/category/components/category-row-other.tsx +64 -13
  172. package/src/widgets-v2/category/style.ts +35 -4
  173. package/src/widgets-v2/category/types.ts +2 -0
  174. package/src/widgets-v2/index.ts +3 -0
  175. package/src/widgets-v2/selection-summary/labels.ts +8 -4
  176. package/src/widgets-v2/selection-summary/selection-summary.test.tsx +15 -9
  177. package/src/widgets-v2/selection-summary/selection-summary.tsx +42 -22
  178. package/src/widgets-v2/selection-summary/style.ts +15 -0
  179. package/src/widgets-v2/wrapper/style.ts +1 -2
  180. package/src/widgets-v2/wrapper/widget-wrapper.test.tsx +30 -0
  181. package/src/widgets-v2/wrapper/widget-wrapper.tsx +11 -1
  182. package/dist/category-DwaeYjpX.js +0 -656
  183. package/dist/category-DwaeYjpX.js.map +0 -1
  184. package/dist/change-column-Cidl_M-4.js +0 -1110
  185. package/dist/change-column-Cidl_M-4.js.map +0 -1
  186. package/dist/lasso-tool-BYbxrJ-7.js.map +0 -1
@@ -0,0 +1,215 @@
1
+ import type { ReactNode } from 'react'
2
+ import type { ButtonBaseProps, SxProps, Theme } from '@mui/material'
3
+
4
+ // === Shared base props ===
5
+ export interface ChatSxProps {
6
+ sx?: SxProps<Theme>
7
+ }
8
+
9
+ // === Error types ===
10
+ export interface ChatErrorAction {
11
+ label: string
12
+ onClick: () => void
13
+ }
14
+
15
+ // === Message props ===
16
+ export interface ChatUserMessageProps extends ChatSxProps {
17
+ children: ReactNode
18
+ /** enabled to render text with a lighter color for indicating things like an error sending the message */
19
+ muted?: boolean
20
+ /** content to render on top of the message for user attachments */
21
+ topContext?: ReactNode
22
+ }
23
+
24
+ export interface ChatAgentMessageProps extends ChatSxProps {
25
+ children: ReactNode
26
+ }
27
+
28
+ export interface ChatErrorMessageProps extends ChatSxProps {
29
+ errors: string[]
30
+ icon?: ReactNode
31
+ actions?: ChatErrorAction[]
32
+ }
33
+
34
+ export interface ChatSuggestionButtonProps
35
+ extends ChatSxProps, Omit<ButtonBaseProps, 'children'> {
36
+ label: ReactNode
37
+ color?: string
38
+ }
39
+
40
+ // === Feedback props ===
41
+ export interface ChatThinkingProps extends ChatSxProps {
42
+ duration?: number
43
+ children?: ReactNode
44
+ }
45
+
46
+ export interface ChatLoaderProps extends ChatSxProps {
47
+ size?: number
48
+ labels?: {
49
+ loading?: string
50
+ }
51
+ }
52
+
53
+ // === Layout props ===
54
+ export interface ChatContentProps extends ChatSxProps {
55
+ children: ReactNode
56
+ /**
57
+ * Smooth-scrolls to the bottom whenever new content is added — but only if
58
+ * the user was already at (or near) the bottom. Readers who scrolled up to
59
+ * revisit older messages are left alone. Defaults to `true`; pass `false`
60
+ * to opt out and manage scroll yourself via the ref.
61
+ */
62
+ autoScroll?: boolean
63
+ labels?: {
64
+ jumpToLatest?: string
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Imperative handle exposed by `ChatContent` via `ref`. Use it to drive scroll
70
+ * from the parent — for example, calling `scrollToBottom()` when a new agent
71
+ * message arrives.
72
+ */
73
+ export interface ChatContentRef {
74
+ /** Smooth-scrolls the content area to the bottom. */
75
+ scrollToBottom: () => void
76
+ /** Smooth-scrolls the content area to the top. */
77
+ scrollToTop: () => void
78
+ /** `true` when the content area is scrolled to (or near) its bottom edge. */
79
+ isAtBottom: boolean
80
+ /** `true` when the content area is scrolled to (or near) its top edge. */
81
+ isAtTop: boolean
82
+ }
83
+
84
+ // === Container props ===
85
+ export interface ChatHeaderProps extends ChatSxProps {
86
+ leftSlot?: ReactNode
87
+ title: ReactNode
88
+ rightSlot?: ReactNode
89
+ onClose?: () => void
90
+ }
91
+
92
+ export interface ChatFooterProps extends ChatSxProps {
93
+ /** Current value of the chat message area. */
94
+ value: string
95
+ /** Called with the new textarea value on every keystroke. */
96
+ onChange: (value: string) => void
97
+ /** Called when the send button is clicked or Enter is pressed (without Shift). */
98
+ onSend: () => void
99
+ /** Called when the stop button is clicked. Only shown while `isGenerating` is true. */
100
+ onStop?: () => void
101
+ /** When true, swaps the send button for a stop button and disables the textarea. */
102
+ isGenerating?: boolean
103
+ /** Disables the textarea and both send/stop buttons. */
104
+ disabled?: boolean
105
+ /** Placeholder text for the textarea. Defaults to `'Type a message...'`. */
106
+ placeholder?: string
107
+ /** Accessible labels for the send and stop buttons (used as `aria-label`). */
108
+ labels?: {
109
+ /** Defaults to `'Send'`. */
110
+ send?: string
111
+ /** Defaults to `'Stop'`. */
112
+ stop?: string
113
+ }
114
+ /** Helper text rendered under the input. Defaults to an AI disclaimer; pass `null` to hide. */
115
+ caption?: ReactNode
116
+ }
117
+
118
+ // === Extras props ===
119
+ export interface ChatStarterItem {
120
+ label: string
121
+ color?: string
122
+ }
123
+
124
+ export interface ChatStarterProps extends ChatSxProps {
125
+ icon?: ReactNode
126
+ title?: ReactNode
127
+ description?: ReactNode
128
+ items: string[] | ChatStarterItem[]
129
+ size?: 'small' | 'medium'
130
+ onSelect?: (prompt: string) => void
131
+ }
132
+
133
+ export interface ChatRatingActionProps {
134
+ onRatingChange?: (rating: 'up' | 'down' | null) => void
135
+ rating?: 'up' | 'down' | null
136
+ labels?: {
137
+ thumbUp?: string
138
+ thumbDown?: string
139
+ }
140
+ }
141
+
142
+ export interface ChatToolItem {
143
+ id: string
144
+ name: string
145
+ status: 'running' | 'complete' | 'error'
146
+ /** Display label shown while status is 'running'. Falls back to a capitalized `name`. */
147
+ runningLabel?: string
148
+ /** Display label shown for non-running statuses. Falls back to a capitalized `name`. */
149
+ label?: string
150
+ /** Friendly reference name for the tool (e.g. "add_marker"). Displayed with icon. */
151
+ reference?: string
152
+ /** Execution duration in seconds (e.g. 1.8) */
153
+ duration?: number
154
+ /** Input arguments as a JSON string or plain text */
155
+ inputArguments?: string
156
+ /** Output as a JSON string or plain text */
157
+ output?: string
158
+ }
159
+
160
+ export interface ChatToolTraceProps extends ChatSxProps {
161
+ tool: ChatToolItem
162
+ /** Whether the trace accordion is expanded */
163
+ expanded?: boolean
164
+ /** Callback when accordion expansion state changes */
165
+ onExpandedChange?: (expanded: boolean) => void
166
+ labels?: {
167
+ toolExecuted?: string
168
+ reference?: string
169
+ duration?: string
170
+ status?: string
171
+ inputArguments?: string
172
+ output?: string
173
+ fullView?: string
174
+ success?: string
175
+ error?: string
176
+ running?: string
177
+ }
178
+ }
179
+
180
+ export interface ChatToolCodeAreaProps extends ChatSxProps {
181
+ /** Code content to display */
182
+ content: string
183
+ /** Label for the full view dialog title */
184
+ title?: string
185
+ /** Render with error styling (red left border, tinted background) */
186
+ isError?: boolean
187
+ labels?: {
188
+ fullView?: string
189
+ }
190
+ }
191
+
192
+ export interface ChatToolFullViewDialogProps {
193
+ open: boolean
194
+ onClose: () => void
195
+ title: string
196
+ content: string
197
+ }
198
+
199
+ export interface ChatToolGroupProps extends ChatSxProps {
200
+ tools: ChatToolItem[]
201
+ /** Whether the group accordion is expanded */
202
+ expanded?: boolean
203
+ /** Callback when group expansion state changes */
204
+ onExpandedChange?: (expanded: boolean) => void
205
+ /** Map of tool IDs to their individual expanded state. Used to preserve expansion state during grouping. */
206
+ expandedTools?: Record<string, boolean>
207
+ /** Callback when an individual tool's expansion state changes */
208
+ onToolExpandedChange?: (
209
+ value: Record<string, boolean>,
210
+ toolId?: string,
211
+ ) => void
212
+ labels?: ChatToolTraceProps['labels'] & {
213
+ toolsUsed?: string
214
+ }
215
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { renderHook } from '@testing-library/react'
3
+ import { useTypewriter } from './use-typewriter'
4
+
5
+ describe('useTypewriter', () => {
6
+ test('starts with empty text and isTyping=true when fullText is non-empty', () => {
7
+ const { result } = renderHook(() => useTypewriter('hello'))
8
+ expect(result.current.displayedText).toBe('')
9
+ expect(result.current.isTyping).toBe(true)
10
+ })
11
+
12
+ test('returns the full text immediately when skipAnimation is true', () => {
13
+ const { result } = renderHook(() =>
14
+ useTypewriter('hello', { skipAnimation: true }),
15
+ )
16
+ expect(result.current.displayedText).toBe('hello')
17
+ expect(result.current.isTyping).toBe(false)
18
+ })
19
+
20
+ test('isTyping=false for empty fullText', () => {
21
+ const { result } = renderHook(() => useTypewriter(''))
22
+ expect(result.current.displayedText).toBe('')
23
+ expect(result.current.isTyping).toBe(false)
24
+ })
25
+
26
+ test('skipAnimation captured at mount — toggling later does not retrigger reveal', () => {
27
+ const { result, rerender } = renderHook(
28
+ ({ skip }: { skip: boolean }) =>
29
+ useTypewriter('hello', { skipAnimation: skip }),
30
+ { initialProps: { skip: true } },
31
+ )
32
+ expect(result.current.displayedText).toBe('hello')
33
+
34
+ rerender({ skip: false })
35
+ expect(result.current.displayedText).toBe('hello')
36
+ expect(result.current.isTyping).toBe(false)
37
+ })
38
+ })
@@ -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'
@@ -7,6 +7,7 @@ export const styles = {
7
7
  alignItems: 'center',
8
8
  justifyContent: 'flex-start',
9
9
  overflow: 'hidden',
10
+ width: 'fit-content',
10
11
 
11
12
  '&.inline': {
12
13
  flexDirection: 'column',
@@ -8,6 +8,7 @@ export const styles = {
8
8
  justifyContent: 'flex-start',
9
9
  gap: ({ spacing }) => spacing(1),
10
10
  overflow: 'hidden',
11
+ width: 'fit-content',
11
12
  },
12
13
  actions: {
13
14
  icon: {
@@ -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,
@@ -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 CloseIcon from '@mui/icons-material/Close'
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>>