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
package/README.md CHANGED
@@ -1,9 +1,193 @@
1
1
  # sparkdesign
2
2
 
3
- > Package name reserved for future development.
3
+ Spark Design 按需引入 CLI:将组件**源码**复制到当前项目,支持在项目内自由修改。
4
4
 
5
- This package is currently a placeholder. The actual implementation will be coming soon.
5
+ **当前版本**:`0.1.6` · 设计系统主包:[sparkdesign](https://www.npmjs.com/package/sparkdesign)
6
6
 
7
- ## Author
7
+ ---
8
+
9
+ ## 快速开始
10
+
11
+ ### 步骤 1:添加组件
12
+
13
+ 在项目根目录执行:
14
+
15
+ ```bash
16
+ npx sparkdesign add button
17
+ ```
18
+
19
+ 添加多个组件时在命令后追加组件名,例如:`npx sparkdesign add button tooltip dropdown-menu`。
20
+
21
+ > 输出目录由 **components.json** 的 `aliases.ui` 决定(默认 `@/components/ui` → `src/components/ui/`)。执行 add 时会在终端打印当前配置与目标目录,修改 `components.json` 后再次 add 即可生效。
22
+ >
23
+ > - Foundation:`<目标目录>/basic/*.tsx`
24
+ > - Chat:`<目标目录>/chat/*.tsx`
25
+ > - 例如默认:`src/components/ui/basic/button.tsx`、`src/components/ui/chat/chat-input/index.tsx`
26
+
27
+ ### 步骤 2:引入设计 token
28
+
29
+ CLI 会生成 `src/qoder-tokens.css`,组件样式依赖其中的设计变量。请在**应用入口**引入一次:
30
+
31
+ > **注意**:组件样式需在 **Tailwind CSS 4** 环境下才能正确生效(token 使用 `@theme` 生成工具类)。请确保项目使用 Tailwind 4,并在**同一份会被 Tailwind 处理的 CSS 入口**中引入 token(详见下方「按需引入完整说明」)。
32
+
33
+ **Vite / CRA(入口为 main.tsx 或 main.jsx):**
34
+
35
+ 在 `src/main.tsx`(或 `src/main.jsx`)顶部添加:
36
+
37
+ ```ts
38
+ import './qoder-tokens.css'
39
+ ```
40
+
41
+ **Next.js(App Router):**
42
+
43
+ 在 `src/app/layout.tsx` 中与其他 import 一并添加:
44
+
45
+ ```ts
46
+ import '../qoder-tokens.css'
47
+ ```
48
+
49
+ (若 layout 位于 `src/app/layout.tsx` 使用 `../qoder-tokens.css`,位于 `app/layout.tsx` 则使用 `./qoder-tokens.css`。)
50
+
51
+ ### 步骤 3:使用组件
52
+
53
+ 默认会生成 `src/components/ui/basic/<name>.tsx`(Foundation)或 `src/components/ui/chat/<name>/`(Chat),因此导入路径在默认配置下为:
54
+
55
+ - **Foundation**:`import { X } from '@/components/ui/basic/<组件名>'`
56
+ - **Chat**:`import { X } from '@/components/ui/chat/<组件名>'`
57
+
58
+ (若你修改了 `aliases.ui`,把上面路径中的 `@/components/ui` 换成你的 alias 即可。)
59
+
60
+ ```tsx
61
+ // 默认 aliases.ui = @/components/ui 时:
62
+ import { Button } from '@/components/ui/basic/button'
63
+
64
+ function App() {
65
+ return (
66
+ <div>
67
+ <Button variant="primary">点击</Button>
68
+ </div>
69
+ )
70
+ }
71
+ ```
72
+
73
+ 可选:在根节点设置 `data-theme` 与 `data-style`(如 `<div data-theme="light" data-style="neutral">`),不设置则使用默认值。
74
+
75
+ ---
76
+
77
+ ## 按需引入完整说明(与 Showcase 一致)
78
+
79
+ 要让按需引入的组件**样式与官方 Showcase 完全一致**,需满足以下环境与配置。
80
+
81
+ ### 环境要求
82
+
83
+ | 项目 | 要求 |
84
+ |------|------|
85
+ | **Tailwind 版本** | 必须使用 **Tailwind CSS 4**。设计 token 使用 Tailwind 4 的 `@theme` 语法,将 CSS 变量映射为工具类(如 `h-7`、`bg-primary`、`rounded-md`)。使用 Tailwind 3 时这些类不会生成,组件样式会异常。 |
86
+ | **安装** | `npm install tailwindcss@^4 @tailwindcss/vite`(Vite 项目);或使用 `@tailwindcss/cli` 做预编译。 |
87
+
88
+ ### Token 引入的两种方式
89
+
90
+ | 方式 | 何时用 | Token 如何生效 |
91
+ |------|--------|----------------|
92
+ | **先 init** | 希望 token 直接写进现有 CSS 入口、少一步手动 import 时 | 执行 `npx sparkdesign init`,CLI 会把 **theme.css + scale.css** 的内容注入到 `src/app/globals.css` 或 `src/index.css`,无需再单独引入 token 文件。 |
93
+ | **直接 add** | 不想改现有流程、先装组件再补样式时 | 执行 `npx sparkdesign add button` 会生成 **`src/qoder-tokens.css`**。须在**应用入口**(如 `main.tsx`、`layout.tsx`)添加一行:`import './qoder-tokens.css'`(路径按实际入口位置调整,如 Next.js 可为 `import '../qoder-tokens.css'`)。 |
94
+
95
+ 无论哪种方式,**token 必须处在「会被 Tailwind 处理的同一份 CSS 管线」中**(即包含 `@import "tailwindcss"` 的那份 CSS 或其 import 链中),这样 `@theme` 才会被 Tailwind 4 读取并生成对应工具类。
96
+
97
+ ### CSS 入口配置(与 Showcase 一致)
98
+
99
+ 1. **顺序**:先 `@import "tailwindcss"`,再引入 token(或包含 token 的文件)。
100
+ 2. **扫描组件目录**:Showcase 会扫描组件所在目录以生成 `h-7`、`w-9` 等工具类。按需引入后组件在你项目里,需让 Tailwind 扫描到这些文件。在**同一份 CSS 入口**中增加 `@source` 指向组件目录(路径对应你当前的 `aliases.ui`)。
101
+
102
+ **最小可用的入口 CSS 示例**(Vite 项目,组件默认在 `src/components/ui`):
103
+
104
+ ```css
105
+ @import "tailwindcss";
106
+ @source "../components/ui";
107
+ @import "./qoder-tokens.css";
108
+ ```
109
+
110
+ 若你已通过 **init** 把 token 写入了本文件,则无需再写 `@import "./qoder-tokens.css"`,只需保留 `@import "tailwindcss"` 和 `@source "../components/ui"` 即可。
111
+
112
+ ### 主题与布局风格(可选)
113
+
114
+ 与 Showcase 一样切换主题/风格时,在根节点设置:
115
+
116
+ - **`data-theme`**:颜色主题,如 `light`、`dark`。
117
+ - **`data-style`**:布局风格,如 `neutral`、`compact`、`soft`、`sharp`、`dense`。
118
+
119
+ 示例:`<div data-theme="light" data-style="neutral">`。不设置则使用默认值。
120
+
121
+ ---
122
+
123
+ ## 命令说明
124
+
125
+ | 命令 | 说明 |
126
+ |------|------|
127
+ | `npx sparkdesign add button` | 添加 button 到 `src/components/ui/`(默认),并自动创建缺失的 config、token 文件、utils |
128
+ | `npx sparkdesign add button tooltip dropdown-menu` | 一次添加多个组件 |
129
+ | `npx sparkdesign init` | 先做一次「完整初始化」:选择布局风格、**组件存放目录(相对 src,默认 components)**、是否安装依赖;token 会写进现有 CSS 入口 |
130
+ | `npx sparkdesign list` | 列出所有可添加的组件名 |
131
+ | `npx sparkdesign diff button` | 对比本地 button 和最新模板的差异 |
132
+ | `npx sparkdesign add button --overwrite` | 覆盖已有组件文件 |
133
+
134
+ ---
135
+
136
+ ## 支持的组件
137
+
138
+ **支持的组件 = CLI 包内 `registry/meta.json` 的全部条目**(覆盖当前 registry 的 Foundation + Chat 组件实现)。
139
+
140
+ 权威列表请运行:
141
+
142
+ ```bash
143
+ npx sparkdesign list
144
+ ```
145
+
146
+ 示例(Foundation + Chat 混合添加):
147
+
148
+ ```bash
149
+ npx sparkdesign add button select dropdown-menu tooltip
150
+ npx sparkdesign add chat-input response markdown
151
+ ```
152
+
153
+ > 说明:tokens(`registry/tokens/*.css`)与 `registry/lib/utils.ts` 由 `init` 或首次 `add` 时的“初始化逻辑”处理,不作为单组件条目列出。
154
+
155
+ ---
156
+
157
+ ## init 与 add 的选用
158
+
159
+ - **直接 add**(推荐):执行 `npx sparkdesign add button` 会生成 `src/qoder-tokens.css`,按步骤 2 在入口添加 `import './qoder-tokens.css'` 即可。
160
+ - **先 init 再 add**:执行 `npx sparkdesign init` 时会询问**组件存放目录**(相对 src,如 `components` 或 `ui`),并写入 `components.json` 的 `aliases.components` 与 **`aliases.ui`**(默认 `@/components/ui`)。之后 add 会按该配置输出到对应目录(如 `src/components/ui/`),并将 token 写入现有 `globals.css` / `index.css`。
161
+
162
+ 两种方式择一使用即可。若要**与官方 Showcase 样式完全一致**,请同时满足上文「按需引入完整说明」中的 Tailwind 4 与 CSS 入口配置(含 `@source` 扫描组件目录)。
163
+
164
+ ---
165
+
166
+ ## 生成文件说明
167
+
168
+ | 文件 | 何时出现 | 说明 |
169
+ |------|----------|------|
170
+ | `components.json` | add 或 init | 配置:`style`、`aliases.components`、`aliases.utils`、**`aliases.ui`**(组件安装目录,默认 **`@/components/ui`**)。修改 **`aliases.ui`** 可改变 add 的输出目录;再次 add 时终端会打印目标目录以确认生效。 |
171
+ | `src/qoder-tokens.css` | 直接 add 且项目中尚无 token 时 | 设计 token 文件(theme + scale),须在**与 Tailwind 同一管线的入口**中引入一次,且项目需使用 Tailwind 4 |
172
+ | `src/lib/utils.ts` | add 或 init 且不存在时 | 工具函数 `cn()`,供组件使用 |
173
+ | `src/<aliases.ui 解析路径>/basic/*.tsx`<br>`src/<aliases.ui 解析路径>/chat/*.tsx` | add 对应组件时 | 组件源码(路径由 `aliases.ui` 决定,默认 `src/components/ui/`),可按需修改 |
174
+
175
+ ---
176
+
177
+ ## 依赖
178
+
179
+ - **样式生效前提**:组件样式依赖设计 token 与 Tailwind 工具类,项目须使用 **Tailwind CSS 4**(见上文「按需引入完整说明」)。若未使用 Tailwind 4,token 中的 `@theme` 不会生效,`h-7`、`bg-primary` 等类不会生成。
180
+ - **运行时依赖**:组件会用到 `class-variance-authority`、`clsx`(以及部分组件需要 `tailwind-merge`、`@radix-ui/*` 等)。若执行 `npx sparkdesign add button` 后出现依赖缺失,请在项目根目录执行:
181
+
182
+ ```bash
183
+ npm install class-variance-authority clsx tailwind-merge
184
+ ```
185
+
186
+ 使用 tooltip、dropdown-menu 等组件时,按提示安装 `@radix-ui/react-tooltip`、`@radix-ui/react-dropdown-menu` 等依赖。
187
+
188
+ ---
189
+
190
+ ## 安装目录配置
191
+
192
+ 支持 **`aliases.ui`** 作为组件安装目录,默认 **`@/components/ui`**;未设置时使用 `aliases.components + '/qoder'` 以兼容旧配置。**改安装位置只需改 `aliases.ui`**,`aliases.components` 一般不必改。详见 [CLI 配置说明](../docs/CLI配置说明.md)。
8
193
 
9
- cunyu666
@@ -0,0 +1,93 @@
1
+ /**
2
+ * [INPUT]: components[] from argv, options.overwrite
3
+ * [OUTPUT]: void - 写入组件文件到用户项目
4
+ * [POS]: cli/src/commands/add.ts
5
+ *
6
+ * [PROTOCOL]: 文件逻辑变更时同步更新此 Header
7
+ */
8
+ import path from 'node:path';
9
+ import fs from 'fs-extra';
10
+ import chalk from 'chalk';
11
+ import { execa } from 'execa';
12
+ import { readConfig, writeConfig, getComponentsBaseAlias, resolveTargetDir } from '../utils/config.js';
13
+ import { getRegistryRoot, getComponentMeta, getComponentSource } from '../utils/registry.js';
14
+ import { stripL3Header, transformImports } from '../utils/transform.js';
15
+ import { projectHasTokens, writeQoderTokensFile, getQoderTokensImportPath } from '../utils/tokens.js';
16
+ export async function add(components, options = {}) {
17
+ const cwd = process.cwd();
18
+ const registryRoot = getRegistryRoot();
19
+ let config = await readConfig(cwd);
20
+ const hasConfig = await fs.pathExists(path.join(cwd, 'components.json'));
21
+ if (!hasConfig) {
22
+ await writeConfig(config, cwd);
23
+ console.log(chalk.green('✓'), 'Created components.json (default config)');
24
+ }
25
+ const hasTokens = await projectHasTokens(cwd);
26
+ if (!hasTokens) {
27
+ const { path: tokensPath, created } = await writeQoderTokensFile(cwd, registryRoot);
28
+ if (created) {
29
+ console.log(chalk.green('✓'), 'Created', tokensPath);
30
+ }
31
+ console.log(chalk.cyan(' To enable component styles, add to your app entry (e.g. main.tsx or layout.tsx):'));
32
+ console.log(chalk.yellow(' import'), chalk.white(`'${getQoderTokensImportPath()}'`));
33
+ console.log(chalk.gray(' (Paste the line above into your entry file.)'));
34
+ console.log(chalk.gray(' Or run'), chalk.cyan('npx sparkdesign init'), chalk.gray('for full setup with tokens in your CSS entry.'));
35
+ }
36
+ const utilsDest = path.join(cwd, 'src', 'lib', 'utils.ts');
37
+ if (!(await fs.pathExists(utilsDest))) {
38
+ const utilsTemplate = await fs.readFile(path.join(registryRoot, 'lib', 'utils.ts'), 'utf-8');
39
+ await fs.ensureDir(path.dirname(utilsDest));
40
+ await fs.writeFile(utilsDest, utilsTemplate, 'utf-8');
41
+ console.log(chalk.green('✓'), 'Created', utilsDest);
42
+ }
43
+ if (!components || components.length === 0) {
44
+ console.log(chalk.cyan('Usage:'), 'npx sparkdesign add button [tooltip] [dropdown-menu]');
45
+ console.log(chalk.cyan('List:'), 'npx sparkdesign list');
46
+ return;
47
+ }
48
+ const targetDir = resolveTargetDir(getComponentsBaseAlias(config), cwd);
49
+ await fs.ensureDir(targetDir);
50
+ const baseAlias = getComponentsBaseAlias(config);
51
+ console.log(chalk.gray(' 组件安装目录:'), chalk.cyan(baseAlias));
52
+ console.log(chalk.gray(' 目标目录:'), chalk.cyan(path.relative(cwd, targetDir) || targetDir));
53
+ console.log(chalk.gray(' (components.json → aliases.ui)'));
54
+ console.log('');
55
+ const allDeps = new Set();
56
+ for (const name of components) {
57
+ const meta = await getComponentMeta(registryRoot, name);
58
+ if (!meta) {
59
+ console.error(chalk.red('✗'), `Component "${name}" not found. Run npx sparkdesign list`);
60
+ continue;
61
+ }
62
+ for (const file of meta.files) {
63
+ const source = await getComponentSource(registryRoot, name, file);
64
+ const transformed = transformImports(stripL3Header(source), config.aliases);
65
+ // 保留相对路径,便于 chat/xxx 与 basic/xxx 的引用关系(对齐 ElevenLabs npx add message 的按需安装)
66
+ const destPath = path.join(targetDir, file);
67
+ const exists = await fs.pathExists(destPath);
68
+ if (exists && !options.overwrite) {
69
+ console.log(chalk.yellow('○'), name, 'already exists, skipped. Use --overwrite to replace.');
70
+ continue;
71
+ }
72
+ await fs.ensureDir(path.dirname(destPath));
73
+ await fs.writeFile(destPath, transformed, 'utf-8');
74
+ console.log(chalk.green('✓'), 'Added', name, '→', destPath);
75
+ }
76
+ if (meta.dependencies) {
77
+ meta.dependencies.forEach((d) => allDeps.add(d));
78
+ }
79
+ }
80
+ if (allDeps.size > 0) {
81
+ console.log('');
82
+ console.log(chalk.cyan('Installing dependencies...'));
83
+ const depsArray = Array.from(allDeps);
84
+ try {
85
+ await execa('npm', ['install', ...depsArray], { cwd, stdio: 'inherit' });
86
+ console.log(chalk.green('✓'), 'Installed', depsArray.length, 'dependencies');
87
+ }
88
+ catch {
89
+ console.log(chalk.yellow('○'), 'Auto-install failed. Run manually:');
90
+ console.log(' npm install', depsArray.join(' '));
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * [INPUT]: component name from argv
3
+ * [OUTPUT]: void - 打印本地文件与注册表版本的 diff
4
+ * [POS]: cli/src/commands/diff.ts
5
+ *
6
+ * [PROTOCOL]: 文件逻辑变更时同步更新此 Header
7
+ */
8
+ import path from 'node:path';
9
+ import fs from 'fs-extra';
10
+ import chalk from 'chalk';
11
+ import { diffLines } from 'diff';
12
+ import { readConfig, getComponentsBaseAlias, resolveTargetDir } from '../utils/config.js';
13
+ import { getRegistryRoot, getComponentMeta, getComponentSource } from '../utils/registry.js';
14
+ import { stripL3Header, transformImports } from '../utils/transform.js';
15
+ function printLineDiffs(changes) {
16
+ for (const part of changes) {
17
+ const lines = part.value.split('\n').filter((l, i, arr) => i < arr.length - 1 || l.length > 0);
18
+ const color = part.added ? chalk.green : part.removed ? chalk.red : chalk.gray;
19
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
20
+ for (const line of lines) {
21
+ console.log(color(prefix + line));
22
+ }
23
+ }
24
+ }
25
+ export async function diffCmd(componentName) {
26
+ const cwd = process.cwd();
27
+ const config = await readConfig(cwd);
28
+ const registryRoot = getRegistryRoot();
29
+ const meta = await getComponentMeta(registryRoot, componentName);
30
+ if (!meta) {
31
+ console.error(chalk.red('✗'), `Component "${componentName}" not found. Run npx sparkdesign list`);
32
+ process.exit(1);
33
+ }
34
+ const targetDir = resolveTargetDir(getComponentsBaseAlias(config), cwd);
35
+ let hasDiff = false;
36
+ for (const file of meta.files) {
37
+ const localPath = path.join(targetDir, file);
38
+ const localExists = await fs.pathExists(localPath);
39
+ const registrySource = await getComponentSource(registryRoot, componentName, file);
40
+ const registryTransformed = transformImports(stripL3Header(registrySource), config.aliases);
41
+ const localContent = localExists ? await fs.readFile(localPath, 'utf-8') : '';
42
+ const changes = diffLines(localContent, registryTransformed);
43
+ const hasChanges = changes.some((c) => c.added || c.removed);
44
+ if (hasChanges) {
45
+ hasDiff = true;
46
+ console.log(chalk.cyan('---'), file);
47
+ printLineDiffs(changes);
48
+ console.log('');
49
+ }
50
+ }
51
+ if (!hasDiff) {
52
+ console.log(chalk.green('No differences for'), componentName);
53
+ }
54
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * [INPUT]: 无(从 process.cwd() 读取项目)
3
+ * [OUTPUT]: void - 写入 components.json、tokens、utils
4
+ * [POS]: cli/src/commands/init.ts
5
+ *
6
+ * [PROTOCOL]: 文件逻辑变更时同步更新此 Header
7
+ */
8
+ import path from 'node:path';
9
+ import fs from 'fs-extra';
10
+ import prompts from 'prompts';
11
+ import chalk from 'chalk';
12
+ import { execa } from 'execa';
13
+ import { writeConfig } from '../utils/config.js';
14
+ import { getRegistryRoot } from '../utils/registry.js';
15
+ import { ensureDesignTokens, projectHasTokens } from '../utils/tokens.js';
16
+ const STYLES = [
17
+ { title: 'neutral(平衡标准)', value: 'neutral' },
18
+ { title: 'compact(紧凑)', value: 'compact' },
19
+ { title: 'soft(舒适)', value: 'soft' },
20
+ { title: 'sharp(几何)', value: 'sharp' },
21
+ { title: 'dense(密集)', value: 'dense' },
22
+ ];
23
+ function resolveComponentsDir(aliasesComponents) {
24
+ const trimmed = aliasesComponents.replace(/^@\//, '');
25
+ return trimmed ? path.join('src', trimmed) : 'src/components';
26
+ }
27
+ export async function init() {
28
+ const cwd = process.cwd();
29
+ const registryRoot = getRegistryRoot();
30
+ const utilsTemplatePath = path.join(registryRoot, 'lib', 'utils.ts');
31
+ const answers = await prompts([
32
+ {
33
+ type: 'select',
34
+ name: 'style',
35
+ message: '选择布局风格 (data-style)',
36
+ choices: STYLES,
37
+ initial: 0,
38
+ },
39
+ {
40
+ type: 'text',
41
+ name: 'componentsDir',
42
+ message: '组件库放在哪个目录下?(我们会在此下创建 ui 子目录;如填 components 即 src/components/ui)',
43
+ initial: 'components',
44
+ },
45
+ {
46
+ type: 'confirm',
47
+ name: 'installDeps',
48
+ message: '是否安装依赖?(class-variance-authority, clsx, tailwind-merge)',
49
+ initial: true,
50
+ },
51
+ ]);
52
+ if (answers.style == null) {
53
+ process.exit(0);
54
+ }
55
+ const style = answers.style;
56
+ const rawDir = (answers.componentsDir ?? 'components').toString().trim().replace(/^src\/?/, '');
57
+ const componentsPath = rawDir || 'components';
58
+ const config = {
59
+ style,
60
+ aliases: {
61
+ components: `@/${componentsPath}`,
62
+ utils: '@/lib/utils',
63
+ ui: `@/${componentsPath}/ui`,
64
+ },
65
+ };
66
+ await writeConfig(config, cwd);
67
+ console.log(chalk.green('✓'), 'Created components.json');
68
+ const componentsDir = resolveComponentsDir(config.aliases.components);
69
+ const libDir = path.join(cwd, 'src', 'lib');
70
+ const utilsDest = path.join(libDir, 'utils.ts');
71
+ await fs.ensureDir(libDir);
72
+ const utilsTemplate = await fs.readFile(utilsTemplatePath, 'utf-8');
73
+ await fs.writeFile(utilsDest, utilsTemplate, 'utf-8');
74
+ console.log(chalk.green('✓'), 'Created', utilsDest);
75
+ const injected = await ensureDesignTokens(cwd, registryRoot);
76
+ if (injected) {
77
+ console.log(chalk.green('✓'), 'Added design tokens (theme + scale) to', injected);
78
+ }
79
+ else if (await projectHasTokens(cwd)) {
80
+ console.log(chalk.yellow('○'), 'Design tokens already present in project');
81
+ }
82
+ else {
83
+ console.log(chalk.yellow('○'), 'Could not inject tokens (no CSS entry found). Add theme/scale manually.');
84
+ }
85
+ if (answers.installDeps) {
86
+ try {
87
+ await execa('npm', ['install', 'class-variance-authority', 'clsx', 'tailwind-merge'], { cwd, stdio: 'inherit' });
88
+ console.log(chalk.green('✓'), 'Installed dependencies');
89
+ }
90
+ catch {
91
+ console.log(chalk.yellow('○'), 'Run manually: npm install class-variance-authority clsx tailwind-merge');
92
+ }
93
+ }
94
+ await fs.ensureDir(path.join(cwd, componentsDir));
95
+ console.log(chalk.green('✓'), 'Spark Design initialized. Run'), chalk.cyan('npx sparkdesign add button'), chalk.green('to add components.');
96
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * [INPUT]: 无
3
+ * [OUTPUT]: void - 打印注册表内所有组件名
4
+ * [POS]: cli/src/commands/list.ts
5
+ *
6
+ * [PROTOCOL]: 文件逻辑变更时同步更新此 Header
7
+ */
8
+ import chalk from 'chalk';
9
+ import { getRegistryRoot, listComponents } from '../utils/registry.js';
10
+ export async function list() {
11
+ const registryRoot = getRegistryRoot();
12
+ try {
13
+ const names = await listComponents(registryRoot);
14
+ if (names.length === 0) {
15
+ console.log('No components in registry.');
16
+ return;
17
+ }
18
+ console.log(chalk.cyan('Available components:'));
19
+ names.forEach((name) => console.log(' ', name));
20
+ }
21
+ catch (e) {
22
+ console.error(chalk.red('Failed to load registry:'), e.message);
23
+ process.exit(1);
24
+ }
25
+ }
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * [INPUT]: process.argv
4
+ * [OUTPUT]: void - 执行对应子命令并退出
5
+ * [POS]: cli/src/index.ts - Spark Design CLI 入口
6
+ *
7
+ * [PROTOCOL]:
8
+ * 1. 逻辑变更时同步更新此 Header
9
+ * 2. 子命令实现位于 commands/
10
+ */
11
+ import { program } from 'commander';
12
+ import { init } from './commands/init.js';
13
+ import { add } from './commands/add.js';
14
+ import { list } from './commands/list.js';
15
+ import { diffCmd } from './commands/diff.js';
16
+ program
17
+ .name('sparkdesign')
18
+ .description('Spark Design CLI - 按需添加组件(源码复制到当前项目)')
19
+ .version('0.1.0');
20
+ program
21
+ .command('init')
22
+ .description('初始化项目:写入 components.json、tokens、utils')
23
+ .action(init);
24
+ program
25
+ .command('add [components...]')
26
+ .description('添加组件到当前项目')
27
+ .option('-o, --overwrite', '覆盖已存在的组件文件')
28
+ .action(add);
29
+ program
30
+ .command('list')
31
+ .description('列出所有可用组件')
32
+ .action(list);
33
+ program
34
+ .command('diff <component>')
35
+ .description('对比本地组件与注册表最新版本的差异')
36
+ .action(diffCmd);
37
+ program.parse();
@@ -0,0 +1,53 @@
1
+ /**
2
+ * [INPUT]: process.cwd() 下的 components.json
3
+ * [OUTPUT]: Config | 默认配置
4
+ * [POS]: cli/src/utils/config.ts
5
+ *
6
+ * [PROTOCOL]: 文件逻辑变更时同步更新此 Header
7
+ */
8
+ import path from 'node:path';
9
+ import fs from 'fs-extra';
10
+ const defaultConfig = {
11
+ style: 'neutral',
12
+ aliases: {
13
+ components: '@/components',
14
+ utils: '@/lib/utils',
15
+ ui: '@/components/ui',
16
+ },
17
+ };
18
+ /** 优先用 aliases.ui 作为组件安装目录,未设时用 components + '/qoder'(兼容旧配置) */
19
+ export function getComponentsBaseAlias(config) {
20
+ return config.aliases.ui ?? config.aliases.components + '/qoder';
21
+ }
22
+ /** 将 alias 解析为绝对目录(如 @/components/qoder → cwd/src/components/qoder) */
23
+ export function resolveTargetDir(aliasesBase, cwd) {
24
+ const withoutAlias = aliasesBase.replace(/^@\//, '').trim();
25
+ let base = withoutAlias || 'components';
26
+ base = base.replace(/^src\/?/, '') || 'components';
27
+ return path.join(cwd, 'src', base);
28
+ }
29
+ export async function readConfig(cwd = process.cwd()) {
30
+ const configPath = path.join(cwd, 'components.json');
31
+ try {
32
+ const content = await fs.readFile(configPath, 'utf-8');
33
+ const parsed = JSON.parse(content);
34
+ return {
35
+ style: parsed.style ?? defaultConfig.style,
36
+ aliases: {
37
+ components: parsed.aliases?.components ?? defaultConfig.aliases.components,
38
+ utils: parsed.aliases?.utils ?? defaultConfig.aliases.utils,
39
+ ui: parsed.aliases?.ui ?? defaultConfig.aliases.ui,
40
+ },
41
+ };
42
+ }
43
+ catch {
44
+ return defaultConfig;
45
+ }
46
+ }
47
+ export async function writeConfig(config, cwd = process.cwd()) {
48
+ const configPath = path.join(cwd, 'components.json');
49
+ await fs.writeJson(configPath, {
50
+ $schema: 'https://qoder.design/schema.json',
51
+ ...config,
52
+ }, { spaces: 2 });
53
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * [INPUT]: 组件名、registry 根路径
3
+ * [OUTPUT]: meta、模板文件内容
4
+ * [POS]: cli/src/utils/registry.ts
5
+ *
6
+ * [PROTOCOL]: 文件逻辑变更时同步更新此 Header
7
+ */
8
+ import path from 'node:path';
9
+ import fs from 'fs-extra';
10
+ import { fileURLToPath } from 'node:url';
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ /** 开发时 src/utils/registry.ts -> 向上到 cli/ -> registry;构建后 dist/utils/registry.js -> 向上到 cli/ -> registry */
13
+ export function getRegistryRoot() {
14
+ const distDir = path.resolve(__dirname, '..');
15
+ const cliRoot = path.resolve(distDir, '..');
16
+ return path.join(cliRoot, 'registry');
17
+ }
18
+ export async function loadMeta(registryRoot) {
19
+ const metaPath = path.join(registryRoot, 'meta.json');
20
+ const content = await fs.readFile(metaPath, 'utf-8');
21
+ return JSON.parse(content);
22
+ }
23
+ export async function getComponentMeta(registryRoot, componentName) {
24
+ const meta = await loadMeta(registryRoot);
25
+ return meta[componentName] ?? null;
26
+ }
27
+ export async function getComponentSource(registryRoot, componentName, relativePath) {
28
+ const fullPath = path.join(registryRoot, relativePath);
29
+ return fs.readFile(fullPath, 'utf-8');
30
+ }
31
+ export async function listComponents(registryRoot) {
32
+ const meta = await loadMeta(registryRoot);
33
+ return Object.keys(meta);
34
+ }