sparkdesign 0.3.1 → 0.3.3

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 (104) hide show
  1. package/README.md +7 -5
  2. package/cli/dist/index.js +0 -0
  3. package/cli/dist/utils/tokens.js +103 -17
  4. package/cli/registry/basic/button.test.tsx +333 -0
  5. package/cli/registry/chat/{question-part.tsx → ask-user-part.tsx} +4 -4
  6. package/cli/registry/chat/{browser-use-part.tsx → browser-action-part.tsx} +6 -6
  7. package/cli/registry/chat/{suggestion-part.tsx → hint-banner.tsx} +4 -4
  8. package/cli/registry/chat/markdown.test.tsx +387 -0
  9. package/cli/registry/chat/{reasoning-step.tsx → reasoning-step/compound.tsx} +163 -185
  10. package/cli/registry/chat/reasoning-step/context.tsx +114 -0
  11. package/cli/registry/chat/reasoning-step/index.tsx +45 -0
  12. package/cli/registry/chat/reasoning-step/types.ts +109 -0
  13. package/cli/registry/chat/response/compound.tsx +210 -0
  14. package/cli/registry/chat/{response.tsx → response/context.tsx} +65 -136
  15. package/cli/registry/chat/response/index.tsx +87 -0
  16. package/cli/registry/chat/response/types.ts +123 -0
  17. package/cli/registry/chat/thinking-indicator.test.tsx +244 -0
  18. package/cli/registry/chat/tool-invocation-card.test.tsx +346 -0
  19. package/cli/registry/chat/{request.tsx → user-message.tsx} +3 -3
  20. package/cli/registry/chat/user-question/compound.tsx +324 -0
  21. package/cli/registry/chat/user-question/context.tsx +456 -0
  22. package/cli/registry/chat/user-question/index.tsx +71 -316
  23. package/cli/registry/chat/user-question/useUserQuestionKeyboard.ts +5 -6
  24. package/cli/registry/tokens/index.css +31 -0
  25. package/cli/registry/tokens/scale/computed.css +103 -0
  26. package/cli/registry/tokens/scale/config.css +110 -0
  27. package/cli/registry/tokens/scale/index.css +30 -0
  28. package/cli/registry/tokens/scale/presets/compact.css +30 -0
  29. package/cli/registry/tokens/scale/presets/dense.css +64 -0
  30. package/cli/registry/tokens/scale/presets/sharp.css +40 -0
  31. package/cli/registry/tokens/scale/presets/soft.css +16 -0
  32. package/cli/registry/tokens/scale.css +12 -298
  33. package/cli/registry/tokens/scrollbar-utility.css +35 -0
  34. package/cli/registry/tokens/themes/dark-parchment.css +132 -0
  35. package/cli/registry/tokens/themes/dark-qoder.css +132 -0
  36. package/cli/registry/tokens/themes/light-parchment.css +123 -0
  37. package/cli/registry/tokens/themes/light-qoder.css +131 -0
  38. package/dist/qoder-design.css +1 -1
  39. package/dist/registry/chat/ask-user-part.d.ts +24 -0
  40. package/dist/registry/chat/browser-action-part.d.ts +28 -0
  41. package/dist/registry/chat/{suggestion-part.d.ts → hint-banner.d.ts} +4 -4
  42. package/dist/registry/chat/reasoning-step/compound.d.ts +17 -0
  43. package/dist/registry/chat/reasoning-step/context.d.ts +10 -0
  44. package/dist/registry/chat/reasoning-step/index.d.ts +14 -0
  45. package/dist/registry/chat/reasoning-step/types.d.ts +95 -0
  46. package/dist/registry/chat/response/compound.d.ts +25 -0
  47. package/dist/registry/chat/response/context.d.ts +9 -0
  48. package/dist/registry/chat/response/index.d.ts +15 -0
  49. package/dist/registry/chat/response/types.d.ts +99 -0
  50. package/dist/registry/chat/user-message.d.ts +6 -0
  51. package/dist/registry/chat/user-question/compound.d.ts +37 -0
  52. package/dist/registry/chat/user-question/context.d.ts +55 -0
  53. package/dist/registry/chat/user-question/index.d.ts +13 -5
  54. package/dist/registry/chat/user-question/useUserQuestionKeyboard.d.ts +2 -3
  55. package/dist/scale.css +9 -303
  56. package/dist/spark-design.cjs.js +62 -62
  57. package/dist/spark-design.es.js +3992 -3826
  58. package/dist/src/components/chat/AskUserPart/index.d.ts +6 -0
  59. package/dist/src/components/chat/BrowserActionPart/index.d.ts +7 -0
  60. package/dist/src/components/chat/HintBanner/index.d.ts +6 -0
  61. package/dist/src/components/chat/ReasoningStep/index.d.ts +11 -5
  62. package/dist/src/components/chat/Response/index.d.ts +16 -6
  63. package/dist/src/components/chat/UserMessage/index.d.ts +7 -0
  64. package/dist/src/components/chat/UserQuestion/index.d.ts +18 -4
  65. package/dist/src/components/index.d.ts +63 -63
  66. package/dist/theme.css +13 -800
  67. package/package.json +27 -3
  68. package/dist/registry/chat/browser-use-part.d.ts +0 -28
  69. package/dist/registry/chat/question-part.d.ts +0 -24
  70. package/dist/registry/chat/reasoning-step.d.ts +0 -35
  71. package/dist/registry/chat/request.d.ts +0 -6
  72. package/dist/registry/chat/response.d.ts +0 -28
  73. package/dist/src/components/chat/BrowserUsePart/index.d.ts +0 -7
  74. package/dist/src/components/chat/QuestionPart/index.d.ts +0 -6
  75. package/dist/src/components/chat/Request/index.d.ts +0 -7
  76. package/dist/src/components/chat/SuggestionPart/index.d.ts +0 -6
  77. /package/dist/src/components/{foundation → basic}/AlertDialog/index.d.ts +0 -0
  78. /package/dist/src/components/{foundation → basic}/Avatar/index.d.ts +0 -0
  79. /package/dist/src/components/{foundation → basic}/Button/index.d.ts +0 -0
  80. /package/dist/src/components/{foundation → basic}/Collapse/index.d.ts +0 -0
  81. /package/dist/src/components/{foundation → basic}/Collapsible/index.d.ts +0 -0
  82. /package/dist/src/components/{foundation → basic}/CollapsibleSection/index.d.ts +0 -0
  83. /package/dist/src/components/{foundation → basic}/DropdownMenu/index.d.ts +0 -0
  84. /package/dist/src/components/{foundation → basic}/EllipsisText/index.d.ts +0 -0
  85. /package/dist/src/components/{foundation → basic}/IconButton/index.d.ts +0 -0
  86. /package/dist/src/components/{foundation → basic}/Kbd/index.d.ts +0 -0
  87. /package/dist/src/components/{foundation → basic}/OptionList/index.d.ts +0 -0
  88. /package/dist/src/components/{foundation → basic}/Pagination/index.d.ts +0 -0
  89. /package/dist/src/components/{foundation → basic}/Progress/index.d.ts +0 -0
  90. /package/dist/src/components/{foundation → basic}/RadioGroup/index.d.ts +0 -0
  91. /package/dist/src/components/{foundation → basic}/Resizable/index.d.ts +0 -0
  92. /package/dist/src/components/{foundation → basic}/Scrollbar/index.d.ts +0 -0
  93. /package/dist/src/components/{foundation → basic}/Select/index.d.ts +0 -0
  94. /package/dist/src/components/{foundation → basic}/Skeleton/index.d.ts +0 -0
  95. /package/dist/src/components/{foundation → basic}/Slider/index.d.ts +0 -0
  96. /package/dist/src/components/{foundation → basic}/Spinner/index.d.ts +0 -0
  97. /package/dist/src/components/{foundation → basic}/Switch/index.d.ts +0 -0
  98. /package/dist/src/components/{foundation → basic}/Table/index.d.ts +0 -0
  99. /package/dist/src/components/{foundation → basic}/Tabs/index.d.ts +0 -0
  100. /package/dist/src/components/{foundation → basic}/Tag/index.d.ts +0 -0
  101. /package/dist/src/components/{foundation → basic}/Toast/index.d.ts +0 -0
  102. /package/dist/src/components/{foundation → basic}/Toggle/index.d.ts +0 -0
  103. /package/dist/src/components/{foundation → basic}/Tooltip/index.d.ts +0 -0
  104. /package/dist/src/components/{foundation → basic}/Typography/index.d.ts +0 -0
@@ -1,37 +1,18 @@
1
- import { useState, useMemo, useCallback, memo, Fragment } from 'react'
1
+ import { memo, useMemo, useCallback, Fragment } from 'react'
2
2
  import type { ReactNode } from 'react'
3
3
  import { motion } from 'framer-motion'
4
- import { cn } from '@/lib/utils'
5
- import { EyeLine, ArrowRightLine } from '../basic/icons-inline'
6
-
7
- export type ReasoningStepDetail =
8
- | { type: 'action'; action: string; desc?: string; details?: ReasoningStepDetail[] }
9
- | {
10
- type: 'file'
11
- filename: string
12
- lines?: string
13
- path?: string
14
- fileType?: string
15
- details?: ReasoningStepDetail[]
16
- }
4
+ import { cn } from '../../lib/utils'
5
+ import { useReasoningStepContext } from './context'
6
+ import type {
7
+ ReasoningStepHeaderProps,
8
+ ReasoningStepDetailsProps,
9
+ ReasoningStepDetailRowProps,
10
+ ReasoningStepDetail,
11
+ } from './types'
17
12
 
18
- export interface ReasoningStepProps {
19
- text: ReactNode
20
- description?: string
21
- status?: 'completed' | 'in-progress'
22
- icon?: ReactNode
23
- showIcon?: boolean
24
- details?: ReasoningStepDetail[]
25
- children?: ReactNode
26
- duration?: number
27
- spread?: number
28
- disableExpandAnimation?: boolean
29
- defaultExpanded?: boolean
30
- expanded?: boolean
31
- onExpandedChange?: (expanded: boolean) => void
32
- eyeIcon?: ReactNode
33
- arrowRightIcon?: ReactNode
34
- }
13
+ // ===== 样式常量 =====
14
+
15
+ const iconSizeStyle = { width: 'var(--font-size-xs)', height: 'var(--font-size-xs)' }
35
16
 
36
17
  const FILE_ICON_COLOR_MAP: Record<string, string> = {
37
18
  ts: 'text-info', tsx: 'text-info', js: 'text-info', jsx: 'text-info',
@@ -44,10 +25,7 @@ function getFileIconColorClass(fileType?: string, filename?: string): string {
44
25
  return FILE_ICON_COLOR_MAP[ext] ?? 'text-text-tertiary'
45
26
  }
46
27
 
47
- /* 与主文 text-xs 共用 token,图标与文字同尺度、随 data-style 一致缩放 */
48
- const iconSizeStyle = { width: 'var(--font-size-xs)', height: 'var(--font-size-xs)' }
49
- const eyeClass = 'w-full h-full text-text-tertiary transition-transform duration-200 ease-in-out'
50
- const arrowClass = 'w-full h-full transition-transform duration-200 ease-in-out'
28
+ // ===== 辅助组件 =====
51
29
 
52
30
  const FileIconPlaceholder = memo(function FileIconPlaceholder({ className }: { className?: string }) {
53
31
  return (
@@ -87,7 +65,86 @@ const InlineRow = memo(function InlineRow({
87
65
  )
88
66
  })
89
67
 
90
- interface DetailRowProps {
68
+ // ===== ReasoningStep.Header =====
69
+
70
+ export function ReasoningStepHeader({ className }: ReasoningStepHeaderProps) {
71
+ const ctx = useReasoningStepContext()
72
+
73
+ const dynamicSpread = useMemo(() => {
74
+ if (typeof ctx.text === 'string') return ctx.text.length * ctx.spread
75
+ return 10 * ctx.spread
76
+ }, [ctx.text, ctx.spread])
77
+
78
+ const getIcon = () => (ctx.icon ? ctx.icon : ctx.eyeIcon)
79
+
80
+ const renderPrimaryText = () => {
81
+ if (typeof ctx.text !== 'string') return ctx.text
82
+ if (ctx.status === 'in-progress') {
83
+ return (
84
+ <motion.span
85
+ className="text-xs leading-xs font-normal relative inline-block bg-clip-text text-transparent bg-[length:250%_100%,auto] bg-no-repeat truncate"
86
+ style={{
87
+ backgroundImage: `linear-gradient(90deg, transparent calc(50% - ${dynamicSpread}px), var(--color-text), transparent calc(50% + ${dynamicSpread}px)), linear-gradient(var(--color-text-tertiary), var(--color-text-tertiary))`,
88
+ }}
89
+ initial={{ backgroundPosition: '100% center' }}
90
+ animate={{ backgroundPosition: '0% center' }}
91
+ transition={{ repeat: Infinity, duration: ctx.duration, ease: 'linear' }}
92
+ >
93
+ {ctx.text}
94
+ </motion.span>
95
+ )
96
+ }
97
+ return (
98
+ <span className="text-xs leading-xs font-normal text-text-tertiary truncate">
99
+ {ctx.text}
100
+ </span>
101
+ )
102
+ }
103
+
104
+ const renderDescription = () => {
105
+ if (!ctx.description) return null
106
+ return (
107
+ <span className="text-xs leading-xs font-normal text-text-quaternary truncate">
108
+ {ctx.description}
109
+ </span>
110
+ )
111
+ }
112
+
113
+ return (
114
+ <div
115
+ className={cn('flex flex-row items-center gap-1.5', ctx.hasExpandableContent && 'cursor-pointer', className)}
116
+ onClick={ctx.toggleExpanded}
117
+ >
118
+ {ctx.effectiveShowIcon && (
119
+ <div className="flex items-center justify-center shrink-0" style={iconSizeStyle}>
120
+ {getIcon()}
121
+ </div>
122
+ )}
123
+ <div className="flex flex-row items-center gap-1 flex-1 min-w-0">
124
+ {renderPrimaryText()}
125
+ {ctx.description ? renderDescription() : null}
126
+ {ctx.hasExpandableContent && (
127
+ <div
128
+ className={cn(
129
+ 'flex items-center justify-center shrink-0 text-text-tertiary transition-opacity duration-200 ease-in-out',
130
+ ctx.effectiveExpanded ? 'opacity-100' : ctx.isHovered ? 'opacity-100' : 'opacity-0',
131
+ ctx.effectiveExpanded ? 'rotate-90' : 'rotate-0'
132
+ )}
133
+ style={iconSizeStyle}
134
+ >
135
+ {ctx.arrowRightIcon}
136
+ </div>
137
+ )}
138
+ </div>
139
+ </div>
140
+ )
141
+ }
142
+
143
+ ReasoningStepHeader.displayName = 'ReasoningStepHeader'
144
+
145
+ // ===== ReasoningStep.DetailRow(内部递归组件)=====
146
+
147
+ interface DetailRowInternalProps {
91
148
  detail: ReasoningStepDetail
92
149
  depth: number
93
150
  pathKey: string
@@ -98,16 +155,15 @@ interface DetailRowProps {
98
155
  arrowRightIcon: ReactNode
99
156
  }
100
157
 
101
- const DetailRow = memo(function DetailRow({
158
+ const DetailRowInternal = memo(function DetailRowInternal({
102
159
  detail,
103
160
  depth,
104
161
  pathKey,
105
162
  detailIndent,
106
- disableExpandAnimation,
107
163
  expandedNestedKeys,
108
164
  onToggleNested,
109
165
  arrowRightIcon,
110
- }: DetailRowProps) {
166
+ }: DetailRowInternalProps) {
111
167
  const hasNested = detail.details != null && detail.details.length > 0
112
168
  const isExpandedNested = expandedNestedKeys.has(pathKey)
113
169
  const showIconOnRow = !hasNested
@@ -175,13 +231,13 @@ const DetailRow = memo(function DetailRow({
175
231
  <div className="overflow-hidden">
176
232
  <div className={cn(nestedWrapperClass)}>
177
233
  {detail.details?.map((d, i) => (
178
- <DetailRow
234
+ <DetailRowInternal
179
235
  key={`${pathKey}-${i}`}
180
236
  detail={d}
181
237
  depth={depth + 1}
182
238
  pathKey={`${pathKey}-${i}`}
183
239
  detailIndent={detailIndent}
184
- disableExpandAnimation={disableExpandAnimation}
240
+ disableExpandAnimation={false}
185
241
  expandedNestedKeys={expandedNestedKeys}
186
242
  onToggleNested={onToggleNested}
187
243
  arrowRightIcon={arrowRightIcon}
@@ -194,165 +250,87 @@ const DetailRow = memo(function DetailRow({
194
250
  )
195
251
  })
196
252
 
197
- export function ReasoningStep({
198
- text,
199
- description,
200
- status = 'completed',
201
- icon,
202
- showIcon = true,
203
- details,
204
- children,
205
- duration = 2,
206
- spread = 2,
207
- disableExpandAnimation = false,
208
- defaultExpanded,
209
- expanded: expandedProp,
210
- onExpandedChange,
211
- eyeIcon,
212
- arrowRightIcon,
213
- }: ReasoningStepProps) {
214
- const isControlled = expandedProp !== undefined
215
- const [isExpandedInternal, setIsExpandedInternal] = useState(defaultExpanded ?? false)
216
- const isExpanded = isControlled ? expandedProp : isExpandedInternal
217
- const [isHovered, setIsHovered] = useState(false)
218
-
219
- const effectiveExpanded = disableExpandAnimation && defaultExpanded !== undefined
220
- ? defaultExpanded
221
- : isExpanded
253
+ // ===== ReasoningStep.DetailRow(公开 API)=====
222
254
 
223
- const effectiveShowIcon = showIcon && typeof text === 'string'
255
+ export function ReasoningStepDetailRow({
256
+ detail,
257
+ depth = 0,
258
+ pathKey = '0',
259
+ }: ReasoningStepDetailRowProps) {
260
+ const ctx = useReasoningStepContext()
261
+ return (
262
+ <DetailRowInternal
263
+ detail={detail}
264
+ depth={depth}
265
+ pathKey={pathKey}
266
+ detailIndent={ctx.detailIndent}
267
+ disableExpandAnimation={ctx.disableExpandAnimation}
268
+ expandedNestedKeys={ctx.expandedNestedKeys}
269
+ onToggleNested={ctx.toggleNested}
270
+ arrowRightIcon={ctx.arrowRightIcon}
271
+ />
272
+ )
273
+ }
224
274
 
225
- const detailIndent = effectiveShowIcon ? 'var(--spacing-1.5)' : 'var(--spacing-2.5)'
275
+ ReasoningStepDetailRow.displayName = 'ReasoningStepDetailRow'
226
276
 
227
- const dynamicSpread = useMemo(() => {
228
- if (typeof text === 'string') return text.length * spread
229
- return 10 * spread
230
- }, [text, spread])
277
+ // ===== ReasoningStep.Details =====
231
278
 
232
- const defaultEye = eyeIcon ?? <EyeLine className={eyeClass} />
233
- const defaultArrow = arrowRightIcon ?? <ArrowRightLine className={arrowClass} />
279
+ export function ReasoningStepDetails({ className, children }: ReasoningStepDetailsProps) {
280
+ const ctx = useReasoningStepContext()
281
+ const hasDetails = ctx.details != null && ctx.details.length > 0
234
282
 
235
- const getIcon = () => (icon ? icon : defaultEye)
283
+ return (
284
+ <div
285
+ className={cn(
286
+ 'grid transition-[grid-template-rows,opacity] duration-200 ease-out',
287
+ ctx.effectiveExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
288
+ className
289
+ )}
290
+ >
291
+ <div className="overflow-hidden">
292
+ {hasDetails ? (
293
+ <div className="flex flex-col">
294
+ {ctx.details?.map((d, i) => (
295
+ <DetailRowInternal
296
+ key={String(i)}
297
+ detail={d}
298
+ depth={0}
299
+ pathKey={String(i)}
300
+ detailIndent={ctx.detailIndent}
301
+ disableExpandAnimation={ctx.disableExpandAnimation}
302
+ expandedNestedKeys={ctx.expandedNestedKeys}
303
+ onToggleNested={ctx.toggleNested}
304
+ arrowRightIcon={ctx.arrowRightIcon}
305
+ />
306
+ ))}
307
+ </div>
308
+ ) : (
309
+ children ?? ctx.detailsChildren
310
+ )}
311
+ </div>
312
+ </div>
313
+ )
314
+ }
236
315
 
237
- const renderPrimaryText = () => {
238
- if (typeof text !== 'string') return text
239
- if (status === 'in-progress') {
240
- return (
241
- <motion.span
242
- className="text-xs leading-xs font-normal relative inline-block bg-clip-text text-transparent bg-[length:250%_100%,auto] bg-no-repeat truncate"
243
- style={{
244
- backgroundImage: `linear-gradient(90deg, transparent calc(50% - ${dynamicSpread}px), var(--color-text), transparent calc(50% + ${dynamicSpread}px)), linear-gradient(var(--color-text-tertiary), var(--color-text-tertiary))`,
245
- }}
246
- initial={{ backgroundPosition: '100% center' }}
247
- animate={{ backgroundPosition: '0% center' }}
248
- transition={{ repeat: Infinity, duration, ease: 'linear' }}
249
- >
250
- {text}
251
- </motion.span>
252
- )
253
- }
254
- return (
255
- <span className="text-xs leading-xs font-normal text-text-tertiary truncate">
256
- {text}
257
- </span>
258
- )
259
- }
316
+ ReasoningStepDetails.displayName = 'ReasoningStepDetails'
260
317
 
261
- const renderDescription = () => {
262
- if (!description) return null
263
- return (
264
- <span className="text-xs leading-xs font-normal text-text-quaternary truncate">
265
- {description}
266
- </span>
267
- )
268
- }
318
+ // ===== ReasoningStep.DefaultLayout =====
269
319
 
270
- const [expandedNestedKeys, setExpandedNestedKeys] = useState<Set<string>>(new Set())
271
- const toggleNested = useCallback((pathKey: string) => {
272
- setExpandedNestedKeys((prev) => {
273
- const next = new Set(prev)
274
- if (next.has(pathKey)) next.delete(pathKey)
275
- else next.add(pathKey)
276
- return next
277
- })
278
- }, [])
279
-
280
- const hasDetails = details != null && details.length > 0
281
- const hasExpandableContent = hasDetails || children != null
282
-
283
- const handleToggle = () => {
284
- if (hasExpandableContent) {
285
- if (isControlled) onExpandedChange?.(!expandedProp)
286
- else setIsExpandedInternal((e) => !e)
287
- }
288
- }
320
+ export function ReasoningStepDefaultLayout() {
321
+ const ctx = useReasoningStepContext()
289
322
 
290
323
  return (
291
324
  <div
292
325
  data-reasoning-step
293
326
  className="flex flex-col gap-1 py-0.5"
294
- onMouseEnter={() => setIsHovered(true)}
295
- onMouseLeave={() => setIsHovered(false)}
327
+ onMouseEnter={() => ctx.setIsHovered(true)}
328
+ onMouseLeave={() => ctx.setIsHovered(false)}
296
329
  >
297
- <div
298
- className={cn('flex flex-row items-center gap-1.5', hasExpandableContent && 'cursor-pointer')}
299
- onClick={handleToggle}
300
- >
301
- {effectiveShowIcon && (
302
- <div className="flex items-center justify-center shrink-0" style={iconSizeStyle}>
303
- {getIcon()}
304
- </div>
305
- )}
306
- <div className="flex flex-row items-center gap-1 flex-1 min-w-0">
307
- {renderPrimaryText()}
308
- {description ? renderDescription() : null}
309
- {hasExpandableContent && (
310
- <div
311
- className={cn(
312
- 'flex items-center justify-center shrink-0 text-text-tertiary transition-opacity duration-200 ease-in-out',
313
- effectiveExpanded ? 'opacity-100' : isHovered ? 'opacity-100' : 'opacity-0',
314
- effectiveExpanded ? 'rotate-90' : 'rotate-0'
315
- )}
316
- style={iconSizeStyle}
317
- >
318
- {defaultArrow}
319
- </div>
320
- )}
321
- </div>
322
- </div>
323
-
324
- {hasExpandableContent && (
325
- <div
326
- className={cn(
327
- 'grid transition-[grid-template-rows,opacity] duration-200 ease-out',
328
- effectiveExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
329
- )}
330
- >
331
- <div className="overflow-hidden">
332
- {hasDetails ? (
333
- <div className="flex flex-col">
334
- {details?.map((d, i) => (
335
- <DetailRow
336
- key={String(i)}
337
- detail={d}
338
- depth={0}
339
- pathKey={String(i)}
340
- detailIndent={detailIndent}
341
- disableExpandAnimation={disableExpandAnimation}
342
- expandedNestedKeys={expandedNestedKeys}
343
- onToggleNested={toggleNested}
344
- arrowRightIcon={defaultArrow}
345
- />
346
- ))}
347
- </div>
348
- ) : (
349
- children
350
- )}
351
- </div>
352
- </div>
353
- )}
330
+ <ReasoningStepHeader />
331
+ {ctx.hasExpandableContent && <ReasoningStepDetails />}
354
332
  </div>
355
333
  )
356
334
  }
357
335
 
358
- ReasoningStep.displayName = 'ReasoningStep'
336
+ ReasoningStepDefaultLayout.displayName = 'ReasoningStepDefaultLayout'
@@ -0,0 +1,114 @@
1
+ import { createContext, useContext, useState, useCallback, useMemo } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { EyeLine, ArrowRightLine } from '../../basic/icons-inline'
4
+ import type { ReasoningStepContextValue, ReasoningStepRootProps } from './types'
5
+
6
+ // ===== 样式常量 =====
7
+
8
+ const eyeClass = 'w-full h-full text-text-tertiary transition-transform duration-200 ease-in-out'
9
+ const arrowClass = 'w-full h-full transition-transform duration-200 ease-in-out'
10
+
11
+ // ===== Context =====
12
+
13
+ const ReasoningStepContext = createContext<ReasoningStepContextValue | null>(null)
14
+ ReasoningStepContext.displayName = 'ReasoningStepContext'
15
+
16
+ export function useReasoningStepContext() {
17
+ const ctx = useContext(ReasoningStepContext)
18
+ if (!ctx) throw new Error('ReasoningStep compound components must be used within ReasoningStep.Root')
19
+ return ctx
20
+ }
21
+
22
+ // ===== Provider =====
23
+
24
+ export function ReasoningStepRootProvider({
25
+ text,
26
+ description,
27
+ status = 'completed',
28
+ icon,
29
+ showIcon = true,
30
+ details,
31
+ children,
32
+ detailsChildren,
33
+ duration = 2,
34
+ spread = 2,
35
+ disableExpandAnimation = false,
36
+ defaultExpanded,
37
+ expanded: expandedProp,
38
+ onExpandedChange,
39
+ eyeIcon,
40
+ arrowRightIcon,
41
+ }: ReasoningStepRootProps & { children?: ReactNode; detailsChildren?: ReactNode }) {
42
+ const isControlled = expandedProp !== undefined
43
+ const [isExpandedInternal, setIsExpandedInternal] = useState(defaultExpanded ?? false)
44
+ const isExpanded = isControlled ? expandedProp : isExpandedInternal
45
+ const [isHovered, setIsHovered] = useState(false)
46
+
47
+ const effectiveExpanded = disableExpandAnimation && defaultExpanded !== undefined
48
+ ? defaultExpanded
49
+ : isExpanded
50
+
51
+ const effectiveShowIcon = showIcon && typeof text === 'string'
52
+ const detailIndent = effectiveShowIcon ? 'var(--spacing-1.5)' : 'var(--spacing-2.5)'
53
+
54
+ const hasDetails = details != null && details.length > 0
55
+ const hasExpandableContent = hasDetails || detailsChildren != null
56
+
57
+ const toggleExpanded = useCallback(() => {
58
+ if (hasExpandableContent) {
59
+ if (isControlled) onExpandedChange?.(!expandedProp)
60
+ else setIsExpandedInternal((e) => !e)
61
+ }
62
+ }, [hasExpandableContent, isControlled, expandedProp, onExpandedChange])
63
+
64
+ const [expandedNestedKeys, setExpandedNestedKeys] = useState<Set<string>>(new Set())
65
+ const toggleNested = useCallback((pathKey: string) => {
66
+ setExpandedNestedKeys((prev) => {
67
+ const next = new Set(prev)
68
+ if (next.has(pathKey)) next.delete(pathKey)
69
+ else next.add(pathKey)
70
+ return next
71
+ })
72
+ }, [])
73
+
74
+ const defaultEye = useMemo(
75
+ () => eyeIcon ?? <EyeLine className={eyeClass} />,
76
+ [eyeIcon]
77
+ )
78
+ const defaultArrow = useMemo(
79
+ () => arrowRightIcon ?? <ArrowRightLine className={arrowClass} />,
80
+ [arrowRightIcon]
81
+ )
82
+
83
+ const ctxValue: ReasoningStepContextValue = {
84
+ status,
85
+ isExpanded,
86
+ isHovered,
87
+ setIsHovered,
88
+ toggleExpanded,
89
+ effectiveExpanded,
90
+ effectiveShowIcon,
91
+ hasExpandableContent,
92
+ text,
93
+ description,
94
+ details,
95
+ detailsChildren,
96
+ duration,
97
+ spread,
98
+ disableExpandAnimation,
99
+ detailIndent,
100
+ expandedNestedKeys,
101
+ toggleNested,
102
+ icon,
103
+ eyeIcon: defaultEye,
104
+ arrowRightIcon: defaultArrow,
105
+ }
106
+
107
+ return (
108
+ <ReasoningStepContext.Provider value={ctxValue}>
109
+ {children}
110
+ </ReasoningStepContext.Provider>
111
+ )
112
+ }
113
+
114
+ ReasoningStepRootProvider.displayName = 'ReasoningStepRoot'
@@ -0,0 +1,45 @@
1
+ import { ReasoningStepRootProvider, useReasoningStepContext } from './context'
2
+ import {
3
+ ReasoningStepHeader,
4
+ ReasoningStepDetails,
5
+ ReasoningStepDetailRow,
6
+ ReasoningStepDefaultLayout,
7
+ } from './compound'
8
+ import type {
9
+ ReasoningStepProps,
10
+ ReasoningStepCompound,
11
+ ReasoningStepDetail,
12
+ ReasoningStepStatus,
13
+ } from './types'
14
+
15
+ // ===== ReasoningStep 便捷组件 =====
16
+
17
+ function ReasoningStepBase(props: ReasoningStepProps) {
18
+ return (
19
+ <ReasoningStepRootProvider {...props}>
20
+ <ReasoningStepDefaultLayout />
21
+ </ReasoningStepRootProvider>
22
+ )
23
+ }
24
+
25
+ ReasoningStepBase.displayName = 'ReasoningStep'
26
+
27
+ // ===== 挂载复合组件 =====
28
+
29
+ type ReasoningStepBaseType = React.FC<ReasoningStepProps> & Partial<ReasoningStepCompound>
30
+
31
+ const ReasoningStepWithSlots = ReasoningStepBase as ReasoningStepBaseType
32
+
33
+ ReasoningStepWithSlots.Root = ReasoningStepRootProvider
34
+ ReasoningStepWithSlots.Header = ReasoningStepHeader
35
+ ReasoningStepWithSlots.Details = ReasoningStepDetails
36
+ ReasoningStepWithSlots.DetailRow = ReasoningStepDetailRow
37
+
38
+ export const ReasoningStep = ReasoningStepWithSlots as React.FC<ReasoningStepProps> &
39
+ ReasoningStepCompound
40
+
41
+ // ===== 导出类型和工具 =====
42
+
43
+ export type { ReasoningStepProps, ReasoningStepDetail, ReasoningStepStatus }
44
+
45
+ export { useReasoningStepContext }
@@ -0,0 +1,109 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ // ===== 基础类型 =====
4
+
5
+ export type ReasoningStepDetail =
6
+ | { type: 'action'; action: string; desc?: string; details?: ReasoningStepDetail[] }
7
+ | {
8
+ type: 'file'
9
+ filename: string
10
+ lines?: string
11
+ path?: string
12
+ fileType?: string
13
+ details?: ReasoningStepDetail[]
14
+ }
15
+
16
+ export type ReasoningStepStatus = 'completed' | 'in-progress'
17
+
18
+ // ===== 组件 Props =====
19
+
20
+ export interface ReasoningStepRootProps {
21
+ /** 主文本 */
22
+ text: ReactNode
23
+ /** 描述文本 */
24
+ description?: string
25
+ /** 状态 */
26
+ status?: ReasoningStepStatus
27
+ /** 自定义图标 */
28
+ icon?: ReactNode
29
+ /** 是否显示图标 */
30
+ showIcon?: boolean
31
+ /** 详情数据 */
32
+ details?: ReasoningStepDetail[]
33
+ /** 子组件 */
34
+ children?: ReactNode
35
+ /** 动画持续时间 */
36
+ duration?: number
37
+ /** 动画扩散度 */
38
+ spread?: number
39
+ /** 禁用展开动画 */
40
+ disableExpandAnimation?: boolean
41
+ /** 默认展开 */
42
+ defaultExpanded?: boolean
43
+ /** 受控展开状态 */
44
+ expanded?: boolean
45
+ /** 展开状态变化回调 */
46
+ onExpandedChange?: (expanded: boolean) => void
47
+ /** 自定义眼睛图标 */
48
+ eyeIcon?: ReactNode
49
+ /** 自定义箭头图标 */
50
+ arrowRightIcon?: ReactNode
51
+ }
52
+
53
+ export type ReasoningStepProps = ReasoningStepRootProps
54
+
55
+ export interface ReasoningStepHeaderProps {
56
+ className?: string
57
+ }
58
+
59
+ export interface ReasoningStepDetailsProps {
60
+ className?: string
61
+ children?: ReactNode
62
+ }
63
+
64
+ export interface ReasoningStepDetailRowProps {
65
+ detail: ReasoningStepDetail
66
+ depth?: number
67
+ pathKey?: string
68
+ className?: string
69
+ }
70
+
71
+ // ===== Context 类型 =====
72
+
73
+ export interface ReasoningStepContextValue {
74
+ // 状态
75
+ status: ReasoningStepStatus
76
+ isExpanded: boolean
77
+ isHovered: boolean
78
+ setIsHovered: (hovered: boolean) => void
79
+ toggleExpanded: () => void
80
+ effectiveExpanded: boolean
81
+ effectiveShowIcon: boolean
82
+ hasExpandableContent: boolean
83
+ // 数据
84
+ text: ReactNode
85
+ description?: string
86
+ details?: ReasoningStepDetail[]
87
+ detailsChildren?: ReactNode
88
+ // 配置
89
+ duration: number
90
+ spread: number
91
+ disableExpandAnimation: boolean
92
+ detailIndent: string
93
+ // 嵌套状态
94
+ expandedNestedKeys: Set<string>
95
+ toggleNested: (pathKey: string) => void
96
+ // 图标
97
+ icon?: ReactNode
98
+ eyeIcon: ReactNode
99
+ arrowRightIcon: ReactNode
100
+ }
101
+
102
+ // ===== 复合组件类型 =====
103
+
104
+ export interface ReasoningStepCompound {
105
+ Root: React.FC<ReasoningStepRootProps>
106
+ Header: React.FC<ReasoningStepHeaderProps>
107
+ Details: React.FC<ReasoningStepDetailsProps>
108
+ DetailRow: React.FC<ReasoningStepDetailRowProps>
109
+ }