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,80 @@
1
+ import * as React from 'react'
2
+ import { motion } from 'framer-motion'
3
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
4
+ import { cn } from '@/lib/utils'
5
+ import { getThemeFromDocument } from './theme-from-document'
6
+
7
+ const TooltipProvider = TooltipPrimitive.Provider
8
+
9
+ const TooltipRoot = TooltipPrimitive.Root
10
+
11
+ const TooltipTrigger = TooltipPrimitive.Trigger
12
+
13
+ const TooltipContent = React.forwardRef<
14
+ React.ElementRef<typeof TooltipPrimitive.Content>,
15
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
16
+ /** When provided (e.g. from ThemeStyleProvider), used for portal wrapper instead of getThemeFromDocument */
17
+ dataStyle?: string
18
+ dataTheme?: string
19
+ }
20
+ >(({ className, sideOffset = 8, children, dataStyle, dataTheme, ...props }, ref) => {
21
+ const fromDoc = getThemeFromDocument()
22
+ const dataProps =
23
+ dataStyle !== undefined
24
+ ? { 'data-style': dataStyle, 'data-theme': dataTheme ?? fromDoc['data-theme'] ?? '' }
25
+ : fromDoc
26
+ return (
27
+ <TooltipPrimitive.Portal>
28
+ <div style={{ display: 'contents' }} {...dataProps}>
29
+ <TooltipPrimitive.Content
30
+ ref={ref}
31
+ sideOffset={sideOffset}
32
+ className={cn(
33
+ 'z-50 max-w-60 overflow-hidden rounded px-2 py-1 text-xs leading-5',
34
+ 'bg-bg-highlight text-text-on-primary shadow-sm',
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.15 }}>
40
+ {children}
41
+ </motion.div>
42
+ </TooltipPrimitive.Content>
43
+ </div>
44
+ </TooltipPrimitive.Portal>
45
+ )
46
+ })
47
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
48
+
49
+ export interface TooltipProps {
50
+ content: React.ReactNode
51
+ children: React.ReactNode
52
+ side?: 'top' | 'right' | 'bottom' | 'left'
53
+ contentClassName?: string
54
+ /** When provided, portal content uses this data-style (e.g. from ThemeStyleProvider) */
55
+ dataStyle?: string
56
+ /** When provided, portal content uses this data-theme */
57
+ dataTheme?: string
58
+ }
59
+
60
+ function Tooltip({ content, children, side = 'top', contentClassName, dataStyle, dataTheme }: TooltipProps) {
61
+ return (
62
+ <TooltipProvider delayDuration={300}>
63
+ <TooltipRoot>
64
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
65
+ <TooltipContent side={side} className={contentClassName} dataStyle={dataStyle} dataTheme={dataTheme}>
66
+ {content}
67
+ </TooltipContent>
68
+ </TooltipRoot>
69
+ </TooltipProvider>
70
+ )
71
+ }
72
+ Tooltip.displayName = 'Tooltip'
73
+
74
+ export {
75
+ TooltipProvider,
76
+ TooltipRoot,
77
+ TooltipTrigger,
78
+ TooltipContent,
79
+ Tooltip,
80
+ }
@@ -0,0 +1,201 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import { Scrollbar } from './scrollbar'
4
+
5
+ export type TypographyH1Props = React.ComponentProps<'h1'>
6
+ function TypographyH1({ className, ...props }: TypographyH1Props) {
7
+ return (
8
+ <h1
9
+ className={cn(
10
+ 'font-sans scroll-m-20 text-4xl font-extrabold tracking-tight text-balance text-text',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+ TypographyH1.displayName = 'TypographyH1'
18
+
19
+ export type TypographyH2Props = React.ComponentProps<'h2'>
20
+ function TypographyH2({ className, ...props }: TypographyH2Props) {
21
+ return (
22
+ <h2
23
+ className={cn(
24
+ 'font-sans scroll-m-20 border-b border-border-tertiary pb-2 text-3xl font-semibold tracking-tight first:mt-0 text-text',
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+ TypographyH2.displayName = 'TypographyH2'
32
+
33
+ export type TypographyH3Props = React.ComponentProps<'h3'>
34
+ function TypographyH3({ className, ...props }: TypographyH3Props) {
35
+ return (
36
+ <h3
37
+ className={cn(
38
+ 'font-sans scroll-m-20 text-2xl font-semibold tracking-tight text-text',
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+ TypographyH3.displayName = 'TypographyH3'
46
+
47
+ export type TypographyH4Props = React.ComponentProps<'h4'>
48
+ function TypographyH4({ className, ...props }: TypographyH4Props) {
49
+ return (
50
+ <h4
51
+ className={cn(
52
+ 'font-sans scroll-m-20 text-xl font-semibold tracking-tight text-text',
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+ TypographyH4.displayName = 'TypographyH4'
60
+
61
+ export type TypographyPProps = React.ComponentProps<'p'>
62
+ function TypographyP({ className, ...props }: TypographyPProps) {
63
+ return (
64
+ <p
65
+ className={cn(
66
+ 'font-sans leading-7 text-text [&:not(:first-child)]:mt-6',
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+ TypographyP.displayName = 'TypographyP'
74
+
75
+ export type TypographyBlockquoteProps = React.ComponentProps<'blockquote'>
76
+ function TypographyBlockquote({ className, ...props }: TypographyBlockquoteProps) {
77
+ return (
78
+ <blockquote
79
+ className={cn(
80
+ 'font-sans mt-6 border-l-2 border-border-tertiary pl-6 italic text-text-secondary',
81
+ className
82
+ )}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+ TypographyBlockquote.displayName = 'TypographyBlockquote'
88
+
89
+ export type TypographyTableProps = React.ComponentProps<'div'>
90
+ function TypographyTable({ className, children, ..._rest }: TypographyTableProps) {
91
+ void _rest
92
+ return (
93
+ <Scrollbar
94
+ scrollbarVisibility="auto"
95
+ className={cn('font-sans my-6 w-full', className)}
96
+ >
97
+ {children}
98
+ </Scrollbar>
99
+ )
100
+ }
101
+ TypographyTable.displayName = 'TypographyTable'
102
+
103
+ export type TypographyListProps = React.ComponentProps<'ul'>
104
+ function TypographyList({ className, ...props }: TypographyListProps) {
105
+ return (
106
+ <ul
107
+ className={cn(
108
+ 'font-sans my-6 ml-6 list-disc text-text [&>li]:mt-2',
109
+ className
110
+ )}
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+ TypographyList.displayName = 'TypographyList'
116
+
117
+ export type TypographyInlineCodeProps = React.ComponentProps<'code'>
118
+ function TypographyInlineCode({ className, ...props }: TypographyInlineCodeProps) {
119
+ return (
120
+ <code
121
+ className={cn(
122
+ 'relative rounded bg-fill-secondary px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-text',
123
+ className
124
+ )}
125
+ {...props}
126
+ />
127
+ )
128
+ }
129
+ TypographyInlineCode.displayName = 'TypographyInlineCode'
130
+
131
+ export type TypographyLeadProps = React.ComponentProps<'p'>
132
+ function TypographyLead({ className, ...props }: TypographyLeadProps) {
133
+ return (
134
+ <p
135
+ className={cn(
136
+ 'font-sans text-xl text-muted-foreground',
137
+ className
138
+ )}
139
+ {...props}
140
+ />
141
+ )
142
+ }
143
+ TypographyLead.displayName = 'TypographyLead'
144
+
145
+ export type TypographyLargeProps = React.ComponentProps<'div'>
146
+ function TypographyLarge({ className, ...props }: TypographyLargeProps) {
147
+ return (
148
+ <div
149
+ className={cn(
150
+ 'font-sans text-lg font-semibold text-text',
151
+ className
152
+ )}
153
+ {...props}
154
+ />
155
+ )
156
+ }
157
+ TypographyLarge.displayName = 'TypographyLarge'
158
+
159
+ export type TypographySmallProps = React.ComponentProps<'small'>
160
+ function TypographySmall({ className, ...props }: TypographySmallProps) {
161
+ return (
162
+ <small
163
+ className={cn(
164
+ 'font-sans text-sm font-medium leading-none text-text',
165
+ className
166
+ )}
167
+ {...props}
168
+ />
169
+ )
170
+ }
171
+ TypographySmall.displayName = 'TypographySmall'
172
+
173
+ export type TypographyMutedProps = React.ComponentProps<'p'>
174
+ function TypographyMuted({ className, ...props }: TypographyMutedProps) {
175
+ return (
176
+ <p
177
+ className={cn(
178
+ 'font-sans text-sm text-muted-foreground',
179
+ className
180
+ )}
181
+ {...props}
182
+ />
183
+ )
184
+ }
185
+ TypographyMuted.displayName = 'TypographyMuted'
186
+
187
+ export {
188
+ TypographyH1,
189
+ TypographyH2,
190
+ TypographyH3,
191
+ TypographyH4,
192
+ TypographyP,
193
+ TypographyBlockquote,
194
+ TypographyTable,
195
+ TypographyList,
196
+ TypographyInlineCode,
197
+ TypographyLead,
198
+ TypographyLarge,
199
+ TypographySmall,
200
+ TypographyMuted,
201
+ }
@@ -0,0 +1,70 @@
1
+ import type { ReactNode } from 'react'
2
+ import { CollapsibleCard } from '../basic/collapsible-card'
3
+ import { Button } from '../basic/button'
4
+ import { QuestionnaireLine, ArrowDownSLine } from '../basic/icons-inline'
5
+
6
+ const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0 text-text-secondary'
7
+
8
+ export interface AskUserPartProps {
9
+ title?: string
10
+ question: string
11
+ skipText?: string
12
+ runText?: string
13
+ askTimeText?: string
14
+ onSkip?: () => void
15
+ onRun?: () => void
16
+ onAskTimeChange?: (value: string) => void
17
+ contentMaxHeight?: number
18
+ collapsible?: boolean
19
+ className?: string
20
+ headerIcon?: ReactNode
21
+ }
22
+
23
+ const defaultHeaderIcon = <QuestionnaireLine className={ICON_CLASS} />
24
+
25
+ export function AskUserPart({
26
+ title = 'Initializing agent…',
27
+ question,
28
+ skipText = 'Skip ⌘⌫',
29
+ runText = 'Run ⌘⏎',
30
+ askTimeText = 'Ask any time',
31
+ onSkip,
32
+ onRun,
33
+ contentMaxHeight,
34
+ collapsible = true,
35
+ className,
36
+ headerIcon,
37
+ }: AskUserPartProps) {
38
+ return (
39
+ <CollapsibleCard
40
+ headerIcon={headerIcon ?? defaultHeaderIcon}
41
+ headerTitle={<span className="text-left">{title}</span>}
42
+ contentPadding="var(--spacing-1\.5) var(--spacing-2)"
43
+ defaultExpanded
44
+ contentMaxHeight={contentMaxHeight}
45
+ collapsible={collapsible}
46
+ footer={
47
+ <>
48
+ <Button variant="ghost" size="sm" className="gap-1 py-0.5 px-0 text-text-secondary hover:text-text">
49
+ <span className="text-xs">{askTimeText}</span>
50
+ <ArrowDownSLine className="w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0" />
51
+ </Button>
52
+ <div className="flex-1" />
53
+ <Button variant="ghost" size="sm" onClick={onSkip} className="bg-fill text-text-tertiary hover:bg-fill-secondary hover:text-text-secondary">
54
+ {skipText}
55
+ </Button>
56
+ <Button variant="secondary" size="sm" onClick={onRun}>
57
+ {runText}
58
+ </Button>
59
+ </>
60
+ }
61
+ className={className}
62
+ >
63
+ <div className="text-sm text-left">
64
+ <span className="text-text">{question}</span>
65
+ </div>
66
+ </CollapsibleCard>
67
+ )
68
+ }
69
+
70
+ AskUserPart.displayName = 'AskUserPart'
@@ -0,0 +1,166 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { CollapsibleCard } from '../basic/collapsible-card'
3
+ import { Button } from '../basic/button'
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from '../basic/select'
11
+ import { GlobalLine, WarningLine, Forbid2Line } from '../basic/icons-inline'
12
+
13
+ const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0'
14
+
15
+ export type BrowserActionStatus = 'normal' | 'failed' | 'skipped'
16
+
17
+ export interface BrowserActionPartProps {
18
+ description?: string
19
+ content?: string
20
+ status?: BrowserActionStatus
21
+ onRun?: () => void
22
+ onSkip?: () => void
23
+ onViewDetail?: () => void
24
+ onAskEveryTimeChange?: (value: string) => void
25
+ className?: string
26
+ /** 供 Select 下拉浮层继承布局风格(如 dense),不传则从 document 读取 */
27
+ dataStyle?: string
28
+ /** 供 Select 下拉浮层继承颜色主题,不传则从 document 读取 */
29
+ dataTheme?: string
30
+ normalIcon?: ReactNode
31
+ failedIcon?: ReactNode
32
+ skippedIcon?: ReactNode
33
+ }
34
+
35
+ const defaultNormalIcon = <GlobalLine className={`${ICON_CLASS} text-text-secondary`} />
36
+ const defaultFailedIcon = <WarningLine className={`${ICON_CLASS} text-warning`} />
37
+ const defaultSkippedIcon = <Forbid2Line className={`${ICON_CLASS} text-text-secondary`} />
38
+
39
+ function BrowserFooter({
40
+ askValue,
41
+ onAskChange,
42
+ onSkip,
43
+ onRun,
44
+ dataStyle,
45
+ dataTheme,
46
+ }: {
47
+ askValue: string
48
+ onAskChange: (value: string) => void
49
+ onSkip: () => void
50
+ onRun: () => void
51
+ dataStyle?: string
52
+ dataTheme?: string
53
+ }) {
54
+ return (
55
+ <div className="flex items-center gap-2 flex-1 min-w-0">
56
+ <div className="shrink-0 w-fit">
57
+ <Select value={askValue} onValueChange={onAskChange}>
58
+ <SelectTrigger
59
+ className="h-7 w-auto min-w-0 border-0 bg-transparent px-2 py-0 text-xs text-text-tertiary hover:text-text [&>svg]:w-[var(--font-size-sm)] [&>svg]:h-[var(--font-size-sm)]"
60
+ onClick={(e) => e.stopPropagation()}
61
+ >
62
+ <SelectValue />
63
+ </SelectTrigger>
64
+ <SelectContent
65
+ dataStyle={dataStyle}
66
+ dataTheme={dataTheme}
67
+ onClick={(e) => e.stopPropagation()}
68
+ >
69
+ <SelectItem value="Ask every time">Ask every time</SelectItem>
70
+ <SelectItem value="Always allow">Always allow</SelectItem>
71
+ <SelectItem value="Always deny">Always deny</SelectItem>
72
+ </SelectContent>
73
+ </Select>
74
+ </div>
75
+ <div className="flex-1 h-4 border-l border-border-tertiary opacity-10" />
76
+ <Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onSkip() }}>
77
+ Skip ⌘⌫
78
+ </Button>
79
+ <Button variant="secondary" size="sm" onClick={(e) => { e.stopPropagation(); onRun() }}>
80
+ Run ⌘⏎
81
+ </Button>
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export function BrowserActionPart({
87
+ description,
88
+ content,
89
+ status = 'normal',
90
+ onRun,
91
+ onSkip,
92
+ onViewDetail,
93
+ onAskEveryTimeChange,
94
+ className,
95
+ dataStyle,
96
+ dataTheme,
97
+ normalIcon,
98
+ failedIcon,
99
+ skippedIcon,
100
+ }: BrowserActionPartProps) {
101
+ const [askValue, setAskValue] = useState('Ask every time')
102
+
103
+ const handleAskChange = (value: string) => {
104
+ setAskValue(value)
105
+ onAskEveryTimeChange?.(value)
106
+ }
107
+
108
+ const headerIcon =
109
+ status === 'failed'
110
+ ? (failedIcon ?? defaultFailedIcon)
111
+ : status === 'skipped'
112
+ ? (skippedIcon ?? defaultSkippedIcon)
113
+ : (normalIcon ?? defaultNormalIcon)
114
+
115
+ const headerTitle =
116
+ status === 'failed' ? (
117
+ <span>Browser Failed</span>
118
+ ) : status === 'skipped' ? (
119
+ <span>Browser Skipped</span>
120
+ ) : (
121
+ <div className="flex items-center gap-2 flex-1 min-w-0">
122
+ <span className="shrink-0">Browser</span>
123
+ {description != null && (
124
+ <span className="flex-1 min-w-0 truncate text-text-tertiary">{description}</span>
125
+ )}
126
+ </div>
127
+ )
128
+
129
+ const headerRight =
130
+ status === 'failed' ? (
131
+ <Button variant="text" size="sm" onClick={(e) => { e.stopPropagation(); onViewDetail?.() }}>
132
+ View Detail
133
+ </Button>
134
+ ) : undefined
135
+
136
+ const footer =
137
+ status === 'normal' ? (
138
+ <BrowserFooter
139
+ askValue={askValue}
140
+ onAskChange={handleAskChange}
141
+ onSkip={() => onSkip?.()}
142
+ onRun={() => onRun?.()}
143
+ dataStyle={dataStyle}
144
+ dataTheme={dataTheme}
145
+ />
146
+ ) : undefined
147
+
148
+ return (
149
+ <CollapsibleCard
150
+ headerIcon={headerIcon}
151
+ headerTitle={headerTitle}
152
+ headerRight={headerRight}
153
+ contentPadding="var(--spacing-1\.5) var(--spacing-2)"
154
+ defaultExpanded={status === 'normal'}
155
+ collapsible={status === 'normal'}
156
+ footer={footer}
157
+ className={className}
158
+ >
159
+ {content != null && content !== '' && (
160
+ <span className="text-xs leading-xs text-text whitespace-pre-wrap break-words">{content}</span>
161
+ )}
162
+ </CollapsibleCard>
163
+ )
164
+ }
165
+
166
+ BrowserActionPart.displayName = 'BrowserActionPart'
@@ -0,0 +1,185 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+ import { FolderButton } from '../folder-button'
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuTrigger,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSub,
9
+ DropdownMenuSubTrigger,
10
+ DropdownMenuSubContent,
11
+ } from '../../basic/dropdown-menu'
12
+ import { FolderPermissionDialog } from './folder-permission-dialog'
13
+ import { FolderLine, FolderFillLine, TimeLine } from '../../basic/icons-inline'
14
+ import type { FolderPermissionConfig, InputPropsWithWebkitDirectory } from './types'
15
+
16
+ const MOCK_HISTORY_FOLDERS = ['~/Projects/qoder-work-ui', '~/Documents/workspace', '~/Desktop/demo', '~/code/my-app']
17
+
18
+ const iconClass = 'mr-2 w-4 h-4 shrink-0'
19
+
20
+ export interface ChatInputFolderSelectorProps {
21
+ disabled: boolean
22
+ folderButtonLabel: string
23
+ folderPermission?: FolderPermissionConfig
24
+ onFolderButtonClick?: () => void
25
+ onFolderClear?: () => void
26
+ onFolderSelect: (files: FileList) => void
27
+ hasFolderSelected: boolean
28
+ dataStyle?: string
29
+ dataTheme?: string
30
+ folderIcon?: React.ReactNode
31
+ folderOpenIcon?: React.ReactNode
32
+ historyIcon?: React.ReactNode
33
+ subMenuArrowIcon?: React.ReactNode
34
+ folderFillIcon?: React.ReactNode
35
+ closeIcon?: React.ReactNode
36
+ }
37
+
38
+ export function ChatInputFolderSelector({
39
+ disabled,
40
+ folderButtonLabel,
41
+ folderPermission,
42
+ onFolderButtonClick,
43
+ onFolderClear,
44
+ onFolderSelect,
45
+ hasFolderSelected,
46
+ dataStyle,
47
+ dataTheme,
48
+ folderIcon = <FolderLine className={iconClass} />,
49
+ folderOpenIcon = <FolderFillLine className={iconClass} />,
50
+ historyIcon = <TimeLine className={iconClass} />,
51
+ subMenuArrowIcon,
52
+ folderFillIcon,
53
+ closeIcon,
54
+ }: ChatInputFolderSelectorProps) {
55
+ const enableFolderPermissionDialog = folderPermission?.enable ?? false
56
+ const folderPermissionProjectName = folderPermission?.projectName ?? '04_代码'
57
+ const [folderPermissionOpen, setFolderPermissionOpen] = useState(false)
58
+ const [folderPermissionAlwaysAllow, setFolderPermissionAlwaysAllow] = useState(false)
59
+ const [folderMenuOpen, setFolderMenuOpen] = useState(false)
60
+ const folderInputRef = useRef<HTMLInputElement>(null)
61
+
62
+ const handleFolderButtonClick = onFolderButtonClick ?? (() => {})
63
+
64
+ const openFolderPicker = () => {
65
+ folderInputRef.current?.click()
66
+ }
67
+
68
+ const handleFolderClear = () => {
69
+ if (folderInputRef.current) folderInputRef.current.value = ''
70
+ onFolderClear?.()
71
+ }
72
+
73
+ const requestFolderPermission = useCallback((mode: 'once' | 'always') => {
74
+ if (mode === 'always') setFolderPermissionAlwaysAllow(true)
75
+ setFolderPermissionOpen(false)
76
+ setFolderMenuOpen(true)
77
+ }, [])
78
+
79
+ const handleFolderMenuOpenChange = (next: boolean) => {
80
+ if (!enableFolderPermissionDialog) {
81
+ setFolderMenuOpen(next)
82
+ return
83
+ }
84
+ if (!next) {
85
+ setFolderMenuOpen(false)
86
+ return
87
+ }
88
+ if (folderPermissionAlwaysAllow) {
89
+ setFolderMenuOpen(true)
90
+ return
91
+ }
92
+ setFolderPermissionOpen(true)
93
+ setFolderMenuOpen(false)
94
+ }
95
+
96
+ const handleFolderInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
97
+ const files = e.target.files
98
+ if (files && files.length > 0) onFolderSelect(files)
99
+ }
100
+
101
+ if (onFolderButtonClick) {
102
+ return (
103
+ <FolderButton
104
+ label={folderButtonLabel}
105
+ onClick={handleFolderButtonClick}
106
+ disabled={disabled}
107
+ folderIcon={folderIcon}
108
+ folderFillIcon={folderFillIcon}
109
+ />
110
+ )
111
+ }
112
+
113
+ return (
114
+ <>
115
+ <DropdownMenu open={folderMenuOpen} onOpenChange={handleFolderMenuOpenChange}>
116
+ <DropdownMenuTrigger asChild>
117
+ <FolderButton
118
+ label={folderButtonLabel}
119
+ disabled={disabled}
120
+ selected={hasFolderSelected}
121
+ onClear={handleFolderClear}
122
+ folderIcon={folderIcon}
123
+ folderFillIcon={folderFillIcon}
124
+ closeIcon={closeIcon}
125
+ className="focus:outline-none focus-visible:outline-none focus:ring-0 focus-visible:ring-0"
126
+ />
127
+ </DropdownMenuTrigger>
128
+ <DropdownMenuContent
129
+ align="start"
130
+ side="top"
131
+ className="min-w-50"
132
+ dataStyle={dataStyle}
133
+ dataTheme={dataTheme}
134
+ >
135
+ <DropdownMenuItem onClick={openFolderPicker}>
136
+ {folderIcon}
137
+ Add folder
138
+ </DropdownMenuItem>
139
+ {hasFolderSelected && (
140
+ <DropdownMenuItem onClick={openFolderPicker}>
141
+ {folderOpenIcon}
142
+ Replace folder
143
+ </DropdownMenuItem>
144
+ )}
145
+ <DropdownMenuSub>
146
+ <DropdownMenuSubTrigger subTriggerIcon={subMenuArrowIcon}>
147
+ {historyIcon}
148
+ Recent folders
149
+ </DropdownMenuSubTrigger>
150
+ <DropdownMenuSubContent
151
+ collisionPadding={{ bottom: 24, left: 8 }}
152
+ dataStyle={dataStyle}
153
+ dataTheme={dataTheme}
154
+ >
155
+ {MOCK_HISTORY_FOLDERS.map((path) => (
156
+ <DropdownMenuItem key={path} onClick={() => {}}>
157
+ {path}
158
+ </DropdownMenuItem>
159
+ ))}
160
+ </DropdownMenuSubContent>
161
+ </DropdownMenuSub>
162
+ </DropdownMenuContent>
163
+ </DropdownMenu>
164
+ <input
165
+ ref={folderInputRef}
166
+ type="file"
167
+ {...({ webkitdirectory: '' } as InputPropsWithWebkitDirectory)}
168
+ multiple
169
+ style={{ display: 'none' }}
170
+ onChange={handleFolderInputChange}
171
+ />
172
+ {enableFolderPermissionDialog && (
173
+ <FolderPermissionDialog
174
+ open={folderPermissionOpen}
175
+ onOpenChange={setFolderPermissionOpen}
176
+ projectName={folderPermissionProjectName}
177
+ onAllowOnce={() => requestFolderPermission('once')}
178
+ onAllowAlways={() => requestFolderPermission('always')}
179
+ />
180
+ )}
181
+ </>
182
+ )
183
+ }
184
+
185
+ ChatInputFolderSelector.displayName = 'ChatInputFolderSelector'