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 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
- #### Core Options
197
+ ### extend セクション(拡張設定)
189
198
 
190
- - `font-pro-weights`: (オプション)プロポーショナルフォントのウェイト配列(デフォルト: `[400, 700]`)
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": ["Montserrat", "Noto Sans JP"],
208
- "font-pro-weights": [400, 500, 600, 700],
204
+ "font-pro": "Montserrat",
209
205
  "font-mono": "Roboto Mono",
210
- "font-mono-weights": [400, 700],
211
- "radius": "md"
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
- 生成される CSS:
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
- "custom-css": "./src/app/custom-tokens.css"
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
- CLI が `globals.css` に `@import` を自動挿入します。CLI はファイルの中身を一切触りません。
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
- ### source-packages オプション
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
- `@goodpatch/sparkle-design` を npm パッケージとして利用するプロジェクトでは、TailwindCSS v4 がパッケージ内のユーティリティクラスを検出できるように `@source` ディレクティブが必要です。
239
+ #### extend.source-packages
254
240
 
255
- `source-packages` を設定すると、CLI が `globals.css` に `@source` ディレクティブを自動挿入します。`@goodpatch/sparkle-design` は常にデフォルトで含まれます。
241
+ `@goodpatch/sparkle-design` を npm パッケージとして利用する場合に必須。`globals.css` に `@source` ディレクティブを自動挿入します。`@goodpatch/sparkle-design` は常にデフォルトで含まれます。
256
242
 
257
- ```json
258
- {
259
- "source-packages": ["@scope/my-extension-design"]
260
- }
261
- ```
243
+ #### extend.custom-css
262
244
 
263
- > **注意**: `source-packages` を指定しない場合、`@source` ディレクティブは生成されません。
245
+ プロジェクト固有のカスタムトークンの CSS ファイルパス。`globals.css` に `@import` が自動挿入されます。`sparkle-design.css` に直接追加すると `generate` 実行時に上書きされるため、必ず別ファイルに分離してください。
264
246
 
265
247
  ## 出力
266
248
 
@@ -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
- font-pro-weights (任意) フォントウェイト配列 (default: [400, 700])
155
- 例: [400, 500, 600, 700]
156
- font-mono-weights (任意) モノスペースフォントのウェイト配列 (default: [400, 700])
157
- source-packages (任意) @source で追加スキャンする npm パッケージ名の配列
158
- custom-css (任意) プロジェクト固有のカスタムトークン CSS ファイルパス
159
- globals.css に @import が自動挿入される
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 / bg-background / border-border を持ち込んでいる
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) => ({
@@ -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[]} fontValue フォント名(文字列または配列)
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 normalizeFontConfig(fontValue) {
57
- if (Array.isArray(fontValue)) {
58
- return fontValue;
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 [fontValue];
106
+ return [{ family: entry.family, weights: entry.weights || FONT_DEFAULTS.WEIGHTS }];
61
107
  }
62
108
 
63
109
  /**
64
- * Google Fonts の @import 行を生成する
65
- * @param {string[]} fonts フォント名の配列
66
- * @param {number[]} weights ウェイトの配列
67
- * @returns {string[]} @import 文の配列
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 generateGoogleFontImports(fonts, weights) {
70
- const weightsStr = weights.sort((a, b) => a - b).join(';');
71
- return fonts.map(font => {
72
- const urlName = font.replace(/\s+/g, '+');
73
- return `@import 'https://fonts.googleapis.com/css2?family=${urlName}:wght@${weightsStr}&display=swap';`;
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} config 設定オブジェクト
168
+ * @param {Object} configOrResolved 設定オブジェクトまたは resolveFontConfig の結果
91
169
  * @returns {string} フォント import ブロック
92
170
  */
93
- function generateFontImportsBlock(config) {
94
- const proFonts = normalizeFontConfig(config['font-pro']);
95
- const monoFonts = normalizeFontConfig(config['font-mono']);
96
- const proWeights = config['font-pro-weights'] || FONT_DEFAULTS.WEIGHTS;
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
- ...generateGoogleFontImports(proFonts, proWeights),
102
- ...generateGoogleFontImports(monoFonts, monoWeights),
178
+ ...generateMergedFontImports([...pro, ...mono]),
103
179
  ];
104
180
 
105
- // 重複排除(font-pro と font-mono が同じフォントでウェイトも同じ場合)
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. フォント import ブロックを生成して置換
134
- const fontImportsBlock = generateFontImportsBlock(config);
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
- // 3. フォントファミリー CSS 値を生成して置換
138
- const proFonts = normalizeFontConfig(config['font-pro']);
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
- // 4. 基本的な設定値による置換(font-pro / font-mono の配列以外)
215
+ // 5. 基本的な設定値による置換(オブジェクト・配列・拡張フィールドはスキップ)
146
216
  Object.entries(config).forEach(([key, value]) => {
147
- // 配列や font-*-weights はスキップ
148
- if (Array.isArray(value) || key.endsWith('-weights') || key === 'custom-css') {
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 config = loadConfig(configPath);
294
+ const rawConfig = loadConfig(configPath);
295
+
296
+ // 2. extend セクションを解決
297
+ const config = resolveExtend(rawConfig, configPath);
225
298
 
226
- // 2. テンプレートを読み込み
299
+ // 3. テンプレートを読み込み
227
300
  const template = loadTemplate();
228
301
 
229
- // 3. colors.json、gray.json、radius.csvを読み込み
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
- // 4. テンプレートを設定値で処理
307
+ // 5. テンプレートを設定値で処理
235
308
  const processedCSS = processTemplate(template, config, grayMapping, radiusMapping, colors);
236
309
 
237
- // 5. CSSファイルを書き出し
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
- // 6. フォント管理の自動処理を実行
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
- normalizeFontConfig,
278
- generateGoogleFontImports,
352
+ normalizeFontsEntry,
353
+ resolveFontConfig,
279
354
  generateFontFamilyValue,
355
+ generateMergedFontImports,
280
356
  generateFontImportsBlock,
281
357
  // フォント管理
282
358
  extractFontImports,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparkle-design-cli",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
5
5
  "main": "lib/generate-css.js",
6
6
  "type": "module",