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,132 @@
1
+ import { memo, useState } from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { ReactNode } from 'react'
4
+ import { TimeLine, CheckboxCircleLine, CloseCircleLine, ArrowRightLine } from '../basic/icons-inline'
5
+ import { Scrollbar } from '../basic/scrollbar'
6
+
7
+ export type ToolInvocationStatus = 'running' | 'success' | 'error'
8
+
9
+ export interface ToolInvocationCardProps {
10
+ toolName: string
11
+ params?: string
12
+ status: ToolInvocationStatus
13
+ response?: string
14
+ defaultExpanded?: boolean
15
+ statusText?: string
16
+ className?: string
17
+ timeIcon?: ReactNode
18
+ checkboxCircleIcon?: ReactNode
19
+ closeCircleIcon?: ReactNode
20
+ arrowRightIcon?: ReactNode
21
+ runningText?: string
22
+ ranText?: string
23
+ failedText?: string
24
+ responseLabel?: string
25
+ }
26
+
27
+ /* 左侧状态图标随 token:容器 text-xs 使 1em = var(--font-size-xs),图标 size-[1em] 继承 */
28
+ const statusIconClass = 'size-[1em] shrink-0'
29
+ const timeClass = `${statusIconClass} text-text-tertiary`
30
+ const checkboxClass = `${statusIconClass} text-success`
31
+ const closeCircleClass = `${statusIconClass} text-text-tertiary`
32
+ const arrowClass = 'w-3.5 h-3.5 text-text-tertiary transition-all duration-200'
33
+
34
+ export const ToolInvocationCard = memo(function ToolInvocationCard({
35
+ toolName,
36
+ params,
37
+ status,
38
+ response,
39
+ defaultExpanded = false,
40
+ statusText: statusTextProp,
41
+ className,
42
+ timeIcon,
43
+ checkboxCircleIcon,
44
+ closeCircleIcon,
45
+ arrowRightIcon,
46
+ runningText = 'Running',
47
+ ranText = 'Ran',
48
+ failedText = 'Failed',
49
+ responseLabel = 'Response',
50
+ }: ToolInvocationCardProps) {
51
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded)
52
+
53
+ const hasResponse = response != null && response.length > 0
54
+ const isRunning = status === 'running'
55
+ const isSuccess = status === 'success'
56
+ const isError = status === 'error'
57
+
58
+ const defaultStatusText = isRunning ? runningText : isSuccess ? ranText : failedText
59
+ const statusText = statusTextProp !== undefined ? statusTextProp : defaultStatusText
60
+ const showStatusText = statusText !== ''
61
+
62
+ const defaultTime = timeIcon ?? <TimeLine className={timeClass} />
63
+ const defaultCheck = checkboxCircleIcon ?? <CheckboxCircleLine className={checkboxClass} />
64
+ const defaultCloseCircle = closeCircleIcon ?? <CloseCircleLine className={closeCircleClass} />
65
+ const defaultArrow = arrowRightIcon ?? <ArrowRightLine className={arrowClass} />
66
+
67
+ return (
68
+ <div className={cn('flex flex-col', className)}>
69
+ <div
70
+ onClick={() => hasResponse && !isRunning && setIsExpanded(!isExpanded)}
71
+ className={cn(
72
+ 'group flex items-center gap-1.5 min-h-5',
73
+ hasResponse && !isRunning && 'cursor-pointer hover:opacity-80 transition-opacity'
74
+ )}
75
+ >
76
+ <div className="shrink-0 flex items-center justify-center text-xs">
77
+ {isRunning && defaultTime}
78
+ {isSuccess && defaultCheck}
79
+ {isError && defaultCloseCircle}
80
+ </div>
81
+ {showStatusText && (
82
+ <span className="text-xs leading-xs text-text-tertiary shrink-0">{statusText}</span>
83
+ )}
84
+ <span className="text-xs leading-xs text-text-secondary shrink-0">{toolName}</span>
85
+ {params != null && params !== '' && (
86
+ <span
87
+ className="text-xs leading-xs text-text-tertiary min-w-0 shrink truncate"
88
+ title={params}
89
+ >
90
+ {params}
91
+ </span>
92
+ )}
93
+ {hasResponse && !isRunning && (
94
+ <div
95
+ className={cn(
96
+ 'shrink-0 inline-flex items-center justify-center',
97
+ 'opacity-0 group-hover:opacity-100',
98
+ isExpanded && '!opacity-100',
99
+ isExpanded ? 'rotate-90' : 'rotate-0'
100
+ )}
101
+ >
102
+ {defaultArrow}
103
+ </div>
104
+ )}
105
+ </div>
106
+
107
+ {hasResponse && isExpanded && (
108
+ <div className="mt-1 ml-4.5 rounded-md border border-border-tertiary bg-bg-base overflow-hidden font-mono text-xs leading-xs">
109
+ <div className="text-text-tertiary px-2 pt-1 pb-0 leading-xs">
110
+ {responseLabel}
111
+ </div>
112
+ <Scrollbar
113
+ scrollbarVisibility="auto"
114
+ maxHeight={208}
115
+ className="px-2 pb-1 leading-xs"
116
+ >
117
+ <pre
118
+ className={cn(
119
+ 'whitespace-pre-wrap break-words leading-xs',
120
+ isError ? 'text-error' : 'text-text-tertiary'
121
+ )}
122
+ >
123
+ {response}
124
+ </pre>
125
+ </Scrollbar>
126
+ </div>
127
+ )}
128
+ </div>
129
+ )
130
+ })
131
+
132
+ ToolInvocationCard.displayName = 'ToolInvocationCard'
@@ -0,0 +1,38 @@
1
+ import { forwardRef } from 'react'
2
+ import type { HTMLAttributes, ReactNode } from 'react'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export interface UserMessageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
6
+ children: ReactNode
7
+ maxWidth?: string | number
8
+ }
9
+
10
+ export const UserMessage = forwardRef<HTMLDivElement, UserMessageProps>(
11
+ ({ children, className, maxWidth = '752px', ...props }, ref) => {
12
+ const containerStyles = cn(
13
+ 'w-full',
14
+ 'bg-sage-bg',
15
+ 'rounded-lg',
16
+ 'p-3',
17
+ 'text-sm leading-sm',
18
+ 'font-normal',
19
+ 'text-text',
20
+ className
21
+ )
22
+
23
+ return (
24
+ <div
25
+ ref={ref}
26
+ className={containerStyles}
27
+ style={{
28
+ maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth,
29
+ }}
30
+ {...props}
31
+ >
32
+ {children}
33
+ </div>
34
+ )
35
+ }
36
+ )
37
+
38
+ UserMessage.displayName = 'UserMessage'
@@ -0,0 +1,198 @@
1
+ import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
2
+ import { useState } from 'react'
3
+ import { cn } from '@/lib/utils'
4
+ import { OptionList } from '../../basic/option-list'
5
+ import type { OptionItem } from '../../basic/option-list'
6
+ import type { UserQuestionItem, UserQuestionLabels } from './types'
7
+
8
+ export interface UserQuestionCardProps {
9
+ question: UserQuestionItem
10
+ qIndex: number
11
+ labels: Required<UserQuestionLabels>
12
+ answers: Record<string, string[]>
13
+ customInputs: Record<string, string>
14
+ focusedOptionIndex: number
15
+ isCurrentQuestion: boolean
16
+ isSubmitting: boolean
17
+ allQuestionsAnswered: boolean
18
+ questionsLength: number
19
+ questionRefs: MutableRefObject<Map<number, HTMLDivElement>>
20
+ customInputRefs: MutableRefObject<Map<string, HTMLInputElement>>
21
+ setCurrentQuestionIndex: (i: number) => void
22
+ setFocusedOptionIndex: (i: number) => void
23
+ setAnswers: Dispatch<SetStateAction<Record<string, string[]>>>
24
+ setCustomInputs: Dispatch<SetStateAction<Record<string, string>>>
25
+ getSelectedOptionIndex: (i: number) => number
26
+ shouldScrollToQuestionRef: MutableRefObject<boolean>
27
+ onOptionClick: (questionText: string, optionLabel: string, questionIndex: number) => void
28
+ onContinue: () => void
29
+ }
30
+
31
+ export function UserQuestionCard({
32
+ question,
33
+ qIndex,
34
+ labels,
35
+ answers,
36
+ customInputs,
37
+ focusedOptionIndex,
38
+ isCurrentQuestion,
39
+ isSubmitting,
40
+ allQuestionsAnswered,
41
+ questionsLength,
42
+ questionRefs,
43
+ customInputRefs,
44
+ setCurrentQuestionIndex,
45
+ setFocusedOptionIndex,
46
+ setAnswers,
47
+ setCustomInputs,
48
+ getSelectedOptionIndex,
49
+ shouldScrollToQuestionRef,
50
+ onOptionClick,
51
+ onContinue,
52
+ }: UserQuestionCardProps) {
53
+ const questionOptions = question.options || []
54
+ const [isInputFocused, setIsInputFocused] = useState(false)
55
+ const hasInputValue = !!(customInputs[question.question]?.trim())
56
+ const showUnderline = isInputFocused || hasInputValue
57
+
58
+ return (
59
+ <div
60
+ key={question.question}
61
+ ref={(el) => {
62
+ if (el) questionRefs.current.set(qIndex, el)
63
+ else questionRefs.current.delete(qIndex)
64
+ }}
65
+ className={cn(
66
+ 'transition-opacity duration-200',
67
+ isCurrentQuestion ? 'opacity-100' : 'opacity-40'
68
+ )}
69
+ >
70
+ <div className="flex items-start gap-3 pl-2 mb-3">
71
+ <div className="flex-shrink-0 w-5 flex items-center justify-center text-sm leading-sm font-medium text-text-tertiary">
72
+ {qIndex + 1}.
73
+ </div>
74
+ <div className="text-sm leading-sm font-medium text-text min-w-0 flex-1">
75
+ {question.question}
76
+ </div>
77
+ </div>
78
+ <OptionList
79
+ items={questionOptions.map((opt) => ({
80
+ id: opt.label,
81
+ label: opt.label,
82
+ description: opt.description,
83
+ }))}
84
+ selectedIds={answers[question.question]?.map((l) => l) || []}
85
+ focusedId={
86
+ isCurrentQuestion ? questionOptions[focusedOptionIndex]?.label : undefined
87
+ }
88
+ disabled={isSubmitting || !isCurrentQuestion}
89
+ onItemClick={(item: OptionItem, index: number) => {
90
+ if (!isCurrentQuestion) {
91
+ setCurrentQuestionIndex(qIndex)
92
+ setFocusedOptionIndex(getSelectedOptionIndex(qIndex))
93
+ return
94
+ }
95
+ onOptionClick(question.question, item.label, qIndex)
96
+ setFocusedOptionIndex(index)
97
+ }}
98
+ />
99
+ {question.allowCustomInput !== false && (
100
+ <div
101
+ className={cn(
102
+ 'w-full flex gap-3 pl-2 mt-3 transition-colors',
103
+ !isCurrentQuestion && 'opacity-40',
104
+ isSubmitting && 'opacity-50'
105
+ )}
106
+ >
107
+ <div
108
+ className={cn(
109
+ 'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs leading-xs font-medium transition-colors',
110
+ (isCurrentQuestion && focusedOptionIndex === questionOptions.length) ||
111
+ customInputs[question.question]?.trim()
112
+ ? 'bg-primary-active text-text-on-primary'
113
+ : 'bg-fill-secondary text-text-tertiary'
114
+ )}
115
+ >
116
+ {questionOptions.length + 1}
117
+ </div>
118
+ <div className="flex-1 min-w-0 flex flex-col">
119
+ <div className="h-5 flex items-center">
120
+ <input
121
+ ref={(el) => {
122
+ if (el) customInputRefs.current.set(question.question, el)
123
+ else customInputRefs.current.delete(question.question)
124
+ }}
125
+ type="text"
126
+ value={customInputs[question.question] || ''}
127
+ style={{ outline: 'none', outlineOffset: 0, boxShadow: 'none', border: 'none' }}
128
+ onChange={(e) => {
129
+ const value = e.target.value
130
+ setCustomInputs((prev) => ({ ...prev, [question.question]: value }))
131
+ if (!question.multiSelect && value.trim()) {
132
+ setAnswers((prev) => ({
133
+ ...prev,
134
+ [question.question]: [],
135
+ }))
136
+ }
137
+ }}
138
+ onFocus={() => {
139
+ setIsInputFocused(true)
140
+ if (isCurrentQuestion) {
141
+ setFocusedOptionIndex(questionOptions.length)
142
+ if (!question.multiSelect) {
143
+ setAnswers((prev) => ({
144
+ ...prev,
145
+ [question.question]: [],
146
+ }))
147
+ }
148
+ }
149
+ }}
150
+ onBlur={() => setIsInputFocused(false)}
151
+ onKeyDown={(e) => {
152
+ if (e.key === 'Enter' && !e.shiftKey) {
153
+ e.preventDefault()
154
+ e.stopPropagation()
155
+ const currentCustomInput = customInputs[question.question]?.trim()
156
+ const currentSelectedOptions = answers[question.question] || []
157
+ const hasAnswer =
158
+ currentSelectedOptions.length > 0 || !!currentCustomInput
159
+ if (hasAnswer) {
160
+ if (allQuestionsAnswered) {
161
+ onContinue()
162
+ } else if (qIndex < questionsLength - 1) {
163
+ const nextIndex = qIndex + 1
164
+ shouldScrollToQuestionRef.current = true
165
+ setCurrentQuestionIndex(nextIndex)
166
+ setFocusedOptionIndex(0)
167
+ } else {
168
+ onContinue()
169
+ }
170
+ }
171
+ }
172
+ }}
173
+ placeholder={labels.customInputPlaceholder}
174
+ disabled={isSubmitting || !isCurrentQuestion}
175
+ className={cn(
176
+ 'w-full h-full py-0 bg-transparent text-sm leading-sm font-medium text-text',
177
+ 'placeholder:text-text-tertiary placeholder:font-normal',
178
+ 'border-0 rounded-none outline-none outline-offset-0',
179
+ 'focus:outline-none focus:outline-offset-0',
180
+ 'focus-visible:outline-none focus-visible:outline-offset-0',
181
+ 'ring-0 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none',
182
+ 'disabled:cursor-not-allowed'
183
+ )}
184
+ />
185
+ </div>
186
+ <div
187
+ className="mt-1.5 overflow-hidden origin-left transition-[width] duration-200 ease-out"
188
+ style={{ width: showUnderline ? '100%' : '0%' }}
189
+ aria-hidden
190
+ >
191
+ <div className="h-px bg-[var(--color-border-tertiary)]" />
192
+ </div>
193
+ </div>
194
+ </div>
195
+ )}
196
+ </div>
197
+ )
198
+ }
@@ -0,0 +1,66 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Button } from '../../basic/button'
3
+ import type { UserQuestionLabels } from './types'
4
+
5
+ export interface UserQuestionFooterProps {
6
+ labels: Required<UserQuestionLabels>
7
+ sparklingIcon: ReactNode
8
+ isSubmitting: boolean
9
+ hasCustomText: boolean
10
+ allQuestionsAnswered: boolean
11
+ onRecommend: () => void
12
+ onSkip: () => void
13
+ onContinue: () => void
14
+ }
15
+
16
+ export function UserQuestionFooter({
17
+ labels,
18
+ sparklingIcon,
19
+ isSubmitting,
20
+ hasCustomText,
21
+ allQuestionsAnswered,
22
+ onRecommend,
23
+ onSkip,
24
+ onContinue,
25
+ }: UserQuestionFooterProps) {
26
+ return (
27
+ <div className="flex items-center justify-between px-3 py-2">
28
+ <Button
29
+ variant="ghost"
30
+ size="sm"
31
+ onClick={onRecommend}
32
+ disabled={isSubmitting}
33
+ >
34
+ {sparklingIcon}
35
+ {labels.recommend}
36
+ </Button>
37
+ <div className="flex items-center gap-2">
38
+ {!allQuestionsAnswered && (
39
+ <Button
40
+ variant="ghost"
41
+ size="sm"
42
+ onClick={onSkip}
43
+ disabled={isSubmitting}
44
+ >
45
+ {labels.skipAll}
46
+ </Button>
47
+ )}
48
+ <Button
49
+ size="sm"
50
+ variant="secondary"
51
+ onClick={onContinue}
52
+ disabled={isSubmitting || hasCustomText || !allQuestionsAnswered}
53
+ >
54
+ {isSubmitting ? (
55
+ labels.sending
56
+ ) : (
57
+ <>
58
+ {allQuestionsAnswered ? labels.submit : labels.continue}
59
+ <span className="opacity-70"> ↵</span>
60
+ </>
61
+ )}
62
+ </Button>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from 'react'
2
+ import { IconButton } from '../../basic/icon-button'
3
+ import type { UserQuestionItem } from './types'
4
+ import type { UserQuestionLabels } from './types'
5
+
6
+ export interface UserQuestionHeaderProps {
7
+ currentQuestion: UserQuestionItem | undefined
8
+ questionsLength: number
9
+ labels: Required<UserQuestionLabels>
10
+ chat4Icon: ReactNode
11
+ arrowUpSIcon: ReactNode
12
+ arrowDownSIcon: ReactNode
13
+ currentQuestionIndex: number
14
+ onPrevious: () => void
15
+ onNext: () => void
16
+ }
17
+
18
+ export function UserQuestionHeader({
19
+ currentQuestion,
20
+ questionsLength,
21
+ labels,
22
+ chat4Icon,
23
+ arrowUpSIcon,
24
+ arrowDownSIcon,
25
+ currentQuestionIndex,
26
+ onPrevious,
27
+ onNext,
28
+ }: UserQuestionHeaderProps) {
29
+ return (
30
+ <div className="flex items-center justify-between mb-2 -mt-2 px-3 pt-2 pb-2 rounded-t-xl bg-fill-tertiary">
31
+ <div className="flex items-center gap-1.5">
32
+ {chat4Icon}
33
+ <span className="text-xs leading-xs text-text-tertiary">
34
+ {currentQuestion?.header || labels.questionHeader}
35
+ </span>
36
+ <span className="text-text-tertiary/50">•</span>
37
+ <span className="text-xs leading-xs text-text-tertiary">
38
+ {currentQuestion?.multiSelect ? labels.multiSelect : labels.singleSelect}
39
+ </span>
40
+ </div>
41
+ {questionsLength > 1 && (
42
+ <div className="flex items-center gap-1">
43
+ <IconButton
44
+ size="sm"
45
+ variant="ghost"
46
+ onClick={onPrevious}
47
+ disabled={currentQuestionIndex === 0}
48
+ icon={arrowUpSIcon}
49
+ />
50
+ <span className="text-xs leading-xs text-text-tertiary px-1">
51
+ {currentQuestionIndex + 1} / {questionsLength}
52
+ </span>
53
+ <IconButton
54
+ size="sm"
55
+ variant="ghost"
56
+ onClick={onNext}
57
+ disabled={currentQuestionIndex === questionsLength - 1}
58
+ icon={arrowDownSIcon}
59
+ />
60
+ </div>
61
+ )}
62
+ </div>
63
+ )
64
+ }