sparkdesign 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/cli/dist/index.js +0 -0
- package/cli/dist/utils/tokens.js +103 -17
- package/cli/registry/basic/button.test.tsx +333 -0
- package/cli/registry/chat/{question-part.tsx → ask-user-part.tsx} +4 -4
- package/cli/registry/chat/{browser-use-part.tsx → browser-action-part.tsx} +6 -6
- package/cli/registry/chat/{suggestion-part.tsx → hint-banner.tsx} +4 -4
- package/cli/registry/chat/markdown.test.tsx +387 -0
- package/cli/registry/chat/{reasoning-step.tsx → reasoning-step/compound.tsx} +163 -185
- package/cli/registry/chat/reasoning-step/context.tsx +114 -0
- package/cli/registry/chat/reasoning-step/index.tsx +45 -0
- package/cli/registry/chat/reasoning-step/types.ts +109 -0
- package/cli/registry/chat/response/compound.tsx +210 -0
- package/cli/registry/chat/{response.tsx → response/context.tsx} +65 -136
- package/cli/registry/chat/response/index.tsx +87 -0
- package/cli/registry/chat/response/types.ts +123 -0
- package/cli/registry/chat/thinking-indicator.test.tsx +244 -0
- package/cli/registry/chat/tool-invocation-card.test.tsx +346 -0
- package/cli/registry/chat/{request.tsx → user-message.tsx} +3 -3
- package/cli/registry/chat/user-question/compound.tsx +324 -0
- package/cli/registry/chat/user-question/context.tsx +456 -0
- package/cli/registry/chat/user-question/index.tsx +71 -316
- package/cli/registry/chat/user-question/useUserQuestionKeyboard.ts +5 -6
- package/cli/registry/tokens/index.css +31 -0
- package/cli/registry/tokens/scale/computed.css +103 -0
- package/cli/registry/tokens/scale/config.css +110 -0
- package/cli/registry/tokens/scale/index.css +30 -0
- package/cli/registry/tokens/scale/presets/compact.css +30 -0
- package/cli/registry/tokens/scale/presets/dense.css +64 -0
- package/cli/registry/tokens/scale/presets/sharp.css +40 -0
- package/cli/registry/tokens/scale/presets/soft.css +16 -0
- package/cli/registry/tokens/scale.css +12 -298
- package/cli/registry/tokens/scrollbar-utility.css +35 -0
- package/cli/registry/tokens/themes/dark-parchment.css +132 -0
- package/cli/registry/tokens/themes/dark-qoder.css +132 -0
- package/cli/registry/tokens/themes/light-parchment.css +123 -0
- package/cli/registry/tokens/themes/light-qoder.css +131 -0
- package/dist/qoder-design.css +1 -1
- package/dist/registry/chat/ask-user-part.d.ts +24 -0
- package/dist/registry/chat/browser-action-part.d.ts +28 -0
- package/dist/registry/chat/{suggestion-part.d.ts → hint-banner.d.ts} +4 -4
- package/dist/registry/chat/reasoning-step/compound.d.ts +17 -0
- package/dist/registry/chat/reasoning-step/context.d.ts +10 -0
- package/dist/registry/chat/reasoning-step/index.d.ts +14 -0
- package/dist/registry/chat/reasoning-step/types.d.ts +95 -0
- package/dist/registry/chat/response/compound.d.ts +25 -0
- package/dist/registry/chat/response/context.d.ts +9 -0
- package/dist/registry/chat/response/index.d.ts +15 -0
- package/dist/registry/chat/response/types.d.ts +99 -0
- package/dist/registry/chat/user-message.d.ts +6 -0
- package/dist/registry/chat/user-question/compound.d.ts +37 -0
- package/dist/registry/chat/user-question/context.d.ts +55 -0
- package/dist/registry/chat/user-question/index.d.ts +13 -5
- package/dist/registry/chat/user-question/useUserQuestionKeyboard.d.ts +2 -3
- package/dist/scale.css +9 -303
- package/dist/spark-design.cjs.js +62 -62
- package/dist/spark-design.es.js +3992 -3826
- package/dist/src/components/chat/AskUserPart/index.d.ts +6 -0
- package/dist/src/components/chat/BrowserActionPart/index.d.ts +7 -0
- package/dist/src/components/chat/HintBanner/index.d.ts +6 -0
- package/dist/src/components/chat/ReasoningStep/index.d.ts +11 -5
- package/dist/src/components/chat/Response/index.d.ts +16 -6
- package/dist/src/components/chat/UserMessage/index.d.ts +7 -0
- package/dist/src/components/chat/UserQuestion/index.d.ts +18 -4
- package/dist/src/components/index.d.ts +63 -63
- package/dist/theme.css +13 -800
- package/package.json +27 -3
- package/dist/registry/chat/browser-use-part.d.ts +0 -28
- package/dist/registry/chat/question-part.d.ts +0 -24
- package/dist/registry/chat/reasoning-step.d.ts +0 -35
- package/dist/registry/chat/request.d.ts +0 -6
- package/dist/registry/chat/response.d.ts +0 -28
- package/dist/src/components/chat/BrowserUsePart/index.d.ts +0 -7
- package/dist/src/components/chat/QuestionPart/index.d.ts +0 -6
- package/dist/src/components/chat/Request/index.d.ts +0 -7
- package/dist/src/components/chat/SuggestionPart/index.d.ts +0 -6
- /package/dist/src/components/{foundation → basic}/AlertDialog/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Avatar/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Button/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Collapse/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Collapsible/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/CollapsibleSection/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/DropdownMenu/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/EllipsisText/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/IconButton/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Kbd/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/OptionList/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Pagination/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Progress/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/RadioGroup/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Resizable/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Scrollbar/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Select/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Skeleton/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Slider/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Spinner/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Switch/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Table/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Tabs/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Tag/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Toast/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Toggle/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Tooltip/index.d.ts +0 -0
- /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 种内置布局风格,提供
|
|
3
|
+
现代化 React 设计系统,支持双维度主题(颜色 theme + 布局 scale),5 种内置布局风格,提供 Basic 基础组件与 Chat 对话流组件,可扩展颜色主题。
|
|
4
4
|
|
|
5
|
-
**当前版本**:`0.
|
|
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
|
-
- **组件分层**:
|
|
18
|
+
- **组件分层**:Basic 原子组件 + Chat 对话组件,开箱即用
|
|
17
19
|
|
|
18
20
|
## 两种使用方式
|
|
19
21
|
|
|
@@ -224,7 +226,7 @@ function App() {
|
|
|
224
226
|
|
|
225
227
|
| 分层 | 说明 | 示例 |
|
|
226
228
|
|------|------|------|
|
|
227
|
-
| **
|
|
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/ #
|
|
274
|
+
├── components/ # basic/、chat/
|
|
273
275
|
└── lib/ # 工具与 ThemeStyleProvider
|
|
274
276
|
```
|
|
275
277
|
|
package/cli/dist/index.js
CHANGED
|
File without changes
|
package/cli/dist/utils/tokens.js
CHANGED
|
@@ -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 使用:把
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
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
|
-
}:
|
|
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
|
-
|
|
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
|
|
15
|
+
export type BrowserActionStatus = 'normal' | 'failed' | 'skipped'
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface BrowserActionPartProps {
|
|
18
18
|
description?: string
|
|
19
19
|
content?: string
|
|
20
|
-
status?:
|
|
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
|
|
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
|
-
}:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
}:
|
|
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
|
-
|
|
165
|
+
HintBanner.displayName = 'HintBanner'
|