@monoharada/wcf-mcp 0.5.0 → 0.6.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
@@ -78,7 +78,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
78
78
  }
79
79
  ```
80
80
 
81
- ## 提供機能(14 tools + 1 prompt + 4 resources)
81
+ ## 提供機能(16 tools + 1 prompt + 4 resources)
82
82
 
83
83
  ### ガードレール
84
84
 
@@ -92,7 +92,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
92
92
  |--------|------|
93
93
  | `list_components` | カテゴリ/クエリ/limit/offset でコンポーネントを段階的に取得(デフォルト20件。全件取得は `limit: 200`) |
94
94
  | `search_icons` | アイコン名をキーワード検索し、usage example を返す |
95
- | `get_component_api` | tagName or className で属性・スロット・イベント・CSS Parts・CSS Custom Properties を取得(`relatedComponents` を含む) |
95
+ | `get_component_api` | tagName or className で属性・スロット・イベント・CSS Parts・CSS Custom Properties を取得。`components` 配列でバッチ取得可能(最大10件) |
96
96
  | `generate_usage_snippet` | コンポーネントの最小限 HTML スニペットを生成 |
97
97
  | `get_install_recipe` | componentId・依存関係・define関数・インストールコマンドを取得 |
98
98
 
@@ -117,6 +117,8 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
117
117
  | `tokenMisuse` | warning | inline style でのトークン誤用 | `color: #000` → `var(--color-*)` |
118
118
  | `ariaLiveNotRecommended` | warning | `aria-live` の使用(DADS 非推奨) | `aria-live="polite"` |
119
119
  | `roleAlertNotRecommended` | warning | `role="alert"` の使用(DADS 非推奨) | `role="alert"` |
120
+ | `emptyLabel` | error | 空の `label` 属性(アクセシビリティ違反) | `<dads-input-text label="">` |
121
+ | `emptyAriaLabel` | error | 空の `aria-label` 属性(アクセシビリティ違反) | `<dads-button aria-label="">` |
120
122
  | `forbiddenAttribute` | warning | 禁止属性 | `placeholder` |
121
123
 
122
124
  ### UI パターン
@@ -126,6 +128,13 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
126
128
  | `list_patterns` | 利用可能な UI パターン(レシピ)を一覧表示 |
127
129
  | `get_pattern_recipe` | パターンの完全レシピ(必要コンポーネント・依存解決・HTML)を取得 |
128
130
  | `generate_pattern_snippet` | パターンの HTML スニペットを生成 |
131
+ | `generate_full_page_html` | HTML フラグメントを `<!DOCTYPE html>` + importmap + boot script 付きの完全ページに変換 |
132
+
133
+ ### コンポーネント選択支援
134
+
135
+ | ツール | 説明 |
136
+ |--------|------|
137
+ | `get_component_selector_guide` | カテゴリ・ユースケースでコンポーネントを選択支援(6カテゴリ: Form, Actions, Navigation, Content, Display, Layout) |
129
138
 
130
139
  ### トークン・ガイドライン検索
131
140
 
@@ -372,6 +381,27 @@ Claude Desktop 設定例:
372
381
  prefix: "myui" → dads-button → myui-button
373
382
  ```
374
383
 
384
+ ## v0.4.0 新機能
385
+
386
+ ### 新ツール
387
+ - **`generate_full_page_html`** — HTML フラグメントを完全な HTML ページに変換(importmap, boot script, tokens CSS 付き)
388
+ - **`get_component_selector_guide`** — カテゴリ・ユースケースキーワードでコンポーネント選択を支援
389
+
390
+ ### 改善
391
+ - **空ラベル検出** — `label=""` / `aria-label=""` を error として検出(`emptyLabel`, `emptyAriaLabel`)
392
+ - **属性プリフィル** — `generate_usage_snippet` で CEM デフォルト値を自動挿入
393
+ - **アイコンエイリアス** — `search_icons` で `"trash"` → `"delete"` 等のエイリアス展開
394
+ - **ガイドライン拡充** — spacing token, `::part()`, div-soup, form-validation の検索対応
395
+ - **パターンビヘイビア** — `get_pattern_recipe` に JS コード例(`behavior` フィールド)を追加
396
+ - **コンポーネントトークン参照** — `get_design_token_detail` に `componentReferencedBy` フィールド追加
397
+ - **バッチ対応** — `get_component_api` に `components` 配列パラメータ(最大10件)
398
+ - **vendor path 統一** — `setupInfo` のパスを `<dir>` プレースホルダに統一
399
+
400
+ ### Breaking Changes
401
+ - `setupInfo.vendorRuntimePath` の値が `vendor-runtime/` から `<dir>/` ベースに変更
402
+
403
+ ---
404
+
375
405
  ## v0.3.0 新機能 — ランタイムセットアップ情報
376
406
 
377
407
  v0.3.0 では、AI エージェントが CDN 非対応の vendor-local 配信モデルを正しく理解できるよう、3つのツールにランタイムセットアップ情報を追加しました。
package/core.mjs CHANGED
@@ -112,6 +112,8 @@ const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
112
112
  'get_design_token_detail',
113
113
  'get_accessibility_docs',
114
114
  'search_guidelines',
115
+ 'generate_full_page_html',
116
+ 'get_component_selector_guide',
115
117
  ]));
116
118
  const A11Y_CATEGORY_LEVEL_MAP = Object.freeze({
117
119
  semantics: 'A',
@@ -141,15 +143,39 @@ const SYNONYM_TABLE = new Map([
141
143
  ['aria-live', ['role=alert', 'aria-describedby', 'live region', 'error text']],
142
144
  ['keyboard', ['focus', 'tab', 'tabindex', 'key event', 'focus trap']],
143
145
  ['contrast', ['color', 'wcag', 'color contrast']],
144
- ['spacing', ['margin', 'padding', 'gap', 'spacing token']],
146
+ ['spacing', ['margin', 'padding', 'gap', 'spacing token', '--spacing']],
145
147
  ['skip-navigation', ['skip-link', 'landmark', 'skip nav']],
146
148
  ['heading', ['heading hierarchy', 'h1', 'heading level']],
147
149
  ['form', ['input', 'validation', 'required', 'label']],
150
+ ['part', ['::part', 'css part', 'shadow dom styling']],
148
151
  ['layout', ['grid', 'flexbox', 'layout-shell', 'responsive', 'breakpoint']],
149
152
  ['responsive', ['media query', 'breakpoint', 'viewport', 'mobile']],
150
153
  ['error', ['validation', 'aria-invalid', 'aria-describedby', 'error text']],
151
154
  ['focus', ['focus-visible', 'focus ring', 'outline', 'tabindex', 'keyboard']],
152
155
  ['token', ['design token', 'css variable', 'custom property', 'spacing token']],
156
+ ['div-soup', ['wrapper', 'unnecessary div', 'minimal dom']],
157
+ ]);
158
+
159
+ // Icon alias table: common alias → canonical icon names (DD-18)
160
+ // Maps user-friendly search terms to actual icon names in the CEM catalog.
161
+ const ICON_ALIAS_TABLE = new Map([
162
+ ['x', ['close', 'cancel']],
163
+ ['trash', ['delete']],
164
+ ['pencil', ['edit']],
165
+ ['magnifying', ['search']],
166
+ ['gear', ['settings']],
167
+ ['plus', ['add']],
168
+ ['minus', ['subtract']],
169
+ ['tick', ['check', 'checkmark']],
170
+ ['alert', ['warning', 'attention']],
171
+ ['info', ['information', 'help']],
172
+ ['hamburger', ['menu']],
173
+ ['back', ['arrowBack', 'arrowLeft']],
174
+ ['forward', ['arrowForward', 'arrowRight']],
175
+ ['eye', ['visibility']],
176
+ ['user', ['person']],
177
+ ['file', ['document']],
178
+ ['bell', ['notification']],
153
179
  ]);
154
180
 
155
181
  // Interaction examples for form components (P-04 / #206)
@@ -466,6 +492,42 @@ export function buildTokenRelationshipIndex(designTokensData) {
466
492
  return { byToken };
467
493
  }
468
494
 
495
+ /**
496
+ * Extract which components reference which design tokens via var() in CEM cssProperties.
497
+ * Returns Map<tokenName, Set<componentTagName>>.
498
+ * DD-25: var(--token, fallback) fallback values are not extracted (known limitation).
499
+ */
500
+ export function buildComponentTokenReferencedBy(manifest) {
501
+ const result = new Map();
502
+ const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
503
+ const varRe = /var\((--[\w-]+)/g;
504
+ for (const mod of modules) {
505
+ const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
506
+ for (const decl of declarations) {
507
+ const tag = decl?.tagName;
508
+ if (typeof tag !== 'string') continue;
509
+ const cssProps = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
510
+ for (const prop of cssProps) {
511
+ const defaultVal = typeof prop?.default === 'string' ? prop.default : '';
512
+ let m;
513
+ while ((m = varRe.exec(defaultVal)) !== null) {
514
+ const tokenName = normalizeTokenIdentifier(m[1]);
515
+ if (!tokenName) continue;
516
+ if (!result.has(tokenName)) result.set(tokenName, new Set());
517
+ result.get(tokenName).add(tag);
518
+ }
519
+ // Also index the css property name itself as a token → component mapping
520
+ const propName = normalizeTokenIdentifier(prop?.name);
521
+ if (propName) {
522
+ if (!result.has(propName)) result.set(propName, new Set());
523
+ result.get(propName).add(tag);
524
+ }
525
+ }
526
+ }
527
+ }
528
+ return result;
529
+ }
530
+
469
531
  function toTokenSummary(token) {
470
532
  return {
471
533
  name: String(token?.name ?? ''),
@@ -895,6 +957,14 @@ export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
895
957
  return 'Use role="alert" only for urgent live updates; otherwise use static text associated via aria-describedby.';
896
958
  }
897
959
 
960
+ if (code === 'emptyLabel') {
961
+ return diagnostic?.hint ?? 'Provide a meaningful label value for accessibility.';
962
+ }
963
+
964
+ if (code === 'emptyAriaLabel') {
965
+ return diagnostic?.hint ?? 'Provide a meaningful aria-label value or use a visible <label> element.';
966
+ }
967
+
898
968
  return undefined;
899
969
  }
900
970
 
@@ -934,6 +1004,74 @@ export function buildIndexes(manifest) {
934
1004
  return { byTag, byClass, modulePathByTag, decls };
935
1005
  }
936
1006
 
1007
+ /**
1008
+ * Extracts the primary component prefix from CEM indexes.
1009
+ * Returns the most common prefix among all tagNames (e.g. 'dads' from 'dads-button').
1010
+ * Falls back to CANONICAL_PREFIX if no tagNames are found.
1011
+ */
1012
+ export function extractPrefixFromIndexes(indexes) {
1013
+ const counts = new Map();
1014
+ for (const { tagName } of indexes.decls) {
1015
+ const i = tagName.indexOf('-');
1016
+ if (i > 0) {
1017
+ const p = tagName.slice(0, i);
1018
+ counts.set(p, (counts.get(p) ?? 0) + 1);
1019
+ }
1020
+ }
1021
+ let best = CANONICAL_PREFIX;
1022
+ let bestCount = 0;
1023
+ for (const [p, c] of counts) {
1024
+ if (c > bestCount) { best = p; bestCount = c; }
1025
+ }
1026
+ return best;
1027
+ }
1028
+
1029
+ /**
1030
+ * Build a full HTML page from a fragment.
1031
+ * @param {{ html: string; prefix: string; cemIndex: Map }} params
1032
+ */
1033
+ export function buildFullPageHtml({ html, prefix, cemIndex }) {
1034
+ // Extract custom element tags from the HTML fragment
1035
+ const tagRe = /<([a-z][a-z0-9]*-[a-z0-9-]*)\b/gi;
1036
+ const tags = new Set();
1037
+ let m;
1038
+ while ((m = tagRe.exec(html))) {
1039
+ tags.add(String(m[1]).toLowerCase());
1040
+ }
1041
+
1042
+ // Build import map entries for recognized components
1043
+ const importEntries = {};
1044
+ for (const tag of tags) {
1045
+ if (cemIndex.has(tag)) {
1046
+ const suffix = tag.replace(/^[^-]+-/, '');
1047
+ importEntries[tag] = `./<dir>/components/${suffix}.js`;
1048
+ }
1049
+ }
1050
+
1051
+ const importMapJson = JSON.stringify({ imports: importEntries }, null, 2);
1052
+
1053
+ const lines = [
1054
+ '<!DOCTYPE html>',
1055
+ `<html lang="ja">`,
1056
+ '<head>',
1057
+ ' <meta charset="utf-8">',
1058
+ ' <meta name="viewport" content="width=device-width, initial-scale=1">',
1059
+ ` <title>WCF Preview</title>`,
1060
+ ` <link rel="stylesheet" href="./<dir>/styles/tokens.css">`,
1061
+ ` <script type="importmap">`,
1062
+ importMapJson,
1063
+ ` </script>`,
1064
+ '</head>',
1065
+ '<body>',
1066
+ html,
1067
+ ` <script type="module" src="./<dir>/boot.js"></script>`,
1068
+ '</body>',
1069
+ '</html>',
1070
+ ];
1071
+
1072
+ return { fullHtml: lines.join('\n'), importEntries };
1073
+ }
1074
+
937
1075
  export function pickDecl({ byTag, byClass }, { tagName, className, prefix }) {
938
1076
  if (typeof tagName === 'string' && tagName.trim() !== '') {
939
1077
  const canonical = toCanonicalTagName(tagName, prefix);
@@ -966,6 +1104,7 @@ export function serializeApi(decl, modulePath, prefix) {
966
1104
  attributes: attributes.map((a) => ({
967
1105
  name: a?.name,
968
1106
  type: a?.type?.text,
1107
+ default: a?.default ?? null,
969
1108
  description: a?.description,
970
1109
  inheritedFrom: a?.inheritedFrom,
971
1110
  deprecated: a?.deprecated,
@@ -1032,8 +1171,12 @@ export function generateSnippet(api, prefix) {
1032
1171
  if (!a) continue;
1033
1172
  const t = String(a.type ?? '').toLowerCase();
1034
1173
  const isBoolean = t.includes('boolean');
1035
- if (isBoolean) lines.push(` ${name}`);
1036
- else lines.push(` ${name}=""`);
1174
+ if (isBoolean) {
1175
+ lines.push(` ${name}`);
1176
+ } else {
1177
+ const defaultVal = typeof a.default === 'string' ? a.default.replace(/^['"]|['"]$/g, '') : '';
1178
+ lines.push(` ${name}="${defaultVal}"`);
1179
+ }
1037
1180
  if (lines.length >= 4) break;
1038
1181
  }
1039
1182
 
@@ -1232,7 +1375,18 @@ export function searchIconCatalog(indexes, { query, limit, offset, prefix } = {}
1232
1375
 
1233
1376
  let icons = buildIconCatalog(indexes, prefix);
1234
1377
  if (q) {
1235
- icons = icons.filter((icon) => icon.name.toLowerCase().includes(q));
1378
+ // Expand query with icon aliases (DD-18)
1379
+ const searchTerms = [q];
1380
+ const aliases = ICON_ALIAS_TABLE.get(q);
1381
+ if (aliases) {
1382
+ for (const alias of aliases) {
1383
+ if (!searchTerms.includes(alias)) searchTerms.push(alias);
1384
+ }
1385
+ }
1386
+ icons = icons.filter((icon) => {
1387
+ const name = icon.name.toLowerCase();
1388
+ return searchTerms.some((term) => name.includes(term));
1389
+ });
1236
1390
  }
1237
1391
 
1238
1392
  const total = icons.length;
@@ -1708,6 +1862,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1708
1862
 
1709
1863
  const manifest = await loadJson('custom-elements.json');
1710
1864
  const indexes = buildIndexes(manifest);
1865
+ const detectedPrefix = extractPrefixFromIndexes(indexes);
1711
1866
  const {
1712
1867
  collectCemCustomElements,
1713
1868
  validateTextAgainstCem,
@@ -1754,13 +1909,14 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1754
1909
  }
1755
1910
 
1756
1911
  const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
1912
+ const componentTokenRefMap = buildComponentTokenReferencedBy(manifest);
1757
1913
 
1758
1914
  const VENDOR_DIR = 'vendor-runtime';
1759
1915
  const PREFIX_STRIP_RE = /^[^-]+-/;
1760
1916
 
1761
1917
  const server = new McpServer({
1762
1918
  name: 'web-components-factory-design-system',
1763
- version: '0.5.0',
1919
+ version: '0.6.0',
1764
1920
  });
1765
1921
 
1766
1922
  server.registerPrompt(
@@ -1910,8 +2066,8 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1910
2066
 
1911
2067
  const overview = {
1912
2068
  name: 'DADS Web Components (wcf)',
1913
- version: '0.5.0',
1914
- prefix: CANONICAL_PREFIX,
2069
+ version: '0.6.0',
2070
+ prefix: detectedPrefix,
1915
2071
  totalComponents: indexes.decls.length,
1916
2072
  componentsByCategory: categoryCount,
1917
2073
  totalPatterns: patternList.length,
@@ -1919,8 +2075,13 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1919
2075
  setupInfo: {
1920
2076
  npmPackage: 'web-components-factory',
1921
2077
  installCommand: 'npm install web-components-factory',
1922
- vendorRuntimePath: `${VENDOR_DIR}/`,
1923
- htmlBoilerplate: '<script type="module" src="vendor-runtime/src/autoload.js"></script>',
2078
+ vendorRuntimePath: '<dir>/',
2079
+ htmlBoilerplate: [
2080
+ '<script type="importmap">',
2081
+ `{ "imports": { "${detectedPrefix}-button": "./<dir>/components/button.js" } }`,
2082
+ '</script>',
2083
+ '<script type="module" src="./<dir>/boot.js"></script>',
2084
+ ].join('\n'),
1924
2085
  noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
1925
2086
  noCDN: true,
1926
2087
  deliveryModel: 'vendor-local',
@@ -1932,19 +2093,20 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1932
2093
  description:
1933
2094
  'Components are installed locally via the wcf CLI. No CDN is available. All assets are served from the project directory using import maps and a boot script.',
1934
2095
  },
1935
- importMapHint: 'WCF uses <script type="importmap"> for module resolution. Each component tag name maps to a local JS file: { "<prefix>-<component>": "./<dir>/components/<component>.js" }. The wcf CLI generates importmap.snippet.json automatically via `wcf init`.',
2096
+ importMapHint: `WCF uses <script type="importmap"> for module resolution. Each component tag name maps to a local JS file: { "${detectedPrefix}-<component>": "./<dir>/components/<component>.js" }. The wcf CLI generates importmap.snippet.json automatically via \`wcf init\`.`,
1936
2097
  bootScript: '<dir>/boot.js — sets the component prefix via setConfig(), then loads wc-autoloader.js which scans the DOM for custom element tags and dynamically imports them via the import map.',
2098
+ detectedPrefix,
1937
2099
  vendorSetup: {
1938
- init: 'wcf init --prefix <prefix> --dir <dir>',
1939
- add: 'wcf add <componentId> --prefix <prefix> --out <dir>',
2100
+ init: `wcf init --prefix ${detectedPrefix} --dir <dir>`,
2101
+ add: `wcf add <componentId> --prefix ${detectedPrefix} --out <dir>`,
1940
2102
  workflow: '1. wcf init で初期化(boot.js, importmap.snippet.json, autoloader を生成) → 2. wcf add で各コンポーネントを追加 → import map と boot.js が自動生成される',
1941
2103
  },
1942
2104
  htmlSetup: [
1943
2105
  '<script type="importmap">',
1944
2106
  '{',
1945
2107
  ' "imports": {',
1946
- ' "<prefix>-button": "./<dir>/components/button.js",',
1947
- ' "<prefix>-card": "./<dir>/components/card.js"',
2108
+ ` "${detectedPrefix}-button": "./<dir>/components/button.js",`,
2109
+ ` "${detectedPrefix}-card": "./<dir>/components/card.js"`,
1948
2110
  ' }',
1949
2111
  '}',
1950
2112
  '</script>',
@@ -1984,6 +2146,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1984
2146
  { name: 'generate_usage_snippet', purpose: 'Minimal HTML usage example' },
1985
2147
  { name: 'get_install_recipe', purpose: 'Installation instructions and dependency tree' },
1986
2148
  { name: 'validate_markup', purpose: 'Validate HTML against CEM schema' },
2149
+ { name: 'generate_full_page_html', purpose: 'Wrap HTML fragment into a complete page with importmap and boot script' },
1987
2150
  { name: 'list_patterns', purpose: 'Browse page-level UI composition patterns' },
1988
2151
  { name: 'get_pattern_recipe', purpose: 'Full pattern recipe with dependencies and HTML' },
1989
2152
  { name: 'generate_pattern_snippet', purpose: 'Pattern HTML snippet only' },
@@ -1991,6 +2154,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1991
2154
  { name: 'get_design_token_detail', purpose: 'Get details, relationships, and usage examples for one token' },
1992
2155
  { name: 'get_accessibility_docs', purpose: 'Search component-level accessibility checklist and WCAG-filtered guidance' },
1993
2156
  { name: 'search_guidelines', purpose: 'Search design system guidelines and best practices' },
2157
+ { name: 'get_component_selector_guide', purpose: 'Component selection guide by category and use case' },
1994
2158
  ],
1995
2159
  recommendedWorkflow: [
1996
2160
  '1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
@@ -2005,7 +2169,8 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2005
2169
  '10. get_component_api → check attributes, slots, events, CSS parts',
2006
2170
  '11. generate_usage_snippet or get_pattern_recipe → get code',
2007
2171
  '12. validate_markup → verify your HTML and use suggestions to self-correct',
2008
- '13. get_install_recipeget import/install instructions',
2172
+ '13. generate_full_page_htmlwrap fragment into a complete preview-ready page',
2173
+ '14. get_install_recipe → get import/install instructions',
2009
2174
  ],
2010
2175
  experimental: {
2011
2176
  plugins: {
@@ -2104,16 +2269,53 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2104
2269
  'get_component_api',
2105
2270
  {
2106
2271
  description:
2107
- 'Get the full API surface of a single component (attributes, slots, events, CSS parts, CSS custom properties). When: you need detailed specs for a component. Returns: complete component specification. After: use generate_usage_snippet for a code example.',
2272
+ 'Get the full API surface of one or more components (attributes, slots, events, CSS parts, CSS custom properties). When: you need detailed specs for components. Returns: complete component specification (single object or array for batch). After: use generate_usage_snippet for a code example.',
2108
2273
  inputSchema: {
2109
2274
  tagName: z.string().optional().describe('Tag name (e.g., "dads-button")'),
2110
2275
  className: z.string().optional().describe('Class name (e.g., "DadsButton")'),
2111
2276
  component: z.string().optional().describe('Any identifier: tagName, className, or bare name (e.g., "button")'),
2277
+ components: z.array(z.string()).max(10).optional().describe('Batch: array of component identifiers (max 10). When provided, component/tagName/className are ignored.'),
2112
2278
  prefix: z.string().optional(),
2113
2279
  },
2114
2280
  },
2115
- async ({ tagName, className, component, prefix }) => {
2281
+ async ({ tagName, className, component, components, prefix }) => {
2116
2282
  const p = normalizePrefix(prefix);
2283
+
2284
+ // Batch mode: components array takes priority (DD-23)
2285
+ if (Array.isArray(components) && components.length > 0) {
2286
+ const results = [];
2287
+ for (const comp of components) {
2288
+ const resolved = resolveDeclByComponent(indexes, comp, p);
2289
+ if (!resolved?.decl) {
2290
+ results.push({ component: comp, error: `Component not found: ${comp}` });
2291
+ continue;
2292
+ }
2293
+ const { decl: d, modulePath: mp } = resolved;
2294
+ const cTag = typeof d.tagName === 'string' ? d.tagName.toLowerCase() : undefined;
2295
+ const mPath = mp ?? (cTag ? indexes.modulePathByTag.get(cTag) : undefined);
2296
+ const api = serializeApi(d, mPath, prefix);
2297
+ const related = getRelatedComponentsForTag({
2298
+ canonicalTagName: cTag,
2299
+ installRegistry,
2300
+ relatedMap: relatedComponentMap,
2301
+ prefix,
2302
+ });
2303
+ if (related.length > 0) api.relatedComponents = related;
2304
+ const a11y = extractAccessibilityChecklist(d, { prefix });
2305
+ if (a11y) api.accessibilityChecklist = a11y;
2306
+ results.push(api);
2307
+ }
2308
+ const resultJson = JSON.stringify(results, null, 2);
2309
+ if (measureToolResultBytes(resultJson) > MAX_TOOL_RESULT_BYTES) {
2310
+ return {
2311
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Batch result exceeds size limit. Reduce the number of components.' }) }],
2312
+ isError: true,
2313
+ };
2314
+ }
2315
+ return buildJsonToolResponse(results);
2316
+ }
2317
+
2318
+ // Single mode (existing behavior)
2117
2319
  let decl;
2118
2320
  let modulePath;
2119
2321
 
@@ -2345,10 +2547,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2345
2547
  severity: 'warning',
2346
2548
  });
2347
2549
 
2550
+ const cemTagNames = new Set(cemIndex.keys());
2348
2551
  const accessibilityDiagnostics = detectAccessibilityMisuseInMarkup({
2349
2552
  filePath: '<markup>',
2350
2553
  text: html,
2351
- severity: 'warning',
2554
+ severity: 'error',
2555
+ cemTagNames,
2352
2556
  });
2353
2557
 
2354
2558
  const slotDiagnostics = detectInvalidSlotName({
@@ -2432,6 +2636,102 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2432
2636
  },
2433
2637
  );
2434
2638
 
2639
+ // -----------------------------------------------------------------------
2640
+ // Tool: generate_full_page_html
2641
+ // -----------------------------------------------------------------------
2642
+ server.registerTool(
2643
+ 'generate_full_page_html',
2644
+ {
2645
+ description:
2646
+ 'Generate a complete, self-contained HTML page from a component HTML fragment. When: you need a preview-ready full page with <!DOCTYPE html>, importmap, and boot script. Returns: { fullHtml, componentCount, importMapEntries }. After: save to a .html file and open via a local HTTP server.',
2647
+ inputSchema: {
2648
+ html: z.string().describe('HTML fragment containing WCF custom elements'),
2649
+ prefix: z.string().optional().describe('Component prefix (default: auto-detected)'),
2650
+ },
2651
+ },
2652
+ async ({ html, prefix }) => {
2653
+ const p = normalizePrefix(prefix);
2654
+ let ci = canonicalCemIndex;
2655
+ if (p !== CANONICAL_PREFIX) {
2656
+ ci = mergeWithPrefixed(canonicalCemIndex, p);
2657
+ }
2658
+
2659
+ const { fullHtml, importEntries } = buildFullPageHtml({ html, prefix: p, cemIndex: ci });
2660
+
2661
+ return {
2662
+ content: [{
2663
+ type: 'text',
2664
+ text: JSON.stringify({
2665
+ fullHtml,
2666
+ componentCount: Object.keys(importEntries).length,
2667
+ importMapEntries: importEntries,
2668
+ }, null, 2),
2669
+ }],
2670
+ };
2671
+ },
2672
+ );
2673
+
2674
+ // -----------------------------------------------------------------------
2675
+ // Tool: get_component_selector_guide
2676
+ // -----------------------------------------------------------------------
2677
+ let selectorGuideData = null;
2678
+ try {
2679
+ selectorGuideData = await loadJson('component-selector-guide.json');
2680
+ } catch {
2681
+ // component-selector-guide.json may not exist yet
2682
+ }
2683
+
2684
+ server.registerTool(
2685
+ 'get_component_selector_guide',
2686
+ {
2687
+ description:
2688
+ 'Get a component selection guide organized by UI category and use case. When: deciding which component to use for a UI requirement. Returns: categories with recommended components and use cases. After: use get_component_api for the selected component details.',
2689
+ inputSchema: {
2690
+ category: z.string().optional().describe('Filter by category key (e.g., "Form", "Navigation", "Layout")'),
2691
+ useCase: z.string().optional().describe('Search by use-case keyword (e.g., "date", "login", "upload")'),
2692
+ },
2693
+ },
2694
+ async ({ category, useCase }) => {
2695
+ if (!selectorGuideData || !Array.isArray(selectorGuideData.categories)) {
2696
+ return {
2697
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Component selector guide not available.' }) }],
2698
+ isError: true,
2699
+ };
2700
+ }
2701
+
2702
+ let categories = selectorGuideData.categories;
2703
+
2704
+ // Filter by category
2705
+ if (typeof category === 'string' && category.trim()) {
2706
+ const cat = category.trim().toLowerCase();
2707
+ categories = categories.filter((c) => c.key.toLowerCase() === cat);
2708
+ }
2709
+
2710
+ // Filter by use case keyword
2711
+ if (typeof useCase === 'string' && useCase.trim()) {
2712
+ const kw = useCase.trim().toLowerCase();
2713
+ categories = categories.map((c) => ({
2714
+ ...c,
2715
+ components: c.components.filter((comp) =>
2716
+ comp.useCase.toLowerCase().includes(kw) ||
2717
+ comp.id.toLowerCase().includes(kw) ||
2718
+ comp.tagName.toLowerCase().includes(kw)
2719
+ ),
2720
+ })).filter((c) => c.components.length > 0);
2721
+ }
2722
+
2723
+ return buildJsonToolResponse({
2724
+ totalCategories: categories.length,
2725
+ categories: categories.map((c) => ({
2726
+ key: c.key,
2727
+ label: c.label,
2728
+ description: c.description,
2729
+ components: c.components,
2730
+ })),
2731
+ });
2732
+ },
2733
+ );
2734
+
2435
2735
  // -----------------------------------------------------------------------
2436
2736
  // Tool: list_patterns
2437
2737
  // -----------------------------------------------------------------------
@@ -2457,7 +2757,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2457
2757
  );
2458
2758
 
2459
2759
  // -----------------------------------------------------------------------
2460
- // Helper: buildFullPageHtml
2760
+ // Helper: buildFullPageHtmlFromImportMap
2461
2761
  // -----------------------------------------------------------------------
2462
2762
  function escapeHtmlTitle(s) {
2463
2763
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -2496,7 +2796,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2496
2796
  * @param {string} [opts.lang='ja'] - HTML lang attribute
2497
2797
  * @returns {string} Complete HTML5 document
2498
2798
  */
2499
- function buildFullPageHtml({ html, title, importMapEntries, dir = VENDOR_DIR, lang = 'ja' }) {
2799
+ function buildFullPageHtmlFromImportMap({ html, title, importMapEntries, dir = VENDOR_DIR, lang = 'ja' }) {
2500
2800
  const importMapJson = JSON.stringify({ imports: importMapEntries }, null, 2);
2501
2801
  return [
2502
2802
  '<!DOCTYPE html>',
@@ -2582,7 +2882,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2582
2882
  let fullPageHtml;
2583
2883
  if (includeArr.includes('fullPage')) {
2584
2884
  const resolvedImportMap = buildImportMapEntries(closure, components, p, VENDOR_DIR);
2585
- fullPageHtml = buildFullPageHtml({
2885
+ fullPageHtml = buildFullPageHtmlFromImportMap({
2586
2886
  html,
2587
2887
  title: pat.title ?? pat.id,
2588
2888
  importMapEntries: resolvedImportMap,
@@ -2604,6 +2904,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2604
2904
  installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
2605
2905
  entryHints,
2606
2906
  scaffoldHint,
2907
+ behavior: typeof pat.behavior === 'string' ? pat.behavior : undefined,
2607
2908
  };
2608
2909
 
2609
2910
  if (fullPageHtml !== undefined) {
@@ -2714,6 +3015,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2714
3015
  isError: true,
2715
3016
  };
2716
3017
  }
3018
+ // Enrich referencedBy with component tagNames from CEM cssProperties
3019
+ const normalizedName = normalizeTokenIdentifier(name);
3020
+ const componentRefs = componentTokenRefMap.get(normalizedName);
3021
+ if (componentRefs && componentRefs.size > 0) {
3022
+ payload.componentReferencedBy = [...componentRefs].sort();
3023
+ }
2717
3024
  return buildJsonToolResponse(payload);
2718
3025
  },
2719
3026
  );
@@ -0,0 +1,102 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "categories": [
4
+ {
5
+ "key": "Form",
6
+ "label": "フォーム入力",
7
+ "description": "User input, selection, and form control components",
8
+ "components": [
9
+ { "id": "input-text", "tagName": "dads-input-text", "useCase": "Single-line text input (name, email, phone, etc.)" },
10
+ { "id": "textarea", "tagName": "dads-textarea", "useCase": "Multi-line text input (comments, descriptions, notes)" },
11
+ { "id": "select", "tagName": "dads-select", "useCase": "Dropdown selection from predefined options" },
12
+ { "id": "checkbox", "tagName": "dads-checkbox", "useCase": "Boolean toggle or multi-select checkboxes" },
13
+ { "id": "radio", "tagName": "dads-radio", "useCase": "Single selection from mutually exclusive options" },
14
+ { "id": "switch", "tagName": "dads-switch", "useCase": "On/off toggle for settings or preferences" },
15
+ { "id": "combobox", "tagName": "dads-combobox", "useCase": "Text input with autocomplete/suggestion dropdown" },
16
+ { "id": "date-picker", "tagName": "dads-date-picker", "useCase": "Date selection with calendar interface" },
17
+ { "id": "file-upload", "tagName": "dads-file-upload", "useCase": "File attachment and upload" },
18
+ { "id": "fieldset", "tagName": "dads-fieldset", "useCase": "Group related form fields with legend" },
19
+ { "id": "search-box", "tagName": "dads-search-box", "useCase": "Search input with clear/submit actions" },
20
+ { "id": "calendar", "tagName": "dads-calendar", "useCase": "Calendar date picker component" }
21
+ ]
22
+ },
23
+ {
24
+ "key": "Actions",
25
+ "label": "アクション",
26
+ "description": "Interactive elements that trigger actions or reveal content",
27
+ "components": [
28
+ { "id": "button", "tagName": "dads-button", "useCase": "Primary action trigger (submit, save, cancel, delete)" },
29
+ { "id": "dialog", "tagName": "dads-dialog", "useCase": "Modal dialog for confirmations, alerts, or focused tasks" },
30
+ { "id": "drawer", "tagName": "dads-drawer", "useCase": "Slide-out panel for secondary content or navigation" },
31
+ { "id": "disclosure", "tagName": "dads-disclosure", "useCase": "Expandable content section (show/hide)" },
32
+ { "id": "accordion-details", "tagName": "dads-accordion-details", "useCase": "Accordion container for collapsible FAQ or grouped content" },
33
+ { "id": "accordion-item-details", "tagName": "dads-accordion-item-details", "useCase": "Individual accordion item within an accordion group" }
34
+ ]
35
+ },
36
+ {
37
+ "key": "Navigation",
38
+ "label": "ナビゲーション",
39
+ "description": "Page navigation, breadcrumbs, tabs, and menu components",
40
+ "components": [
41
+ { "id": "breadcrumb", "tagName": "dads-breadcrumb", "useCase": "Hierarchical page location indicator" },
42
+ { "id": "page-navigation", "tagName": "dads-page-navigation", "useCase": "Pagination for multi-page content (tables, search results)" },
43
+ { "id": "step-navigation", "tagName": "dads-step-navigation", "useCase": "Step-by-step wizard progress indicator" },
44
+ { "id": "menu-list", "tagName": "dads-menu-list", "useCase": "Vertical navigation menu list" },
45
+ { "id": "tab", "tagName": "dads-tab", "useCase": "Tab interface for switching between related content panels" },
46
+ { "id": "global-menu", "tagName": "dads-global-menu", "useCase": "Site-wide header navigation menu" },
47
+ { "id": "language-selector", "tagName": "dads-language-selector", "useCase": "Language/locale switcher" },
48
+ { "id": "hamburger-menu-button", "tagName": "dads-hamburger-menu-button", "useCase": "Mobile menu toggle button" },
49
+ { "id": "utility-link", "tagName": "dads-utility-link", "useCase": "Utility navigation link (login, settings, help)" },
50
+ { "id": "mobile-menu", "tagName": "dads-mobile-menu", "useCase": "Mobile-optimized navigation menu" }
51
+ ]
52
+ },
53
+ {
54
+ "key": "Content",
55
+ "label": "コンテンツ表示",
56
+ "description": "Text, list, table, and structured content display",
57
+ "components": [
58
+ { "id": "card", "tagName": "dads-card", "useCase": "Contained content block (article preview, info card)" },
59
+ { "id": "heading", "tagName": "dads-heading", "useCase": "Section heading with proper hierarchy (h1-h6)" },
60
+ { "id": "text", "tagName": "dads-text", "useCase": "Styled text block with typography tokens" },
61
+ { "id": "blockquote", "tagName": "dads-blockquote", "useCase": "Quoted text or citation block" },
62
+ { "id": "code-block", "tagName": "dads-code-block", "useCase": "Code snippet display with syntax styling" },
63
+ { "id": "divider", "tagName": "dads-divider", "useCase": "Visual separator between content sections" },
64
+ { "id": "list", "tagName": "dads-list", "useCase": "Ordered or unordered list" },
65
+ { "id": "description-list", "tagName": "dads-description-list", "useCase": "Key-value pair list (terms and definitions)" },
66
+ { "id": "resource-list", "tagName": "dads-resource-list", "useCase": "List of downloadable resources or links" },
67
+ { "id": "table", "tagName": "dads-table", "useCase": "Data table with sortable columns" },
68
+ { "id": "table-control", "tagName": "dads-table-control", "useCase": "Table toolbar with filters and actions" }
69
+ ]
70
+ },
71
+ {
72
+ "key": "Display",
73
+ "label": "表示・フィードバック",
74
+ "description": "Visual indicators, status displays, and feedback elements",
75
+ "components": [
76
+ { "id": "avatar", "tagName": "dads-avatar", "useCase": "User profile image or initials" },
77
+ { "id": "icon", "tagName": "dads-icon", "useCase": "SVG icon display" },
78
+ { "id": "chip-label", "tagName": "dads-chip-label", "useCase": "Label chip for categories or tags" },
79
+ { "id": "chip-tag", "tagName": "dads-chip-tag", "useCase": "Interactive tag chip (filterable, removable)" },
80
+ { "id": "notification-banner", "tagName": "dads-notification-banner", "useCase": "Page-level notification or info banner" },
81
+ { "id": "emergency-banner", "tagName": "dads-emergency-banner", "useCase": "Critical alert banner (system-wide)" },
82
+ { "id": "carousel", "tagName": "dads-carousel", "useCase": "Image or content carousel/slider" },
83
+ { "id": "device-mock", "tagName": "dads-device-mock", "useCase": "Device frame mockup for previews" },
84
+ { "id": "progress-indicator", "tagName": "dads-progress-indicator", "useCase": "Progress display (determinate or indeterminate)" },
85
+ { "id": "spinner", "tagName": "dads-spinner", "useCase": "Loading spinner for async operations" },
86
+ { "id": "progress-bar", "tagName": "dads-progress-bar", "useCase": "Linear progress bar" },
87
+ { "id": "loading-icon", "tagName": "dads-loading-icon", "useCase": "Inline loading indicator" }
88
+ ]
89
+ },
90
+ {
91
+ "key": "Layout",
92
+ "label": "レイアウト",
93
+ "description": "Page layout containers and structural components",
94
+ "components": [
95
+ { "id": "layout-shell", "tagName": "dads-layout-shell", "useCase": "Page-level layout container (website, app-shell, master-detail)" },
96
+ { "id": "layout-sidebar", "tagName": "dads-layout-sidebar", "useCase": "Sidebar navigation panel for app layouts" },
97
+ { "id": "layout-aside", "tagName": "dads-layout-aside", "useCase": "Aside panel for detail/supplementary content" },
98
+ { "id": "header-container", "tagName": "dads-header-container", "useCase": "Header area container with slots" }
99
+ ]
100
+ }
101
+ ]
102
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.2.0",
3
- "extractedAt": "2026-03-02T05:22:54.308Z",
3
+ "extractedAt": "2026-03-03T05:48:06.856Z",
4
4
  "themes": {
5
5
  "default": "light",
6
6
  "available": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.1.0",
3
- "indexedAt": "2026-03-02T05:22:54.736Z",
3
+ "indexedAt": "2026-03-03T05:48:07.276Z",
4
4
  "documents": [
5
5
  {
6
6
  "id": ".claude/skills/css-writing-rules/references/core-principles.md",
@@ -6,121 +6,240 @@
6
6
  "id": "search-form",
7
7
  "title": "検索フォーム(最小)",
8
8
  "description": "見出し + 検索フォーム(検索語 + ボタン)",
9
- "requires": ["heading", "search-box", "button"],
9
+ "requires": [
10
+ "heading",
11
+ "search-box",
12
+ "button"
13
+ ],
10
14
  "stability": "stable",
11
15
  "contractVersion": "1.0",
12
- "entryHints": ["boot", "@wcf", "index"],
13
- "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">検索</dads-heading>\n <form id=\"search-form\">\n <dads-search-box aria-label=\"検索\"></dads-search-box>\n <dads-button type=\"submit\">検索</dads-button>\n </form>\n</main>\n"
16
+ "entryHints": [
17
+ "boot",
18
+ "@wcf",
19
+ "index"
20
+ ],
21
+ "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">検索</dads-heading>\n <form id=\"search-form\">\n <dads-search-box aria-label=\"検索\"></dads-search-box>\n <dads-button type=\"submit\">検索</dads-button>\n </form>\n</main>\n",
22
+ "behavior": "document.querySelector(\"#search-form\").addEventListener(\"submit\", (e) => {\n e.preventDefault();\n const query = e.target.querySelector(\"dads-search-box\").value;\n console.log(\"Search:\", query);\n});"
14
23
  },
15
24
  "search-results": {
16
25
  "id": "search-results",
17
26
  "title": "検索結果一覧",
18
27
  "description": "見出し + 検索フォーム + 結果カード + ページネーション",
19
- "requires": ["heading", "search-box", "card", "page-navigation"],
28
+ "requires": [
29
+ "heading",
30
+ "search-box",
31
+ "card",
32
+ "page-navigation"
33
+ ],
20
34
  "stability": "stable",
21
35
  "contractVersion": "1.0",
22
- "entryHints": ["boot", "@wcf", "index"],
23
- "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">検索</dads-heading>\n <form id=\"search-form\">\n <dads-search-box aria-label=\"検索\"></dads-search-box>\n </form>\n <h2>結果</h2>\n <ul>\n <li><dads-card>ダミー結果 1</dads-card></li>\n <li><dads-card>ダミー結果 2</dads-card></li>\n <li><dads-card>ダミー結果 3</dads-card></li>\n </ul>\n <dads-page-navigation current=\"1\" total=\"1\"></dads-page-navigation>\n</main>\n"
36
+ "entryHints": [
37
+ "boot",
38
+ "@wcf",
39
+ "index"
40
+ ],
41
+ "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">検索</dads-heading>\n <form id=\"search-form\">\n <dads-search-box aria-label=\"検索\"></dads-search-box>\n </form>\n <h2>結果</h2>\n <ul>\n <li><dads-card>ダミー結果 1</dads-card></li>\n <li><dads-card>ダミー結果 2</dads-card></li>\n <li><dads-card>ダミー結果 3</dads-card></li>\n </ul>\n <dads-page-navigation current=\"1\" total=\"1\"></dads-page-navigation>\n</main>\n",
42
+ "behavior": "document.querySelector(\"#search-form\").addEventListener(\"submit\", (e) => {\n e.preventDefault();\n const query = e.target.querySelector(\"dads-search-box\").value;\n // Fetch results and update the list\n console.log(\"Search:\", query);\n});"
24
43
  },
25
44
  "table-with-pagination": {
26
45
  "id": "table-with-pagination",
27
46
  "title": "テーブル + ページネーション",
28
47
  "description": "テーブル一覧とページネーションの基本構成",
29
- "requires": ["heading", "table", "page-navigation"],
48
+ "requires": [
49
+ "heading",
50
+ "table",
51
+ "page-navigation"
52
+ ],
30
53
  "stability": "stable",
31
54
  "contractVersion": "1.0",
32
- "entryHints": ["boot", "@wcf", "index"],
33
- "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">一覧</dads-heading>\n <dads-table>\n <table>\n <thead>\n <tr><th>項目</th><th>値</th></tr>\n </thead>\n <tbody>\n <tr><td>サンプル</td><td>1</td></tr>\n </tbody>\n </table>\n </dads-table>\n <dads-page-navigation current=\"1\" total=\"3\"></dads-page-navigation>\n</main>\n"
55
+ "entryHints": [
56
+ "boot",
57
+ "@wcf",
58
+ "index"
59
+ ],
60
+ "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">一覧</dads-heading>\n <dads-table>\n <table>\n <thead>\n <tr><th>項目</th><th>値</th></tr>\n </thead>\n <tbody>\n <tr><td>サンプル</td><td>1</td></tr>\n </tbody>\n </table>\n </dads-table>\n <dads-page-navigation current=\"1\" total=\"3\"></dads-page-navigation>\n</main>\n",
61
+ "behavior": "document.querySelector(\"dads-page-navigation\").addEventListener(\"page-change\", (e) => {\n console.log(\"Page:\", e.detail.page);\n // Fetch and render table rows for the new page\n});"
34
62
  },
35
63
  "card-grid": {
36
64
  "id": "card-grid",
37
65
  "title": "カードグリッド",
38
66
  "description": "カードで一覧表示する基本レイアウト",
39
- "requires": ["heading", "card", "button"],
67
+ "requires": [
68
+ "heading",
69
+ "card",
70
+ "button"
71
+ ],
40
72
  "stability": "stable",
41
73
  "contractVersion": "1.0",
42
- "entryHints": ["boot", "@wcf", "index"],
43
- "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">お知らせ</dads-heading>\n <section>\n <dads-card>\n <h2>カード1</h2>\n <dads-button variant=\"outlined\">詳細</dads-button>\n </dads-card>\n <dads-card>\n <h2>カード2</h2>\n <dads-button variant=\"outlined\">詳細</dads-button>\n </dads-card>\n </section>\n</main>\n"
74
+ "entryHints": [
75
+ "boot",
76
+ "@wcf",
77
+ "index"
78
+ ],
79
+ "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">お知らせ</dads-heading>\n <section>\n <dads-card>\n <h2>カード1</h2>\n <dads-button variant=\"outlined\">詳細</dads-button>\n </dads-card>\n <dads-card>\n <h2>カード2</h2>\n <dads-button variant=\"outlined\">詳細</dads-button>\n </dads-card>\n </section>\n</main>\n",
80
+ "behavior": "document.querySelectorAll(\"dads-button\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n console.log(\"Card detail clicked\");\n });\n});"
44
81
  },
45
82
  "layout-website-hero-section-footer": {
46
83
  "id": "layout-website-hero-section-footer",
47
84
  "title": "レイアウト(Website: Hero + Section + Footer)",
48
85
  "description": "コンテンツ主導の1カラムサイト向けレイアウト。",
49
- "requires": ["layout-shell", "heading", "card", "button"],
86
+ "requires": [
87
+ "layout-shell",
88
+ "heading",
89
+ "card",
90
+ "button"
91
+ ],
50
92
  "stability": "stable",
51
93
  "contractVersion": "1.0",
52
- "entryHints": ["boot", "@wcf", "index"],
53
- "html": "<dads-layout-shell data-dads-typeset pattern=\"website\" mode=\"auto\">\n <header slot=\"header\">\n <dads-heading level=\"1\">くらしの手続きポータル</dads-heading>\n <p>必要な手続きを1つの画面で確認できます。</p>\n </header>\n <section>\n <dads-card>\n <dads-heading level=\"2\">はじめての方へ</dads-heading>\n <p>制度の概要と申請までの流れを案内します。</p>\n <dads-button variant=\"outlined\">詳しく見る</dads-button>\n </dads-card>\n </section>\n <footer slot=\"footer\">© Digital Service</footer>\n</dads-layout-shell>\n"
94
+ "entryHints": [
95
+ "boot",
96
+ "@wcf",
97
+ "index"
98
+ ],
99
+ "html": "<dads-layout-shell data-dads-typeset pattern=\"website\" mode=\"auto\">\n <header slot=\"header\">\n <dads-heading level=\"1\">くらしの手続きポータル</dads-heading>\n <p>必要な手続きを1つの画面で確認できます。</p>\n </header>\n <section>\n <dads-card>\n <dads-heading level=\"2\">はじめての方へ</dads-heading>\n <p>制度の概要と申請までの流れを案内します。</p>\n <dads-button variant=\"outlined\">詳しく見る</dads-button>\n </dads-card>\n </section>\n <footer slot=\"footer\">© Digital Service</footer>\n</dads-layout-shell>\n",
100
+ "behavior": "// Responsive: layout-shell handles mode automatically via mode=\"auto\""
54
101
  },
55
102
  "layout-app-shell": {
56
103
  "id": "layout-app-shell",
57
104
  "title": "レイアウト(App/SaaS: Header + Sidebar + Main)",
58
105
  "description": "業務アプリ向けの標準App Shellレイアウト。",
59
- "requires": ["layout-shell", "layout-sidebar", "heading", "card"],
106
+ "requires": [
107
+ "layout-shell",
108
+ "layout-sidebar",
109
+ "heading",
110
+ "card"
111
+ ],
60
112
  "stability": "stable",
61
113
  "contractVersion": "1.0",
62
- "entryHints": ["boot", "@wcf", "index"],
63
- "html": "<dads-layout-shell data-dads-typeset pattern=\"app-shell\" mode=\"auto\">\n <div slot=\"header\">\n <dads-heading level=\"2\">業務ダッシュボード</dads-heading>\n </div>\n <dads-layout-sidebar slot=\"sidebar\">\n <ul>\n <li>案件一覧</li>\n <li>承認待ち</li>\n <li>設定</li>\n </ul>\n </dads-layout-sidebar>\n <section>\n <dads-card>\n <dads-heading level=\"3\">進捗サマリー</dads-heading>\n <p>主要KPIを表示します。</p>\n </dads-card>\n </section>\n</dads-layout-shell>\n"
114
+ "entryHints": [
115
+ "boot",
116
+ "@wcf",
117
+ "index"
118
+ ],
119
+ "html": "<dads-layout-shell data-dads-typeset pattern=\"app-shell\" mode=\"auto\">\n <div slot=\"header\">\n <dads-heading level=\"2\">業務ダッシュボード</dads-heading>\n </div>\n <dads-layout-sidebar slot=\"sidebar\">\n <ul>\n <li>案件一覧</li>\n <li>承認待ち</li>\n <li>設定</li>\n </ul>\n </dads-layout-sidebar>\n <section>\n <dads-card>\n <dads-heading level=\"3\">進捗サマリー</dads-heading>\n <p>主要KPIを表示します。</p>\n </dads-card>\n </section>\n</dads-layout-shell>\n",
120
+ "behavior": "// layout-shell + layout-sidebar handle responsive behavior via mode=\"auto\""
64
121
  },
65
122
  "layout-master-detail": {
66
123
  "id": "layout-master-detail",
67
124
  "title": "レイアウト(Master-Detail: Main + Aside)",
68
125
  "description": "一覧 + 詳細を同時表示する2カラムレイアウト。",
69
- "requires": ["layout-shell", "layout-aside", "heading", "table"],
126
+ "requires": [
127
+ "layout-shell",
128
+ "layout-aside",
129
+ "heading",
130
+ "table"
131
+ ],
70
132
  "stability": "stable",
71
133
  "contractVersion": "1.0",
72
- "entryHints": ["boot", "@wcf", "index"],
73
- "html": "<dads-layout-shell data-dads-typeset pattern=\"master-detail\" mode=\"auto\">\n <section>\n <dads-heading level=\"2\">申請一覧</dads-heading>\n <dads-table>\n <table>\n <thead>\n <tr><th scope=\"col\">申請ID</th><th scope=\"col\">状態</th></tr>\n </thead>\n <tbody>\n <tr><td>A-1001</td><td>審査中</td></tr>\n <tr><td>A-1002</td><td>差戻し</td></tr>\n </tbody>\n </table>\n </dads-table>\n </section>\n <dads-layout-aside slot=\"aside\">\n <dads-heading level=\"3\">詳細情報</dads-heading>\n <p>選択中レコードの詳細を表示します。</p>\n </dads-layout-aside>\n</dads-layout-shell>\n"
134
+ "entryHints": [
135
+ "boot",
136
+ "@wcf",
137
+ "index"
138
+ ],
139
+ "html": "<dads-layout-shell data-dads-typeset pattern=\"master-detail\" mode=\"auto\">\n <section>\n <dads-heading level=\"2\">申請一覧</dads-heading>\n <dads-table>\n <table>\n <thead>\n <tr><th scope=\"col\">申請ID</th><th scope=\"col\">状態</th></tr>\n </thead>\n <tbody>\n <tr><td>A-1001</td><td>審査中</td></tr>\n <tr><td>A-1002</td><td>差戻し</td></tr>\n </tbody>\n </table>\n </dads-table>\n </section>\n <dads-layout-aside slot=\"aside\">\n <dads-heading level=\"3\">詳細情報</dads-heading>\n <p>選択中レコードの詳細を表示します。</p>\n </dads-layout-aside>\n</dads-layout-shell>\n",
140
+ "behavior": "// Master row click updates aside detail\ndocument.querySelector(\"dads-table\").addEventListener(\"row-select\", (e) => {\n const aside = document.querySelector(\"dads-layout-aside\");\n aside.textContent = \"Selected: \" + e.detail.id;\n});"
74
141
  },
75
142
  "application-form-single-validation": {
76
143
  "id": "application-form-single-validation",
77
144
  "title": "申請フォーム(1ページ・検証エラー)",
78
145
  "description": "必須項目を含む1ページ申請フォームとバリデーションエラー表示",
79
- "requires": ["heading", "fieldset", "input-text", "select", "textarea", "button"],
146
+ "requires": [
147
+ "heading",
148
+ "fieldset",
149
+ "input-text",
150
+ "select",
151
+ "textarea",
152
+ "button"
153
+ ],
80
154
  "stability": "experimental",
81
155
  "contractVersion": "1.0",
82
- "entryHints": ["boot", "@wcf", "index"],
83
- "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">申請フォーム</dads-heading>\n <form id=\"application-form-single\">\n <dads-fieldset>\n <legend>申請情報</legend>\n <dads-input-text name=\"name\" required error error-text=\"氏名は必須です\"></dads-input-text>\n <dads-select name=\"type\" required></dads-select>\n <dads-textarea name=\"reason\" required></dads-textarea>\n </dads-fieldset>\n <dads-button type=\"submit\">送信</dads-button>\n </form>\n</main>\n"
156
+ "entryHints": [
157
+ "boot",
158
+ "@wcf",
159
+ "index"
160
+ ],
161
+ "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">申請フォーム</dads-heading>\n <form id=\"application-form-single\">\n <dads-fieldset>\n <legend>申請情報</legend>\n <dads-input-text label=\"氏名\" name=\"name\" required error error-text=\"氏名は必須です\"></dads-input-text>\n <dads-select label=\"種別\" name=\"type\" required></dads-select>\n <dads-textarea label=\"理由\" name=\"reason\" required></dads-textarea>\n </dads-fieldset>\n <dads-button type=\"submit\">送信</dads-button>\n </form>\n</main>\n",
162
+ "behavior": "document.querySelector(\"#application-form-single\").addEventListener(\"submit\", (e) => {\n e.preventDefault();\n const form = e.target;\n let valid = true;\n form.querySelectorAll(\"[required]\").forEach((el) => {\n if (!el.value) { el.setAttribute(\"error\", \"\"); valid = false; }\n else { el.removeAttribute(\"error\"); }\n });\n if (valid) console.log(\"Submit OK\");\n});"
84
163
  },
85
164
  "application-form-step-validation": {
86
165
  "id": "application-form-step-validation",
87
166
  "title": "申請フォーム(ステップ・検証エラー)",
88
167
  "description": "ステップナビゲーション付き申請フォームと検証エラー表示",
89
- "requires": ["heading", "step-navigation", "fieldset", "input-text", "button"],
168
+ "requires": [
169
+ "heading",
170
+ "step-navigation",
171
+ "fieldset",
172
+ "input-text",
173
+ "button"
174
+ ],
90
175
  "stability": "experimental",
91
176
  "contractVersion": "1.0",
92
- "entryHints": ["boot", "@wcf", "index"],
93
- "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">申請フォーム(ステップ)</dads-heading>\n <dads-step-navigation></dads-step-navigation>\n <form id=\"application-form-step\">\n <dads-fieldset>\n <legend>ステップ1: 申請者情報</legend>\n <dads-input-text name=\"name\" required error error-text=\"氏名は必須です\"></dads-input-text>\n </dads-fieldset>\n <dads-button type=\"submit\">次へ</dads-button>\n </form>\n</main>\n"
177
+ "entryHints": [
178
+ "boot",
179
+ "@wcf",
180
+ "index"
181
+ ],
182
+ "html": "<main data-dads-typeset>\n <dads-heading level=\"1\">申請フォーム(ステップ)</dads-heading>\n <dads-step-navigation></dads-step-navigation>\n <form id=\"application-form-step\">\n <dads-fieldset>\n <legend>ステップ1: 申請者情報</legend>\n <dads-input-text label=\"氏名\" name=\"name\" required error error-text=\"氏名は必須です\"></dads-input-text>\n </dads-fieldset>\n <dads-button type=\"submit\">次へ</dads-button>\n </form>\n</main>\n",
183
+ "behavior": "let step = 0;\ndocument.querySelector(\"#application-form-step\").addEventListener(\"submit\", (e) => {\n e.preventDefault();\n // Validate current step, advance step++\n console.log(\"Step\", step, \"validated\");\n});"
94
184
  },
95
185
  "mockup-website": {
96
186
  "id": "mockup-website",
97
187
  "title": "モックアップ(Website)",
98
188
  "description": "Webサイト向けのヒーロー + セクション構成を device-mock で確認するモックアップ。",
99
- "requires": ["device-mock", "layout-shell", "heading", "card", "button"],
189
+ "requires": [
190
+ "device-mock",
191
+ "layout-shell",
192
+ "heading",
193
+ "card",
194
+ "button"
195
+ ],
100
196
  "stability": "stable",
101
197
  "contractVersion": "1.0",
102
- "entryHints": ["boot"],
103
- "html": "<section data-dads-typeset>\n <dads-device-mock device=\"desktop\">\n <dads-layout-shell pattern=\"website\" mode=\"desktop\">\n <header slot=\"header\">\n <dads-heading level=\"1\">公共サービス ポータル</dads-heading>\n <p>申請・確認・問い合わせを1つの画面で行えます。</p>\n </header>\n <section>\n <dads-card>\n <dads-heading level=\"2\">新着のお知らせ</dads-heading>\n <p>重要なお知らせを確認してください。</p>\n <dads-button variant=\"outlined\">詳細</dads-button>\n </dads-card>\n </section>\n <footer slot=\"footer\">© Digital Service</footer>\n </dads-layout-shell>\n </dads-device-mock>\n</section>\n"
198
+ "entryHints": [
199
+ "boot"
200
+ ],
201
+ "html": "<section data-dads-typeset>\n <dads-device-mock device=\"desktop\">\n <dads-layout-shell pattern=\"website\" mode=\"desktop\">\n <header slot=\"header\">\n <dads-heading level=\"1\">公共サービス ポータル</dads-heading>\n <p>申請・確認・問い合わせを1つの画面で行えます。</p>\n </header>\n <section>\n <dads-card>\n <dads-heading level=\"2\">新着のお知らせ</dads-heading>\n <p>重要なお知らせを確認してください。</p>\n <dads-button variant=\"outlined\">詳細</dads-button>\n </dads-card>\n </section>\n <footer slot=\"footer\">© Digital Service</footer>\n </dads-layout-shell>\n </dads-device-mock>\n</section>\n",
202
+ "behavior": "// Mockups are visual-only; no runtime behavior needed."
104
203
  },
105
204
  "mockup-app-shell": {
106
205
  "id": "mockup-app-shell",
107
206
  "title": "モックアップ(App Shell)",
108
207
  "description": "業務画面向けのヘッダー + サイドバー + メイン領域を device-mock で再現するモックアップ。",
109
- "requires": ["device-mock", "layout-shell", "layout-sidebar", "heading", "card"],
208
+ "requires": [
209
+ "device-mock",
210
+ "layout-shell",
211
+ "layout-sidebar",
212
+ "heading",
213
+ "card"
214
+ ],
110
215
  "stability": "stable",
111
216
  "contractVersion": "1.0",
112
- "entryHints": ["boot"],
113
- "html": "<section data-dads-typeset>\n <dads-device-mock device=\"desktop\">\n <dads-layout-shell pattern=\"app-shell\" mode=\"desktop\">\n <div slot=\"header\">\n <dads-heading level=\"2\">申請管理ダッシュボード</dads-heading>\n </div>\n <dads-layout-sidebar slot=\"sidebar\">\n <ul>\n <li>一覧</li>\n <li>承認待ち</li>\n <li>設定</li>\n </ul>\n </dads-layout-sidebar>\n <section>\n <dads-card>\n <dads-heading level=\"3\">本日の処理件数</dads-heading>\n <p>処理済み 128 件 / 未処理 24 件</p>\n </dads-card>\n </section>\n </dads-layout-shell>\n </dads-device-mock>\n</section>\n"
217
+ "entryHints": [
218
+ "boot"
219
+ ],
220
+ "html": "<section data-dads-typeset>\n <dads-device-mock device=\"desktop\">\n <dads-layout-shell pattern=\"app-shell\" mode=\"desktop\">\n <div slot=\"header\">\n <dads-heading level=\"2\">申請管理ダッシュボード</dads-heading>\n </div>\n <dads-layout-sidebar slot=\"sidebar\">\n <ul>\n <li>一覧</li>\n <li>承認待ち</li>\n <li>設定</li>\n </ul>\n </dads-layout-sidebar>\n <section>\n <dads-card>\n <dads-heading level=\"3\">本日の処理件数</dads-heading>\n <p>処理済み 128 件 / 未処理 24 件</p>\n </dads-card>\n </section>\n </dads-layout-shell>\n </dads-device-mock>\n</section>\n",
221
+ "behavior": "// Mockups are visual-only; no runtime behavior needed."
114
222
  },
115
223
  "mockup-mobile-form": {
116
224
  "id": "mockup-mobile-form",
117
225
  "title": "モックアップ(Mobile Form)",
118
226
  "description": "モバイル端末上で入力フォームの配置と余白を確認するモックアップ。",
119
- "requires": ["device-mock", "heading", "fieldset", "input-text", "select", "textarea", "button"],
227
+ "requires": [
228
+ "device-mock",
229
+ "heading",
230
+ "fieldset",
231
+ "input-text",
232
+ "select",
233
+ "textarea",
234
+ "button"
235
+ ],
120
236
  "stability": "stable",
121
237
  "contractVersion": "1.0",
122
- "entryHints": ["boot"],
123
- "html": "<section data-dads-typeset>\n <dads-device-mock device=\"mobile\" visible-height=\"560px\">\n <main>\n <dads-heading level=\"2\">申請フォーム</dads-heading>\n <form>\n <dads-fieldset>\n <legend>基本情報</legend>\n <dads-input-text name=\"name\" required></dads-input-text>\n <dads-select name=\"type\" required></dads-select>\n <dads-textarea name=\"detail\" required></dads-textarea>\n </dads-fieldset>\n <dads-button type=\"submit\">確認へ進む</dads-button>\n </form>\n </main>\n </dads-device-mock>\n</section>\n"
238
+ "entryHints": [
239
+ "boot"
240
+ ],
241
+ "html": "<section data-dads-typeset>\n <dads-device-mock device=\"mobile\" visible-height=\"560px\">\n <main>\n <dads-heading level=\"2\">申請フォーム</dads-heading>\n <form>\n <dads-fieldset>\n <legend>基本情報</legend>\n <dads-input-text label=\"氏名\" name=\"name\" required></dads-input-text>\n <dads-select label=\"種別\" name=\"type\" required></dads-select>\n <dads-textarea label=\"詳細\" name=\"detail\" required></dads-textarea>\n </dads-fieldset>\n <dads-button type=\"submit\">確認へ進む</dads-button>\n </form>\n </main>\n </dads-device-mock>\n</section>\n",
242
+ "behavior": "// Mockups are visual-only; no runtime behavior needed."
124
243
  }
125
244
  }
126
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoharada/wcf-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for the web-components-factory design system. Provides component discovery, validation, and pattern-based UI composition without cloning the repository.",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -21,6 +21,7 @@ const REPO_FILE_MAP = {
21
21
  'custom-elements.json': 'custom-elements.json',
22
22
  'install-registry.json': 'registry/install-registry.json',
23
23
  'pattern-registry.json': 'registry/pattern-registry.json',
24
+ 'component-selector-guide.json': 'registry/component-selector-guide.json',
24
25
  'design-tokens.json': 'design-tokens.json',
25
26
  'guidelines-index.json': 'guidelines-index.json',
26
27
  'llms-full.txt': 'llms-full.txt',
package/validator.mjs CHANGED
@@ -471,12 +471,14 @@ export function detectTokenMisuseInInlineStyles({
471
471
  * filePath?: string;
472
472
  * text: string;
473
473
  * severity?: string;
474
+ * cemTagNames?: Set<string>;
474
475
  * }} params
475
476
  */
476
477
  export function detectAccessibilityMisuseInMarkup({
477
478
  filePath = '<input>',
478
479
  text,
479
480
  severity = 'warning',
481
+ cemTagNames,
480
482
  }) {
481
483
  const diagnostics = [];
482
484
  const lineStarts = computeLineIndex(text);
@@ -508,7 +510,9 @@ export function detectAccessibilityMisuseInMarkup({
508
510
  });
509
511
  }
510
512
 
511
- const roleAttr = parseAttributes(attrChunk).find(({ name }) => String(name ?? '').toLowerCase() === 'role');
513
+ const parsedAttrs = parseAttributes(attrChunk);
514
+
515
+ const roleAttr = parsedAttrs.find(({ name }) => String(name ?? '').toLowerCase() === 'role');
512
516
  const roleValue = String(roleAttr?.value ?? '').trim().toLowerCase();
513
517
  if (roleAttr && roleValue === 'alert') {
514
518
  const attrName = 'role';
@@ -527,6 +531,35 @@ export function detectAccessibilityMisuseInMarkup({
527
531
  hint: 'Replace role=\"alert\" with non-live text associated to the control.',
528
532
  });
529
533
  }
534
+
535
+ // Empty label / aria-label detection (v0.4.0, DD-26)
536
+ // Only check CEM-registered custom elements to avoid false positives on third-party elements
537
+ const isCemElement = cemTagNames ? cemTagNames.has(tag) : tag.includes('-');
538
+ if (isCemElement) {
539
+ const EMPTY_LABEL_CHECKS = [
540
+ { attr: 'label', code: 'emptyLabel', hint: 'Set label to a descriptive text, e.g. label="氏名".', msg: (t) => `Empty label attribute on <${t}>. Provide a meaningful label for accessibility.` },
541
+ { attr: 'aria-label', code: 'emptyAriaLabel', hint: 'Set aria-label to descriptive text or use a visible <label> element instead.', msg: (t) => `Empty aria-label attribute on <${t}>. Provide a meaningful label for accessibility.` },
542
+ ];
543
+ for (const { name, offset, value } of parsedAttrs) {
544
+ const attrLower = String(name ?? '').toLowerCase();
545
+ if (typeof value !== 'string' || value.trim() !== '') continue;
546
+ const check = EMPTY_LABEL_CHECKS.find((c) => c.attr === attrLower);
547
+ if (!check) continue;
548
+ const startIndex = rawAttrsStart + offset;
549
+ const endIndex = startIndex + name.length;
550
+ const range = makeRange(lineStarts, startIndex, endIndex);
551
+ diagnostics.push({
552
+ file: filePath,
553
+ range,
554
+ severity,
555
+ code: check.code,
556
+ message: check.msg(tag),
557
+ tagName: tag,
558
+ attrName: check.attr,
559
+ hint: check.hint,
560
+ });
561
+ }
562
+ }
530
563
  }
531
564
 
532
565
  return diagnostics;