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.
- package/.claude/skills/ksk-design-system/SKILL.md +153 -0
- package/.claude/skills/ksk-design-system/references/component-decisions.md +87 -0
- package/.claude/skills/ksk-design-system/references/hig-m3-mapping.md +92 -0
- package/.claude/skills/ksk-design-system/references/prohibited-rationale.md +148 -0
- package/.claude/skills/ksk-design-system/references/tokens-reference.md +79 -0
- package/AGENTS.md +31 -6
- package/CLAUDE.md +31 -6
- package/DESIGN.md +23 -0
- package/MIGRATION.md +12 -0
- package/README.md +77 -0
- package/RELEASE.md +13 -0
- package/bin/ds-shape-override.js +149 -0
- package/bin/lint.js +23 -2
- package/bin/unlayered-global-override.js +96 -0
- package/contracts/components.json +126 -29
- package/contracts/consumer-requests.json +2 -2
- package/contracts/product-theme-overrides.json +4 -0
- package/contracts/rules.json +25 -1
- package/contracts/token-hex-cache.json +14 -1
- package/dist/index.js +2540 -2243
- package/dist/native/ui.js +5 -1
- package/dist/{native-BHDWkOgd.js → native-BQg-H087.js} +72 -24
- package/dist/native.js +1 -1
- package/dist/types/components/patterns/admin/bulk-actions.d.ts +3 -1
- package/dist/types/components/patterns/admin/data-table.d.ts +3 -1
- package/dist/types/components/patterns/confirm-dialog.d.ts +43 -3
- package/dist/types/components/patterns/sidebar-nav.d.ts +30 -1
- package/dist/types/components/ui/alert-dialog.d.ts +3 -1
- package/dist/types/components/ui/breadcrumb.d.ts +11 -1
- package/dist/types/components/ui/button.d.ts +29 -1
- package/dist/types/components/ui/checkbox-card.d.ts +1 -1
- package/dist/types/components/ui/combobox.d.ts +7 -1
- package/dist/types/components/ui/dialog.d.ts +27 -2
- package/dist/types/components/ui/error-boundary.d.ts +7 -0
- package/dist/types/components/ui/multi-select.d.ts +7 -1
- package/dist/types/components/ui/navigation-bar.d.ts +10 -2
- package/dist/types/components/ui/number-input.d.ts +2 -2
- package/dist/types/components/ui/pagination.d.ts +12 -0
- package/dist/types/components/ui/responsive-dialog.d.ts +1 -1
- package/dist/types/components/ui/select.d.ts +8 -2
- package/dist/types/components/ui/sheet.d.ts +11 -1
- package/dist/types/components/ui/skeleton.d.ts +3 -1
- package/dist/types/components/ui/slider.d.ts +7 -1
- package/dist/types/components/ui/tabs.d.ts +6 -0
- package/dist/types/components/ui/toast.d.ts +4 -1
- package/dist/types/index.d.ts +5 -4
- package/dist/types/lib/active-element.d.ts +2 -0
- package/dist/types/native/components/ErrorBoundary.d.ts +3 -0
- package/dist/types/tokens/native/themes.d.ts +48 -0
- package/package.json +10 -5
- package/src/components/COMPONENT_LOOKUP.md +15 -15
- package/src/preset-core.css +9 -0
- package/src/styles/semantic.css +50 -0
- package/src/styles/source-safelist.css +40 -10
- package/templates/AGENTS.md +2 -2
- package/templates/CLAUDE.md +2 -2
- 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
|
|
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
|
|
34
|
-
- [ ]
|
|
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
|
-
-
|
|
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
|
|
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
|
|
34
|
-
- [ ]
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|