ksk-design-system 2.0.1 → 2.1.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/AGENTS.md CHANGED
@@ -279,6 +279,9 @@ npm run lint:design
279
279
  # 非推奨 API 台帳の整合検査(台帳 ⇔ 実ソースの @deprecated JSDoc)
280
280
  npm run lint:deprecations
281
281
 
282
+ # `Closes #N` 誤リンク監査(gh 必須。npm run check には入っていない)
283
+ npm run audit:issue-links -- --recent 60
284
+
282
285
  # MIGRATION.md の「非推奨 API 一覧」節を台帳から再生成
283
286
  npm run generate:migration-doc
284
287
 
@@ -293,8 +296,25 @@ npm run test:interaction
293
296
 
294
297
  # a11y 機械検証(axe-core。全ストーリー対象。issue #261)
295
298
  npm run test:a11y
299
+
300
+ # 旧 Radix 固定の回帰テスト(consumer が古い Radix を掴む条件を再現。issue #516)
301
+ npm run test:legacy-radix
296
302
  ```
297
303
 
304
+ **`Closes #N` 誤リンク監査について(issue #486 が由来):**
305
+
306
+ - PR 本文の `Closes #N` が**別 issue を閉じてしまう**事故を検出する。実例:
307
+ PR #488 が `Closes #486` を書きながら実装したのは ResponsiveOverlayFrame で、
308
+ issue #486(BottomTabBar pill の面)は**未実装のまま自動クローズ**され、
309
+ 消費側がリリースされない要望を待ち続けていた。
310
+ - 判定は `contracts/components.json` のコンポーネント名を語彙にして、
311
+ 「PR が触った / 言及したコンポーネント」と「issue が指すコンポーネント」に
312
+ 共通要素が 1 つも無ければ疑う。どちらかが空(インフラ・トークン系)なら
313
+ 判定材料なしとして黙って飛ばす — **見逃してでも誤検知を出さない**方針。
314
+ - `gh` CLI(ネットワーク)が要るため `npm run check` には**含めない**。
315
+ リリース前の棚卸しと、疑わしい PR を見つけたときに手で回す。
316
+ - 単発検査は `npm run audit:issue-links -- --pr 488`。
317
+
298
318
  **interaction テストについて(issue #256):**
299
319
 
300
320
  - 実体は Storybook の play 関数。`@storybook/addon-vitest` + vitest browser mode(playwright chromium)で
@@ -325,6 +345,20 @@ npm run test:a11y
325
345
  `eslint/icon-button-aria-label.js`(`ksk-a11y/icon-button-aria-label`)で lint 時に検出する
326
346
  (`.stories.tsx` は対象外)。
327
347
 
348
+ **旧 Radix 回帰テストについて(issue #516):**
349
+
350
+ - DS の node_modules は ref churn 修正済みの Radix を解決するため、`npm test` では
351
+ 「consumer が古い Radix を掴んだときだけ落ちる」種類の回帰を**原理的に再現できない**
352
+ (#516 は belle-todo で初めて表面化した)。
353
+ - `npm run test:legacy-radix` は当時 belle-todo が解決していた radix-ui@1.4.3 を
354
+ `node_modules/.ksk-legacy-radix` へ install し、`vitest.legacy-radix.config.ts` の
355
+ alias でそちらを掴ませて `test/legacy-radix/` を回す。CI では常時実行。
356
+ - 併せて `package.json` の `radix-ui` / `@radix-ui/react-slot` の下限を安全版へ固定して
357
+ いる(`scripts/check-radix-floor.mjs` が `npm run check` で検査。正本データと版ごとの
358
+ 差分は `scripts/radix-ref-churn.mjs`)。
359
+ - **オーバーレイ系(Dialog / Sheet / ResponsiveOverlayFrame)や `useComposedRef` を
360
+ 触ったら `npm run test:legacy-radix` を回すこと。**
361
+
328
362
  ---
329
363
 
330
364
  ## カラートークン体系(3層構造)
package/CLAUDE.md CHANGED
@@ -279,6 +279,9 @@ npm run lint:design
279
279
  # 非推奨 API 台帳の整合検査(台帳 ⇔ 実ソースの @deprecated JSDoc)
280
280
  npm run lint:deprecations
281
281
 
282
+ # `Closes #N` 誤リンク監査(gh 必須。npm run check には入っていない)
283
+ npm run audit:issue-links -- --recent 60
284
+
282
285
  # MIGRATION.md の「非推奨 API 一覧」節を台帳から再生成
283
286
  npm run generate:migration-doc
284
287
 
@@ -293,8 +296,25 @@ npm run test:interaction
293
296
 
294
297
  # a11y 機械検証(axe-core。全ストーリー対象。issue #261)
295
298
  npm run test:a11y
299
+
300
+ # 旧 Radix 固定の回帰テスト(consumer が古い Radix を掴む条件を再現。issue #516)
301
+ npm run test:legacy-radix
296
302
  ```
297
303
 
304
+ **`Closes #N` 誤リンク監査について(issue #486 が由来):**
305
+
306
+ - PR 本文の `Closes #N` が**別 issue を閉じてしまう**事故を検出する。実例:
307
+ PR #488 が `Closes #486` を書きながら実装したのは ResponsiveOverlayFrame で、
308
+ issue #486(BottomTabBar pill の面)は**未実装のまま自動クローズ**され、
309
+ 消費側がリリースされない要望を待ち続けていた。
310
+ - 判定は `contracts/components.json` のコンポーネント名を語彙にして、
311
+ 「PR が触った / 言及したコンポーネント」と「issue が指すコンポーネント」に
312
+ 共通要素が 1 つも無ければ疑う。どちらかが空(インフラ・トークン系)なら
313
+ 判定材料なしとして黙って飛ばす — **見逃してでも誤検知を出さない**方針。
314
+ - `gh` CLI(ネットワーク)が要るため `npm run check` には**含めない**。
315
+ リリース前の棚卸しと、疑わしい PR を見つけたときに手で回す。
316
+ - 単発検査は `npm run audit:issue-links -- --pr 488`。
317
+
298
318
  **interaction テストについて(issue #256):**
299
319
 
300
320
  - 実体は Storybook の play 関数。`@storybook/addon-vitest` + vitest browser mode(playwright chromium)で
@@ -325,6 +345,20 @@ npm run test:a11y
325
345
  `eslint/icon-button-aria-label.js`(`ksk-a11y/icon-button-aria-label`)で lint 時に検出する
326
346
  (`.stories.tsx` は対象外)。
327
347
 
348
+ **旧 Radix 回帰テストについて(issue #516):**
349
+
350
+ - DS の node_modules は ref churn 修正済みの Radix を解決するため、`npm test` では
351
+ 「consumer が古い Radix を掴んだときだけ落ちる」種類の回帰を**原理的に再現できない**
352
+ (#516 は belle-todo で初めて表面化した)。
353
+ - `npm run test:legacy-radix` は当時 belle-todo が解決していた radix-ui@1.4.3 を
354
+ `node_modules/.ksk-legacy-radix` へ install し、`vitest.legacy-radix.config.ts` の
355
+ alias でそちらを掴ませて `test/legacy-radix/` を回す。CI では常時実行。
356
+ - 併せて `package.json` の `radix-ui` / `@radix-ui/react-slot` の下限を安全版へ固定して
357
+ いる(`scripts/check-radix-floor.mjs` が `npm run check` で検査。正本データと版ごとの
358
+ 差分は `scripts/radix-ref-churn.mjs`)。
359
+ - **オーバーレイ系(Dialog / Sheet / ResponsiveOverlayFrame)や `useComposedRef` を
360
+ 触ったら `npm run test:legacy-radix` を回すこと。**
361
+
328
362
  ---
329
363
 
330
364
  ## カラートークン体系(3層構造)
package/UPDATING.md CHANGED
@@ -202,6 +202,23 @@ publish 直後は registry への反映に数分かかることがあるため
202
202
  `package.json#peerDependencies` の範囲を確認し、警告の対象パッケージ自体を先に更新してから
203
203
  `ksk-design-system` を上げる。警告を `--force` / `--legacy-peer-deps` で握りつぶして進めない。
204
204
 
205
+ ### Radix の版が古いまま固定されている(`Maximum update depth exceeded`)
206
+
207
+ DS の依存 `radix-ui` は **`^1.6.1`**(#516 対応以降)。1.6.1 未満の Radix は
208
+ 合成 ref(`composeRefs`)を毎 render 作り直すため、DS のオーバーレイ面(Dialog / Sheet /
209
+ ResponsiveOverlayFrame)が再描画を無限に誘発することがある(issue #516。belle-todo が
210
+ radix-ui@1.4.3 を解決していて踏んだ)。
211
+
212
+ 症状が出たら、消費リポで実際に解決されている版を確認する:
213
+
214
+ ```bash
215
+ npm ls radix-ui @radix-ui/react-focus-scope @radix-ui/react-presence
216
+ ```
217
+
218
+ `radix-ui@1.6.1` 未満が出る場合は lockfile が古い版を固定している。`npm install` で
219
+ DS の下限に追随させる(`overrides` で Radix を固定しているなら、その固定を外すか
220
+ 1.6.1 以上へ上げる)。
221
+
205
222
  ### Issue 起票先
206
223
 
207
224
  DS 本体のバグ・想定外の破壊変更に遭遇した場合は、
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "KSK Design System — Component Contracts",
4
- "version": "2.0.1",
4
+ "version": "2.1.0",
5
5
  "description": "全コンポーネントの構造化定義。バリアント・アクセシビリティ要件・使用ルールを機械可読形式で管理。",
6
6
  "counts": {
7
7
  "ui": 68,
@@ -23,7 +23,7 @@
23
23
  "statusPaletteNote": "statusPalette は「消費側が DS の primitive を自分の Tailwind alias(--color-sun-800 等)の実体として使っている」ケース向けの公開段(issue #384)。@theme inline の alias は生成ユーティリティに var(--Primitive-*) が直接展開されるため、alias 側を dark で差し替えても効かず、primitive を上書きする以外の経路が無い。公開しているのは status の tint 段(-50 × 4色)と、依頼のあったアクセント段(Red-600 / Green-700)だけ。-50 は DS の .dark ブロックが一切参照していない(dark の Surface-Caution/Success/Warning/Info は -900 と実値)ので、dark 内での上書きは DS コンポーネントに対して無害。light では --Surface-Caution / --Surface-Success / --Surface-Warning / --Surface-Info を駆動するので上書きは効く。アクセント段は light / dark の両方で DS semantic を駆動するため、パレット全段ではなく依頼ベースで1段ずつ開ける。",
24
24
  "statusPaletteSideEffects": "--Primitive-Red-600 は --Caution-Base(light / dark 共通)と、dark の --Active-Destructive-Button を駆動する。dark で Red-600 を Red-500(#EF4444) より明るい値に差し替えると、destructive ボタンの active が hover より明るくなり押下の階調が反転する。差し替えるなら --Hover-Destructive-Button との明度関係を実測すること。--Primitive-Green-700 は light の --Text-Success / --Success-Base のみを駆動する(dark は Green-400 なので影響なし)。",
25
25
  "contrastResponsibility": "neutralPalette(--Primitive-Gray-* / White / Black)を差し替えたら、本文×背景のコントラストは消費プロダクト側の責任になる。Gray は --Text-High-Emphasis(本文色)・--Text-Medium-Emphasis・--Surface-Secondary・--Border-Low-Emphasis など semantic.css の 33 箇所を駆動しているため、差し替え後に本文テキスト×各 Surface が WCAG AA(4.5:1、大きい文字は 3:1)を満たすか必ず実測すること。DS 側の scripts/check-contrast.mjs は DS 既定パレット向けの検査で、消費側の差し替えは見ていない。light / dark の両方を確認する。",
26
- "navSurfaceNote": "--Nav-Center-Action-Surface / -Border / -Shadow と --Nav-Selected-Surface / -Shadow は例外的に :root へ既定値を置いていない(issue #471)。DS 既定は tone=\"inverse\" / dark で出し分ける素材(.glass-accent・ガラスの選択プラッター)で、単一の :root 値に畳めないため。src/styles/bottom-nav.css が var(--Nav-*, <DS 既定>) の形で持ち、消費側が宣言したときだけ上書きされる。宣言しなければレンダリングは 1px も変わらない。"
26
+ "navSurfaceNote": "--Nav-Center-Action-Surface / -Border / -Shadow と --Nav-Selected-Surface / -Shadow は例外的に :root へ既定値を置いていない(issue #471)。DS 既定は tone=\"inverse\" / dark で出し分ける素材(.glass-accent・ガラスの選択プラッター)で、単一の :root 値に畳めないため。src/styles/bottom-nav.css が var(--Nav-*, <DS 既定>) の形で持ち、消費側が宣言したときだけ上書きされる。宣言しなければレンダリングは 1px も変わらない。 --Nav-Pill-Surface / -Backdrop / -Border / -Shadow / -Specular も同じ扱い(issue #486)。pill 本体の DS 既定は Liquid Glass(.glass / tone=\"inverse\" は .glass-dark)で、背後コンテンツの可読性を優先して不透明面にしたい消費側のための口。フォールバックは .glass 自身の内部変数そのものなので、宣言しなければレンダリングは 1px も変わらない。ぼかしを完全に切るなら --Nav-Pill-Backdrop: none、ガラスのスペキュラ / 屈折リムまで消すなら --Nav-Pill-Specular: none(擬似要素の content を none にして生成させない)を併せて宣言する。"
27
27
  },
28
28
  "allowedVariables": {
29
29
  "brandColor": [
@@ -165,6 +165,11 @@
165
165
  "--Nav-Pill-Padding-X",
166
166
  "--Nav-Pill-Padding-Y",
167
167
  "--Nav-Pill-Gap",
168
+ "--Nav-Pill-Surface",
169
+ "--Nav-Pill-Backdrop",
170
+ "--Nav-Pill-Border",
171
+ "--Nav-Pill-Shadow",
172
+ "--Nav-Pill-Specular",
168
173
  "--Nav-Item-Min-Height",
169
174
  "--Nav-Item-Gap",
170
175
  "--Nav-Center-Action-Size",
@@ -267,9 +272,15 @@
267
272
  "--Nav-Center-Action-*",
268
273
  "--Nav-Selected-*"
269
274
  ],
270
- "ResponsiveDialog": ["--Overlay-Desktop-Breakpoint"],
271
- "ResponsiveOverlayFrame": ["--Overlay-Desktop-Breakpoint"],
272
- "Toast": ["--Toast-Viewport-Offset-Bottom"]
275
+ "ResponsiveDialog": [
276
+ "--Overlay-Desktop-Breakpoint"
277
+ ],
278
+ "ResponsiveOverlayFrame": [
279
+ "--Overlay-Desktop-Breakpoint"
280
+ ],
281
+ "Toast": [
282
+ "--Toast-Viewport-Offset-Bottom"
283
+ ]
273
284
  },
274
285
  "aiGuidance": [
275
286
  "プロダクト差分は、まずこの契約の CSS 変数上書きで表現する。className での個別上書きを何十箇所にも散らさない。",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "KSK Design System — Semantic Token Hex Cache",
4
- "version": "2.0.1",
4
+ "version": "2.1.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",
package/dist/index.js CHANGED
@@ -13089,8 +13089,8 @@ function Ef(e) {
13089
13089
  liftInset: e === "lift" ? t : 0
13090
13090
  };
13091
13091
  }
13092
- function Df({ className: e, items: t, centerAction: n, showLabels: r, tone: i = "default", maxWidth: a, variant: o = "default", pillPosition: s = "fixed", floatingPosition: c = "center", keyboardBehavior: l = "stay", scrollEdge: u = !1, navLabel: d = "メインナビゲーション", ...f }) {
13093
- let p = Ef(l);
13092
+ function Df({ className: e, items: t, centerAction: n, showLabels: r, tone: i = "default", maxWidth: a, variant: o = "default", pillPosition: s = "fixed", floatingPosition: c = "center", keyboardBehavior: l = "stay", scrollEdge: u = !1, navLabel: d = "メインナビゲーション", showOnDesktop: f = !1, ...p }) {
13093
+ let m = Ef(l);
13094
13094
  return o === "pill" ? /* @__PURE__ */ B(kf, {
13095
13095
  className: e,
13096
13096
  items: t,
@@ -13101,29 +13101,31 @@ function Df({ className: e, items: t, centerAction: n, showLabels: r, tone: i =
13101
13101
  pillPosition: s,
13102
13102
  floatingPosition: c,
13103
13103
  scrollEdge: u,
13104
- keyboardState: p,
13104
+ keyboardState: m,
13105
13105
  navLabel: d,
13106
- ...f
13106
+ showOnDesktop: f,
13107
+ ...p
13107
13108
  }) : /* @__PURE__ */ B(Of, {
13108
13109
  className: e,
13109
13110
  items: t,
13110
- keyboardState: p,
13111
+ keyboardState: m,
13111
13112
  navLabel: d,
13112
- ...f
13113
+ showOnDesktop: f,
13114
+ ...p
13113
13115
  });
13114
13116
  }
13115
- function Of({ className: e, items: t, keyboardState: n, style: r, navLabel: i = "メインナビゲーション", ...a }) {
13117
+ function Of({ className: e, items: t, keyboardState: n, style: r, navLabel: i = "メインナビゲーション", showOnDesktop: a = !1, ...o }) {
13116
13118
  return /* @__PURE__ */ B("nav", {
13117
13119
  "data-slot": "bottom-tab-bar",
13118
13120
  "data-keyboard-behavior": n.keyboardBehavior,
13119
13121
  "data-keyboard-open": n.isKeyboardOpen || void 0,
13120
13122
  "aria-label": i,
13121
- className: G("fixed inset-x-0 z-[var(--Z-Nav)] transition-all duration-[var(--Motion-Duration-Base)]", n.keyboardBehavior === "lift" ? "bottom-[var(--ksk-bottom-tab-bar-keyboard-inset)]" : "bottom-0", "border-t border-[var(--Border-Low-Emphasis)] bg-[var(--Surface-Primary)]", "pb-[env(safe-area-inset-bottom)] lg:hidden", n.shouldHide && "translate-y-2 opacity-0 pointer-events-none invisible", e),
13123
+ className: G("fixed inset-x-0 z-[var(--Z-Nav)] transition-all duration-[var(--Motion-Duration-Base)]", n.keyboardBehavior === "lift" ? "bottom-[var(--ksk-bottom-tab-bar-keyboard-inset)]" : "bottom-0", "border-t border-[var(--Border-Low-Emphasis)] bg-[var(--Surface-Primary)]", "pb-[env(safe-area-inset-bottom)]", a ? "lg:block" : "lg:hidden", n.shouldHide && "translate-y-2 opacity-0 pointer-events-none invisible", e),
13122
13124
  style: {
13123
13125
  "--ksk-bottom-tab-bar-keyboard-inset": `${n.liftInset}px`,
13124
13126
  ...r
13125
13127
  },
13126
- ...a,
13128
+ ...o,
13127
13129
  children: /* @__PURE__ */ B("div", {
13128
13130
  className: "flex h-14 items-center justify-around px-1",
13129
13131
  children: t.map((e) => /* @__PURE__ */ B(Af, {
@@ -13133,17 +13135,17 @@ function Of({ className: e, items: t, keyboardState: n, style: r, navLabel: i =
13133
13135
  })
13134
13136
  });
13135
13137
  }
13136
- function kf({ className: e, items: t, centerAction: n, showLabels: r, tone: i = "default", maxWidth: a = 430, pillPosition: o = "fixed", floatingPosition: s = "center", scrollEdge: c = !1, keyboardState: l, style: d, navLabel: f = "メインナビゲーション", ...p }) {
13137
- let m = !!n || r === !0, h = r ?? !!n, g = n ? Math.ceil(t.length / 2) : t.length, _ = t.slice(0, g), v = t.slice(g), y = u.useRef(null), [b, x] = u.useState(null), [S, C] = u.useState(!1), w = u.useCallback(() => {
13138
- let e = y.current;
13138
+ function kf({ className: e, items: t, centerAction: n, showLabels: r, tone: i = "default", maxWidth: a = 430, pillPosition: o = "fixed", floatingPosition: s = "center", scrollEdge: c = !1, keyboardState: l, style: d, navLabel: f = "メインナビゲーション", showOnDesktop: p = !1, ...m }) {
13139
+ let h = !!n || r === !0, g = r ?? !!n, _ = n ? Math.ceil(t.length / 2) : t.length, v = t.slice(0, _), y = t.slice(_), b = u.useRef(null), [x, S] = u.useState(null), [C, w] = u.useState(!1), T = u.useCallback(() => {
13140
+ let e = b.current;
13139
13141
  if (!e) return;
13140
13142
  let t = e.querySelector("[data-platter-anchor]");
13141
13143
  if (!t) {
13142
- x(null);
13144
+ S(null);
13143
13145
  return;
13144
13146
  }
13145
13147
  let n = e.getBoundingClientRect(), r = t.getBoundingClientRect();
13146
- x((e) => {
13148
+ S((e) => {
13147
13149
  let t = {
13148
13150
  x: r.left - n.left,
13149
13151
  y: r.top - n.top,
@@ -13153,66 +13155,66 @@ function kf({ className: e, items: t, centerAction: n, showLabels: r, tone: i =
13153
13155
  return e && Math.abs(e.x - t.x) < .5 && Math.abs(e.y - t.y) < .5 && Math.abs(e.w - t.w) < .5 && Math.abs(e.h - t.h) < .5 ? e : t;
13154
13156
  });
13155
13157
  }, []);
13156
- Tf(w), u.useEffect(() => {
13157
- if (b && !S) {
13158
- let e = requestAnimationFrame(() => C(!0));
13158
+ Tf(T), u.useEffect(() => {
13159
+ if (x && !C) {
13160
+ let e = requestAnimationFrame(() => w(!0));
13159
13161
  return () => cancelAnimationFrame(e);
13160
13162
  }
13161
- }, [b, S]), u.useEffect(() => {
13162
- let e = y.current;
13163
+ }, [x, C]), u.useEffect(() => {
13164
+ let e = b.current;
13163
13165
  if (!e || typeof ResizeObserver > "u") return;
13164
- let t = new ResizeObserver(w);
13166
+ let t = new ResizeObserver(T);
13165
13167
  return t.observe(e), () => t.disconnect();
13166
- }, [w]);
13167
- let T = s === "left" || s === "right", E = {
13168
+ }, [T]);
13169
+ let E = s === "left" || s === "right", D = {
13168
13170
  "--ksk-bottom-tab-bar-keyboard-inset": `${l.liftInset}px`,
13169
- ...m && !T ? {
13171
+ ...h && !E ? {
13170
13172
  width: "calc(100vw - 24px)",
13171
13173
  maxWidth: a
13172
13174
  } : {},
13173
- ...T ? { maxWidth: "calc(100vw - 92px)" } : {},
13175
+ ...E ? { maxWidth: "calc(100vw - 92px)" } : {},
13174
13176
  ...d
13175
13177
  };
13176
13178
  return /* @__PURE__ */ V(z, { children: [c && /* @__PURE__ */ B("div", {
13177
13179
  "aria-hidden": "true",
13178
- className: G("inset-x-0 bottom-0 z-[var(--Z-Sticky)] h-28 lg:hidden", "glass-scroll-edge-bottom", "transition-opacity duration-[var(--Motion-Duration-Base)]", o === "fixed" ? "fixed" : "absolute", l.shouldHide && "opacity-0")
13180
+ className: G("inset-x-0 bottom-0 z-[var(--Z-Sticky)] h-28", p ? "lg:block" : "lg:hidden", "glass-scroll-edge-bottom", "transition-opacity duration-[var(--Motion-Duration-Base)]", o === "fixed" ? "fixed" : "absolute", l.shouldHide && "opacity-0")
13179
13181
  }), /* @__PURE__ */ V("nav", {
13180
13182
  "data-slot": "bottom-nav-pill",
13181
13183
  "data-tone": i,
13182
13184
  "data-keyboard-behavior": l.keyboardBehavior,
13183
13185
  "data-keyboard-open": l.isKeyboardOpen || void 0,
13184
13186
  "aria-label": f,
13185
- className: G("z-[var(--Z-Nav)] lg:hidden transition-all duration-[var(--Motion-Duration-Base)]", o === "fixed" ? "fixed" : "absolute", l.keyboardBehavior === "lift" ? "bottom-[calc(env(safe-area-inset-bottom)_+_12px_+_var(--ksk-bottom-tab-bar-keyboard-inset))]" : "bottom-[calc(env(safe-area-inset-bottom)_+_12px)]", s === "left" && "left-3 right-20", s === "right" && "right-3 left-20", s === "center" && "left-1/2 -translate-x-1/2", "flex items-center rounded-full glass-specular", i === "inverse" ? "glass-dark" : "glass", m ? "min-h-[var(--Nav-Pill-Min-Height)] gap-[var(--Nav-Pill-Gap)] px-[var(--Nav-Pill-Padding-X)] py-[var(--Nav-Pill-Padding-Y)]" : "h-[58px] gap-0 px-3", l.shouldHide && "translate-y-2 opacity-0 pointer-events-none invisible", e),
13186
- style: E,
13187
- ...p,
13188
- ref: y,
13187
+ className: G("z-[var(--Z-Nav)] transition-all duration-[var(--Motion-Duration-Base)]", p ? "lg:flex" : "lg:hidden", o === "fixed" ? "fixed" : "absolute", l.keyboardBehavior === "lift" ? "bottom-[calc(env(safe-area-inset-bottom)_+_12px_+_var(--ksk-bottom-tab-bar-keyboard-inset))]" : "bottom-[calc(env(safe-area-inset-bottom)_+_12px)]", s === "left" && "left-3 right-20", s === "right" && "right-3 left-20", s === "center" && "left-1/2 -translate-x-1/2", "flex items-center rounded-full glass-specular", i === "inverse" ? "glass-dark" : "glass", h ? "min-h-[var(--Nav-Pill-Min-Height)] gap-[var(--Nav-Pill-Gap)] px-[var(--Nav-Pill-Padding-X)] py-[var(--Nav-Pill-Padding-Y)]" : "h-[58px] gap-0 px-3", l.shouldHide && "translate-y-2 opacity-0 pointer-events-none invisible", e),
13188
+ style: D,
13189
+ ...m,
13190
+ ref: b,
13189
13191
  children: [
13190
- b && /* @__PURE__ */ B("span", {
13192
+ x && /* @__PURE__ */ B("span", {
13191
13193
  "aria-hidden": "true",
13192
13194
  "data-slot": wf,
13193
13195
  "data-nav-platter": !0,
13194
- className: G("absolute left-0 top-0 rounded-full pointer-events-none", S && "transition-[transform,width,height] duration-[var(--Motion-Duration-Base)] ease-[var(--Motion-Easing-Standard)] motion-reduce:transition-none"),
13196
+ className: G("absolute left-0 top-0 rounded-full pointer-events-none", C && "transition-[transform,width,height] duration-[var(--Motion-Duration-Base)] ease-[var(--Motion-Easing-Standard)] motion-reduce:transition-none"),
13195
13197
  style: {
13196
- width: b.w,
13197
- height: b.h,
13198
- transform: `translate3d(${b.x}px, ${b.y}px, 0)`
13198
+ width: x.w,
13199
+ height: x.h,
13200
+ transform: `translate3d(${x.x}px, ${x.y}px, 0)`
13199
13201
  }
13200
13202
  }),
13201
- _.map((e, t) => /* @__PURE__ */ B(Af, {
13203
+ v.map((e, t) => /* @__PURE__ */ B(Af, {
13202
13204
  item: e,
13203
13205
  compact: !0,
13204
- showLabel: h,
13206
+ showLabel: g,
13205
13207
  tone: i,
13206
- suppressPlatter: b !== null
13208
+ suppressPlatter: x !== null
13207
13209
  }, `${e.href ?? e.label}-${t}`)),
13208
13210
  n ? /* @__PURE__ */ B(jf, { item: n }) : null,
13209
- v.map((e, t) => /* @__PURE__ */ B(Af, {
13211
+ y.map((e, t) => /* @__PURE__ */ B(Af, {
13210
13212
  item: e,
13211
13213
  compact: !0,
13212
- showLabel: h,
13214
+ showLabel: g,
13213
13215
  tone: i,
13214
- suppressPlatter: b !== null
13215
- }, `${e.href ?? e.label}-${t + g}`))
13216
+ suppressPlatter: x !== null
13217
+ }, `${e.href ?? e.label}-${t + _}`))
13216
13218
  ]
13217
13219
  })] });
13218
13220
  }
@@ -13282,11 +13284,11 @@ function Mf() {
13282
13284
  })
13283
13285
  });
13284
13286
  }
13285
- function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition: i = "center", className: a }) {
13286
- let o = u.useCallback((e) => {
13287
+ function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition: i = "center", showOnDesktop: a = !1, className: o }) {
13288
+ let s = u.useCallback((e) => {
13287
13289
  let t = e.target;
13288
13290
  t instanceof Element && t.closest("button, a")?.blur();
13289
- }, []), s = e.map((e) => {
13291
+ }, []), c = e.map((e) => {
13290
13292
  let r = e.key === t, i = e.Icon;
13291
13293
  return {
13292
13294
  label: e.label,
@@ -13302,7 +13304,7 @@ function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition
13302
13304
  onClick: () => n(e.key),
13303
13305
  tabKey: e.key
13304
13306
  };
13305
- }), c = r ? {
13307
+ }), l = r ? {
13306
13308
  label: "",
13307
13309
  ariaLabel: r.ariaLabel ?? r.label,
13308
13310
  icon: /* @__PURE__ */ B(Mf, {}),
@@ -13310,15 +13312,16 @@ function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition
13310
13312
  onClick: r.onClick
13311
13313
  } : void 0;
13312
13314
  return /* @__PURE__ */ B(Df, {
13313
- className: a ? `lg:hidden ${a}` : "lg:hidden",
13315
+ className: a ? o : o ? `lg:hidden ${o}` : "lg:hidden",
13314
13316
  variant: "pill",
13315
13317
  pillPosition: "fixed",
13316
13318
  floatingPosition: i,
13317
13319
  showLabels: !0,
13318
13320
  keyboardBehavior: "hide",
13319
- onPointerUpCapture: o,
13320
- items: s,
13321
- centerAction: c
13321
+ onPointerUpCapture: s,
13322
+ items: c,
13323
+ centerAction: l,
13324
+ showOnDesktop: a
13322
13325
  });
13323
13326
  }
13324
13327
  //#endregion
@@ -76,6 +76,20 @@ interface BottomTabBarProps extends React.ComponentProps<"nav"> {
76
76
  scrollEdge?: boolean;
77
77
  /** nav 要素の aria-label。@default "メインナビゲーション" */
78
78
  navLabel?: string;
79
+ /**
80
+ * デスクトップ幅(`lg` = 1024px 以上)でもナビを表示するか(issue #486)。
81
+ *
82
+ * DS の既定はモバイル専用(`lg:hidden`)。PC ではサイドナビや
83
+ * ヘッダーナビへ切り替えるアプリを想定しているため。
84
+ * **デスクトップ用シェルを持たず、全幅でこのナビを出し続けたいアプリ**
85
+ * (消費側でこれを `className="lg:flex"` で毎回打ち消していた)は
86
+ * `showOnDesktop` を渡す。
87
+ *
88
+ * `variant="pill"` では scroll edge の装飾帯も一緒に追従する。
89
+ *
90
+ * @default false(従来どおり lg 以上で非表示)
91
+ */
92
+ showOnDesktop?: boolean;
79
93
  }
80
94
  /**
81
95
  * BottomTabBar — モバイルのグローバルナビゲーション。
@@ -112,6 +126,22 @@ interface BottomTabBarProps extends React.ComponentProps<"nav"> {
112
126
  * `--Nav-Selected-Surface` / `-Shadow` は**宣言したときだけ** DS 既定
113
127
  * (`.glass-accent` / ガラスの選択プラッター)を置き換える。未宣言なら
114
128
  * tone / dark の出し分けは従来どおり DS 側が持つ。
129
+ * - `--Nav-Pill-Surface` / `-Backdrop` / `-Border` / `-Shadow` / `-Specular`
130
+ * … pill 本体の面(issue #486)。DS 既定は Liquid Glass(`.glass`、
131
+ * `tone="inverse"` は `.glass-dark`)で、これも**宣言したときだけ**効く。
132
+ * 背後コンテンツの可読性を優先して不透明なナビにしたい場合に使う:
133
+ *
134
+ * ```css
135
+ * :root {
136
+ * --Nav-Pill-Surface: var(--Surface-Primary);
137
+ * --Nav-Pill-Backdrop: none; (ガラスのぼかしを切る)
138
+ * --Nav-Pill-Specular: none; (スペキュラ / 屈折リムも消す)
139
+ * --Nav-Pill-Shadow: 0 4px 16px rgb(0 0 0 / 0.12);
140
+ * }
141
+ * ```
142
+ *
143
+ * `className` で `.glass` を打ち消すことはできない(Tailwind ユーティリティ
144
+ * ではないため tailwind-merge が落とせない)ので、面の変更は必ずこの変数で行う。
115
145
  * - 寸法系が効くのは prominent レイアウト(`showLabels` か `centerAction` が
116
146
  * ある pill)。アイコンのみの compact pill は契約外で固定。
117
147
  *
@@ -142,6 +172,6 @@ interface BottomTabBarProps extends React.ComponentProps<"nav"> {
142
172
  *
143
173
  * いずれも呼び出し側の義務にしていない(明示値があればそちらが優先)。
144
174
  */
145
- declare function BottomTabBar({ className, items, centerAction, showLabels, tone, maxWidth, variant, pillPosition, floatingPosition, keyboardBehavior, scrollEdge, navLabel, ...props }: BottomTabBarProps): React.JSX.Element;
175
+ declare function BottomTabBar({ className, items, centerAction, showLabels, tone, maxWidth, variant, pillPosition, floatingPosition, keyboardBehavior, scrollEdge, navLabel, showOnDesktop, ...props }: BottomTabBarProps): React.JSX.Element;
146
176
  export { BottomTabBar };
147
177
  export type { BottomTabBarAction, BottomTabBarFloatingPosition, BottomTabBarItem, BottomTabBarKeyboardBehavior, BottomTabBarProps, };
@@ -22,6 +22,11 @@ interface MobileTabBarProps<T extends string> {
22
22
  * addAction と併用しない片側フロート + FAB 併置レイアウトで使う。
23
23
  */
24
24
  floatingPosition?: BottomTabBarFloatingPosition;
25
+ /**
26
+ * デスクトップ幅(`lg` = 1024px 以上)でもナビを表示するか(issue #486)。
27
+ * BottomTabBar へそのままパススルーする。@default false
28
+ */
29
+ showOnDesktop?: boolean;
25
30
  className?: string;
26
31
  }
27
32
  /**
@@ -34,6 +39,6 @@ interface MobileTabBarProps<T extends string> {
34
39
  * - 内部は BottomTabBar(variant="pill", pillPosition="fixed", showLabels,
35
40
  * keyboardBehavior="hide")の薄いラッパー。floatingPosition はパススルー。
36
41
  */
37
- declare function MobileTabBar<T extends string>({ tabs, activeTab, onSelect, addAction, floatingPosition, className, }: MobileTabBarProps<T>): React.JSX.Element;
42
+ declare function MobileTabBar<T extends string>({ tabs, activeTab, onSelect, addAction, floatingPosition, showOnDesktop, className, }: MobileTabBarProps<T>): React.JSX.Element;
38
43
  export { MobileTabBar };
39
44
  export type { MobileTabBarAddAction, MobileTabBarProps, MobileTabItem };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ksk-design-system",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "KSK Design System — フリーランス向けマルチテーマ対応デザインシステム",
6
6
  "license": "MIT",
@@ -164,6 +164,7 @@
164
164
  "lint:prefix-order": "node scripts/check-prefix-order.mjs",
165
165
  "lint:screen-contracts": "node scripts/check-screen-contracts.mjs",
166
166
  "lint:deprecations": "node scripts/check-deprecations.mjs",
167
+ "audit:issue-links": "node scripts/check-issue-close-links.mjs",
167
168
  "lint:migration-doc": "node scripts/generate-migration-doc.mjs --check",
168
169
  "generate:migration-doc": "node scripts/generate-migration-doc.mjs",
169
170
  "metrics": "node scripts/download-metrics.mjs",
@@ -178,6 +179,7 @@
178
179
  "check:native-parity": "node scripts/check-native-parity.mjs",
179
180
  "check:native-lookup": "node scripts/generate-native-component-lookup.mjs --check",
180
181
  "test": "vitest run",
182
+ "test:legacy-radix": "node scripts/test-legacy-radix.mjs",
181
183
  "test:consumer:web": "npm run build:lib && node scripts/test-packed-web-consumer.mjs",
182
184
  "test:consumer:native": "npm run build:lib && node scripts/test-packed-native-consumer.mjs",
183
185
  "test:consumer:cli": "node scripts/test-packed-cli-consumer.mjs",
@@ -185,18 +187,18 @@
185
187
  "test:watch": "vitest",
186
188
  "test:interaction": "vitest run --config vitest.storybook.config.ts",
187
189
  "test:a11y": "vitest run --config vitest.a11y.config.ts",
188
- "check": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.type-tests.json --noEmit && bash scripts/lint-scratch.sh && bash scripts/check-deps.sh && node scripts/check-published-deps.mjs && node scripts/check-css-setup.mjs && bash scripts/check-drift.sh && node scripts/check-screen-contracts.mjs && node scripts/check-docs-drift.mjs && node scripts/check-agents-docs-sync.mjs && node scripts/check-contrast.mjs && node scripts/check-design-md.mjs && node scripts/check-tailwind-v4.mjs && node scripts/check-prefix-order.mjs && node scripts/check-flex-shrink.mjs && node scripts/check-deprecations.mjs && node scripts/check-rules-contract.mjs && node scripts/sync-version.mjs --check && node scripts/generate-migration-doc.mjs --check && bash scripts/check-responsive.sh && node scripts/generate-component-lookup.mjs --check && node scripts/generate-platform-tokens.mjs --check && node scripts/generate-token-hex-cache.mjs --check && node scripts/generate-source-safelist.mjs --check && tsc -p tsconfig.native.json --noEmit && node scripts/check-native-parity.mjs && node scripts/generate-native-component-lookup.mjs --check",
190
+ "check": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.type-tests.json --noEmit && bash scripts/lint-scratch.sh && bash scripts/check-deps.sh && node scripts/check-published-deps.mjs && node scripts/check-radix-floor.mjs && node scripts/check-css-setup.mjs && bash scripts/check-drift.sh && node scripts/check-screen-contracts.mjs && node scripts/check-docs-drift.mjs && node scripts/check-agents-docs-sync.mjs && node scripts/check-contrast.mjs && node scripts/check-design-md.mjs && node scripts/check-tailwind-v4.mjs && node scripts/check-prefix-order.mjs && node scripts/check-flex-shrink.mjs && node scripts/check-deprecations.mjs && node scripts/check-rules-contract.mjs && node scripts/sync-version.mjs --check && node scripts/generate-migration-doc.mjs --check && bash scripts/check-responsive.sh && node scripts/generate-component-lookup.mjs --check && node scripts/generate-platform-tokens.mjs --check && node scripts/generate-token-hex-cache.mjs --check && node scripts/generate-source-safelist.mjs --check && tsc -p tsconfig.native.json --noEmit && node scripts/check-native-parity.mjs && node scripts/generate-native-component-lookup.mjs --check",
189
191
  "check:agent": "npm run lint && npm run check && npm test && npm run lint:story-reuse",
190
192
  "version": "node scripts/sync-version.mjs",
191
193
  "mcp:build": "cd mcp-server && npm install && npm run build",
192
194
  "mcp:start": "node mcp-server/dist/index.js"
193
195
  },
194
196
  "dependencies": {
195
- "@radix-ui/react-slot": "^1.0.0",
197
+ "@radix-ui/react-slot": "^1.3.0",
196
198
  "class-variance-authority": "^0.7.1",
197
199
  "clsx": "^2.1.1",
198
200
  "iconsax-reactjs": "^0.0.8",
199
- "radix-ui": "^1.0.0",
201
+ "radix-ui": "^1.6.1",
200
202
  "react-day-picker": "^10.0.1",
201
203
  "react-hook-form": "^7.75.0",
202
204
  "tailwind-merge": "^3.5.0"
@@ -260,18 +262,19 @@
260
262
  "iconsax-reactjs": "^0.0.8",
261
263
  "jsdom": "^25.0.1",
262
264
  "playwright": "^1.62.0",
263
- "radix-ui": "^1.4.3",
265
+ "radix-ui": "^1.6.1",
264
266
  "react": "^19.2.0",
265
267
  "react-dom": "^19.2.0",
266
268
  "react-native": "^0.86.0",
267
269
  "react-native-web": "^0.21.2",
270
+ "semver": "^7.8.5",
268
271
  "storybook": "^10.4.6",
269
272
  "tailwindcss": "^4.2.1",
270
273
  "tw-animate-css": "^1.4.0",
274
+ "typescript": "^6.0.3",
271
275
  "typescript-eslint": "^8.48.0",
272
276
  "vite": "^8.1.3",
273
277
  "vitest": "^4.1.7",
274
- "zod": "^4.4.2",
275
- "typescript": "^6.0.3"
278
+ "zod": "^4.4.2"
276
279
  }
277
280
  }
@@ -168,7 +168,7 @@
168
168
 
169
169
  | Component | Import | Variants | Stories |
170
170
  |-----------|--------|----------|---------|
171
- | BottomTabBar | `@/components/patterns/commerce/bottom-tab-bar` | — | Default, Pill — iOS 26 Liquid Glass, Pill — on light background, Liquid Glass — center action, Liquid Glass — labels visible, Liquid Glass — 5 items, Liquid Glass — over dark photo/gradient, Liquid Glass — light fallback, Liquid Glass — sliding selection (interactive), Liquid Glass — scroll edge effect, Floating position — left + FAB, Floating position — right + FAB, Keyboard behavior — hide |
171
+ | BottomTabBar | `@/components/patterns/commerce/bottom-tab-bar` | — | Default, Pill — iOS 26 Liquid Glass, Pill — on light background, Liquid Glass — center action, Liquid Glass — labels visible, Liquid Glass — 5 items, Liquid Glass — over dark photo/gradient, Liquid Glass — light fallback, Liquid Glass — sliding selection (interactive), Liquid Glass — scroll edge effect, Floating position — left + FAB, Floating position — right + FAB, Keyboard behavior — hide, Pill の面を不透明にする(--Nav-Pill-*), デスクトップ幅でも表示する(showOnDesktop) |
172
172
  | FilterBar | `@/components/patterns/commerce/filter-bar` | — | Default, Interactive |
173
173
  | ImageCarousel | `@/components/patterns/commerce/image-carousel` | — | Banner, Square, Video, AutoPlay |
174
174
  | MobileTabBar | `@/components/patterns/commerce/mobile-tab-bar` | — | Default, With addAction (center CTA), floatingPosition=, ProductCustomization |
@@ -8,7 +8,7 @@
8
8
  (belle-todo)が壊れ、`!important` の山が残った事故が由来。
9
9
 
10
10
  ── 寸法は product-theme.css の --Nav-* が担当(:root に既定値あり)。
11
- ── 面(背景・境界・影)はここが担当し、**既定値を :root に置かない**。
11
+ ── 面(背景・backdrop・境界・影)はここが担当し、**既定値を :root に置かない**。
12
12
  var(--Nav-*, <DS 既定>) の形で書き、消費側が宣言したときだけ上書きされる。
13
13
  宣言しなければ tone="inverse" / dark の出し分けは従来どおり DS が持つ。
14
14
 
@@ -28,6 +28,50 @@
28
28
  SSR / 計測前に面を兼ねるタブ本体・アイコン領域)に当たる。
29
29
  ============================================================= */
30
30
 
31
+ /* ─── pill 本体の面(issue #486)───
32
+ DS 既定は Liquid Glass(`.glass` / tone="inverse" は `.glass-dark`)。
33
+ 背後コンテンツの可読性を優先して**不透明な面**にしたい消費側が、
34
+ 内部クラスに触らず面だけ差し替えられるようにする。
35
+
36
+ `.glass` / `.glass-dark` と同じ詳細度(0,1,0)だが、glass.css より後に
37
+ 読み込まれるこのファイルが勝つ(中央アクションと同じ手口)。
38
+ フォールバックは `.glass` 自身が使う内部変数そのものなので、
39
+ 未宣言時のレンダリングは 1px も変わらない。
40
+
41
+ backdrop-filter は `none` を宣言すればガラスのぼかしを完全に切れる。
42
+ -webkit- を先・標準形を後に書く(消費側 minifier の dedupe 対策)。 */
43
+ [data-slot="bottom-nav-pill"] {
44
+ background: var(--Nav-Pill-Surface, var(--glass-bg));
45
+ -webkit-backdrop-filter: var(--Nav-Pill-Backdrop, var(--glass-blur));
46
+ backdrop-filter: var(--Nav-Pill-Backdrop, var(--glass-blur));
47
+ border: var(--Nav-Pill-Border, 0.5px solid var(--glass-border));
48
+ box-shadow: var(--Nav-Pill-Shadow, var(--glass-highlight), var(--glass-shadow));
49
+ }
50
+
51
+ [data-slot="bottom-nav-pill"][data-tone="inverse"] {
52
+ background: var(--Nav-Pill-Surface, var(--glass-bg-dark));
53
+ -webkit-backdrop-filter: var(--Nav-Pill-Backdrop, var(--glass-blur-media-dark));
54
+ backdrop-filter: var(--Nav-Pill-Backdrop, var(--glass-blur-media-dark));
55
+ border: var(--Nav-Pill-Border, 0.5px solid var(--glass-border-dark));
56
+ box-shadow: var(
57
+ --Nav-Pill-Shadow,
58
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
59
+ 0 8px 32px rgba(0, 0, 0, 0.28)
60
+ );
61
+ }
62
+
63
+ /* ─── pill のスペキュラ / 屈折リム(issue #486)───
64
+ `.glass-specular` の ::before(レンズ上面)と ::after(エッジの屈折リム)は
65
+ 面を不透明にしても残るため、ガラスを切りたい消費側にはノイズになる。
66
+
67
+ 既定は `.glass-specular` 自身と同じ `content: ""` なので**未宣言なら何も変わらない**。
68
+ `--Nav-Pill-Specular: none` を宣言すると擬似要素そのものが生成されなくなり、
69
+ リムとスペキュラが消える。 */
70
+ [data-slot="bottom-nav-pill"]::before,
71
+ [data-slot="bottom-nav-pill"]::after {
72
+ content: var(--Nav-Pill-Specular, "");
73
+ }
74
+
31
75
  /* ─── 中央アクションの面 ───
32
76
  .glass-accent と同じ詳細度(0,1,0)なので、glass.css より後に読み込まれる
33
77
  このファイルが勝つ。フォールバックは .glass-accent 自身が同じ要素に載せる