ksk-design-system 2.9.0 → 2.10.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.
Files changed (57) hide show
  1. package/.claude/skills/ksk-design-system/SKILL.md +153 -0
  2. package/.claude/skills/ksk-design-system/references/component-decisions.md +87 -0
  3. package/.claude/skills/ksk-design-system/references/hig-m3-mapping.md +92 -0
  4. package/.claude/skills/ksk-design-system/references/prohibited-rationale.md +148 -0
  5. package/.claude/skills/ksk-design-system/references/tokens-reference.md +79 -0
  6. package/AGENTS.md +31 -6
  7. package/CLAUDE.md +31 -6
  8. package/DESIGN.md +23 -0
  9. package/MIGRATION.md +12 -0
  10. package/README.md +77 -0
  11. package/RELEASE.md +13 -0
  12. package/bin/ds-shape-override.js +149 -0
  13. package/bin/lint.js +23 -2
  14. package/bin/unlayered-global-override.js +96 -0
  15. package/contracts/components.json +126 -29
  16. package/contracts/consumer-requests.json +2 -2
  17. package/contracts/product-theme-overrides.json +4 -0
  18. package/contracts/rules.json +25 -1
  19. package/contracts/token-hex-cache.json +14 -1
  20. package/dist/index.js +2540 -2243
  21. package/dist/native/ui.js +5 -1
  22. package/dist/{native-BHDWkOgd.js → native-BQg-H087.js} +72 -24
  23. package/dist/native.js +1 -1
  24. package/dist/types/components/patterns/admin/bulk-actions.d.ts +3 -1
  25. package/dist/types/components/patterns/admin/data-table.d.ts +3 -1
  26. package/dist/types/components/patterns/confirm-dialog.d.ts +43 -3
  27. package/dist/types/components/patterns/sidebar-nav.d.ts +30 -1
  28. package/dist/types/components/ui/alert-dialog.d.ts +3 -1
  29. package/dist/types/components/ui/breadcrumb.d.ts +11 -1
  30. package/dist/types/components/ui/button.d.ts +29 -1
  31. package/dist/types/components/ui/checkbox-card.d.ts +1 -1
  32. package/dist/types/components/ui/combobox.d.ts +7 -1
  33. package/dist/types/components/ui/dialog.d.ts +27 -2
  34. package/dist/types/components/ui/error-boundary.d.ts +7 -0
  35. package/dist/types/components/ui/multi-select.d.ts +7 -1
  36. package/dist/types/components/ui/navigation-bar.d.ts +10 -2
  37. package/dist/types/components/ui/number-input.d.ts +2 -2
  38. package/dist/types/components/ui/pagination.d.ts +12 -0
  39. package/dist/types/components/ui/responsive-dialog.d.ts +1 -1
  40. package/dist/types/components/ui/select.d.ts +8 -2
  41. package/dist/types/components/ui/sheet.d.ts +11 -1
  42. package/dist/types/components/ui/skeleton.d.ts +3 -1
  43. package/dist/types/components/ui/slider.d.ts +7 -1
  44. package/dist/types/components/ui/tabs.d.ts +6 -0
  45. package/dist/types/components/ui/toast.d.ts +4 -1
  46. package/dist/types/index.d.ts +5 -4
  47. package/dist/types/lib/active-element.d.ts +2 -0
  48. package/dist/types/native/components/ErrorBoundary.d.ts +3 -0
  49. package/dist/types/tokens/native/themes.d.ts +48 -0
  50. package/package.json +10 -5
  51. package/src/components/COMPONENT_LOOKUP.md +15 -15
  52. package/src/preset-core.css +9 -0
  53. package/src/styles/semantic.css +50 -0
  54. package/src/styles/source-safelist.css +40 -10
  55. package/templates/AGENTS.md +2 -2
  56. package/templates/CLAUDE.md +2 -2
  57. package/tokens.json +35 -4
package/AGENTS.md CHANGED
@@ -28,10 +28,10 @@ UI を書く前に必ず確認すること:
28
28
  - [ ] 既存コンポーネントを `src/components/COMPONENT_LOOKUP.md` で確認したか(手書き・再定義は禁止)
29
29
  - [ ] 色は semantic token(`var(--Surface-*)` / `var(--Brand-Primary)` 等)か。Tailwind標準色・生 `#hex` は禁止
30
30
  - [ ] `border` は色を併記したか(`border-[var(--Border-Low-Emphasis)]` 等)。Tailwind v4 では無色 border は currentColor になり、消費側の濃色テキストで黒ずむ(preset.css の base layer が保険だが明示が原則)
31
- - [ ] **文脈非依存**か(テキスト要素に `text-[var(--Text-*)]`、サーフェス/オーバーレイに `bg-[var(--Surface-*)]` を明示)。親の継承や currentColor に頼ると消費側の色文脈で崩れる。Storybook ツールバーの **Hostile ctx** を loud にして、文字/アイコンがマゼンタ化・背景が透けないか確認する
31
+ - [ ] **文脈非依存**か(テキスト要素に `text-[var(--Text-High-Emphasis)]`、サーフェス/オーバーレイに `bg-[var(--Surface-Primary)]` を明示)。親の継承や currentColor に頼ると消費側の色文脈で崩れる。Storybook ツールバーの **Hostile ctx** を loud にして、文字/アイコンがマゼンタ化・背景が透けないか確認する
32
32
  - [ ] typography は `typo-*` クラスか(`font-bold` 等の直書きは禁止)
33
- - [ ] アニメーションは `duration-[var(--Motion-Duration-*)]` / `ease-[var(--Motion-Easing-*)]` か(`duration-200` や生 `cubic-bezier` の直書きは禁止。トークン参照でないと `prefers-reduced-motion` の一括制御から漏れる)
34
- - [ ] 重なり順は `z-[var(--Z-*)]` か(`z-50` 一律だと Portal のマウント順で勝敗が決まる。`z-10` / `z-20` のコンポーネント内部の重なりは対象外。順序は DESIGN.md の Layering 節)
33
+ - [ ] アニメーションは Motion トークン参照か(例: `duration-[var(--Motion-Duration-Base)]` / `ease-[var(--Motion-Easing-Standard)]`)(`duration-200` や生 `cubic-bezier` の直書きは禁止。トークン参照でないと `prefers-reduced-motion` の一括制御から漏れる)
34
+ - [ ] 重なり順は用途に合うトークン(例: `z-[var(--Z-Modal)]` / `z-[var(--Z-Popover)]`)か(`z-50` 一律だと Portal のマウント順で勝敗が決まる。`z-10` / `z-20` のコンポーネント内部の重なりは対象外。順序は DESIGN.md の Layering 節)
35
35
  - [ ] アイコンは `iconsax-reactjs` か(`lucide-react` / `heroicons` は使わない)
36
36
  - [ ] **選んだアイコンの「絵」を実際に見たか**。iconsax は名前と絵が一致しないものがある。`Check` は小切手(checkbook)でチェックマークではない(PR #349 で出荷直前まで残った)。チェックマークは `TickSquare` / `TickCircle`。また **"i" のグリフは iconsax に存在せず**、`InfoCircle` / `Information` / `Danger` / `Warning2` はすべて感嘆符「!」の器違いなので、情報アイコンは DS 自前の `InfoCircleIcon` を使う。名前で選んだら描画結果を目視し、意味が変わると困る箇所は path で固定するテストを書く <!-- docs-drift-ignore: Check TickSquare TickCircle InfoCircle Information Danger Warning2 InfoCircleIcon -->
37
37
  - [ ] 生タグ(`<button>` / `<input>` / `<a href>`)でなく DS コンポーネントを使ったか
@@ -179,6 +179,21 @@ afterEach フックが全ストーリー(tags フィルタなし)に対し
179
179
  (AA 未達トークンの darken と opacity 減衰の廃止により全ストーリー通過。
180
180
  トークンペアの正本チェックは `scripts/check-contrast.mjs`)。CI では常時実行される。
181
181
 
182
+ **WebKit(Safari 系)でも実行できる:**
183
+
184
+ ```bash
185
+ npx playwright install webkit # 初回のみ
186
+ npm run test:interaction:webkit
187
+ npm run test:a11y:webkit
188
+ ```
189
+
190
+ 同じ設定(`vitest.storybook.config.ts` / `vitest.a11y.config.ts`)を環境変数
191
+ `KSK_BROWSER=webkit` で WebKit に切り替えて回す。Chromium では見えない差
192
+ (フォーカス・スクロール・描画差)を拾う。CI では専用ジョブ `webkit` で Chromium 側と
193
+ 並列に常時実行される。現状は全ストーリーが WebKit でも通っており、**除外している
194
+ ストーリー・検査はない**(WebKit 固有の制限で除外が必要になったら、理由付きでここに一覧を残す)。
195
+ ボタン・リンク・Select 等のフォーカス/キーボード操作を触ったときは WebKit 側も回すこと。
196
+
182
197
  **Button など、フォーカス表示の `outline-none` / `outline-hidden` 系クラスを触ったら `npm run test:forced-colors` も実行:**
183
198
 
184
199
  ```bash
@@ -287,6 +302,9 @@ npm run storybook
287
302
  # ビルド
288
303
  npm run build-storybook
289
304
 
305
+ # 初回のみ: mcp-server のテスト(npm test に含まれる)が MCP SDK を使うため依存を入れる
306
+ npm ci --prefix mcp-server
307
+
290
308
  # スクラッチ検出(実装後に必ず実行)
291
309
  bash scripts/lint-scratch.sh
292
310
 
@@ -302,6 +320,9 @@ npm run lint:design
302
320
  # 非推奨 API 台帳の整合検査(台帳 ⇔ 実ソースの @deprecated JSDoc)
303
321
  npm run lint:deprecations
304
322
 
323
+ # 未定義 CSS 変数の参照検査(変数名のタイプミス検出。実行時注入の変数は scripts/check-css-vars.mjs の ALLOWLIST)
324
+ npm run lint:css-vars
325
+
305
326
  # `Closes #N` 誤リンク監査(gh 必須。npm run check には入っていない)
306
327
  npm run audit:issue-links -- --recent 60
307
328
 
@@ -352,8 +373,10 @@ npm run test:forced-colors
352
373
  v1.48.x で連続した種類の不具合をここで落とす。
353
374
  - **play 関数のあるコンポーネント(Button / Dialog / AlertDialog / Sheet / Select /
354
375
  DropdownMenu / Combobox / Tabs / Form / Toast)を触ったら `npm run test:interaction` を回すこと。**
355
- - ビジュアル回帰(スクリーンショット差分)は未導入。`.claude/skills/audit-pages/SKILL.md` による
356
- 手動の視覚監査が現状の代替。
376
+ - ビジュアル回帰(画像比較)は固定場面 + `tags: ["vrt"]` を付けた主要部品の代表ストーリー
377
+ × light/dark だけを撮る(全ストーリーは撮らない)。手順・基準画像の撮り直し・使われなく
378
+ なった基準画像の検出は `docs/visual-and-docs-checks.md`。横断の視覚監査は
379
+ `.claude/skills/audit-pages/SKILL.md`。
357
380
 
358
381
  **a11y 機械検証について(issue #261):**
359
382
 
@@ -362,7 +385,9 @@ npm run test:forced-colors
362
385
  - color-contrast ルールも有効(全ストーリー対象)。当初はトークン債務のため無効化していたが、
363
386
  AA 未達トークンの darken(Success/Warning-Base・Text-Caution/Warning・Text-Low-Emphasis)と
364
387
  opacity 減衰の廃止で解消済み。無効状態デモ等 WCAG 1.4.3 の inactive 例外だけ、ストーリー側で
365
- 理由コメント付きの rule 除外を許可(`.storybook/preview.ts` 参照)。トークンペアの正本チェックは
388
+ 理由コメント付きの rule 除外を許可(`.storybook/preview.ts` 参照)。除外は
389
+ `test/known-exceptions.json` に理由・issue・見直し期限(reviewBy)付きで登録し、未登録・
390
+ 期限切れ・使われていない登録は `npm run check` と `npm run test:a11y` が落とす。トークンペアの正本チェックは
366
391
  `scripts/check-contrast.mjs`(`npm run check` 経由)。
367
392
  - rules.json の `accessibility.requirements` に `machineVerified` / `verifiedBy` を追加済み。
368
393
  axe でカバーできない項目(フォーカスリング視認・タッチターゲット実測・エラー表示の
package/CLAUDE.md CHANGED
@@ -28,10 +28,10 @@ UI を書く前に必ず確認すること:
28
28
  - [ ] 既存コンポーネントを `src/components/COMPONENT_LOOKUP.md` で確認したか(手書き・再定義は禁止)
29
29
  - [ ] 色は semantic token(`var(--Surface-*)` / `var(--Brand-Primary)` 等)か。Tailwind標準色・生 `#hex` は禁止
30
30
  - [ ] `border` は色を併記したか(`border-[var(--Border-Low-Emphasis)]` 等)。Tailwind v4 では無色 border は currentColor になり、消費側の濃色テキストで黒ずむ(preset.css の base layer が保険だが明示が原則)
31
- - [ ] **文脈非依存**か(テキスト要素に `text-[var(--Text-*)]`、サーフェス/オーバーレイに `bg-[var(--Surface-*)]` を明示)。親の継承や currentColor に頼ると消費側の色文脈で崩れる。Storybook ツールバーの **Hostile ctx** を loud にして、文字/アイコンがマゼンタ化・背景が透けないか確認する
31
+ - [ ] **文脈非依存**か(テキスト要素に `text-[var(--Text-High-Emphasis)]`、サーフェス/オーバーレイに `bg-[var(--Surface-Primary)]` を明示)。親の継承や currentColor に頼ると消費側の色文脈で崩れる。Storybook ツールバーの **Hostile ctx** を loud にして、文字/アイコンがマゼンタ化・背景が透けないか確認する
32
32
  - [ ] typography は `typo-*` クラスか(`font-bold` 等の直書きは禁止)
33
- - [ ] アニメーションは `duration-[var(--Motion-Duration-*)]` / `ease-[var(--Motion-Easing-*)]` か(`duration-200` や生 `cubic-bezier` の直書きは禁止。トークン参照でないと `prefers-reduced-motion` の一括制御から漏れる)
34
- - [ ] 重なり順は `z-[var(--Z-*)]` か(`z-50` 一律だと Portal のマウント順で勝敗が決まる。`z-10` / `z-20` のコンポーネント内部の重なりは対象外。順序は DESIGN.md の Layering 節)
33
+ - [ ] アニメーションは Motion トークン参照か(例: `duration-[var(--Motion-Duration-Base)]` / `ease-[var(--Motion-Easing-Standard)]`)(`duration-200` や生 `cubic-bezier` の直書きは禁止。トークン参照でないと `prefers-reduced-motion` の一括制御から漏れる)
34
+ - [ ] 重なり順は用途に合うトークン(例: `z-[var(--Z-Modal)]` / `z-[var(--Z-Popover)]`)か(`z-50` 一律だと Portal のマウント順で勝敗が決まる。`z-10` / `z-20` のコンポーネント内部の重なりは対象外。順序は DESIGN.md の Layering 節)
35
35
  - [ ] アイコンは `iconsax-reactjs` か(`lucide-react` / `heroicons` は使わない)
36
36
  - [ ] **選んだアイコンの「絵」を実際に見たか**。iconsax は名前と絵が一致しないものがある。`Check` は小切手(checkbook)でチェックマークではない(PR #349 で出荷直前まで残った)。チェックマークは `TickSquare` / `TickCircle`。また **"i" のグリフは iconsax に存在せず**、`InfoCircle` / `Information` / `Danger` / `Warning2` はすべて感嘆符「!」の器違いなので、情報アイコンは DS 自前の `InfoCircleIcon` を使う。名前で選んだら描画結果を目視し、意味が変わると困る箇所は path で固定するテストを書く <!-- docs-drift-ignore: Check TickSquare TickCircle InfoCircle Information Danger Warning2 InfoCircleIcon -->
37
37
  - [ ] 生タグ(`<button>` / `<input>` / `<a href>`)でなく DS コンポーネントを使ったか
@@ -159,6 +159,21 @@ afterEach フックが全ストーリー(tags フィルタなし)に対し
159
159
  (AA 未達トークンの darken と opacity 減衰の廃止により全ストーリー通過。
160
160
  トークンペアの正本チェックは `scripts/check-contrast.mjs`)。CI では常時実行される。
161
161
 
162
+ **WebKit(Safari 系)でも実行できる:**
163
+
164
+ ```bash
165
+ npx playwright install webkit # 初回のみ
166
+ npm run test:interaction:webkit
167
+ npm run test:a11y:webkit
168
+ ```
169
+
170
+ 同じ設定(`vitest.storybook.config.ts` / `vitest.a11y.config.ts`)を環境変数
171
+ `KSK_BROWSER=webkit` で WebKit に切り替えて回す。Chromium では見えない差
172
+ (フォーカス・スクロール・描画差)を拾う。CI では専用ジョブ `webkit` で Chromium 側と
173
+ 並列に常時実行される。現状は全ストーリーが WebKit でも通っており、**除外している
174
+ ストーリー・検査はない**(WebKit 固有の制限で除外が必要になったら、理由付きでここに一覧を残す)。
175
+ ボタン・リンク・Select 等のフォーカス/キーボード操作を触ったときは WebKit 側も回すこと。
176
+
162
177
  **Button など、フォーカス表示の `outline-none` / `outline-hidden` 系クラスを触ったら `npm run test:forced-colors` も実行:**
163
178
 
164
179
  ```bash
@@ -287,6 +302,9 @@ npm run storybook
287
302
  # ビルド
288
303
  npm run build-storybook
289
304
 
305
+ # 初回のみ: mcp-server のテスト(npm test に含まれる)が MCP SDK を使うため依存を入れる
306
+ npm ci --prefix mcp-server
307
+
290
308
  # スクラッチ検出(実装後に必ず実行)
291
309
  bash scripts/lint-scratch.sh
292
310
 
@@ -302,6 +320,9 @@ npm run lint:design
302
320
  # 非推奨 API 台帳の整合検査(台帳 ⇔ 実ソースの @deprecated JSDoc)
303
321
  npm run lint:deprecations
304
322
 
323
+ # 未定義 CSS 変数の参照検査(変数名のタイプミス検出。実行時注入の変数は scripts/check-css-vars.mjs の ALLOWLIST)
324
+ npm run lint:css-vars
325
+
305
326
  # `Closes #N` 誤リンク監査(gh 必須。npm run check には入っていない)
306
327
  npm run audit:issue-links -- --recent 60
307
328
 
@@ -352,8 +373,10 @@ npm run test:forced-colors
352
373
  v1.48.x で連続した種類の不具合をここで落とす。
353
374
  - **play 関数のあるコンポーネント(Button / Dialog / AlertDialog / Sheet / Select /
354
375
  DropdownMenu / Combobox / Tabs / Form / Toast)を触ったら `npm run test:interaction` を回すこと。**
355
- - ビジュアル回帰(スクリーンショット差分)は未導入。`.claude/skills/audit-pages/SKILL.md` による
356
- 手動の視覚監査が現状の代替。
376
+ - ビジュアル回帰(画像比較)は固定場面 + `tags: ["vrt"]` を付けた主要部品の代表ストーリー
377
+ × light/dark だけを撮る(全ストーリーは撮らない)。手順・基準画像の撮り直し・使われなく
378
+ なった基準画像の検出は `docs/visual-and-docs-checks.md`。横断の視覚監査は
379
+ `.claude/skills/audit-pages/SKILL.md`。
357
380
 
358
381
  **a11y 機械検証について(issue #261):**
359
382
 
@@ -362,7 +385,9 @@ npm run test:forced-colors
362
385
  - color-contrast ルールも有効(全ストーリー対象)。当初はトークン債務のため無効化していたが、
363
386
  AA 未達トークンの darken(Success/Warning-Base・Text-Caution/Warning・Text-Low-Emphasis)と
364
387
  opacity 減衰の廃止で解消済み。無効状態デモ等 WCAG 1.4.3 の inactive 例外だけ、ストーリー側で
365
- 理由コメント付きの rule 除外を許可(`.storybook/preview.ts` 参照)。トークンペアの正本チェックは
388
+ 理由コメント付きの rule 除外を許可(`.storybook/preview.ts` 参照)。除外は
389
+ `test/known-exceptions.json` に理由・issue・見直し期限(reviewBy)付きで登録し、未登録・
390
+ 期限切れ・使われていない登録は `npm run check` と `npm run test:a11y` が落とす。トークンペアの正本チェックは
366
391
  `scripts/check-contrast.mjs`(`npm run check` 経由)。
367
392
  - rules.json の `accessibility.requirements` に `machineVerified` / `verifiedBy` を追加済み。
368
393
  axe でカバーできない項目(フォーカスリング視認・タッチターゲット実測・エラー表示の
package/DESIGN.md CHANGED
@@ -145,6 +145,29 @@ KSK の必須正本・publish 依存にせず、KSK 固有の multi-theme / nati
145
145
  - **Dark mode**: `.dark` で Semantic 層が自動反転(コンポーネント側の変更不要)。テーマ差し替え(Brand)と
146
146
  light/dark は**直交2軸**。dark の semantic 値は `src/styles/semantic.css` の `.dark` が実装で、`tokens.json` の
147
147
  `colors.semanticDark` に機械可読ミラーを持つ(契約テストで同期を保証)。
148
+ - **操作部品のコントラスト(issue #636)**: 入力・選択の枠、スイッチ(オフ)の地、スライダーの溝は、
149
+ 通常の light/dark から WCAG 1.4.11(非テキスト 3:1)を満たす専用ロールを使う。
150
+ 入力系の枠は `var(--Border-Control, var(--Border-Medium-Emphasis))`、スイッチ(オフ)の地は
151
+ `var(--Surface-Switch-Off, var(--Surface-Quaternary))`、hover は `--Surface-Switch-Off-Hover`、
152
+ スライダーの溝は `var(--Surface-Slider-Track, var(--Surface-Tertiary))` を参照する。
153
+ 専用ロールは `:root` / `.dark` に宣言済み。`prefers-contrast: more` と `data-contrast="more"` でも
154
+ 同じ基準値を維持する。フォールバックは専用ロールが無い旧プリセットとの互換用。
155
+ 対象は Input / Textarea / Select / Checkbox / Radio / Combobox / MultiSelect / DatePicker / TimePicker /
156
+ NumberInput / SearchBar / TagInput / CheckboxCard / Chip(outline)/ FileUpload / ImageUploader /
157
+ ChartControls / QuantitySelector / StarRating(入力用)/ Switch(オフ)/ Slider の溝。
158
+ 非操作の区切り線・表示用の枠の `--Border-Medium-Emphasis` は従来の色を維持する。
159
+ consumer が色を変更する場合は専用ロールを上書きし、変更後の背景との 3:1 を再確認する。
160
+
161
+ | 操作部品専用ロール | light | dark |
162
+ |---|---|---|
163
+ | 枠 `--Border-Control` | Gray-500(4.83:1) | White-Alpha-500 を Gray-900 へ合成(3.33:1) |
164
+ | オフ地 `--Surface-Switch-Off` / 溝 `--Surface-Slider-Track` | Gray-500(4.83:1) | Gray-500(3.67:1) |
165
+ | オフ hover `--Surface-Switch-Off-Hover` | Gray-600(7.73:1) | Gray-400(6.91:1) |
166
+
167
+ 値の正本は `src/styles/semantic.css` と `tokens.json` の `colors.semantic` / `colors.semanticDark`。
168
+ `colors.semanticContrastMore` は高コントラスト指定時の同値ミラーで、契約テストが同期を保証する。
169
+ `scripts/check-contrast.mjs` が通常表示・高コントラスト指定とも例外なしで 3:1 を必須にする。
170
+ dark の枠は hover(`--Border-High-Emphasis`)と同値のため、フォーカスの枠・リングで状態を示す。
148
171
  - **Categorical(質的)**: `var(--Categorical-{1..16})` + `-Subtle`(背景) / `-Bold`(文字)。
149
172
  カテゴリの色分け専用でテーマ非依存。WCAG/色覚多様性を検証済み。文字には必ず `-Bold`。
150
173
 
package/MIGRATION.md CHANGED
@@ -39,6 +39,18 @@ npx ksk-ds check-migration ./src
39
39
 
40
40
  ---
41
41
 
42
+ ## v2.10.0
43
+
44
+ APIの削除・改名はありません。次の表示・操作の既定値変更は、取り込み時に利用画面で確認してください。
45
+
46
+ - Webの操作枠は `--Border-Control`、Switchオフ地は `--Surface-Switch-Off` / `--Surface-Switch-Off-Hover`、Slider溝は `--Surface-Slider-Track` を通常表示から宣言します。従来の `--Border-Medium-Emphasis` / `--Surface-Quaternary` 等の上書きでは操作部品を変更しません。独自テーマは専用ロールを上書きし、背景との3:1を再検証してください。非操作区切りは従来どおりです。Native部品本体の既存枠色を一括変更するリリースではありません。
47
+ - AlertDialogは既定でCancelにフォーカスし、閉じると開く前の要素へ戻ります。画面独自の移動が必要なら `onOpenAutoFocus` / `onCloseAutoFocus` で `preventDefault()`、自動復帰の無効化は `restoreFocusOnClose={false}` を使ってください。
48
+ - DataTableRowのhover背景は操作可能な行だけに付き、選択行の背景はhover中も維持します。Chipのhover背景とAlert成功枠も変更されます。
49
+ - Shadow DOMの対応範囲とRadix由来の制約は [利用上の注意](https://github.com/ekusiek716/ksk-design-system/blob/main/docs/shadow-dom.md) を参照してください。ToastをShadow内に描画するときはProvider配下にToasterを明示してください。
50
+ - CLIのP052/P053検査は部品の形の上書き・`@layer`外のグローバルCSSを新たに検出します。既存コードで検出された場合は正本ルールと例外の理由を確認してください。
51
+
52
+ ---
53
+
42
54
  ## v2.0.0
43
55
 
44
56
  **破壊変更は peerDependencies の要件変更 1 点のみ。** 識別子 / prop の削除・rename は
package/README.md CHANGED
@@ -299,6 +299,67 @@ Footer を別コンポーネントに切り出した構成、preset 経路(`mo
299
299
  対象外です。P046 と同じく TypeScript の構文解析を使うため、`typescript` が無いプロジェクトでは
300
300
  P051 だけ skip して案内を1行出します。
301
301
 
302
+ #### P052: DS 部品の `className` で形(角丸・高さ・左右 padding)を上書きしない
303
+
304
+ `Button` / `Input` / `Textarea` / `SelectTrigger` / `DateField` / `Chip` / `Skeleton` などの角丸・高さ・
305
+ 左右 padding は DS が token(`--Control-Radius` / `--Field-Radius` / `--Field-Height-*` /
306
+ `--Field-Padding-X-*`)と prop(`size` / `rounded` / `unstyled` / `startAdornment`)で持っています。
307
+ 部品ごとに `rounded-2xl` / `h-12` / `px-4` を被せると、同じ画面の他の部品と形が揃わなくなるため
308
+ P052 が warn します(belle-todo で、ボタンが pill と角丸長方形で混在し、入力欄も欄ごとに角丸・高さが
309
+ ずれていた事例が由来)。
310
+
311
+ ```tsx
312
+ // ❌ 部品単位で形を被せる
313
+ <Button className="w-full rounded-2xl">保存</Button>
314
+ <Input className="h-12 rounded-2xl pl-8" /> // ¥ を自前で絶対配置
315
+
316
+ // ✅ 既定に任せ、変えたいときは product theme で 1 箇所だけ
317
+ <Button className="w-full">保存</Button>
318
+ <Input startAdornment="¥" />
319
+ ```
320
+
321
+ アプリ全体でボタンや入力欄の角丸を変えたいときは、`--Control-Radius` / `--Field-Radius` を
322
+ product theme(P049 の許可リスト)で上書きします。枠なしのインライン編集欄は `unstyled`、
323
+ `Skeleton` の角丸は `rounded` prop を使います。
324
+
325
+ `"ksk-design-system"` から import した部品の `className` に書かれた文字列リテラル(`cn()` / 三項 /
326
+ テンプレートの固定部分を含む)だけを見ます。`unstyled` を付けた部品、`h-auto` / `h-full` / `min-h-0`
327
+ 等のレイアウト用の高さ、変数経由のクラスは対象外です。ボタンをカード状のタイルとして使う等の
328
+ 正当な例外は `// ksk-ds-lint-ignore P052 -- 理由` で外します。P046 / P051 と同じく
329
+ `typescript` が無いプロジェクトでは skip します。
330
+
331
+ #### P053: `@layer` の外のグローバル CSS で DS 部品の形を変えない
332
+
333
+ CSS の仕様上、`@layer` の外に書いた宣言は `@layer` の中の宣言(Tailwind / DS のユーティリティは
334
+ `@layer utilities`)に**詳細度と無関係に勝ちます**。消費側の `globals.css` に layer 外で
335
+ タグ / role / 汎用属性セレクタの寸法・余白・角丸・文字サイズ・outline を書くと、DS 部品の
336
+ `className` では打ち消せず、DS 部品の形が壊れます。belle-todo では次がすべてこれでした。
337
+
338
+ | layer 外のルール | 起きたこと |
339
+ |---|---|
340
+ | `[role="radio"] { min-height: var(--touch-min) }` | DS Chip(32px)が 44〜52px に膨らむ |
341
+ | `:focus-visible { border-radius: 8px }` | pill のボタンがフォーカス中だけ角ばる / DS 部品のフォーカス枠が二重 |
342
+ | `input:focus-visible { border-radius: 16px }` | 入力欄の角丸がフォーカス中だけ変わる |
343
+ | `nav { padding-bottom: env(safe-area-inset-bottom) }` | 浮いたタブバーに safe-area が二重 |
344
+
345
+ ```css
346
+ /* ❌ layer 外 */
347
+ :focus-visible { outline: 2px solid var(--Brand-Primary); }
348
+
349
+ /* ✅ base に置けば DS 部品は DS のフォーカスリングが勝ち、素の要素だけにこの outline が当たる */
350
+ @layer base {
351
+ :focus-visible { outline: 2px solid var(--Brand-Primary); }
352
+ }
353
+
354
+ /* ✅ layer 外に残すなら DS 部品を外す */
355
+ a:not([data-slot]) { min-height: 44px; }
356
+ ```
357
+
358
+ `@layer` / `@theme` の中、クラスで始まるセレクタ(消費側の独自クラス)、`html` / `body` / `:root` /
359
+ `*`、keyframes、`data-slot` を含むセレクタ(DS の公開 data-slot を明示的に狙う・外す)は対象外です。
360
+ DS 自身 / ベンダリングされた DS の CSS も P049 と同じく対象外。例外は
361
+ `/* ksk-ds-lint-ignore P053 -- 理由 */` で外します。
362
+
302
363
  #### ルールごとの除外(rules.json の excludes 系)
303
364
 
304
365
  `contracts/rules.json` の除外指定は、当てる対象ごとに 3 つに分かれています。
@@ -524,6 +585,22 @@ Claude Code をこのリポで開いて:
524
585
 
525
586
  DS コンポーネントを最大限活用したモックが `src/prototypes/` に生成され、`http://localhost:5173` のプレビューに即反映されます。スクラッチで UI を組まず、既存コンポーネントを組み合わせて作るのでブレません。
526
587
 
588
+ ## 🤖 MCP サーバー(AI エージェント向け)
589
+
590
+ `mcp-server/`(`ksk-ds-mcp`)は DS の正本を MCP で配信する。リポジトリを clone した環境で `npm run mcp:build` → `npm run mcp:start`(stdio)。
591
+
592
+ | 種別 | 名前 | 内容 |
593
+ |------|------|------|
594
+ | tool | `get_component` / `get_token` / `check_rule` / `search` | コンポーネント・トークン・禁止ルールの照会と横断検索 |
595
+ | tool | `list_contracts` | 配信している正本の名前・説明・URI の一覧 |
596
+ | tool | `get_contract` | 正本を名前で取得。JSON は `path`(例: `meta.counts`)、Markdown は `section`(見出しの部分一致)で部分取得。大きい契約は目次のみ返し、`full: true` で全文 |
597
+ | resource | `ksk-ds://contracts/<name>` | `contracts/*.json`・`tokens.json`(components / rules / tokens / composition / screen-patterns 等) |
598
+ | resource | `ksk-ds://docs/<name>` | `DESIGN.md`・`COMPONENT_LOOKUP.md`(web / native) |
599
+
600
+ - resources を読めないクライアントでも `get_contract` で同じ内容を取得できる。
601
+ - 配信するのは npm パッケージ(`package.json` の `files`)に同梱されるファイルだけ。新しい `contracts/*.json` を足したら `mcp-server/src/tools/get-contract.ts` の一覧にも追加する(`mcp-server/__tests__/get-contract.test.ts` が登録漏れと同梱漏れを検出する)。
602
+ - テストは MCP SDK のクライアントを使うため、先に `npm ci --prefix mcp-server` が要る。
603
+
527
604
  ## 📖 ドキュメント
528
605
 
529
606
  - **ライブ Storybook**: https://ksk-design-system.vercel.app — 全コンポーネントのバリアント・テーマ切り替えを操作可能
package/RELEASE.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  `ksk-design-system` のリリーススケジュール・運用ルール。
4
4
 
5
+ ## v2.10.0(2026-10-16)
6
+
7
+ 2.9.0以降の未公開13コミットをまとめたminorリリース。既存APIの削除はなく、操作部品の見た目とフォーカスの既定動作を改善しています。
8
+
9
+ - Buttonの処理中表示、`useConfirm`、Dialog/Sheetの幅指定、ルーターLinkへの差し替えなど、既存コードと併用できるAPIを追加しました(#638・#642)。
10
+ - 入力・選択部品の枠、オフのSwitch、Sliderの溝を通常light/darkでも背景との3:1以上へ改善しました。区切り線など非操作要素の色は維持します(#636・#645・#662)。
11
+ - キーボード操作、確認ダイアログの初期フォーカスと復帰、Shadow DOM内のフォーカス検出、選択行hover、Chipのdark hover、成功Alertの色を修正しました(#640・#662)。
12
+ - Portal/Toastの描画先、NumberInputの入力属性、ErrorBoundaryのresetKeys、NavigationBarの操作属性、データ表のラベル指定などconsumer APIの不足を補いました(#662)。
13
+ - リポジトリをcloneした開発環境向けのMCP正本参照(DSのnpm配布物には含めない)、未定義CSS変数・形の上書き・グローバルCSSの検査を追加し、実測レイアウト修正、WebKit検証、画像比較、AI向けルールの配布検査を強化しました(#635・#637・#639・#641・#643・#644)。
14
+ - consumer更新ツールは依存指定の完全固定・`^`・`~`を維持します。公開通知台帳への修正PR登録も含みます(#633・#634)。
15
+
16
+ 導入時は [2.10.0の注意](./MIGRATION.md#v2100) を確認してください。npm公開・タグ・GitHub Releaseはversion変更のmainマージ後に `publish.yml` が実行します。consumerへの取り込みとアプリ配布は別工程で、今回のDS公開準備には含めません。独立したMCPサーバーパッケージのnpm公開も今回の対象外です。
17
+
5
18
  ## 通常リリースサイクル
6
19
 
7
20
  | 種類 | 頻度 | 曜日 |
@@ -0,0 +1,149 @@
1
+ import ts from "typescript"
2
+
3
+ /**
4
+ * P052: DS 部品の className で「形」(角丸・高さ・左右 padding)を上書きしない
5
+ *
6
+ * 由来: belle-todo(2026-10-01)。Button に rounded-2xl / rounded-lg を部品ごとに
7
+ * 被せた結果、同じ画面でボタンの角丸が pill と角丸長方形で混在し、入力欄も
8
+ * TextInput / DateField だけ rounded-2xl で他の欄とずれていた。形は DS が
9
+ * token(--Control-Radius / --Field-Radius / --Field-Height-* / --Field-Padding-X-*)と
10
+ * prop(size / rounded / unstyled / startAdornment)で持っているので、部品単位で被せると
11
+ * 必ず他の部品と揃わなくなる。アプリ全体で変えたいなら product theme override API で
12
+ * 1 箇所だけ変える。
13
+ *
14
+ * 検査範囲(誤検知を出さない方を優先):
15
+ * - "ksk-design-system"(サブパス含む)から import した部品だけを見る。DS 内部の相対 import や
16
+ * consumer 自作の同名部品は対象外
17
+ * - className 属性の中の文字列リテラル(cn() / 三項 / テンプレートの固定部分を含む)から
18
+ * クラスを拾う。変数経由で渡すクラスや実行時に組み立てる文字列は追えない
19
+ * - `unstyled` を付けた部品は DS の見た目を出力しないので対象外
20
+ * - h-auto / h-full / min-h-0 等のレイアウト用の高さ指定は許可
21
+ */
22
+
23
+ /** 部品 → 検査するクラスの種類(radius: rounded-* / height: h-* min-h-* size-* / padX: px-* pl-* pr-*) */
24
+ export const SHAPE_TARGETS = {
25
+ Button: { radius: true, height: true },
26
+ Input: { radius: true, height: true, padX: true },
27
+ Textarea: { radius: true, padX: true },
28
+ AutoGrowTextarea: { radius: true, padX: true },
29
+ CommitInput: { radius: true, height: true, padX: true },
30
+ CommitTextarea: { radius: true, padX: true },
31
+ CommitAutoGrowTextarea: { radius: true, padX: true },
32
+ SelectTrigger: { radius: true, height: true, padX: true },
33
+ DateField: { radius: true, height: true, padX: true },
34
+ DatePicker: { radius: true, height: true, padX: true },
35
+ TimePicker: { radius: true, height: true, padX: true },
36
+ Combobox: { radius: true, height: true, padX: true },
37
+ Chip: { radius: true, height: true },
38
+ ChipSelector: { radius: true },
39
+ SettingsListRow: { radius: true, height: true },
40
+ // Skeleton は rounded prop(sm / lg / 2xl / full)を持つので className で渡さない
41
+ Skeleton: { radius: true, hint: 'rounded prop を使う(例: <Skeleton rounded="2xl" />)' },
42
+ }
43
+
44
+ const DS_MODULE_RE = /^(?:ksk-design-system|@ksk\/design-system)(?:\/.*)?$/
45
+ const VARIANT_PREFIX = String.raw`(?:[a-z0-9-]+:|\[[^\]]*\]:)*`
46
+ const RADIUS_RE = new RegExp(`^${VARIANT_PREFIX}rounded(?:-[trblxyse]{1,2})?(?:-[a-z0-9]+|-\\[[^\\]]+\\])?$`)
47
+ const HEIGHT_RE = new RegExp(`^${VARIANT_PREFIX}(?:h|min-h|size)-\\S+$`)
48
+ const LAYOUT_HEIGHT_RE = new RegExp(`^${VARIANT_PREFIX}(?:h|min-h|size)-(?:auto|full|screen|fit|min|max|dvh|svh|lvh|0)$`)
49
+ const PAD_X_RE = new RegExp(`^${VARIANT_PREFIX}p[xlr]-\\S+$`)
50
+
51
+ export function classifyShapeClass(token, rule) {
52
+ if (rule.radius && RADIUS_RE.test(token)) return true
53
+ if (rule.height && HEIGHT_RE.test(token) && !LAYOUT_HEIGHT_RE.test(token)) return true
54
+ if (rule.padX && PAD_X_RE.test(token)) return true
55
+ return false
56
+ }
57
+
58
+ /** import { Button as Btn } from "ksk-design-system" → Map("Btn" => "Button") */
59
+ function collectDsImports(sourceFile) {
60
+ const local = new Map()
61
+ for (const statement of sourceFile.statements) {
62
+ if (!ts.isImportDeclaration(statement)) continue
63
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue
64
+ if (!DS_MODULE_RE.test(statement.moduleSpecifier.text)) continue
65
+ const bindings = statement.importClause?.namedBindings
66
+ if (!bindings || !ts.isNamedImports(bindings)) continue
67
+ for (const element of bindings.elements) {
68
+ if (element.isTypeOnly) continue
69
+ const imported = (element.propertyName ?? element.name).text
70
+ if (Object.hasOwn(SHAPE_TARGETS, imported)) local.set(element.name.text, imported)
71
+ }
72
+ }
73
+ return local
74
+ }
75
+
76
+ function attributeOf(opening, name) {
77
+ return opening.attributes.properties.find(
78
+ (property) => ts.isJsxAttribute(property) && property.name.getText() === name,
79
+ )
80
+ }
81
+
82
+ /** `unstyled` / `unstyled={true}` なら true。`unstyled={cond}` 等の式は判定できないので true 扱い(誤検知回避) */
83
+ function isUnstyled(opening) {
84
+ const attribute = attributeOf(opening, "unstyled")
85
+ if (!attribute) return false
86
+ const init = attribute.initializer
87
+ if (!init) return true
88
+ if (ts.isJsxExpression(init) && init.expression?.kind === ts.SyntaxKind.FalseKeyword) return false
89
+ return true
90
+ }
91
+
92
+ /** className の式から文字列リテラルの中身だけを集める */
93
+ function collectClassTokens(attribute) {
94
+ const texts = []
95
+ function visit(node) {
96
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
97
+ texts.push(node.text)
98
+ return
99
+ }
100
+ if (ts.isTemplateExpression(node)) {
101
+ texts.push(node.head.text)
102
+ for (const span of node.templateSpans) {
103
+ visit(span.expression)
104
+ texts.push(span.literal.text)
105
+ }
106
+ return
107
+ }
108
+ ts.forEachChild(node, visit)
109
+ }
110
+ const init = attribute.initializer
111
+ if (!init) return []
112
+ if (ts.isStringLiteral(init)) texts.push(init.text)
113
+ else if (ts.isJsxExpression(init) && init.expression) visit(init.expression)
114
+ return texts.join(" ").split(/\s+/).filter(Boolean)
115
+ }
116
+
117
+ export function inspectDsShapeOverrides(source, filePath = "source.tsx") {
118
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
119
+ const local = collectDsImports(sourceFile)
120
+ const findings = []
121
+ if (local.size === 0) return findings
122
+
123
+ function check(opening) {
124
+ if (!ts.isIdentifier(opening.tagName)) return
125
+ const component = local.get(opening.tagName.text)
126
+ if (!component) return
127
+ if (isUnstyled(opening)) return
128
+ const classAttribute = attributeOf(opening, "className")
129
+ if (!classAttribute) return
130
+ const rule = SHAPE_TARGETS[component]
131
+ const classes = collectClassTokens(classAttribute).filter((token) => classifyShapeClass(token, rule))
132
+ if (classes.length === 0) return
133
+ const position = sourceFile.getLineAndCharacterOfPosition(opening.getStart(sourceFile))
134
+ findings.push({
135
+ line: position.line + 1,
136
+ tag: component,
137
+ classes,
138
+ detail: `<${component}> ${classes.join(" ")}${rule.hint ? `(${rule.hint})` : ""}`,
139
+ })
140
+ }
141
+
142
+ function walk(node) {
143
+ if (ts.isJsxElement(node)) check(node.openingElement)
144
+ else if (ts.isJsxSelfClosingElement(node)) check(node)
145
+ ts.forEachChild(node, walk)
146
+ }
147
+ walk(sourceFile)
148
+ return findings
149
+ }
package/bin/lint.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  inspectProductThemeOverrides,
7
7
  loadProductThemeContract,
8
8
  } from "./product-theme-override.js"
9
+ import { inspectUnlayeredGlobalOverrides } from "./unlayered-global-override.js"
9
10
 
10
11
  // card-child-spacing.js / overlay-footer-padding.js は `import ts from "typescript"` している
11
12
  // (P046 / P051 専用エンジン)。typescript は 24MB 前後あり、AST が要るのはこの2ルールだけなので、
@@ -40,6 +41,11 @@ const TS_ENGINES = {
40
41
  exportName: "inspectOverlayFooterPadding",
41
42
  label: "P051(ResponsiveOverlayFrame + Footer の padding={false} 付け忘れ)",
42
43
  },
44
+ "ds-shape-override": {
45
+ module: "./ds-shape-override.js",
46
+ exportName: "inspectDsShapeOverrides",
47
+ label: "P052(DS 部品の className での角丸・高さ・左右 padding の上書き)",
48
+ },
43
49
  }
44
50
 
45
51
  const DEFAULT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"])
@@ -381,7 +387,8 @@ export async function runLintCli(argv, { cwd = process.cwd(), pkgRoot = resolve(
381
387
  // CSS 専用エンジン一覧。P049(product-theme-override)と P050(parallel-palette、
382
388
  // issue #393)はどちらも .css だけを対象にした独自エンジンで、TSX 向けの
383
389
  // 正規表現エンジンとは別の走査経路(lintCssFile)を通す。
384
- const CSS_ENGINES = new Set(["product-theme-override", "parallel-palette"])
390
+ // P053(unlayered-global-override)も .css だけを見る独自エンジン。
391
+ const CSS_ENGINES = new Set(["product-theme-override", "parallel-palette", "unlayered-global-override"])
385
392
  const rules = loadRules(rulesPath)
386
393
  const cssRules = rules.filter((rule) => CSS_ENGINES.has(rule.engine))
387
394
  let sourceRules = rules.filter((rule) => !CSS_ENGINES.has(rule.engine))
@@ -735,6 +742,18 @@ function lintCssFile(file, cwd, cssRules, contract, dsCssIdentity = null, pkgRoo
735
742
  // capability タグを含まないので、この判定でも従来どおり P049/P050 が当たる。
736
743
  if (!ruleAppliesTo(rule, { capabilities: CSS_CAPABILITIES, filePath: rel })) continue
737
744
 
745
+ if (rule.engine === "unlayered-global-override") {
746
+ for (const violation of inspectUnlayeredGlobalOverrides(source)) {
747
+ if (matchesRuleExclude(rule, { file: rel, line: violation.selector, isDsFile })) continue
748
+ if (ignores.suppresses(rule.id ?? "UNKNOWN", violation.line)) continue
749
+ findings.push({
750
+ ...toFinding(rule, rel, violation.line, "web"),
751
+ message: `${rule.message ?? "@layer 外のグローバル CSS が DS 部品の形を変えています"}: ${violation.selector} { ${violation.property} }`,
752
+ })
753
+ }
754
+ continue
755
+ }
756
+
738
757
  if (rule.engine === "parallel-palette") {
739
758
  // contract が読めない(DS の名前空間一覧が無い)と何が DS 名前空間かを
740
759
  // 判定できず false positive しか出せないので、その場合は skip する。
@@ -808,7 +827,9 @@ function lintFile(file, cwd, rules, options = {}) {
808
827
  const line = lines[finding.line - 1] ?? ""
809
828
  if (ignores.suppresses(rule.id ?? "UNKNOWN", finding.line)) continue
810
829
  if (!matchesRuleExclude(rule, { file: rel, line, isDsFile })) {
811
- findings.push(toFinding(rule, rel, finding.line, platform))
830
+ const base = toFinding(rule, rel, finding.line, platform)
831
+ // エンジンが該当箇所の具体(どの部品のどのクラスか等)を返すときはメッセージに添える(P052)
832
+ findings.push(finding.detail ? { ...base, message: `${base.message}: ${finding.detail}` } : base)
812
833
  }
813
834
  }
814
835
  continue
@@ -0,0 +1,96 @@
1
+ /**
2
+ * P053: @layer の外で、タグ / role / 汎用属性セレクタに寸法・余白・角丸・文字サイズ・outline を当てない
3
+ *
4
+ * 由来: belle-todo(2026-10-01)。消費側の globals.css に @layer 無しで書いた
5
+ * `[role="radio"] { min-height: 44〜52px }` → DS Chip(32px)が見た目ごと膨らむ
6
+ * `:focus-visible { outline; border-radius: 8px }` → pill のボタンがフォーカス中だけ角ばる・
7
+ * DS 部品でフォーカス枠が二重になる
8
+ * `input:focus-visible { border-radius: 16px }` → 入力欄の角丸がフォーカス中だけ変わる
9
+ * `nav { padding-bottom: env(safe-area-inset-bottom) }` → 浮いたタブバーに safe-area が二重
10
+ * がいずれも DS の見た目を壊した。CSS の仕様上、layer 外の宣言は @layer の中の宣言
11
+ * (Tailwind / DS のユーティリティは @layer utilities)に詳細度と無関係に勝つため、
12
+ * DS 部品の className では打ち消せない。
13
+ *
14
+ * 報告しないもの(誤検知を出さない方を優先):
15
+ * - @layer の中(base に置けば DS のユーティリティが勝つ)・@theme の中
16
+ * - クラスで始まるセレクタ(消費側の独自クラス)、html / body / :root / *、keyframes
17
+ * - data-slot を含むセレクタ(DS の公開 data-slot を明示的に狙う・:not([data-slot]) で外すのは意図的)
18
+ */
19
+
20
+ const PROPS = ["min-height", "height", "padding", "border-radius", "font-size", "line-height", "outline"]
21
+ // 接尾辞 (-bottom / -left 等) は全候補に掛ける (padding-bottom / outline-offset 等)
22
+ const PROP_RE = new RegExp(String.raw`(?:^|[;{\s])((?:${PROPS.join("|")})(?:-[a-z]+)?)\s*:`)
23
+
24
+ /** コメントを同じ長さの空白に置き換える(行番号を保つ) */
25
+ function maskCssComments(css) {
26
+ return css.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, " "))
27
+ }
28
+
29
+ /** セレクタ群をカンマで分ける。:is(a, button) / :not(...) / [a="b,c"] の中のカンマでは分けない */
30
+ export function splitSelectors(prelude) {
31
+ const out = []
32
+ let depth = 0
33
+ let current = ""
34
+ for (const ch of prelude) {
35
+ if (ch === "(" || ch === "[") depth++
36
+ else if (ch === ")" || ch === "]") depth--
37
+ if (ch === "," && depth === 0) {
38
+ out.push(current)
39
+ current = ""
40
+ continue
41
+ }
42
+ current += ch
43
+ }
44
+ out.push(current)
45
+ return out.map((selector) => selector.trim()).filter(Boolean)
46
+ }
47
+
48
+ /** DS 部品にも当たりうるセレクタか */
49
+ export function isBroadSelector(selector) {
50
+ if (/^(?:html|body|:root|::selection|\*)(?![\w-])/.test(selector)) return false
51
+ if (/^\./.test(selector)) return false
52
+ if (/^(?:\d+(?:\.\d+)?%|from|to)$/.test(selector)) return false
53
+ if (/data-slot/.test(selector)) return false
54
+ return true
55
+ }
56
+
57
+ /**
58
+ * @returns {{ line: number, selector: string, property: string }[]}
59
+ */
60
+ export function inspectUnlayeredGlobalOverrides(source) {
61
+ const css = maskCssComments(source)
62
+ const findings = []
63
+ const stack = []
64
+ let segmentStart = 0
65
+ for (let index = 0; index < css.length; index++) {
66
+ const ch = css[index]
67
+ if (ch === "{") {
68
+ const prelude = css.slice(segmentStart, index)
69
+ const trimmed = prelude.trim()
70
+ const kind = /^@layer\b/.test(trimmed) ? "layer" : /^@theme\b/.test(trimmed) ? "theme" : /^@/.test(trimmed) ? "at" : "rule"
71
+ // 行番号はセレクタの先頭(前置の空白・改行を除いた位置)で数える
72
+ const leading = prelude.length - prelude.trimStart().length
73
+ stack.push({ kind, prelude: trimmed, bodyStart: index + 1, selectorStart: segmentStart + leading })
74
+ segmentStart = index + 1
75
+ } else if (ch === "}") {
76
+ const block = stack.pop()
77
+ if (block && block.kind === "rule") {
78
+ const insideLayer = stack.some((entry) => entry.kind === "layer" || entry.kind === "theme")
79
+ const body = css.slice(block.bodyStart, index)
80
+ const match = !insideLayer && body.match(PROP_RE)
81
+ if (match) {
82
+ const broad = splitSelectors(block.prelude).filter(isBroadSelector)
83
+ if (broad.length > 0) {
84
+ const line = css.slice(0, block.selectorStart).split("\n").length
85
+ findings.push({ line, selector: broad.join(", "), property: match[1] })
86
+ }
87
+ }
88
+ }
89
+ segmentStart = index + 1
90
+ } else if (ch === ";" && stack.length === 0) {
91
+ // @import / @charset 等の文
92
+ segmentStart = index + 1
93
+ }
94
+ }
95
+ return findings
96
+ }