ksk-design-system 2.8.0 → 2.9.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 (45) hide show
  1. package/AGENTS.md +33 -0
  2. package/CLAUDE.md +33 -0
  3. package/DESIGN.md +1 -1
  4. package/PUBLISHING.md +3 -1
  5. package/README.md +14 -0
  6. package/bin/lint.js +45 -24
  7. package/bin/overlay-footer-padding.js +129 -0
  8. package/contracts/components.json +42 -6
  9. package/contracts/consumer-requests.json +5 -0
  10. package/contracts/product-theme-overrides.json +5 -0
  11. package/contracts/rules.json +16 -4
  12. package/contracts/token-hex-cache.json +15 -3
  13. package/dist/class-names.js +1 -1
  14. package/dist/index.js +1965 -1890
  15. package/dist/native/ui.js +60 -28
  16. package/dist/{native-_H0V_r0X.js → native-BHDWkOgd.js} +90 -18
  17. package/dist/native.js +1 -1
  18. package/dist/{server-variants-DTpkG8A7.js → server-variants-C7GNdOYi.js} +28 -3
  19. package/dist/types/components/patterns/admin/data-table.d.ts +1 -1
  20. package/dist/types/components/patterns/chip.d.ts +1 -1
  21. package/dist/types/components/patterns/commerce/bottom-tab-bar.d.ts +4 -1
  22. package/dist/types/components/patterns/commerce/price-display.d.ts +1 -1
  23. package/dist/types/components/patterns/responsive-overlay-frame.d.ts +11 -1
  24. package/dist/types/components/patterns/sidebar-nav.d.ts +59 -0
  25. package/dist/types/components/patterns/tag.d.ts +1 -1
  26. package/dist/types/components/ui/accordion.d.ts +12 -3
  27. package/dist/types/components/ui/badge.d.ts +1 -1
  28. package/dist/types/components/ui/button.d.ts +7 -0
  29. package/dist/types/components/ui/card.d.ts +1 -1
  30. package/dist/types/components/ui/container.d.ts +1 -1
  31. package/dist/types/components/ui/section.d.ts +1 -1
  32. package/dist/types/components/ui/select.d.ts +1 -1
  33. package/dist/types/components/ui/toast.d.ts +1 -1
  34. package/dist/types/index.d.ts +2 -0
  35. package/dist/types/lib/server-variants/button-variants.d.ts +2 -20
  36. package/dist/types/lib/server-variants/unstyled.d.ts +1 -1
  37. package/dist/types/native/components/Alert.d.ts +13 -1
  38. package/dist/types/native/components/PillToggle.d.ts +7 -0
  39. package/dist/types/native/components/ResponsiveDialog.d.ts +6 -1
  40. package/dist/types/tokens/native/themes.d.ts +84 -12
  41. package/package.json +2 -1
  42. package/src/components/COMPONENT_LOOKUP.md +4 -3
  43. package/src/styles/semantic.css +22 -2
  44. package/src/styles/source-safelist.css +13 -5
  45. package/tokens.json +15 -3
package/AGENTS.md CHANGED
@@ -179,6 +179,19 @@ afterEach フックが全ストーリー(tags フィルタなし)に対し
179
179
  (AA 未達トークンの darken と opacity 減衰の廃止により全ストーリー通過。
180
180
  トークンペアの正本チェックは `scripts/check-contrast.mjs`)。CI では常時実行される。
181
181
 
182
+ **Button など、フォーカス表示の `outline-none` / `outline-hidden` 系クラスを触ったら `npm run test:forced-colors` も実行:**
183
+
184
+ ```bash
185
+ npm run test:forced-colors
186
+ ```
187
+
188
+ 強制配色(forced-colors / Windows ハイコントラスト等)モードでのフォーカス表示を
189
+ chromium 実機で検証する(issue #620)。axe-core はフォーカスリングの視認性を
190
+ 検証できないため、CDP で forced-colors を有効化し、`outline-hidden` を使う
191
+ コンポーネント(対象例: Button)がキーボードフォーカス時に outline を出すことを
192
+ `__tests__/browser/` の専用テストで確認する。設定は `vitest.forced-colors.config.ts`。
193
+ CI では常時実行される。
194
+
182
195
  エラーが出た場合は修正してから次に進むこと。
183
196
 
184
197
  ---
@@ -309,6 +322,9 @@ npm run test:a11y
309
322
 
310
323
  # 旧 Radix 固定の回帰テスト(consumer が古い Radix を掴む条件を再現。issue #516)
311
324
  npm run test:legacy-radix
325
+
326
+ # 強制配色モードのフォーカス表示検証(chromium の CDP で forced-colors を再現。issue #620)
327
+ npm run test:forced-colors
312
328
  ```
313
329
 
314
330
  **`Closes #N` 誤リンク監査について(issue #486 が由来):**
@@ -355,6 +371,23 @@ npm run test:legacy-radix
355
371
  `eslint/icon-button-aria-label.js`(`ksk-a11y/icon-button-aria-label`)で lint 時に検出する
356
372
  (`.stories.tsx` は対象外)。
357
373
 
374
+ **forced-colors(強制配色)テストについて(issue #620):**
375
+
376
+ - axe-core はフォーカスリングが実際に見えるかまでは検証しない。DS のフォーカス表示は
377
+ `focus-visible:ring-*`(box-shadow)だが、box-shadow は forced-colors モードで
378
+ 描画されないため、`focus-visible:outline-hidden` を併用し、forced-colors 時だけ
379
+ `outline: 2px solid transparent` を出して OS 側の枠に委ねる(`outline-none` では
380
+ このメディアクエリ自体が生成されず枠が消えたままになる)。
381
+ - `__tests__/browser/` に置いた専用テストが CDP (`Emulation.setEmulatedMedia`) で
382
+ forced-colors を有効化し、Tab フォーカス後の `outlineStyle` が `none` でないことを
383
+ chromium 実機で確認する。設定は `vitest.forced-colors.config.ts`(`vitest.config.ts`
384
+ の既定テストからは `__tests__/browser/**` を除外し、`npm run test:forced-colors` で
385
+ 個別に実行する)。
386
+ - DS コンポーネント本体で `outline-none`(前置バリアント付き含む)の新規追加が
387
+ 無いことは `__tests__/focus-ring-outline-pairing.test.ts` の機械検証(`npm test`)で
388
+ 検出する。consumer 向けの `outline-none` 検出は `contracts/rules.json` P023(`bin/lint.js`)
389
+ と `scripts/lint-scratch.sh` W7 が担う。
390
+
358
391
  **旧 Radix 回帰テストについて(issue #516):**
359
392
 
360
393
  - DS の node_modules は ref churn 修正済みの Radix を解決するため、`npm test` では
package/CLAUDE.md CHANGED
@@ -159,6 +159,19 @@ afterEach フックが全ストーリー(tags フィルタなし)に対し
159
159
  (AA 未達トークンの darken と opacity 減衰の廃止により全ストーリー通過。
160
160
  トークンペアの正本チェックは `scripts/check-contrast.mjs`)。CI では常時実行される。
161
161
 
162
+ **Button など、フォーカス表示の `outline-none` / `outline-hidden` 系クラスを触ったら `npm run test:forced-colors` も実行:**
163
+
164
+ ```bash
165
+ npm run test:forced-colors
166
+ ```
167
+
168
+ 強制配色(forced-colors / Windows ハイコントラスト等)モードでのフォーカス表示を
169
+ chromium 実機で検証する(issue #620)。axe-core はフォーカスリングの視認性を
170
+ 検証できないため、CDP で forced-colors を有効化し、`outline-hidden` を使う
171
+ コンポーネント(対象例: Button)がキーボードフォーカス時に outline を出すことを
172
+ `__tests__/browser/` の専用テストで確認する。設定は `vitest.forced-colors.config.ts`。
173
+ CI では常時実行される。
174
+
162
175
  エラーが出た場合は修正してから次に進むこと。
163
176
 
164
177
  ---
@@ -309,6 +322,9 @@ npm run test:a11y
309
322
 
310
323
  # 旧 Radix 固定の回帰テスト(consumer が古い Radix を掴む条件を再現。issue #516)
311
324
  npm run test:legacy-radix
325
+
326
+ # 強制配色モードのフォーカス表示検証(chromium の CDP で forced-colors を再現。issue #620)
327
+ npm run test:forced-colors
312
328
  ```
313
329
 
314
330
  **`Closes #N` 誤リンク監査について(issue #486 が由来):**
@@ -355,6 +371,23 @@ npm run test:legacy-radix
355
371
  `eslint/icon-button-aria-label.js`(`ksk-a11y/icon-button-aria-label`)で lint 時に検出する
356
372
  (`.stories.tsx` は対象外)。
357
373
 
374
+ **forced-colors(強制配色)テストについて(issue #620):**
375
+
376
+ - axe-core はフォーカスリングが実際に見えるかまでは検証しない。DS のフォーカス表示は
377
+ `focus-visible:ring-*`(box-shadow)だが、box-shadow は forced-colors モードで
378
+ 描画されないため、`focus-visible:outline-hidden` を併用し、forced-colors 時だけ
379
+ `outline: 2px solid transparent` を出して OS 側の枠に委ねる(`outline-none` では
380
+ このメディアクエリ自体が生成されず枠が消えたままになる)。
381
+ - `__tests__/browser/` に置いた専用テストが CDP (`Emulation.setEmulatedMedia`) で
382
+ forced-colors を有効化し、Tab フォーカス後の `outlineStyle` が `none` でないことを
383
+ chromium 実機で確認する。設定は `vitest.forced-colors.config.ts`(`vitest.config.ts`
384
+ の既定テストからは `__tests__/browser/**` を除外し、`npm run test:forced-colors` で
385
+ 個別に実行する)。
386
+ - DS コンポーネント本体で `outline-none`(前置バリアント付き含む)の新規追加が
387
+ 無いことは `__tests__/focus-ring-outline-pairing.test.ts` の機械検証(`npm test`)で
388
+ 検出する。consumer 向けの `outline-none` 検出は `contracts/rules.json` P023(`bin/lint.js`)
389
+ と `scripts/lint-scratch.sh` W7 が担う。
390
+
358
391
  **旧 Radix 回帰テストについて(issue #516):**
359
392
 
360
393
  - DS の node_modules は ref churn 修正済みの Radix を解決するため、`npm test` では
package/DESIGN.md CHANGED
@@ -335,7 +335,7 @@ Portal に載る要素(Dialog / Sheet / Popover / Toast 等)は DOM 上の
335
335
 
336
336
  代表例(トークン参照で構成)。
337
337
 
338
- - **Button**: bg `{colors.primary}` / text 白 / `rounded-full` / 高さ 48px。variant: default·secondary·tertiary·ghost·destructive·link。
338
+ - **Button**: bg `{colors.primary}` / text 白 / `rounded-full` / 高さ 48px。variant: default·secondary·tertiary·ghost·destructive·info·warning·success·link(info・warning・success は状態色。不可逆操作は destructive)。
339
339
  - **Card**: bg `{colors.surface}` + 1px `{colors.border}` + `{elevation.md}` + `{rounded.surface}`。
340
340
  - **Dialog(中央モーダル)**= タスク面(フォーム/絞り込み)。`role="dialog"`、`{rounded.modal}`。
341
341
  - **AlertDialog / ConfirmDialog(割り込み確認)**= 削除等の"止める"操作。`role="alertdialog"`、背景タップで閉じない・中央表示。
package/PUBLISHING.md CHANGED
@@ -7,7 +7,9 @@
7
7
  > 一本化しており、`main` への version 変更 push が公開の唯一のトリガー。
8
8
  > 各消費リポジトリの `package.json` は `ksk-design-system@X.Y.Z` を参照する。
9
9
  > 対象の消費リポ一覧は `scripts/update-consumers.sh` の `DEFAULT_REPOS` が正本
10
- > (単体リポ・monorepo が混在し、`~/LocalDev/` と `~/LocalDev/Examination/` 配下にまたがる)。
10
+ > (単体リポ・monorepo が混在する。ExamKit 本体と資格アプリは含めず、ExamKit で
11
+ > `npm i ksk-design-system@X.Y.Z --save-exact` → `kit:bump` → `kit:rollout` の順で届ける。
12
+ > 資格アプリは DS を ExamKit と同じ版に完全固定しているため、`^X.Y.Z` を書く一括 bump を流すと版がずれる)。
11
13
 
12
14
  ## 前提
13
15
 
package/README.md CHANGED
@@ -285,6 +285,20 @@ DS 名前空間に属さないカスタムプロパティへ色値(`#hex` / `r
285
285
  ように DS トークンを参照しているだけのものはカウントしません。意図的な独立パレット
286
286
  (LINE 風チャット画面のライトなど)は `// ksk-ds-lint-ignore P050 -- 理由` で抑制できます。
287
287
 
288
+ #### P051: ResponsiveOverlayFrame + Footer の `padding={false}` 付け忘れ(issue #619)
289
+
290
+ `preset="plain"` / `side="float"` / `side="float-glass"` の `ResponsiveOverlayFrame` は、
291
+ 既定で `p-6` と safe-area 込みの下余白を持ちます。`ResponsiveOverlayFooter` も自前で
292
+ safe-area 込みの下余白を持つため、併用するときは Frame に `padding={false}` が必要です。
293
+ 付け忘れると下の余白が二重になるので、P051 が warn します。
294
+
295
+ 誤検知を出さない方を優先し、`side` / `preset` が文字列リテラルで確定していて、Frame に
296
+ spread props(`{...props}`)が無く、`padding` が未指定・値なし・`{true}` で、Footer が
297
+ Frame の JSX の中に直接書かれているときだけ報告します。`padding={cond}` のような式、
298
+ Footer を別コンポーネントに切り出した構成、preset 経路(`mobile-form` 等。余白を持たない)は
299
+ 対象外です。P046 と同じく TypeScript の構文解析を使うため、`typescript` が無いプロジェクトでは
300
+ P051 だけ skip して案内を1行出します。
301
+
288
302
  #### ルールごとの除外(rules.json の excludes 系)
289
303
 
290
304
  `contracts/rules.json` の除外指定は、当てる対象ごとに 3 つに分かれています。
package/bin/lint.js CHANGED
@@ -7,19 +7,39 @@ import {
7
7
  loadProductThemeContract,
8
8
  } from "./product-theme-override.js"
9
9
 
10
- // card-child-spacing.js は `import ts from "typescript"` している(P046 専用エンジン)。
11
- // typescript は 24MB 前後あり、59ルール中 P046 の1つにしか使わないため、
12
- // P046 が実際に有効なとき(rules.json に該当ルールがあり、対象ファイルも存在するとき)
10
+ // card-child-spacing.js / overlay-footer-padding.js は `import ts from "typescript"` している
11
+ // (P046 / P051 専用エンジン)。typescript は 24MB 前後あり、AST が要るのはこの2ルールだけなので、
12
+ // 該当ルールが実際に有効なとき(rules.json に該当ルールがあり、対象ファイルも存在するとき)
13
13
  // だけ動的 import する(issue #409)。typescript が未インストールの consumer では
14
- // ERR_MODULE_NOT_FOUND で落とさず、P046 だけを skip して案内を1行出す。
15
- let cardChildSpacingModulePromise = null
16
- async function loadCardChildSpacing() {
17
- if (!cardChildSpacingModulePromise) {
18
- cardChildSpacingModulePromise = import("./card-child-spacing.js").catch((error) => {
19
- return { __unavailable: true, error }
20
- })
14
+ // ERR_MODULE_NOT_FOUND で落とさず、該当ルールだけを skip して案内を1行出す。
15
+ const tsEngineModulePromises = new Map()
16
+ function loadTsEngine(specifier) {
17
+ if (!tsEngineModulePromises.has(specifier)) {
18
+ tsEngineModulePromises.set(
19
+ specifier,
20
+ import(specifier).catch((error) => {
21
+ return { __unavailable: true, error }
22
+ }),
23
+ )
21
24
  }
22
- return cardChildSpacingModulePromise
25
+ return tsEngineModulePromises.get(specifier)
26
+ }
27
+
28
+ /**
29
+ * TypeScript AST を使うエンジン。engine 名 → { module, export, label }。
30
+ * label は typescript 不在で skip するときの案内文に使う。
31
+ */
32
+ const TS_ENGINES = {
33
+ "card-direct-child-spacing": {
34
+ module: "./card-child-spacing.js",
35
+ exportName: "inspectCardChildSpacing",
36
+ label: "P046(Card 直下の子要素スペーシング)",
37
+ },
38
+ "overlay-footer-padding": {
39
+ module: "./overlay-footer-padding.js",
40
+ exportName: "inspectOverlayFooterPadding",
41
+ label: "P051(ResponsiveOverlayFrame + Footer の padding={false} 付け忘れ)",
42
+ },
23
43
  }
24
44
 
25
45
  const DEFAULT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"])
@@ -369,20 +389,20 @@ export async function runLintCli(argv, { cwd = process.cwd(), pkgRoot = resolve(
369
389
  // DS 自身 / ベンダリングされた DS の CSS を P049 の対象から外すための材料(issue #407)
370
390
  const dsCssIdentity = cssRules.length > 0 ? collectDsCssIdentity(pkgRoot) : null
371
391
 
372
- // P046(card-direct-child-spacing)が有効なときだけ typescript を動的 import する。
373
- // 未インストール環境では ERR_MODULE_NOT_FOUND で落とさず、P046 だけ skip して1行案内する(issue #409)。
374
- let inspectCardChildSpacing = null
375
- const hasCardSpacingRule = sourceRules.some((rule) => rule.engine === "card-direct-child-spacing")
376
- if (hasCardSpacingRule) {
377
- const mod = await loadCardChildSpacing()
392
+ // TypeScript AST エンジン(P046 / P051)は有効なときだけ typescript を動的 import する。
393
+ // 未インストール環境では ERR_MODULE_NOT_FOUND で落とさず、該当ルールだけ skip して1行案内する(issue #409)。
394
+ const tsInspectors = {}
395
+ for (const [engine, spec] of Object.entries(TS_ENGINES)) {
396
+ if (!sourceRules.some((rule) => rule.engine === engine)) continue
397
+ const mod = await loadTsEngine(spec.module)
378
398
  if (mod.__unavailable) {
379
399
  console.log(
380
- `[ksk-ds lint] typescript が見つからないため P046(Card 直下の子要素スペーシング)を skip します。` +
400
+ `[ksk-ds lint] typescript が見つからないため ${spec.label} を skip します。` +
381
401
  `devDependencies に typescript を追加すると検査対象になります。`,
382
402
  )
383
- sourceRules = sourceRules.filter((rule) => rule.engine !== "card-direct-child-spacing")
403
+ sourceRules = sourceRules.filter((rule) => rule.engine !== engine)
384
404
  } else {
385
- inspectCardChildSpacing = mod.inspectCardChildSpacing
405
+ tsInspectors[engine] = mod[spec.exportName]
386
406
  }
387
407
  }
388
408
 
@@ -422,7 +442,7 @@ export async function runLintCli(argv, { cwd = process.cwd(), pkgRoot = resolve(
422
442
  continue
423
443
  }
424
444
  findings.push(
425
- ...lintFile(file, cwd, sourceRules, { ...options, inspectCardChildSpacing, pkgRoot }),
445
+ ...lintFile(file, cwd, sourceRules, { ...options, tsInspectors, pkgRoot }),
426
446
  )
427
447
  } catch (error) {
428
448
  if (error && error.code === "ENOENT") {
@@ -781,9 +801,10 @@ function lintFile(file, cwd, rules, options = {}) {
781
801
 
782
802
  for (const rule of rules) {
783
803
  if (!ruleAppliesTo(rule, { capabilities, filePath: rel })) continue
784
- if (rule.engine === "card-direct-child-spacing") {
785
- if (!options.inspectCardChildSpacing) continue
786
- for (const finding of options.inspectCardChildSpacing(source, file)) {
804
+ if (rule.engine && Object.hasOwn(TS_ENGINES, rule.engine)) {
805
+ const inspect = options.tsInspectors?.[rule.engine]
806
+ if (!inspect) continue
807
+ for (const finding of inspect(source, file)) {
787
808
  const line = lines[finding.line - 1] ?? ""
788
809
  if (ignores.suppresses(rule.id ?? "UNKNOWN", finding.line)) continue
789
810
  if (!matchesRuleExclude(rule, { file: rel, line, isDsFile })) {
@@ -0,0 +1,129 @@
1
+ import ts from "typescript"
2
+
3
+ /**
4
+ * P051: ResponsiveOverlayFrame + ResponsiveOverlayFooter の padding={false} 付け忘れ(issue #619)
5
+ *
6
+ * `preset="plain"` / `side="float" | "float-glass"` の ResponsiveOverlayFrame は
7
+ * 既定で `p-6` と safe-area 込みの下余白を持つ(#599 / #613)。
8
+ * ResponsiveOverlayFooter も自前で `pb-[max(1rem,env(safe-area-inset-bottom))]` を
9
+ * 持つため、Frame 側を `padding={false}` にしないと下余白が二重になる。
10
+ *
11
+ * 誤検知を出さない方を優先し、静的に確定できるときだけ報告する:
12
+ * - Frame の `side` / `preset` が文字列リテラルで、plain / float 系だと確定している
13
+ * - Frame に spread props(`{...props}`)が無い(padding が来うるため)
14
+ * - `padding` が未指定・値なし(`padding`)・`{true}` のいずれか(`{cond}` 等の式は対象外)
15
+ * - Footer が Frame の JSX 子孫に直接書かれている(別コンポーネントへの切り出しは追えない)
16
+ *
17
+ * preset 経路(mobile-form 等 / BottomSheetFrame 経由)は padding を受け付けず、
18
+ * 余白も持たないので対象外。
19
+ */
20
+
21
+ const FRAME = "ResponsiveOverlayFrame"
22
+ const FOOTER = "ResponsiveOverlayFooter"
23
+ const FLOAT_SIDES = new Set(["float", "float-glass"])
24
+
25
+ /** `Foo` / `ns.Foo` の末尾の名前 */
26
+ function tagBaseName(tagName) {
27
+ if (ts.isIdentifier(tagName)) return tagName.text
28
+ if (ts.isPropertyAccessExpression(tagName)) return tagName.name.text
29
+ return ""
30
+ }
31
+
32
+ function openingOf(node) {
33
+ if (ts.isJsxElement(node)) return node.openingElement
34
+ if (ts.isJsxSelfClosingElement(node)) return node
35
+ return null
36
+ }
37
+
38
+ const UNKNOWN = Symbol("unknown")
39
+
40
+ /**
41
+ * 属性値を静的に読む。
42
+ * - 属性なし → undefined
43
+ * - 値なし(`padding`)→ true
44
+ * - 文字列 / true / false リテラル → その値
45
+ * - それ以外の式 → UNKNOWN
46
+ */
47
+ function readAttribute(opening, name) {
48
+ const attribute = opening.attributes.properties.find(
49
+ (property) => ts.isJsxAttribute(property) && property.name.getText() === name,
50
+ )
51
+ if (!attribute) return undefined
52
+ const init = attribute.initializer
53
+ if (!init) return true
54
+ if (ts.isStringLiteral(init)) return init.text
55
+ if (ts.isJsxExpression(init) && init.expression) {
56
+ const expression = init.expression
57
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
58
+ return expression.text
59
+ }
60
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) return true
61
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) return false
62
+ }
63
+ return UNKNOWN
64
+ }
65
+
66
+ /** padding を持つ経路(plain / float 系)だと静的に確定しているか */
67
+ function isPaddedVariant(opening) {
68
+ const side = readAttribute(opening, "side")
69
+ if (side === UNKNOWN) return false
70
+ if (typeof side === "string" && FLOAT_SIDES.has(side)) return true
71
+ if (side !== undefined && side !== "bottom") return false
72
+ return readAttribute(opening, "preset") === "plain"
73
+ }
74
+
75
+ function hasSpread(opening) {
76
+ return opening.attributes.properties.some((property) => ts.isJsxSpreadAttribute(property))
77
+ }
78
+
79
+ /** Frame の子孫に Footer が書かれているか(入れ子の Frame の中は数えない) */
80
+ function containsFooter(frame) {
81
+ let found = false
82
+ function visit(node) {
83
+ if (found) return
84
+ const opening = openingOf(node)
85
+ if (opening) {
86
+ const name = tagBaseName(opening.tagName)
87
+ if (name === FOOTER) {
88
+ found = true
89
+ return
90
+ }
91
+ // 入れ子の Frame は自分自身として別途判定される
92
+ if (name === FRAME) return
93
+ }
94
+ ts.forEachChild(node, visit)
95
+ }
96
+ for (const child of frame.children) visit(child)
97
+ return found
98
+ }
99
+
100
+ export function inspectOverlayFooterPadding(source, filePath = "source.tsx") {
101
+ const sourceFile = ts.createSourceFile(
102
+ filePath,
103
+ source,
104
+ ts.ScriptTarget.Latest,
105
+ true,
106
+ ts.ScriptKind.TSX,
107
+ )
108
+ const findings = []
109
+
110
+ function walk(node) {
111
+ if (ts.isJsxElement(node) && tagBaseName(node.openingElement.tagName) === FRAME) {
112
+ const opening = node.openingElement
113
+ const padding = readAttribute(opening, "padding")
114
+ if (
115
+ isPaddedVariant(opening) &&
116
+ !hasSpread(opening) &&
117
+ (padding === undefined || padding === true) &&
118
+ containsFooter(node)
119
+ ) {
120
+ const position = sourceFile.getLineAndCharacterOfPosition(opening.getStart(sourceFile))
121
+ findings.push({ line: position.line + 1, tag: FRAME })
122
+ }
123
+ }
124
+ ts.forEachChild(node, walk)
125
+ }
126
+
127
+ walk(sourceFile)
128
+ return findings
129
+ }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "KSK Design System — Component Contracts",
4
- "version": "2.8.0",
4
+ "version": "2.9.0",
5
5
  "description": "全コンポーネントの構造化定義。バリアント・アクセシビリティ要件・使用ルールを機械可読形式で管理。",
6
6
  "counts": {
7
7
  "ui": 69,
8
- "patterns": 61,
8
+ "patterns": 62,
9
9
  "commerce": 12,
10
10
  "admin": 8,
11
11
  "shells": 3,
12
- "total": 153
12
+ "total": 154
13
13
  },
14
14
  "exportNaming": {
15
15
  "_doc": "contracts の name が `import { name } from \"ksk-design-system\"` で解決できるかを示す。__tests__/contracts-export-integrity.test.ts が src/index.ts の実 export と突き合わせて固定する。",
@@ -42,6 +42,9 @@
42
42
  "ghost",
43
43
  "link",
44
44
  "destructive",
45
+ "info",
46
+ "warning",
47
+ "success",
45
48
  "glass",
46
49
  "glass-inverse",
47
50
  "glass-accent",
@@ -78,10 +81,12 @@
78
81
  "Icon-only buttons require aria-label",
79
82
  "Native Button supports loading/loadingLabel and leadingIcon/trailingIcon; do not wrap ActivityIndicator in Text",
80
83
  "Native Button supports spinnerColor to override the loading ActivityIndicator color; default unchanged (falls back to variant fg color, issue #450)",
84
+ "variant=info / warning / success は状態色で種類や意味を伝えるボタン(issue #598)。背景色を className で上書きせず variant を使う。不可逆操作には warning ではなく destructive を使う",
81
85
  "variant=glass は背景がグラデーション/写真の場合のみ使用",
82
86
  "variant=glass-inverse は暗い写真・動画・濃色面上の Liquid Glass CTA 専用",
83
87
  "variant=inverse / ghost-inverse は暗背景・ヒーローセクション専用",
84
88
  "size=hero は hero / final-CTA 専用(ピル型 56px)",
89
+ "size=xs / sm / icon-sm は透明な before 擬似要素で当たり判定を 44×44px に広げる(見た目の寸法は不変・issue #601)。隣と重ねないには見た目の端同士を sm / icon-sm で gap-3、xs で gap-5 以上空ける。glass 系 variant と overflow-hidden を足したボタンでは拡張されない",
85
90
  "既存の手書き CSS を持つ画面の段階移行では unstyled を使う(variant / size は無視され、focus ring 以外の視覚クラスを出さない)"
86
91
  ]
87
92
  },
@@ -494,12 +499,19 @@
494
499
  {
495
500
  "name": "Accordion",
496
501
  "path": "src/components/ui/accordion.tsx",
497
- "description": "アコーディオン。FAQ・設定項目など折りたたみコンテンツに使用。",
502
+ "description": "アコーディオン。FAQ・設定項目など折りたたみコンテンツに使用。size=\"lg\" で LP の「よくある質問」のような大きめの見せ方にできる(issue #600)。",
498
503
  "subcomponents": [
499
504
  "AccordionItem",
500
505
  "AccordionTrigger",
501
506
  "AccordionContent"
502
- ]
507
+ ],
508
+ "sizes": [
509
+ "default",
510
+ "lg"
511
+ ],
512
+ "props": {
513
+ "size": "\"default\" | \"lg\" — Accordion ルートで指定すると context 経由で Trigger/Content へ伝わる(既定 \"default\")。Trigger/Content に直接渡すと個別上書きできる。lg は Trigger: typo-label-lg py-6 / Content: typo-body-lg"
514
+ }
503
515
  },
504
516
  {
505
517
  "name": "Separator",
@@ -2312,6 +2324,8 @@
2312
2324
  "preset ごとのデスクトップ寸法写像(幅・max-height・角丸)",
2313
2325
  "side=float / float-glass のデスクトップ写像(32rem / min(85dvh,46rem))。float 系は preset ではなく padding が効く",
2314
2326
  "preset=\"plain\" は BottomSheetFrame を通さない素の bottom シート(全幅・下端固定・p-6・max-h-90dvh のまま)で、デスクトップだけ中央モーダル(32rem / min(90dvh,46rem))になる。padding が効く(issue #486)",
2327
+ "モバイル分岐の下余白に home indicator の safe-area を含める(issue #599)。plain は pb = 1.5rem + env(safe-area-inset-bottom)、float 系は面が下端から 12px 浮いているので pb = max(1.5rem, env(safe-area-inset-bottom))。padding={false} / safeArea={false} / キーボード表示中は足さない。デスクトップ分岐と preset 経路は変えない",
2328
+ "モバイルの side=float / float-glass は既定で max-height: min(85dvh, 100dvh - 1.5rem - 上部safe-area)(lg 以上は min(85dvh,46rem))を持ち、超えた分は面の中でスクロールする(issue #599)。キーボード表示中は #337 の inline style が優先",
2315
2329
  "description / autoFocus / restoreFocusOnClose / closeOnEsc / zIndex を両分岐へ透過",
2316
2330
  "snap sheet はデスクトップでもシートのまま",
2317
2331
  "ResponsiveOverlayFooter がモバイルのみキーボード追従になる",
@@ -2326,7 +2340,8 @@
2326
2340
  "side / preset ごとに効く prop が判別ユニオンで分かれている: preset 経路は preset / surface / desktopPosition、float 系と preset=\"plain\" は padding。効かない組み合わせは黙って無視されずコンパイルエラーになる(型テスト test/types/responsive-overlay-frame.tsx)",
2327
2341
  "デスクトップ分岐のキーボード補正は max-height だけで lift(bottom)は当てない。DialogContent は top:50% + translate-y:-50% で位置決めしているため bottom を足すと両拘束になり高さが縦一杯へ伸びる。中央寄せのキャップは 100dvh - 2*kb(100dvh - kb では下端がキーボードへ潜り込む。desktopPosition=\"top\" / \"fullscreen\" は上端固定なのでキーボード 1 回分 / #487)",
2328
2342
  "3 つのデスクトップ分岐(preset / plain / float 系)はすべて data-frame=\"responsive-overlay-frame\" + data-side + data-position を出す。consumer が独自のキーボード補正を当てる場合もこの属性で絞る(#487)",
2329
- "float-glass の実スクロールは src/styles/sheet-keyboard.css の :is(sheet-content, dialog-content)[data-side=\"float-glass\"] が担う。.glass-specular の overflow:hidden が非レイヤー CSS で className を踏み潰すため、className だけでは効かない(#337 / #479)"
2343
+ "float-glass の実スクロールは src/styles/sheet-keyboard.css の :is(sheet-content, dialog-content)[data-side=\"float-glass\"] が担う。.glass-specular の overflow:hidden が非レイヤー CSS で className を踏み潰すため、className だけでは効かない(#337 / #479)",
2344
+ "モバイルの下余白(safe-area)や float の最大高さを consumer 側で pb-8 / max-h-[85dvh] overflow-y-auto と書き足さない(#599 で既定化)。値を変えたいときだけ className / mobileClassName の pb-* / max-h-* で上書きする(twMerge で既定と置き換わり二重にならない)。ResponsiveOverlayFooter を使う面は padding={false} にする(フッタ自身が safe-area を持つ。付け忘れは npx ksk-ds lint の P051 が warn)"
2330
2345
  ],
2331
2346
  "subcomponents": [
2332
2347
  "ResponsiveOverlayFooter"
@@ -2408,6 +2423,27 @@
2408
2423
  "既に fixed な nav を渡す場合は bottomNavMode=external を使う"
2409
2424
  ]
2410
2425
  },
2426
+ {
2427
+ "name": "SidebarNav",
2428
+ "path": "src/components/patterns/sidebar-nav.tsx",
2429
+ "description": "PC 表示の左サイドバーに縦に並べるナビ項目。行高 44px・角丸・選択中の面/文字色・アイコン位置・件数バッジを DS が持つ。items は BottomTabBarItem と同じ形で、モバイルの BottomTabBar と同じ配列を渡せる。collapsed でアイコンだけのレール表示。",
2430
+ "variants": [
2431
+ "collapsed: false / true"
2432
+ ],
2433
+ "accessibility": [
2434
+ "nav に aria-label(既定 \"サイドナビゲーション\")を付与",
2435
+ "isActive の項目に aria-current=\"page\" を付与",
2436
+ "role=tab / aria-selected は使わない",
2437
+ "collapsed 時はラベルを aria-label と title に移す",
2438
+ "行の高さは 44px 以上"
2439
+ ],
2440
+ "rules": [
2441
+ "MobileAppShell の desktopSidebar / AdminShell の sidebar slot に置く。Button を行の形へ上書きして代用しない",
2442
+ "モバイルの BottomTabBar と同じ items 配列を渡す",
2443
+ "ページ内アンカーの目次は SectionNav、ビュー状態の切替は SubNav を使う",
2444
+ "ロゴ行・フッターは部品に含めず slot 側で並べる"
2445
+ ]
2446
+ },
2411
2447
  {
2412
2448
  "name": "CompactFilePicker / ImageAttachmentPicker",
2413
2449
  "exported": false,
@@ -45,6 +45,11 @@
45
45
  "dsIssue": 595,
46
46
  "consumerIssue": "https://github.com/ekusiek716/yokoku-app/issues/321",
47
47
  "fixPr": null
48
+ },
49
+ {
50
+ "dsIssue": 608,
51
+ "consumerIssue": "https://github.com/ekusiek716/camera-app/issues/257",
52
+ "fixPr": null
48
53
  }
49
54
  ]
50
55
  }
@@ -303,6 +303,11 @@
303
303
  "--Field-Height-Md",
304
304
  "--Field-Padding-X-Md",
305
305
  "--Field-Radius"
306
+ ],
307
+ "DateField": [
308
+ "--Field-Height-Md",
309
+ "--Field-Padding-X-Md",
310
+ "--Field-Radius"
306
311
  ]
307
312
  },
308
313
  "aiGuidance": [
@@ -2,7 +2,7 @@
2
2
  "meta": {
3
3
  "name": "KSK Design System — Rules Contracts",
4
4
  "version": "1.2.0",
5
- "description": "禁止パターン・アクセシビリティ要件・ライティングルールの構造化定義。lint-scratch.sh と check-drift.sh で自動検証。禁止パターン50件・AIアンチパターン10件。"
5
+ "description": "禁止パターン・アクセシビリティ要件・ライティングルールの構造化定義。lint-scratch.sh と check-drift.sh で自動検証。禁止パターン51件・AIアンチパターン10件。"
6
6
  },
7
7
  "prohibited": [
8
8
  {
@@ -260,9 +260,9 @@
260
260
  "pattern": "outline-none\\b",
261
261
  "excludePaths": [".stories."],
262
262
  "excludeDsPaths": ["components/ui/", "components/patterns/"],
263
- "excludeLines": ["focus-visible"],
264
- "message": "outline-none 禁止(フォーカスリング削除はアクセシビリティ違反)",
265
- "fix": "focus-visible:ring-[3px] focus-visible:ring-[var(--Focus-High-Emphasis)]/50"
263
+ "message": "outline-none 禁止(フォーカスリング削除はアクセシビリティ違反。強制配色モードでは outline-hidden でないと枠が消える)",
264
+ "fix": "focus-visible:outline-hidden focus-visible:ring-[3px] focus-visible:ring-[var(--Focus-High-Emphasis)]/50",
265
+ "note": "issue #620。Tailwind v4 で outline-none は常に outline-style: none(強制配色モードでも非表示のまま)。outline-hidden は通常時 none だが forced-colors 時だけ outline: 2px solid transparent になり OS 色へ置換されるため、box-shadow の ring が描画されない強制配色モードでも枠が残る。以前は excludeLines: [\"focus-visible\"] で `focus-visible:outline-none` を含む行を丸ごと素通りさせていたため、まさに forced-colors で問題になるパターン(outline-none + ring の組み合わせ)を検出できていなかった(レビュー指摘)。前置バリアント(`md:` / `data-[...]:` / `group-focus-visible:` / `!` 等)付きでも `outline-none` 部分文字列があれば検出する。DS コンポーネント本体(components/ui・components/patterns)はこのルールの対象外で、__tests__/focus-ring-outline-pairing.test.ts が outline-hidden 必須を機械検証する"
266
266
  },
267
267
  {
268
268
  "id": "P024",
@@ -568,6 +568,18 @@
568
568
  "message": "DS を参照しない並行パレットの疑い(DS 名前空間に属さないカスタムプロパティに色値を5個以上定義)",
569
569
  "fix": "ksk-design-system/preset + テーマの導入、または既存変数の値を DS トークン参照へ置換",
570
570
  "note": "P049 は DS 名前空間(--Primitive- 等)に触れる CSS だけを違反にするため、DS を一切使わない独自パレット(--bg / --accent / --surface 等)を敷いた CSS には無言だった(issue #393)。:root / .dark / [data-theme=...] のようなルート的セレクタの中で、DS 名前空間(contracts/product-theme-overrides.json の dsVariableNamespaces)に属さないカスタムプロパティへ色値(hex / rgb() / hsl() / oklch())を minColorVars 個以上定義していたら warn する。値が var(--Primitive-...) のように DS トークンを参照しているだけのものはカウントしない。意図的な独立パレット(チャット画面の LINE 風ライト等)は // ksk-ds-lint-ignore P050 -- 理由 で抑制できる。DS 自身 / ベンダリングされた DS の CSS は P049 と同じ判定(issue #407)で対象外"
571
+ },
572
+ {
573
+ "id": "P051",
574
+ "severity": "warning",
575
+ "category": "spacing",
576
+ "appliesTo": ["dom"],
577
+ "pattern": "overlay-footer-padding",
578
+ "engine": "overlay-footer-padding",
579
+ "excludeLines": [],
580
+ "message": "ResponsiveOverlayFrame(preset=\"plain\" / side=\"float\" 系)の中に ResponsiveOverlayFooter を置くのに padding={false} が無い(下余白が二重になる)",
581
+ "fix": "Frame に padding={false} を付ける。ResponsiveOverlayFooter が safe-area 込みの下余白を自前で持つため、Frame 側の p-6 と safe-area の下余白は不要",
582
+ "note": "issue #619。#599 / #613 で plain / float 系の Frame が既定で p-6 + safe-area 込みの下余白を持つようになったため、Footer と併用すると二重になる。公開 CLI と lint-scratch.sh は共通の TypeScript AST 検査(bin/overlay-footer-padding.js)。誤検知を出さない方を優先し、side / preset が文字列リテラルで plain / float 系と確定し、Frame に spread props が無く、padding が未指定・値なし・{true} で、Footer が Frame の JSX 子孫に直接書かれているときだけ報告する。padding={cond} のような式・{...props}・Footer を別コンポーネントに切り出した構成・preset 経路(mobile-form 等。padding を受け付けず余白も持たない)は対象外"
571
583
  }
572
584
  ],
573
585
  "aiPatterns": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "KSK Design System — Semantic Token Hex Cache",
4
- "version": "2.8.0",
4
+ "version": "2.9.0",
5
5
  "description": "semantic / semanticDark トークン(var(--Primitive-*) 参照)を実 hex に解決したサイドカー生成物。hex はデフォルト(Blue)テーマでの解決値であり、Brand 系(meta.themeDependentKeys に列挙)はテーマ差し替え(orange/green/violet 等)で実色が変わる。テーマ別の完全解決値は `ksk-design-system/native` エクスポート(バンドル済み native トークンモジュール)の themes を参照。tokens.json 本体のスキーマは変更せず、AI がこのファイルだけで実色を把握できるようにし、primitive 値の変更による semantic 実色のドリフトを --check で機械検出する。",
6
6
  "generatedBy": "scripts/generate-token-hex-cache.mjs",
7
7
  "theme": "default",
@@ -346,8 +346,14 @@
346
346
  "hover.tertiary-button": "#F4F4F5",
347
347
  "hover.ghost-button": "#F4F4F5",
348
348
  "hover.destructive-button": "#B91C1C",
349
+ "hover.info-button": "#1D4ED8",
350
+ "hover.warning-button": "#9A3412",
351
+ "hover.success-button": "#166534",
349
352
  "active.primary-button": "#1E40AF",
350
353
  "active.destructive-button": "#991B1B",
354
+ "active.info-button": "#1E40AF",
355
+ "active.warning-button": "#7C2D12",
356
+ "active.success-button": "#14532D",
351
357
  "active.secondary-button": "#DBEAFE",
352
358
  "active.tertiary-button": "#E4E4E7",
353
359
  "focus.high-emphasis": "#60A5FA",
@@ -452,9 +458,15 @@
452
458
  "brand.on-apple": "#000000",
453
459
  "hover.primary-button": "#3B82F6",
454
460
  "hover.secondary-button": "#1E3A8A",
455
- "hover.destructive-button": "#EF4444",
461
+ "hover.destructive-button": "#FCA5A5",
462
+ "hover.info-button": "#93C5FD",
463
+ "hover.warning-button": "#FDBA74",
464
+ "hover.success-button": "#86EFAC",
456
465
  "active.primary-button": "#2563EB",
457
- "active.destructive-button": "#DC2626",
466
+ "active.destructive-button": "#FECACA",
467
+ "active.info-button": "#BFDBFE",
468
+ "active.warning-button": "#FED7AA",
469
+ "active.success-button": "#BBF7D0",
458
470
  "active.secondary-button": "#1E40AF",
459
471
  "focus.high-emphasis": "#60A5FA",
460
472
  "caution.base": "#F87171",
@@ -1,2 +1,2 @@
1
- import { n as e } from "./server-variants-DTpkG8A7.js";
1
+ import { n as e } from "./server-variants-C7GNdOYi.js";
2
2
  export { e as buttonVariants };