sparkle-design-cli 1.1.9 → 1.2.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 +34 -0
- package/lib/color-utils.js +110 -0
- package/lib/constants.js +124 -0
- package/lib/file-loader.js +168 -0
- package/lib/font-manager.js +201 -0
- package/lib/generate-css.js +63 -248
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,40 @@ sparkle-design-cli -c ./config/custom.json -o ./dist/styles.css
|
|
|
83
83
|
- カスタム出力先: `-o` オプションで指定可能
|
|
84
84
|
- 実行場所を基準として相対パスで処理されます
|
|
85
85
|
|
|
86
|
+
## 自動フォント管理
|
|
87
|
+
|
|
88
|
+
**v1.2.0 以降の新機能**
|
|
89
|
+
|
|
90
|
+
CLI は自動的にフォント管理を行います:
|
|
91
|
+
|
|
92
|
+
1. **フォント検出**: `sparkle-design.css` から Google Fonts の `@import` 文を検出
|
|
93
|
+
2. **globals.css への移動**: フォントの `@import` を `globals.css` の先頭に移動
|
|
94
|
+
3. **sparkle-design.css から削除**: 元のファイルからフォント `@import` を削除
|
|
95
|
+
|
|
96
|
+
### なぜこの処理が必要なのか?
|
|
97
|
+
|
|
98
|
+
CSS の仕様では、`@import` 文は `@charset` と `@layer` 以外のすべてのルールより前に記述する必要があります。Tailwind CSS v4 と組み合わせた場合、この順序が正しくないとビルド時に警告が発生します。
|
|
99
|
+
|
|
100
|
+
この機能により、以下の警告が自動的に解決されます:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
⚠️ @import rules must precede all rules aside from @charset and @layer statements
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 動作条件
|
|
107
|
+
|
|
108
|
+
- `globals.css` が `sparkle-design.css` と同じディレクトリに存在する場合のみ実行されます
|
|
109
|
+
- `globals.css` が存在しない場合は、フォント管理処理はスキップされます
|
|
110
|
+
|
|
111
|
+
### 出力例
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
📦 フォント管理処理を開始します...
|
|
115
|
+
📝 3個のフォントimportを検出しました
|
|
116
|
+
✅ globals.css にフォントimportを追加しました
|
|
117
|
+
✅ sparkle-design.css からフォントimportを削除しました
|
|
118
|
+
```
|
|
119
|
+
|
|
86
120
|
## 開発
|
|
87
121
|
|
|
88
122
|
### セットアップ
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* カラー変換ユーティリティ
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { oklch } from 'culori';
|
|
6
|
+
import { COLOR_PRECISION, GRAY_LEVEL_MAP, MESSAGES } from './constants.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 16進数カラーをrgba形式に変換
|
|
10
|
+
* @param {string} hex 16進数のカラーコード(#付きまたは無し)
|
|
11
|
+
* @returns {string} rgba形式の文字列
|
|
12
|
+
*/
|
|
13
|
+
export function hexToRgba(hex) {
|
|
14
|
+
// #を削除
|
|
15
|
+
let cleanHex = hex.replace('#', '');
|
|
16
|
+
|
|
17
|
+
// 3桁の場合は6桁に拡張
|
|
18
|
+
if (cleanHex.length === 3) {
|
|
19
|
+
cleanHex = cleanHex
|
|
20
|
+
.split('')
|
|
21
|
+
.map((char) => char + char)
|
|
22
|
+
.join('');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const r = parseInt(cleanHex.substring(0, 2), 16);
|
|
26
|
+
const g = parseInt(cleanHex.substring(2, 4), 16);
|
|
27
|
+
const b = parseInt(cleanHex.substring(4, 6), 16);
|
|
28
|
+
|
|
29
|
+
return `rgba(${r}, ${g}, ${b}, 1)`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 16進数カラーをOKLCH形式に変換
|
|
34
|
+
* @param {string} hex 16進数のカラーコード(#付きまたは無し)
|
|
35
|
+
* @returns {string} OKLCH形式の文字列
|
|
36
|
+
*/
|
|
37
|
+
export function hexToOklch(hex) {
|
|
38
|
+
// #を削除
|
|
39
|
+
const cleanHex = hex.replace('#', '');
|
|
40
|
+
|
|
41
|
+
// 3桁の場合は6桁に、4桁の場合は8桁に拡張
|
|
42
|
+
let fullHex = cleanHex;
|
|
43
|
+
if (cleanHex.length === 3 || cleanHex.length === 4) {
|
|
44
|
+
fullHex = cleanHex
|
|
45
|
+
.split('')
|
|
46
|
+
.map((char) => char + char)
|
|
47
|
+
.join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// culoriを使用してHEXからOKLCHに変換
|
|
51
|
+
const oklchColor = oklch(`#${fullHex}`);
|
|
52
|
+
|
|
53
|
+
if (!oklchColor) {
|
|
54
|
+
console.warn(MESSAGES.COLOR_CONVERSION_FAILED(hex));
|
|
55
|
+
return `oklch(0 0 0)`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 精度を調整して出力
|
|
59
|
+
const l = Math.round((oklchColor.l || 0) * COLOR_PRECISION.OKLCH_L) / COLOR_PRECISION.OKLCH_L;
|
|
60
|
+
const c = Math.round((oklchColor.c || 0) * COLOR_PRECISION.OKLCH_C) / COLOR_PRECISION.OKLCH_C;
|
|
61
|
+
const h = oklchColor.h
|
|
62
|
+
? Math.round(oklchColor.h * COLOR_PRECISION.OKLCH_H) / COLOR_PRECISION.OKLCH_H
|
|
63
|
+
: 0;
|
|
64
|
+
|
|
65
|
+
// アルファ値がある場合(1未満の場合のみ出力に追加)
|
|
66
|
+
if (oklchColor.alpha !== undefined && oklchColor.alpha < 1) {
|
|
67
|
+
const a =
|
|
68
|
+
Math.round(oklchColor.alpha * COLOR_PRECISION.OKLCH_ALPHA) / COLOR_PRECISION.OKLCH_ALPHA;
|
|
69
|
+
return `oklch(${l} ${c} ${h} / ${a})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `oklch(${l} ${c} ${h})`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 色のトークンを生成する
|
|
77
|
+
* @param {Object} colors colors.jsonから読み込んだ色の定義
|
|
78
|
+
* @param {Object} grayMapping gray.jsonから読み込んだグレーの定義
|
|
79
|
+
* @param {string} primaryColor プライマリカラーの名前
|
|
80
|
+
* @returns {string} 色のトークンのCSS文字列
|
|
81
|
+
*/
|
|
82
|
+
export function generateColorTokens(colors, grayMapping, primaryColor) {
|
|
83
|
+
const colorTokens = [];
|
|
84
|
+
|
|
85
|
+
// 通常の色トークンを生成
|
|
86
|
+
Object.entries(colors).forEach(([colorName, colorValues]) => {
|
|
87
|
+
if (typeof colorValues === 'object' && colorValues !== null) {
|
|
88
|
+
Object.entries(colorValues).forEach(([level, hexValue]) => {
|
|
89
|
+
const oklchValue = hexToOklch(hexValue);
|
|
90
|
+
colorTokens.push(` --color-${colorName}-${level}: ${oklchValue};`);
|
|
91
|
+
});
|
|
92
|
+
} else if (typeof colorValues === 'string') {
|
|
93
|
+
// black, white, web-focusなどの単色
|
|
94
|
+
const oklchValue = hexToOklch(colorValues);
|
|
95
|
+
colorTokens.push(` --color-${colorName}: ${oklchValue};`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// グレーの色トークンを生成(プライマリカラーに基づく)
|
|
100
|
+
if (primaryColor && grayMapping[primaryColor]) {
|
|
101
|
+
const grayColors = grayMapping[primaryColor];
|
|
102
|
+
Object.entries(grayColors).forEach(([key, hexValue]) => {
|
|
103
|
+
const grayLevel = GRAY_LEVEL_MAP[key] || key;
|
|
104
|
+
const oklchValue = hexToOklch(hexValue);
|
|
105
|
+
colorTokens.push(` --color-gray-${grayLevel}: ${oklchValue};`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return colorTokens.join('\n');
|
|
110
|
+
}
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sparkle-design-cli で使用する定数定義
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ファイルパス関連
|
|
6
|
+
export const FILE_NAMES = {
|
|
7
|
+
CONFIG: 'sparkle.config.json',
|
|
8
|
+
OUTPUT: 'sparkle-design.css',
|
|
9
|
+
GLOBALS: 'globals.css',
|
|
10
|
+
TEMPLATE: 'sparkle-design.template.css',
|
|
11
|
+
COLORS: 'colors.json',
|
|
12
|
+
GRAY: 'gray.json',
|
|
13
|
+
RADIUS: 'radius.csv',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const PATHS = {
|
|
17
|
+
DEFAULT_OUTPUT_DIR: ['src', 'app'],
|
|
18
|
+
TEMPLATE_DIR: ['templates', 'sparkle-variables'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 正規表現パターン
|
|
22
|
+
export const REGEX = {
|
|
23
|
+
// フォント関連
|
|
24
|
+
FONT_IMPORT: /@import\s+url\([^)]+fonts\.googleapis\.com[^)]+\);?/g,
|
|
25
|
+
FONT_IMPORT_WITH_COMMENT:
|
|
26
|
+
/\/\*\s*フォントのインポート\s*\*\/\s*\n?|@import\s+url\([^)]+fonts\.googleapis\.com[^)]+\);?\s*\n?/g,
|
|
27
|
+
EXISTING_FONT_IMPORT_BLOCK:
|
|
28
|
+
/\/\*\s*フォントのインポート[^*]*\*\/\s*\n?(?:@import\s+url\([^)]+fonts\.googleapis\.com[^)]+\);?\s*\n?)*\n?/g,
|
|
29
|
+
|
|
30
|
+
// Sparkle Design関連
|
|
31
|
+
SPARKLE_IMPORT:
|
|
32
|
+
/\/\*\s*Sparkle Design[^*]*\*\/\s*\n?|@import\s+['"](\.\/)?sparkle-design\.css['"];?\s*\n?/g,
|
|
33
|
+
TAILWIND_IMPORT: /@import\s+['"]tailwindcss['"];?/,
|
|
34
|
+
|
|
35
|
+
// テンプレート関連
|
|
36
|
+
COLOR_TOKENS_PLACEHOLDER: /\s*\/\* \{\{COLOR_TOKENS\}\} \*\/\s*/,
|
|
37
|
+
RADIUS_SEMANTIC_PATTERN: (semanticName) =>
|
|
38
|
+
new RegExp(`--radius-${semanticName}:\\s*var\\(--radius-[^)]+\\)`, 'g'),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// テンプレートプレースホルダー
|
|
42
|
+
export const PLACEHOLDERS = {
|
|
43
|
+
COLOR_TOKENS: '{{COLOR_TOKENS}}',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// カラー変換精度
|
|
47
|
+
export const COLOR_PRECISION = {
|
|
48
|
+
OKLCH_L: 10000,
|
|
49
|
+
OKLCH_C: 10000,
|
|
50
|
+
OKLCH_H: 10,
|
|
51
|
+
OKLCH_ALPHA: 10000,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// グレーレベルのマッピング
|
|
55
|
+
export const GRAY_LEVEL_MAP = {
|
|
56
|
+
0: '50',
|
|
57
|
+
1: '100',
|
|
58
|
+
2: '200',
|
|
59
|
+
3: '300',
|
|
60
|
+
4: '400',
|
|
61
|
+
5: '500',
|
|
62
|
+
6: '600',
|
|
63
|
+
7: '700',
|
|
64
|
+
8: '800',
|
|
65
|
+
9: '900',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// コメントテンプレート
|
|
69
|
+
export const COMMENTS = {
|
|
70
|
+
FONT_IMPORT: '/* フォントのインポート(CSSの仕様上、@importは最初に記述する必要がある) */',
|
|
71
|
+
SPARKLE_IMPORT: '/* Sparkle Design のカスタム定義(Tailwindの後にインポート) */',
|
|
72
|
+
TAILWIND_IMPORT: '/* Tailwindのインポート */',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// インポート文のテンプレート
|
|
76
|
+
export const IMPORTS = {
|
|
77
|
+
SPARKLE_DESIGN: '@import "./sparkle-design.css";',
|
|
78
|
+
TAILWIND: '@import "tailwindcss";',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ログメッセージ
|
|
82
|
+
export const MESSAGES = {
|
|
83
|
+
START: '🚀 Sparkle Design CSS Generator を開始します...\n',
|
|
84
|
+
SUCCESS: '\n🎉 CSS生成が完了しました!',
|
|
85
|
+
FONT_MANAGEMENT_START: '\n📦 フォント管理処理を開始します...',
|
|
86
|
+
|
|
87
|
+
// 成功メッセージ
|
|
88
|
+
CONFIG_LOADED: (path) => `✅ sparkle.config.json を読み込みました: ${path}`,
|
|
89
|
+
TEMPLATE_LOADED: '✅ CSSテンプレートを読み込みました',
|
|
90
|
+
COLORS_LOADED: '✅ colors.json を読み込みました',
|
|
91
|
+
GRAY_LOADED: '✅ gray.json を読み込みました',
|
|
92
|
+
RADIUS_LOADED: '✅ radius.csv を読み込みました',
|
|
93
|
+
TEMPLATE_PROCESSED: '✅ テンプレートを設定値で処理しました',
|
|
94
|
+
CSS_GENERATED: (path) => `✅ sparkle-design.css を生成しました: ${path}`,
|
|
95
|
+
RADIUS_APPLIED: (radius) => `✅ ${radius}に対応するradius設定を適用しました`,
|
|
96
|
+
GLOBALS_UPDATED: (path) =>
|
|
97
|
+
`✅ globals.css にフォントimportとsparkle-design.css importを追加しました: ${path}`,
|
|
98
|
+
FONT_REMOVED: '✅ sparkle-design.css からフォントimportを削除しました',
|
|
99
|
+
|
|
100
|
+
// 情報メッセージ
|
|
101
|
+
GLOBALS_NOT_FOUND: 'ℹ️ globals.css が見つかりません。フォント処理をスキップします。',
|
|
102
|
+
NO_FONT_IMPORTS: 'ℹ️ sparkle-design.css にフォントimportが見つかりません。',
|
|
103
|
+
FONT_DETECTED: (count) => `📝 ${count}個のフォントimportを検出しました`,
|
|
104
|
+
|
|
105
|
+
// スキップメッセージ
|
|
106
|
+
FONT_REMOVE_SKIPPED: 'ℹ️ globals.css の更新に失敗したため、フォントimport削除をスキップします。',
|
|
107
|
+
|
|
108
|
+
// 警告メッセージ
|
|
109
|
+
TAILWIND_NOT_FOUND: '⚠️ globals.css に Tailwind import が見つかりません。',
|
|
110
|
+
GLOBALS_UPDATE_FAILED: (error) => `⚠️ globals.css の更新に失敗しました: ${error}`,
|
|
111
|
+
FONT_MANAGEMENT_ERROR: (error) => `⚠️ フォント管理処理でエラーが発生しました: ${error}`,
|
|
112
|
+
COLOR_CONVERSION_FAILED: (hex) => `⚠️ 色の変換に失敗しました: ${hex}`,
|
|
113
|
+
|
|
114
|
+
// エラーメッセージ
|
|
115
|
+
CONFIG_LOAD_FAILED: (error) => `❌ sparkle.config.json の読み込みに失敗しました: ${error}`,
|
|
116
|
+
CONFIG_PATH_INFO: (path) => `設定ファイルパス: ${path}`,
|
|
117
|
+
CONFIG_HELP: '設定ファイルは専用プラグインから生成するか、手動で作成してください',
|
|
118
|
+
TEMPLATE_LOAD_FAILED: (error) => `❌ CSSテンプレートの読み込みに失敗しました: ${error}`,
|
|
119
|
+
TEMPLATE_PATH_INFO: (path) => `テンプレートファイルが存在することを確認してください: ${path}`,
|
|
120
|
+
COLORS_LOAD_FAILED: (error) => `❌ colors.json の読み込みに失敗しました: ${error}`,
|
|
121
|
+
GRAY_LOAD_FAILED: (error) => `❌ gray.json の読み込みに失敗しました: ${error}`,
|
|
122
|
+
RADIUS_LOAD_FAILED: (error) => `❌ radius.csv の読み込みに失敗しました: ${error}`,
|
|
123
|
+
CSS_WRITE_FAILED: (error) => `❌ CSSファイルの書き出しに失敗しました: ${error}`,
|
|
124
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ファイル読み込み機能
|
|
3
|
+
* 設定ファイル、テンプレート、カラー定義などの読み込み処理を統一
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { FILE_NAMES, PATHS, MESSAGES } from './constants.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ファイルを読み込む共通関数
|
|
16
|
+
* @param {string} filePath ファイルパス
|
|
17
|
+
* @param {string} successMessage 成功時のメッセージ
|
|
18
|
+
* @param {string|Function} errorMessage エラー時のメッセージ(文字列 or (error) => string)
|
|
19
|
+
* @param {Function} additionalErrorHandler 追加のエラーハンドラ(オプション)
|
|
20
|
+
* @returns {string} ファイル内容
|
|
21
|
+
*/
|
|
22
|
+
function readFile(filePath, successMessage, errorMessage, additionalErrorHandler = null) {
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
25
|
+
console.log(successMessage);
|
|
26
|
+
return content;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message =
|
|
29
|
+
typeof errorMessage === 'function'
|
|
30
|
+
? errorMessage(error)
|
|
31
|
+
: errorMessage || `❌ ファイルの読み込みに失敗しました: ${error.message}`;
|
|
32
|
+
|
|
33
|
+
console.error(message);
|
|
34
|
+
if (additionalErrorHandler) {
|
|
35
|
+
additionalErrorHandler(filePath, error);
|
|
36
|
+
}
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* JSONファイルを読み込む共通関数
|
|
43
|
+
* @param {string} filePath ファイルパス
|
|
44
|
+
* @param {string} successMessage 成功時のメッセージ
|
|
45
|
+
* @param {string|Function} errorMessage エラー時のメッセージ(文字列 or (error) => string)
|
|
46
|
+
* @param {Function} additionalErrorHandler 追加のエラーハンドラ(オプション)
|
|
47
|
+
* @returns {Object} パース済みのJSONオブジェクト
|
|
48
|
+
*/
|
|
49
|
+
function readJsonFile(filePath, successMessage, errorMessage, additionalErrorHandler = null) {
|
|
50
|
+
const content = readFile(filePath, successMessage, errorMessage, additionalErrorHandler);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message =
|
|
56
|
+
typeof errorMessage === 'function'
|
|
57
|
+
? errorMessage(error)
|
|
58
|
+
: errorMessage || `❌ JSONのパースに失敗しました: ${error.message}`;
|
|
59
|
+
|
|
60
|
+
console.error(message);
|
|
61
|
+
if (additionalErrorHandler) {
|
|
62
|
+
additionalErrorHandler(filePath, error);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* テンプレートディレクトリのパスを取得する
|
|
70
|
+
* @returns {string} テンプレートディレクトリのパス
|
|
71
|
+
*/
|
|
72
|
+
function getTemplateDir() {
|
|
73
|
+
return path.join(__dirname, '..', ...PATHS.TEMPLATE_DIR);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* sparkle.config.jsonを読み込む
|
|
78
|
+
* @param {string|null} configPath カスタム設定ファイルのパス(オプション)
|
|
79
|
+
* @returns {Object} 設定オブジェクト
|
|
80
|
+
*/
|
|
81
|
+
export function loadConfig(configPath = null) {
|
|
82
|
+
// カスタムパスが指定されていない場合は実行場所のsparkle.config.jsonを探す
|
|
83
|
+
const defaultConfigPath = path.resolve(process.cwd(), FILE_NAMES.CONFIG);
|
|
84
|
+
const resolvedConfigPath = configPath ? path.resolve(configPath) : defaultConfigPath;
|
|
85
|
+
|
|
86
|
+
const config = readJsonFile(
|
|
87
|
+
resolvedConfigPath,
|
|
88
|
+
MESSAGES.CONFIG_LOADED(resolvedConfigPath),
|
|
89
|
+
(error) => MESSAGES.CONFIG_LOAD_FAILED(error.message),
|
|
90
|
+
(filePath, _error) => {
|
|
91
|
+
console.error(MESSAGES.CONFIG_PATH_INFO(filePath));
|
|
92
|
+
console.error(MESSAGES.CONFIG_HELP);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
console.log(' 設定内容:', config);
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* CSSテンプレートを読み込む
|
|
102
|
+
* @returns {string} テンプレート内容
|
|
103
|
+
*/
|
|
104
|
+
export function loadTemplate() {
|
|
105
|
+
const templatePath = path.join(getTemplateDir(), FILE_NAMES.TEMPLATE);
|
|
106
|
+
|
|
107
|
+
return readFile(
|
|
108
|
+
templatePath,
|
|
109
|
+
MESSAGES.TEMPLATE_LOADED,
|
|
110
|
+
(error) => MESSAGES.TEMPLATE_LOAD_FAILED(error.message),
|
|
111
|
+
(filePath) => {
|
|
112
|
+
console.error(MESSAGES.TEMPLATE_PATH_INFO(filePath));
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 色定義(colors.json)を読み込む
|
|
119
|
+
* @returns {Object} 色定義オブジェクト
|
|
120
|
+
*/
|
|
121
|
+
export function loadColors() {
|
|
122
|
+
const colorsPath = path.join(getTemplateDir(), FILE_NAMES.COLORS);
|
|
123
|
+
|
|
124
|
+
return readJsonFile(colorsPath, MESSAGES.COLORS_LOADED, (error) =>
|
|
125
|
+
MESSAGES.COLORS_LOAD_FAILED(error.message)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* gray.jsonを読み込む
|
|
131
|
+
* @returns {Object} グレーマッピングオブジェクト
|
|
132
|
+
*/
|
|
133
|
+
export function loadGrayMapping() {
|
|
134
|
+
const grayJsonPath = path.join(getTemplateDir(), FILE_NAMES.GRAY);
|
|
135
|
+
|
|
136
|
+
return readJsonFile(grayJsonPath, MESSAGES.GRAY_LOADED, (error) =>
|
|
137
|
+
MESSAGES.GRAY_LOAD_FAILED(error.message)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* radius.csvを読み込む
|
|
143
|
+
* @returns {Object} Radiusマッピングオブジェクト
|
|
144
|
+
*/
|
|
145
|
+
export function loadRadiusMapping() {
|
|
146
|
+
const radiusCsvPath = path.join(getTemplateDir(), FILE_NAMES.RADIUS);
|
|
147
|
+
|
|
148
|
+
const csvContent = readFile(radiusCsvPath, MESSAGES.RADIUS_LOADED, (error) =>
|
|
149
|
+
MESSAGES.RADIUS_LOAD_FAILED(error.message)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// CSVをパース
|
|
153
|
+
const lines = csvContent.trim().split('\n');
|
|
154
|
+
const headers = lines[0].split(',');
|
|
155
|
+
const radiusMapping = {};
|
|
156
|
+
|
|
157
|
+
for (let i = 1; i < lines.length; i++) {
|
|
158
|
+
const values = lines[i].split(',');
|
|
159
|
+
const name = values[0];
|
|
160
|
+
radiusMapping[name] = {};
|
|
161
|
+
|
|
162
|
+
for (let j = 1; j < headers.length; j++) {
|
|
163
|
+
radiusMapping[name][headers[j]] = values[j];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return radiusMapping;
|
|
168
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* フォント管理機能
|
|
3
|
+
* sparkle-design.css からフォントimportを抽出し、globals.css に移動する
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { REGEX, COMMENTS, IMPORTS, MESSAGES } from './constants.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* sparkle-design.css からフォントの@import文を抽出する
|
|
12
|
+
* @param {string} cssContent CSS内容
|
|
13
|
+
* @returns {Array<string>} フォントimport文の配列
|
|
14
|
+
*/
|
|
15
|
+
export function extractFontImports(cssContent) {
|
|
16
|
+
const matches = cssContent.match(REGEX.FONT_IMPORT);
|
|
17
|
+
return matches || [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* sparkle-design.css からフォントの@import文を削除する
|
|
22
|
+
* @param {string} cssContent CSS内容
|
|
23
|
+
* @returns {string} フォントimportを削除したCSS
|
|
24
|
+
*/
|
|
25
|
+
export function removeFontImportsFromCSS(cssContent) {
|
|
26
|
+
return cssContent.replace(REGEX.FONT_IMPORT_WITH_COMMENT, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* フォントimportブロックを生成する
|
|
31
|
+
* @param {Array<string>} fontImports フォントimport文の配列
|
|
32
|
+
* @returns {string} フォントimportブロック
|
|
33
|
+
*/
|
|
34
|
+
function createFontImportBlock(fontImports) {
|
|
35
|
+
return [COMMENTS.FONT_IMPORT, ...fontImports, ''].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* sparkle-design.css importブロックを生成する
|
|
40
|
+
* @returns {string} sparkle-design.css importブロック
|
|
41
|
+
*/
|
|
42
|
+
function createSparkleImportBlock() {
|
|
43
|
+
return ['', COMMENTS.SPARKLE_IMPORT, IMPORTS.SPARKLE_DESIGN, ''].join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* globals.css の既存のimportを削除する
|
|
48
|
+
* @param {string} globalsContent globals.cssの内容
|
|
49
|
+
* @returns {string} 既存importを削除したglobals.css
|
|
50
|
+
*/
|
|
51
|
+
function removeExistingImports(globalsContent) {
|
|
52
|
+
let cleaned = globalsContent;
|
|
53
|
+
|
|
54
|
+
// 既存のフォントimportを削除
|
|
55
|
+
cleaned = cleaned.replace(REGEX.EXISTING_FONT_IMPORT_BLOCK, '');
|
|
56
|
+
|
|
57
|
+
// 既存のsparkle-design.css importを削除
|
|
58
|
+
cleaned = cleaned.replace(REGEX.SPARKLE_IMPORT, '');
|
|
59
|
+
|
|
60
|
+
return cleaned;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tailwind import の位置を見つける
|
|
65
|
+
* @param {string} globalsContent globals.cssの内容
|
|
66
|
+
* @returns {{match: string, index: number, afterIndex: number} | null} Tailwind importの情報
|
|
67
|
+
*/
|
|
68
|
+
function findTailwindImport(globalsContent) {
|
|
69
|
+
const tailwindMatch = globalsContent.match(REGEX.TAILWIND_IMPORT);
|
|
70
|
+
|
|
71
|
+
if (!tailwindMatch) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tailwindIndex = globalsContent.indexOf(tailwindMatch[0]);
|
|
76
|
+
const afterTailwindIndex = tailwindIndex + tailwindMatch[0].length;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
match: tailwindMatch[0],
|
|
80
|
+
index: tailwindIndex,
|
|
81
|
+
afterIndex: afterTailwindIndex,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* globals.css を再構築する
|
|
87
|
+
* @param {string} globalsContent 元のglobals.cssの内容
|
|
88
|
+
* @param {string} fontImportBlock フォントimportブロック
|
|
89
|
+
* @param {string} sparkleImportBlock sparkle-design.css importブロック
|
|
90
|
+
* @param {Object} tailwindInfo Tailwind importの情報
|
|
91
|
+
* @returns {string} 再構築されたglobals.css
|
|
92
|
+
*/
|
|
93
|
+
function reconstructGlobalsCss(globalsContent, fontImportBlock, sparkleImportBlock, tailwindInfo) {
|
|
94
|
+
const beforeTailwind = globalsContent.substring(0, tailwindInfo.index);
|
|
95
|
+
const afterTailwind = globalsContent.substring(tailwindInfo.afterIndex);
|
|
96
|
+
|
|
97
|
+
// フォントimport + Tailwind + sparkle-design.css + 残りのコンテンツ
|
|
98
|
+
return (
|
|
99
|
+
fontImportBlock +
|
|
100
|
+
beforeTailwind +
|
|
101
|
+
tailwindInfo.match +
|
|
102
|
+
sparkleImportBlock +
|
|
103
|
+
afterTailwind.trimStart()
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* globals.css を構造化して管理する
|
|
109
|
+
* - フォントimportを先頭に配置
|
|
110
|
+
* - Tailwind importを確保
|
|
111
|
+
* - sparkle-design.css importをTailwindの後に配置
|
|
112
|
+
* @param {Array<string>} fontImports フォントimport文の配列
|
|
113
|
+
* @param {string} globalsPath globals.cssのパス
|
|
114
|
+
* @returns {boolean} globals.css の更新に成功した場合は true
|
|
115
|
+
*/
|
|
116
|
+
export function updateGlobalsWithFonts(fontImports, globalsPath) {
|
|
117
|
+
if (fontImports.length === 0) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// 1. globals.css を読み込む
|
|
123
|
+
let globalsContent = fs.readFileSync(globalsPath, 'utf8');
|
|
124
|
+
|
|
125
|
+
// 2. 既存のimportを削除
|
|
126
|
+
globalsContent = removeExistingImports(globalsContent);
|
|
127
|
+
|
|
128
|
+
// 3. Tailwind import の位置を見つける
|
|
129
|
+
const tailwindInfo = findTailwindImport(globalsContent);
|
|
130
|
+
if (!tailwindInfo) {
|
|
131
|
+
console.warn(MESSAGES.TAILWIND_NOT_FOUND);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 4. importブロックを生成
|
|
136
|
+
const fontImportBlock = createFontImportBlock(fontImports);
|
|
137
|
+
const sparkleImportBlock = createSparkleImportBlock();
|
|
138
|
+
|
|
139
|
+
// 5. globals.css を再構築
|
|
140
|
+
const reconstructedContent = reconstructGlobalsCss(
|
|
141
|
+
globalsContent,
|
|
142
|
+
fontImportBlock,
|
|
143
|
+
sparkleImportBlock,
|
|
144
|
+
tailwindInfo
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// 6. 更新したglobals.cssを書き込む
|
|
148
|
+
fs.writeFileSync(globalsPath, reconstructedContent, 'utf8');
|
|
149
|
+
console.log(MESSAGES.GLOBALS_UPDATED(globalsPath));
|
|
150
|
+
return true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(MESSAGES.GLOBALS_UPDATE_FAILED(error.message));
|
|
153
|
+
// globals.cssの更新は必須ではないので、エラーでも処理を続行
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* フォント管理の自動処理を実行する
|
|
160
|
+
* sparkle-design.css からフォントimportを抽出し、globals.css に移動する
|
|
161
|
+
* @param {string} sparkleDesignPath sparkle-design.cssのパス
|
|
162
|
+
*/
|
|
163
|
+
export function manageFontImports(sparkleDesignPath) {
|
|
164
|
+
try {
|
|
165
|
+
// 1. globals.cssのパスを推定(sparkle-design.cssと同じディレクトリ)
|
|
166
|
+
const globalsPath = path.join(path.dirname(sparkleDesignPath), 'globals.css');
|
|
167
|
+
|
|
168
|
+
// 2. globals.cssが存在しない場合はスキップ
|
|
169
|
+
if (!fs.existsSync(globalsPath)) {
|
|
170
|
+
console.log(MESSAGES.GLOBALS_NOT_FOUND);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 3. sparkle-design.css からフォントimportを抽出
|
|
175
|
+
const sparkleContent = fs.readFileSync(sparkleDesignPath, 'utf8');
|
|
176
|
+
const fontImports = extractFontImports(sparkleContent);
|
|
177
|
+
|
|
178
|
+
if (fontImports.length === 0) {
|
|
179
|
+
console.log(MESSAGES.NO_FONT_IMPORTS);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(MESSAGES.FONT_DETECTED(fontImports.length));
|
|
184
|
+
|
|
185
|
+
// 4. globals.css にフォントimportを追加
|
|
186
|
+
const globalsUpdated = updateGlobalsWithFonts(fontImports, globalsPath);
|
|
187
|
+
|
|
188
|
+
if (!globalsUpdated) {
|
|
189
|
+
console.log(MESSAGES.FONT_REMOVE_SKIPPED);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 5. sparkle-design.css からフォントimportを削除
|
|
194
|
+
const cleanedSparkleContent = removeFontImportsFromCSS(sparkleContent);
|
|
195
|
+
fs.writeFileSync(sparkleDesignPath, cleanedSparkleContent, 'utf8');
|
|
196
|
+
console.log(MESSAGES.FONT_REMOVED);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(MESSAGES.FONT_MANAGEMENT_ERROR(error.message));
|
|
199
|
+
// フォント管理は必須ではないので、エラーでも処理を続行
|
|
200
|
+
}
|
|
201
|
+
}
|
package/lib/generate-css.js
CHANGED
|
@@ -1,124 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { oklch } from 'culori';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* sparkle.config.jsonを読み込む
|
|
13
|
-
*/
|
|
14
|
-
function loadConfig(configPath = null) {
|
|
15
|
-
// カスタムパスが指定されていない場合は実行場所のsparkle.config.jsonを探す
|
|
16
|
-
const defaultConfigPath = path.resolve(process.cwd(), 'sparkle.config.json');
|
|
17
|
-
const resolvedConfigPath = configPath ? path.resolve(configPath) : defaultConfigPath;
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const configContent = fs.readFileSync(resolvedConfigPath, 'utf8');
|
|
21
|
-
const config = JSON.parse(configContent);
|
|
22
|
-
console.log('✅ sparkle.config.json を読み込みました:', resolvedConfigPath);
|
|
23
|
-
console.log(' 設定内容:', config);
|
|
24
|
-
return config;
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.error('❌ sparkle.config.json の読み込みに失敗しました:', error.message);
|
|
27
|
-
console.error('設定ファイルパス:', resolvedConfigPath);
|
|
28
|
-
console.error('設定ファイルは専用プラグインから生成するか、手動で作成してください');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* CSSテンプレートを読み込む
|
|
35
|
-
*/
|
|
36
|
-
function loadTemplate() {
|
|
37
|
-
const templatePath = path.join(
|
|
38
|
-
__dirname,
|
|
39
|
-
'..',
|
|
40
|
-
'templates',
|
|
41
|
-
'sparkle-variables',
|
|
42
|
-
'sparkle-design.template.css'
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
47
|
-
console.log('✅ CSSテンプレートを読み込みました');
|
|
48
|
-
return templateContent;
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error('❌ CSSテンプレートの読み込みに失敗しました:', error.message);
|
|
51
|
-
console.error('テンプレートファイルが存在することを確認してください:', templatePath);
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 色定義(colors.json)を読み込む
|
|
58
|
-
*/
|
|
59
|
-
function loadColors() {
|
|
60
|
-
const colorsPath = path.join(__dirname, '..', 'templates', 'sparkle-variables', 'colors.json');
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const colorsContent = fs.readFileSync(colorsPath, 'utf8');
|
|
64
|
-
const colors = JSON.parse(colorsContent);
|
|
65
|
-
console.log('✅ colors.json を読み込みました');
|
|
66
|
-
return colors;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error('❌ colors.json の読み込みに失敗しました:', error.message);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
3
|
/**
|
|
74
|
-
*
|
|
4
|
+
* Sparkle Design CSS Generator
|
|
5
|
+
* メインの CSS 生成処理
|
|
75
6
|
*/
|
|
76
|
-
function loadGrayMapping() {
|
|
77
|
-
const grayJsonPath = path.join(__dirname, '..', 'templates', 'sparkle-variables', 'gray.json');
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const grayContent = fs.readFileSync(grayJsonPath, 'utf8');
|
|
81
|
-
const grayMapping = JSON.parse(grayContent);
|
|
82
|
-
console.log('✅ gray.json を読み込みました');
|
|
83
|
-
return grayMapping;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error('❌ gray.json の読み込みに失敗しました:', error.message);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
7
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (let j = 1; j < headers.length; j++) {
|
|
108
|
-
radiusMapping[name][headers[j]] = values[j];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
console.log('✅ radius.csv を読み込みました');
|
|
113
|
-
return radiusMapping;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('❌ radius.csv の読み込みに失敗しました:', error.message);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { REGEX, PATHS, MESSAGES } from './constants.js';
|
|
11
|
+
import {
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadTemplate,
|
|
14
|
+
loadColors,
|
|
15
|
+
loadGrayMapping,
|
|
16
|
+
loadRadiusMapping,
|
|
17
|
+
} from './file-loader.js';
|
|
18
|
+
import { generateColorTokens, hexToRgba, hexToOklch } from './color-utils.js';
|
|
19
|
+
import {
|
|
20
|
+
manageFontImports,
|
|
21
|
+
extractFontImports,
|
|
22
|
+
removeFontImportsFromCSS,
|
|
23
|
+
updateGlobalsWithFonts,
|
|
24
|
+
} from './font-manager.js';
|
|
119
25
|
|
|
120
26
|
/**
|
|
121
27
|
* テンプレート変数を設定値で置換する
|
|
28
|
+
* @param {string} template CSSテンプレート
|
|
29
|
+
* @param {Object} config 設定オブジェクト
|
|
30
|
+
* @param {Object} grayMapping グレーマッピング
|
|
31
|
+
* @param {Object} radiusMapping Radiusマッピング
|
|
32
|
+
* @param {Object} colors 色定義
|
|
33
|
+
* @returns {string} 処理済みのCSS
|
|
122
34
|
*/
|
|
123
35
|
function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
124
36
|
let processedCSS = template;
|
|
@@ -126,7 +38,7 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
126
38
|
// 1. 色のトークンを生成して置換
|
|
127
39
|
const colorTokens = generateColorTokens(colors, grayMapping, config.primary);
|
|
128
40
|
// プレースホルダーを色のトークンで置換(改行を含む)
|
|
129
|
-
processedCSS = processedCSS.replace(
|
|
41
|
+
processedCSS = processedCSS.replace(REGEX.COLOR_TOKENS_PLACEHOLDER, `\n${colorTokens}\n`);
|
|
130
42
|
|
|
131
43
|
// 2. 基本的な設定値による置換
|
|
132
44
|
Object.entries(config).forEach(([key, value]) => {
|
|
@@ -149,150 +61,32 @@ function processTemplate(template, config, grayMapping, radiusMapping, colors) {
|
|
|
149
61
|
Object.entries(radiusValues).forEach(([semanticName, radiusValue]) => {
|
|
150
62
|
if (semanticName === 'round') return; // roundは特別扱い
|
|
151
63
|
|
|
152
|
-
const semanticPattern =
|
|
153
|
-
`--radius-${semanticName}:\\s*var\\(--radius-[^)]+\\)`,
|
|
154
|
-
'g'
|
|
155
|
-
);
|
|
64
|
+
const semanticPattern = REGEX.RADIUS_SEMANTIC_PATTERN(semanticName);
|
|
156
65
|
processedCSS = processedCSS.replace(
|
|
157
66
|
semanticPattern,
|
|
158
67
|
`--radius-${semanticName}: var(--radius-${radiusValue})`
|
|
159
68
|
);
|
|
160
69
|
});
|
|
161
70
|
|
|
162
|
-
console.log(
|
|
71
|
+
console.log(MESSAGES.RADIUS_APPLIED(config.radius));
|
|
163
72
|
}
|
|
164
73
|
|
|
165
|
-
console.log(
|
|
74
|
+
console.log(MESSAGES.TEMPLATE_PROCESSED);
|
|
166
75
|
return processedCSS;
|
|
167
76
|
}
|
|
168
77
|
|
|
169
|
-
/**
|
|
170
|
-
* 16進数カラーをrgba形式に変換
|
|
171
|
-
*/
|
|
172
|
-
function hexToRgba(hex) {
|
|
173
|
-
// #を削除
|
|
174
|
-
hex = hex.replace('#', '');
|
|
175
|
-
|
|
176
|
-
// 3桁の場合は6桁に拡張
|
|
177
|
-
if (hex.length === 3) {
|
|
178
|
-
hex = hex
|
|
179
|
-
.split('')
|
|
180
|
-
.map((char) => char + char)
|
|
181
|
-
.join('');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const r = parseInt(hex.substring(0, 2), 16);
|
|
185
|
-
const g = parseInt(hex.substring(2, 4), 16);
|
|
186
|
-
const b = parseInt(hex.substring(4, 6), 16);
|
|
187
|
-
|
|
188
|
-
return `rgba(${r}, ${g}, ${b}, 1)`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 16進数カラーをOKLCH形式に変換
|
|
193
|
-
* @param {string} hex 16進数のカラーコード(#付きまたは無し)
|
|
194
|
-
* @returns {string} OKLCH形式の文字列
|
|
195
|
-
*/
|
|
196
|
-
function hexToOklch(hex) {
|
|
197
|
-
// #を削除
|
|
198
|
-
const cleanHex = hex.replace('#', '');
|
|
199
|
-
|
|
200
|
-
// 3桁の場合は6桁に、4桁の場合は8桁に拡張
|
|
201
|
-
let fullHex = cleanHex;
|
|
202
|
-
if (cleanHex.length === 3 || cleanHex.length === 4) {
|
|
203
|
-
fullHex = cleanHex
|
|
204
|
-
.split('')
|
|
205
|
-
.map((char) => char + char)
|
|
206
|
-
.join('');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// culoriを使用してHEXからOKLCHに変換
|
|
210
|
-
const oklchColor = oklch(`#${fullHex}`);
|
|
211
|
-
|
|
212
|
-
if (!oklchColor) {
|
|
213
|
-
console.warn(`⚠️ 色の変換に失敗しました: ${hex}`);
|
|
214
|
-
return `oklch(0 0 0)`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 精度を調整して出力
|
|
218
|
-
const l = Math.round((oklchColor.l || 0) * 10000) / 10000;
|
|
219
|
-
const c = Math.round((oklchColor.c || 0) * 10000) / 10000;
|
|
220
|
-
const h = oklchColor.h ? Math.round(oklchColor.h * 10) / 10 : 0;
|
|
221
|
-
|
|
222
|
-
// アルファ値がある場合(1未満の場合のみ出力に追加)
|
|
223
|
-
if (oklchColor.alpha !== undefined && oklchColor.alpha < 1) {
|
|
224
|
-
const a = Math.round(oklchColor.alpha * 10000) / 10000;
|
|
225
|
-
return `oklch(${l} ${c} ${h} / ${a})`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return `oklch(${l} ${c} ${h})`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* 色のトークンを生成する
|
|
233
|
-
* @param {Object} colors colors.jsonから読み込んだ色の定義
|
|
234
|
-
* @param {Object} grayMapping gray.jsonから読み込んだグレーの定義
|
|
235
|
-
* @param {string} primaryColor プライマリカラーの名前
|
|
236
|
-
* @returns {string} 色のトークンのCSS文字列
|
|
237
|
-
*/
|
|
238
|
-
function generateColorTokens(colors, grayMapping, primaryColor) {
|
|
239
|
-
const colorTokens = [];
|
|
240
|
-
|
|
241
|
-
// 通常の色トークンを生成
|
|
242
|
-
Object.entries(colors).forEach(([colorName, colorValues]) => {
|
|
243
|
-
if (typeof colorValues === 'object' && colorValues !== null) {
|
|
244
|
-
Object.entries(colorValues).forEach(([level, hexValue]) => {
|
|
245
|
-
const oklchValue = hexToOklch(hexValue);
|
|
246
|
-
colorTokens.push(` --color-${colorName}-${level}: ${oklchValue};`);
|
|
247
|
-
});
|
|
248
|
-
} else if (typeof colorValues === 'string') {
|
|
249
|
-
// black, white, web-focusなどの単色
|
|
250
|
-
const oklchValue = hexToOklch(colorValues);
|
|
251
|
-
colorTokens.push(` --color-${colorName}: ${oklchValue};`);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// グレーの色トークンを生成(プライマリカラーに基づく)
|
|
256
|
-
if (primaryColor && grayMapping[primaryColor]) {
|
|
257
|
-
const grayColors = grayMapping[primaryColor];
|
|
258
|
-
Object.entries(grayColors).forEach(([key, hexValue]) => {
|
|
259
|
-
const grayLevel =
|
|
260
|
-
key === '0'
|
|
261
|
-
? '50'
|
|
262
|
-
: key === '1'
|
|
263
|
-
? '100'
|
|
264
|
-
: key === '2'
|
|
265
|
-
? '200'
|
|
266
|
-
: key === '3'
|
|
267
|
-
? '300'
|
|
268
|
-
: key === '4'
|
|
269
|
-
? '400'
|
|
270
|
-
: key === '5'
|
|
271
|
-
? '500'
|
|
272
|
-
: key === '6'
|
|
273
|
-
? '600'
|
|
274
|
-
: key === '7'
|
|
275
|
-
? '700'
|
|
276
|
-
: key === '8'
|
|
277
|
-
? '800'
|
|
278
|
-
: key === '9'
|
|
279
|
-
? '900'
|
|
280
|
-
: key;
|
|
281
|
-
|
|
282
|
-
const oklchValue = hexToOklch(hexValue);
|
|
283
|
-
colorTokens.push(` --color-gray-${grayLevel}: ${oklchValue};`);
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return colorTokens.join('\n');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
78
|
/**
|
|
291
79
|
* 生成されたCSSをファイルに書き出す
|
|
80
|
+
* @param {string} cssContent CSS内容
|
|
81
|
+
* @param {string|null} outputPath カスタム出力パス(オプション)
|
|
292
82
|
*/
|
|
293
83
|
function writeCSS(cssContent, outputPath = null) {
|
|
294
84
|
// カスタムパスが指定されていない場合は実行場所からsrc/appディレクトリに出力
|
|
295
|
-
const defaultOutputPath = path.resolve(
|
|
85
|
+
const defaultOutputPath = path.resolve(
|
|
86
|
+
process.cwd(),
|
|
87
|
+
...PATHS.DEFAULT_OUTPUT_DIR,
|
|
88
|
+
'sparkle-design.css'
|
|
89
|
+
);
|
|
296
90
|
const resolvedOutputPath = outputPath ? path.resolve(outputPath) : defaultOutputPath;
|
|
297
91
|
|
|
298
92
|
try {
|
|
@@ -303,18 +97,20 @@ function writeCSS(cssContent, outputPath = null) {
|
|
|
303
97
|
}
|
|
304
98
|
|
|
305
99
|
fs.writeFileSync(resolvedOutputPath, cssContent, 'utf8');
|
|
306
|
-
console.log(
|
|
100
|
+
console.log(MESSAGES.CSS_GENERATED(resolvedOutputPath));
|
|
307
101
|
} catch (error) {
|
|
308
|
-
console.error(
|
|
102
|
+
console.error(MESSAGES.CSS_WRITE_FAILED(error.message));
|
|
309
103
|
process.exit(1);
|
|
310
104
|
}
|
|
311
105
|
}
|
|
312
106
|
|
|
313
107
|
/**
|
|
314
108
|
* メイン処理
|
|
109
|
+
* @param {string|null} configPath カスタム設定ファイルのパス(オプション)
|
|
110
|
+
* @param {string|null} outputPath カスタム出力パス(オプション)
|
|
315
111
|
*/
|
|
316
112
|
export function generateCSS(configPath = null, outputPath = null) {
|
|
317
|
-
console.log(
|
|
113
|
+
console.log(MESSAGES.START);
|
|
318
114
|
|
|
319
115
|
// 1. 設定ファイルを読み込み
|
|
320
116
|
const config = loadConfig(configPath);
|
|
@@ -331,9 +127,19 @@ export function generateCSS(configPath = null, outputPath = null) {
|
|
|
331
127
|
const processedCSS = processTemplate(template, config, grayMapping, radiusMapping, colors);
|
|
332
128
|
|
|
333
129
|
// 5. CSSファイルを書き出し
|
|
130
|
+
const defaultOutputPath = path.resolve(
|
|
131
|
+
process.cwd(),
|
|
132
|
+
...PATHS.DEFAULT_OUTPUT_DIR,
|
|
133
|
+
'sparkle-design.css'
|
|
134
|
+
);
|
|
135
|
+
const resolvedOutputPath = outputPath ? path.resolve(outputPath) : defaultOutputPath;
|
|
334
136
|
writeCSS(processedCSS, outputPath);
|
|
335
137
|
|
|
336
|
-
|
|
138
|
+
// 6. フォント管理の自動処理を実行
|
|
139
|
+
console.log(MESSAGES.FONT_MANAGEMENT_START);
|
|
140
|
+
manageFontImports(resolvedOutputPath);
|
|
141
|
+
|
|
142
|
+
console.log(MESSAGES.SUCCESS);
|
|
337
143
|
}
|
|
338
144
|
|
|
339
145
|
// スクリプトが直接実行された場合のみメイン処理を実行
|
|
@@ -341,15 +147,24 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
341
147
|
generateCSS();
|
|
342
148
|
}
|
|
343
149
|
|
|
150
|
+
// テスト用エクスポート
|
|
344
151
|
export {
|
|
152
|
+
// メイン関数
|
|
153
|
+
processTemplate,
|
|
154
|
+
writeCSS,
|
|
155
|
+
// ファイル読み込み
|
|
345
156
|
loadConfig,
|
|
346
157
|
loadTemplate,
|
|
347
158
|
loadColors,
|
|
348
159
|
loadGrayMapping,
|
|
349
160
|
loadRadiusMapping,
|
|
350
|
-
|
|
351
|
-
writeCSS,
|
|
161
|
+
// カラー変換
|
|
352
162
|
hexToRgba,
|
|
353
163
|
hexToOklch,
|
|
354
164
|
generateColorTokens,
|
|
165
|
+
// フォント管理
|
|
166
|
+
extractFontImports,
|
|
167
|
+
removeFontImportsFromCSS,
|
|
168
|
+
updateGlobalsWithFonts,
|
|
169
|
+
manageFontImports,
|
|
355
170
|
};
|