sparkle-design-cli 1.3.8 → 1.3.9
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 +73 -17
- package/bin/sparkle-design.js +136 -53
- package/lib/anti-pattern-rules.js +698 -0
- package/lib/check.js +104 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sparkle Design CLI
|
|
2
2
|
|
|
3
|
-
デザインシステム CSS
|
|
3
|
+
デザインシステム CSS の生成と、Sparkle Design のアンチパターン検査を行う CLI ツールです。
|
|
4
4
|
|
|
5
5
|
## インストール
|
|
6
6
|
|
|
@@ -10,7 +10,22 @@ npm install -g sparkle-design-cli
|
|
|
10
10
|
|
|
11
11
|
## 使用方法
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### サブコマンド
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# CSS を生成
|
|
17
|
+
npx sparkle-design-cli generate
|
|
18
|
+
|
|
19
|
+
# アンチパターンを検査
|
|
20
|
+
npx sparkle-design-cli check src --strict
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 後方互換モード
|
|
24
|
+
|
|
25
|
+
既存の `npx sparkle-design-cli` も引き続き動作し、内部的には `generate` として扱われます。
|
|
26
|
+
ただし後方互換のための動作なので、新規利用やドキュメント上の案内では `sparkle-design-cli generate` を使用してください。
|
|
27
|
+
|
|
28
|
+
### generate: 基本的な使用方法
|
|
14
29
|
|
|
15
30
|
1. プロジェクトのルートディレクトリに `sparkle.config.json` ファイルを作成または配置します:
|
|
16
31
|
|
|
@@ -26,39 +41,77 @@ npm install -g sparkle-design-cli
|
|
|
26
41
|
2. CLI ツールを実行します:
|
|
27
42
|
|
|
28
43
|
```bash
|
|
29
|
-
npx sparkle-design-cli
|
|
44
|
+
npx sparkle-design-cli generate
|
|
30
45
|
```
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
既存プロジェクトでサブコマンドなしの呼び出しを使っている場合も、後方互換として次の実行方法を継続利用できます:
|
|
33
48
|
|
|
34
49
|
```bash
|
|
35
|
-
sparkle-design-cli
|
|
50
|
+
npx sparkle-design-cli
|
|
36
51
|
```
|
|
37
52
|
|
|
38
53
|
3. `src/app/sparkle-design.css` に CSS ファイルが生成されます。
|
|
39
54
|
|
|
40
|
-
### コマンドオプション
|
|
55
|
+
### generate: コマンドオプション
|
|
41
56
|
|
|
42
57
|
```bash
|
|
43
58
|
# ヘルプを表示
|
|
44
|
-
sparkle-design-cli --help
|
|
59
|
+
sparkle-design-cli generate --help
|
|
45
60
|
|
|
46
61
|
# カスタム設定ファイルを指定
|
|
47
|
-
sparkle-design-cli --config ./config/design.json
|
|
62
|
+
sparkle-design-cli generate --config ./config/design.json
|
|
48
63
|
|
|
49
64
|
# カスタム出力先を指定
|
|
50
|
-
sparkle-design-cli --output ./styles/design.css
|
|
65
|
+
sparkle-design-cli generate --output ./styles/design.css
|
|
51
66
|
|
|
52
67
|
# 設定ファイルと出力先を両方指定
|
|
53
|
-
sparkle-design-cli -c ./config/custom.json -o ./dist/styles.css
|
|
68
|
+
sparkle-design-cli generate -c ./config/custom.json -o ./dist/styles.css
|
|
54
69
|
```
|
|
55
70
|
|
|
56
|
-
#### オプション一覧
|
|
71
|
+
#### generate オプション一覧
|
|
57
72
|
|
|
58
73
|
- `-h, --help`: ヘルプメッセージを表示
|
|
59
74
|
- `-c, --config <パス>`: 設定ファイルのパス(デフォルト: `./sparkle.config.json`)
|
|
60
75
|
- `-o, --output <パス>`: 出力ファイルのパス(デフォルト: `./src/app/sparkle-design.css`)
|
|
61
76
|
|
|
77
|
+
### check: アンチパターン検査
|
|
78
|
+
|
|
79
|
+
`sparkle-design-cli check` は、導入先プロジェクトで発生しやすい Sparkle Design のアンチパターンを検出します。
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# src を検査
|
|
83
|
+
sparkle-design-cli check
|
|
84
|
+
|
|
85
|
+
# 特定ディレクトリを strict モードで検査
|
|
86
|
+
sparkle-design-cli check src/components src/features --strict
|
|
87
|
+
|
|
88
|
+
# check のヘルプを表示
|
|
89
|
+
sparkle-design-cli check --help
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### check オプション一覧
|
|
93
|
+
|
|
94
|
+
- `-h, --help`: ヘルプメッセージを表示
|
|
95
|
+
- `--strict`: 違反が見つかった場合に exit code 1 で終了
|
|
96
|
+
|
|
97
|
+
#### 現在検出するルール
|
|
98
|
+
|
|
99
|
+
- フォーム入力に Dialog を使わない
|
|
100
|
+
- DialogCancel/DialogAction を Button で二重ラップしない
|
|
101
|
+
- shadcn/ui 既定 token を Sparkle Design 内へ持ち込まない
|
|
102
|
+
|
|
103
|
+
#### 導入先での推奨設定
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"scripts": {
|
|
108
|
+
"lint:sparkle": "sparkle-design-cli check src --strict"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
AI エージェントや CI からこの `lint:sparkle` を呼ぶ運用にすると、ガイドラインの注意書きだけに頼らず機械的に検査できます。
|
|
114
|
+
|
|
62
115
|
## 設定ファイル (sparkle.config.json)
|
|
63
116
|
|
|
64
117
|
### 設定ファイルの作成
|
|
@@ -105,12 +158,12 @@ sparkle-design-cli -c ./config/custom.json -o ./dist/styles.css
|
|
|
105
158
|
生成される `globals.css`:
|
|
106
159
|
|
|
107
160
|
```css
|
|
108
|
-
@import
|
|
161
|
+
@import 'tailwindcss';
|
|
109
162
|
/* npm パッケージのコンテンツスキャン(Tailwind がクラスを検出するために必要) */
|
|
110
163
|
@source "../node_modules/@goodpatch/sparkle-design/dist";
|
|
111
164
|
@source "../node_modules/@scope/my-extension-design/dist";
|
|
112
165
|
/* Sparkle Design のカスタム定義(Tailwindの後にインポート) */
|
|
113
|
-
@import
|
|
166
|
+
@import './sparkle-design.css';
|
|
114
167
|
```
|
|
115
168
|
|
|
116
169
|
> **注意**: `source-packages` を指定しない場合、`@source` ディレクティブは生成されません。
|
|
@@ -192,14 +245,17 @@ npm run format:check
|
|
|
192
245
|
### CLI実行テスト
|
|
193
246
|
|
|
194
247
|
```bash
|
|
195
|
-
#
|
|
248
|
+
# CSS生成
|
|
249
|
+
sparkle-design-cli generate
|
|
250
|
+
|
|
251
|
+
# 既存呼び出しの後方互換モード
|
|
196
252
|
sparkle-design-cli
|
|
197
253
|
|
|
198
254
|
# ヘルプ表示
|
|
199
|
-
sparkle-design-cli --help
|
|
255
|
+
sparkle-design-cli generate --help
|
|
200
256
|
|
|
201
|
-
#
|
|
202
|
-
sparkle-design-cli
|
|
257
|
+
# 検査
|
|
258
|
+
sparkle-design-cli check src --strict
|
|
203
259
|
```
|
|
204
260
|
|
|
205
261
|
## ライセンス
|
package/bin/sparkle-design.js
CHANGED
|
@@ -1,93 +1,176 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { generateCSS } from '../lib/generate-css.js';
|
|
4
|
+
import { checkProject } from '../lib/check.js';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const SUBCOMMANDS = new Set(['generate', 'check']);
|
|
7
|
+
|
|
8
|
+
function requireOptionValue(args, index, flags) {
|
|
9
|
+
const value = args[index + 1];
|
|
10
|
+
if (!value || value.startsWith('-')) {
|
|
11
|
+
throw new Error(`Missing value for ${flags}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseGenerateOptions(args) {
|
|
8
17
|
const options = {
|
|
9
18
|
configPath: null,
|
|
10
19
|
outputPath: null,
|
|
11
20
|
help: false,
|
|
12
21
|
};
|
|
13
22
|
|
|
14
|
-
for (let i = 0; i < args.length; i
|
|
23
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
15
24
|
const arg = args[i];
|
|
16
25
|
|
|
17
26
|
if (arg === '-h' || arg === '--help') {
|
|
18
27
|
options.help = true;
|
|
19
28
|
} else if (arg === '-c' || arg === '--config') {
|
|
20
|
-
options.configPath = args
|
|
21
|
-
i
|
|
29
|
+
options.configPath = requireOptionValue(args, i, '-c/--config');
|
|
30
|
+
i += 1;
|
|
22
31
|
} else if (arg === '-o' || arg === '--output') {
|
|
23
|
-
options.outputPath = args
|
|
24
|
-
i
|
|
32
|
+
options.outputPath = requireOptionValue(args, i, '-o/--output');
|
|
33
|
+
i += 1;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`Unknown option for generate: ${arg}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return options;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseCheckOptions(args) {
|
|
43
|
+
const options = {
|
|
44
|
+
targets: [],
|
|
45
|
+
strict: false,
|
|
46
|
+
help: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
50
|
+
const arg = args[i];
|
|
51
|
+
|
|
52
|
+
if (arg === '-h' || arg === '--help') {
|
|
53
|
+
options.help = true;
|
|
54
|
+
} else if (arg === '--strict') {
|
|
55
|
+
options.strict = true;
|
|
56
|
+
} else if (arg.startsWith('-')) {
|
|
57
|
+
throw new Error(`Unknown option for check: ${arg}`);
|
|
58
|
+
} else {
|
|
59
|
+
options.targets.push(arg);
|
|
25
60
|
}
|
|
26
61
|
}
|
|
27
62
|
|
|
28
63
|
return options;
|
|
29
64
|
}
|
|
30
65
|
|
|
31
|
-
// ヘルプメッセージを表示
|
|
32
66
|
function showHelp() {
|
|
33
67
|
console.log(`
|
|
34
|
-
Sparkle Design CLI
|
|
68
|
+
Sparkle Design CLI
|
|
35
69
|
|
|
36
70
|
使用方法:
|
|
37
|
-
sparkle-design-cli [
|
|
71
|
+
sparkle-design-cli <command> [options]
|
|
72
|
+
|
|
73
|
+
Commands:
|
|
74
|
+
generate sparkle.config.json から CSS を生成
|
|
75
|
+
check Sparkle Design のアンチパターンを検査
|
|
76
|
+
|
|
77
|
+
後方互換:
|
|
78
|
+
sparkle-design-cli [generate options]
|
|
79
|
+
サブコマンドなしの実行は現在も generate として動作しますが、
|
|
80
|
+
正式リリース時に削除予定です。今後は sparkle-design-cli generate を使ってください。
|
|
38
81
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
82
|
+
Generate:
|
|
83
|
+
sparkle-design-cli generate
|
|
84
|
+
sparkle-design-cli generate --config ./config/design.json
|
|
85
|
+
sparkle-design-cli generate --output ./styles/design.css
|
|
86
|
+
sparkle-design-cli generate -c ./config/custom.json -o ./dist/styles.css
|
|
43
87
|
|
|
44
|
-
|
|
88
|
+
Check:
|
|
89
|
+
sparkle-design-cli check
|
|
90
|
+
sparkle-design-cli check src --strict
|
|
91
|
+
sparkle-design-cli check src/components src/features
|
|
92
|
+
|
|
93
|
+
Generate options:
|
|
45
94
|
-h, --help このヘルプメッセージを表示
|
|
46
|
-
-c, --config
|
|
47
|
-
-o, --output
|
|
48
|
-
|
|
49
|
-
使用例:
|
|
50
|
-
sparkle-design-cli
|
|
51
|
-
sparkle-design-cli --config ./config/design.json
|
|
52
|
-
sparkle-design-cli --output ./styles/design.css
|
|
53
|
-
sparkle-design-cli -c ./config/custom.json -o ./dist/styles.css
|
|
54
|
-
|
|
55
|
-
設定ファイル (sparkle.config.json):
|
|
56
|
-
設定ファイルは専用のプラグインから自動生成することを推奨します。
|
|
57
|
-
プロジェクトのルートディレクトリに配置してください。
|
|
58
|
-
|
|
59
|
-
設定例:
|
|
60
|
-
{
|
|
61
|
-
"primary": "blue",
|
|
62
|
-
"font-pro": "BIZ UDPGothic",
|
|
63
|
-
"font-mono": "BIZ UDGothic",
|
|
64
|
-
"radius": "md"
|
|
65
|
-
}
|
|
95
|
+
-c, --config <path> 設定ファイルのパス (default: ./sparkle.config.json)
|
|
96
|
+
-o, --output <path> 出力ファイルのパス (default: ./src/app/sparkle-design.css)
|
|
66
97
|
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
- font-mono: モノスペースフォント (Google Fonts の名前)
|
|
71
|
-
- radius: 角丸設定 (sm, md, lg など)
|
|
98
|
+
Check options:
|
|
99
|
+
-h, --help このヘルプメッセージを表示
|
|
100
|
+
--strict 違反があれば exit code 1 で終了
|
|
72
101
|
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
102
|
+
Check で検出する主なパターン:
|
|
103
|
+
- フォーム入力に Dialog を使っている
|
|
104
|
+
- DialogCancel / DialogAction を <Button> で二重ラップしている
|
|
105
|
+
- text-muted-foreground / bg-background / border-border を持ち込んでいる
|
|
77
106
|
`);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
function showGenerateDeprecationNotice() {
|
|
110
|
+
console.warn(
|
|
111
|
+
'[sparkle-design-cli] Running without a subcommand defaults to `generate`. ' +
|
|
112
|
+
'Use `sparkle-design-cli generate` instead. This compatibility mode will be removed at the formal release.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
83
115
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
116
|
+
function main() {
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
const command = args[0];
|
|
88
119
|
|
|
89
120
|
try {
|
|
90
|
-
|
|
121
|
+
if (args.length === 0) {
|
|
122
|
+
showGenerateDeprecationNotice();
|
|
123
|
+
generateCSS();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (command === '-h' || command === '--help') {
|
|
128
|
+
showHelp();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
if (command === 'generate') {
|
|
132
|
+
const options = parseGenerateOptions(args.slice(1));
|
|
133
|
+
|
|
134
|
+
if (options.help) {
|
|
135
|
+
showHelp();
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
generateCSS(options.configPath, options.outputPath);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (command === 'check') {
|
|
144
|
+
const options = parseCheckOptions(args.slice(1));
|
|
145
|
+
|
|
146
|
+
if (options.help) {
|
|
147
|
+
showHelp();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hasFindings = checkProject(options.targets, { strict: options.strict });
|
|
152
|
+
if (hasFindings && options.strict) {
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!SUBCOMMANDS.has(command) && command.startsWith('-')) {
|
|
159
|
+
showGenerateDeprecationNotice();
|
|
160
|
+
const options = parseGenerateOptions(args);
|
|
161
|
+
|
|
162
|
+
if (options.help) {
|
|
163
|
+
showHelp();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
generateCSS(options.configPath, options.outputPath);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!SUBCOMMANDS.has(command)) {
|
|
172
|
+
throw new Error(`Unknown command: ${command}`);
|
|
173
|
+
}
|
|
91
174
|
} catch (error) {
|
|
92
175
|
console.error('❌ エラーが発生しました:', error.message);
|
|
93
176
|
process.exit(1);
|
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
const COMMON_INTRO =
|
|
2
|
+
'以下は頻繁に発生する誤用パターンです。コンポーネントが提供する専用 props・サブコンポーネントを使ってください。';
|
|
3
|
+
|
|
4
|
+
function lines(values) {
|
|
5
|
+
return values.join('\n');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function renderJSDocSection(section) {
|
|
9
|
+
const output = ['**アンチパターン / Anti-patterns**', ''];
|
|
10
|
+
|
|
11
|
+
for (const bullet of section.bullets) {
|
|
12
|
+
output.push(`- ${bullet.ja}`);
|
|
13
|
+
output.push(` en: ${bullet.en}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (section.example) {
|
|
17
|
+
output.push('');
|
|
18
|
+
output.push('```tsx');
|
|
19
|
+
output.push(...section.example.split('\n'));
|
|
20
|
+
output.push('```');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return output.map((line) => (line ? ` * ${line}` : ' *')).join('\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ANTI_PATTERN_GROUPS = [
|
|
27
|
+
{
|
|
28
|
+
id: 'card-description',
|
|
29
|
+
featureSection: lines([
|
|
30
|
+
'### CardTitle の補足テキストには CardDescription を使う',
|
|
31
|
+
'',
|
|
32
|
+
'```tsx',
|
|
33
|
+
'// ✅ Correct',
|
|
34
|
+
'<CardHeader>',
|
|
35
|
+
' <CardTitle>',
|
|
36
|
+
' プロジェクト一覧',
|
|
37
|
+
' <CardDescription className="character-3-regular-pro text-text-low">',
|
|
38
|
+
' 全 12 件',
|
|
39
|
+
' </CardDescription>',
|
|
40
|
+
' </CardTitle>',
|
|
41
|
+
'</CardHeader>',
|
|
42
|
+
'',
|
|
43
|
+
'// ❌ Wrong — CardTitle 内に span で補足を入れない',
|
|
44
|
+
'<CardHeader>',
|
|
45
|
+
' <CardTitle>',
|
|
46
|
+
' プロジェクト一覧',
|
|
47
|
+
' <span className="text-sm text-neutral-500">(12件)</span>',
|
|
48
|
+
' </CardTitle>',
|
|
49
|
+
'</CardHeader>',
|
|
50
|
+
'```',
|
|
51
|
+
'',
|
|
52
|
+
'CardTitle 内の補足情報や件数は CardDescription を使い、必要な typography / color token は className で明示する。',
|
|
53
|
+
]),
|
|
54
|
+
jsdocTargets: [],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'size-alignment',
|
|
58
|
+
featureSection: lines([
|
|
59
|
+
'### Input / Select と横並びの Button はサイズを揃える',
|
|
60
|
+
'',
|
|
61
|
+
'```tsx',
|
|
62
|
+
'// ✅ Correct — 同じサイズで統一',
|
|
63
|
+
'<div className="flex gap-2">',
|
|
64
|
+
' <Input placeholder="検索..." />',
|
|
65
|
+
' <Button size="md">検索</Button>',
|
|
66
|
+
'</div>',
|
|
67
|
+
'',
|
|
68
|
+
'// ❌ Wrong — サイズ不一致で高さが揃わない',
|
|
69
|
+
'<div className="flex gap-2">',
|
|
70
|
+
' <Input placeholder="検索..." />',
|
|
71
|
+
' <Button size="sm">検索</Button>',
|
|
72
|
+
'</div>',
|
|
73
|
+
'```',
|
|
74
|
+
'',
|
|
75
|
+
'Input / Select / Textarea のデフォルトサイズは md。横並びの Button も原則 md に合わせる。テーブル内アクションなど独立した Button は sm でよい。',
|
|
76
|
+
]),
|
|
77
|
+
jsdocTargets: [
|
|
78
|
+
{
|
|
79
|
+
file: 'src/components/ui/input/index.tsx',
|
|
80
|
+
targetName: 'Input',
|
|
81
|
+
section: {
|
|
82
|
+
bullets: [
|
|
83
|
+
{
|
|
84
|
+
ja: 'Input の横にアイコンボタンを手動で配置しないでください。`isTrigger` / `triggerIcon` props を使用してください。',
|
|
85
|
+
en: 'Do not manually place an IconButton next to Input. Use `isTrigger` / `triggerIcon` props instead.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
ja: 'Input と横並びの Button は原則同じサイズにしてください。デフォルトの Input に対しては `Button size="md"` を使ってください。',
|
|
89
|
+
en: 'Keep Button size aligned when placing it next to Input. Use `Button size="md"` with the default Input size.',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
example: lines([
|
|
93
|
+
'// ✅ Correct',
|
|
94
|
+
'<>',
|
|
95
|
+
' <Input',
|
|
96
|
+
' isTrigger',
|
|
97
|
+
' triggerIcon="search"',
|
|
98
|
+
' triggerAriaLabel="検索"',
|
|
99
|
+
' onIconButtonClick={() => {}}',
|
|
100
|
+
' />',
|
|
101
|
+
' <div className="flex gap-2">',
|
|
102
|
+
' <Input placeholder="検索..." />',
|
|
103
|
+
' <Button size="md">検索</Button>',
|
|
104
|
+
' </div>',
|
|
105
|
+
'</>',
|
|
106
|
+
'',
|
|
107
|
+
'// ❌ Wrong - 手動配置',
|
|
108
|
+
'<div className="flex">',
|
|
109
|
+
' <Input />',
|
|
110
|
+
' <IconButton icon="search" aria-label="検索" />',
|
|
111
|
+
'</div>',
|
|
112
|
+
'',
|
|
113
|
+
'// ❌ Wrong - サイズ不一致',
|
|
114
|
+
'<div className="flex gap-2">',
|
|
115
|
+
' <Input placeholder="検索..." />',
|
|
116
|
+
' <Button size="sm">検索</Button>',
|
|
117
|
+
'</div>',
|
|
118
|
+
]),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
file: 'src/components/ui/select/index.tsx',
|
|
123
|
+
targetName: 'Select',
|
|
124
|
+
section: {
|
|
125
|
+
bullets: [
|
|
126
|
+
{
|
|
127
|
+
ja: '`SelectTrigger` と横並びの Button は原則同じサイズにしてください。デフォルトの `SelectTrigger` に対しては `Button size="md"` を使ってください。',
|
|
128
|
+
en: 'Keep Button size aligned when placing it next to `SelectTrigger`. Use `Button size="md"` with the default `SelectTrigger` size.',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
example: lines([
|
|
132
|
+
'// ✅ Correct',
|
|
133
|
+
'<div className="flex gap-2">',
|
|
134
|
+
' <Select>',
|
|
135
|
+
' <SelectTrigger size="md" className="w-60">',
|
|
136
|
+
' <SelectValue placeholder="選択してください" />',
|
|
137
|
+
' </SelectTrigger>',
|
|
138
|
+
' </Select>',
|
|
139
|
+
' <Button size="md">追加</Button>',
|
|
140
|
+
'</div>',
|
|
141
|
+
'',
|
|
142
|
+
'// ❌ Wrong - 高さが揃わないサイズ不一致',
|
|
143
|
+
'<div className="flex gap-2">',
|
|
144
|
+
' <Select>',
|
|
145
|
+
' <SelectTrigger size="md" className="w-60">',
|
|
146
|
+
' <SelectValue placeholder="選択してください" />',
|
|
147
|
+
' </SelectTrigger>',
|
|
148
|
+
' </Select>',
|
|
149
|
+
' <Button size="sm">追加</Button>',
|
|
150
|
+
'</div>',
|
|
151
|
+
]),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'badge-tag',
|
|
158
|
+
featureSection: lines([
|
|
159
|
+
'### Badge と Tag を用途で使い分ける',
|
|
160
|
+
'',
|
|
161
|
+
'| コンポーネント | 用途 | 例 |',
|
|
162
|
+
'| --- | --- | --- |',
|
|
163
|
+
'| **Badge** | 特定の要素に**数値情報**を付与 | `<Badge>3</Badge>` |',
|
|
164
|
+
'| **Tag** | 情報の**ラベリング / ステータス付与** | `<Tag status="success">完了</Tag>` |',
|
|
165
|
+
'',
|
|
166
|
+
'```tsx',
|
|
167
|
+
'// ✅ Correct — ステータス表示には Tag を使う',
|
|
168
|
+
'<Tag variant="outline" status="warning">未紐付け</Tag>',
|
|
169
|
+
'',
|
|
170
|
+
'// ❌ Wrong — Badge をステータスラベルに使わない',
|
|
171
|
+
'<Badge>未紐付け</Badge>',
|
|
172
|
+
'```',
|
|
173
|
+
]),
|
|
174
|
+
jsdocTargets: [
|
|
175
|
+
{
|
|
176
|
+
file: 'src/components/ui/badge/index.tsx',
|
|
177
|
+
targetName: 'Badge',
|
|
178
|
+
section: {
|
|
179
|
+
bullets: [
|
|
180
|
+
{
|
|
181
|
+
ja: 'ステータス表示や分類ラベルには `Badge` を使わず、`Tag` を使ってください。',
|
|
182
|
+
en: 'Do not use `Badge` for status display or category labels. Use `Tag` instead.',
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
example: lines([
|
|
186
|
+
'// ✅ Correct',
|
|
187
|
+
'<>',
|
|
188
|
+
' <Badge>3</Badge>',
|
|
189
|
+
' <Tag status="warning" variant="outline">未紐付け</Tag>',
|
|
190
|
+
'</>',
|
|
191
|
+
'',
|
|
192
|
+
'// ❌ Wrong - ステータスラベルに Badge を使わない',
|
|
193
|
+
'<Badge>未紐付け</Badge>',
|
|
194
|
+
]),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
file: 'src/components/ui/tag/index.tsx',
|
|
199
|
+
targetName: 'Tag',
|
|
200
|
+
section: {
|
|
201
|
+
bullets: [
|
|
202
|
+
{
|
|
203
|
+
ja: '通知数や件数などの数値情報には `Tag` を使わず、`Badge` を使ってください。',
|
|
204
|
+
en: 'Do not use `Tag` for numeric information such as notification counts. Use `Badge` instead.',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
example: lines([
|
|
208
|
+
'// ✅ Correct',
|
|
209
|
+
'<>',
|
|
210
|
+
' <Tag status="success">完了</Tag>',
|
|
211
|
+
' <Badge>3</Badge>',
|
|
212
|
+
'</>',
|
|
213
|
+
'',
|
|
214
|
+
'// ❌ Wrong - 件数表示に Tag を使わない',
|
|
215
|
+
'<Tag status="info">3件</Tag>',
|
|
216
|
+
]),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'shadcn-token',
|
|
223
|
+
check: {
|
|
224
|
+
description: 'shadcn/ui 既定 token を Sparkle Design 内へ持ち込まない',
|
|
225
|
+
recommendation:
|
|
226
|
+
'text-muted-foreground / bg-background / border-border などは Sparkle Design token に置き換えてください。',
|
|
227
|
+
pattern: /\b(text-muted-foreground|bg-background|border-border)\b/g,
|
|
228
|
+
},
|
|
229
|
+
featureSection: lines([
|
|
230
|
+
'### shadcn/ui 由来の class / token をそのまま使わない',
|
|
231
|
+
'',
|
|
232
|
+
'```tsx',
|
|
233
|
+
'// ✅ Correct — Sparkle Design の typography / color token を使う',
|
|
234
|
+
'<CardDescription className="character-3-regular-pro text-text-low">',
|
|
235
|
+
' 全 12 件',
|
|
236
|
+
'</CardDescription>',
|
|
237
|
+
'',
|
|
238
|
+
'// ❌ Wrong — shadcn/ui の既定 token や素の font utility を持ち込まない',
|
|
239
|
+
'<CardDescription className="text-sm text-muted-foreground">',
|
|
240
|
+
' 全 12 件',
|
|
241
|
+
'</CardDescription>',
|
|
242
|
+
'<CardDescription className="font-medium text-slate-500">',
|
|
243
|
+
' 全 12 件',
|
|
244
|
+
'</CardDescription>',
|
|
245
|
+
'```',
|
|
246
|
+
'',
|
|
247
|
+
'shadcn/ui と混在するプロジェクトでも、Sparkle Design のコンポーネント内では `character-*` / `text-text-*` / Sparkle の color token を優先する。',
|
|
248
|
+
]),
|
|
249
|
+
jsdocTargets: [],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'button-icons',
|
|
253
|
+
featureSection: lines([
|
|
254
|
+
'### Button: prefixIcon / suffixIcon を使う',
|
|
255
|
+
'',
|
|
256
|
+
'```tsx',
|
|
257
|
+
'// ✅ Correct',
|
|
258
|
+
'<Button prefixIcon="check">確定</Button>',
|
|
259
|
+
'<Button suffixIcon="arrow_forward">次へ</Button>',
|
|
260
|
+
'',
|
|
261
|
+
'// ❌ Wrong - Icon を children に入れない(レイアウト崩れ・ローディング時に非表示にならない)',
|
|
262
|
+
'<Button><Icon icon="check" /> 確定</Button>',
|
|
263
|
+
'',
|
|
264
|
+
'// ❌ Wrong - アイコンのみのボタンは IconButton を使う',
|
|
265
|
+
'<Button><Icon icon="edit" /></Button>',
|
|
266
|
+
'// ✅ Correct',
|
|
267
|
+
'<IconButton icon="edit" aria-label="編集" />',
|
|
268
|
+
'```',
|
|
269
|
+
'',
|
|
270
|
+
'> Button は size に応じてアイコンサイズを自動設定する(sm→4, md→5, lg→6)。isLoading 時にアイコンを自動で非表示にする。',
|
|
271
|
+
]),
|
|
272
|
+
jsdocTargets: [
|
|
273
|
+
{
|
|
274
|
+
file: 'src/components/ui/button/index.tsx',
|
|
275
|
+
targetName: 'Button',
|
|
276
|
+
section: {
|
|
277
|
+
bullets: [
|
|
278
|
+
{
|
|
279
|
+
ja: '`<Icon>` を children として渡さないでください。`prefixIcon` / `suffixIcon` props を使用してください。',
|
|
280
|
+
en: 'Do not pass `<Icon>` as children. Use `prefixIcon` / `suffixIcon` props instead.',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
ja: 'アイコンのみのボタンには `IconButton` を使用してください。',
|
|
284
|
+
en: 'Use `IconButton` for icon-only buttons.',
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
example: lines([
|
|
288
|
+
'// ✅ Correct',
|
|
289
|
+
'<Button prefixIcon="check">確定</Button>',
|
|
290
|
+
'',
|
|
291
|
+
'// ❌ Wrong - Icon を children に入れない',
|
|
292
|
+
'<Button><Icon icon="check" /> 確定</Button>',
|
|
293
|
+
'',
|
|
294
|
+
'// ❌ Wrong - アイコンのみは IconButton を使う',
|
|
295
|
+
'<Button><Icon icon="edit" /></Button>',
|
|
296
|
+
'// ✅ Correct',
|
|
297
|
+
'<IconButton icon="edit" aria-label="編集" />',
|
|
298
|
+
]),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 'card-control',
|
|
305
|
+
featureSection: lines([
|
|
306
|
+
'### CardHeader: CardControl を使う',
|
|
307
|
+
'',
|
|
308
|
+
'```tsx',
|
|
309
|
+
'// ✅ Correct',
|
|
310
|
+
'<CardHeader>',
|
|
311
|
+
' <CardTitle>タイトル</CardTitle>',
|
|
312
|
+
' <CardControl>',
|
|
313
|
+
' <Button>アクション</Button>',
|
|
314
|
+
' </CardControl>',
|
|
315
|
+
'</CardHeader>',
|
|
316
|
+
'',
|
|
317
|
+
'// ❌ Wrong - 手動 flex を使わない(CardHeader は内部で flex justify-between を適用済み)',
|
|
318
|
+
'<CardHeader>',
|
|
319
|
+
' <div className="flex justify-between">',
|
|
320
|
+
' <CardTitle>タイトル</CardTitle>',
|
|
321
|
+
' <Button>アクション</Button>',
|
|
322
|
+
' </div>',
|
|
323
|
+
'</CardHeader>',
|
|
324
|
+
'```',
|
|
325
|
+
]),
|
|
326
|
+
jsdocTargets: [
|
|
327
|
+
{
|
|
328
|
+
file: 'src/components/ui/card/index.tsx',
|
|
329
|
+
targetName: 'CardHeader',
|
|
330
|
+
section: {
|
|
331
|
+
bullets: [
|
|
332
|
+
{
|
|
333
|
+
ja: 'CardHeader 内で手動の flex レイアウト(`<div className="flex justify-between">`)を使わないでください。CardHeader は内部で flex レイアウトを適用済みです。アクションボタンは `CardControl` で囲んでください。',
|
|
334
|
+
en: 'Do not use manual flex layout inside CardHeader. CardHeader already applies flex layout internally. Wrap action buttons with `CardControl`.',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
ja: 'CardTitle の補足情報や件数は、`span` などを直書きせず `CardDescription` を使ってください。',
|
|
338
|
+
en: 'For supporting text or counts inside CardTitle, do not inline a `span`; use `CardDescription`.',
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
example: lines([
|
|
342
|
+
'// ✅ Correct',
|
|
343
|
+
'<CardHeader>',
|
|
344
|
+
' <CardTitle>',
|
|
345
|
+
' タイトル',
|
|
346
|
+
' <CardDescription className="character-3-regular-pro text-text-low">',
|
|
347
|
+
' 全 12 件',
|
|
348
|
+
' </CardDescription>',
|
|
349
|
+
' </CardTitle>',
|
|
350
|
+
' <CardControl>',
|
|
351
|
+
' <Button>アクション</Button>',
|
|
352
|
+
' </CardControl>',
|
|
353
|
+
'</CardHeader>',
|
|
354
|
+
'',
|
|
355
|
+
'// ❌ Wrong - 手動 flex を使わない',
|
|
356
|
+
'<CardHeader>',
|
|
357
|
+
' <div className="flex justify-between">',
|
|
358
|
+
' <CardTitle>タイトル</CardTitle>',
|
|
359
|
+
' <Button>アクション</Button>',
|
|
360
|
+
' </div>',
|
|
361
|
+
'</CardHeader>',
|
|
362
|
+
'',
|
|
363
|
+
'// ❌ Wrong - CardTitle 内に補足を入れない',
|
|
364
|
+
'<CardHeader>',
|
|
365
|
+
' <CardTitle>',
|
|
366
|
+
' タイトル',
|
|
367
|
+
' <span className="text-sm text-neutral-500">全 12 件</span>',
|
|
368
|
+
' </CardTitle>',
|
|
369
|
+
'</CardHeader>',
|
|
370
|
+
]),
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: 'icon-scale',
|
|
377
|
+
featureSection: lines([
|
|
378
|
+
'### Icon / Spinner: スケール値(1-12)を使う',
|
|
379
|
+
'',
|
|
380
|
+
'```tsx',
|
|
381
|
+
'// ✅ Correct - スケール値',
|
|
382
|
+
'<Icon icon="settings" size={6} /> // 24px 相当',
|
|
383
|
+
'',
|
|
384
|
+
'// ❌ Wrong - ピクセル値は受け付けない',
|
|
385
|
+
'<Icon icon="settings" size={24} />',
|
|
386
|
+
'```',
|
|
387
|
+
'',
|
|
388
|
+
'> スケール対応表は上記「Icon Size Scale」セクション参照。',
|
|
389
|
+
]),
|
|
390
|
+
jsdocTargets: [
|
|
391
|
+
{
|
|
392
|
+
file: 'src/components/ui/icon/index.tsx',
|
|
393
|
+
targetName: 'Icon',
|
|
394
|
+
section: {
|
|
395
|
+
bullets: [
|
|
396
|
+
{
|
|
397
|
+
ja: '`size` にピクセル値(24, 32 など)を渡さないでください。スケール値(1-12)を使用してください。',
|
|
398
|
+
en: 'Do not pass pixel values to `size`. Use scale values (1-12) instead.',
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
example: lines([
|
|
402
|
+
'// ✅ Correct - スケール値',
|
|
403
|
+
'<Icon icon="settings" size={6} /> // 24px 相当',
|
|
404
|
+
'',
|
|
405
|
+
'// ❌ Wrong - ピクセル値',
|
|
406
|
+
'<Icon icon="settings" size={24} />',
|
|
407
|
+
]),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
file: 'src/components/ui/spinner/index.tsx',
|
|
412
|
+
targetName: 'Spinner',
|
|
413
|
+
section: {
|
|
414
|
+
bullets: [
|
|
415
|
+
{
|
|
416
|
+
ja: '`size` にピクセル値(24, 32 など)を渡さないでください。スケール値(1-12)を使用してください。',
|
|
417
|
+
en: 'Do not pass pixel values to `size`. Use scale values (1-12) instead.',
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
example: lines([
|
|
421
|
+
'// ✅ Correct - スケール値',
|
|
422
|
+
'<Spinner size={6} />',
|
|
423
|
+
'',
|
|
424
|
+
'// ❌ Wrong - ピクセル値',
|
|
425
|
+
'<Spinner size={24} />',
|
|
426
|
+
]),
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'dialog-button-wrap',
|
|
433
|
+
check: {
|
|
434
|
+
description: 'DialogCancel/DialogAction を Button で二重ラップしない',
|
|
435
|
+
recommendation:
|
|
436
|
+
'DialogCancel と DialogAction は内部で Button を描画するので、children に Button を入れないでください。',
|
|
437
|
+
pattern: /<(DialogCancel|DialogAction)\b[^>]*>\s*<Button\b/g,
|
|
438
|
+
},
|
|
439
|
+
featureSection: lines([
|
|
440
|
+
'### DialogCancel / DialogAction: Button で二重ラップしない',
|
|
441
|
+
'',
|
|
442
|
+
'```tsx',
|
|
443
|
+
'// ✅ Correct - 文字列を渡すだけ(内部で Button を描画)',
|
|
444
|
+
'<DialogCancel>キャンセル</DialogCancel>',
|
|
445
|
+
'<DialogAction>確定</DialogAction>',
|
|
446
|
+
'',
|
|
447
|
+
'// ❌ Wrong - 二重ラップ',
|
|
448
|
+
'<DialogCancel><Button>キャンセル</Button></DialogCancel>',
|
|
449
|
+
'```',
|
|
450
|
+
]),
|
|
451
|
+
jsdocTargets: [
|
|
452
|
+
{
|
|
453
|
+
file: 'src/components/ui/dialog/index.tsx',
|
|
454
|
+
targetName: 'DialogCancel',
|
|
455
|
+
section: {
|
|
456
|
+
bullets: [
|
|
457
|
+
{
|
|
458
|
+
ja: '`DialogCancel` の children に `<Button>` を渡さないでください。内部で Button を描画します。',
|
|
459
|
+
en: 'Do not pass `<Button>` as children. DialogCancel renders a Button internally.',
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
example: lines([
|
|
463
|
+
'// ✅ Correct',
|
|
464
|
+
'<DialogCancel>キャンセル</DialogCancel>',
|
|
465
|
+
'',
|
|
466
|
+
'// ❌ Wrong - 二重ラップ',
|
|
467
|
+
'<DialogCancel><Button>キャンセル</Button></DialogCancel>',
|
|
468
|
+
]),
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
file: 'src/components/ui/dialog/index.tsx',
|
|
473
|
+
targetName: 'DialogAction',
|
|
474
|
+
section: {
|
|
475
|
+
bullets: [
|
|
476
|
+
{
|
|
477
|
+
ja: '`DialogAction` の children に `<Button>` を渡さないでください。内部で Button を描画します。',
|
|
478
|
+
en: 'Do not pass `<Button>` as children. DialogAction renders a Button internally.',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
example: lines([
|
|
482
|
+
'// ✅ Correct',
|
|
483
|
+
'<DialogAction>確定</DialogAction>',
|
|
484
|
+
'',
|
|
485
|
+
'// ❌ Wrong - 二重ラップ',
|
|
486
|
+
'<DialogAction><Button>確定</Button></DialogAction>',
|
|
487
|
+
]),
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: 'dialog-form',
|
|
494
|
+
check: {
|
|
495
|
+
description: 'フォーム入力に Dialog を使わない',
|
|
496
|
+
recommendation: '確認ダイアログは Dialog、フォーム入力や詳細表示は Modal を使ってください。',
|
|
497
|
+
pattern:
|
|
498
|
+
/<Dialog(?:\s|>)[\s\S]*?<(Input|Select|Textarea|Checkbox|Radio|Switch|Form|FormField|FormItem)\b[\s\S]*?<\/Dialog>/g,
|
|
499
|
+
},
|
|
500
|
+
featureSection: lines([
|
|
501
|
+
'### Dialog / Modal: 用途を混ぜない',
|
|
502
|
+
'',
|
|
503
|
+
'| コンポーネント | ベース | 用途 | 例 |',
|
|
504
|
+
'| --- | --- | --- | --- |',
|
|
505
|
+
'| **Dialog** | `AlertDialog` | アクション確認 | 削除確認、再試行確認、保存前の警告 |',
|
|
506
|
+
'| **Modal** | `Dialog` | フォーム入力・情報表示 | 作成/編集フォーム、詳細表示 |',
|
|
507
|
+
'',
|
|
508
|
+
'```tsx',
|
|
509
|
+
'// ✅ Correct - フォーム入力には Modal を使う',
|
|
510
|
+
'<Modal>',
|
|
511
|
+
' <ModalContent size="md">',
|
|
512
|
+
' <ModalHeader>',
|
|
513
|
+
' <ModalTitle>ユーザー作成</ModalTitle>',
|
|
514
|
+
' <ModalClose />',
|
|
515
|
+
' </ModalHeader>',
|
|
516
|
+
' <ModalBody isSpace>',
|
|
517
|
+
' <Input />',
|
|
518
|
+
' <Select />',
|
|
519
|
+
' </ModalBody>',
|
|
520
|
+
' <ModalFooter>',
|
|
521
|
+
' <Button variant="ghost">キャンセル</Button>',
|
|
522
|
+
' <Button>作成</Button>',
|
|
523
|
+
' </ModalFooter>',
|
|
524
|
+
' </ModalContent>',
|
|
525
|
+
'</Modal>',
|
|
526
|
+
'',
|
|
527
|
+
'// ❌ Wrong - フォーム入力に Dialog を使わない',
|
|
528
|
+
'<Dialog>',
|
|
529
|
+
' <DialogContent>',
|
|
530
|
+
' <DialogHeader>',
|
|
531
|
+
' <DialogTitle>ユーザー作成</DialogTitle>',
|
|
532
|
+
' </DialogHeader>',
|
|
533
|
+
' <Input />',
|
|
534
|
+
' <Select />',
|
|
535
|
+
' <DialogFooter>',
|
|
536
|
+
' <DialogCancel>キャンセル</DialogCancel>',
|
|
537
|
+
' <DialogAction>作成</DialogAction>',
|
|
538
|
+
' </DialogFooter>',
|
|
539
|
+
' </DialogContent>',
|
|
540
|
+
'</Dialog>',
|
|
541
|
+
'```',
|
|
542
|
+
]),
|
|
543
|
+
jsdocTargets: [
|
|
544
|
+
{
|
|
545
|
+
file: 'src/components/ui/dialog/index.tsx',
|
|
546
|
+
targetName: 'Dialog',
|
|
547
|
+
section: {
|
|
548
|
+
bullets: [
|
|
549
|
+
{
|
|
550
|
+
ja: 'フォーム入力や詳細表示には使わず、削除確認や再試行確認などのアクション確認に使ってください。',
|
|
551
|
+
en: 'Do not use Dialog for forms or detail views. Use it for action confirmations such as delete and retry flows.',
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
example: lines([
|
|
555
|
+
'// ✅ Correct',
|
|
556
|
+
'<Dialog>',
|
|
557
|
+
' <DialogContent>',
|
|
558
|
+
' <DialogHeader>',
|
|
559
|
+
' <DialogTitle>削除確認</DialogTitle>',
|
|
560
|
+
' <DialogDescription>本当に削除しますか?</DialogDescription>',
|
|
561
|
+
' </DialogHeader>',
|
|
562
|
+
' <DialogFooter>',
|
|
563
|
+
' <DialogCancel>キャンセル</DialogCancel>',
|
|
564
|
+
' <DialogAction>削除</DialogAction>',
|
|
565
|
+
' </DialogFooter>',
|
|
566
|
+
' </DialogContent>',
|
|
567
|
+
'</Dialog>',
|
|
568
|
+
'',
|
|
569
|
+
'// ❌ Wrong - フォーム入力に Dialog を使わない',
|
|
570
|
+
'<Dialog>',
|
|
571
|
+
' <DialogContent>',
|
|
572
|
+
' <Input />',
|
|
573
|
+
' <Select />',
|
|
574
|
+
' </DialogContent>',
|
|
575
|
+
'</Dialog>',
|
|
576
|
+
]),
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
file: 'src/components/ui/modal/index.tsx',
|
|
581
|
+
targetName: 'Modal',
|
|
582
|
+
section: {
|
|
583
|
+
bullets: [
|
|
584
|
+
{
|
|
585
|
+
ja: '作成/編集フォームや詳細表示など、確認だけではない情報入力・閲覧に使ってください。',
|
|
586
|
+
en: 'Use Modal for forms and detail views such as create and edit flows, not for simple action confirmation.',
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
example: lines([
|
|
590
|
+
'// ✅ Correct',
|
|
591
|
+
'<Modal>',
|
|
592
|
+
' <ModalContent size="md">',
|
|
593
|
+
' <ModalHeader>',
|
|
594
|
+
' <ModalTitle>ユーザー作成</ModalTitle>',
|
|
595
|
+
' <ModalClose />',
|
|
596
|
+
' </ModalHeader>',
|
|
597
|
+
' <ModalBody isSpace>',
|
|
598
|
+
' <Input />',
|
|
599
|
+
' <Select />',
|
|
600
|
+
' </ModalBody>',
|
|
601
|
+
' <ModalFooter>',
|
|
602
|
+
' <Button>作成</Button>',
|
|
603
|
+
' </ModalFooter>',
|
|
604
|
+
' </ModalContent>',
|
|
605
|
+
'</Modal>',
|
|
606
|
+
]),
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
id: 'input-trigger',
|
|
613
|
+
featureSection: lines([
|
|
614
|
+
'### Input: 組み込みトリガーを使う',
|
|
615
|
+
'',
|
|
616
|
+
'```tsx',
|
|
617
|
+
'// ✅ Correct',
|
|
618
|
+
'<Input isTrigger triggerIcon="search" triggerAriaLabel="検索" onIconButtonClick={handleSearch} />',
|
|
619
|
+
'',
|
|
620
|
+
'// ❌ Wrong - 手動で IconButton を配置しない',
|
|
621
|
+
'<div className="flex">',
|
|
622
|
+
' <Input />',
|
|
623
|
+
' <IconButton icon="search" aria-label="検索" />',
|
|
624
|
+
'</div>',
|
|
625
|
+
'```',
|
|
626
|
+
]),
|
|
627
|
+
jsdocTargets: [],
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
id: 'link-open-in-new',
|
|
631
|
+
featureSection: lines([
|
|
632
|
+
'### Link: isOpenInNew を使う',
|
|
633
|
+
'',
|
|
634
|
+
'```tsx',
|
|
635
|
+
'// ✅ Correct - 自動で open_in_new アイコンが表示される',
|
|
636
|
+
'<Link href="https://example.com" isOpenInNew>外部リンク</Link>',
|
|
637
|
+
'',
|
|
638
|
+
'// ❌ Wrong - 手動でアイコンを追加しない',
|
|
639
|
+
'<Link href="https://example.com">外部リンク <Icon icon="open_in_new" /></Link>',
|
|
640
|
+
'```',
|
|
641
|
+
]),
|
|
642
|
+
jsdocTargets: [
|
|
643
|
+
{
|
|
644
|
+
file: 'src/components/ui/link/index.tsx',
|
|
645
|
+
targetName: 'Link',
|
|
646
|
+
section: {
|
|
647
|
+
bullets: [
|
|
648
|
+
{
|
|
649
|
+
ja: '外部リンクアイコンを手動で追加しないでください。`isOpenInNew` prop が自動で `open_in_new` アイコンを表示します。',
|
|
650
|
+
en: 'Do not manually add external link icons. The `isOpenInNew` prop automatically displays the `open_in_new` icon.',
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
example: lines([
|
|
654
|
+
'// ✅ Correct',
|
|
655
|
+
'<Link href="https://example.com" isOpenInNew>外部リンク</Link>',
|
|
656
|
+
'',
|
|
657
|
+
'// ❌ Wrong - 手動アイコン',
|
|
658
|
+
'<Link href="https://example.com">外部リンク <Icon icon="open_in_new" /></Link>',
|
|
659
|
+
]),
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
];
|
|
665
|
+
|
|
666
|
+
function getCheckRules() {
|
|
667
|
+
const order = ['dialog-form', 'dialog-button-wrap', 'shadcn-token'];
|
|
668
|
+
|
|
669
|
+
return ANTI_PATTERN_GROUPS.filter((group) => group.check)
|
|
670
|
+
.map((group) => ({
|
|
671
|
+
id: group.id,
|
|
672
|
+
...group.check,
|
|
673
|
+
}))
|
|
674
|
+
.sort((left, right) => {
|
|
675
|
+
const leftIndex = order.indexOf(left.id);
|
|
676
|
+
const rightIndex = order.indexOf(right.id);
|
|
677
|
+
const normalizedLeftIndex = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
|
678
|
+
const normalizedRightIndex = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
|
679
|
+
return normalizedLeftIndex - normalizedRightIndex;
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function getAllJSDocTargets() {
|
|
684
|
+
return ANTI_PATTERN_GROUPS.flatMap((group) => group.jsdocTargets);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function renderFeatureSections() {
|
|
688
|
+
return ANTI_PATTERN_GROUPS.map((group) => group.featureSection).join('\n\n');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export {
|
|
692
|
+
ANTI_PATTERN_GROUPS,
|
|
693
|
+
COMMON_INTRO,
|
|
694
|
+
getAllJSDocTargets,
|
|
695
|
+
getCheckRules,
|
|
696
|
+
renderFeatureSections,
|
|
697
|
+
renderJSDocSection,
|
|
698
|
+
};
|
package/lib/check.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { getCheckRules } from './anti-pattern-rules.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TARGET = 'src';
|
|
7
|
+
const TEXT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
|
8
|
+
const RULES = getCheckRules();
|
|
9
|
+
|
|
10
|
+
function collectFiles(targetPath, files, visited) {
|
|
11
|
+
if (!fs.existsSync(targetPath)) {
|
|
12
|
+
throw new Error(`Target path does not exist: ${targetPath}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const realPath = fs.realpathSync(targetPath);
|
|
16
|
+
if (visited.has(realPath)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
visited.add(realPath);
|
|
20
|
+
|
|
21
|
+
const stat = fs.statSync(realPath);
|
|
22
|
+
|
|
23
|
+
if (stat.isFile()) {
|
|
24
|
+
if (TEXT_EXTENSIONS.has(path.extname(realPath))) {
|
|
25
|
+
files.add(realPath);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!stat.isDirectory()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const entry of fs.readdirSync(realPath, { withFileTypes: true })) {
|
|
35
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
collectFiles(path.join(realPath, entry.name), files, visited);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getLineNumber(content, index) {
|
|
44
|
+
return content.slice(0, index).split(/\r?\n/).length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatMatch(match) {
|
|
48
|
+
return match[0].replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectFindings(filePath) {
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
+
const findings = [];
|
|
54
|
+
|
|
55
|
+
for (const rule of RULES) {
|
|
56
|
+
for (const match of content.matchAll(rule.pattern)) {
|
|
57
|
+
findings.push({
|
|
58
|
+
filePath,
|
|
59
|
+
id: rule.id,
|
|
60
|
+
description: rule.description,
|
|
61
|
+
recommendation: rule.recommendation,
|
|
62
|
+
line: getLineNumber(content, match.index ?? 0),
|
|
63
|
+
snippet: formatMatch(match),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function checkProject(targets = [], options = {}) {
|
|
72
|
+
const resolvedTargets = targets.length > 0 ? targets : [DEFAULT_TARGET];
|
|
73
|
+
const files = new Set();
|
|
74
|
+
const visited = new Set();
|
|
75
|
+
|
|
76
|
+
for (const target of resolvedTargets) {
|
|
77
|
+
collectFiles(path.resolve(process.cwd(), target), files, visited);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const findings = [...files].flatMap(collectFindings);
|
|
81
|
+
|
|
82
|
+
if (findings.length === 0) {
|
|
83
|
+
console.log('sparkle-design-cli check: no findings');
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`sparkle-design-cli check: ${findings.length} finding(s)\n`);
|
|
88
|
+
|
|
89
|
+
for (const finding of findings) {
|
|
90
|
+
const relativePath = path.relative(process.cwd(), finding.filePath);
|
|
91
|
+
console.log(`${relativePath}:${finding.line} [${finding.id}] ${finding.description}`);
|
|
92
|
+
console.log(` Recommendation: ${finding.recommendation}`);
|
|
93
|
+
console.log(` Snippet: ${finding.snippet}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (options.strict) {
|
|
98
|
+
console.error('sparkle-design-cli check: failed because --strict was specified');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { RULES, collectFindings };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
4
4
|
"description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
|
|
5
5
|
"main": "lib/generate-css.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"lint": "eslint bin/ lib/ test/",
|
|
14
14
|
"lint:fix": "eslint bin/ lib/ test/ --fix",
|
|
15
15
|
"format": "prettier --write bin/ lib/ test/ *.md *.json",
|
|
16
|
-
"format:check": "prettier --check bin/ lib/ test/ *.md *.json"
|
|
16
|
+
"format:check": "prettier --check bin/ lib/ test/ *.md *.json",
|
|
17
|
+
"sync:anti-pattern-docs": "node scripts/sync-anti-pattern-docs.mjs"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"css",
|