sparkdesign 0.0.1 → 0.1.10

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 (118) hide show
  1. package/README.md +188 -4
  2. package/dist/commands/add.js +93 -0
  3. package/dist/commands/diff.js +54 -0
  4. package/dist/commands/init.js +96 -0
  5. package/dist/commands/list.js +25 -0
  6. package/dist/index.js +37 -0
  7. package/dist/utils/config.js +53 -0
  8. package/dist/utils/registry.js +34 -0
  9. package/dist/utils/tokens.js +176 -0
  10. package/dist/utils/transform.js +19 -0
  11. package/package.json +33 -10
  12. package/registry/__tests__/basic/button.test.tsx +333 -0
  13. package/registry/__tests__/chat/markdown.test.tsx +387 -0
  14. package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
  15. package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
  16. package/registry/basic/alert-dialog.tsx +180 -0
  17. package/registry/basic/avatar.tsx +120 -0
  18. package/registry/basic/button.tsx +100 -0
  19. package/registry/basic/collapse.tsx +94 -0
  20. package/registry/basic/collapsible-card.tsx +230 -0
  21. package/registry/basic/collapsible.tsx +21 -0
  22. package/registry/basic/dropdown-menu.tsx +254 -0
  23. package/registry/basic/icon-button.tsx +66 -0
  24. package/registry/basic/icons-inline.tsx +206 -0
  25. package/registry/basic/kbd.tsx +50 -0
  26. package/registry/basic/option-list.tsx +125 -0
  27. package/registry/basic/pagination.tsx +132 -0
  28. package/registry/basic/progress.tsx +42 -0
  29. package/registry/basic/radio-group.tsx +69 -0
  30. package/registry/basic/resizable.tsx +67 -0
  31. package/registry/basic/scrollbar.tsx +114 -0
  32. package/registry/basic/select.tsx +177 -0
  33. package/registry/basic/shimmering-text.tsx +115 -0
  34. package/registry/basic/sidebar-menu.tsx +177 -0
  35. package/registry/basic/skeleton.tsx +33 -0
  36. package/registry/basic/slider.tsx +55 -0
  37. package/registry/basic/sonner.tsx +104 -0
  38. package/registry/basic/spinner.tsx +17 -0
  39. package/registry/basic/switch.tsx +49 -0
  40. package/registry/basic/table.tsx +117 -0
  41. package/registry/basic/tabs.tsx +85 -0
  42. package/registry/basic/tag.tsx +161 -0
  43. package/registry/basic/theme-from-document.ts +10 -0
  44. package/registry/basic/toggle.tsx +223 -0
  45. package/registry/basic/tooltip.tsx +80 -0
  46. package/registry/basic/typography.tsx +201 -0
  47. package/registry/chat/ask-user-part.tsx +70 -0
  48. package/registry/chat/browser-action-part.tsx +166 -0
  49. package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
  50. package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
  51. package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
  52. package/registry/chat/chat-input/compound.tsx +334 -0
  53. package/registry/chat/chat-input/context.tsx +189 -0
  54. package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
  55. package/registry/chat/chat-input/index.tsx +123 -0
  56. package/registry/chat/chat-input/types.ts +77 -0
  57. package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
  58. package/registry/chat/code-block-part.tsx +151 -0
  59. package/registry/chat/file-attachment.tsx +44 -0
  60. package/registry/chat/file-card.tsx +68 -0
  61. package/registry/chat/file-review-part.tsx +259 -0
  62. package/registry/chat/folder-button.tsx +169 -0
  63. package/registry/chat/generated-images-grid.tsx +56 -0
  64. package/registry/chat/generation-status-bar.tsx +72 -0
  65. package/registry/chat/hint-banner.tsx +165 -0
  66. package/registry/chat/image-attachment.tsx +166 -0
  67. package/registry/chat/image-generating.tsx +281 -0
  68. package/registry/chat/markdown.tsx +146 -0
  69. package/registry/chat/mermaid-part.tsx +90 -0
  70. package/registry/chat/permission-card.tsx +178 -0
  71. package/registry/chat/plan-part.tsx +168 -0
  72. package/registry/chat/queue-indicator.tsx +234 -0
  73. package/registry/chat/reasoning-step/compound.tsx +336 -0
  74. package/registry/chat/reasoning-step/context.tsx +114 -0
  75. package/registry/chat/reasoning-step/index.tsx +45 -0
  76. package/registry/chat/reasoning-step/types.ts +109 -0
  77. package/registry/chat/related-prompts.tsx +91 -0
  78. package/registry/chat/response/compound.tsx +210 -0
  79. package/registry/chat/response/context.tsx +200 -0
  80. package/registry/chat/response/index.tsx +87 -0
  81. package/registry/chat/response/types.ts +123 -0
  82. package/registry/chat/send-button.tsx +94 -0
  83. package/registry/chat/streaming-markdown-block.tsx +111 -0
  84. package/registry/chat/task-part.tsx +109 -0
  85. package/registry/chat/terminal-code-block-part.tsx +69 -0
  86. package/registry/chat/thinking-indicator.tsx +91 -0
  87. package/registry/chat/tool-invocation-card.tsx +132 -0
  88. package/registry/chat/user-message.tsx +38 -0
  89. package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
  90. package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
  91. package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
  92. package/registry/chat/user-question/compound.tsx +324 -0
  93. package/registry/chat/user-question/context.tsx +456 -0
  94. package/registry/chat/user-question/index.tsx +95 -0
  95. package/registry/chat/user-question/types.ts +61 -0
  96. package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
  97. package/registry/chat/user-question/useUserQuestionState.ts +165 -0
  98. package/registry/chat/user-question-answer.tsx +62 -0
  99. package/registry/lib/file-icon-maps.ts +150 -0
  100. package/registry/lib/use-mermaid-render.ts +76 -0
  101. package/registry/lib/utils.ts +6 -0
  102. package/registry/meta.json +1 -0
  103. package/registry/tokens/index.css +31 -0
  104. package/registry/tokens/scale/computed.css +103 -0
  105. package/registry/tokens/scale/config.css +110 -0
  106. package/registry/tokens/scale/index.css +30 -0
  107. package/registry/tokens/scale/presets/compact.css +30 -0
  108. package/registry/tokens/scale/presets/dense.css +64 -0
  109. package/registry/tokens/scale/presets/sharp.css +40 -0
  110. package/registry/tokens/scale/presets/soft.css +16 -0
  111. package/registry/tokens/scale.css +13 -0
  112. package/registry/tokens/scrollbar-utility.css +35 -0
  113. package/registry/tokens/theme.css +633 -0
  114. package/registry/tokens/themes/dark-parchment.css +132 -0
  115. package/registry/tokens/themes/dark-qoder.css +132 -0
  116. package/registry/tokens/themes/light-parchment.css +123 -0
  117. package/registry/tokens/themes/light-qoder.css +131 -0
  118. package/index.js +0 -5
@@ -0,0 +1,346 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ToolInvocationCard } from '../../chat/tool-invocation-card'
4
+
5
+ describe('ToolInvocationCard', () => {
6
+ // ============================================================
7
+ // 基础渲染测试
8
+ // ============================================================
9
+ describe('基础渲染', () => {
10
+ it('应正确渲染工具名称', () => {
11
+ render(<ToolInvocationCard toolName="read_file" status="success" />)
12
+ expect(screen.getByText('read_file')).toBeInTheDocument()
13
+ })
14
+
15
+ it('应设置正确的 displayName', () => {
16
+ expect(ToolInvocationCard.displayName).toBe('ToolInvocationCard')
17
+ })
18
+
19
+ it('应渲染参数文本', () => {
20
+ render(
21
+ <ToolInvocationCard
22
+ toolName="search"
23
+ params="query: hello world"
24
+ status="success"
25
+ />
26
+ )
27
+ expect(screen.getByText('query: hello world')).toBeInTheDocument()
28
+ })
29
+
30
+ it('无参数时不应渲染参数区域', () => {
31
+ const { container } = render(
32
+ <ToolInvocationCard toolName="list_dir" status="success" />
33
+ )
34
+ // 只有工具名和状态文本
35
+ expect(container.textContent).toContain('list_dir')
36
+ })
37
+ })
38
+
39
+ // ============================================================
40
+ // 状态图标测试
41
+ // ============================================================
42
+ describe('状态图标', () => {
43
+ it('running 状态应显示时间图标', () => {
44
+ const { container } = render(
45
+ <ToolInvocationCard toolName="test" status="running" />
46
+ )
47
+ // 默认 timeIcon 使用 TimeLine 组件
48
+ expect(container.querySelector('svg')).toBeInTheDocument()
49
+ })
50
+
51
+ it('success 状态应显示勾选图标', () => {
52
+ const { container } = render(
53
+ <ToolInvocationCard toolName="test" status="success" />
54
+ )
55
+ expect(container.querySelector('svg')).toBeInTheDocument()
56
+ })
57
+
58
+ it('error 状态应显示关闭图标', () => {
59
+ const { container } = render(
60
+ <ToolInvocationCard toolName="test" status="error" />
61
+ )
62
+ expect(container.querySelector('svg')).toBeInTheDocument()
63
+ })
64
+
65
+ it('应接受自定义图标', () => {
66
+ render(
67
+ <ToolInvocationCard
68
+ toolName="test"
69
+ status="success"
70
+ checkboxCircleIcon={<span data-testid="custom-check">✓</span>}
71
+ />
72
+ )
73
+ expect(screen.getByTestId('custom-check')).toBeInTheDocument()
74
+ })
75
+ })
76
+
77
+ // ============================================================
78
+ // 状态文本测试
79
+ // ============================================================
80
+ describe('状态文本', () => {
81
+ it('running 状态应显示默认文本 "Running"', () => {
82
+ render(<ToolInvocationCard toolName="test" status="running" />)
83
+ expect(screen.getByText('Running')).toBeInTheDocument()
84
+ })
85
+
86
+ it('success 状态应显示默认文本 "Ran"', () => {
87
+ render(<ToolInvocationCard toolName="test" status="success" />)
88
+ expect(screen.getByText('Ran')).toBeInTheDocument()
89
+ })
90
+
91
+ it('error 状态应显示默认文本 "Failed"', () => {
92
+ render(<ToolInvocationCard toolName="test" status="error" />)
93
+ expect(screen.getByText('Failed')).toBeInTheDocument()
94
+ })
95
+
96
+ it('应接受自定义 statusText', () => {
97
+ render(
98
+ <ToolInvocationCard
99
+ toolName="test"
100
+ status="success"
101
+ statusText="执行完成"
102
+ />
103
+ )
104
+ expect(screen.getByText('执行完成')).toBeInTheDocument()
105
+ })
106
+
107
+ it('statusText 为空字符串时不应显示状态文本', () => {
108
+ render(
109
+ <ToolInvocationCard
110
+ toolName="test"
111
+ status="success"
112
+ statusText=""
113
+ />
114
+ )
115
+ expect(screen.queryByText('Ran')).not.toBeInTheDocument()
116
+ })
117
+
118
+ it('应接受自定义状态文本 props', () => {
119
+ render(
120
+ <ToolInvocationCard
121
+ toolName="test"
122
+ status="running"
123
+ runningText="执行中"
124
+ />
125
+ )
126
+ expect(screen.getByText('执行中')).toBeInTheDocument()
127
+ })
128
+ })
129
+
130
+ // ============================================================
131
+ // 响应展示测试
132
+ // ============================================================
133
+ describe('响应展示', () => {
134
+ it('有响应时应可展开', () => {
135
+ render(
136
+ <ToolInvocationCard
137
+ toolName="test"
138
+ status="success"
139
+ response="Tool output result"
140
+ />
141
+ )
142
+ // 点击展开
143
+ fireEvent.click(screen.getByText('test'))
144
+ expect(screen.getByText('Tool output result')).toBeInTheDocument()
145
+ })
146
+
147
+ it('defaultExpanded=true 时应默认展开', () => {
148
+ render(
149
+ <ToolInvocationCard
150
+ toolName="test"
151
+ status="success"
152
+ response="Already expanded"
153
+ defaultExpanded={true}
154
+ />
155
+ )
156
+ expect(screen.getByText('Already expanded')).toBeInTheDocument()
157
+ })
158
+
159
+ it('无响应时不应显示展开箭头', () => {
160
+ const { container } = render(
161
+ <ToolInvocationCard toolName="test" status="success" />
162
+ )
163
+ // 没有箭头图标(ArrowRightLine 的 SVG)
164
+ const arrows = container.querySelectorAll('[class*="rotate"]')
165
+ expect(arrows).toHaveLength(0)
166
+ })
167
+
168
+ it('running 状态时不应可展开', () => {
169
+ render(
170
+ <ToolInvocationCard
171
+ toolName="test"
172
+ status="running"
173
+ response="Some response"
174
+ />
175
+ )
176
+ // running 状态下点击不应展开
177
+ fireEvent.click(screen.getByText('test'))
178
+ expect(screen.queryByText('Some response')).not.toBeInTheDocument()
179
+ })
180
+
181
+ it('点击应切换展开/收起状态', () => {
182
+ render(
183
+ <ToolInvocationCard
184
+ toolName="toggle-test"
185
+ status="success"
186
+ response="Toggle content"
187
+ />
188
+ )
189
+ const trigger = screen.getByText('toggle-test')
190
+
191
+ // 第一次点击展开
192
+ fireEvent.click(trigger)
193
+ expect(screen.getByText('Toggle content')).toBeInTheDocument()
194
+
195
+ // 第二次点击收起
196
+ fireEvent.click(trigger)
197
+ expect(screen.queryByText('Toggle content')).not.toBeInTheDocument()
198
+ })
199
+ })
200
+
201
+ // ============================================================
202
+ // 响应标签测试
203
+ // ============================================================
204
+ describe('响应标签', () => {
205
+ it('展开时应显示默认标签 "Response"', () => {
206
+ render(
207
+ <ToolInvocationCard
208
+ toolName="test"
209
+ status="success"
210
+ response="Content"
211
+ defaultExpanded
212
+ />
213
+ )
214
+ expect(screen.getByText('Response')).toBeInTheDocument()
215
+ })
216
+
217
+ it('应接受自定义 responseLabel', () => {
218
+ render(
219
+ <ToolInvocationCard
220
+ toolName="test"
221
+ status="success"
222
+ response="Content"
223
+ responseLabel="输出结果"
224
+ defaultExpanded
225
+ />
226
+ )
227
+ expect(screen.getByText('输出结果')).toBeInTheDocument()
228
+ })
229
+ })
230
+
231
+ // ============================================================
232
+ // 错误状态样式测试
233
+ // ============================================================
234
+ describe('错误状态', () => {
235
+ it('error 状态响应应应用错误样式', () => {
236
+ render(
237
+ <ToolInvocationCard
238
+ toolName="test"
239
+ status="error"
240
+ response="Error message"
241
+ defaultExpanded
242
+ />
243
+ )
244
+ const errorText = screen.getByText('Error message')
245
+ expect(errorText).toHaveClass('text-error')
246
+ })
247
+
248
+ it('success 状态响应不应有错误样式', () => {
249
+ render(
250
+ <ToolInvocationCard
251
+ toolName="test"
252
+ status="success"
253
+ response="Success message"
254
+ defaultExpanded
255
+ />
256
+ )
257
+ const successText = screen.getByText('Success message')
258
+ expect(successText).not.toHaveClass('text-error')
259
+ })
260
+ })
261
+
262
+ // ============================================================
263
+ // className 测试
264
+ // ============================================================
265
+ describe('className', () => {
266
+ it('应合并自定义 className', () => {
267
+ const { container } = render(
268
+ <ToolInvocationCard
269
+ toolName="test"
270
+ status="success"
271
+ className="custom-card"
272
+ />
273
+ )
274
+ expect(container.firstChild).toHaveClass('custom-card')
275
+ })
276
+ })
277
+
278
+ // ============================================================
279
+ // 自定义图标 props 测试
280
+ // ============================================================
281
+ describe('自定义图标 props', () => {
282
+ it('应接受自定义 timeIcon', () => {
283
+ render(
284
+ <ToolInvocationCard
285
+ toolName="test"
286
+ status="running"
287
+ timeIcon={<span data-testid="time-icon">⏱</span>}
288
+ />
289
+ )
290
+ expect(screen.getByTestId('time-icon')).toBeInTheDocument()
291
+ })
292
+
293
+ it('应接受自定义 closeCircleIcon', () => {
294
+ render(
295
+ <ToolInvocationCard
296
+ toolName="test"
297
+ status="error"
298
+ closeCircleIcon={<span data-testid="close-icon">✗</span>}
299
+ />
300
+ )
301
+ expect(screen.getByTestId('close-icon')).toBeInTheDocument()
302
+ })
303
+
304
+ it('应接受自定义 arrowRightIcon', () => {
305
+ render(
306
+ <ToolInvocationCard
307
+ toolName="test"
308
+ status="success"
309
+ response="Content"
310
+ arrowRightIcon={<span data-testid="arrow-icon">→</span>}
311
+ />
312
+ )
313
+ expect(screen.getByTestId('arrow-icon')).toBeInTheDocument()
314
+ })
315
+ })
316
+
317
+ // ============================================================
318
+ // 组合场景测试
319
+ // ============================================================
320
+ describe('组合场景', () => {
321
+ it('应支持所有 props 组合', () => {
322
+ render(
323
+ <ToolInvocationCard
324
+ toolName="full_test"
325
+ params="/path/to/file.ts"
326
+ status="success"
327
+ response="File content here..."
328
+ defaultExpanded
329
+ statusText="已完成"
330
+ responseLabel="文件内容"
331
+ className="test-class"
332
+ checkboxCircleIcon={<span data-testid="check">✓</span>}
333
+ arrowRightIcon={<span data-testid="arrow">→</span>}
334
+ />
335
+ )
336
+
337
+ expect(screen.getByText('full_test')).toBeInTheDocument()
338
+ expect(screen.getByText('/path/to/file.ts')).toBeInTheDocument()
339
+ expect(screen.getByText('已完成')).toBeInTheDocument()
340
+ expect(screen.getByText('文件内容')).toBeInTheDocument()
341
+ expect(screen.getByText('File content here...')).toBeInTheDocument()
342
+ expect(screen.getByTestId('check')).toBeInTheDocument()
343
+ expect(screen.getByTestId('arrow')).toBeInTheDocument()
344
+ })
345
+ })
346
+ })
@@ -0,0 +1,180 @@
1
+ import * as React from 'react'
2
+ import { motion } from 'framer-motion'
3
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+ import { cn } from '@/lib/utils'
6
+ import { getThemeFromDocument } from './theme-from-document'
7
+
8
+ const AlertDialog = AlertDialogPrimitive.Root
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
11
+
12
+ const AlertDialogOverlay = React.forwardRef<
13
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
14
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
15
+ >(({ className, ...props }, ref) => (
16
+ <AlertDialogPrimitive.Overlay ref={ref} asChild {...props}>
17
+ <motion.div
18
+ className={cn('fixed inset-0 z-50 bg-bg-mask/60', className)}
19
+ initial={{ opacity: 0 }}
20
+ animate={{ opacity: 1 }}
21
+ transition={{ duration: 0.15 }}
22
+ />
23
+ </AlertDialogPrimitive.Overlay>
24
+ ))
25
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
26
+
27
+ const contentVariants = cva(
28
+ 'font-sans fixed left-1/2 top-1/2 z-50 w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-bg-base text-text shadow-lg p-6',
29
+ {
30
+ variants: {
31
+ size: { default: 'max-w-lg', sm: 'max-w-sm' },
32
+ },
33
+ defaultVariants: { size: 'default' },
34
+ }
35
+ )
36
+
37
+ export interface AlertDialogContentProps
38
+ extends React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>,
39
+ VariantProps<typeof contentVariants> {
40
+ size?: 'default' | 'sm'
41
+ /** When provided (e.g. from ThemeStyleProvider), used for portal wrapper */
42
+ dataStyle?: string
43
+ dataTheme?: string
44
+ }
45
+
46
+ const AlertDialogContent = React.forwardRef<
47
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
48
+ AlertDialogContentProps
49
+ >(({ className, children, size = 'default', dataStyle, dataTheme, ...props }, ref) => {
50
+ const fromDoc = getThemeFromDocument()
51
+ const dataProps =
52
+ dataStyle !== undefined
53
+ ? { 'data-style': dataStyle, 'data-theme': dataTheme ?? fromDoc['data-theme'] ?? '' }
54
+ : fromDoc
55
+ return (
56
+ <AlertDialogPortal>
57
+ <div style={{ display: 'contents' }} {...dataProps}>
58
+ <AlertDialogOverlay />
59
+ <AlertDialogPrimitive.Content ref={ref} asChild {...props}>
60
+ <motion.div
61
+ className={cn(contentVariants({ size }), className)}
62
+ initial={{ opacity: 0, y: 8 }}
63
+ animate={{ opacity: 1, y: 0 }}
64
+ transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
65
+ >
66
+ {children}
67
+ </motion.div>
68
+ </AlertDialogPrimitive.Content>
69
+ </div>
70
+ </AlertDialogPortal>
71
+ )
72
+ })
73
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
74
+
75
+ const AlertDialogHeader = React.forwardRef<
76
+ HTMLDivElement,
77
+ React.HTMLAttributes<HTMLDivElement>
78
+ >(({ className, ...props }, ref) => (
79
+ <div ref={ref} className={cn('flex flex-col gap-2', className)} {...props} />
80
+ ))
81
+ AlertDialogHeader.displayName = 'AlertDialogHeader'
82
+
83
+ const AlertDialogFooter = React.forwardRef<
84
+ HTMLDivElement,
85
+ React.HTMLAttributes<HTMLDivElement>
86
+ >(({ className, ...props }, ref) => (
87
+ <div
88
+ ref={ref}
89
+ className={cn('mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
90
+ {...props}
91
+ />
92
+ ))
93
+ AlertDialogFooter.displayName = 'AlertDialogFooter'
94
+
95
+ const AlertDialogTitle = React.forwardRef<
96
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
97
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
98
+ >(({ className, ...props }, ref) => (
99
+ <AlertDialogPrimitive.Title
100
+ ref={ref}
101
+ className={cn('font-sans text-base font-medium leading-6 text-text', className)}
102
+ {...props}
103
+ />
104
+ ))
105
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
106
+
107
+ const AlertDialogDescription = React.forwardRef<
108
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
109
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
110
+ >(({ className, ...props }, ref) => (
111
+ <AlertDialogPrimitive.Description
112
+ ref={ref}
113
+ className={cn('text-sm leading-6 text-text-tertiary', className)}
114
+ {...props}
115
+ />
116
+ ))
117
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
118
+
119
+ const actionVariants = cva(
120
+ 'inline-flex h-9 items-center justify-center rounded px-4 text-sm font-medium transition-colors focus:outline-none disabled:pointer-events-none disabled:opacity-50 cursor-pointer',
121
+ {
122
+ variants: {
123
+ destructive: {
124
+ true: 'bg-error text-text-on-primary hover:bg-error-hover active:bg-error',
125
+ false: 'bg-bg-highlight text-text-on-primary hover:bg-bg-highlight-hover',
126
+ },
127
+ },
128
+ defaultVariants: { destructive: false },
129
+ }
130
+ )
131
+
132
+ export interface AlertDialogActionProps
133
+ extends React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>,
134
+ VariantProps<typeof actionVariants> {
135
+ destructive?: boolean
136
+ }
137
+
138
+ const AlertDialogAction = React.forwardRef<
139
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
140
+ AlertDialogActionProps
141
+ >(({ className, destructive = false, ...props }, ref) => (
142
+ <AlertDialogPrimitive.Action
143
+ ref={ref}
144
+ className={cn(actionVariants({ destructive }), className)}
145
+ {...props}
146
+ />
147
+ ))
148
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
149
+
150
+ const AlertDialogCancel = React.forwardRef<
151
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
152
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
153
+ >(({ className, ...props }, ref) => (
154
+ <AlertDialogPrimitive.Cancel
155
+ ref={ref}
156
+ className={cn(
157
+ 'inline-flex h-9 items-center justify-center rounded px-4 text-sm font-medium cursor-pointer',
158
+ 'bg-transparent text-text-secondary hover:bg-fill-secondary hover:text-text',
159
+ 'transition-colors focus:outline-none',
160
+ className
161
+ )}
162
+ {...props}
163
+ />
164
+ ))
165
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
166
+
167
+ export {
168
+ AlertDialog,
169
+ AlertDialogTrigger,
170
+ AlertDialogPortal,
171
+ AlertDialogOverlay,
172
+ AlertDialogContent,
173
+ AlertDialogHeader,
174
+ AlertDialogFooter,
175
+ AlertDialogTitle,
176
+ AlertDialogDescription,
177
+ AlertDialogAction,
178
+ AlertDialogCancel,
179
+ }
180
+ export { contentVariants as alertDialogContentVariants, actionVariants as alertDialogActionVariants }
@@ -0,0 +1,120 @@
1
+ import * as React from 'react'
2
+ import * as AvatarPrimitive from '@radix-ui/react-avatar'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const avatarVariants = cva('relative flex shrink-0 rounded-full', {
7
+ variants: {
8
+ size: {
9
+ sm: 'h-8 w-8',
10
+ default: 'h-10 w-10',
11
+ lg: 'h-12 w-12',
12
+ },
13
+ },
14
+ defaultVariants: { size: 'default' },
15
+ })
16
+
17
+ export interface AvatarProps
18
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
19
+ VariantProps<typeof avatarVariants> {}
20
+
21
+ const Avatar = React.forwardRef<
22
+ React.ElementRef<typeof AvatarPrimitive.Root>,
23
+ AvatarProps
24
+ >(({ className, size = 'default', ...props }, ref) => (
25
+ <AvatarPrimitive.Root
26
+ ref={ref}
27
+ className={cn(avatarVariants({ size }), className)}
28
+ {...props}
29
+ />
30
+ ))
31
+ Avatar.displayName = AvatarPrimitive.Root.displayName
32
+
33
+ export type AvatarImageProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
34
+
35
+ const AvatarImage = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Image>,
37
+ AvatarImageProps
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Image
40
+ ref={ref}
41
+ className={cn(
42
+ 'aspect-square h-full w-full overflow-hidden rounded-full object-cover',
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ ))
48
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
49
+
50
+ export type AvatarFallbackProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
51
+
52
+ const AvatarFallback = React.forwardRef<
53
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
54
+ AvatarFallbackProps
55
+ >(({ className, ...props }, ref) => (
56
+ <AvatarPrimitive.Fallback
57
+ ref={ref}
58
+ className={cn(
59
+ 'flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-fill-tertiary text-sm font-medium text-text',
60
+ className
61
+ )}
62
+ {...props}
63
+ />
64
+ ))
65
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
66
+
67
+ export type AvatarBadgeProps = React.HTMLAttributes<HTMLSpanElement>
68
+
69
+ const AvatarBadge = React.forwardRef<HTMLSpanElement, AvatarBadgeProps>(
70
+ ({ className, ...props }, ref) => (
71
+ <span
72
+ ref={ref}
73
+ className={cn(
74
+ 'absolute bottom-0 right-0 z-10 block h-3 w-3 rounded-full border-2 border-bg-base bg-success',
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ )
80
+ )
81
+ AvatarBadge.displayName = 'AvatarBadge'
82
+
83
+ export type AvatarGroupProps = React.HTMLAttributes<HTMLDivElement>
84
+
85
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
86
+ ({ className, ...props }, ref) => (
87
+ <div
88
+ ref={ref}
89
+ className={cn('flex -space-x-2 [&>*]:ring-2 [&>*]:ring-bg-base', className)}
90
+ {...props}
91
+ />
92
+ )
93
+ )
94
+ AvatarGroup.displayName = 'AvatarGroup'
95
+
96
+ export type AvatarGroupCountProps = React.HTMLAttributes<HTMLSpanElement>
97
+
98
+ const AvatarGroupCount = React.forwardRef<HTMLSpanElement, AvatarGroupCountProps>(
99
+ ({ className, ...props }, ref) => (
100
+ <span
101
+ ref={ref}
102
+ className={cn(
103
+ 'inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-bg-base bg-fill-tertiary text-sm font-medium text-text',
104
+ className
105
+ )}
106
+ {...props}
107
+ />
108
+ )
109
+ )
110
+ AvatarGroupCount.displayName = 'AvatarGroupCount'
111
+
112
+ export {
113
+ Avatar,
114
+ AvatarImage,
115
+ AvatarFallback,
116
+ AvatarBadge,
117
+ AvatarGroup,
118
+ AvatarGroupCount,
119
+ }
120
+ export { avatarVariants }