ksk-design-system 2.0.0 → 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/MIGRATION.md CHANGED
@@ -116,7 +116,7 @@ install に失敗しうるため、SemVer どおり major に倒した。
116
116
 
117
117
  ## v1 系内の minor 変更(参考)
118
118
 
119
- ### 次のリリース — `ResponsiveOverlayFrame` に `preset="plain"` を追加(issue #486)
119
+ ### v1.70.0 — `ResponsiveOverlayFrame` に `preset="plain"` を追加(issue #486)
120
120
 
121
121
  `side="bottom"` の経路は必ず `BottomSheetFrame` の preset を通るため、
122
122
  `sm:`(640px 以上)でフロートカード化し、内側 padding も落ちる(`p-0`)。
@@ -162,7 +162,7 @@ install に失敗しうるため、SemVer どおり major に倒した。
162
162
  - `desktopPosition` は `"center"` / `"top"` のみ。`"fullscreen"` は plain の
163
163
  幅指定と噛み合わないため型で禁止している。
164
164
 
165
- ### 次のリリース — `ResponsiveOverlayFrame` が float / float-glass シートを受けられるようになった(issue #479)
165
+ ### v1.69.0 — `ResponsiveOverlayFrame` が float / float-glass シートを受けられるようになった(issue #479)
166
166
 
167
167
  #472 で入った `ResponsiveOverlayFrame` は内部で `BottomSheetFrame`(`side="bottom"` 固定)を
168
168
  使うため、左右・下に余白を持つカード型のシート(`side="float"` / `"float-glass"`)を
@@ -226,7 +226,7 @@ global CSS(`!important`)を残す必要があった。
226
226
  `side="bottom"` 専用なので、`snapPoints` と float 系の併用は snap もデスクトップ変換も
227
227
  効かない「何も起きない」組み合わせになる。どちらかに寄せること。
228
228
 
229
- ### 次のリリース — `ResponsiveOverlayFrame` 追加(`BottomSheetFrame` + 消費側デスクトップ CSS からの移行 / issue #472)
229
+ ### v1.67.0 — `ResponsiveOverlayFrame` 追加(`BottomSheetFrame` + 消費側デスクトップ CSS からの移行 / issue #472)
230
230
 
231
231
  `BottomSheetFrame` はモバイルの preset(`mobile-full` / `mobile-page` /
232
232
  `mobile-form`)を持つ一方でデスクトップでは中央モーダルにならず、消費側は
@@ -298,7 +298,7 @@ global CSS で `position` / `transform` / `width` / `max-height` / `radius` を
298
298
  既存の `BottomSheetFrame` / `ResponsiveDialogContent` は非推奨ではない
299
299
  (モバイル専用面・preset 不要の面では引き続きそのまま使う)。
300
300
 
301
- ### 次のリリース — `typescript` が必須依存から optional peer dependency になった(issue #409)
301
+ ### v1.62.0 — `typescript` が必須依存から optional peer dependency になった(issue #409)
302
302
 
303
303
  CLI の1ルール(P046)のためだけに typescript(24MB)が全 consumer の
304
304
  node_modules に入っていたのを解消した。install サイズが減り、`ksk-ds lint` の
@@ -311,7 +311,7 @@ node_modules に入っていたのを解消した。install サイズが減り
311
311
  typescript を追加すること
312
312
 
313
313
 
314
- ### 次のリリース — `ksk-ds lint` の severity 語彙が `warn` → `warning` に統一(要確認)
314
+ ### v1.62.0 — `ksk-ds lint` の severity 語彙が `warn` → `warning` に統一(要確認)
315
315
 
316
316
  `--format json` の `results[].severity` と text 出力の severity 表記が、これまで
317
317
  `"warn"` と `"warning"` で不統一だった(contract の `contracts/rules.json` は
@@ -335,7 +335,7 @@ text 出力の要約行も `0 error / 3 warn` → `0 error / 3 warning` に変
335
335
  - **未知のオプションはエラー終了**(従来は黙って捨てられ、CI に書いた
336
336
  `--strict` が無言で効かないまま緑になっていた)
337
337
 
338
- ### 次のリリース — `ksk-ds lint` の escape がルール単位・行単位で書けるようになった
338
+ ### v1.62.0 — `ksk-ds lint` の escape がルール単位・行単位で書けるようになった
339
339
 
340
340
  ファイル全体・全ルールを無期限に外す `// ksk-ds-allow-custom-ui: 理由` は
341
341
  後方互換で残るが**非推奨**。新規はルール単位を使う(issue #405):
@@ -351,7 +351,7 @@ ignore は効かない。あわせて escape マーカーは**文字列リテラ
351
351
  無効化されていた)。既に short な理由を書いていたファイルは lint が復活するので、
352
352
  理由を書き足すか違反を直すこと。
353
353
 
354
- ### 次のリリース — 自動生成物と DS 自身のトークン CSS を lint 対象外にした
354
+ ### v1.62.0 — 自動生成物と DS 自身のトークン CSS を lint 対象外にした
355
355
 
356
356
  - 先頭 12 行に自動生成マーカー(`AUTO-GENERATED` / `DO NOT EDIT` / `自動生成` 等)を
357
357
  持つファイルは全ルールを skip する(issue #408)。直す先は生成物ではなく生成元。
@@ -359,7 +359,7 @@ ignore は効かない。あわせて escape マーカーは**文字列リテラ
359
359
  トークン定義 CSS を対象外にする(issue #407)。`vendor/ksk-design-system/**` のように
360
360
  DS を再梱包しているリポで P049 が大量に出ていたのが解消する。
361
361
 
362
- ### 次のリリース — `Footer` の `paymentIcons` 既定値が空になった(挙動変更)
362
+ ### v1.62.0 — `Footer` の `paymentIcons` 既定値が空になった(挙動変更)
363
363
 
364
364
  `Footer` は `paymentIcons` を省略すると `["VISA", "Master", "JCB", "AmEx", "PayPay", "LINE Pay"]`
365
365
  の決済バッジを既定で描画していた。EC 以外のプロダクトでも「取り扱っていない決済手段」が
@@ -376,7 +376,7 @@ ignore は効かない。あわせて escape マーカーは**文字列リテラ
376
376
 
377
377
  `paymentIcons` を明示的に渡していた箇所は影響なし。
378
378
 
379
- ### 次のリリース — `postinstall` による AI ルールファイル自動設置を廃止(要確認)
379
+ ### v1.60.0 — `postinstall` による AI ルールファイル自動設置を廃止(要確認)
380
380
 
381
381
  これまで `npm install ksk-design-system` の `postinstall` フックが、消費側の
382
382
  プロジェクトルートに `CLAUDE.md` / `AGENTS.md`(`node_modules` 内の DS ルールを指す
@@ -397,7 +397,7 @@ npx ksk-ds init --force # 既存ファイルを最新テンプレートで上
397
397
  `postinstall` を持つ限りバージョンを上げても判定が引き継がれる。あわせて
398
398
  `INIT_CWD` 参照も削除し、書き込み先はコマンドを実行したディレクトリに固定した。
399
399
 
400
- ### 次のリリース — PillToggle の onChange/onValueChange 統一(破壊変更なし)
400
+ ### v1.49.0 — PillToggle の onChange/onValueChange 統一(破壊変更なし)
401
401
 
402
402
  `PillToggle` のみ他コンポーネント(Switch 等の native 系を除く)と異なり `onValueChange`
403
403
  を主 API として案内していたため、`onChange` に統一した(issue #264⑥)。
@@ -414,7 +414,7 @@ npx ksk-ds init --force # 既存ファイルを最新テンプレートで上
414
414
  `onChange` と `onValueChange` を両方渡した場合は `onChange` が優先される。
415
415
  `onValueChange` は将来のメジャーバージョンで削除予定。
416
416
 
417
- ### 次のリリース — Tailwind 4.1 以上が必要(要確認)
417
+ ### v1.49.0 — Tailwind 4.1 以上が必要(要確認)
418
418
 
419
419
  preset に DS 内部ユーティリティの safelist(`src/styles/source-safelist.css`・自動生成)を同梱した(issue #258)。
420
420
  これにより、消費側の `@source ".../ksk-design-system/dist"` の設定漏れやパスずれで
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.0",
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.0",
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
@@ -1022,13 +1022,30 @@ function $t(e, t) {
1022
1022
  e && typeof e == "object" && (e.current = t);
1023
1023
  }
1024
1024
  function en(e, t) {
1025
- return u.useCallback((n) => {
1026
- e(n);
1027
- let r = $t(t, n);
1028
- return () => {
1029
- e(null), r ? r() : $t(t, null);
1025
+ let n = u.useRef(e), r = u.useRef(t), i = u.useRef(null), a = u.useRef(null), o = u.useRef(null), s = u.useCallback(() => {
1026
+ let e = o.current;
1027
+ e && (o.current = null, e.cleanup ? e.cleanup() : $t(e.ref, null));
1028
+ }, []), c = u.useCallback((e, t) => {
1029
+ o.current = {
1030
+ ref: e,
1031
+ cleanup: $t(e, t)
1030
1032
  };
1031
- }, [e, t]);
1033
+ }, []);
1034
+ return u.useInsertionEffect(() => {
1035
+ n.current = e, r.current = t;
1036
+ let l = i.current;
1037
+ if (l != null) {
1038
+ if (a.current !== e) {
1039
+ let t = a.current;
1040
+ a.current = e, t?.(null), e(l);
1041
+ }
1042
+ o.current?.ref !== t && (s(), c(t, l));
1043
+ }
1044
+ }), u.useCallback((e) => (i.current = e, a.current = e == null ? null : n.current, n.current(e), s(), e != null && c(r.current, e), () => {
1045
+ i.current = null;
1046
+ let e = a.current;
1047
+ a.current = null, e?.(null), s();
1048
+ }), [c, s]);
1032
1049
  }
1033
1050
  //#endregion
1034
1051
  //#region src/lib/title-level.tsx
@@ -12689,8 +12706,8 @@ function of(e) {
12689
12706
  }
12690
12707
  var sf = "min(85dvh, 46rem)", cf = "min(90dvh, 46rem)";
12691
12708
  function lf(e, t, n) {
12692
- let r = u.useRef(null), [i, a] = u.useState(0), o = u.useCallback((e) => {
12693
- r.current = e, a((e) => e + 1);
12709
+ let r = u.useRef(null), [i, a] = u.useState(null), o = u.useCallback((e) => {
12710
+ r.current = e, a(e);
12694
12711
  }, []), [s, c] = u.useState();
12695
12712
  return u.useLayoutEffect(() => {
12696
12713
  let n = r.current;
@@ -13072,8 +13089,8 @@ function Ef(e) {
13072
13089
  liftInset: e === "lift" ? t : 0
13073
13090
  };
13074
13091
  }
13075
- 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 }) {
13076
- 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);
13077
13094
  return o === "pill" ? /* @__PURE__ */ B(kf, {
13078
13095
  className: e,
13079
13096
  items: t,
@@ -13084,29 +13101,31 @@ function Df({ className: e, items: t, centerAction: n, showLabels: r, tone: i =
13084
13101
  pillPosition: s,
13085
13102
  floatingPosition: c,
13086
13103
  scrollEdge: u,
13087
- keyboardState: p,
13104
+ keyboardState: m,
13088
13105
  navLabel: d,
13089
- ...f
13106
+ showOnDesktop: f,
13107
+ ...p
13090
13108
  }) : /* @__PURE__ */ B(Of, {
13091
13109
  className: e,
13092
13110
  items: t,
13093
- keyboardState: p,
13111
+ keyboardState: m,
13094
13112
  navLabel: d,
13095
- ...f
13113
+ showOnDesktop: f,
13114
+ ...p
13096
13115
  });
13097
13116
  }
13098
- 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 }) {
13099
13118
  return /* @__PURE__ */ B("nav", {
13100
13119
  "data-slot": "bottom-tab-bar",
13101
13120
  "data-keyboard-behavior": n.keyboardBehavior,
13102
13121
  "data-keyboard-open": n.isKeyboardOpen || void 0,
13103
13122
  "aria-label": i,
13104
- 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),
13105
13124
  style: {
13106
13125
  "--ksk-bottom-tab-bar-keyboard-inset": `${n.liftInset}px`,
13107
13126
  ...r
13108
13127
  },
13109
- ...a,
13128
+ ...o,
13110
13129
  children: /* @__PURE__ */ B("div", {
13111
13130
  className: "flex h-14 items-center justify-around px-1",
13112
13131
  children: t.map((e) => /* @__PURE__ */ B(Af, {
@@ -13116,17 +13135,17 @@ function Of({ className: e, items: t, keyboardState: n, style: r, navLabel: i =
13116
13135
  })
13117
13136
  });
13118
13137
  }
13119
- 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 }) {
13120
- 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(() => {
13121
- 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;
13122
13141
  if (!e) return;
13123
13142
  let t = e.querySelector("[data-platter-anchor]");
13124
13143
  if (!t) {
13125
- x(null);
13144
+ S(null);
13126
13145
  return;
13127
13146
  }
13128
13147
  let n = e.getBoundingClientRect(), r = t.getBoundingClientRect();
13129
- x((e) => {
13148
+ S((e) => {
13130
13149
  let t = {
13131
13150
  x: r.left - n.left,
13132
13151
  y: r.top - n.top,
@@ -13136,66 +13155,66 @@ function kf({ className: e, items: t, centerAction: n, showLabels: r, tone: i =
13136
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;
13137
13156
  });
13138
13157
  }, []);
13139
- Tf(w), u.useEffect(() => {
13140
- if (b && !S) {
13141
- let e = requestAnimationFrame(() => C(!0));
13158
+ Tf(T), u.useEffect(() => {
13159
+ if (x && !C) {
13160
+ let e = requestAnimationFrame(() => w(!0));
13142
13161
  return () => cancelAnimationFrame(e);
13143
13162
  }
13144
- }, [b, S]), u.useEffect(() => {
13145
- let e = y.current;
13163
+ }, [x, C]), u.useEffect(() => {
13164
+ let e = b.current;
13146
13165
  if (!e || typeof ResizeObserver > "u") return;
13147
- let t = new ResizeObserver(w);
13166
+ let t = new ResizeObserver(T);
13148
13167
  return t.observe(e), () => t.disconnect();
13149
- }, [w]);
13150
- let T = s === "left" || s === "right", E = {
13168
+ }, [T]);
13169
+ let E = s === "left" || s === "right", D = {
13151
13170
  "--ksk-bottom-tab-bar-keyboard-inset": `${l.liftInset}px`,
13152
- ...m && !T ? {
13171
+ ...h && !E ? {
13153
13172
  width: "calc(100vw - 24px)",
13154
13173
  maxWidth: a
13155
13174
  } : {},
13156
- ...T ? { maxWidth: "calc(100vw - 92px)" } : {},
13175
+ ...E ? { maxWidth: "calc(100vw - 92px)" } : {},
13157
13176
  ...d
13158
13177
  };
13159
13178
  return /* @__PURE__ */ V(z, { children: [c && /* @__PURE__ */ B("div", {
13160
13179
  "aria-hidden": "true",
13161
- 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")
13162
13181
  }), /* @__PURE__ */ V("nav", {
13163
13182
  "data-slot": "bottom-nav-pill",
13164
13183
  "data-tone": i,
13165
13184
  "data-keyboard-behavior": l.keyboardBehavior,
13166
13185
  "data-keyboard-open": l.isKeyboardOpen || void 0,
13167
13186
  "aria-label": f,
13168
- 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),
13169
- style: E,
13170
- ...p,
13171
- 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,
13172
13191
  children: [
13173
- b && /* @__PURE__ */ B("span", {
13192
+ x && /* @__PURE__ */ B("span", {
13174
13193
  "aria-hidden": "true",
13175
13194
  "data-slot": wf,
13176
13195
  "data-nav-platter": !0,
13177
- 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"),
13178
13197
  style: {
13179
- width: b.w,
13180
- height: b.h,
13181
- 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)`
13182
13201
  }
13183
13202
  }),
13184
- _.map((e, t) => /* @__PURE__ */ B(Af, {
13203
+ v.map((e, t) => /* @__PURE__ */ B(Af, {
13185
13204
  item: e,
13186
13205
  compact: !0,
13187
- showLabel: h,
13206
+ showLabel: g,
13188
13207
  tone: i,
13189
- suppressPlatter: b !== null
13208
+ suppressPlatter: x !== null
13190
13209
  }, `${e.href ?? e.label}-${t}`)),
13191
13210
  n ? /* @__PURE__ */ B(jf, { item: n }) : null,
13192
- v.map((e, t) => /* @__PURE__ */ B(Af, {
13211
+ y.map((e, t) => /* @__PURE__ */ B(Af, {
13193
13212
  item: e,
13194
13213
  compact: !0,
13195
- showLabel: h,
13214
+ showLabel: g,
13196
13215
  tone: i,
13197
- suppressPlatter: b !== null
13198
- }, `${e.href ?? e.label}-${t + g}`))
13216
+ suppressPlatter: x !== null
13217
+ }, `${e.href ?? e.label}-${t + _}`))
13199
13218
  ]
13200
13219
  })] });
13201
13220
  }
@@ -13265,11 +13284,11 @@ function Mf() {
13265
13284
  })
13266
13285
  });
13267
13286
  }
13268
- function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition: i = "center", className: a }) {
13269
- 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) => {
13270
13289
  let t = e.target;
13271
13290
  t instanceof Element && t.closest("button, a")?.blur();
13272
- }, []), s = e.map((e) => {
13291
+ }, []), c = e.map((e) => {
13273
13292
  let r = e.key === t, i = e.Icon;
13274
13293
  return {
13275
13294
  label: e.label,
@@ -13285,7 +13304,7 @@ function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition
13285
13304
  onClick: () => n(e.key),
13286
13305
  tabKey: e.key
13287
13306
  };
13288
- }), c = r ? {
13307
+ }), l = r ? {
13289
13308
  label: "",
13290
13309
  ariaLabel: r.ariaLabel ?? r.label,
13291
13310
  icon: /* @__PURE__ */ B(Mf, {}),
@@ -13293,15 +13312,16 @@ function Nf({ tabs: e, activeTab: t, onSelect: n, addAction: r, floatingPosition
13293
13312
  onClick: r.onClick
13294
13313
  } : void 0;
13295
13314
  return /* @__PURE__ */ B(Df, {
13296
- className: a ? `lg:hidden ${a}` : "lg:hidden",
13315
+ className: a ? o : o ? `lg:hidden ${o}` : "lg:hidden",
13297
13316
  variant: "pill",
13298
13317
  pillPosition: "fixed",
13299
13318
  floatingPosition: i,
13300
13319
  showLabels: !0,
13301
13320
  keyboardBehavior: "hide",
13302
- onPointerUpCapture: o,
13303
- items: s,
13304
- centerAction: c
13321
+ onPointerUpCapture: s,
13322
+ items: c,
13323
+ centerAction: l,
13324
+ showOnDesktop: a
13305
13325
  });
13306
13326
  }
13307
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 };
@@ -13,6 +13,25 @@ import { type SheetSurface } from "./sheet-surface";
13
13
  * float 系では `preset` は効かず、代わりに `padding` が効く。
14
14
  */
15
15
  type ResponsiveOverlaySide = "bottom" | "float" | "float-glass";
16
+ /**
17
+ * 面に実際に効いている `max-height`(実測値)。
18
+ *
19
+ * 既定キャップは className でも上書きできる(desktopClassName の max-h ユーティリティ)。
20
+ * inline style は className より強いので、経路の既定値だけを見て畳むと
21
+ * consumer が締めたキャップを緩めてしまう(#487 の Codex レビュー指摘)。
22
+ * 補正が当たっていない間の computed 値を読めば、className / inline style /
23
+ * 経路の既定のどれで決まっていても「いま効いているキャップ」が取れる。
24
+ *
25
+ * 補正が当たっている間は自分の inline 値を読み返してしまうので、その間だけ
26
+ * inline の max-height を外して測り、すぐ戻す(layout effect の中なので
27
+ * 塗り替えは挟まらない)。「Sheet からの切り替え直後にすでにキーボードが
28
+ * 出ている」(iPad を境界跨ぎで回転した等)ケースでも初回から実測できる
29
+ * ようにするため、active を理由に測定を諦めない(#487 の Codex レビュー指摘)。
30
+ */
31
+ export declare function useEffectiveMaxHeight(active: boolean, consumerInlineMaxHeight: string, classSignature: string): {
32
+ setElement: (node: HTMLDivElement | null) => void;
33
+ measured: string | undefined;
34
+ };
16
35
  /**
17
36
  * ズームを打ち消したソフトキーボード高さ(レイアウト px / issue #487)。
18
37
  *
@@ -17,6 +17,13 @@ export declare function assignRef<T>(ref: React.Ref<T> | undefined, node: T | nu
17
17
  * - consumer が cleanup を返していれば、それを呼ぶ
18
18
  * - 返していなければ(object ref / 旧来のコールバック ref)`null` を書き戻す
19
19
  *
20
+ * **返すコールバック ref の identity は固定する**(issue #516)。React は
21
+ * コールバック ref の identity が render 間で変わると detach → re-attach を
22
+ * 行うため、`internal` / `external` を依存にして毎 render 作り直すと、
23
+ * consumer が毎 render 新しい関数を渡す呼び出し面(Radix の composeRefs が
24
+ * 生成する合成関数など)で ref chain 全体の付け外しが増える。identity を
25
+ * 固定し、差し替えは insertion effect で node を付け替えることで吸収する。
26
+ *
20
27
  * @param internal 内部側の setter。attach 時に node、detach 時に null で呼ばれる
21
28
  * @param external consumer から渡された ref(未指定可)
22
29
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ksk-design-system",
3
- "version": "2.0.0",
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 自身が同じ要素に載せる