@lism-css/mcp 0.1.0 → 0.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.
@@ -3,119 +3,120 @@
3
3
  "category": "color",
4
4
  "description": "セマンティックカラートークン。サイト全体のカラー設計に使用。",
5
5
  "tokens": [
6
- { "name": "--base", "value": "サイトの基本背景色", "description": "ベース背景色" },
7
- { "name": "--base-2", "value": "サイトの第2背景色", "description": "代替背景色" },
8
- { "name": "--text", "value": "メインのテキストカラー", "description": "基本テキスト色" },
9
- { "name": "--text-2", "value": "サブテキストカラー", "description": "補助テキスト色" },
10
- { "name": "--divider", "value": "ボーダー・区切り線の色", "description": "区切り線色" },
11
- { "name": "--link", "value": "リンクテキスト色", "description": "リンク色" },
12
- { "name": "--brand", "value": "ブランドカラー", "description": "ブランド色" },
13
- { "name": "--accent", "value": "アクセントカラー", "description": "アクセント色" }
6
+ { "name": "--base", "value": "hsl(224 4% 99%)", "description": "ベース背景色" },
7
+ { "name": "--base-2", "value": "hsl(224 8% 95%)", "description": "代替背景色" },
8
+ { "name": "--text", "value": "hsl(224 4% 8%)", "description": "基本テキスト色" },
9
+ { "name": "--text-2", "value": "hsl(224 6% 32%)", "description": "補助テキスト色" },
10
+ { "name": "--divider", "value": "hsl(224 8% 88%)", "description": "区切り線色" },
11
+ { "name": "--link", "value": "oklch(50% 0.3 240)", "description": "リンク色" },
12
+ { "name": "--brand", "value": "#1e5f8c", "description": "ブランド色" },
13
+ { "name": "--accent", "value": "#d94a6a", "description": "アクセント色" }
14
14
  ]
15
15
  },
16
16
  {
17
17
  "category": "color",
18
- "description": "パレットカラートークン。装飾やバリエーションに使用。",
18
+ "description": "パレットカラートークン。OKLCHで定義。--L(明度)と--C(彩度)の基準値を調整するとパレット全体の色味を変更可能。",
19
19
  "tokens": [
20
- { "name": "--red", "value": "赤系カラー" },
21
- { "name": "--blue", "value": "青系カラー" },
22
- { "name": "--green", "value": "緑系カラー" },
23
- { "name": "--yellow", "value": "黄系カラー" },
24
- { "name": "--purple", "value": "紫系カラー" },
25
- { "name": "--orange", "value": "オレンジ系カラー" },
26
- { "name": "--pink", "value": "ピンク系カラー" },
27
- { "name": "--gray", "value": "グレー系カラー" },
28
- { "name": "--white", "value": "" },
29
- { "name": "--black", "value": "" },
20
+ { "name": "--red", "value": "oklch(var(--L) var(--C) 20)" },
21
+ { "name": "--orange", "value": "oklch(calc(var(--L) + 4%) calc(var(--C) - 0.01) 52)" },
22
+ { "name": "--yellow", "value": "oklch(calc(var(--L) + 12%) calc(var(--C) - 0.025) 84)" },
23
+ { "name": "--green", "value": "oklch(calc(var(--L) + 4%) calc(var(--C) - 0.02) 152)" },
24
+ { "name": "--blue", "value": "oklch(calc(var(--L) - 2%) calc(var(--C) + 0.01) 260)" },
25
+ { "name": "--purple", "value": "oklch(calc(var(--L) - 4%) calc(var(--C) + 0.02) 292)" },
26
+ { "name": "--pink", "value": "oklch(calc(var(--L) + 2%) calc(var(--C) + 0.02) 348)" },
27
+ { "name": "--gray", "value": "oklch(calc(var(--L) - 4%) 0.04 256)" },
28
+ { "name": "--black", "value": "#000" },
29
+ { "name": "--white", "value": "#fff" },
30
30
  { "name": "--keycolor", "value": "コンポーネントごとのキーカラー" }
31
31
  ]
32
32
  },
33
33
  {
34
34
  "category": "spacing",
35
- "description": "スペーシングトークン。padding, margin, gap 等で使用。CSS変数名は --s{値}",
35
+ "description": "スペーシングトークン。padding, margin, gap 等で使用。CSS変数名は --s{値}。フィボナッチ数列ベースのスケーリング。--s-unit(≒8px)を基準とする。",
36
36
  "tokens": [
37
- { "name": "--s5", "value": "最小の余白 (step 5)" },
38
- { "name": "--s10", "value": "余白 step 10" },
39
- { "name": "--s15", "value": "余白 step 15" },
40
- { "name": "--s20", "value": "余白 step 20" },
41
- { "name": "--s30", "value": "余白 step 30" },
42
- { "name": "--s40", "value": "余白 step 40" },
43
- { "name": "--s50", "value": "余白 step 50" },
44
- { "name": "--s60", "value": "余白 step 60" },
45
- { "name": "--s70", "value": "余白 step 70" },
46
- { "name": "--s80", "value": "最大の余白 (step 80)" }
37
+ { "name": "--s5", "value": "calc(0.5 * var(--s-unit))", "description": "≒ 4px" },
38
+ { "name": "--s10", "value": "var(--s-unit)", "description": " 8px" },
39
+ { "name": "--s15", "value": "calc(1.5 * var(--s-unit))", "description": "≒ 12px" },
40
+ { "name": "--s20", "value": "calc(2 * var(--s-unit))", "description": "≒ 16px" },
41
+ { "name": "--s30", "value": "calc(3 * var(--s-unit))", "description": "≒ 24px" },
42
+ { "name": "--s40", "value": "calc(5 * var(--s-unit))", "description": "≒ 40px" },
43
+ { "name": "--s50", "value": "calc(8 * var(--s-unit))", "description": "≒ 64px" },
44
+ { "name": "--s60", "value": "calc(13 * var(--s-unit))", "description": "≒ 104px" },
45
+ { "name": "--s70", "value": "calc(21 * var(--s-unit))", "description": "≒ 168px" },
46
+ { "name": "--s80", "value": "calc(34 * var(--s-unit))", "description": "≒ 272px" }
47
47
  ]
48
48
  },
49
49
  {
50
50
  "category": "fontSize",
51
- "description": "フォントサイズトークン。CSS変数名は --fz--{値}",
51
+ "description": "フォントサイズトークン。CSS変数名は --fz--{値}。分子8の調和数列(倍音列)ベースのスケーリング。",
52
52
  "tokens": [
53
- { "name": "--fz--root", "value": "ルートのフォントサイズ" },
54
- { "name": "--fz--base", "value": "本文の基本フォントサイズ" },
55
- { "name": "--fz--5xl", "value": "最大サイズ" },
56
- { "name": "--fz--4xl", "value": "特大サイズ" },
57
- { "name": "--fz--3xl", "value": "3XLサイズ" },
58
- { "name": "--fz--2xl", "value": "2XLサイズ" },
59
- { "name": "--fz--xl", "value": "XLサイズ" },
60
- { "name": "--fz--l", "value": "Lサイズ" },
61
- { "name": "--fz--m", "value": "Mサイズ" },
62
- { "name": "--fz--s", "value": "Sサイズ" },
63
- { "name": "--fz--xs", "value": "XSサイズ" },
64
- { "name": "--fz--2xs", "value": "最小サイズ" }
53
+ { "name": "--fz--base", "value": "var(--REM)", "description": "本文の基本フォントサイズ(≒ 1rem)" },
54
+ { "name": "--fz--5xl", "value": "4em", "description": "最大サイズ(8/2)" },
55
+ { "name": "--fz--4xl", "value": "2.667em", "description": "特大サイズ(8/3)" },
56
+ { "name": "--fz--3xl", "value": "2em", "description": "3XLサイズ(8/4)" },
57
+ { "name": "--fz--2xl", "value": "1.6em", "description": "2XLサイズ(8/5)" },
58
+ { "name": "--fz--xl", "value": "1.333em", "description": "XLサイズ(8/6)" },
59
+ { "name": "--fz--l", "value": "1.143em", "description": "Lサイズ(8/7)" },
60
+ { "name": "--fz--m", "value": "1em", "description": "Mサイズ(基準)" },
61
+ { "name": "--fz--s", "value": "0.889em", "description": "Sサイズ(8/9)" },
62
+ { "name": "--fz--xs", "value": "0.8em", "description": "XSサイズ(8/10)" },
63
+ { "name": "--fz--2xs", "value": "0.727em", "description": "最小サイズ(8/11)" }
65
64
  ]
66
65
  },
67
66
  {
68
67
  "category": "lineHeight",
69
- "description": "行間 (heading line-height) トークン。CSS変数名は --hl--{値}。",
68
+ "description": "行間(ハーフレディング)トークン。CSS変数名は --hl--{値}。line-heightは calc(1em + var(--hl) * 2) で算出される。--hl-unit(≒2px)を基準とする。",
70
69
  "tokens": [
71
- { "name": "--hl--base", "value": "基本の行間" },
72
- { "name": "--hl--xs", "value": "極小の行間" },
73
- { "name": "--hl--s", "value": "小さめの行間" },
74
- { "name": "--hl--l", "value": "大きめの行間" }
70
+ { "name": "--hl--xs", "value": "var(--hl-unit)", "description": "極小の行間(≒ 2px)" },
71
+ { "name": "--hl--s", "value": "calc(var(--hl-unit) * 2)", "description": "小さめの行間(≒ 4px)" },
72
+ { "name": "--hl--base", "value": "calc(var(--hl-unit) * 3)", "description": "基本の行間(≒ 6px)" },
73
+ { "name": "--hl--l", "value": "calc(var(--hl-unit) * 4)", "description": "大きめの行間(≒ 8px)" }
75
74
  ]
76
75
  },
77
76
  {
78
77
  "category": "letterSpacing",
79
78
  "description": "文字間隔トークン。CSS変数名は --lts--{値}。",
80
79
  "tokens": [
81
- { "name": "--lts--base", "value": "基本の文字間隔" },
82
- { "name": "--lts--s", "value": "狭めの文字間隔" },
83
- { "name": "--lts--l", "value": "広めの文字間隔" }
80
+ { "name": "--lts--base", "value": "normal", "description": "基本の文字間隔" },
81
+ { "name": "--lts--s", "value": "-0.05em", "description": "狭めの文字間隔" },
82
+ { "name": "--lts--l", "value": "0.05em", "description": "広めの文字間隔" }
84
83
  ]
85
84
  },
86
85
  {
87
86
  "category": "fontFamily",
88
87
  "description": "フォントファミリートークン。CSS変数名は --ff--{値}。",
89
88
  "tokens": [
90
- { "name": "--ff--base", "value": "基本フォント" },
91
- { "name": "--ff--accent", "value": "アクセントフォント" },
92
- { "name": "--ff--mono", "value": "モノスペースフォント" }
89
+ {
90
+ "name": "--ff--base",
91
+ "value": "-apple-system, 'BlinkMacSystemFont', 'Hiragino Sans', sans-serif, 'Segoe UI Emoji'",
92
+ "description": "基本フォント"
93
+ },
94
+ { "name": "--ff--accent", "value": "'Garamond', 'Baskerville', 'Times New Roman', serif", "description": "アクセントフォント" },
95
+ { "name": "--ff--mono", "value": "ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace", "description": "モノスペースフォント" }
93
96
  ]
94
97
  },
95
98
  {
96
99
  "category": "radius",
97
100
  "description": "角丸トークン。CSS変数名は --bdrs--{値}。段階的に値が大きくなる。",
98
101
  "tokens": [
99
- { "name": "--bdrs--5", "value": "角丸 step 5" },
100
- { "name": "--bdrs--10", "value": "角丸 step 10" },
101
- { "name": "--bdrs--20", "value": "角丸 step 20" },
102
- { "name": "--bdrs--30", "value": "角丸 step 30" },
103
- { "name": "--bdrs--40", "value": "角丸 step 40" },
104
- { "name": "--bdrs--50", "value": "角丸 step 50" },
105
- { "name": "--bdrs--99", "value": "完全な丸 (pill)" },
106
- { "name": "--bdrs--inner", "value": "内側の角丸 (親要素に合わせる)" }
102
+ { "name": "--bdrs--5", "value": "2px", "description": "角丸 step 5" },
103
+ { "name": "--bdrs--10", "value": "0.25rem", "description": "角丸 step 10(≒ 4px)" },
104
+ { "name": "--bdrs--20", "value": "0.5rem", "description": "角丸 step 20(≒ 8px)" },
105
+ { "name": "--bdrs--30", "value": "1rem", "description": "角丸 step 30(≒ 16px)" },
106
+ { "name": "--bdrs--40", "value": "1.5rem", "description": "角丸 step 40(≒ 24px)" },
107
+ { "name": "--bdrs--50", "value": "2rem", "description": "角丸 step 50(≒ 32px)" },
108
+ { "name": "--bdrs--99", "value": "99rem", "description": "完全な丸 (pill)" }
107
109
  ]
108
110
  },
109
111
  {
110
112
  "category": "shadow",
111
- "description": "ボックスシャドウトークン。CSS変数名は --bxsh--{値}。段階的に影が強くなる。",
113
+ "description": "ボックスシャドウトークン。CSS変数名は --bxsh--{値}。2段階の複合シャドウで構成される。--shc(シャドウカラー: hsl(220 4% 8% / 5%))を使用。",
112
114
  "tokens": [
113
- { "name": "--bxsh--5", "value": "シャドウ step 5 (最も薄い)" },
114
- { "name": "--bxsh--10", "value": "シャドウ step 10" },
115
- { "name": "--bxsh--20", "value": "シャドウ step 20" },
116
- { "name": "--bxsh--30", "value": "シャドウ step 30" },
117
- { "name": "--bxsh--40", "value": "シャドウ step 40" },
118
- { "name": "--bxsh--50", "value": "シャドウ step 50 (最も濃い)" }
115
+ { "name": "--bxsh--10", "value": "var(--sh--5), var(--sh--10)", "description": "シャドウ step 10(最も薄い)" },
116
+ { "name": "--bxsh--20", "value": "var(--sh--10), var(--sh--20)", "description": "シャドウ step 20" },
117
+ { "name": "--bxsh--30", "value": "var(--sh--20), var(--sh--30)", "description": "シャドウ step 30" },
118
+ { "name": "--bxsh--40", "value": "var(--sh--30), var(--sh--40)", "description": "シャドウ step 40" },
119
+ { "name": "--bxsh--50", "value": "var(--sh--40), var(--sh--50)", "description": "シャドウ step 50(最も濃い)" }
119
120
  ]
120
121
  },
121
122
  {
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { registerGetTokens } from './tools/get-tokens.js';
6
6
  import { registerGetPropsSystem } from './tools/get-props-system.js';
7
7
  import { registerGetComponent } from './tools/get-component.js';
8
8
  import { registerSearchDocs } from './tools/search-docs.js';
9
+ import { registerConvertCss } from './tools/convert-css.js';
9
10
  async function main() {
10
11
  preloadAll();
11
12
  const server = new McpServer({
@@ -17,6 +18,7 @@ async function main() {
17
18
  registerGetPropsSystem(server);
18
19
  registerGetComponent(server);
19
20
  registerSearchDocs(server);
21
+ registerConvertCss(server);
20
22
  const transport = new StdioServerTransport();
21
23
  await server.connect(transport);
22
24
  }
@@ -29,6 +29,7 @@ export declare const ComponentInfoSchema: z.ZodObject<{
29
29
  package: z.ZodEnum<["lism-css", "@lism-css/ui"]>;
30
30
  category: z.ZodString;
31
31
  description: z.ZodString;
32
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
32
33
  props: z.ZodArray<z.ZodObject<{
33
34
  name: z.ZodString;
34
35
  type: z.ZodString;
@@ -58,6 +59,7 @@ export declare const ComponentInfoSchema: z.ZodObject<{
58
59
  default?: string | undefined;
59
60
  }[];
60
61
  usage: string;
62
+ aliases?: string[] | undefined;
61
63
  }, {
62
64
  description: string;
63
65
  category: string;
@@ -70,6 +72,7 @@ export declare const ComponentInfoSchema: z.ZodObject<{
70
72
  default?: string | undefined;
71
73
  }[];
72
74
  usage: string;
75
+ aliases?: string[] | undefined;
73
76
  }>;
74
77
  export declare const TokenCategorySchema: z.ZodObject<{
75
78
  category: z.ZodString;
@@ -19,6 +19,7 @@ export const ComponentInfoSchema = z.object({
19
19
  package: z.enum(['lism-css', '@lism-css/ui']),
20
20
  category: z.string(),
21
21
  description: z.string(),
22
+ aliases: z.array(z.string()).optional(),
22
23
  props: z.array(ComponentPropSchema),
23
24
  usage: z.string(),
24
25
  });
@@ -1,2 +1,21 @@
1
- import type { DocsEntry, SearchResult } from './types.js';
2
- export declare function searchDocs(entries: DocsEntry[], query: string, category?: string, limit?: number): SearchResult[];
1
+ import type { DocsEntry, PropCategory, SearchResult } from './types.js';
2
+ /**
3
+ * コンポーネント名 → aliases (lowercase) のマップを構築。
4
+ * ComponentInfo に限らず { name, aliases? } を持つ任意の配列から生成できる。
5
+ */
6
+ export declare function buildAliasMap(items: Array<{
7
+ name: string;
8
+ aliases?: string[];
9
+ }>): Map<string, string[]>;
10
+ /**
11
+ * CSSプロパティ名 → Lism prop名のマップを構築。
12
+ * 検索クエリにCSSプロパティ名が含まれていた場合に、対応するLism prop名で展開するために使用。
13
+ */
14
+ export declare function buildCssPropertyMap(categories: PropCategory[]): Map<string, string[]>;
15
+ export interface SearchDocsOptions {
16
+ category?: string;
17
+ limit?: number;
18
+ aliasMap?: Map<string, string[]>;
19
+ cssPropertyMap?: Map<string, string[]>;
20
+ }
21
+ export declare function searchDocs(entries: DocsEntry[], query: string, options?: SearchDocsOptions): SearchResult[];
@@ -4,7 +4,68 @@ function tokenize(text) {
4
4
  .split(/[\s\-_./]+/)
5
5
  .filter((t) => t.length > 0);
6
6
  }
7
- function scoreEntry(entry, queryTokens) {
7
+ /**
8
+ * コンポーネント名 → aliases (lowercase) のマップを構築。
9
+ * ComponentInfo に限らず { name, aliases? } を持つ任意の配列から生成できる。
10
+ */
11
+ export function buildAliasMap(items) {
12
+ const map = new Map();
13
+ for (const item of items) {
14
+ if (item.aliases && item.aliases.length > 0) {
15
+ map.set(item.name.toLowerCase(), item.aliases.map((a) => a.toLowerCase()));
16
+ }
17
+ }
18
+ return map;
19
+ }
20
+ /**
21
+ * CSSプロパティ名 → Lism prop名のマップを構築。
22
+ * 検索クエリにCSSプロパティ名が含まれていた場合に、対応するLism prop名で展開するために使用。
23
+ */
24
+ export function buildCssPropertyMap(categories) {
25
+ const map = new Map();
26
+ for (const cat of categories) {
27
+ for (const p of cat.props) {
28
+ // "(class: ...)" はスキップ
29
+ if (p.cssProperty.startsWith('(class:'))
30
+ continue;
31
+ // "--hl (CSS変数)" → "--hl"
32
+ const normalized = p.cssProperty.replace(/\s*\(.*\)$/, '').trim().toLowerCase();
33
+ if (!normalized)
34
+ continue;
35
+ const existing = map.get(normalized) ?? [];
36
+ existing.push(p.prop.toLowerCase());
37
+ map.set(normalized, existing);
38
+ }
39
+ }
40
+ return map;
41
+ }
42
+ /**
43
+ * 検索クエリをCSSプロパティ名で展開する。
44
+ * 例: "font-size" → "font-size fz", "padding" → "padding p"
45
+ */
46
+ function expandQueryWithCssProps(query, cssPropertyMap) {
47
+ if (!cssPropertyMap)
48
+ return query;
49
+ const queryLower = query.toLowerCase();
50
+ const additions = [];
51
+ for (const [cssProp, lismProps] of cssPropertyMap) {
52
+ if (queryLower.includes(cssProp)) {
53
+ additions.push(...lismProps);
54
+ }
55
+ }
56
+ return additions.length > 0 ? `${query} ${additions.join(' ')}` : query;
57
+ }
58
+ // docs entry のタイトルに含まれるコンポーネント名から aliases を取得
59
+ function getEntryAliases(entry, aliasMap) {
60
+ const titleLower = entry.title.toLowerCase();
61
+ for (const [name, aliases] of aliasMap) {
62
+ if (titleLower.includes(name)) {
63
+ return aliases;
64
+ }
65
+ }
66
+ return [];
67
+ }
68
+ function scoreEntry(entry, queryTokens, aliasMap) {
8
69
  let score = 0;
9
70
  const titleLower = entry.title.toLowerCase();
10
71
  const descLower = entry.description.toLowerCase();
@@ -28,10 +89,24 @@ function scoreEntry(entry, queryTokens) {
28
89
  if (snippetLower.includes(token))
29
90
  score += 1;
30
91
  }
92
+ // コンポーネントの aliases によるブースト
93
+ if (aliasMap) {
94
+ const aliases = getEntryAliases(entry, aliasMap);
95
+ if (aliases.length > 0) {
96
+ const aliasesStr = aliases.join(' ');
97
+ for (const token of queryTokens) {
98
+ if (aliasesStr.includes(token))
99
+ score += 5;
100
+ }
101
+ }
102
+ }
31
103
  return score;
32
104
  }
33
- export function searchDocs(entries, query, category, limit = 10) {
34
- const queryTokens = tokenize(query);
105
+ export function searchDocs(entries, query, options) {
106
+ const { category, limit = 10, aliasMap, cssPropertyMap } = options ?? {};
107
+ // CSSプロパティ名をLism prop名に展開してからトークナイズ
108
+ const expandedQuery = expandQueryWithCssProps(query, cssPropertyMap);
109
+ const queryTokens = tokenize(expandedQuery);
35
110
  if (queryTokens.length === 0)
36
111
  return [];
37
112
  let filtered = entries;
@@ -40,7 +115,7 @@ export function searchDocs(entries, query, category, limit = 10) {
40
115
  }
41
116
  const scored = filtered
42
117
  .map((entry) => {
43
- const score = scoreEntry(entry, queryTokens);
118
+ const score = scoreEntry(entry, queryTokens, aliasMap);
44
119
  return { entry, score };
45
120
  })
46
121
  .filter(({ score }) => score > 0)
@@ -24,6 +24,7 @@ export interface ComponentInfo {
24
24
  package: 'lism-css' | '@lism-css/ui';
25
25
  category: string;
26
26
  description: string;
27
+ aliases?: string[];
27
28
  props: ComponentProp[];
28
29
  usage: string;
29
30
  }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConvertCss(server: McpServer): void;
@@ -0,0 +1,223 @@
1
+ import { z } from 'zod';
2
+ import { loadJSON } from '../lib/load-data.js';
3
+ import { PropsSystemDataSchema } from '../lib/schemas.js';
4
+ import { success, error, READ_ONLY_ANNOTATIONS } from '../lib/response.js';
5
+ // ----------------------------------------------------------------
6
+ // CSS パース
7
+ // ----------------------------------------------------------------
8
+ function parseCssDeclarations(cssText) {
9
+ // コメント除去
10
+ let cleaned = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
11
+ // セレクタ + ブレースを除去(裸の宣言リストも受け付ける)
12
+ cleaned = cleaned.replace(/[^{}]*\{/g, '').replace(/\}/g, '');
13
+ const declarations = [];
14
+ for (const line of cleaned.split(';')) {
15
+ const trimmed = line.trim();
16
+ if (!trimmed)
17
+ continue;
18
+ const colonIdx = trimmed.indexOf(':');
19
+ if (colonIdx === -1)
20
+ continue;
21
+ const property = trimmed.substring(0, colonIdx).trim().toLowerCase();
22
+ const value = trimmed.substring(colonIdx + 1).trim();
23
+ if (property && value) {
24
+ declarations.push({ property, value });
25
+ }
26
+ }
27
+ return declarations;
28
+ }
29
+ // ----------------------------------------------------------------
30
+ // CSSプロパティ → PropEntry マップ
31
+ // ----------------------------------------------------------------
32
+ function buildCssPropertyMap(categories) {
33
+ const map = new Map();
34
+ for (const cat of categories) {
35
+ for (const prop of cat.props) {
36
+ const normalized = normalizeCssPropertyName(prop.cssProperty);
37
+ if (normalized)
38
+ map.set(normalized, prop);
39
+ }
40
+ }
41
+ return map;
42
+ }
43
+ /** cssProperty フィールドの正規化 */
44
+ function normalizeCssPropertyName(raw) {
45
+ // "(class: is--container)" → スキップ(CSS プロパティではない)
46
+ if (raw.startsWith('(class:'))
47
+ return null;
48
+ // "--hl (CSS変数)" → "--hl"
49
+ return raw.replace(/\s*\(.*\)$/, '').trim().toLowerCase();
50
+ }
51
+ // ----------------------------------------------------------------
52
+ // 値のマッピング
53
+ // ----------------------------------------------------------------
54
+ /** よくある CSS 値 → Lism トークン値の変換テーブル */
55
+ const VALUE_ALIASES = {
56
+ column: 'col',
57
+ 'column-reverse': 'col-r',
58
+ 'row-reverse': 'row-r',
59
+ 'space-between': 'between',
60
+ 'flex-start': 'flex-s',
61
+ 'flex-end': 'flex-e',
62
+ currentcolor: 'cc',
63
+ uppercase: 'upper',
64
+ lowercase: 'lower',
65
+ };
66
+ function suggestValue(propEntry, cssValue) {
67
+ const tokens = propEntry.values;
68
+ if (!tokens || tokens.length === 0)
69
+ return null;
70
+ // 直接一致
71
+ if (tokens.includes(cssValue))
72
+ return cssValue;
73
+ // エイリアス変換後に一致
74
+ const aliased = VALUE_ALIASES[cssValue.toLowerCase()];
75
+ if (aliased && tokens.includes(aliased))
76
+ return aliased;
77
+ return null;
78
+ }
79
+ // ----------------------------------------------------------------
80
+ // コンポーネント検出
81
+ // ----------------------------------------------------------------
82
+ function detectComponent(declarations) {
83
+ const propMap = new Map(declarations.map((d) => [d.property, d.value.toLowerCase()]));
84
+ const display = propMap.get('display');
85
+ const flexDirection = propMap.get('flex-direction');
86
+ const placeItems = propMap.get('place-items');
87
+ // Stack: flex + column
88
+ if (display === 'flex' && (flexDirection === 'column' || flexDirection === 'column-reverse')) {
89
+ return {
90
+ name: 'Stack',
91
+ reason: 'display: flex + flex-direction: column → Stack(縦積み Flex)',
92
+ implicitCss: ['display: flex', 'flex-direction: column'],
93
+ };
94
+ }
95
+ // Center: grid + place-items: center
96
+ if (display === 'grid' && placeItems === 'center') {
97
+ return {
98
+ name: 'Center',
99
+ reason: 'display: grid + place-items: center → Center(中央配置 Grid)',
100
+ implicitCss: ['display: grid', 'place-items: center'],
101
+ };
102
+ }
103
+ // Flex
104
+ if (display === 'flex') {
105
+ return {
106
+ name: 'Flex',
107
+ reason: 'display: flex → Flex コンポーネント',
108
+ implicitCss: ['display: flex'],
109
+ };
110
+ }
111
+ // Grid
112
+ if (display === 'grid') {
113
+ return {
114
+ name: 'Grid',
115
+ reason: 'display: grid → Grid コンポーネント',
116
+ implicitCss: ['display: grid'],
117
+ };
118
+ }
119
+ return null;
120
+ }
121
+ // ----------------------------------------------------------------
122
+ // 変換メイン
123
+ // ----------------------------------------------------------------
124
+ function findCategory(categories, propName) {
125
+ for (const cat of categories) {
126
+ if (cat.props.some((p) => p.prop === propName))
127
+ return cat.category;
128
+ }
129
+ return 'unknown';
130
+ }
131
+ function buildExample(conversions, component) {
132
+ const tagName = component?.name ?? 'Lism';
133
+ const implicitCssSet = new Set(component?.implicitCss.map((c) => c.split(':')[0].trim()) ?? []);
134
+ const props = [];
135
+ const styles = [];
136
+ for (const conv of conversions) {
137
+ const cssProp = conv.css.split(':')[0].trim();
138
+ if (!conv.lismProp) {
139
+ styles.push(conv.css);
140
+ continue;
141
+ }
142
+ // コンポーネントが暗黙的に付与する CSS はスキップ
143
+ if (implicitCssSet.has(cssProp))
144
+ continue;
145
+ if (conv.suggestedValue != null) {
146
+ props.push(`${conv.lismProp}='${conv.suggestedValue}'`);
147
+ }
148
+ else {
149
+ const cssValue = conv.css.split(':').slice(1).join(':').trim();
150
+ props.push(`${conv.lismProp}='${cssValue}'`);
151
+ }
152
+ }
153
+ let result = `<${tagName}`;
154
+ if (props.length > 0)
155
+ result += ` ${props.join(' ')}`;
156
+ if (styles.length > 0) {
157
+ result += ` style="${styles.map((s) => s.replace(/"/g, "'")).join('; ')}"`;
158
+ }
159
+ result += `>...</${tagName}>`;
160
+ return result;
161
+ }
162
+ // ----------------------------------------------------------------
163
+ // ツール登録
164
+ // ----------------------------------------------------------------
165
+ export function registerConvertCss(server) {
166
+ server.registerTool('convert_css', {
167
+ description: 'Convert CSS code to lism-css props, utility classes, and component suggestions. Accepts CSS declarations (with or without selectors) and returns the equivalent lism-css representation. Use this to migrate existing CSS to lism-css, or to understand how CSS properties map to lism-css.',
168
+ inputSchema: {
169
+ css: z
170
+ .string()
171
+ .describe('CSS code to convert. Accepts a full rule block with selector (e.g. ".foo { padding: 1rem; }") or bare declarations (e.g. "padding: 1rem; font-size: 16px;").'),
172
+ },
173
+ annotations: READ_ONLY_ANNOTATIONS,
174
+ }, ({ css }) => {
175
+ try {
176
+ const propsData = loadJSON('props-system.json', PropsSystemDataSchema);
177
+ const cssPropertyMap = buildCssPropertyMap(propsData.categories);
178
+ const declarations = parseCssDeclarations(css);
179
+ if (declarations.length === 0) {
180
+ return error('CSS 宣言が見つかりません。"property: value;" 形式の CSS を入力してください。');
181
+ }
182
+ // 各宣言を変換
183
+ const conversions = declarations.map((decl) => {
184
+ const propEntry = cssPropertyMap.get(decl.property);
185
+ if (!propEntry) {
186
+ return {
187
+ css: `${decl.property}: ${decl.value}`,
188
+ lismProp: null,
189
+ suggestedValue: null,
190
+ availableTokens: null,
191
+ note: 'Lism Props に該当なし。style で直接指定してください。',
192
+ };
193
+ }
194
+ const suggested = suggestValue(propEntry, decl.value);
195
+ const category = findCategory(propsData.categories, propEntry.prop);
196
+ return {
197
+ css: `${decl.property}: ${decl.value}`,
198
+ lismProp: propEntry.prop,
199
+ suggestedValue: suggested,
200
+ availableTokens: propEntry.values ?? null,
201
+ note: suggested
202
+ ? `トークン値 '${suggested}' を使用(カテゴリ: ${category})`
203
+ : propEntry.values && propEntry.values.length > 0
204
+ ? `カスタム値。利用可能なトークン: ${propEntry.values.join(', ')}(カテゴリ: ${category})`
205
+ : `カスタム値として指定(カテゴリ: ${category})`,
206
+ };
207
+ });
208
+ // コンポーネント検出
209
+ const suggestedComponent = detectComponent(declarations);
210
+ // 使用例
211
+ const example = buildExample(conversions, suggestedComponent);
212
+ return success({
213
+ conversions,
214
+ suggestedComponent,
215
+ example,
216
+ tip: 'トークン値にマッチしない値は style 属性で CSS 変数として指定できます(例: style="--p: 1rem")。get_props_system で各 prop の詳細を確認できます。',
217
+ });
218
+ }
219
+ catch (e) {
220
+ return error(`CSS 変換に失敗しました: ${e instanceof Error ? e.message : String(e)}`);
221
+ }
222
+ });
223
+ }