sparkle-design-cli 1.4.1 → 1.5.1
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 +40 -58
- package/bin/sparkle-design.js +26 -10
- package/lib/anti-pattern-rules.js +216 -1
- package/lib/generate-css.js +128 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,6 +111,14 @@ sparkle-design-cli check --help
|
|
|
111
111
|
- children なしの Button に prefixIcon / suffixIcon を使わない
|
|
112
112
|
- Material Symbols を className 直書きで使わない
|
|
113
113
|
- shadcn/ui 既定 token を Sparkle Design 内へ持ち込まない
|
|
114
|
+
- Tailwind デフォルト typography(text-sm, font-medium 等)を使わない
|
|
115
|
+
- CardTitle に typography を上書きしない
|
|
116
|
+
- CardControl に Button / IconButton 以外を入れない
|
|
117
|
+
- CardHeader / CardContent の padding を上書きしない
|
|
118
|
+
- asChild と prefixIcon / suffixIcon / isLoading を併用しない
|
|
119
|
+
- disabled を isDisabled の代わりに使わない
|
|
120
|
+
- Button の prefixIcon に JSX を渡さない
|
|
121
|
+
- Icon の children にテキストを渡さない
|
|
114
122
|
|
|
115
123
|
#### Manual Review Reminders
|
|
116
124
|
|
|
@@ -119,6 +127,7 @@ sparkle-design-cli check --help
|
|
|
119
127
|
- Badge と Tag の意味的な使い分け
|
|
120
128
|
- CardDescription の typography / color token 明示
|
|
121
129
|
- Dialog と Modal の UX 上の使い分け
|
|
130
|
+
- 手書き span が Tag で置き換え可能か
|
|
122
131
|
|
|
123
132
|
#### 導入先での推奨設定
|
|
124
133
|
|
|
@@ -181,86 +190,59 @@ npx --yes sparkle-design-cli setup --assistant generic
|
|
|
181
190
|
#### Core(プラグインが出力)
|
|
182
191
|
|
|
183
192
|
- `primary`: プライマリカラー(blue, red, orange など)
|
|
184
|
-
- `font-pro`:
|
|
185
|
-
- `font-mono`:
|
|
193
|
+
- `font-pro`: プロポーショナルフォント([Google Fonts](https://fonts.google.com/) の名前)
|
|
194
|
+
- `font-mono`: モノスペースフォント([Google Fonts](https://fonts.google.com/) の名前)
|
|
186
195
|
- `radius`: 角丸設定(sm, md, lg など)
|
|
187
196
|
|
|
188
|
-
|
|
197
|
+
### extend セクション(拡張設定)
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
- `font-mono-weights`: (オプション)モノスペースフォントのウェイト配列(デフォルト: `[400, 700]`)
|
|
192
|
-
- `source-packages`: (オプション)`@source` ディレクティブで追加スキャンする npm パッケージの配列
|
|
193
|
-
|
|
194
|
-
#### Custom Settings
|
|
195
|
-
|
|
196
|
-
- `custom-css`: (オプション)プロジェクト固有のカスタムトークン CSS ファイルのパス。`globals.css` に `@import` が自動挿入される
|
|
197
|
-
|
|
198
|
-
### フォントウェイトとフォールバック
|
|
199
|
-
|
|
200
|
-
`font-pro-weights` / `font-mono-weights` でフォントウェイトをカスタマイズできます。未指定時は `[400, 700]` がデフォルトです。
|
|
201
|
-
|
|
202
|
-
`font-pro` / `font-mono` に配列を指定すると、フォントフォールバックチェーンとして複数の Google Fonts import が生成されます。
|
|
199
|
+
Figma プラグインが出力する基本4項目に加えて、プロジェクト固有の拡張を `extend` セクションにまとめます。`extend` はオブジェクト直書き(推奨)またはファイルパスを指定できます。
|
|
203
200
|
|
|
204
201
|
```json
|
|
205
202
|
{
|
|
206
203
|
"primary": "blue",
|
|
207
|
-
"font-pro":
|
|
208
|
-
"font-pro-weights": [400, 500, 600, 700],
|
|
204
|
+
"font-pro": "Montserrat",
|
|
209
205
|
"font-mono": "Roboto Mono",
|
|
210
|
-
"
|
|
211
|
-
"
|
|
206
|
+
"radius": "md",
|
|
207
|
+
"extend": {
|
|
208
|
+
"fonts": {
|
|
209
|
+
"pro": [
|
|
210
|
+
{ "family": "Montserrat", "weights": [500, 600, 700] },
|
|
211
|
+
{ "family": "Noto Sans JP", "weights": [400, 500, 600, 700] }
|
|
212
|
+
],
|
|
213
|
+
"mono": [
|
|
214
|
+
{ "family": "Roboto Mono", "weights": [400, 700] }
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"source-packages": ["@goodpatch/sparkle-design-internal"],
|
|
218
|
+
"custom-css": "./src/app/custom-tokens.css"
|
|
219
|
+
}
|
|
212
220
|
}
|
|
213
221
|
```
|
|
214
222
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
```css
|
|
218
|
-
--font-family-pro: 'Montserrat', 'Noto Sans JP', sans-serif;
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
```css
|
|
222
|
-
@import 'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap';
|
|
223
|
-
@import 'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;600;700&display=swap';
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### custom-css オプション
|
|
227
|
-
|
|
228
|
-
プロジェクト固有のカスタムトークン(Display, Heading, Body 等のタイポグラフィトークン、追加のフォントウェイト、メディアクエリなど)を `sparkle-design.css` に直接追加するのは避けてください。`generate` 実行時に上書きされます。
|
|
229
|
-
|
|
230
|
-
代わりに、カスタムトークンを別の CSS ファイルに定義し、`custom-css` で指定してください:
|
|
223
|
+
ファイル参照も可能です:
|
|
231
224
|
|
|
232
225
|
```json
|
|
233
226
|
{
|
|
234
|
-
"
|
|
227
|
+
"primary": "blue",
|
|
228
|
+
"font-pro": "Montserrat",
|
|
229
|
+
"font-mono": "Roboto Mono",
|
|
230
|
+
"radius": "md",
|
|
231
|
+
"extend": "./sparkle.extend.json"
|
|
235
232
|
}
|
|
236
233
|
```
|
|
237
234
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
```css
|
|
241
|
-
/* globals.css に自動生成される構造 */
|
|
242
|
-
/* フォントのインポート */
|
|
243
|
-
@import '...google fonts...';
|
|
244
|
-
@import "tailwindcss";
|
|
245
|
-
/* Sparkle Design のカスタム定義 */
|
|
246
|
-
@import "./sparkle-design.css";
|
|
247
|
-
/* プロジェクト固有のカスタムトークン */
|
|
248
|
-
@import "./custom-tokens.css";
|
|
249
|
-
```
|
|
235
|
+
#### extend.fonts
|
|
250
236
|
|
|
251
|
-
|
|
237
|
+
フォントごとにウェイトを個別指定。`extend.fonts` がない場合は `font-pro` / `font-mono` + デフォルトウェイト `[400, 700]` が使われます。`extend.fonts` が存在しても `fonts.pro` / `fonts.mono` のどちらかが未指定なら、そのスロットのみ `font-pro` / `font-mono` にフォールバックします。同じフォントファミリーが `pro` と `mono` で重複する場合、ウェイトはマージされ import は 1 行に統合されます。
|
|
252
238
|
|
|
253
|
-
|
|
239
|
+
#### extend.source-packages
|
|
254
240
|
|
|
255
|
-
|
|
241
|
+
`@goodpatch/sparkle-design` を npm パッケージとして利用する場合に必須。`globals.css` に `@source` ディレクティブを自動挿入します。`@goodpatch/sparkle-design` は常にデフォルトで含まれます。
|
|
256
242
|
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
"source-packages": ["@scope/my-extension-design"]
|
|
260
|
-
}
|
|
261
|
-
```
|
|
243
|
+
#### extend.custom-css
|
|
262
244
|
|
|
263
|
-
|
|
245
|
+
プロジェクト固有のカスタムトークンの CSS ファイルパス。`globals.css` に `@import` が自動挿入されます。`sparkle-design.css` に直接追加すると `generate` 実行時に上書きされるため、必ず別ファイルに分離してください。
|
|
264
246
|
|
|
265
247
|
## 出力
|
|
266
248
|
|
package/bin/sparkle-design.js
CHANGED
|
@@ -145,18 +145,26 @@ Generate options:
|
|
|
145
145
|
-o, --output <path> 出力ファイルのパス (default: ./src/app/sparkle-design.css)
|
|
146
146
|
|
|
147
147
|
sparkle.config.json の設定フィールド:
|
|
148
|
+
|
|
149
|
+
基本設定(Figma プラグインで出力可能):
|
|
148
150
|
primary プライマリカラー (blue, red, orange, green, purple, pink, yellow など)
|
|
149
151
|
font-pro プロポーショナルフォント (Google Fonts の名前)
|
|
150
|
-
|
|
151
|
-
例: ["Montserrat", "Noto Sans JP"]
|
|
152
|
-
font-mono モノスペースフォント (Google Fonts の名前、配列も可)
|
|
152
|
+
font-mono モノスペースフォント (Google Fonts の名前)
|
|
153
153
|
radius 角丸設定 (none, sm, md, lg, xl, full)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
|
|
155
|
+
拡張設定(extend セクション、任意):
|
|
156
|
+
extend オブジェクト直書き、またはファイルパス(例: "./sparkle.extend.json")
|
|
157
|
+
extend.fonts.pro プロフォント定義。フォントごとにウェイトを指定可能
|
|
158
|
+
文字列: "Inter"
|
|
159
|
+
配列: [{ "family": "Montserrat", "weights": [500, 600, 700] },
|
|
160
|
+
{ "family": "Noto Sans JP", "weights": [400, 500, 600, 700] }]
|
|
161
|
+
extend.fonts.mono モノスペースフォント定義(形式は fonts.pro と同じ)
|
|
162
|
+
extend.source-packages @source で追加スキャンする npm パッケージ名の配列
|
|
163
|
+
extend.custom-css プロジェクト固有のカスタムトークン CSS ファイルパス
|
|
164
|
+
|
|
165
|
+
extend.fonts がある場合、font-pro / font-mono より優先される。
|
|
166
|
+
ない場合は font-pro / font-mono + デフォルトウェイト [400, 700] が使われる。
|
|
167
|
+
同じフォントファミリーが pro と mono で重複する場合、ウェイトはマージされ import は 1 行になる。
|
|
160
168
|
|
|
161
169
|
設定ファイルは Sparkle Design Theme Settings Figma プラグインから書き出せます:
|
|
162
170
|
https://www.figma.com/community/plugin/1443500367756891364
|
|
@@ -178,7 +186,15 @@ Check で検出する主なパターン:
|
|
|
178
186
|
- DialogCancel / DialogAction を <Button> で二重ラップしている
|
|
179
187
|
- children なしの Button に prefixIcon / suffixIcon を使っている
|
|
180
188
|
- Material Symbols を className 直書きしている
|
|
181
|
-
- text-muted-foreground
|
|
189
|
+
- shadcn/ui 既定 token(text-muted-foreground 等)を持ち込んでいる
|
|
190
|
+
- Tailwind デフォルト typography(text-sm, font-medium 等)を使っている
|
|
191
|
+
- CardTitle に typography を上書きしている
|
|
192
|
+
- CardControl に Button / IconButton 以外を入れている
|
|
193
|
+
- CardHeader / CardContent の padding を上書きしている
|
|
194
|
+
- asChild と prefixIcon / suffixIcon / isLoading を併用している
|
|
195
|
+
- disabled を isDisabled の代わりに使っている
|
|
196
|
+
- Button の prefixIcon に JSX を渡している
|
|
197
|
+
- Icon の children にテキストを渡している
|
|
182
198
|
|
|
183
199
|
JSON 出力では manualReviewReminders も返すため、AI から実行する場合は --format json を推奨
|
|
184
200
|
`);
|
|
@@ -40,6 +40,11 @@ const MANUAL_REVIEW_REMINDERS = [
|
|
|
40
40
|
message:
|
|
41
41
|
'Dialog が確認用途、Modal が入力・詳細表示用途になっているか確認してください。文脈判断が必要なケースは機械検出されません。',
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
id: 'tag-vs-handwritten-span',
|
|
45
|
+
message:
|
|
46
|
+
'ステータスバッジ風の手書き span(bg-*-100 text-*-700 rounded-full 等)が Tag コンポーネントで置き換え可能か確認してください。',
|
|
47
|
+
},
|
|
43
48
|
];
|
|
44
49
|
|
|
45
50
|
const ANTI_PATTERN_GROUPS = [
|
|
@@ -859,10 +864,220 @@ const ANTI_PATTERN_GROUPS = [
|
|
|
859
864
|
},
|
|
860
865
|
],
|
|
861
866
|
},
|
|
867
|
+
{
|
|
868
|
+
id: 'tailwind-typography',
|
|
869
|
+
check: {
|
|
870
|
+
description: 'Tailwind デフォルト typography を Sparkle Design コンポーネント内で使わない',
|
|
871
|
+
recommendation:
|
|
872
|
+
'text-sm / text-xs / text-base / font-medium / font-semibold / font-bold は character-* utility に置き換えてください。',
|
|
873
|
+
pattern: /\b(text-(?:xs|sm|base|lg|xl|2xl)|font-(?:medium|semibold|bold|normal|light))\b/g,
|
|
874
|
+
},
|
|
875
|
+
featureSection: lines([
|
|
876
|
+
'### Tailwind デフォルト typography を使わない',
|
|
877
|
+
'',
|
|
878
|
+
'```tsx',
|
|
879
|
+
'// ✅ Correct — character-* utility を使う',
|
|
880
|
+
'<span className="character-3-regular-pro">テキスト</span>',
|
|
881
|
+
'',
|
|
882
|
+
'// ❌ Wrong — Tailwind デフォルトの typography を使わない',
|
|
883
|
+
'<span className="text-sm font-medium">テキスト</span>',
|
|
884
|
+
'```',
|
|
885
|
+
'',
|
|
886
|
+
'Sparkle Design コンポーネント内では `character-*-pro` / `character-*-mono` を使用する。Tailwind の `text-sm` / `font-medium` 等は使わない。',
|
|
887
|
+
]),
|
|
888
|
+
jsdocTargets: [],
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
id: 'card-title-typography',
|
|
892
|
+
check: {
|
|
893
|
+
description: 'CardTitle に typography 系クラスを付与しない',
|
|
894
|
+
recommendation:
|
|
895
|
+
'CardTitle は character-4-bold-pro を内蔵しています。className で typography を上書きしないでください。',
|
|
896
|
+
pattern: /<CardTitle\b[^>]*\bclassName\s*=\s*(?:"[^"]*\bcharacter-[^"]*"|'[^']*\bcharacter-[^']*'|\{[^}]*\bcharacter-[^}]*\})[^>]*>/g,
|
|
897
|
+
},
|
|
898
|
+
featureSection: lines([
|
|
899
|
+
'### CardTitle に typography を上書きしない',
|
|
900
|
+
'',
|
|
901
|
+
'```tsx',
|
|
902
|
+
'// ✅ Correct — CardTitle はデフォルトで character-4-bold-pro が適用される',
|
|
903
|
+
'<CardTitle>タイトル</CardTitle>',
|
|
904
|
+
'',
|
|
905
|
+
'// ❌ Wrong — typography を上書きしない',
|
|
906
|
+
'<CardTitle className="character-2-bold-pro">タイトル</CardTitle>',
|
|
907
|
+
'```',
|
|
908
|
+
]),
|
|
909
|
+
jsdocTargets: [],
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
id: 'card-control-non-button',
|
|
913
|
+
check: {
|
|
914
|
+
description: 'CardControl に Button / IconButton 以外を入れない',
|
|
915
|
+
recommendation:
|
|
916
|
+
'CardControl はアクションボタン用です。ステータス表示には CardDescription を使ってください。',
|
|
917
|
+
pattern: /<CardControl\b[^>]*>[\s\S]*?<(?!Button\b|IconButton\b|\/CardControl)\w+/g,
|
|
918
|
+
},
|
|
919
|
+
featureSection: lines([
|
|
920
|
+
'### CardControl にはアクションボタンのみを入れる',
|
|
921
|
+
'',
|
|
922
|
+
'```tsx',
|
|
923
|
+
'// ✅ Correct — Button / IconButton を CardControl に入れる',
|
|
924
|
+
'<CardControl>',
|
|
925
|
+
' <Button>保存</Button>',
|
|
926
|
+
'</CardControl>',
|
|
927
|
+
'',
|
|
928
|
+
'// ❌ Wrong — ステータス表示を CardControl に入れない(CardDescription を使う)',
|
|
929
|
+
'<CardControl>',
|
|
930
|
+
' <Tag status="negative">警告</Tag>',
|
|
931
|
+
'</CardControl>',
|
|
932
|
+
'```',
|
|
933
|
+
]),
|
|
934
|
+
jsdocTargets: [
|
|
935
|
+
{
|
|
936
|
+
file: 'src/components/ui/card/index.tsx',
|
|
937
|
+
targetName: 'CardControl',
|
|
938
|
+
section: {
|
|
939
|
+
bullets: [
|
|
940
|
+
{
|
|
941
|
+
ja: 'CardControl にはアクション用の Button / IconButton のみを入れてください。ステータス表示(Tag 等)は CardDescription に入れてください。',
|
|
942
|
+
en: 'CardControl is for action buttons (Button / IconButton) only. Place status displays (Tag, etc.) in CardDescription.',
|
|
943
|
+
},
|
|
944
|
+
],
|
|
945
|
+
example: lines([
|
|
946
|
+
'// ✅ Correct',
|
|
947
|
+
'<CardControl>',
|
|
948
|
+
' <Button theme="neutral" variant="outline">キャンセル</Button>',
|
|
949
|
+
' <Button>保存</Button>',
|
|
950
|
+
'</CardControl>',
|
|
951
|
+
'',
|
|
952
|
+
'// ❌ Wrong - ステータス表示を CardControl に入れない',
|
|
953
|
+
'<CardControl>',
|
|
954
|
+
' <Tag status="negative">警告</Tag>',
|
|
955
|
+
'</CardControl>',
|
|
956
|
+
]),
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
],
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
id: 'card-padding-override',
|
|
963
|
+
check: {
|
|
964
|
+
description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
|
|
965
|
+
recommendation:
|
|
966
|
+
'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
|
|
967
|
+
pattern: /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*(?:"[^"]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^"]*"|'[^']*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^']*'|\{[^}]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^}]*\})[^>]*>/g,
|
|
968
|
+
},
|
|
969
|
+
featureSection: lines([
|
|
970
|
+
'### Card 系コンポーネントの padding を上書きしない',
|
|
971
|
+
'',
|
|
972
|
+
'```tsx',
|
|
973
|
+
'// ✅ Correct — デフォルトの padding をそのまま使う',
|
|
974
|
+
'<CardHeader>',
|
|
975
|
+
' <CardTitle>タイトル</CardTitle>',
|
|
976
|
+
'</CardHeader>',
|
|
977
|
+
'',
|
|
978
|
+
'// ❌ Wrong — padding を安易に上書きしない',
|
|
979
|
+
'<CardHeader className="p-4 pb-2">',
|
|
980
|
+
' <CardTitle>タイトル</CardTitle>',
|
|
981
|
+
'</CardHeader>',
|
|
982
|
+
'```',
|
|
983
|
+
]),
|
|
984
|
+
jsdocTargets: [],
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
id: 'aschild-with-icon-props',
|
|
988
|
+
check: {
|
|
989
|
+
description: 'asChild 使用時に prefixIcon / suffixIcon / isLoading を使わない',
|
|
990
|
+
recommendation:
|
|
991
|
+
'asChild モードでは prefixIcon / suffixIcon / isLoading は無視されます。アイコン付きの Link が必要なら asChild を外してください。',
|
|
992
|
+
pattern: /<Button\b(?=[^>]*\basChild\b)(?=[^>]*\b(?:prefixIcon|suffixIcon|isLoading)\b)[^>]*>/g,
|
|
993
|
+
},
|
|
994
|
+
featureSection: lines([
|
|
995
|
+
'### asChild と prefixIcon / suffixIcon / isLoading を併用しない',
|
|
996
|
+
'',
|
|
997
|
+
'```tsx',
|
|
998
|
+
'// ✅ Correct',
|
|
999
|
+
'<Button asChild>',
|
|
1000
|
+
' <Link href="/items/new">新規作成</Link>',
|
|
1001
|
+
'</Button>',
|
|
1002
|
+
'',
|
|
1003
|
+
'// ❌ Wrong — asChild では prefixIcon は反映されない',
|
|
1004
|
+
'<Button asChild prefixIcon="add">',
|
|
1005
|
+
' <Link href="/items/new">新規作成</Link>',
|
|
1006
|
+
'</Button>',
|
|
1007
|
+
'```',
|
|
1008
|
+
]),
|
|
1009
|
+
jsdocTargets: [],
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
id: 'disabled-vs-is-disabled',
|
|
1013
|
+
check: {
|
|
1014
|
+
description: 'Sparkle Design コンポーネントでは isDisabled を使う',
|
|
1015
|
+
recommendation:
|
|
1016
|
+
'Button / Input / Checkbox / IconButton では disabled ではなく isDisabled を使ってください。',
|
|
1017
|
+
pattern: /<(Button|Input|Checkbox|IconButton)\b[^>]*\sdisabled(?!=\s*\{)\b/g,
|
|
1018
|
+
},
|
|
1019
|
+
featureSection: lines([
|
|
1020
|
+
'### disabled ではなく isDisabled を使う',
|
|
1021
|
+
'',
|
|
1022
|
+
'```tsx',
|
|
1023
|
+
'// ✅ Correct',
|
|
1024
|
+
'<Button isDisabled>確定</Button>',
|
|
1025
|
+
'<Input isDisabled placeholder="無効状態" />',
|
|
1026
|
+
'',
|
|
1027
|
+
'// ❌ Wrong — disabled 属性を直接使わない',
|
|
1028
|
+
'<Button disabled>確定</Button>',
|
|
1029
|
+
'```',
|
|
1030
|
+
'',
|
|
1031
|
+
'HTML 標準の `disabled` も互換のため受け付けますが、Sparkle Design のコードでは `isDisabled` に統一します。',
|
|
1032
|
+
]),
|
|
1033
|
+
jsdocTargets: [],
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
id: 'button-prefixicon-jsx',
|
|
1037
|
+
check: {
|
|
1038
|
+
description: 'Button の prefixIcon / suffixIcon に JSX を渡さない',
|
|
1039
|
+
recommendation:
|
|
1040
|
+
'prefixIcon / suffixIcon には Material Symbols のアイコン名(文字列)を渡してください。JSX(<Icon ... />)は渡さないでください。',
|
|
1041
|
+
pattern: /<Button\b[^>]*\b(?:prefixIcon|suffixIcon)\s*=\s*\{[^}]*</g,
|
|
1042
|
+
},
|
|
1043
|
+
featureSection: lines([
|
|
1044
|
+
'### Button の prefixIcon / suffixIcon に JSX を渡さない',
|
|
1045
|
+
'',
|
|
1046
|
+
'```tsx',
|
|
1047
|
+
'// ✅ Correct — 文字列でアイコン名を渡す',
|
|
1048
|
+
'<Button prefixIcon="bolt">アクション</Button>',
|
|
1049
|
+
'',
|
|
1050
|
+
'// ❌ Wrong — JSX を渡さない',
|
|
1051
|
+
'<Button prefixIcon={<Icon icon="bolt" size={4} />}>アクション</Button>',
|
|
1052
|
+
'```',
|
|
1053
|
+
]),
|
|
1054
|
+
jsdocTargets: [],
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
id: 'icon-children-text',
|
|
1058
|
+
check: {
|
|
1059
|
+
description: 'Icon の children にテキストを渡さない',
|
|
1060
|
+
recommendation:
|
|
1061
|
+
'Icon には icon prop でアイコン名を渡してください。children にテキストを渡すのは旧 Material Icons の書き方です。',
|
|
1062
|
+
pattern: /<Icon\b(?:[^>]|\/(?!>))*>[^<]+<\/Icon>/g,
|
|
1063
|
+
},
|
|
1064
|
+
featureSection: lines([
|
|
1065
|
+
'### Icon の children にテキストを渡さない',
|
|
1066
|
+
'',
|
|
1067
|
+
'```tsx',
|
|
1068
|
+
'// ✅ Correct — icon prop を使う',
|
|
1069
|
+
'<Icon icon="chevron_left" size={4} />',
|
|
1070
|
+
'',
|
|
1071
|
+
'// ❌ Wrong — children にテキストを渡さない',
|
|
1072
|
+
'<Icon size={4}>chevron_left</Icon>',
|
|
1073
|
+
'```',
|
|
1074
|
+
]),
|
|
1075
|
+
jsdocTargets: [],
|
|
1076
|
+
},
|
|
862
1077
|
];
|
|
863
1078
|
|
|
864
1079
|
function getCheckRules() {
|
|
865
|
-
const order = ['dialog-form', 'dialog-button-wrap', 'button-icon-only', 'material-symbols-direct', 'shadcn-token'];
|
|
1080
|
+
const order = ['dialog-form', 'dialog-button-wrap', 'button-icon-only', 'material-symbols-direct', 'shadcn-token', 'tailwind-typography', 'card-title-typography', 'card-control-non-button', 'card-padding-override', 'aschild-with-icon-props', 'disabled-vs-is-disabled', 'button-prefixicon-jsx', 'icon-children-text'];
|
|
866
1081
|
|
|
867
1082
|
return ANTI_PATTERN_GROUPS.filter((group) => group.check)
|
|
868
1083
|
.map((group) => ({
|
package/lib/generate-css.js
CHANGED
|
@@ -23,6 +23,41 @@ import {
|
|
|
23
23
|
updateGlobalsWithFonts,
|
|
24
24
|
} from './font-manager.js';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* config の extend セクションを解決する
|
|
28
|
+
* extend が文字列(ファイルパス)の場合は読み込んでマージ、オブジェクトならそのまま使用
|
|
29
|
+
* @param {Object} config 設定オブジェクト
|
|
30
|
+
* @param {string|null} configPath config ファイルのパス(extend ファイルの相対パス解決用)
|
|
31
|
+
* @returns {Object} extend が解決された config
|
|
32
|
+
*/
|
|
33
|
+
function resolveExtend(config, configPath = null) {
|
|
34
|
+
if (!config.extend) {
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let extend = config.extend;
|
|
39
|
+
|
|
40
|
+
// 文字列の場合はファイルパスとして読み込む
|
|
41
|
+
if (typeof extend === 'string') {
|
|
42
|
+
const baseDir = configPath ? path.dirname(path.resolve(configPath)) : process.cwd();
|
|
43
|
+
const extendPath = path.resolve(baseDir, extend);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(extendPath, 'utf8');
|
|
47
|
+
extend = JSON.parse(content);
|
|
48
|
+
console.log(`✅ extend ファイルを読み込みました: ${extendPath}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`❌ extend ファイルの読み込みに失敗しました: ${extendPath} (${error.message})`);
|
|
51
|
+
const { extend: _, ...rest } = config;
|
|
52
|
+
return rest;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// extend の値を config にマージ(extend 内のフィールドが優先)
|
|
57
|
+
const { extend: _, ...rest } = config;
|
|
58
|
+
return { ...rest, ...extend };
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
/**
|
|
27
62
|
* 同一のフォント @import を重複排除する
|
|
28
63
|
* @param {string} cssContent CSS内容
|
|
@@ -49,68 +84,101 @@ function dedupeFontImports(cssContent) {
|
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
/**
|
|
52
|
-
*
|
|
53
|
-
* @param {string|string[]}
|
|
54
|
-
* @returns {string[]}
|
|
87
|
+
* fonts セクションのエントリを正規化する
|
|
88
|
+
* @param {string|Array<string|{family:string, weights?:number[]}>} entry
|
|
89
|
+
* @returns {{family:string, weights:number[]}[]}
|
|
55
90
|
*/
|
|
56
|
-
function
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
91
|
+
function normalizeFontsEntry(entry) {
|
|
92
|
+
if (!entry) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (typeof entry === 'string') {
|
|
96
|
+
return [{ family: entry, weights: FONT_DEFAULTS.WEIGHTS }];
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(entry)) {
|
|
99
|
+
return entry.map(item => {
|
|
100
|
+
if (typeof item === 'string') {
|
|
101
|
+
return { family: item, weights: FONT_DEFAULTS.WEIGHTS };
|
|
102
|
+
}
|
|
103
|
+
return { family: item.family, weights: item.weights || FONT_DEFAULTS.WEIGHTS };
|
|
104
|
+
});
|
|
59
105
|
}
|
|
60
|
-
return [
|
|
106
|
+
return [{ family: entry.family, weights: entry.weights || FONT_DEFAULTS.WEIGHTS }];
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @param {
|
|
67
|
-
* @returns {string[]}
|
|
110
|
+
* config からフォント定義を解決する
|
|
111
|
+
* fonts セクションがあればそちらを優先、なければ font-pro / font-mono にフォールバック
|
|
112
|
+
* @param {Object} config 設定オブジェクト
|
|
113
|
+
* @returns {{ pro: {family:string, weights:number[]}[], mono: {family:string, weights:number[]}[] }}
|
|
68
114
|
*/
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
function resolveFontConfig(config) {
|
|
116
|
+
if (config.fonts) {
|
|
117
|
+
return {
|
|
118
|
+
pro: normalizeFontsEntry(config.fonts.pro || config['font-pro']),
|
|
119
|
+
mono: normalizeFontsEntry(config.fonts.mono || config['font-mono']),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
pro: normalizeFontsEntry(config['font-pro']),
|
|
124
|
+
mono: normalizeFontsEntry(config['font-mono']),
|
|
125
|
+
};
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
/**
|
|
78
129
|
* フォントファミリーの CSS 値を生成する
|
|
79
|
-
* @param {string[]} fonts
|
|
130
|
+
* @param {{family:string}[]} fonts フォント定義の配列
|
|
80
131
|
* @param {string} genericFamily ジェネリックファミリー(sans-serif / monospace)
|
|
81
132
|
* @returns {string} CSS font-family 値(例: 'Montserrat', 'Noto Sans JP', sans-serif)
|
|
82
133
|
*/
|
|
83
134
|
function generateFontFamilyValue(fonts, genericFamily) {
|
|
84
|
-
const quoted = fonts.map(f => `'${f}'`);
|
|
135
|
+
const quoted = fonts.map(f => `'${f.family}'`);
|
|
85
136
|
return [...quoted, genericFamily].join(', ');
|
|
86
137
|
}
|
|
87
138
|
|
|
139
|
+
/**
|
|
140
|
+
* フォント定義からウェイトをマージした import 行を生成する
|
|
141
|
+
* 同じフォントファミリーはウェイトを統合して1行にする
|
|
142
|
+
* @param {{family:string, weights:number[]}[]} allFonts 全フォント定義(pro + mono)
|
|
143
|
+
* @returns {string[]} @import 文の配列
|
|
144
|
+
*/
|
|
145
|
+
function generateMergedFontImports(allFonts) {
|
|
146
|
+
// 同じファミリーのウェイトをマージ
|
|
147
|
+
const familyWeightsMap = new Map();
|
|
148
|
+
for (const font of allFonts) {
|
|
149
|
+
const existing = familyWeightsMap.get(font.family);
|
|
150
|
+
if (existing) {
|
|
151
|
+
for (const w of font.weights) {
|
|
152
|
+
existing.add(w);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
familyWeightsMap.set(font.family, new Set(font.weights));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [...familyWeightsMap.entries()].map(([family, weightsSet]) => {
|
|
160
|
+
const weightsStr = [...weightsSet].sort((a, b) => a - b).join(';');
|
|
161
|
+
const urlName = family.replace(/\s+/g, '+');
|
|
162
|
+
return `@import 'https://fonts.googleapis.com/css2?family=${urlName}:wght@${weightsStr}&display=swap';`;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
88
166
|
/**
|
|
89
167
|
* フォント import ブロック全体を生成する(Material Symbols + PRO + MONO)
|
|
90
|
-
* @param {Object}
|
|
168
|
+
* @param {Object} configOrResolved 設定オブジェクトまたは resolveFontConfig の結果
|
|
91
169
|
* @returns {string} フォント import ブロック
|
|
92
170
|
*/
|
|
93
|
-
function generateFontImportsBlock(
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const monoWeights = config['font-mono-weights'] || FONT_DEFAULTS.WEIGHTS;
|
|
171
|
+
function generateFontImportsBlock(configOrResolved) {
|
|
172
|
+
const { pro, mono } = configOrResolved.pro && configOrResolved.mono
|
|
173
|
+
? configOrResolved
|
|
174
|
+
: resolveFontConfig(configOrResolved);
|
|
98
175
|
|
|
99
176
|
const imports = [
|
|
100
177
|
FONT_DEFAULTS.MATERIAL_SYMBOLS_IMPORT,
|
|
101
|
-
...
|
|
102
|
-
...generateGoogleFontImports(monoFonts, monoWeights),
|
|
178
|
+
...generateMergedFontImports([...pro, ...mono]),
|
|
103
179
|
];
|
|
104
180
|
|
|
105
|
-
|
|
106
|
-
const seen = new Set();
|
|
107
|
-
const deduped = imports.filter(line => {
|
|
108
|
-
if (seen.has(line)) return false;
|
|
109
|
-
seen.add(line);
|
|
110
|
-
return true;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return deduped.join('\n');
|
|
181
|
+
return imports.join('\n');
|
|
114
182
|
}
|
|
115
183
|
|
|
116
184
|
/**
|
|
@@ -130,22 +198,24 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
130
198
|
// プレースホルダーを色のトークンで置換(改行を含む)
|
|
131
199
|
processedCSS = processedCSS.replace(REGEX.COLOR_TOKENS_PLACEHOLDER, `${colorTokens}\n`);
|
|
132
200
|
|
|
133
|
-
// 2.
|
|
134
|
-
const
|
|
201
|
+
// 2. フォント設定を解決(一度だけ)
|
|
202
|
+
const resolvedFonts = resolveFontConfig(config);
|
|
203
|
+
|
|
204
|
+
// 3. フォント import ブロックを生成して置換
|
|
205
|
+
const fontImportsBlock = generateFontImportsBlock(resolvedFonts);
|
|
135
206
|
processedCSS = processedCSS.replace(REGEX.FONT_IMPORTS_PLACEHOLDER, `${fontImportsBlock}\n`);
|
|
136
207
|
|
|
137
|
-
//
|
|
138
|
-
const proFonts =
|
|
139
|
-
const monoFonts = normalizeFontConfig(config['font-mono']);
|
|
208
|
+
// 4. フォントファミリー CSS 値を生成して置換
|
|
209
|
+
const { pro: proFonts, mono: monoFonts } = resolvedFonts;
|
|
140
210
|
const proFamilyValue = generateFontFamilyValue(proFonts, FONT_DEFAULTS.GENERIC_FAMILY.pro);
|
|
141
211
|
const monoFamilyValue = generateFontFamilyValue(monoFonts, FONT_DEFAULTS.GENERIC_FAMILY.mono);
|
|
142
212
|
processedCSS = processedCSS.replace(/\{\{FONT_FAMILY_PRO\}\}/g, proFamilyValue);
|
|
143
213
|
processedCSS = processedCSS.replace(/\{\{FONT_FAMILY_MONO\}\}/g, monoFamilyValue);
|
|
144
214
|
|
|
145
|
-
//
|
|
215
|
+
// 5. 基本的な設定値による置換(オブジェクト・配列・拡張フィールドはスキップ)
|
|
146
216
|
Object.entries(config).forEach(([key, value]) => {
|
|
147
|
-
//
|
|
148
|
-
if (Array.isArray(value) ||
|
|
217
|
+
// 配列・オブジェクト・拡張フィールドはスキップ
|
|
218
|
+
if (Array.isArray(value) || (typeof value === 'object' && value !== null) || key === 'custom-css' || key === 'fonts' || key === 'extend' || key === 'source-packages') {
|
|
149
219
|
return;
|
|
150
220
|
}
|
|
151
221
|
// 通常のプレースホルダー(CSS用 - スペースはそのまま)
|
|
@@ -221,20 +291,23 @@ export function generateCSS(configPath = null, outputPath = null) {
|
|
|
221
291
|
console.log(MESSAGES.START);
|
|
222
292
|
|
|
223
293
|
// 1. 設定ファイルを読み込み
|
|
224
|
-
const
|
|
294
|
+
const rawConfig = loadConfig(configPath);
|
|
295
|
+
|
|
296
|
+
// 2. extend セクションを解決
|
|
297
|
+
const config = resolveExtend(rawConfig, configPath);
|
|
225
298
|
|
|
226
|
-
//
|
|
299
|
+
// 3. テンプレートを読み込み
|
|
227
300
|
const template = loadTemplate();
|
|
228
301
|
|
|
229
|
-
//
|
|
302
|
+
// 4. colors.json、gray.json、radius.csvを読み込み
|
|
230
303
|
const colors = loadColors();
|
|
231
304
|
const grayMapping = loadGrayMapping();
|
|
232
305
|
const radiusMapping = loadRadiusMapping();
|
|
233
306
|
|
|
234
|
-
//
|
|
307
|
+
// 5. テンプレートを設定値で処理
|
|
235
308
|
const processedCSS = processTemplate(template, config, grayMapping, radiusMapping, colors);
|
|
236
309
|
|
|
237
|
-
//
|
|
310
|
+
// 6. CSSファイルを書き出し
|
|
238
311
|
const defaultOutputPath = path.resolve(
|
|
239
312
|
process.cwd(),
|
|
240
313
|
...PATHS.DEFAULT_OUTPUT_DIR,
|
|
@@ -243,7 +316,7 @@ export function generateCSS(configPath = null, outputPath = null) {
|
|
|
243
316
|
const resolvedOutputPath = outputPath ? path.resolve(outputPath) : defaultOutputPath;
|
|
244
317
|
writeCSS(processedCSS, outputPath);
|
|
245
318
|
|
|
246
|
-
//
|
|
319
|
+
// 7. フォント管理の自動処理を実行
|
|
247
320
|
console.log(MESSAGES.FONT_MANAGEMENT_START);
|
|
248
321
|
const sourcePackages = 'source-packages' in config ? (config['source-packages'] || []) : null;
|
|
249
322
|
const customCssPath = config['custom-css'] || null;
|
|
@@ -273,10 +346,13 @@ export {
|
|
|
273
346
|
hexToOklch,
|
|
274
347
|
generateColorTokens,
|
|
275
348
|
dedupeFontImports,
|
|
349
|
+
// 設定解決
|
|
350
|
+
resolveExtend,
|
|
276
351
|
// フォント処理
|
|
277
|
-
|
|
278
|
-
|
|
352
|
+
normalizeFontsEntry,
|
|
353
|
+
resolveFontConfig,
|
|
279
354
|
generateFontFamilyValue,
|
|
355
|
+
generateMergedFontImports,
|
|
280
356
|
generateFontImportsBlock,
|
|
281
357
|
// フォント管理
|
|
282
358
|
extractFontImports,
|