sparkdesign 0.3.0 → 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
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Spark Design
2
2
 
3
- 现代化 React 设计系统,支持双维度主题(颜色 theme + 布局 scale),5 种内置布局风格,提供 Foundation 基础组件与 Chat 对话流组件,可扩展颜色主题。
3
+ 现代化 React 设计系统,支持双维度主题(颜色 theme + 布局 scale),5 种内置布局风格,提供 Basic 基础组件与 Chat 对话流组件,可扩展颜色主题。
4
4
 
5
- **当前版本**:`0.2.4`([变更说明](docs/NPM发布指南.md))
5
+ **当前版本**:`0.3.1`([变更说明](docs/NPM发布指南.md))
6
+
7
+ > 🌟 **纯新手?** 查看 [快速上手指南-新手版](docs/快速上手指南-新手版.md),一步步带你跑起来!
6
8
 
7
9
  > 以下为**安装与使用说明**;仓库结构、开发脚本与贡献方式见文末 [仓库与开发](#仓库与开发)。
8
10
 
@@ -13,7 +15,7 @@
13
15
  - **内置颜色主题**:light / dark
14
16
  - **CSS 变量驱动**:组件仅用 token,覆盖变量即可定制
15
17
  - **Tailwind 友好**:类名与设计 token 映射,无硬编码色值
16
- - **组件分层**:Foundation 原子组件 + Chat 对话组件,开箱即用
18
+ - **组件分层**:Basic 原子组件 + Chat 对话组件,开箱即用
17
19
 
18
20
  ## 两种使用方式
19
21
 
@@ -224,7 +226,7 @@ function App() {
224
226
 
225
227
  | 分层 | 说明 | 示例 |
226
228
  |------|------|------|
227
- | **Foundation** | 原子级 UI 组件 | Button, IconButton, Tooltip, Select, DropdownMenu, Tabs, Toast, Tag, Progress, Avatar, Table, Slider, Pagination, Collapse, Resizable, Scrollbar, Skeleton, Spinner, Kbd, EllipsisText, Switch, Toggle, ToggleGroup, RadioGroup, AlertDialog, OptionList … |
229
+ | **Basic** | 原子级 UI 组件 | Button, IconButton, Tooltip, Select, DropdownMenu, Tabs, Toast, Tag, Progress, Avatar, Table, Slider, Pagination, Collapse, Resizable, Scrollbar, Skeleton, Spinner, Kbd, EllipsisText, Switch, Toggle, ToggleGroup, RadioGroup, AlertDialog, OptionList … |
228
230
  | **Chat** | 对话流相关组件 | ChatInput, SendButton, Request, Response, FileCard, FileAttachment, ImageAttachment, FolderButton, ReasoningStep, ToolInvocationCard, PermissionCard, MarkdownBody, GenerationStatusBar, ThinkingIndicator, ImageGenerating, RelatedPrompts, SuggestionPart, SidebarMenu … |
229
231
 
230
232
  完整导出见 [src/components/index.ts](src/components/index.ts)。图标通过 `IconsProvider` / `useIcon` 注入,可替换为 Lucide、Remix 等,见 [图标自定义说明](docs/图标自定义说明.md)。
@@ -269,7 +271,7 @@ src/
269
271
  ├── index.css # dev 模式 CSS 入口
270
272
  ├── lib.css # 库构建 CSS 入口(@tailwindcss/cli)
271
273
  ├── tokens/ # theme.css、scale.css
272
- ├── components/ # foundation/、chat/
274
+ ├── components/ # basic/、chat/
273
275
  └── lib/ # 工具与 ThemeStyleProvider
274
276
  ```
275
277
 
package/cli/dist/index.js CHANGED
File without changes
@@ -10,6 +10,41 @@
10
10
  import path from 'node:path';
11
11
  import fs from 'fs-extra';
12
12
  const TOKEN_MARKER = 'Spark Design tokens';
13
+ /** 辅助函数:读取文件,不存在则返回空字符串 */
14
+ async function readFileIfExists(filePath) {
15
+ if (await fs.pathExists(filePath)) {
16
+ return fs.readFile(filePath, 'utf-8');
17
+ }
18
+ return '';
19
+ }
20
+ /** 辅助函数:读取 scale 目录下的所有文件并内联 */
21
+ async function readScaleContents(registryRoot) {
22
+ const scaleDir = path.join(registryRoot, 'tokens', 'scale');
23
+ const scaleLegacy = path.join(registryRoot, 'tokens', 'scale.css');
24
+ // 新结构:读取 scale/ 目录
25
+ if (await fs.pathExists(scaleDir)) {
26
+ const configContent = await readFileIfExists(path.join(scaleDir, 'config.css'));
27
+ const computedContent = await readFileIfExists(path.join(scaleDir, 'computed.css'));
28
+ const presetsDir = path.join(scaleDir, 'presets');
29
+ const compactContent = await readFileIfExists(path.join(presetsDir, 'compact.css'));
30
+ const softContent = await readFileIfExists(path.join(presetsDir, 'soft.css'));
31
+ const sharpContent = await readFileIfExists(path.join(presetsDir, 'sharp.css'));
32
+ const denseContent = await readFileIfExists(path.join(presetsDir, 'dense.css'));
33
+ return [configContent, computedContent, compactContent, softContent, sharpContent, denseContent]
34
+ .filter(Boolean)
35
+ .join('\n\n');
36
+ }
37
+ // 兼容旧结构:读取单文件 scale.css
38
+ if (await fs.pathExists(scaleLegacy)) {
39
+ const content = await fs.readFile(scaleLegacy, 'utf-8');
40
+ // 如果是 @import 入口,跳过
41
+ if (content.includes('@import "./scale/')) {
42
+ return '';
43
+ }
44
+ return content;
45
+ }
46
+ return '';
47
+ }
13
48
  /** 项目里是否已有 token(入口 CSS 内联了变量、或已 import qoder-tokens.css) */
14
49
  export async function projectHasTokens(cwd) {
15
50
  const candidates = [
@@ -30,12 +65,19 @@ export async function projectHasTokens(cwd) {
30
65
  }
31
66
  return false;
32
67
  }
33
- /** 仅 init 使用:把 theme+scale+scrollbar-utility 注入到用户入口 CSS(globals.css / index.css) */
68
+ /** 仅 init 使用:把 tokens 注入到用户入口 CSS(globals.css / index.css) */
34
69
  export async function ensureDesignTokens(cwd, registryRoot) {
70
+ // 新结构:使用 index.css 统一入口
71
+ const indexPath = path.join(registryRoot, 'tokens', 'index.css');
72
+ // 兼容旧结构
35
73
  const themePath = path.join(registryRoot, 'tokens', 'theme.css');
36
74
  const scalePath = path.join(registryRoot, 'tokens', 'scale.css');
37
75
  const scrollbarPath = path.join(registryRoot, 'tokens', 'scrollbar-utility.css');
38
- if (!(await fs.pathExists(themePath)) || !(await fs.pathExists(scalePath))) {
76
+ const themesDir = path.join(registryRoot, 'tokens', 'themes');
77
+ // 检查是否有可用的 tokens 文件
78
+ const hasNewStructure = await fs.pathExists(indexPath);
79
+ const hasLegacyStructure = (await fs.pathExists(themePath)) && (await fs.pathExists(scalePath));
80
+ if (!hasNewStructure && !hasLegacyStructure) {
39
81
  return null;
40
82
  }
41
83
  if (await projectHasTokens(cwd)) {
@@ -49,13 +91,34 @@ export async function ensureDesignTokens(cwd, registryRoot) {
49
91
  cssEntry = path.join(cwd, 'src', 'index.css');
50
92
  await fs.ensureDir(path.dirname(cssEntry));
51
93
  }
52
- const themeContent = await fs.readFile(themePath, 'utf-8');
53
- const scaleContent = await fs.readFile(scalePath, 'utf-8');
54
- const scrollbarContent = (await fs.pathExists(scrollbarPath))
55
- ? await fs.readFile(scrollbarPath, 'utf-8')
56
- : '';
57
- const tokensBlock = `/* ${TOKEN_MARKER} - 由 npx sparkdesign init 写入,驱动组件样式 */\n${scaleContent}\n\n${themeContent}\n` +
58
- (scrollbarContent ? `\n${scrollbarContent}\n` : '');
94
+ let tokensBlock;
95
+ if (hasNewStructure && (await fs.pathExists(themesDir))) {
96
+ // 新结构:读取拆分的主题文件并内联
97
+ const scaleContent = await readScaleContents(registryRoot);
98
+ const lightContent = await readFileIfExists(path.join(themesDir, 'light-qoder.css'));
99
+ const darkContent = await readFileIfExists(path.join(themesDir, 'dark-qoder.css'));
100
+ const lightParchmentContent = await readFileIfExists(path.join(themesDir, 'light-parchment.css'));
101
+ const darkParchmentContent = await readFileIfExists(path.join(themesDir, 'dark-parchment.css'));
102
+ const scrollbarContent = await readFileIfExists(scrollbarPath);
103
+ tokensBlock = `/* ${TOKEN_MARKER} - 由 npx sparkdesign init 写入,驱动组件样式 */\n` +
104
+ `${scaleContent}\n\n` +
105
+ `${lightContent}\n\n` +
106
+ `${darkContent}\n\n` +
107
+ `${lightParchmentContent}\n\n` +
108
+ `${darkParchmentContent}\n` +
109
+ (scrollbarContent ? `\n${scrollbarContent}\n` : '');
110
+ }
111
+ else {
112
+ // 兼容旧结构
113
+ const themeContent = await fs.readFile(themePath, 'utf-8');
114
+ const scaleContent = await fs.readFile(scalePath, 'utf-8');
115
+ const scrollbarContent = (await fs.pathExists(scrollbarPath))
116
+ ? await fs.readFile(scrollbarPath, 'utf-8')
117
+ : '';
118
+ tokensBlock =
119
+ `/* ${TOKEN_MARKER} - 由 npx sparkdesign init 写入,驱动组件样式 */\n${scaleContent}\n\n${themeContent}\n` +
120
+ (scrollbarContent ? `\n${scrollbarContent}\n` : '');
121
+ }
59
122
  const existing = (await fs.pathExists(cssEntry)) ? await fs.readFile(cssEntry, 'utf-8') : '';
60
123
  const newContent = tokensBlock + (existing ? '\n' + existing : '');
61
124
  await fs.writeFile(cssEntry, newContent, 'utf-8');
@@ -64,23 +127,46 @@ export async function ensureDesignTokens(cwd, registryRoot) {
64
127
  const DEDICATED_TOKENS_FILE = 'qoder-tokens.css';
65
128
  /** 仅 add 使用:在 src 下生成独立 token 文件,不修改用户入口。若已存在则不覆盖。返回路径及是否本次创建。 */
66
129
  export async function writeQoderTokensFile(cwd, registryRoot) {
130
+ const indexPath = path.join(registryRoot, 'tokens', 'index.css');
67
131
  const themePath = path.join(registryRoot, 'tokens', 'theme.css');
68
132
  const scalePath = path.join(registryRoot, 'tokens', 'scale.css');
69
133
  const scrollbarPath = path.join(registryRoot, 'tokens', 'scrollbar-utility.css');
134
+ const themesDir = path.join(registryRoot, 'tokens', 'themes');
70
135
  const targetPath = path.join(cwd, 'src', DEDICATED_TOKENS_FILE);
71
136
  if (await fs.pathExists(targetPath)) {
72
137
  return { path: targetPath, created: false };
73
138
  }
74
- if (!(await fs.pathExists(themePath)) || !(await fs.pathExists(scalePath))) {
139
+ const hasNewStructure = await fs.pathExists(indexPath);
140
+ const hasLegacyStructure = (await fs.pathExists(themePath)) && (await fs.pathExists(scalePath));
141
+ if (!hasNewStructure && !hasLegacyStructure) {
75
142
  return { path: targetPath, created: false };
76
143
  }
77
- const themeContent = await fs.readFile(themePath, 'utf-8');
78
- const scaleContent = await fs.readFile(scalePath, 'utf-8');
79
- const scrollbarContent = (await fs.pathExists(scrollbarPath))
80
- ? await fs.readFile(scrollbarPath, 'utf-8')
81
- : '';
82
- const content = `/* ${TOKEN_MARKER} - 在应用入口 import 此文件以驱动组件样式 */\n${scaleContent}\n\n${themeContent}\n` +
83
- (scrollbarContent ? `\n${scrollbarContent}\n` : '');
144
+ let content;
145
+ if (hasNewStructure && (await fs.pathExists(themesDir))) {
146
+ // 新结构:读取拆分的主题文件并内联
147
+ const scaleContent = await readScaleContents(registryRoot);
148
+ const lightContent = await readFileIfExists(path.join(themesDir, 'light-qoder.css'));
149
+ const darkContent = await readFileIfExists(path.join(themesDir, 'dark-qoder.css'));
150
+ const lightParchmentContent = await readFileIfExists(path.join(themesDir, 'light-parchment.css'));
151
+ const darkParchmentContent = await readFileIfExists(path.join(themesDir, 'dark-parchment.css'));
152
+ const scrollbarContent = await readFileIfExists(scrollbarPath);
153
+ content = `/* ${TOKEN_MARKER} - 在应用入口 import 此文件以驱动组件样式 */\n` +
154
+ `${scaleContent}\n\n` +
155
+ `${lightContent}\n\n` +
156
+ `${darkContent}\n\n` +
157
+ `${lightParchmentContent}\n\n` +
158
+ `${darkParchmentContent}\n` +
159
+ (scrollbarContent ? `\n${scrollbarContent}\n` : '');
160
+ }
161
+ else {
162
+ // 兼容旧结构
163
+ const themeContent = await fs.readFile(themePath, 'utf-8');
164
+ const scaleContent = await fs.readFile(scalePath, 'utf-8');
165
+ const scrollbarContent = await readFileIfExists(scrollbarPath);
166
+ content =
167
+ `/* ${TOKEN_MARKER} - 在应用入口 import 此文件以驱动组件样式 */\n${scaleContent}\n\n${themeContent}\n` +
168
+ (scrollbarContent ? `\n${scrollbarContent}\n` : '');
169
+ }
84
170
  await fs.ensureDir(path.dirname(targetPath));
85
171
  await fs.writeFile(targetPath, content, 'utf-8');
86
172
  return { path: targetPath, created: true };
@@ -0,0 +1,333 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { Button } from './button'
4
+ import { createRef } from 'react'
5
+
6
+ describe('Button', () => {
7
+ // ============================================================
8
+ // 渲染测试
9
+ // ============================================================
10
+ describe('渲染', () => {
11
+ it('应正确渲染 children 文本', () => {
12
+ render(<Button>点击我</Button>)
13
+ expect(screen.getByRole('button')).toHaveTextContent('点击我')
14
+ })
15
+
16
+ it('应渲染为原生 button 元素', () => {
17
+ render(<Button>按钮</Button>)
18
+ expect(screen.getByRole('button').tagName).toBe('BUTTON')
19
+ })
20
+
21
+ it('应设置正确的 displayName', () => {
22
+ expect(Button.displayName).toBe('Button')
23
+ })
24
+ })
25
+
26
+ // ============================================================
27
+ // Variant 变体测试
28
+ // ============================================================
29
+ describe('variant 变体', () => {
30
+ it('默认应使用 ghost 变体', () => {
31
+ render(<Button>Ghost</Button>)
32
+ expect(screen.getByRole('button')).toHaveClass('bg-transparent')
33
+ })
34
+
35
+ it('variant="primary" 应应用主色样式', () => {
36
+ render(<Button variant="primary">Primary</Button>)
37
+ expect(screen.getByRole('button')).toHaveClass('bg-primary')
38
+ })
39
+
40
+ it('variant="secondary" 应应用次级样式', () => {
41
+ render(<Button variant="secondary">Secondary</Button>)
42
+ expect(screen.getByRole('button')).toHaveClass('bg-bg-highlight')
43
+ })
44
+
45
+ it('variant="tertiary" 应应用三级样式', () => {
46
+ render(<Button variant="tertiary">Tertiary</Button>)
47
+ expect(screen.getByRole('button')).toHaveClass('bg-fill-secondary')
48
+ })
49
+
50
+ it('variant="text" 应应用纯文字样式', () => {
51
+ render(<Button variant="text">Text</Button>)
52
+ expect(screen.getByRole('button')).toHaveClass('bg-transparent')
53
+ expect(screen.getByRole('button')).toHaveClass('text-text-secondary')
54
+ })
55
+ })
56
+
57
+ // ============================================================
58
+ // Size 尺寸测试
59
+ // ============================================================
60
+ describe('size 尺寸', () => {
61
+ it('默认应使用 md 尺寸', () => {
62
+ render(<Button>Medium</Button>)
63
+ expect(screen.getByRole('button')).toHaveClass('h-9')
64
+ })
65
+
66
+ it('size="sm" 应应用小尺寸样式', () => {
67
+ render(<Button size="sm">Small</Button>)
68
+ expect(screen.getByRole('button')).toHaveClass('h-7')
69
+ expect(screen.getByRole('button')).toHaveClass('text-xs')
70
+ })
71
+
72
+ it('size="md" 应应用中等尺寸样式', () => {
73
+ render(<Button size="md">Medium</Button>)
74
+ expect(screen.getByRole('button')).toHaveClass('h-9')
75
+ expect(screen.getByRole('button')).toHaveClass('text-sm')
76
+ })
77
+
78
+ it('size="lg" 应应用大尺寸样式', () => {
79
+ render(<Button size="lg">Large</Button>)
80
+ expect(screen.getByRole('button')).toHaveClass('h-11')
81
+ expect(screen.getByRole('button')).toHaveClass('text-base')
82
+ })
83
+ })
84
+
85
+ // ============================================================
86
+ // Rounded 圆角测试
87
+ // ============================================================
88
+ describe('rounded 圆角', () => {
89
+ it('默认应使用 square 圆角', () => {
90
+ render(<Button>Square</Button>)
91
+ expect(screen.getByRole('button')).toHaveClass('rounded')
92
+ expect(screen.getByRole('button')).not.toHaveClass('rounded-full')
93
+ })
94
+
95
+ it('rounded="pill" 应应用全圆角样式', () => {
96
+ render(<Button rounded="pill">Pill</Button>)
97
+ expect(screen.getByRole('button')).toHaveClass('rounded-full')
98
+ })
99
+ })
100
+
101
+ // ============================================================
102
+ // textButton 模式测试
103
+ // ============================================================
104
+ describe('textButton 模式', () => {
105
+ it('textButton=true 应移除高度和内边距', () => {
106
+ render(<Button textButton>Text Only</Button>)
107
+ const button = screen.getByRole('button')
108
+ expect(button).toHaveClass('h-auto')
109
+ expect(button).toHaveClass('px-0')
110
+ })
111
+
112
+ it('textButton=true 应强制使用 text 变体样式', () => {
113
+ render(<Button textButton variant="primary">Text</Button>)
114
+ const button = screen.getByRole('button')
115
+ // 即使传入 primary,textButton 也会覆盖为 text 变体
116
+ expect(button).toHaveClass('bg-transparent')
117
+ })
118
+
119
+ it('textButton 应保留正确的字体大小', () => {
120
+ const { rerender } = render(<Button textButton size="sm">SM</Button>)
121
+ expect(screen.getByRole('button')).toHaveClass('text-xs')
122
+
123
+ rerender(<Button textButton size="md">MD</Button>)
124
+ expect(screen.getByRole('button')).toHaveClass('text-sm')
125
+
126
+ rerender(<Button textButton size="lg">LG</Button>)
127
+ expect(screen.getByRole('button')).toHaveClass('text-base')
128
+ })
129
+ })
130
+
131
+ // ============================================================
132
+ // 图标测试
133
+ // ============================================================
134
+ describe('图标', () => {
135
+ it('应渲染 prefixIcon', () => {
136
+ render(
137
+ <Button prefixIcon={<span data-testid="prefix">←</span>}>
138
+ 有前置图标
139
+ </Button>
140
+ )
141
+ expect(screen.getByTestId('prefix')).toBeInTheDocument()
142
+ expect(screen.getByTestId('prefix')).toHaveTextContent('←')
143
+ })
144
+
145
+ it('应渲染 suffixIcon', () => {
146
+ render(
147
+ <Button suffixIcon={<span data-testid="suffix">→</span>}>
148
+ 有后置图标
149
+ </Button>
150
+ )
151
+ expect(screen.getByTestId('suffix')).toBeInTheDocument()
152
+ expect(screen.getByTestId('suffix')).toHaveTextContent('→')
153
+ })
154
+
155
+ it('应同时渲染前后置图标', () => {
156
+ render(
157
+ <Button
158
+ prefixIcon={<span data-testid="prefix">←</span>}
159
+ suffixIcon={<span data-testid="suffix">→</span>}
160
+ >
161
+ 双图标
162
+ </Button>
163
+ )
164
+ expect(screen.getByTestId('prefix')).toBeInTheDocument()
165
+ expect(screen.getByTestId('suffix')).toBeInTheDocument()
166
+ })
167
+
168
+ it('无图标时不应渲染图标包裹元素', () => {
169
+ const { container } = render(<Button>无图标</Button>)
170
+ // 检查没有图标包裹的 span 元素
171
+ const iconWrappers = container.querySelectorAll('button > span.inline-flex')
172
+ expect(iconWrappers).toHaveLength(0)
173
+ })
174
+ })
175
+
176
+ // ============================================================
177
+ // 交互测试
178
+ // ============================================================
179
+ describe('交互', () => {
180
+ it('点击时应触发 onClick 回调', () => {
181
+ const handleClick = vi.fn()
182
+ render(<Button onClick={handleClick}>点击</Button>)
183
+
184
+ fireEvent.click(screen.getByRole('button'))
185
+ expect(handleClick).toHaveBeenCalledTimes(1)
186
+ })
187
+
188
+ it('多次点击应触发相应次数的回调', () => {
189
+ const handleClick = vi.fn()
190
+ render(<Button onClick={handleClick}>点击</Button>)
191
+
192
+ const button = screen.getByRole('button')
193
+ fireEvent.click(button)
194
+ fireEvent.click(button)
195
+ fireEvent.click(button)
196
+
197
+ expect(handleClick).toHaveBeenCalledTimes(3)
198
+ })
199
+ })
200
+
201
+ // ============================================================
202
+ // Disabled 状态测试
203
+ // ============================================================
204
+ describe('disabled 状态', () => {
205
+ it('disabled=true 应设置 disabled 属性', () => {
206
+ render(<Button disabled>禁用</Button>)
207
+ expect(screen.getByRole('button')).toBeDisabled()
208
+ })
209
+
210
+ it('disabled 时应应用禁用样式', () => {
211
+ render(<Button disabled>禁用</Button>)
212
+ expect(screen.getByRole('button')).toHaveClass('disabled:opacity-50')
213
+ expect(screen.getByRole('button')).toHaveClass('disabled:cursor-not-allowed')
214
+ })
215
+
216
+ it('disabled 时点击不应触发 onClick', () => {
217
+ const handleClick = vi.fn()
218
+ render(<Button disabled onClick={handleClick}>禁用</Button>)
219
+
220
+ fireEvent.click(screen.getByRole('button'))
221
+ expect(handleClick).not.toHaveBeenCalled()
222
+ })
223
+
224
+ it('默认不应禁用', () => {
225
+ render(<Button>正常</Button>)
226
+ expect(screen.getByRole('button')).not.toBeDisabled()
227
+ })
228
+ })
229
+
230
+ // ============================================================
231
+ // Ref 转发测试
232
+ // ============================================================
233
+ describe('ref 转发', () => {
234
+ it('应正确转发 ref 到 button 元素', () => {
235
+ const ref = createRef<HTMLButtonElement>()
236
+ render(<Button ref={ref}>带 Ref</Button>)
237
+
238
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement)
239
+ })
240
+
241
+ it('ref.current 应指向渲染的 button', () => {
242
+ const ref = createRef<HTMLButtonElement>()
243
+ render(<Button ref={ref}>带 Ref</Button>)
244
+
245
+ expect(ref.current).toBe(screen.getByRole('button'))
246
+ })
247
+
248
+ it('callback ref 应被正确调用', () => {
249
+ const callbackRef = vi.fn()
250
+ render(<Button ref={callbackRef}>Callback Ref</Button>)
251
+
252
+ expect(callbackRef).toHaveBeenCalled()
253
+ expect(callbackRef.mock.calls[0][0]).toBeInstanceOf(HTMLButtonElement)
254
+ })
255
+ })
256
+
257
+ // ============================================================
258
+ // className 覆盖测试
259
+ // ============================================================
260
+ describe('className 覆盖', () => {
261
+ it('应合并自定义 className', () => {
262
+ render(<Button className="custom-class">自定义类</Button>)
263
+ expect(screen.getByRole('button')).toHaveClass('custom-class')
264
+ })
265
+
266
+ it('自定义 className 应与默认类名共存', () => {
267
+ render(<Button className="my-custom" variant="primary">合并类</Button>)
268
+ const button = screen.getByRole('button')
269
+ expect(button).toHaveClass('my-custom')
270
+ expect(button).toHaveClass('bg-primary')
271
+ })
272
+ })
273
+
274
+ // ============================================================
275
+ // 原生属性透传测试
276
+ // ============================================================
277
+ describe('原生属性透传', () => {
278
+ it('应透传 type 属性', () => {
279
+ render(<Button type="submit">提交</Button>)
280
+ expect(screen.getByRole('button')).toHaveAttribute('type', 'submit')
281
+ })
282
+
283
+ it('应透传 aria-label 属性', () => {
284
+ render(<Button aria-label="关闭按钮">×</Button>)
285
+ expect(screen.getByRole('button')).toHaveAttribute('aria-label', '关闭按钮')
286
+ })
287
+
288
+ it('应透传 data-* 属性', () => {
289
+ render(<Button data-testid="my-button">测试</Button>)
290
+ expect(screen.getByTestId('my-button')).toBeInTheDocument()
291
+ })
292
+
293
+ it('应透传 id 属性', () => {
294
+ render(<Button id="unique-button">ID 按钮</Button>)
295
+ expect(screen.getByRole('button')).toHaveAttribute('id', 'unique-button')
296
+ })
297
+ })
298
+
299
+ // ============================================================
300
+ // 组合场景测试
301
+ // ============================================================
302
+ describe('组合场景', () => {
303
+ it('应支持所有 props 组合', () => {
304
+ const handleClick = vi.fn()
305
+ render(
306
+ <Button
307
+ variant="primary"
308
+ size="lg"
309
+ rounded="pill"
310
+ prefixIcon={<span>🚀</span>}
311
+ suffixIcon={<span>✨</span>}
312
+ onClick={handleClick}
313
+ className="extra-class"
314
+ data-testid="combo-button"
315
+ >
316
+ 组合按钮
317
+ </Button>
318
+ )
319
+
320
+ const button = screen.getByTestId('combo-button')
321
+ expect(button).toHaveClass('bg-primary')
322
+ expect(button).toHaveClass('h-11')
323
+ expect(button).toHaveClass('rounded-full')
324
+ expect(button).toHaveClass('extra-class')
325
+ expect(button).toHaveTextContent('🚀')
326
+ expect(button).toHaveTextContent('组合按钮')
327
+ expect(button).toHaveTextContent('✨')
328
+
329
+ fireEvent.click(button)
330
+ expect(handleClick).toHaveBeenCalled()
331
+ })
332
+ })
333
+ })
@@ -5,7 +5,7 @@ import { QuestionnaireLine, ArrowDownSLine } from '../basic/icons-inline'
5
5
 
6
6
  const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0 text-text-secondary'
7
7
 
8
- export interface QuestionPartProps {
8
+ export interface AskUserPartProps {
9
9
  title?: string
10
10
  question: string
11
11
  skipText?: string
@@ -22,7 +22,7 @@ export interface QuestionPartProps {
22
22
 
23
23
  const defaultHeaderIcon = <QuestionnaireLine className={ICON_CLASS} />
24
24
 
25
- export function QuestionPart({
25
+ export function AskUserPart({
26
26
  title = 'Initializing agent…',
27
27
  question,
28
28
  skipText = 'Skip ⌘⌫',
@@ -34,7 +34,7 @@ export function QuestionPart({
34
34
  collapsible = true,
35
35
  className,
36
36
  headerIcon,
37
- }: QuestionPartProps) {
37
+ }: AskUserPartProps) {
38
38
  return (
39
39
  <CollapsibleCard
40
40
  headerIcon={headerIcon ?? defaultHeaderIcon}
@@ -67,4 +67,4 @@ export function QuestionPart({
67
67
  )
68
68
  }
69
69
 
70
- QuestionPart.displayName = 'QuestionPart'
70
+ AskUserPart.displayName = 'AskUserPart'
@@ -12,12 +12,12 @@ import { GlobalLine, WarningLine, Forbid2Line } from '../basic/icons-inline'
12
12
 
13
13
  const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0'
14
14
 
15
- export type BrowserUseStatus = 'normal' | 'failed' | 'skipped'
15
+ export type BrowserActionStatus = 'normal' | 'failed' | 'skipped'
16
16
 
17
- export interface BrowserUsePartProps {
17
+ export interface BrowserActionPartProps {
18
18
  description?: string
19
19
  content?: string
20
- status?: BrowserUseStatus
20
+ status?: BrowserActionStatus
21
21
  onRun?: () => void
22
22
  onSkip?: () => void
23
23
  onViewDetail?: () => void
@@ -83,7 +83,7 @@ function BrowserFooter({
83
83
  )
84
84
  }
85
85
 
86
- export function BrowserUsePart({
86
+ export function BrowserActionPart({
87
87
  description,
88
88
  content,
89
89
  status = 'normal',
@@ -97,7 +97,7 @@ export function BrowserUsePart({
97
97
  normalIcon,
98
98
  failedIcon,
99
99
  skippedIcon,
100
- }: BrowserUsePartProps) {
100
+ }: BrowserActionPartProps) {
101
101
  const [askValue, setAskValue] = useState('Ask every time')
102
102
 
103
103
  const handleAskChange = (value: string) => {
@@ -163,4 +163,4 @@ export function BrowserUsePart({
163
163
  )
164
164
  }
165
165
 
166
- BrowserUsePart.displayName = 'BrowserUsePart'
166
+ BrowserActionPart.displayName = 'BrowserActionPart'
@@ -10,7 +10,7 @@ import {
10
10
  import { CloseLine, ArrowDownSLine, WarningLine, SparklingLine } from '../basic/icons-inline'
11
11
  import { Scrollbar } from '../basic/scrollbar'
12
12
 
13
- export interface SuggestionPartProps {
13
+ export interface HintBannerProps {
14
14
  type?: 'wiki' | 'credits'
15
15
  title?: string
16
16
  description?: string
@@ -36,7 +36,7 @@ const closeClass = 'h-3.5 w-3.5'
36
36
  const arrowClass = 'h-3 w-3'
37
37
  const suggestionClass = 'h-4 w-4'
38
38
 
39
- export function SuggestionPart({
39
+ export function HintBanner({
40
40
  type = 'wiki',
41
41
  title,
42
42
  description,
@@ -52,7 +52,7 @@ export function SuggestionPart({
52
52
  suggestionIcon,
53
53
  dataStyle,
54
54
  dataTheme,
55
- }: SuggestionPartProps) {
55
+ }: HintBannerProps) {
56
56
  const isCreditsType = type === 'credits'
57
57
  const defaultClose = closeIcon ?? <CloseLine className={closeClass} />
58
58
  const defaultArrow = arrowDownSIcon ?? <ArrowDownSLine className={arrowClass} />
@@ -162,4 +162,4 @@ export function SuggestionPart({
162
162
  )
163
163
  }
164
164
 
165
- SuggestionPart.displayName = 'SuggestionPart'
165
+ HintBanner.displayName = 'HintBanner'