sparkle-design 1.0.7 → 2.0.0-beta.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 (61) hide show
  1. package/dist/components/ui/badge/index.d.ts +2 -2
  2. package/dist/components/ui/badge/index.js +8 -8
  3. package/dist/components/ui/breadcrumb/index.d.ts +6 -6
  4. package/dist/components/ui/breadcrumb/index.js +2 -2
  5. package/dist/components/ui/button/index.d.ts +70 -5
  6. package/dist/components/ui/button/index.js +257 -103
  7. package/dist/components/ui/card/index.d.ts +119 -23
  8. package/dist/components/ui/card/index.js +155 -45
  9. package/dist/components/ui/checkbox/index.d.ts +1 -1
  10. package/dist/components/ui/checkbox/index.js +23 -23
  11. package/dist/components/ui/dialog/index.d.ts +15 -15
  12. package/dist/components/ui/dialog/index.js +6 -6
  13. package/dist/components/ui/divider/index.d.ts +5 -2
  14. package/dist/components/ui/divider/index.js +6 -11
  15. package/dist/components/ui/form/index.d.ts +10 -7
  16. package/dist/components/ui/form/index.js +17 -7
  17. package/dist/components/ui/form/label.d.ts +1 -1
  18. package/dist/components/ui/icon/index.d.ts +6 -3
  19. package/dist/components/ui/icon/index.js +4 -9
  20. package/dist/components/ui/icon-button/index.d.ts +118 -5
  21. package/dist/components/ui/icon-button/index.js +270 -52
  22. package/dist/components/ui/inline-message/index.d.ts +13 -4
  23. package/dist/components/ui/inline-message/index.js +20 -20
  24. package/dist/components/ui/input/index.d.ts +6 -3
  25. package/dist/components/ui/input/index.js +14 -14
  26. package/dist/components/ui/input-password/index.d.ts +4 -1
  27. package/dist/components/ui/input-password/index.js +14 -2
  28. package/dist/components/ui/link/index.d.ts +13 -6
  29. package/dist/components/ui/link/index.js +16 -13
  30. package/dist/components/ui/modal/index.d.ts +11 -11
  31. package/dist/components/ui/modal/index.js +2 -2
  32. package/dist/components/ui/overlay/index.d.ts +5 -2
  33. package/dist/components/ui/overlay/index.js +4 -5
  34. package/dist/components/ui/radio/index.d.ts +10 -4
  35. package/dist/components/ui/radio/index.js +90 -20
  36. package/dist/components/ui/select/index.d.ts +11 -11
  37. package/dist/components/ui/select/index.js +13 -13
  38. package/dist/components/ui/skeleton/index.d.ts +1 -1
  39. package/dist/components/ui/skeleton/index.js +1 -1
  40. package/dist/components/ui/slider/index.d.ts +53 -1
  41. package/dist/components/ui/slider/index.js +75 -13
  42. package/dist/components/ui/spinner/index.d.ts +6 -3
  43. package/dist/components/ui/spinner/index.js +5 -10
  44. package/dist/components/ui/switch/index.d.ts +1 -1
  45. package/dist/components/ui/switch/index.js +11 -7
  46. package/dist/components/ui/tabs/index.d.ts +8 -4
  47. package/dist/components/ui/tabs/index.js +40 -37
  48. package/dist/components/ui/tag/index.d.ts +6 -3
  49. package/dist/components/ui/tag/index.js +19 -24
  50. package/dist/components/ui/textarea/index.d.ts +5 -2
  51. package/dist/components/ui/textarea/index.js +9 -14
  52. package/dist/components/ui/toast/index.d.ts +1 -1
  53. package/dist/components/ui/toast/index.js +1 -1
  54. package/dist/components/ui/tooltip/index.d.ts +4 -4
  55. package/dist/components/ui/tooltip/index.js +1 -1
  56. package/dist/lib/utils.d.ts +8 -0
  57. package/dist/lib/utils.js +60 -1
  58. package/dist/test/helpers.d.ts +3 -2
  59. package/dist/test/helpers.js +23 -4
  60. package/dist/test/setup.js +37 -31
  61. package/package.json +17 -10
@@ -24,14 +24,16 @@ import { Spinner } from "../../../components/ui/spinner/index.js";
24
24
  const buttonVariants = cva([
25
25
  // `relative` is required because we absolutely-position the loading spinner at the center.
26
26
  "relative inline-flex items-center justify-center gap-0.5 whitespace-nowrap rounded-action transition-colors",
27
- "cursor-pointer disabled:cursor-not-allowed",
27
+ "cursor-pointer disabled:cursor-not-allowed aria-disabled:cursor-not-allowed",
28
28
  "shrink-0 outline-none",
29
- "focus-visible:ring-2 focus-visible:ring-[var(--color-ring-normal)] focus-visible:ring-offset-2",
29
+ "focus-visible:ring-2 focus-visible:ring-border-ring focus-visible:ring-offset-2",
30
30
  ].join(" "), {
31
31
  variants: {
32
32
  variant: {
33
- solid: "border shadow-raise",
34
- outline: "border shadow-raise",
33
+ // solid の 1px 枠線は Figma の刷新で削除された(shadow も base = 影なし)
34
+ // en: The 1px border on solid was removed in the Figma refresh (shadow is base = none).
35
+ solid: "shadow-base",
36
+ outline: "border shadow-base",
35
37
  ghost: "",
36
38
  },
37
39
  size: {
@@ -56,22 +58,20 @@ const buttonVariants = cva([
56
58
  theme: "primary",
57
59
  isLoading: false,
58
60
  className: [
59
- "bg-primary-500",
60
- "text-white",
61
- "border-primary-600",
62
- "hover:bg-primary-600",
63
- "hover:border-primary-700",
64
- "active:bg-primary-700",
65
- "active:border-primary-800",
66
- "disabled:bg-primary-200",
67
- "disabled:border-none",
61
+ "bg-surface-primary-high-enabled",
62
+ "text-text-inverse",
63
+ "hover:bg-surface-primary-high-hover",
64
+ "active:bg-surface-primary-high-active",
65
+ "disabled:bg-surface-primary-high-disabled aria-disabled:bg-surface-primary-high-disabled",
68
66
  ].join(" "),
69
67
  },
70
68
  {
69
+ // ローディング中に見えるのはスピナー(currentColor 継承)なので object トークンで着色する
70
+ // en: Only the spinner (inherits currentColor) is visible while loading, so color it with the object token.
71
71
  variant: "solid",
72
72
  theme: "primary",
73
73
  isLoading: true,
74
- className: "disabled:bg-primary-500 disabled:text-white disabled:border-primary-600",
74
+ className: "disabled:bg-surface-primary-high-enabled aria-disabled:bg-surface-primary-high-enabled disabled:text-object-inverse aria-disabled:text-object-inverse",
75
75
  },
76
76
  // solid neutral
77
77
  {
@@ -79,22 +79,18 @@ const buttonVariants = cva([
79
79
  theme: "neutral",
80
80
  isLoading: false,
81
81
  className: [
82
- "bg-[var(--color-black-alpha-600)]",
83
- "text-white",
84
- "border-neutral-600",
85
- "hover:bg-[var(--color-black-alpha-700)]",
86
- "hover:border-neutral-700",
87
- "active:bg-[var(--color-black-alpha-800)]",
88
- "active:border-neutral-800",
89
- "disabled:bg-[var(--color-black-alpha-200)]",
90
- "disabled:border-none",
82
+ "bg-surface-neutral-high-enabled",
83
+ "text-text-inverse",
84
+ "hover:bg-surface-neutral-high-hover",
85
+ "active:bg-surface-neutral-high-active",
86
+ "disabled:bg-surface-neutral-high-disabled aria-disabled:bg-surface-neutral-high-disabled",
91
87
  ].join(" "),
92
88
  },
93
89
  {
94
90
  variant: "solid",
95
91
  theme: "neutral",
96
92
  isLoading: true,
97
- className: "disabled:bg-[var(--color-black-alpha-600)] disabled:text-white disabled:border-neutral-600",
93
+ className: "disabled:bg-surface-neutral-high-enabled aria-disabled:bg-surface-neutral-high-enabled disabled:text-object-inverse aria-disabled:text-object-inverse",
98
94
  },
99
95
  // solid negative
100
96
  {
@@ -102,22 +98,18 @@ const buttonVariants = cva([
102
98
  theme: "negative",
103
99
  isLoading: false,
104
100
  className: [
105
- "bg-negative-500",
106
- "text-white",
107
- "border-negative-600",
108
- "hover:bg-negative-600",
109
- "hover:border-negative-700",
110
- "active:bg-negative-700",
111
- "active:border-negative-800",
112
- "disabled:bg-negative-200",
113
- "disabled:border-none",
101
+ "bg-surface-negative-high-enabled",
102
+ "text-text-inverse",
103
+ "hover:bg-surface-negative-high-hover",
104
+ "active:bg-surface-negative-high-active",
105
+ "disabled:bg-surface-negative-high-disabled aria-disabled:bg-surface-negative-high-disabled",
114
106
  ].join(" "),
115
107
  },
116
108
  {
117
109
  variant: "solid",
118
110
  theme: "negative",
119
111
  isLoading: true,
120
- className: "disabled:bg-negative-500 disabled:text-white disabled:border-negative-600",
112
+ className: "disabled:bg-surface-negative-high-enabled aria-disabled:bg-surface-negative-high-enabled disabled:text-object-inverse aria-disabled:text-object-inverse",
121
113
  },
122
114
  // outline primary
123
115
  {
@@ -125,23 +117,23 @@ const buttonVariants = cva([
125
117
  theme: "primary",
126
118
  isLoading: false,
127
119
  className: [
128
- "bg-white",
129
- "text-primary-500",
130
- "border-primary-300",
131
- "hover:bg-primary-50",
132
- "active:bg-primary-100",
133
- "active:border-primary-400",
134
- "active:text-primary-600",
135
- "disabled:bg-[var(--color-white-alpha-700)]",
136
- "disabled:border-primary-100",
137
- "disabled:text-primary-200",
120
+ "bg-surface-base-0",
121
+ "text-text-primary-enabled",
122
+ "border-border-primary-high",
123
+ "hover:bg-surface-primary-low-hover",
124
+ "hover:text-text-primary-hover",
125
+ "active:bg-surface-primary-low-active",
126
+ "active:text-text-primary-active",
127
+ "disabled:bg-surface-primary-low-disabled aria-disabled:bg-surface-primary-low-disabled",
128
+ "disabled:border-border-primary-low aria-disabled:border-border-primary-low",
129
+ "disabled:text-text-primary-disabled aria-disabled:text-text-primary-disabled",
138
130
  ].join(" "),
139
131
  },
140
132
  {
141
133
  variant: "outline",
142
134
  theme: "primary",
143
135
  isLoading: true,
144
- className: "disabled:white disabled:text-primary-500 disabled:border-primary-300",
136
+ className: "disabled:bg-surface-base-0 aria-disabled:bg-surface-base-0 disabled:text-object-primary-enabled aria-disabled:text-object-primary-enabled disabled:border-border-primary-high aria-disabled:border-border-primary-high",
145
137
  },
146
138
  // outline neutral
147
139
  {
@@ -149,22 +141,23 @@ const buttonVariants = cva([
149
141
  theme: "neutral",
150
142
  isLoading: false,
151
143
  className: [
152
- "bg-white",
153
- "text-neutral-700",
154
- "border-neutral-300",
155
- "hover:bg-[var(--color-black-alpha-50)]",
156
- "active:bg-[var(--color-black-alpha-100)]",
157
- "active:border-neutral-300",
158
- "disabled:bg-[var(--color-white-alpha-700)]",
159
- "disabled:border-neutral-100",
160
- "disabled:text-neutral-200",
144
+ "bg-surface-base-0",
145
+ "text-text-neutral-middle",
146
+ "border-border-neutral-high",
147
+ "hover:bg-surface-neutral-low-hover",
148
+ "hover:text-text-neutral-high",
149
+ "active:bg-surface-neutral-low-active",
150
+ "active:text-text-neutral-high",
151
+ "disabled:bg-surface-neutral-low-disabled aria-disabled:bg-surface-neutral-low-disabled",
152
+ "disabled:border-border-neutral-low aria-disabled:border-border-neutral-low",
153
+ "disabled:text-text-neutral-disabled aria-disabled:text-text-neutral-disabled",
161
154
  ].join(" "),
162
155
  },
163
156
  {
164
157
  variant: "outline",
165
158
  theme: "neutral",
166
159
  isLoading: true,
167
- className: "disabled:bg-white disabled:text-neutral-700 disabled:border-neutral-300",
160
+ className: "disabled:bg-surface-base-0 aria-disabled:bg-surface-base-0 disabled:text-object-neutral-middle aria-disabled:text-object-neutral-middle disabled:border-border-neutral-high aria-disabled:border-border-neutral-high",
168
161
  },
169
162
  // outline negative
170
163
  {
@@ -172,23 +165,23 @@ const buttonVariants = cva([
172
165
  theme: "negative",
173
166
  isLoading: false,
174
167
  className: [
175
- "bg-white",
176
- "text-negative-500",
177
- "border-negative-300",
178
- "hover:bg-negative-50",
179
- "active:bg-negative-100",
180
- "active:border-negative-400",
181
- "active:text-negative-600",
182
- "disabled:bg-[var(--color-white-alpha-700)]",
183
- "disabled:border-negative-100",
184
- "disabled:text-negative-200",
168
+ "bg-surface-base-0",
169
+ "text-text-negative-enabled",
170
+ "border-border-negative-high",
171
+ "hover:bg-surface-negative-low-hover",
172
+ "hover:text-text-negative-hover",
173
+ "active:bg-surface-negative-low-active",
174
+ "active:text-text-negative-active",
175
+ "disabled:bg-surface-negative-low-disabled aria-disabled:bg-surface-negative-low-disabled",
176
+ "disabled:border-border-negative-low aria-disabled:border-border-negative-low",
177
+ "disabled:text-text-negative-disabled aria-disabled:text-text-negative-disabled",
185
178
  ].join(" "),
186
179
  },
187
180
  {
188
181
  variant: "outline",
189
182
  theme: "negative",
190
183
  isLoading: true,
191
- className: "disabled:bg-white disabled:text-negative-500 disabled:border-negative-300",
184
+ className: "disabled:bg-surface-base-0 aria-disabled:bg-surface-base-0 disabled:text-object-negative-enabled aria-disabled:text-object-negative-enabled disabled:border-border-negative-high aria-disabled:border-border-negative-high",
192
185
  },
193
186
  // ghost primary
194
187
  {
@@ -196,19 +189,20 @@ const buttonVariants = cva([
196
189
  theme: "primary",
197
190
  isLoading: false,
198
191
  className: [
199
- "text-primary-500",
200
- "hover:bg-primary-50",
201
- "active:bg-primary-100",
202
- "active:text-primary-600",
203
- "disabled:bg-[var(--color-white-alpha-700)]",
204
- "disabled:text-primary-200",
192
+ "text-text-primary-enabled",
193
+ "hover:bg-surface-primary-low-hover",
194
+ "hover:text-text-primary-hover",
195
+ "active:bg-surface-primary-low-active",
196
+ "active:text-text-primary-active",
197
+ "disabled:bg-surface-primary-low-disabled aria-disabled:bg-surface-primary-low-disabled",
198
+ "disabled:text-text-primary-disabled aria-disabled:text-text-primary-disabled",
205
199
  ].join(" "),
206
200
  },
207
201
  {
208
202
  variant: "ghost",
209
203
  theme: "primary",
210
204
  isLoading: true,
211
- className: "disabled:text-primary-500",
205
+ className: "disabled:text-object-primary-enabled aria-disabled:text-object-primary-enabled",
212
206
  },
213
207
  // ghost neutral
214
208
  {
@@ -216,18 +210,20 @@ const buttonVariants = cva([
216
210
  theme: "neutral",
217
211
  isLoading: false,
218
212
  className: [
219
- "text-neutral-700",
220
- "hover:bg-[var(--color-black-alpha-50)]",
221
- "active:bg-[var(--color-black-alpha-100)]",
222
- "disabled:bg-[var(--color-white-alpha-700)]",
223
- "disabled:text-neutral-200",
213
+ "text-text-neutral-middle",
214
+ "hover:bg-surface-neutral-low-hover",
215
+ "hover:text-text-neutral-high",
216
+ "active:bg-surface-neutral-low-active",
217
+ "active:text-text-neutral-high",
218
+ "disabled:bg-surface-neutral-low-disabled aria-disabled:bg-surface-neutral-low-disabled",
219
+ "disabled:text-text-neutral-disabled aria-disabled:text-text-neutral-disabled",
224
220
  ].join(" "),
225
221
  },
226
222
  {
227
223
  variant: "ghost",
228
224
  theme: "neutral",
229
225
  isLoading: true,
230
- className: "disabled:text-neutral-700",
226
+ className: "disabled:text-object-neutral-middle aria-disabled:text-object-neutral-middle",
231
227
  },
232
228
  // ghost negative
233
229
  {
@@ -235,19 +231,20 @@ const buttonVariants = cva([
235
231
  theme: "negative",
236
232
  isLoading: false,
237
233
  className: [
238
- "text-negative-500",
239
- "hover:bg-negative-50",
240
- "active:bg-negative-100",
241
- "active:text-negative-600",
242
- "disabled:bg-[var(--color-white-alpha-700)]",
243
- "disabled:text-negative-200",
234
+ "text-text-negative-enabled",
235
+ "hover:bg-surface-negative-low-hover",
236
+ "hover:text-text-negative-hover",
237
+ "active:bg-surface-negative-low-active",
238
+ "active:text-text-negative-active",
239
+ "disabled:bg-surface-negative-low-disabled aria-disabled:bg-surface-negative-low-disabled",
240
+ "disabled:text-text-negative-disabled aria-disabled:text-text-negative-disabled",
244
241
  ].join(" "),
245
242
  },
246
243
  {
247
244
  variant: "ghost",
248
245
  theme: "negative",
249
246
  isLoading: true,
250
- className: "disabled:text-negative-500",
247
+ className: "disabled:text-object-negative-enabled aria-disabled:text-object-negative-enabled",
251
248
  },
252
249
  ],
253
250
  defaultVariants: {
@@ -276,6 +273,10 @@ const buttonVariants = cva([
276
273
  * en: Use `IconButton` for icon-only buttons.
277
274
  * - `asChild` 使用時は `prefixIcon` / `suffixIcon` / `isLoading` が反映されません。必要ならスロット先でアイコンやローディング表現を構成してください。
278
275
  * en: In `asChild` mode, `prefixIcon`, `suffixIcon`, and `isLoading` are ignored. Render icons and loading states inside the slotted child when needed.
276
+ * - `asChild` で差し込んだ要素が指定した props(`type` や `disabled` など)は、Slot のマージで
277
+ * コンポーネント側の値より優先されます。
278
+ * en: Props set on the slotted element itself (such as `type` or `disabled`) win over the
279
+ * component's own values through Slot's prop merging.
279
280
  *
280
281
  * ```tsx
281
282
  * // ✅ Correct
@@ -298,21 +299,69 @@ const buttonVariants = cva([
298
299
  * <Link href="/items/new">新規作成</Link>
299
300
  * </Button>
300
301
  * ```
301
-
302
+ *
302
303
  * **アクセシビリティ / Accessibility**
303
304
  *
304
305
  * - ボタンにはアクセシブルネームが必要です(通常は `children` のテキスト)。
305
306
  * アイコンのみの場合は `aria-label` / `aria-labelledby` を付与するか、可能なら `IconButton` を使用してください。
306
307
  * - `isLoading` の場合でもアクセシブルネームは維持されます。
307
- * - `asChild` を使う場合、子要素がボタン相当のセマンティクス(role/disabled/キーボード操作)を満たすようにしてください。
308
+ * - `asChild` を使う場合、子要素がボタン相当のセマンティクス(role/キーボード操作)を満たすようにしてください。
309
+ * - `asChild` で `<a>` など button 以外の要素を差し込んで無効化した場合、`aria-disabled` の付与、
310
+ * click / auxclick / Enter・Space の抑止、`aria-disabled:` プレフィックスによる無効時の配色まで
311
+ * このコンポーネントが行います(`data-disabled="true"` も利用側のスタイルフックとして出力)。
312
+ * 差し込み先が native の `<button>` の場合は `disabled` 属性がそのまま渡ります。
313
+ * - `type` / `form` / `value` など button 専用の props は、差し込み先が native の `<button>` の
314
+ * ときだけ転送されます。それ以外の要素では不正な属性になるため落とし、dev ビルドで警告します。
315
+ * 差し込み先が内部で `<button>` を描画するコンポーネントの場合のみ、その要素側に直接指定してください
316
+ * (Slot のマージで子の指定が優先されます)。`<a>` 等では属性自体が機能しないため指定不要です。
317
+ * en: Button-only props such as `type`, `form`, and `value` are forwarded only when the slot is a
318
+ * native `<button>`; on other elements they would be invalid attributes, so they are dropped with
319
+ * a dev warning. Set them on the slotted element only when that component renders a `<button>`
320
+ * internally (child props win in Slot's merge) — on `<a>` and friends the attributes do nothing.
321
+ * - `aria-disabled` を直接渡した場合も、無効時の配色が当たり操作が抑止されます(native の
322
+ * `disabled` 属性は付けないため、フォーカスは残ります)。無効化には基本的に `isDisabled` を
323
+ * 使ってください。
324
+ * en: Passing `aria-disabled` directly also applies the disabled colors and blocks activation
325
+ * (the native `disabled` attribute is not set, so the element stays focusable). Prefer
326
+ * `isDisabled` for disabling.
327
+ * なお `aria-disabled` の要素は既定でフォーカス可能なままです。無効な項目の存在と理由を
328
+ * 支援技術の利用者に伝えられるため通常はそのままで構いません。タブ順から外す必要がある場合のみ
329
+ * 差し込む要素側で `tabIndex={-1}` を指定してください。
330
+ * en: When disabled with `asChild` and a non-button element such as `<a>`, this component sets
331
+ * `aria-disabled`, blocks click / auxclick / Enter / Space, and applies the disabled colors via
332
+ * `aria-disabled:`-prefixed utilities (`data-disabled="true"` is also emitted as a styling hook).
333
+ * A slotted native `<button>` receives the real `disabled` attribute instead.
334
+ * - native の `<button>` かどうかは差し込んだ要素そのもので判定するため、内部で `<button>` を
335
+ * 描画するカスタムコンポーネントを渡した場合は非 button として扱われます。
336
+ * また無効時に抑止できるのは click / auxclick / Enter・Space までで、コンテキストメニューの
337
+ * 「新しいタブで開く」は止められません。確実に遷移させたくない場合は差し込む要素側で
338
+ * `href` を外してください。
339
+ * en: Whether the slot is a native `<button>` is decided from the slotted element itself, so a
340
+ * custom component that renders a `<button>` internally is treated as a non-button. Also, only
341
+ * click / auxclick / Enter / Space can be blocked while disabled — "open in new tab" from the
342
+ * context menu cannot. Drop `href` on the slotted element when navigation must not happen.
343
+ * An `aria-disabled` element stays focusable by design, which is usually what you want; set
344
+ * `tabIndex={-1}` on the slotted element only when it must leave the tab order.
308
345
  *
309
346
  * @param {ButtonProps} props
310
347
  */
311
- const Button = React.forwardRef(function Button(_a, ref) {
312
- var { className, variant, size, theme, isLoading = false, isDisabled = false, asChild = false, disabled, prefixIcon, suffixIcon, children } = _a, props = __rest(_a, ["className", "variant", "size", "theme", "isLoading", "isDisabled", "asChild", "disabled", "prefixIcon", "suffixIcon", "children"]);
348
+ function Button(_a) {
349
+ var { className, variant, size, theme, isLoading = false, isDisabled = false, asChild = false, disabled, prefixIcon, suffixIcon, ref, children } = _a, props = __rest(_a, ["className", "variant", "size", "theme", "isLoading", "isDisabled", "asChild", "disabled", "prefixIcon", "suffixIcon", "ref", "children"]);
313
350
  const Comp = asChild ? SlotPrimitive.Slot : "button";
314
351
  // disabled状態の管理(isDisabled、disabled、またはisLoadingがtrueの場合)
315
352
  const isButtonDisabled = isLoading || isDisabled || disabled;
353
+ // 利用者が aria-disabled を直接指定した場合も、見た目に合わせて操作を抑止する。
354
+ // native の disabled 属性は付けないため、フォーカスは残る(soft disabled)
355
+ // en: A caller-provided aria-disabled also blocks activation so the behavior matches the look.
356
+ // The native `disabled` attribute is not set, so the element stays focusable (soft disabled).
357
+ const isSoftDisabled = props["aria-disabled"] === true || props["aria-disabled"] === "true";
358
+ const isActivationBlocked = Boolean(isButtonDisabled) || isSoftDisabled;
359
+ // asChild で差し込まれたのが native の <button> なら、button 専用の props を渡してよい。
360
+ // Slot は子の props を優先するため、差し込み側が明示した type はそのまま尊重される
361
+ // en: When the slotted element is a native <button>, button-only props can be forwarded.
362
+ // Slot gives the child's props precedence, so an explicit `type` on the child still wins.
363
+ const isSlottedNativeButton = asChild && React.isValidElement(children) && children.type === "button";
364
+ const canUseButtonProps = !asChild || isSlottedNativeButton;
316
365
  // アイコンのサイズをボタンサイズに合わせて設定
317
366
  const getIconSize = () => {
318
367
  switch (size) {
@@ -327,14 +376,52 @@ const Button = React.forwardRef(function Button(_a, ref) {
327
376
  const hasAccessibleNameProp = ("aria-label" in props && Boolean(props["aria-label"])) ||
328
377
  ("aria-labelledby" in props && Boolean(props["aria-labelledby"]));
329
378
  const hasChildren = React.Children.count(children) > 0;
379
+ // asChild では children が差し込む要素そのものなので、その要素のラベル / テキストを見る
380
+ // en: With asChild the children are the slotted element itself, so inspect its label / text.
381
+ const slottedChildProps = React.isValidElement(children)
382
+ ? children.props
383
+ : undefined;
384
+ const hasAccessibleName = asChild
385
+ ? hasAccessibleNameProp ||
386
+ Boolean((slottedChildProps === null || slottedChildProps === void 0 ? void 0 : slottedChildProps["aria-label"]) ||
387
+ (slottedChildProps === null || slottedChildProps === void 0 ? void 0 : slottedChildProps["aria-labelledby"])) ||
388
+ React.Children.count(slottedChildProps === null || slottedChildProps === void 0 ? void 0 : slottedChildProps.children) > 0
389
+ : hasChildren || hasAccessibleNameProp;
330
390
  if (process.env.NODE_ENV !== "production") {
331
- if (!hasChildren && !hasAccessibleNameProp) {
391
+ if (!hasAccessibleName) {
332
392
  // Icon-only button should use IconButton, or provide aria-label/labelledby.
333
393
  // Keep it as a warning (not an exception) to avoid breaking existing usage.
334
394
  console.warn("[Button] Accessible name is missing. Provide children text, or set aria-label/aria-labelledby. For icon-only actions, consider using IconButton.");
335
395
  }
336
- if (asChild && isButtonDisabled) {
337
- console.warn("[Button] asChild + disabled/loading: the child element must handle disabled semantics (e.g., aria-disabled + preventing activation). Ensure the slotted element is button-like.");
396
+ // asChild で単一要素以外を渡すと、Slot が何も描画しない / React が例外を投げる
397
+ // en: With asChild, anything other than a single element makes Slot render nothing or throw.
398
+ if (asChild && !React.isValidElement(children)) {
399
+ console.warn("[Button] asChild には単一の React 要素を children として渡してください。" +
400
+ "要素以外では何も描画されず、複数要素では実行時エラーになります。" +
401
+ " / asChild requires a single React element child: anything else renders nothing or throws.");
402
+ }
403
+ // button 専用の props は button 以外の差し込み先には渡さない(無効な属性になるため)
404
+ // en: Button-only props are not forwarded to non-button slots: they would be invalid attributes.
405
+ if (asChild && !canUseButtonProps) {
406
+ const droppedProps = [
407
+ "type",
408
+ "form",
409
+ "formAction",
410
+ "formEncType",
411
+ "formMethod",
412
+ "formNoValidate",
413
+ "formTarget",
414
+ "value",
415
+ "popoverTarget",
416
+ "popoverTargetAction",
417
+ ].filter(name => props[name] !== undefined && props[name] !== false);
418
+ if (droppedProps.length > 0) {
419
+ console.warn(`[Button] asChild で button 以外の要素を差し込む場合、${droppedProps.join(" / ")} は無視されます。` +
420
+ "これらは button / input 専用の属性です。差し込み先が内部で <button> を描画するコンポーネントなら、" +
421
+ "その要素側に直接指定してください(<a> 等では属性自体が機能しません)。" +
422
+ ` / In asChild mode with a non-button element, ${droppedProps.join(" / ")} are ignored: they only apply to button / input. ` +
423
+ "If the slotted component renders a <button> internally, set them on that element instead (they do nothing on <a> and friends).");
424
+ }
338
425
  }
339
426
  if (asChild && (prefixIcon || suffixIcon || isLoading)) {
340
427
  console.warn("[Button] asChild mode does not support prefixIcon, suffixIcon, or isLoading. These props will be ignored.");
@@ -343,26 +430,93 @@ const Button = React.forwardRef(function Button(_a, ref) {
343
430
  console.warn("[Button] onMouseDown/onPointerDown/onTouchStart are deprecated for accessibility reasons (WCAG 2.5.2 Pointer Cancellation). Prefer onClick (activation on release).");
344
431
  }
345
432
  }
346
- const { onClick, onKeyDown } = props, restProps = __rest(props, ["onClick", "onKeyDown"]);
347
- const handleClick = event => {
348
- if (isButtonDisabled) {
433
+ const { onClick, onKeyDown, onClickCapture, onKeyDownCapture, onAuxClickCapture, type, form, formAction, formEncType, formMethod, formNoValidate, formTarget, value, popoverTarget, popoverTargetAction, "aria-disabled": ariaDisabled } = props, restProps = __rest(props, ["onClick", "onKeyDown", "onClickCapture", "onKeyDownCapture", "onAuxClickCapture", "type", "form", "formAction", "formEncType", "formMethod", "formNoValidate", "formTarget", "value", "popoverTarget", "popoverTargetAction", "aria-disabled"]);
434
+ // button 専用の props は、差し込み先が native の <button> のときだけ渡す。
435
+ // <a> 等に渡すと不正な属性になるため。
436
+ // `name` は <a> でも受け付けられる(HTML Living Standard 上は obsolete だが UA が互換のため
437
+ // サポートし続けている)ので、ここでは落とさず転送したままにしている。
438
+ // React の型(ButtonHTMLAttributes)に button 専用 props が増えた場合はこのリストも追随すること
439
+ // en: Button-only props are forwarded only when the slot is a native <button>; on elements like
440
+ // <a> they would be invalid attributes. `name` is deliberately kept: <a name> is obsolete in the
441
+ // HTML Living Standard but still accepted by UAs for compatibility. Keep this list in sync when
442
+ // React's ButtonHTMLAttributes gains new button-only props.
443
+ const buttonOnlyProps = canUseButtonProps
444
+ ? {
445
+ type: type || "button",
446
+ form,
447
+ formAction,
448
+ formEncType,
449
+ formMethod,
450
+ formNoValidate,
451
+ formTarget,
452
+ value,
453
+ popoverTarget,
454
+ popoverTargetAction,
455
+ }
456
+ : {};
457
+ // 無効時のガードは capture フェーズで行う。Radix Slot は「差し込んだ要素自身のハンドラ →
458
+ // Slot 側のハンドラ」の順で合成するため、bubble フェーズのガードでは子のハンドラを止められない
459
+ // en: Guard in the capture phase. Radix Slot composes handlers as "the slotted element's own
460
+ // handler first, then the slot's", so a bubble-phase guard cannot stop the child's handler.
461
+ const handleClickCapture = event => {
462
+ if (isActivationBlocked) {
349
463
  event.preventDefault();
350
464
  event.stopPropagation();
351
465
  return;
352
466
  }
353
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
467
+ onClickCapture === null || onClickCapture === void 0 ? void 0 : onClickCapture(event);
354
468
  };
355
- const handleKeyDown = event => {
356
- if (isButtonDisabled) {
357
- // Prevent activation keys when used with asChild (e.g., <a>).
469
+ const handleKeyDownCapture = event => {
470
+ if (isActivationBlocked) {
471
+ // asChild で <a> 等を差し込んだ場合に Enter / Space での実行を止める。
472
+ // それ以外のキーは伝播を止めないが、無効時は利用者の onKeyDown も呼ばない
473
+ // en: Other keys keep propagating, but the caller's onKeyDown is not invoked
474
+ // while disabled.
358
475
  if (event.key === "Enter" || event.key === " ") {
359
476
  event.preventDefault();
360
477
  event.stopPropagation();
361
478
  }
362
479
  return;
363
480
  }
481
+ onKeyDownCapture === null || onKeyDownCapture === void 0 ? void 0 : onKeyDownCapture(event);
482
+ };
483
+ // 中クリックは click ではなく auxclick として飛ぶため、別途止める必要がある
484
+ // en: A middle click fires `auxclick`, not `click`, so it needs its own guard.
485
+ const handleAuxClickCapture = event => {
486
+ if (isActivationBlocked) {
487
+ event.preventDefault();
488
+ event.stopPropagation();
489
+ return;
490
+ }
491
+ onAuxClickCapture === null || onAuxClickCapture === void 0 ? void 0 : onAuxClickCapture(event);
492
+ };
493
+ // capture 側で stopPropagation するため、無効時にこの bubble 側ハンドラへは到達しない。
494
+ // capture が本体・こちらは保険として残している
495
+ // en: The capture guard stops propagation, so this bubble handler is not reached while
496
+ // disabled. The capture phase is the real guard; this one is a safety net.
497
+ const handleClick = event => {
498
+ if (isActivationBlocked) {
499
+ return;
500
+ }
501
+ onClick === null || onClick === void 0 ? void 0 : onClick(event);
502
+ };
503
+ const handleKeyDown = event => {
504
+ if (isActivationBlocked) {
505
+ return;
506
+ }
364
507
  onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
365
508
  };
509
+ // Slot は「差し込んだ要素自身のハンドラ → Slot のハンドラ」の順で合成するため、
510
+ // 無効時は子側の capture ハンドラを外してガードを確実に先に効かせる。
511
+ // 同様に子が明示した disabled も、無効時はコンポーネント側の状態を優先する
512
+ // en: Slot composes "the slotted element's own handler first, then the slot's", so while
513
+ // disabled we strip the child's capture handlers to keep the guard authoritative. The
514
+ // component's disabled state also overrides a `disabled` set on the child.
515
+ const slottedChildren = asChild && isActivationBlocked && React.isValidElement(children)
516
+ ? React.cloneElement(children, Object.assign({ onClickCapture: undefined, onAuxClickCapture: undefined, onKeyDownCapture: undefined }, (canUseButtonProps && isButtonDisabled
517
+ ? { disabled: true }
518
+ : {})))
519
+ : children;
366
520
  return (_jsx(Comp
367
521
  // asChild ケースで <a> 等を受け入れるため公開 API は HTMLElement で広く受けるが、
368
522
  // 内部の Comp は <button> 固定の union が含まれるためここで narrow する。
@@ -371,13 +525,13 @@ const Button = React.forwardRef(function Button(_a, ref) {
371
525
  // asChild ケースで <a> 等を受け入れるため公開 API は HTMLElement で広く受けるが、
372
526
  // 内部の Comp は <button> 固定の union が含まれるためここで narrow する。
373
527
  // en: Public ref is HTMLElement (covers asChild targets); inner Comp's button branch needs narrowing.
374
- ref: ref, "data-slot": "button", "aria-busy": isLoading || undefined, "aria-disabled": asChild && isButtonDisabled ? true : undefined, "data-disabled": asChild && isButtonDisabled ? "true" : undefined, disabled: asChild ? undefined : isButtonDisabled, className: cn(buttonVariants({
528
+ ref: ref }, buttonOnlyProps, { "data-slot": "button", "aria-busy": isLoading || undefined, "aria-disabled": !canUseButtonProps && isButtonDisabled ? true : ariaDisabled, "data-disabled": asChild && isButtonDisabled ? "true" : undefined, disabled: canUseButtonProps ? isButtonDisabled : undefined, className: cn(buttonVariants({
375
529
  variant,
376
530
  size,
377
531
  theme,
378
532
  isLoading,
379
533
  className,
380
- })), type: asChild ? undefined : props.type || "button", onClick: handleClick, onKeyDown: handleKeyDown }, restProps, { children: asChild ? (children) : (_jsxs(_Fragment, { children: [prefixIcon && (_jsx(Icon, { icon: prefixIcon, size: getIconSize(), className: cn({ "opacity-0": isLoading }) })), isLoading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex", children: _jsx(Spinner, { size: getIconSize(), className: "text-current" }) }), _jsx("span", { className: "opacity-0", children: children })] })) : (_jsx("span", { className: "px-1", children: children })), suffixIcon && (_jsx(Icon, { icon: suffixIcon, size: getIconSize(), className: cn({ "opacity-0": isLoading }) }))] })) })));
381
- });
534
+ })), onClickCapture: handleClickCapture, onAuxClickCapture: handleAuxClickCapture, onKeyDownCapture: handleKeyDownCapture, onClick: handleClick, onKeyDown: handleKeyDown }, restProps, { children: asChild ? (slottedChildren) : (_jsxs(_Fragment, { children: [prefixIcon && (_jsx(Icon, { icon: prefixIcon, size: getIconSize(), className: cn({ "opacity-0": isLoading }) })), isLoading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex", children: _jsx(Spinner, { size: getIconSize(), className: "text-current" }) }), _jsx("span", { className: "opacity-0", children: children })] })) : (_jsx("span", { className: "px-1", children: children })), suffixIcon && (_jsx(Icon, { icon: suffixIcon, size: getIconSize(), className: cn({ "opacity-0": isLoading }) }))] })) })));
535
+ }
382
536
  Button.displayName = "Button";
383
537
  export { Button, buttonVariants };