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,126 @@
1
+ import { useEffect } from 'react'
2
+ import type { UserQuestionItem, UserQuestionOption } from './types'
3
+
4
+ export interface UseUserQuestionKeyboardProps {
5
+ isSubmitting: boolean
6
+ currentQuestion: UserQuestionItem | undefined
7
+ currentOptions: UserQuestionOption[]
8
+ currentQuestionIndex: number
9
+ questions: UserQuestionItem[]
10
+ focusedOptionIndex: number
11
+ setFocusedOptionIndex: (n: number) => void
12
+ currentQuestionHasAnswer: boolean
13
+ getSelectedOptionIndex: (i: number) => number
14
+ setShouldScrollToQuestion: (v: boolean) => void
15
+ setCurrentQuestionIndex: (i: number) => void
16
+ handleOptionClick: (questionText: string, optionLabel: string, questionIndex: number) => void
17
+ handleContinue: () => void
18
+ }
19
+
20
+ function isFocusInInput(activeEl: Element | null): boolean {
21
+ return (
22
+ activeEl instanceof HTMLInputElement ||
23
+ activeEl instanceof HTMLTextAreaElement ||
24
+ activeEl?.getAttribute('contenteditable') === 'true'
25
+ )
26
+ }
27
+
28
+ export function useUserQuestionKeyboard({
29
+ isSubmitting,
30
+ currentQuestion,
31
+ currentOptions,
32
+ currentQuestionIndex,
33
+ questions,
34
+ focusedOptionIndex,
35
+ setFocusedOptionIndex,
36
+ currentQuestionHasAnswer,
37
+ getSelectedOptionIndex,
38
+ setShouldScrollToQuestion,
39
+ setCurrentQuestionIndex,
40
+ handleOptionClick,
41
+ handleContinue,
42
+ }: UseUserQuestionKeyboardProps) {
43
+ useEffect(() => {
44
+ const keyHandlers: Record<string, (e: KeyboardEvent) => void> = {
45
+ ArrowDown(e) {
46
+ e.preventDefault()
47
+ if (focusedOptionIndex < currentOptions.length - 1) {
48
+ setFocusedOptionIndex(focusedOptionIndex + 1)
49
+ } else if (currentQuestionIndex < questions.length - 1) {
50
+ const nextIndex = currentQuestionIndex + 1
51
+ setShouldScrollToQuestion(true)
52
+ setCurrentQuestionIndex(nextIndex)
53
+ setFocusedOptionIndex(getSelectedOptionIndex(nextIndex))
54
+ }
55
+ },
56
+ ArrowUp(e) {
57
+ e.preventDefault()
58
+ if (focusedOptionIndex > 0) {
59
+ setFocusedOptionIndex(focusedOptionIndex - 1)
60
+ } else if (currentQuestionIndex > 0) {
61
+ const prevIndex = currentQuestionIndex - 1
62
+ setShouldScrollToQuestion(true)
63
+ setCurrentQuestionIndex(prevIndex)
64
+ setFocusedOptionIndex(getSelectedOptionIndex(prevIndex))
65
+ }
66
+ },
67
+ Enter(e) {
68
+ e.preventDefault()
69
+ if (currentQuestionHasAnswer) {
70
+ handleContinue()
71
+ } else if (currentQuestion && currentOptions[focusedOptionIndex]) {
72
+ handleOptionClick(
73
+ currentQuestion.question,
74
+ currentOptions[focusedOptionIndex].label,
75
+ currentQuestionIndex
76
+ )
77
+ }
78
+ },
79
+ }
80
+
81
+ const handleKeyDown = (e: KeyboardEvent) => {
82
+ if (isSubmitting) return
83
+ if (isFocusInInput(document.activeElement)) return
84
+
85
+ const handler = keyHandlers[e.key]
86
+ if (handler) {
87
+ handler(e)
88
+ return
89
+ }
90
+
91
+ if (e.key >= '1' && e.key <= '9') {
92
+ const numberIndex = parseInt(e.key, 10) - 1
93
+ if (
94
+ numberIndex >= 0 &&
95
+ numberIndex < currentOptions.length &&
96
+ currentQuestion
97
+ ) {
98
+ e.preventDefault()
99
+ handleOptionClick(
100
+ currentQuestion.question,
101
+ currentOptions[numberIndex].label,
102
+ currentQuestionIndex
103
+ )
104
+ setFocusedOptionIndex(numberIndex)
105
+ }
106
+ }
107
+ }
108
+
109
+ document.addEventListener('keydown', handleKeyDown)
110
+ return () => document.removeEventListener('keydown', handleKeyDown)
111
+ }, [
112
+ isSubmitting,
113
+ currentQuestion,
114
+ currentOptions,
115
+ currentQuestionIndex,
116
+ questions.length,
117
+ focusedOptionIndex,
118
+ setFocusedOptionIndex,
119
+ currentQuestionHasAnswer,
120
+ getSelectedOptionIndex,
121
+ setCurrentQuestionIndex,
122
+ handleOptionClick,
123
+ handleContinue,
124
+ setShouldScrollToQuestion,
125
+ ])
126
+ }
@@ -0,0 +1,165 @@
1
+ import { useState, useEffect, useCallback, useRef, useImperativeHandle } from 'react'
2
+ import type { Ref } from 'react'
3
+ import type { UserQuestionItem, UserQuestionHandle } from './types'
4
+
5
+ export interface UseUserQuestionStateProps {
6
+ questions: UserQuestionItem[]
7
+ resetKey?: string
8
+ onAnswer: (answers: Record<string, string>) => void
9
+ ref: Ref<UserQuestionHandle | null>
10
+ }
11
+
12
+ export function useUserQuestionState({
13
+ questions,
14
+ resetKey,
15
+ ref,
16
+ }: UseUserQuestionStateProps) {
17
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
18
+ const [answers, setAnswers] = useState<Record<string, string[]>>({})
19
+ const [customInputs, setCustomInputs] = useState<Record<string, string>>({})
20
+ const [focusedOptionIndex, setFocusedOptionIndex] = useState(0)
21
+ const [isSubmitting, setIsSubmitting] = useState(false)
22
+ const [maxQuestionHeight, setMaxQuestionHeight] = useState<number | undefined>(undefined)
23
+
24
+ const prevIndexRef = useRef(currentQuestionIndex)
25
+ const prevResetKeyRef = useRef(resetKey)
26
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
27
+ const questionRefs = useRef<Map<number, HTMLDivElement>>(new Map())
28
+ const currentIndexRef = useRef(currentQuestionIndex)
29
+ const isProgrammaticScrollRef = useRef(false)
30
+ const shouldScrollToQuestionRef = useRef(false)
31
+ const customInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
32
+
33
+ const getSelectedOptionIndex = useCallback(
34
+ (questionIndex: number) => {
35
+ const question = questions[questionIndex]
36
+ if (!question) return 0
37
+ const selectedLabels = answers[question.question] || []
38
+ if (selectedLabels.length === 0) return 0
39
+ const options = question.options || []
40
+ const selectedIndex = options.findIndex((opt) => selectedLabels.includes(opt.label))
41
+ return selectedIndex >= 0 ? selectedIndex : 0
42
+ },
43
+ [questions, answers]
44
+ )
45
+
46
+ useImperativeHandle(
47
+ ref,
48
+ () => ({
49
+ getAnswers: () => {
50
+ const formattedAnswers: Record<string, string> = {}
51
+ for (const question of questions) {
52
+ const selected = answers[question.question] || []
53
+ const customInput = customInputs[question.question]?.trim()
54
+ const allParts: string[] = []
55
+ if (selected.length > 0) allParts.push(...selected)
56
+ if (customInput) allParts.push(customInput)
57
+ if (allParts.length > 0) {
58
+ formattedAnswers[question.question] = allParts.join(', ')
59
+ }
60
+ }
61
+ return formattedAnswers
62
+ },
63
+ }),
64
+ [answers, customInputs, questions]
65
+ )
66
+
67
+ useEffect(() => {
68
+ if (prevResetKeyRef.current !== resetKey) {
69
+ setIsSubmitting(false)
70
+ setCurrentQuestionIndex(0)
71
+ currentIndexRef.current = 0
72
+ setAnswers({})
73
+ setCustomInputs({})
74
+ setFocusedOptionIndex(0)
75
+ prevResetKeyRef.current = resetKey
76
+ questionRefs.current.clear()
77
+ customInputRefs.current.clear()
78
+ }
79
+ }, [resetKey])
80
+
81
+ useEffect(() => {
82
+ if (prevIndexRef.current !== currentQuestionIndex) {
83
+ currentIndexRef.current = currentQuestionIndex
84
+ if (shouldScrollToQuestionRef.current) {
85
+ isProgrammaticScrollRef.current = true
86
+ const questionEl = questionRefs.current.get(currentQuestionIndex)
87
+ const container = scrollContainerRef.current
88
+ if (questionEl && container) {
89
+ const handleScrollEnd = () => {
90
+ isProgrammaticScrollRef.current = false
91
+ shouldScrollToQuestionRef.current = false
92
+ container.removeEventListener('scrollend', handleScrollEnd)
93
+ }
94
+ container.addEventListener('scrollend', handleScrollEnd, { once: true })
95
+ questionEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
96
+ } else {
97
+ isProgrammaticScrollRef.current = false
98
+ shouldScrollToQuestionRef.current = false
99
+ }
100
+ }
101
+ prevIndexRef.current = currentQuestionIndex
102
+ }
103
+ }, [currentQuestionIndex])
104
+
105
+ useEffect(() => {
106
+ const timer = setTimeout(() => {
107
+ let maxHeight = 0
108
+ questionRefs.current.forEach((el) => {
109
+ const h = el.getBoundingClientRect().height
110
+ if (h > maxHeight) maxHeight = h
111
+ })
112
+ if (maxHeight > 0) setMaxQuestionHeight(maxHeight)
113
+ }, 50)
114
+ return () => clearTimeout(timer)
115
+ }, [questions, resetKey])
116
+
117
+ useEffect(() => {
118
+ const container = scrollContainerRef.current
119
+ if (!container) return
120
+ const observer = new IntersectionObserver(
121
+ (entries) => {
122
+ if (isProgrammaticScrollRef.current) return
123
+ let maxRatio = 0
124
+ let visibleIndex = currentIndexRef.current
125
+ entries.forEach((entry) => {
126
+ if (entry.isIntersecting && entry.intersectionRatio > maxRatio) {
127
+ maxRatio = entry.intersectionRatio
128
+ questionRefs.current.forEach((el, index) => {
129
+ if (el === entry.target) visibleIndex = index
130
+ })
131
+ }
132
+ })
133
+ if (visibleIndex !== currentIndexRef.current && maxRatio > 0.5) {
134
+ currentIndexRef.current = visibleIndex
135
+ setCurrentQuestionIndex(visibleIndex)
136
+ setFocusedOptionIndex(getSelectedOptionIndex(visibleIndex))
137
+ }
138
+ },
139
+ { root: container, threshold: [0, 0.25, 0.5, 0.75, 1] }
140
+ )
141
+ questionRefs.current.forEach((el) => {
142
+ if (el?.isConnected) observer.observe(el)
143
+ })
144
+ return () => observer.disconnect()
145
+ }, [questions, getSelectedOptionIndex])
146
+
147
+ return {
148
+ currentQuestionIndex,
149
+ setCurrentQuestionIndex,
150
+ answers,
151
+ setAnswers,
152
+ customInputs,
153
+ setCustomInputs,
154
+ focusedOptionIndex,
155
+ setFocusedOptionIndex,
156
+ isSubmitting,
157
+ setIsSubmitting,
158
+ maxQuestionHeight,
159
+ scrollContainerRef,
160
+ questionRefs,
161
+ shouldScrollToQuestionRef,
162
+ customInputRefs,
163
+ getSelectedOptionIndex,
164
+ }
165
+ }
@@ -0,0 +1,62 @@
1
+ import { memo } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { QuestionAnswerLine } from '../basic/icons-inline'
4
+
5
+ export interface UserQuestionAnswerLabels {
6
+ answer?: string
7
+ answers?: string
8
+ }
9
+
10
+ export interface UserQuestionAnswerProps {
11
+ answers: Record<string, string>
12
+ showHeader?: boolean
13
+ labels?: UserQuestionAnswerLabels
14
+ className?: string
15
+ questionAnswerIcon?: ReactNode
16
+ }
17
+
18
+ const DEFAULT_LABELS: Required<UserQuestionAnswerLabels> = {
19
+ answer: 'Answer',
20
+ answers: 'Answers',
21
+ }
22
+
23
+ const iconClass = 'w-4 h-4 shrink-0 text-text-tertiary'
24
+
25
+ export const UserQuestionAnswer = memo(function UserQuestionAnswer({
26
+ answers,
27
+ showHeader = true,
28
+ labels: labelsProp,
29
+ className = '',
30
+ questionAnswerIcon,
31
+ }: UserQuestionAnswerProps) {
32
+ const labels = { ...DEFAULT_LABELS, ...labelsProp }
33
+ const entries = Object.entries(answers)
34
+ const defaultIcon = questionAnswerIcon ?? <QuestionAnswerLine className={iconClass} />
35
+
36
+ if (entries.length === 0) return null
37
+
38
+ return (
39
+ <div
40
+ className={`rounded-lg border border-border-tertiary bg-bg-container overflow-hidden mx-2 ${className}`.trim()}
41
+ >
42
+ {showHeader && (
43
+ <div className="flex items-center gap-2 pl-3 pr-3 h-8 border-b border-border-tertiary">
44
+ {defaultIcon}
45
+ <span className="text-xs leading-xs text-text-tertiary">
46
+ {entries.length === 1 ? labels.answer : labels.answers}
47
+ </span>
48
+ </div>
49
+ )}
50
+ <div className="flex flex-col gap-3 p-3 text-xs leading-xs">
51
+ {entries.map(([question, answer], idx) => (
52
+ <div key={idx} className="flex flex-col gap-1">
53
+ <span className="font-medium text-text">{question}</span>
54
+ <span className="text-text-tertiary">{answer}</span>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )
60
+ })
61
+
62
+ UserQuestionAnswer.displayName = 'UserQuestionAnswer'
@@ -0,0 +1,150 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // Seti 字体字符映射(与 QodeChatPanel 完全一致:FileReviewPart 内联 + utils/fileIconMaps.js)
3
+ // 用于 qoder-seti.woff,勿改为 VSCode vs-seti 码位(E051/E055 等)
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+ export const SETI_CHAR_MAP: Record<string, string> = {
6
+ javascript: '\ue054',
7
+ typescript: '\ue099',
8
+ react: '\ue07d',
9
+ json: '\ue059',
10
+ html: '\ue048',
11
+ css: '\ue01d',
12
+ python: '\ue07b',
13
+ java: '\ue050',
14
+ go: '\ue039',
15
+ rust: '\ue082',
16
+ php: '\ue070',
17
+ ruby: '\ue081',
18
+ vue: '\ue09d',
19
+ svelte: '\ue090',
20
+ markdown: '\ue060',
21
+ yaml: '\ue0a7',
22
+ xml: '\ue0a5',
23
+ shell: '\ue089',
24
+ sass: '\ue084',
25
+ settings: '\ue019',
26
+ default: '\ue023',
27
+ }
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+ // Seti 图标颜色(hex,用于 Seti 字体渲染 style.color)
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+ export const SETI_COLOR_MAP: Record<string, string> = {
33
+ javascript: '#F8CA3E',
34
+ typescript: '#1E8DEE',
35
+ react: '#1E8DEE',
36
+ json: '#F8CA3E',
37
+ html: '#1E8DEE',
38
+ css: '#1E8DEE',
39
+ python: '#1E8DEE',
40
+ java: '#E84654',
41
+ go: '#1E8DEE',
42
+ rust: '#6A89E6',
43
+ php: '#AE4EE2',
44
+ ruby: '#E84654',
45
+ vue: '#4EBD35',
46
+ svelte: '#E84654',
47
+ markdown: '#1E8DEE',
48
+ yaml: '#AE4EE2',
49
+ xml: '#F18745',
50
+ shell: '#4EBD35',
51
+ sass: '#E84654',
52
+ settings: '#6A89E6',
53
+ default: '#6A89E6',
54
+ }
55
+
56
+ // ═══════════════════════════════════════════════════════════════════════════════
57
+ // 扩展名 -> 类型名
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+ export const FILE_TYPE_MAP: Record<string, string> = {
60
+ js: 'javascript',
61
+ jsx: 'react',
62
+ ts: 'typescript',
63
+ tsx: 'typescript',
64
+ json: 'json',
65
+ html: 'html',
66
+ css: 'css',
67
+ scss: 'sass',
68
+ sass: 'sass',
69
+ less: 'css',
70
+ py: 'python',
71
+ java: 'java',
72
+ go: 'go',
73
+ rs: 'rust',
74
+ php: 'php',
75
+ rb: 'ruby',
76
+ vue: 'vue',
77
+ svelte: 'svelte',
78
+ md: 'markdown',
79
+ yml: 'yaml',
80
+ yaml: 'yaml',
81
+ xml: 'xml',
82
+ sh: 'shell',
83
+ bash: 'shell',
84
+ zsh: 'shell',
85
+ config: 'settings',
86
+ conf: 'settings',
87
+ }
88
+
89
+ // ═══════════════════════════════════════════════════════════════════════════════
90
+ // 类型 -> Tailwind 设计 token 类(无 Seti 字体时用通用图标+此类)
91
+ // ═══════════════════════════════════════════════════════════════════════════════
92
+ export const TYPE_TO_TOKEN_CLASS: Record<string, string> = {
93
+ javascript: 'text-info',
94
+ typescript: 'text-info',
95
+ react: 'text-info',
96
+ json: 'text-info',
97
+ html: 'text-info',
98
+ css: 'text-info',
99
+ python: 'text-teal',
100
+ java: 'text-error',
101
+ go: 'text-info',
102
+ rust: 'text-text-tertiary',
103
+ php: 'text-text-tertiary',
104
+ ruby: 'text-error',
105
+ vue: 'text-success',
106
+ svelte: 'text-error',
107
+ markdown: 'text-text-secondary',
108
+ yaml: 'text-text-tertiary',
109
+ xml: 'text-text-tertiary',
110
+ shell: 'text-success',
111
+ sass: 'text-error',
112
+ settings: 'text-text-tertiary',
113
+ default: 'text-text-tertiary',
114
+ }
115
+
116
+ export function getSetiIconType(fileType?: string, filename?: string): string {
117
+ let iconType: string | null = null
118
+
119
+ if (filename && typeof filename === 'string' && filename.trim()) {
120
+ const lastDot = filename.lastIndexOf('.')
121
+ if (lastDot > 0 && lastDot < filename.length - 1) {
122
+ const ext = filename.substring(lastDot + 1).toLowerCase().trim()
123
+ if (ext && FILE_TYPE_MAP[ext]) iconType = FILE_TYPE_MAP[ext]
124
+ }
125
+ }
126
+
127
+ if (!iconType && fileType && typeof fileType === 'string' && fileType.trim()) {
128
+ const lower = fileType.toLowerCase().trim()
129
+ if (FILE_TYPE_MAP[lower]) iconType = FILE_TYPE_MAP[lower]
130
+ else if (SETI_CHAR_MAP[lower]) iconType = lower
131
+ }
132
+
133
+ return iconType || 'default'
134
+ }
135
+
136
+ export function getSetiIcon(fileType?: string, filename?: string): string {
137
+ const iconType = getSetiIconType(fileType, filename)
138
+ return SETI_CHAR_MAP[iconType] ?? SETI_CHAR_MAP.default
139
+ }
140
+
141
+ export function getSetiIconColor(fileType?: string, filename?: string): string {
142
+ const type = getSetiIconType(fileType, filename)
143
+ return SETI_COLOR_MAP[type] ?? SETI_COLOR_MAP.default
144
+ }
145
+
146
+ /** 返回 Tailwind token 类名,用于通用文件图标(非 Seti 字体) */
147
+ export function getFileIconColorClass(fileType?: string, filename?: string): string {
148
+ const type = getSetiIconType(fileType, filename)
149
+ return TYPE_TO_TOKEN_CLASS[type] ?? TYPE_TO_TOKEN_CLASS.default
150
+ }
@@ -0,0 +1,76 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ export interface UseMermaidRenderResult {
4
+ svg: string
5
+ error: string
6
+ }
7
+
8
+ /** 根据 data-style 取 Mermaid 节点圆角(px),与 scale.css 各风格对应 */
9
+ function getMermaidRadius(dataStyle: string): string {
10
+ const style = (dataStyle || 'neutral').toLowerCase()
11
+ if (style === 'soft') return '12px'
12
+ if (style === 'sharp') return '0'
13
+ if (style === 'dense') return '4px'
14
+ return '6px' // neutral / compact 等
15
+ }
16
+
17
+ /** 从 document 读取当前 data-style(仅在无传入 dataStyle 时使用) */
18
+ function getDataStyleFromDocument(): string {
19
+ if (typeof document === 'undefined') return 'neutral'
20
+ return (
21
+ document.documentElement.getAttribute('data-style') ||
22
+ document.body?.getAttribute('data-style') ||
23
+ 'neutral'
24
+ )
25
+ }
26
+
27
+ export function useMermaidRender(
28
+ content: string,
29
+ id: string,
30
+ dataStyle?: string
31
+ ): UseMermaidRenderResult {
32
+ const [svg, setSvg] = useState('')
33
+ const [error, setError] = useState('')
34
+ const style = dataStyle ?? getDataStyleFromDocument()
35
+ const radius = getMermaidRadius(style)
36
+
37
+ useEffect(() => {
38
+ let cancelled = false
39
+ const run = async () => {
40
+ setError('')
41
+ setSvg('')
42
+ if (!content) return
43
+ try {
44
+ const mod = await import('mermaid')
45
+ const mermaid = mod?.default ?? mod
46
+ const themeCSS = [
47
+ /* 矩形:直接设 rx/ry */
48
+ `.node rect { rx: ${radius}; ry: ${radius}; }`,
49
+ `.cluster rect { rx: ${radius}; ry: ${radius}; }`,
50
+ `.label rect { rx: ${radius}; ry: ${radius}; }`,
51
+ `.edgeLabel rect { rx: ${radius}; ry: ${radius}; }`,
52
+ /* 菱形、六边形等 polygon:描边圆角 */
53
+ `.node polygon { stroke-linejoin: round; stroke-linecap: round; }`,
54
+ `.node path { stroke-linejoin: round; stroke-linecap: round; }`,
55
+ `.cluster polygon { stroke-linejoin: round; stroke-linecap: round; }`,
56
+ `.cluster path { stroke-linejoin: round; stroke-linecap: round; }`,
57
+ ].join('\n')
58
+ mermaid.initialize({
59
+ startOnLoad: false,
60
+ securityLevel: 'strict',
61
+ theme: 'dark',
62
+ themeCSS,
63
+ })
64
+ const renderId = `mermaid-${id.replace(/:/g, '')}`
65
+ const { svg: nextSvg } = await mermaid.render(renderId, content)
66
+ if (!cancelled) setSvg(nextSvg ?? '')
67
+ } catch (e) {
68
+ if (!cancelled) setError((e as Error)?.message ?? 'Failed to render mermaid.')
69
+ }
70
+ }
71
+ run()
72
+ return () => { cancelled = true }
73
+ }, [id, content, radius])
74
+
75
+ return { svg, error }
76
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1 @@
1
+ {"shimmering-text":{"files":["basic/shimmering-text.tsx"],"dependencies":["clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"button":{"files":["basic/button.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"icon-button":{"files":["basic/icon-button.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"tooltip":{"files":["basic/tooltip.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-tooltip","class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"dropdown-menu":{"files":["basic/dropdown-menu.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-dropdown-menu","class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"alert-dialog":{"files":["basic/alert-dialog.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-alert-dialog","class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"progress":{"files":["basic/progress.tsx"],"dependencies":["@radix-ui/react-progress","clsx"],"peerDependencies":["react","react-dom"]},"skeleton":{"files":["basic/skeleton.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"kbd":{"files":["basic/kbd.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"tag":{"files":["basic/tag.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"tabs":{"files":["basic/tabs.tsx"],"dependencies":["@radix-ui/react-tabs","class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"switch":{"files":["basic/switch.tsx"],"dependencies":["@radix-ui/react-switch","class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"avatar":{"files":["basic/avatar.tsx"],"dependencies":["@radix-ui/react-avatar","class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"collapse":{"files":["basic/collapse.tsx","basic/icons-inline.tsx"],"dependencies":["@radix-ui/react-accordion","clsx"],"peerDependencies":["react","react-dom"]},"slider":{"files":["basic/slider.tsx"],"dependencies":["@radix-ui/react-slider","clsx"],"peerDependencies":["react","react-dom"]},"select":{"files":["basic/select.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-select","clsx"],"peerDependencies":["react","react-dom"]},"table":{"files":["basic/table.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"toggle":{"files":["basic/toggle.tsx"],"dependencies":["@radix-ui/react-toggle","@radix-ui/react-toggle-group","class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"radio-group":{"files":["basic/radio-group.tsx"],"dependencies":["@radix-ui/react-radio-group","class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"typography":{"files":["basic/typography.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"spinner":{"files":["basic/spinner.tsx","basic/icons-inline.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"pagination":{"files":["basic/pagination.tsx","basic/icons-inline.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"option-list":{"files":["basic/option-list.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"collapsible":{"files":["basic/collapsible.tsx"],"dependencies":["@radix-ui/react-collapsible","clsx"],"peerDependencies":["react","react-dom"]},"resizable":{"files":["basic/resizable.tsx"],"dependencies":["react-resizable-panels","clsx"],"peerDependencies":["react","react-dom"]},"scrollbar":{"files":["basic/scrollbar.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"sonner":{"files":["basic/sonner.tsx","basic/theme-from-document.ts"],"dependencies":["sonner"],"peerDependencies":["react","react-dom"]},"send-button":{"files":["chat/send-button.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"folder-button":{"files":["chat/folder-button.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"file-attachment":{"files":["chat/file-attachment.tsx","basic/tag.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"image-attachment":{"files":["chat/image-attachment.tsx","basic/icon-button.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"file-card":{"files":["chat/file-card.tsx","basic/icons-inline.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"request":{"files":["chat/request.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"related-prompts":{"files":["chat/related-prompts.tsx","basic/tooltip.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-tooltip","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"permission-card":{"files":["chat/permission-card.tsx","basic/button.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"suggestion-part":{"files":["chat/suggestion-part.tsx","basic/button.tsx","basic/dropdown-menu.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-dropdown-menu","class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"generation-status-bar":{"files":["chat/generation-status-bar.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"queue-indicator":{"files":["chat/queue-indicator.tsx","basic/tooltip.tsx","basic/icon-button.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-tooltip","class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"tool-invocation-card":{"files":["chat/tool-invocation-card.tsx","basic/icons-inline.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"reasoning-step":{"files":["chat/reasoning-step.tsx","basic/icons-inline.tsx"],"dependencies":["clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"sidebar-menu":{"files":["basic/sidebar-menu.tsx","basic/dropdown-menu.tsx","basic/icons-inline.tsx","basic/theme-from-document.ts"],"dependencies":["@radix-ui/react-dropdown-menu","class-variance-authority","clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"user-question-answer":{"files":["chat/user-question-answer.tsx","basic/icons-inline.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"generated-images-grid":{"files":["chat/generated-images-grid.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"thinking-indicator":{"files":["chat/thinking-indicator.tsx"],"dependencies":["clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"image-generating":{"files":["chat/image-generating.tsx","basic/icons-inline.tsx"],"dependencies":["clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"markdown":{"files":["chat/markdown.tsx"],"dependencies":["react-markdown","remark-gfm","remark-math","rehype-katex","clsx"],"peerDependencies":["react","react-dom"]},"streaming-markdown-block":{"files":["chat/streaming-markdown-block.tsx","chat/markdown.tsx"],"dependencies":["clsx"],"peerDependencies":["react","react-dom"]},"response":{"files":["chat/response.tsx"],"dependencies":["clsx","framer-motion"],"peerDependencies":["react","react-dom"]},"chat-input":{"files":["chat/chat-input/index.tsx","chat/chat-input/types.ts","chat/chat-input/useAutoResizeTextarea.ts","chat/chat-input/chat-input-textarea.tsx","chat/chat-input/chat-input-folder-selector.tsx","chat/chat-input/chat-input-model-switcher.tsx","chat/chat-input/folder-permission-dialog.tsx","chat/send-button.tsx","chat/folder-button.tsx","chat/generation-status-bar.tsx","basic/icon-button.tsx","basic/dropdown-menu.tsx","basic/alert-dialog.tsx","basic/icons-inline.tsx"],"dependencies":["class-variance-authority","clsx"],"peerDependencies":["react","react-dom"]},"user-question":{"files":["chat/user-question.tsx","basic/button.tsx","basic/icon-button.tsx","basic/option-list.tsx","basic/kbd.tsx","basic/icons-inline.tsx"],"dependencies":["clsx","framer-motion"],"peerDependencies":["react","react-dom"]}}
@@ -0,0 +1,31 @@
1
+ /* ============================================
2
+ * Token 系统统一入口 (CLI 分发版)
3
+ *
4
+ * 引入方式:
5
+ * 1. 全量引入(默认): @import "qoder-tokens.css";
6
+ * 2. 按需引入:
7
+ * @import "./scale/index.css"; // 布局风格
8
+ * @import "./themes/light-qoder.css"; // 亮色主题
9
+ * @import "./themes/dark-qoder.css"; // 暗色主题
10
+ *
11
+ * 自定义主题:
12
+ * @import "./scale/index.css";
13
+ * @import "./my-custom-theme.css"; // 定义 [data-theme="xxx"] { --token-color-*: ... }
14
+ *
15
+ * 自定义布局配置:
16
+ * @import "./scale/config.css";
17
+ * @import "./scale/computed.css";
18
+ * :root { --config-radius-default: 10px; } // 覆盖配置
19
+ * ============================================ */
20
+
21
+ /* 布局风格 Token (data-style: neutral | compact | soft | sharp | dense) */
22
+ @import "./scale.css";
23
+
24
+ /* 内置主题 (data-theme) */
25
+ @import "./themes/light-qoder.css";
26
+ @import "./themes/dark-qoder.css";
27
+ @import "./themes/light-parchment.css";
28
+ @import "./themes/dark-parchment.css";
29
+
30
+ /* 滚动条工具样式 */
31
+ @import "./scrollbar-utility.css";