sparkle-design-cli 1.4.0 → 1.5.0

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
@@ -185,82 +185,55 @@ npx --yes sparkle-design-cli setup --assistant generic
185
185
  - `font-mono`: モノスペースフォント(文字列または配列)
186
186
  - `radius`: 角丸設定(sm, md, lg など)
187
187
 
188
- #### Core Options
188
+ ### extend セクション(拡張設定)
189
189
 
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 が生成されます。
190
+ Figma プラグインが出力する基本4項目に加えて、プロジェクト固有の拡張を `extend` セクションにまとめます。`extend` はオブジェクト直書き(推奨)またはファイルパスを指定できます。
203
191
 
204
192
  ```json
205
193
  {
206
194
  "primary": "blue",
207
- "font-pro": ["Montserrat", "Noto Sans JP"],
208
- "font-pro-weights": [400, 500, 600, 700],
195
+ "font-pro": "Montserrat",
209
196
  "font-mono": "Roboto Mono",
210
- "font-mono-weights": [400, 700],
211
- "radius": "md"
197
+ "radius": "md",
198
+ "extend": {
199
+ "fonts": {
200
+ "pro": [
201
+ { "family": "Montserrat", "weights": [500, 600, 700] },
202
+ { "family": "Noto Sans JP", "weights": [400, 500, 600, 700] }
203
+ ],
204
+ "mono": [
205
+ { "family": "Roboto Mono", "weights": [400, 700] }
206
+ ]
207
+ },
208
+ "source-packages": ["@goodpatch/sparkle-design-internal"],
209
+ "custom-css": "./src/app/custom-tokens.css"
210
+ }
212
211
  }
213
212
  ```
214
213
 
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` で指定してください:
214
+ ファイル参照も可能です:
231
215
 
232
216
  ```json
233
217
  {
234
- "custom-css": "./src/app/custom-tokens.css"
218
+ "primary": "blue",
219
+ "font-pro": "Montserrat",
220
+ "font-mono": "Roboto Mono",
221
+ "radius": "md",
222
+ "extend": "./sparkle.extend.json"
235
223
  }
236
224
  ```
237
225
 
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
- ```
226
+ #### extend.fonts
250
227
 
251
- ### source-packages オプション
228
+ フォントごとにウェイトを個別指定。`extend.fonts` がない場合は `font-pro` / `font-mono` + デフォルトウェイト `[400, 700]` が使われます。`extend.fonts` が存在しても `fonts.pro` / `fonts.mono` のどちらかが未指定なら、そのスロットのみ `font-pro` / `font-mono` にフォールバックします。同じフォントファミリーが `pro` と `mono` で重複する場合、ウェイトはマージされ import は 1 行に統合されます。
252
229
 
253
- `@goodpatch/sparkle-design` を npm パッケージとして利用するプロジェクトでは、TailwindCSS v4 がパッケージ内のユーティリティクラスを検出できるように `@source` ディレクティブが必要です。
230
+ #### extend.source-packages
254
231
 
255
- `source-packages` を設定すると、CLI が `globals.css` に `@source` ディレクティブを自動挿入します。`@goodpatch/sparkle-design` は常にデフォルトで含まれます。
232
+ `@goodpatch/sparkle-design` を npm パッケージとして利用する場合に必須。`globals.css` に `@source` ディレクティブを自動挿入します。`@goodpatch/sparkle-design` は常にデフォルトで含まれます。
256
233
 
257
- ```json
258
- {
259
- "source-packages": ["@scope/my-extension-design"]
260
- }
261
- ```
234
+ #### extend.custom-css
262
235
 
263
- > **注意**: `source-packages` を指定しない場合、`@source` ディレクティブは生成されません。
236
+ プロジェクト固有のカスタムトークンの CSS ファイルパス。`globals.css` に `@import` が自動挿入されます。`sparkle-design.css` に直接追加すると `generate` 実行時に上書きされるため、必ず別ファイルに分離してください。
264
237
 
265
238
  ## 出力
266
239
 
@@ -144,6 +144,31 @@ Generate options:
144
144
  -c, --config <path> 設定ファイルのパス (default: ./sparkle.config.json)
145
145
  -o, --output <path> 出力ファイルのパス (default: ./src/app/sparkle-design.css)
146
146
 
147
+ sparkle.config.json の設定フィールド:
148
+
149
+ 基本設定(Figma プラグインで出力可能):
150
+ primary プライマリカラー (blue, red, orange, green, purple, pink, yellow など)
151
+ font-pro プロポーショナルフォント (Google Fonts の名前)
152
+ font-mono モノスペースフォント (Google Fonts の名前)
153
+ radius 角丸設定 (none, sm, md, lg, xl, full)
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 行になる。
168
+
169
+ 設定ファイルは Sparkle Design Theme Settings Figma プラグインから書き出せます:
170
+ https://www.figma.com/community/plugin/1443500367756891364
171
+
147
172
  Check options:
148
173
  -h, --help このヘルプメッセージを表示
149
174
  --strict 違反があれば exit code 1 で終了
@@ -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. 基本的な設定値による置換(font-pro / font-mono の配列以外)
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.0",
3
+ "version": "1.5.0",
4
4
  "description": "Sparkle Design CSS Generator - デザインシステムCSSを設定ファイルから生成するツール",
5
5
  "main": "lib/generate-css.js",
6
6
  "type": "module",