@monoharada/wcf-mcp 0.11.0 → 0.12.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 +8 -3
- package/core/constants.mjs +12 -6
- package/core/prefix.mjs +13 -1
- package/core/register.mjs +170 -16
- package/core.mjs +8 -0
- package/data/component-selector-guide.json +198 -56
- package/data/custom-elements.json +174 -6
- package/data/design-tokens.json +1 -1
- package/data/guidelines-index.json +1 -1
- package/data/llms-full.txt +85 -19
- package/data/pattern-registry.json +18 -0
- package/data/skills-registry.json +41 -0
- package/package.json +1 -1
- package/validator.mjs +314 -0
package/data/llms-full.txt
CHANGED
|
@@ -945,8 +945,8 @@ None
|
|
|
945
945
|
|
|
946
946
|
| Attribute | Type | Default | Description |
|
|
947
947
|
|-----------|------|---------|-------------|
|
|
948
|
-
| `color` |
|
|
949
|
-
| `variant` |
|
|
948
|
+
| `color` | 'gray' \| 'blue' \| 'light-blue' \| 'cyan' \| 'green' \| 'lime' \| 'yellow' \| 'orange' \| 'red' \| 'magenta' \| 'purple' | - | カラー |
|
|
949
|
+
| `variant` | 'text' \| 'outline' \| 'filled-outline' \| 'fill' | - | バリアント |
|
|
950
950
|
|
|
951
951
|
|
|
952
952
|
#### Slots
|
|
@@ -2420,12 +2420,13 @@ None
|
|
|
2420
2420
|
#### Usage
|
|
2421
2421
|
|
|
2422
2422
|
```html
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2423
|
+
<!-- ページ見出し -->
|
|
2424
|
+
<dads-heading level="1" size="36">申請一覧</dads-heading>
|
|
2425
|
+
|
|
2426
|
+
<!-- typeset コンテナ内の節見出し -->
|
|
2427
|
+
<main data-dads-typeset>
|
|
2428
|
+
<dads-heading level="2" size="24" margin="top">基本情報</dads-heading>
|
|
2429
|
+
</main>
|
|
2429
2430
|
```
|
|
2430
2431
|
|
|
2431
2432
|
---
|
|
@@ -2499,6 +2500,7 @@ None
|
|
|
2499
2500
|
| `error` | boolean | - | エラー状態 |
|
|
2500
2501
|
| `error-text` | string | - | エラーメッセージ(スロット未使用時のフォールバック) |
|
|
2501
2502
|
| `input-width` | string | - | 幅バリアント (short | medium | full | カスタム値) |
|
|
2503
|
+
| `inputmode` | 'none' \| 'text' \| 'decimal' \| 'numeric' \| 'tel' \| 'search' \| 'email' \| 'url' | - | モバイル向け入力モードヒント |
|
|
2502
2504
|
| `label` | string | - | ラベルテキスト(スロット未使用時のフォールバック) |
|
|
2503
2505
|
| `name` | string | - | フォーム名 |
|
|
2504
2506
|
| `readonly` | boolean | - | 読み取り専用 |
|
|
@@ -3897,14 +3899,14 @@ None
|
|
|
3897
3899
|
#### Usage
|
|
3898
3900
|
|
|
3899
3901
|
```html
|
|
3900
|
-
<dads-resource-list
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
<
|
|
3907
|
-
<
|
|
3902
|
+
<dads-resource-list
|
|
3903
|
+
href="/files/guide.pdf"
|
|
3904
|
+
download
|
|
3905
|
+
data-interaction="whole"
|
|
3906
|
+
data-style="frame"
|
|
3907
|
+
>
|
|
3908
|
+
<span slot="title">申請ガイド(PDF)</span>
|
|
3909
|
+
<span slot="support">PDF 1.2MB</span>
|
|
3908
3910
|
</dads-resource-list>
|
|
3909
3911
|
```
|
|
3910
3912
|
|
|
@@ -4671,9 +4673,43 @@ None
|
|
|
4671
4673
|
#### Usage
|
|
4672
4674
|
|
|
4673
4675
|
```html
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4676
|
+
<!-- 基本テーブル -->
|
|
4677
|
+
<dads-table>
|
|
4678
|
+
<table>
|
|
4679
|
+
<thead>
|
|
4680
|
+
<tr>
|
|
4681
|
+
<th scope="col">項目</th>
|
|
4682
|
+
<th scope="col">値</th>
|
|
4683
|
+
</tr>
|
|
4684
|
+
</thead>
|
|
4685
|
+
<tbody>
|
|
4686
|
+
<tr>
|
|
4687
|
+
<td>サンプル</td>
|
|
4688
|
+
<td>1</td>
|
|
4689
|
+
</tr>
|
|
4690
|
+
</tbody>
|
|
4691
|
+
</table>
|
|
4692
|
+
</dads-table>
|
|
4693
|
+
|
|
4694
|
+
<!-- ソート + 行選択 -->
|
|
4695
|
+
<dads-table selectable hover sort-behavior="dom">
|
|
4696
|
+
<table>
|
|
4697
|
+
<thead>
|
|
4698
|
+
<tr>
|
|
4699
|
+
<th scope="col"><input type="checkbox" data-select-all aria-label="表示中の行をすべて選択" /></th>
|
|
4700
|
+
<th scope="col" data-sort-type="string"><button type="button" data-sort>氏名</button></th>
|
|
4701
|
+
<th scope="col" data-sort-type="number"><button type="button" data-sort>金額</button></th>
|
|
4702
|
+
</tr>
|
|
4703
|
+
</thead>
|
|
4704
|
+
<tbody>
|
|
4705
|
+
<tr>
|
|
4706
|
+
<td><input type="checkbox" data-select-row aria-label="行を選択: A001" /></td>
|
|
4707
|
+
<td>山田 太郎</td>
|
|
4708
|
+
<td data-sort-value="1200">1,200</td>
|
|
4709
|
+
</tr>
|
|
4710
|
+
</tbody>
|
|
4711
|
+
</table>
|
|
4712
|
+
</dads-table>
|
|
4677
4713
|
```
|
|
4678
4714
|
|
|
4679
4715
|
---
|
|
@@ -4970,6 +5006,36 @@ Requires: `heading`, `table`, `page-navigation`
|
|
|
4970
5006
|
</main>
|
|
4971
5007
|
```
|
|
4972
5008
|
|
|
5009
|
+
### table-with-sort
|
|
5010
|
+
|
|
5011
|
+
**テーブル(ソート付き)**: 列ヘッダーのクリックでソートできるデータテーブル
|
|
5012
|
+
|
|
5013
|
+
Requires: `heading`, `table`
|
|
5014
|
+
|
|
5015
|
+
```html
|
|
5016
|
+
<main data-dads-typeset>
|
|
5017
|
+
<dads-heading level="1" size="36">一覧</dads-heading>
|
|
5018
|
+
<dads-table hover sort-behavior="dom">
|
|
5019
|
+
<table>
|
|
5020
|
+
<thead>
|
|
5021
|
+
<tr>
|
|
5022
|
+
<th scope="col" data-sort-type="string"><button type="button" data-sort>氏名</button></th>
|
|
5023
|
+
<th scope="col" data-sort-type="date"><button type="button" data-sort>更新日</button></th>
|
|
5024
|
+
<th scope="col">操作</th>
|
|
5025
|
+
</tr>
|
|
5026
|
+
</thead>
|
|
5027
|
+
<tbody>
|
|
5028
|
+
<tr>
|
|
5029
|
+
<td>山田 太郎</td>
|
|
5030
|
+
<td data-sort-value="2026-03-17">2026/03/17</td>
|
|
5031
|
+
<td>詳細</td>
|
|
5032
|
+
</tr>
|
|
5033
|
+
</tbody>
|
|
5034
|
+
</table>
|
|
5035
|
+
</dads-table>
|
|
5036
|
+
</main>
|
|
5037
|
+
```
|
|
5038
|
+
|
|
4973
5039
|
### card-grid
|
|
4974
5040
|
|
|
4975
5041
|
**カードグリッド**: カードで一覧表示する基本レイアウト
|
|
@@ -60,6 +60,24 @@
|
|
|
60
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
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});"
|
|
62
62
|
},
|
|
63
|
+
"table-with-sort": {
|
|
64
|
+
"id": "table-with-sort",
|
|
65
|
+
"title": "テーブル(ソート付き)",
|
|
66
|
+
"description": "列ヘッダーのクリックでソートできるデータテーブル",
|
|
67
|
+
"requires": [
|
|
68
|
+
"heading",
|
|
69
|
+
"table"
|
|
70
|
+
],
|
|
71
|
+
"stability": "stable",
|
|
72
|
+
"contractVersion": "1.0",
|
|
73
|
+
"entryHints": [
|
|
74
|
+
"boot",
|
|
75
|
+
"@wcf",
|
|
76
|
+
"index"
|
|
77
|
+
],
|
|
78
|
+
"html": "<main data-dads-typeset>\n <dads-heading level=\"1\" size=\"36\">一覧</dads-heading>\n <dads-table hover sort-behavior=\"dom\">\n <table>\n <thead>\n <tr>\n <th scope=\"col\" data-sort-type=\"string\"><button type=\"button\" data-sort>氏名</button></th>\n <th scope=\"col\" data-sort-type=\"date\"><button type=\"button\" data-sort>更新日</button></th>\n <th scope=\"col\">操作</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>山田 太郎</td>\n <td data-sort-value=\"2026-03-17\">2026/03/17</td>\n <td>詳細</td>\n </tr>\n </tbody>\n </table>\n </dads-table>\n</main>\n",
|
|
79
|
+
"behavior": "document.querySelector(\"dads-table\").addEventListener(\"dads-sort-change\", (e) => {\n console.log(\"Sort:\", e.detail.columnIndex, e.detail.direction);\n});"
|
|
80
|
+
},
|
|
63
81
|
"card-grid": {
|
|
64
82
|
"id": "card-grid",
|
|
65
83
|
"title": "カードグリッド",
|
|
@@ -256,6 +256,47 @@
|
|
|
256
256
|
"lastUpdated": "2026-03-04"
|
|
257
257
|
}
|
|
258
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"name": "wcf-mcp-release",
|
|
261
|
+
"path": ".claude/skills/wcf-mcp-release",
|
|
262
|
+
"entry": "SKILL.md",
|
|
263
|
+
"clients": [
|
|
264
|
+
"codex",
|
|
265
|
+
"claude_code",
|
|
266
|
+
"cursor"
|
|
267
|
+
],
|
|
268
|
+
"status": "active",
|
|
269
|
+
"description": "Bump and verify the publishable @monoharada/wcf-mcp package.",
|
|
270
|
+
"tags": [
|
|
271
|
+
"workflow"
|
|
272
|
+
],
|
|
273
|
+
"version": "1.0.0",
|
|
274
|
+
"dependencies": [],
|
|
275
|
+
"compat": {
|
|
276
|
+
"minVersion": {
|
|
277
|
+
"claude_code": "1.0",
|
|
278
|
+
"cursor": "0.50",
|
|
279
|
+
"codex": "0.1"
|
|
280
|
+
},
|
|
281
|
+
"capabilities": [
|
|
282
|
+
"read_repo",
|
|
283
|
+
"run_commands",
|
|
284
|
+
"write_repo"
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
"manifest": {
|
|
288
|
+
"author": "wcf-team",
|
|
289
|
+
"license": "MIT",
|
|
290
|
+
"entryFormat": "markdown",
|
|
291
|
+
"sections": [
|
|
292
|
+
"overview",
|
|
293
|
+
"workflow",
|
|
294
|
+
"do_dont"
|
|
295
|
+
],
|
|
296
|
+
"sizeBytes": 2031,
|
|
297
|
+
"lastUpdated": "2026-03-18"
|
|
298
|
+
}
|
|
299
|
+
},
|
|
259
300
|
{
|
|
260
301
|
"name": "wcf-ui-builder",
|
|
261
302
|
"path": ".claude/skills/wcf-ui-builder",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoharada/wcf-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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/validator.mjs
CHANGED
|
@@ -1248,3 +1248,317 @@ export function detectMissingRuntimeScaffold({
|
|
|
1248
1248
|
|
|
1249
1249
|
return diagnostics;
|
|
1250
1250
|
}
|
|
1251
|
+
|
|
1252
|
+
function getAttributeRecord(attrs, ...names) {
|
|
1253
|
+
const lowered = names.map((name) => name.toLowerCase());
|
|
1254
|
+
return attrs.find((attr) => lowered.includes(String(attr?.name ?? '').toLowerCase()));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
export function detectTableAuthoringMisuse({
|
|
1258
|
+
filePath = '<input>',
|
|
1259
|
+
text,
|
|
1260
|
+
prefix = 'dads',
|
|
1261
|
+
severity = 'warning',
|
|
1262
|
+
}) {
|
|
1263
|
+
const diagnostics = [];
|
|
1264
|
+
const sourceText = sanitizeMarkupForValidation(text);
|
|
1265
|
+
const lineStarts = computeLineIndex(sourceText);
|
|
1266
|
+
const tableTag = `${String(prefix).toLowerCase()}-table`;
|
|
1267
|
+
const tagRe = /<\/?([a-z][a-z0-9-]*)\b([^<>]*?)\/?>/gi;
|
|
1268
|
+
const stack = [];
|
|
1269
|
+
let m;
|
|
1270
|
+
|
|
1271
|
+
while ((m = tagRe.exec(sourceText))) {
|
|
1272
|
+
const fullMatch = m[0];
|
|
1273
|
+
const tag = String(m[1] ?? '').toLowerCase();
|
|
1274
|
+
const isClosing = fullMatch.startsWith('</');
|
|
1275
|
+
const isSelfClosing = fullMatch.endsWith('/>');
|
|
1276
|
+
const attrChunk = String(m[2] ?? '');
|
|
1277
|
+
const rawAttrsStart = m.index + 1 + tag.length;
|
|
1278
|
+
|
|
1279
|
+
if (isClosing) {
|
|
1280
|
+
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
1281
|
+
if (stack[index] === tag) {
|
|
1282
|
+
stack.splice(index, 1);
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const insideTable = stack.includes(tableTag) || tag === tableTag;
|
|
1290
|
+
const attrs = parseAttributes(attrChunk);
|
|
1291
|
+
const sortAttr = getAttributeRecord(attrs, 'data-sort', 'data-js-sort');
|
|
1292
|
+
const sortTypeAttr = getAttributeRecord(attrs, 'data-sort-type');
|
|
1293
|
+
const rowSelectAttr = getAttributeRecord(attrs, 'data-select-row', 'data-js-check');
|
|
1294
|
+
const selectAllAttr = getAttributeRecord(attrs, 'data-select-all', 'data-js-check-all');
|
|
1295
|
+
const typeAttr = getAttributeRecord(attrs, 'type');
|
|
1296
|
+
const typeValue = String(typeAttr?.value ?? '').toLowerCase();
|
|
1297
|
+
|
|
1298
|
+
if (insideTable && sortAttr && tag === 'th') {
|
|
1299
|
+
const startIndex = rawAttrsStart + sortAttr.offset;
|
|
1300
|
+
diagnostics.push({
|
|
1301
|
+
file: filePath,
|
|
1302
|
+
range: makeRange(lineStarts, startIndex, startIndex + sortAttr.name.length),
|
|
1303
|
+
severity,
|
|
1304
|
+
code: 'sortOnTh',
|
|
1305
|
+
message: 'data-sort must be placed on a <button> inside the <th>, not on the <th> itself.',
|
|
1306
|
+
tagName: tag,
|
|
1307
|
+
attrName: sortAttr.name,
|
|
1308
|
+
hint: 'Wrap the header text in <button type="button" data-sort>...</button> and keep data-sort-type on the <th>.',
|
|
1309
|
+
});
|
|
1310
|
+
} else if (insideTable && sortAttr && tag !== 'button') {
|
|
1311
|
+
const startIndex = rawAttrsStart + sortAttr.offset;
|
|
1312
|
+
diagnostics.push({
|
|
1313
|
+
file: filePath,
|
|
1314
|
+
range: makeRange(lineStarts, startIndex, startIndex + sortAttr.name.length),
|
|
1315
|
+
severity,
|
|
1316
|
+
code: 'sortWrongTarget',
|
|
1317
|
+
message: 'data-sort controls must be native <button> elements.',
|
|
1318
|
+
tagName: tag,
|
|
1319
|
+
attrName: sortAttr.name,
|
|
1320
|
+
hint: 'Use <button type="button" data-sort>...</button> inside the sortable <th>.',
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (insideTable && sortTypeAttr && tag === 'button') {
|
|
1325
|
+
const startIndex = rawAttrsStart + sortTypeAttr.offset;
|
|
1326
|
+
diagnostics.push({
|
|
1327
|
+
file: filePath,
|
|
1328
|
+
range: makeRange(lineStarts, startIndex, startIndex + sortTypeAttr.name.length),
|
|
1329
|
+
severity,
|
|
1330
|
+
code: 'sortTypeOnWrongElement',
|
|
1331
|
+
message: 'data-sort-type is read from the enclosing <th>, not from the sort button.',
|
|
1332
|
+
tagName: tag,
|
|
1333
|
+
attrName: sortTypeAttr.name,
|
|
1334
|
+
hint: 'Move data-sort-type to the parent <th scope="col" data-sort-type="...">.',
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const selectionAttr = rowSelectAttr ?? selectAllAttr;
|
|
1339
|
+
if (insideTable && selectionAttr && !(tag === 'input' && typeValue === 'checkbox')) {
|
|
1340
|
+
const startIndex = rawAttrsStart + selectionAttr.offset;
|
|
1341
|
+
diagnostics.push({
|
|
1342
|
+
file: filePath,
|
|
1343
|
+
range: makeRange(lineStarts, startIndex, startIndex + selectionAttr.name.length),
|
|
1344
|
+
severity,
|
|
1345
|
+
code: 'selectionControlWrongElement',
|
|
1346
|
+
message: `${selectionAttr.name} must be placed on input[type="checkbox"].`,
|
|
1347
|
+
tagName: tag,
|
|
1348
|
+
attrName: selectionAttr.name,
|
|
1349
|
+
hint: `Use <input type="checkbox" ${selectionAttr.name} aria-label="...">.`,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (!isSelfClosing && !HTML_VOID_ELEMENTS.has(tag)) {
|
|
1354
|
+
stack.push(tag);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return diagnostics;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
export function detectResourceListAuthoringMisuse({
|
|
1362
|
+
filePath = '<input>',
|
|
1363
|
+
text,
|
|
1364
|
+
prefix = 'dads',
|
|
1365
|
+
severity = 'warning',
|
|
1366
|
+
}) {
|
|
1367
|
+
const diagnostics = [];
|
|
1368
|
+
const sourceText = sanitizeMarkupForValidation(text);
|
|
1369
|
+
const lineStarts = computeLineIndex(sourceText);
|
|
1370
|
+
const tagName = `${String(prefix).toLowerCase()}-resource-list`;
|
|
1371
|
+
const tagRe = new RegExp(`<(${tagName})\\b([^<>]*?)>([\\s\\S]*?)</${tagName}>`, 'gi');
|
|
1372
|
+
let m;
|
|
1373
|
+
|
|
1374
|
+
while ((m = tagRe.exec(sourceText))) {
|
|
1375
|
+
const attrChunk = String(m[2] ?? '');
|
|
1376
|
+
const bodyChunk = String(m[3] ?? '');
|
|
1377
|
+
const rawAttrsStart = m.index + 1 + tagName.length;
|
|
1378
|
+
const attrs = parseAttributes(attrChunk);
|
|
1379
|
+
const hrefAttr = getAttributeRecord(attrs, 'href');
|
|
1380
|
+
const interactionAttr = getAttributeRecord(attrs, 'data-interaction');
|
|
1381
|
+
if (!hrefAttr) continue;
|
|
1382
|
+
const interactionValue = String(interactionAttr?.value ?? '').toLowerCase();
|
|
1383
|
+
if (interactionValue === 'whole') continue;
|
|
1384
|
+
const hasDelegatedTitleLink = /slot\s*=\s*["']title["'][^>]*>[\s\S]*?<a\b[^>]*href=/i.test(bodyChunk);
|
|
1385
|
+
if (hasDelegatedTitleLink) continue;
|
|
1386
|
+
|
|
1387
|
+
const startIndex = rawAttrsStart + hrefAttr.offset;
|
|
1388
|
+
diagnostics.push({
|
|
1389
|
+
file: filePath,
|
|
1390
|
+
range: makeRange(lineStarts, startIndex, startIndex + hrefAttr.name.length),
|
|
1391
|
+
severity,
|
|
1392
|
+
code: 'resourceListWholeLinkMissingInteraction',
|
|
1393
|
+
message: '<dads-resource-list href="..."> requires data-interaction="whole" for host-level whole-link mode.',
|
|
1394
|
+
tagName,
|
|
1395
|
+
attrName: 'href',
|
|
1396
|
+
hint: 'Add data-interaction="whole", or move the main link into slot="title" as a delegated title link.',
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return diagnostics;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
export function detectReplaceableNativePatterns({
|
|
1404
|
+
filePath = '<input>',
|
|
1405
|
+
text,
|
|
1406
|
+
prefix = 'dads',
|
|
1407
|
+
}) {
|
|
1408
|
+
const diagnostics = [];
|
|
1409
|
+
const sourceText = sanitizeMarkupForValidation(text);
|
|
1410
|
+
const lineStarts = computeLineIndex(sourceText);
|
|
1411
|
+
const p = String(prefix).toLowerCase();
|
|
1412
|
+
const customStack = [];
|
|
1413
|
+
const tagRe = /<\/?([a-z][a-z0-9-]*)\b([^<>]*?)\/?>/gi;
|
|
1414
|
+
let m;
|
|
1415
|
+
|
|
1416
|
+
while ((m = tagRe.exec(sourceText))) {
|
|
1417
|
+
const fullMatch = m[0];
|
|
1418
|
+
const tag = String(m[1] ?? '').toLowerCase();
|
|
1419
|
+
const isClosing = fullMatch.startsWith('</');
|
|
1420
|
+
const isSelfClosing = fullMatch.endsWith('/>');
|
|
1421
|
+
const attrChunk = String(m[2] ?? '');
|
|
1422
|
+
const rawAttrsStart = m.index + 1 + tag.length;
|
|
1423
|
+
|
|
1424
|
+
if (isClosing) {
|
|
1425
|
+
if (tag.startsWith(`${p}-`)) {
|
|
1426
|
+
for (let index = customStack.length - 1; index >= 0; index -= 1) {
|
|
1427
|
+
if (customStack[index] === tag) {
|
|
1428
|
+
customStack.splice(index, 1);
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const insideWcfCustomElement = customStack.some((entry) => entry.startsWith(`${p}-`));
|
|
1437
|
+
const attrs = parseAttributes(attrChunk);
|
|
1438
|
+
const roleAttr = getAttributeRecord(attrs, 'role');
|
|
1439
|
+
const roleValue = String(roleAttr?.value ?? '').toLowerCase();
|
|
1440
|
+
const typeAttr = getAttributeRecord(attrs, 'type');
|
|
1441
|
+
const typeValue = String(typeAttr?.value ?? '').toLowerCase();
|
|
1442
|
+
|
|
1443
|
+
if (!insideWcfCustomElement && roleAttr && roleValue === 'tablist') {
|
|
1444
|
+
const startIndex = rawAttrsStart + roleAttr.offset;
|
|
1445
|
+
diagnostics.push({
|
|
1446
|
+
file: filePath,
|
|
1447
|
+
range: makeRange(lineStarts, startIndex, startIndex + roleAttr.name.length),
|
|
1448
|
+
severity: 'warning',
|
|
1449
|
+
code: 'nativePatternReplaceable',
|
|
1450
|
+
message: 'Custom tablist markup detected outside dads-tab.',
|
|
1451
|
+
tagName: tag,
|
|
1452
|
+
attrName: 'role',
|
|
1453
|
+
hint: 'Consider using <dads-tab> instead of a custom role="tablist" implementation.',
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (!insideWcfCustomElement && (tag === 'dialog' || (roleAttr && roleValue === 'dialog'))) {
|
|
1458
|
+
const startIndex = tag === 'dialog' ? m.index + 1 : rawAttrsStart + roleAttr.offset;
|
|
1459
|
+
diagnostics.push({
|
|
1460
|
+
file: filePath,
|
|
1461
|
+
range: makeRange(lineStarts, startIndex, startIndex + (tag === 'dialog' ? tag.length : roleAttr.name.length)),
|
|
1462
|
+
severity: 'info',
|
|
1463
|
+
code: 'nativePatternReplaceable',
|
|
1464
|
+
message: 'Dialog-like markup detected outside dads-dialog or dads-drawer.',
|
|
1465
|
+
tagName: tag,
|
|
1466
|
+
attrName: roleAttr ? 'role' : undefined,
|
|
1467
|
+
hint: 'Consider using <dads-dialog> for modal dialogs or <dads-drawer> for slide-out panels.',
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (!insideWcfCustomElement && roleAttr && roleValue === 'progressbar') {
|
|
1472
|
+
const startIndex = rawAttrsStart + roleAttr.offset;
|
|
1473
|
+
diagnostics.push({
|
|
1474
|
+
file: filePath,
|
|
1475
|
+
range: makeRange(lineStarts, startIndex, startIndex + roleAttr.name.length),
|
|
1476
|
+
severity: 'warning',
|
|
1477
|
+
code: 'nativePatternReplaceable',
|
|
1478
|
+
message: 'Progress/loading markup detected outside dads-spinner or dads-progress-bar.',
|
|
1479
|
+
tagName: tag,
|
|
1480
|
+
attrName: 'role',
|
|
1481
|
+
hint: 'Consider using <dads-spinner> for indeterminate loading or <dads-progress-bar> for determinate progress.',
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (!insideWcfCustomElement && tag === 'input' && typeValue === 'file') {
|
|
1486
|
+
const startIndex = rawAttrsStart + (typeAttr?.offset ?? 0);
|
|
1487
|
+
diagnostics.push({
|
|
1488
|
+
file: filePath,
|
|
1489
|
+
range: makeRange(lineStarts, startIndex, startIndex + (typeAttr?.name.length ?? 4)),
|
|
1490
|
+
severity: 'info',
|
|
1491
|
+
code: 'nativePatternReplaceable',
|
|
1492
|
+
message: 'Native file input detected outside dads-file-upload.',
|
|
1493
|
+
tagName: tag,
|
|
1494
|
+
attrName: 'type',
|
|
1495
|
+
hint: 'Consider using <dads-file-upload> when you need upload UI, drag-and-drop, or file list feedback.',
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
if (!insideWcfCustomElement && tag === 'dl') {
|
|
1500
|
+
const startIndex = m.index + 1;
|
|
1501
|
+
diagnostics.push({
|
|
1502
|
+
file: filePath,
|
|
1503
|
+
range: makeRange(lineStarts, startIndex, startIndex + tag.length),
|
|
1504
|
+
severity: 'info',
|
|
1505
|
+
code: 'nativePatternReplaceable',
|
|
1506
|
+
message: 'Description-list style markup detected outside dads-description-list.',
|
|
1507
|
+
tagName: tag,
|
|
1508
|
+
hint: 'Consider using <dads-description-list> when you need the DADS key-value presentation pattern.',
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (!insideWcfCustomElement && tag === 'nav') {
|
|
1513
|
+
const windowText = sourceText.slice(m.index, Math.min(sourceText.length, m.index + 2000)).toLowerCase();
|
|
1514
|
+
if (windowText.includes('<ol') && (windowText.includes('aria-current="step"') || windowText.includes("aria-current='step'") || /\b(step|wizard)\b/.test(windowText))) {
|
|
1515
|
+
const startIndex = m.index + 1;
|
|
1516
|
+
diagnostics.push({
|
|
1517
|
+
file: filePath,
|
|
1518
|
+
range: makeRange(lineStarts, startIndex, startIndex + tag.length),
|
|
1519
|
+
severity: 'info',
|
|
1520
|
+
code: 'nativePatternReplaceable',
|
|
1521
|
+
message: 'Step-navigation like markup detected outside dads-step-navigation.',
|
|
1522
|
+
tagName: tag,
|
|
1523
|
+
hint: 'Consider using <dads-step-navigation> and <dads-step-navigation-item> for wizard progress UI.',
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (!isSelfClosing && !HTML_VOID_ELEMENTS.has(tag) && tag.startsWith(`${p}-`)) {
|
|
1529
|
+
customStack.push(tag);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
return diagnostics;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
export function detectReplaceableAnimationPatterns({
|
|
1537
|
+
filePath = '<input>',
|
|
1538
|
+
text,
|
|
1539
|
+
}) {
|
|
1540
|
+
const diagnostics = [];
|
|
1541
|
+
const lineStarts = computeLineIndex(text);
|
|
1542
|
+
const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
1543
|
+
let m;
|
|
1544
|
+
|
|
1545
|
+
while ((m = styleBlockRe.exec(text))) {
|
|
1546
|
+
const cssText = String(m[1] ?? '');
|
|
1547
|
+
if (!/@keyframes\s+[^{\s]*(spin|spinner|rotate)/i.test(cssText)) continue;
|
|
1548
|
+
if (!/animation\s*:/i.test(cssText)) continue;
|
|
1549
|
+
const nearbyMarkup = text.slice(Math.max(0, m.index - 500), Math.min(text.length, m.index + m[0].length + 1500));
|
|
1550
|
+
if (!/(loading|spinner|processing|sending|処理中|送信中|読み込み中|ローディング|スピナー)/i.test(nearbyMarkup)) continue;
|
|
1551
|
+
|
|
1552
|
+
const styleIndex = m.index + m[0].toLowerCase().indexOf('@keyframes');
|
|
1553
|
+
diagnostics.push({
|
|
1554
|
+
file: filePath,
|
|
1555
|
+
range: makeRange(lineStarts, styleIndex, styleIndex + 10),
|
|
1556
|
+
severity: 'warning',
|
|
1557
|
+
code: 'customAnimationReplaceable',
|
|
1558
|
+
message: 'Custom spinner/loading animation detected in CSS.',
|
|
1559
|
+
hint: 'Consider using <dads-spinner> or <dads-progress-bar> instead of hand-written loading animation CSS.',
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return diagnostics;
|
|
1564
|
+
}
|