sparkdesign 0.3.1 → 0.3.2

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
@@ -0,0 +1,324 @@
1
+ import { useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { cn } from '../../lib/utils'
4
+ import { motion } from 'framer-motion'
5
+ import { IconButton } from '../../basic/icon-button'
6
+ import { Button } from '../../basic/button'
7
+ import { OptionList } from '../../basic/option-list'
8
+ import { Scrollbar } from '../../basic/scrollbar'
9
+ import type { OptionItem } from '../../basic/option-list'
10
+ import { useUserQuestionContext } from './context'
11
+ import type { UserQuestionItem } from './types'
12
+
13
+ // ===== UserQuestion.Header =====
14
+
15
+ export interface UserQuestionHeaderProps {
16
+ className?: string
17
+ }
18
+
19
+ export function UserQuestionHeader({ className }: UserQuestionHeaderProps) {
20
+ const ctx = useUserQuestionContext()
21
+
22
+ return (
23
+ <div className={cn('flex items-center justify-between mb-2 -mt-2 px-3 pt-2 pb-2 rounded-t-xl bg-fill-tertiary', className)}>
24
+ <div className="flex items-center gap-1.5">
25
+ {ctx.chat4Icon}
26
+ <span className="text-xs leading-xs text-text-tertiary">
27
+ {ctx.currentQuestion?.header || ctx.labels.questionHeader}
28
+ </span>
29
+ <span className="text-text-tertiary/50">•</span>
30
+ <span className="text-xs leading-xs text-text-tertiary">
31
+ {ctx.currentQuestion?.multiSelect ? ctx.labels.multiSelect : ctx.labels.singleSelect}
32
+ </span>
33
+ </div>
34
+ {ctx.questions.length > 1 && (
35
+ <div className="flex items-center gap-1">
36
+ <IconButton
37
+ size="sm"
38
+ variant="ghost"
39
+ onClick={ctx.handlePrevious}
40
+ disabled={ctx.currentQuestionIndex === 0}
41
+ icon={ctx.arrowUpSIcon}
42
+ />
43
+ <span className="text-xs leading-xs text-text-tertiary px-1">
44
+ {ctx.currentQuestionIndex + 1} / {ctx.questions.length}
45
+ </span>
46
+ <IconButton
47
+ size="sm"
48
+ variant="ghost"
49
+ onClick={ctx.handleNext}
50
+ disabled={ctx.currentQuestionIndex === ctx.questions.length - 1}
51
+ icon={ctx.arrowDownSIcon}
52
+ />
53
+ </div>
54
+ )}
55
+ </div>
56
+ )
57
+ }
58
+
59
+ UserQuestionHeader.displayName = 'UserQuestionHeader'
60
+
61
+ // ===== UserQuestion.Card =====
62
+
63
+ export interface UserQuestionCardProps {
64
+ question: UserQuestionItem
65
+ qIndex: number
66
+ className?: string
67
+ }
68
+
69
+ export function UserQuestionCard({ question, qIndex, className }: UserQuestionCardProps) {
70
+ const ctx = useUserQuestionContext()
71
+ const questionOptions = question.options || []
72
+ const [isInputFocused, setIsInputFocused] = useState(false)
73
+ const hasInputValue = !!(ctx.customInputs[question.question]?.trim())
74
+ const showUnderline = isInputFocused || hasInputValue
75
+ const isCurrentQuestion = qIndex === ctx.currentQuestionIndex
76
+
77
+ return (
78
+ <div
79
+ key={question.question}
80
+ ref={(el) => {
81
+ if (el) ctx.questionRefs.current.set(qIndex, el)
82
+ else ctx.questionRefs.current.delete(qIndex)
83
+ }}
84
+ className={cn(
85
+ 'transition-opacity duration-200',
86
+ isCurrentQuestion ? 'opacity-100' : 'opacity-40',
87
+ className
88
+ )}
89
+ >
90
+ <div className="flex items-start gap-3 pl-2 mb-3">
91
+ <div className="flex-shrink-0 w-5 flex items-center justify-center text-sm leading-sm font-medium text-text-tertiary">
92
+ {qIndex + 1}.
93
+ </div>
94
+ <div className="text-sm leading-sm font-medium text-text min-w-0 flex-1">
95
+ {question.question}
96
+ </div>
97
+ </div>
98
+ <OptionList
99
+ items={questionOptions.map((opt) => ({
100
+ id: opt.label,
101
+ label: opt.label,
102
+ description: opt.description,
103
+ }))}
104
+ selectedIds={ctx.answers[question.question]?.map((l) => l) || []}
105
+ focusedId={
106
+ isCurrentQuestion ? questionOptions[ctx.focusedOptionIndex]?.label : undefined
107
+ }
108
+ disabled={ctx.isSubmitting || !isCurrentQuestion}
109
+ onItemClick={(item: OptionItem, index: number) => {
110
+ if (!isCurrentQuestion) {
111
+ ctx.setCurrentQuestionIndex(qIndex)
112
+ ctx.setFocusedOptionIndex(ctx.getSelectedOptionIndex(qIndex))
113
+ return
114
+ }
115
+ ctx.handleOptionClick(question.question, item.label, qIndex)
116
+ ctx.setFocusedOptionIndex(index)
117
+ }}
118
+ />
119
+ {question.allowCustomInput !== false && (
120
+ <div
121
+ className={cn(
122
+ 'w-full flex gap-3 pl-2 mt-3 transition-colors',
123
+ !isCurrentQuestion && 'opacity-40',
124
+ ctx.isSubmitting && 'opacity-50'
125
+ )}
126
+ >
127
+ <div
128
+ className={cn(
129
+ 'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs leading-xs font-medium transition-colors',
130
+ (isCurrentQuestion && ctx.focusedOptionIndex === questionOptions.length) ||
131
+ ctx.customInputs[question.question]?.trim()
132
+ ? 'bg-primary-active text-text-on-primary'
133
+ : 'bg-fill-secondary text-text-tertiary'
134
+ )}
135
+ >
136
+ {questionOptions.length + 1}
137
+ </div>
138
+ <div className="flex-1 min-w-0 flex flex-col">
139
+ <div className="h-5 flex items-center">
140
+ <input
141
+ ref={(el) => {
142
+ if (el) ctx.customInputRefs.current.set(question.question, el)
143
+ else ctx.customInputRefs.current.delete(question.question)
144
+ }}
145
+ type="text"
146
+ value={ctx.customInputs[question.question] || ''}
147
+ style={{ outline: 'none', outlineOffset: 0, boxShadow: 'none', border: 'none' }}
148
+ onChange={(e) => {
149
+ const value = e.target.value
150
+ ctx.setCustomInputs((prev) => ({ ...prev, [question.question]: value }))
151
+ if (!question.multiSelect && value.trim()) {
152
+ ctx.setAnswers((prev) => ({
153
+ ...prev,
154
+ [question.question]: [],
155
+ }))
156
+ }
157
+ }}
158
+ onFocus={() => {
159
+ setIsInputFocused(true)
160
+ if (isCurrentQuestion) {
161
+ ctx.setFocusedOptionIndex(questionOptions.length)
162
+ if (!question.multiSelect) {
163
+ ctx.setAnswers((prev) => ({
164
+ ...prev,
165
+ [question.question]: [],
166
+ }))
167
+ }
168
+ }
169
+ }}
170
+ onBlur={() => setIsInputFocused(false)}
171
+ onKeyDown={(e) => {
172
+ if (e.key === 'Enter' && !e.shiftKey) {
173
+ e.preventDefault()
174
+ e.stopPropagation()
175
+ const currentCustomInput = ctx.customInputs[question.question]?.trim()
176
+ const currentSelectedOptions = ctx.answers[question.question] || []
177
+ const hasAnswer =
178
+ currentSelectedOptions.length > 0 || !!currentCustomInput
179
+ if (hasAnswer) {
180
+ if (ctx.allQuestionsAnswered) {
181
+ ctx.handleContinue()
182
+ } else if (qIndex < ctx.questions.length - 1) {
183
+ const nextIndex = qIndex + 1
184
+ ctx.setShouldScrollToQuestion(true)
185
+ ctx.setCurrentQuestionIndex(nextIndex)
186
+ ctx.setFocusedOptionIndex(0)
187
+ } else {
188
+ ctx.handleContinue()
189
+ }
190
+ }
191
+ }
192
+ }}
193
+ placeholder={ctx.labels.customInputPlaceholder}
194
+ disabled={ctx.isSubmitting || !isCurrentQuestion}
195
+ className={cn(
196
+ 'w-full h-full py-0 bg-transparent text-sm leading-sm font-medium text-text',
197
+ 'placeholder:text-text-tertiary placeholder:font-normal',
198
+ 'border-0 rounded-none outline-none outline-offset-0',
199
+ 'focus:outline-none focus:outline-offset-0',
200
+ 'focus-visible:outline-none focus-visible:outline-offset-0',
201
+ 'ring-0 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none',
202
+ 'disabled:cursor-not-allowed'
203
+ )}
204
+ />
205
+ </div>
206
+ <div
207
+ className="mt-1.5 overflow-hidden origin-left transition-[width] duration-200 ease-out"
208
+ style={{ width: showUnderline ? '100%' : '0%' }}
209
+ aria-hidden
210
+ >
211
+ <div className="h-px bg-[var(--color-border-tertiary)]" />
212
+ </div>
213
+ </div>
214
+ </div>
215
+ )}
216
+ </div>
217
+ )
218
+ }
219
+
220
+ UserQuestionCard.displayName = 'UserQuestionCard'
221
+
222
+ // ===== UserQuestion.Cards (渲染所有问题卡片) =====
223
+
224
+ export interface UserQuestionCardsProps {
225
+ className?: string
226
+ children?: ReactNode
227
+ }
228
+
229
+ export function UserQuestionCards({ className, children }: UserQuestionCardsProps) {
230
+ const ctx = useUserQuestionContext()
231
+ // Destructure to avoid lint false positives about ref access during render
232
+ const { scrollContainerRef, maxQuestionHeight, questions } = ctx
233
+
234
+ return (
235
+ <Scrollbar
236
+ ref={scrollContainerRef}
237
+ scrollbarVisibility="auto"
238
+ maxHeight={maxQuestionHeight ? maxQuestionHeight : undefined}
239
+ className={cn('scroll-smooth', className)}
240
+ >
241
+ <div className="space-y-4 px-3">
242
+ {children ?? questions.map((question, qIndex) => (
243
+ <UserQuestionCard key={question.question} question={question} qIndex={qIndex} />
244
+ ))}
245
+ </div>
246
+ </Scrollbar>
247
+ )
248
+ }
249
+
250
+ UserQuestionCards.displayName = 'UserQuestionCards'
251
+
252
+ // ===== UserQuestion.Footer =====
253
+
254
+ export interface UserQuestionFooterProps {
255
+ className?: string
256
+ }
257
+
258
+ export function UserQuestionFooter({ className }: UserQuestionFooterProps) {
259
+ const ctx = useUserQuestionContext()
260
+
261
+ return (
262
+ <div className={cn('flex items-center justify-between px-3 py-2', className)}>
263
+ <Button
264
+ variant="ghost"
265
+ size="sm"
266
+ onClick={ctx.handleRecommend}
267
+ disabled={ctx.isSubmitting}
268
+ >
269
+ {ctx.sparklingIcon}
270
+ {ctx.labels.recommend}
271
+ </Button>
272
+ <div className="flex items-center gap-2">
273
+ {!ctx.allQuestionsAnswered && (
274
+ <Button
275
+ variant="ghost"
276
+ size="sm"
277
+ onClick={ctx.handleSkip}
278
+ disabled={ctx.isSubmitting}
279
+ >
280
+ {ctx.labels.skipAll}
281
+ </Button>
282
+ )}
283
+ <Button
284
+ size="sm"
285
+ variant="secondary"
286
+ onClick={ctx.handleContinue}
287
+ disabled={ctx.isSubmitting || ctx.hasCustomText || !ctx.allQuestionsAnswered}
288
+ >
289
+ {ctx.isSubmitting ? (
290
+ ctx.labels.sending
291
+ ) : (
292
+ <>
293
+ {ctx.allQuestionsAnswered ? ctx.labels.submit : ctx.labels.continue}
294
+ <span className="opacity-70"> ↵</span>
295
+ </>
296
+ )}
297
+ </Button>
298
+ </div>
299
+ </div>
300
+ )
301
+ }
302
+
303
+ UserQuestionFooter.displayName = 'UserQuestionFooter'
304
+
305
+ // ===== UserQuestion.DefaultLayout =====
306
+
307
+ export function UserQuestionDefaultLayout() {
308
+ return (
309
+ <motion.div
310
+ initial={{ opacity: 0, y: 8 }}
311
+ animate={{ opacity: 1, y: 0 }}
312
+ transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
313
+ className={cn(
314
+ 'border rounded-t-xl border-b-0 border-border border-border-tertiary bg-bg-base overflow-hidden pt-2 pb-2'
315
+ )}
316
+ >
317
+ <UserQuestionHeader />
318
+ <UserQuestionCards />
319
+ <UserQuestionFooter />
320
+ </motion.div>
321
+ )
322
+ }
323
+
324
+ UserQuestionDefaultLayout.displayName = 'UserQuestionDefaultLayout'