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,131 @@
1
+ import {
2
+ DropdownMenu,
3
+ DropdownMenuTrigger,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ } from '../../basic/dropdown-menu'
7
+ import { ArrowDownSLine, CheckLine } from '../../basic/icons-inline'
8
+ import type { FooterLeftModelSwitchConfig } from './types'
9
+
10
+ export interface ChatInputModelSwitcherProps {
11
+ config: FooterLeftModelSwitchConfig
12
+ disabled: boolean
13
+ arrowDownIcon?: React.ReactNode
14
+ dataStyle?: string
15
+ dataTheme?: string
16
+ }
17
+
18
+ export function ChatInputModelSwitcher({
19
+ config,
20
+ disabled,
21
+ dataStyle,
22
+ dataTheme,
23
+ arrowDownIcon = <ArrowDownSLine className="h-3.5 w-3.5 shrink-0 text-text-secondary" />,
24
+ }: ChatInputModelSwitcherProps) {
25
+ const currentModeOption = config.modeSwitch.options.find((o) => o.value === config.modeSwitch.value)
26
+ const modeTriggerIcon = currentModeOption?.icon ?? config.modeSwitch.icon
27
+
28
+ return (
29
+ <div className="flex items-center gap-3 text-sm leading-sm text-text-secondary">
30
+ <DropdownMenu>
31
+ <DropdownMenuTrigger asChild>
32
+ <button
33
+ type="button"
34
+ className="inline-flex items-center gap-1.5 rounded-md bg-fill-secondary px-2.5 py-1.5 text-sm leading-sm text-text hover:bg-fill focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
35
+ disabled={disabled}
36
+ >
37
+ {modeTriggerIcon != null && (
38
+ <span className="flex shrink-0 items-center [&>*]:block [&>*]:leading-none">
39
+ {modeTriggerIcon}
40
+ </span>
41
+ )}
42
+ <span>{config.modeSwitch.label}</span>
43
+ {arrowDownIcon}
44
+ </button>
45
+ </DropdownMenuTrigger>
46
+ <DropdownMenuContent align="start" side="top" className="min-w-30" dataStyle={dataStyle} dataTheme={dataTheme}>
47
+ {config.modeSwitch.options.map((opt) => (
48
+ <DropdownMenuItem
49
+ key={opt.value}
50
+ onClick={() => config.modeSwitch.onChange(opt.value)}
51
+ className="flex items-center justify-between gap-2"
52
+ >
53
+ <span className="flex items-center gap-2">
54
+ {opt.icon != null && (
55
+ <span className="flex shrink-0 [&>*]:block [&>*]:leading-none [&>*]:size-4">
56
+ {opt.icon}
57
+ </span>
58
+ )}
59
+ {opt.label}
60
+ </span>
61
+ {config.modeSwitch.value === opt.value && (
62
+ <CheckLine className="h-4 w-4 shrink-0 text-primary" />
63
+ )}
64
+ </DropdownMenuItem>
65
+ ))}
66
+ </DropdownMenuContent>
67
+ </DropdownMenu>
68
+
69
+ <DropdownMenu>
70
+ <DropdownMenuTrigger asChild>
71
+ <button
72
+ type="button"
73
+ className="inline-flex items-center gap-1 text-text-secondary hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1 py-0.5 disabled:opacity-50"
74
+ disabled={disabled}
75
+ >
76
+ <span>{config.modelSwitch.label}</span>
77
+ {arrowDownIcon}
78
+ </button>
79
+ </DropdownMenuTrigger>
80
+ <DropdownMenuContent align="start" side="top" className="min-w-25" dataStyle={dataStyle} dataTheme={dataTheme}>
81
+ {config.modelSwitch.options.map((opt) => (
82
+ <DropdownMenuItem
83
+ key={opt.value}
84
+ onClick={() => config.modelSwitch.onChange(opt.value)}
85
+ className="flex items-center justify-between gap-2"
86
+ >
87
+ {opt.label}
88
+ {config.modelSwitch.value === opt.value && (
89
+ <CheckLine className="h-4 w-4 shrink-0 text-primary" />
90
+ )}
91
+ </DropdownMenuItem>
92
+ ))}
93
+ </DropdownMenuContent>
94
+ </DropdownMenu>
95
+
96
+ {config.consumptionMultiplier.options != null &&
97
+ config.consumptionMultiplier.options.length > 0 &&
98
+ config.consumptionMultiplier.onChange != null ? (
99
+ <DropdownMenu>
100
+ <DropdownMenuTrigger asChild>
101
+ <button
102
+ type="button"
103
+ className="inline-flex items-center text-text-secondary hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1 py-0.5 disabled:opacity-50"
104
+ disabled={disabled}
105
+ >
106
+ {config.consumptionMultiplier.value}
107
+ </button>
108
+ </DropdownMenuTrigger>
109
+ <DropdownMenuContent align="start" side="top" className="min-w-20" dataStyle={dataStyle} dataTheme={dataTheme}>
110
+ {config.consumptionMultiplier.options.map((opt) => (
111
+ <DropdownMenuItem
112
+ key={opt.value}
113
+ onClick={() => config.consumptionMultiplier.onChange?.(opt.value)}
114
+ className="flex items-center justify-between gap-2"
115
+ >
116
+ {opt.label}
117
+ {config.consumptionMultiplier.value === opt.value && (
118
+ <CheckLine className="h-4 w-4 shrink-0 text-primary" />
119
+ )}
120
+ </DropdownMenuItem>
121
+ ))}
122
+ </DropdownMenuContent>
123
+ </DropdownMenu>
124
+ ) : (
125
+ <span className="text-text-secondary">{config.consumptionMultiplier.value}</span>
126
+ )}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ ChatInputModelSwitcher.displayName = 'ChatInputModelSwitcher'
@@ -0,0 +1,67 @@
1
+ import { forwardRef, useCallback, useRef } from 'react'
2
+ import { useAutoResizeTextarea } from './useAutoResizeTextarea'
3
+
4
+ export interface ChatInputTextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange'> {
5
+ value: string
6
+ onChange?: (value: string) => void
7
+ placeholder?: string
8
+ disabled?: boolean
9
+ maxRows?: number
10
+ onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
11
+ }
12
+
13
+ export const ChatInputTextarea = forwardRef<HTMLTextAreaElement, ChatInputTextareaProps>(
14
+ (
15
+ {
16
+ value,
17
+ onChange,
18
+ placeholder = 'What can I help you with today?',
19
+ disabled = false,
20
+ maxRows = 6,
21
+ onKeyDown,
22
+ className,
23
+ style,
24
+ ...props
25
+ },
26
+ ref
27
+ ) => {
28
+ const innerRef = useRef<HTMLTextAreaElement>(null)
29
+ useAutoResizeTextarea(innerRef, value, maxRows)
30
+
31
+ const setRef = useCallback(
32
+ (node: HTMLTextAreaElement | null) => {
33
+ (innerRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
34
+ if (typeof ref === 'function') ref(node)
35
+ else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
36
+ },
37
+ [ref]
38
+ )
39
+
40
+ const handleChange = useCallback(
41
+ (e: React.ChangeEvent<HTMLTextAreaElement>) => onChange?.(e.target.value),
42
+ [onChange]
43
+ )
44
+
45
+ const textareaNoFocusStyle = { outline: 'none', border: 'none', boxShadow: 'none' } as const
46
+ const textareaStyles =
47
+ 'w-full resize-none border-none bg-transparent text-sm leading-sm font-normal placeholder:text-text-tertiary overflow-y-auto scrollbar-auto outline-none shadow-none focus:outline-none focus:ring-0 focus:shadow-none focus-visible:outline-none focus-visible:ring-0 focus-visible:shadow-none [&:focus-visible]:outline-none [&:focus-visible]:ring-0 [&:focus-visible]:ring-offset-0 [&:focus-visible]:shadow-none ' +
48
+ (value?.trim() ? 'text-text' : 'text-text-quaternary')
49
+
50
+ return (
51
+ <textarea
52
+ ref={setRef}
53
+ value={value}
54
+ onChange={handleChange}
55
+ onKeyDown={onKeyDown}
56
+ placeholder={placeholder}
57
+ disabled={disabled}
58
+ rows={1}
59
+ className={textareaStyles}
60
+ {...props}
61
+ style={{ ...(style ?? {}), ...textareaNoFocusStyle }}
62
+ />
63
+ )
64
+ }
65
+ )
66
+
67
+ ChatInputTextarea.displayName = 'ChatInputTextarea'
@@ -0,0 +1,334 @@
1
+ import React, { forwardRef, useCallback } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { cva } from 'class-variance-authority'
4
+ import { cn } from '@/lib/utils'
5
+ import { SendButton } from '../send-button'
6
+ import { IconButton } from '../../basic/icon-button'
7
+ import { MoreLine } from '../../basic/icons-inline'
8
+ import { GenerationStatusBar } from '../generation-status-bar'
9
+ import { ChatInputTextarea } from './chat-input-textarea'
10
+ import { ChatInputFolderSelector } from './chat-input-folder-selector'
11
+ import { ChatInputModelSwitcher } from './chat-input-model-switcher'
12
+ import { useChatInputContext } from './context'
13
+
14
+ const chatInputContainerVariants = cva(
15
+ [
16
+ 'flex flex-col w-full bg-bg-base border border-primary-border rounded-xl transition-all duration-200',
17
+ /* 参考 Claude 输入框:2 级阴影默认,4 级 focus,使用本项目 1~4 级 shadow token */
18
+ 'shadow-[0_0.25rem_1.25rem_var(--color-shadow-secondary)]',
19
+ 'hover:shadow-[0_0.25rem_1.25rem_var(--color-shadow-secondary)]',
20
+ 'focus-within:shadow-[0_0.25rem_1.25rem_var(--color-shadow-quaternary)]',
21
+ 'hover:focus-within:shadow-[0_0.25rem_1.25rem_var(--color-shadow-quaternary)]',
22
+ ].join(' '),
23
+ {
24
+ variants: { disabled: { true: 'opacity-50 cursor-not-allowed', false: '' } },
25
+ defaultVariants: { disabled: false },
26
+ }
27
+ )
28
+ // Box 使用 rounded-xl (= --radius-xl)
29
+
30
+ const attachmentRowStyles = 'flex flex-wrap items-center gap-2 px-3 pt-3 pb-2'
31
+ const contentStyles = 'flex flex-1 items-center min-h-6 p-3'
32
+ const footerStyles = 'flex shrink-0 items-center justify-between w-full self-stretch gap-2 p-3'
33
+
34
+ // ----- 框外 -----
35
+ // Above 高度 = 主题下 md 按钮高度(h-9),文字 text-sm;宽度 = Box 宽度 - 2*圆角,居中;无水平 padding
36
+ export interface ChatInputAboveProps {
37
+ className?: string
38
+ style?: React.CSSProperties
39
+ children?: ReactNode
40
+ }
41
+
42
+ export function ChatInputAbove({ className, style, children }: ChatInputAboveProps) {
43
+ return (
44
+ <div
45
+ className={cn(
46
+ 'flex min-h-9 h-9 w-[calc(100%-2*var(--radius-xl))] mx-auto items-center overflow-hidden rounded-t-xl text-sm leading-sm',
47
+ className
48
+ )}
49
+ style={style}
50
+ >
51
+ {children}
52
+ </div>
53
+ )
54
+ }
55
+ ChatInputAbove.displayName = 'ChatInputAbove'
56
+
57
+ // ----- 框容器 -----
58
+ export interface ChatInputBoxProps {
59
+ className?: string
60
+ children?: ReactNode
61
+ }
62
+
63
+ export function ChatInputBox({ className, children }: ChatInputBoxProps) {
64
+ const ctx = useChatInputContext()
65
+ return (
66
+ <div
67
+ className={chatInputContainerVariants({
68
+ disabled: ctx.disabled,
69
+ className: cn('relative', className),
70
+ })}
71
+ >
72
+ {children}
73
+ </div>
74
+ )
75
+ }
76
+ ChatInputBox.displayName = 'ChatInputBox'
77
+
78
+ // ----- 框内·第一层 附件区 -----
79
+ export interface ChatInputAttachmentsProps {
80
+ className?: string
81
+ children?: ReactNode
82
+ }
83
+
84
+ export function ChatInputAttachments({ className, children }: ChatInputAttachmentsProps) {
85
+ if (!children) return null
86
+ return <div className={cn(attachmentRowStyles, className)}>{children}</div>
87
+ }
88
+ ChatInputAttachments.displayName = 'ChatInputAttachments'
89
+
90
+ // ----- 框内·第二层 输入区 -----
91
+ export interface ChatInputInputProps extends Omit<React.ComponentProps<typeof ChatInputTextarea>, 'value' | 'onChange' | 'onKeyDown'> {
92
+ placeholder?: string
93
+ maxRows?: number
94
+ }
95
+
96
+ export const ChatInputInput = forwardRef<HTMLTextAreaElement, ChatInputInputProps>(
97
+ ({ placeholder = 'What can I help you with today?', maxRows = 6, ...props }, ref) => {
98
+ const ctx = useChatInputContext()
99
+ const setRef = useCallback(
100
+ (node: HTMLTextAreaElement | null) => {
101
+ ctx.setTextareaRef(node)
102
+ if (typeof ref === 'function') ref(node)
103
+ else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
104
+ },
105
+ [ctx, ref]
106
+ )
107
+ return (
108
+ <div className={contentStyles}>
109
+ <ChatInputTextarea
110
+ ref={setRef}
111
+ value={ctx.value}
112
+ onChange={ctx.onChange}
113
+ onKeyDown={ctx.handleKeyDown}
114
+ placeholder={placeholder}
115
+ disabled={ctx.disabled}
116
+ maxRows={maxRows}
117
+ {...props}
118
+ />
119
+ </div>
120
+ )
121
+ }
122
+ )
123
+ ChatInputInput.displayName = 'ChatInputInput'
124
+
125
+ // ----- 框内·第三层 操作区 -----
126
+ export interface ChatInputActionsProps {
127
+ className?: string
128
+ children?: ReactNode
129
+ }
130
+
131
+ export function ChatInputActions({ className, children }: ChatInputActionsProps) {
132
+ return <div className={cn(footerStyles, className)}>{children}</div>
133
+ }
134
+ ChatInputActions.displayName = 'ChatInputActions'
135
+
136
+ export interface ChatInputActionsLeftProps {
137
+ className?: string
138
+ children?: ReactNode
139
+ }
140
+
141
+ export function ChatInputActionsLeft({ className, children }: ChatInputActionsLeftProps) {
142
+ return <div className={cn('flex items-center gap-2', className)}>{children}</div>
143
+ }
144
+ ChatInputActionsLeft.displayName = 'ChatInputActionsLeft'
145
+
146
+ export interface ChatInputActionsRightProps {
147
+ className?: string
148
+ children?: ReactNode
149
+ }
150
+
151
+ export function ChatInputActionsRight({ className, children }: ChatInputActionsRightProps) {
152
+ return <div className={cn('flex items-center gap-2', className)}>{children}</div>
153
+ }
154
+ ChatInputActionsRight.displayName = 'ChatInputActionsRight'
155
+
156
+ // ----- 固定·框内右上角放大按钮 -----
157
+ export interface ChatInputExpandButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
158
+ children?: ReactNode
159
+ }
160
+
161
+ export function ChatInputExpandButton({ className, style, ...props }: ChatInputExpandButtonProps) {
162
+ return (
163
+ <button
164
+ type="button"
165
+ className={cn(
166
+ 'absolute top-2 right-2 z-10 flex h-8 w-8 items-center justify-center rounded-lg text-text-secondary hover:bg-fill-secondary hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50',
167
+ className
168
+ )}
169
+ style={{ ...style }}
170
+ {...props}
171
+ />
172
+ )
173
+ }
174
+ ChatInputExpandButton.displayName = 'ChatInputExpandButton'
175
+
176
+ // ----- 操作区子组件(从 Context 消费) -----
177
+ export function ChatInputFolderButton() {
178
+ const ctx = useChatInputContext()
179
+ if (!ctx.showFolderButton) return null
180
+ return (
181
+ <ChatInputFolderSelector
182
+ disabled={ctx.disabled}
183
+ folderButtonLabel={ctx.folderButtonLabel}
184
+ folderPermission={ctx.folderPermission}
185
+ onFolderButtonClick={ctx.onFolderButtonClick}
186
+ onFolderClear={ctx.handleFolderClear}
187
+ onFolderSelect={ctx.handleFolderSelect}
188
+ hasFolderSelected={ctx.hasFolderSelected}
189
+ dataStyle={ctx.dataStyle}
190
+ dataTheme={ctx.dataTheme}
191
+ folderIcon={ctx.folderIcon}
192
+ folderOpenIcon={ctx.folderOpenIcon}
193
+ historyIcon={ctx.historyIcon}
194
+ subMenuArrowIcon={ctx.subMenuArrowIcon}
195
+ folderFillIcon={ctx.folderFillIcon}
196
+ closeIcon={ctx.closeIcon}
197
+ />
198
+ )
199
+ }
200
+ ChatInputFolderButton.displayName = 'ChatInputFolderButton'
201
+
202
+ export function ChatInputMoreButton() {
203
+ const ctx = useChatInputContext()
204
+ if (!ctx.showMoreButton) return null
205
+ return (
206
+ <IconButton
207
+ variant="ghost"
208
+ icon={ctx.addLargeIcon ?? <MoreLine className="h-5 w-5" />}
209
+ onClick={ctx.onMoreButtonClick}
210
+ disabled={ctx.disabled}
211
+ />
212
+ )
213
+ }
214
+ ChatInputMoreButton.displayName = 'ChatInputMoreButton'
215
+
216
+ // 使用 chat 内共用的 SendButton,仅从 Context 注入 disabled/status/onClick|form
217
+ export function ChatInputSendButton() {
218
+ const ctx = useChatInputContext()
219
+ return (
220
+ <SendButton
221
+ disabled={!ctx.canSend || ctx.disabled}
222
+ status={ctx.sendButtonStatus}
223
+ {...(ctx.form ? { type: 'submit' as const, form: ctx.form } : { onClick: ctx.handleSend })}
224
+ />
225
+ )
226
+ }
227
+ ChatInputSendButton.displayName = 'ChatInputSendButton'
228
+
229
+ // 左侧操作区默认内容:有 footerLeftConfig 时渲染模式/模型切换,否则渲染 FolderButton + MoreButton
230
+ export function ChatInputFooterLeft() {
231
+ const ctx = useChatInputContext()
232
+ if (ctx.footerLeftConfig) {
233
+ return (
234
+ <ChatInputModelSwitcher
235
+ config={ctx.footerLeftConfig}
236
+ disabled={ctx.disabled}
237
+ arrowDownIcon={ctx.arrowDownIcon}
238
+ dataStyle={ctx.dataStyle}
239
+ dataTheme={ctx.dataTheme}
240
+ />
241
+ )
242
+ }
243
+ return (
244
+ <>
245
+ <ChatInputFolderButton />
246
+ <ChatInputMoreButton />
247
+ </>
248
+ )
249
+ }
250
+ ChatInputFooterLeft.displayName = 'ChatInputFooterLeft'
251
+
252
+ // ----- 默认布局(ChatInput 便捷导出使用) -----
253
+ export interface ChatInputDefaultLayoutProps {
254
+ className?: string
255
+ hasAbove?: boolean
256
+ hasAttachments?: boolean
257
+ hasFooter?: boolean
258
+ aboveOverlap?: boolean
259
+ }
260
+
261
+ export const ChatInputDefaultLayout = forwardRef<HTMLTextAreaElement, ChatInputDefaultLayoutProps>(
262
+ function ChatInputDefaultLayout(
263
+ { className, hasAbove = false, hasAttachments = false, hasFooter = true, aboveOverlap = true },
264
+ ref
265
+ ) {
266
+ const ctx = useChatInputContext()
267
+
268
+ const renderAttachments = () => {
269
+ if (!hasAttachments || ctx.attachments == null || ctx.attachments === false) return null
270
+ const att = ctx.attachments
271
+ if (typeof att === 'object' && !React.isValidElement(att) && ('files' in att || 'images' in att)) {
272
+ return (
273
+ <>
274
+ {(att as { files?: ReactNode }).files && (
275
+ <div className={attachmentRowStyles}>{(att as { files?: ReactNode }).files}</div>
276
+ )}
277
+ {(att as { images?: ReactNode }).images && (
278
+ <div className={attachmentRowStyles}>{(att as { images?: ReactNode }).images}</div>
279
+ )}
280
+ </>
281
+ )
282
+ }
283
+ return <div className={attachmentRowStyles}>{att as ReactNode}</div>
284
+ }
285
+
286
+ const renderFooterLeft = () => <ChatInputFooterLeft />
287
+
288
+ const boxContent = (
289
+ <ChatInputBox className={cn(!hasFooter && !hasAttachments && 'min-h-14')}>
290
+ {renderAttachments()}
291
+ <ChatInputInput ref={ref} />
292
+ {/* 始终保留操作区右侧 SendButton:hasFooter 时显示完整 footer,否则仅显示右侧发送按钮(Minimal 模式) */}
293
+ <ChatInputActions>
294
+ {hasFooter ? (
295
+ <>
296
+ <ChatInputActionsLeft>{renderFooterLeft()}</ChatInputActionsLeft>
297
+ <div className="flex-1 h-5" />
298
+ <ChatInputActionsRight>
299
+ <ChatInputSendButton />
300
+ </ChatInputActionsRight>
301
+ </>
302
+ ) : (
303
+ <>
304
+ <div className="flex-1 h-5" />
305
+ <ChatInputActionsRight>
306
+ <ChatInputSendButton />
307
+ </ChatInputActionsRight>
308
+ </>
309
+ )}
310
+ </ChatInputActions>
311
+ </ChatInputBox>
312
+ )
313
+
314
+ if (hasAbove && ctx.showGenerationStatus && ctx.generationStatus) {
315
+ return (
316
+ <div className={cn('flex flex-col w-full', className)}>
317
+ <ChatInputAbove>
318
+ <GenerationStatusBar
319
+ status={typeof ctx.generationStatus?.content === 'string' ? ctx.generationStatus.content : undefined}
320
+ icon={ctx.generationStatus?.icon}
321
+ time={ctx.generationStatus?.time}
322
+ >
323
+ {ctx.generationStatus?.content}
324
+ </GenerationStatusBar>
325
+ </ChatInputAbove>
326
+ {boxContent}
327
+ </div>
328
+ )
329
+ }
330
+
331
+ return <div className={className}>{boxContent}</div>
332
+ }
333
+ )
334
+ ChatInputDefaultLayout.displayName = 'ChatInputDefaultLayout'