banhaten 0.3.0 → 0.5.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 (348) hide show
  1. package/CHANGELOG.md +139 -73
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +98 -98
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +89 -89
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +177 -176
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -79
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +127 -76
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -61
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -69
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -60
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1277
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -255
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -126
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -603
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -555
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -677
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -358
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -236
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -11
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -57
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +458 -224
@@ -1,872 +1,872 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as DialogPrimitive from "@radix-ui/react-dialog"
5
- import { cva, type VariantProps } from "class-variance-authority"
6
- import { InfoIcon, TriangleAlertIcon, XIcon } from "lucide-react"
7
-
8
- import {
9
- AlertDialog,
10
- AlertDialogAction,
11
- AlertDialogCancel,
12
- AlertDialogContent,
13
- AlertDialogDescription,
14
- AlertDialogTitle,
15
- type AlertDialogContentProps,
16
- } from "@/components/ui/alert-dialog"
17
- import { Button, type ButtonProps } from "@/components/ui/button"
18
- import { Checkbox, type CheckboxProps } from "@/components/ui/checkbox"
19
- import { Spinner } from "@/components/ui/spinner"
20
- import { cn } from "@/lib/utils"
21
- import { useResolvedDirection } from "@/hooks/use-direction"
22
-
23
- type ModalSize = "sm" | "default" | "lg"
24
- type ModalIntent = "default" | "warning" | "danger"
25
-
26
- type ModalMessages = {
27
- checkboxLabel: React.ReactNode
28
- closeLabel: string
29
- primaryActionLabel: React.ReactNode
30
- secondaryActionLabel: React.ReactNode
31
- }
32
-
33
- type ConfirmModalMessages = {
34
- cancelLabel: React.ReactNode
35
- closeLabel: string
36
- confirmLabel: React.ReactNode
37
- confirmLoadingLabel: React.ReactNode
38
- }
39
-
40
- const modalSurfaceBaseClassName = [
41
- "flex max-h-[calc(100vh-(var(--bh-space-8xl-48)*2))] w-[var(--bh-modal-width)] max-w-[calc(100vw-(var(--bh-space-4xl-20)*2))] flex-col overflow-hidden rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--shadow-modal)] outline-none",
42
- "[--bh-modal-border:var(--bh-border-subtle)] [--shadow-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-modal-border),var(--shadow-lg)]",
43
- ]
44
-
45
- const modalSizeVariants = {
46
- sm: "[--bh-modal-width:calc(var(--bh-space-19xl-384)+var(--bh-space-8xl-48)+var(--bh-space-md-8))]",
47
- default:
48
- "[--bh-modal-width:calc(var(--bh-space-19xl-384)+var(--bh-space-12xl-128)+var(--bh-space-8xl-48))]",
49
- lg: "[--bh-modal-width:calc(var(--bh-space-19xl-384)+var(--bh-space-16xl-256)+var(--bh-space-9xl-64)-var(--bh-space-xs-4))]",
50
- } as const
51
-
52
- const modalSurfaceVariants = cva(modalSurfaceBaseClassName, {
53
- variants: {
54
- size: modalSizeVariants,
55
- },
56
- defaultVariants: {
57
- size: "sm",
58
- },
59
- })
60
-
61
- const modalContentVariants = cva(
62
- [
63
- "fixed left-1/2 top-1/2 z-[var(--bh-z-overlay)] -translate-x-1/2 -translate-y-1/2",
64
- ...modalSurfaceBaseClassName,
65
- ],
66
- {
67
- variants: {
68
- size: modalSizeVariants,
69
- },
70
- defaultVariants: {
71
- size: "sm",
72
- },
73
- }
74
- )
75
-
76
- const modalHeaderIconIntentClassNames: Record<ModalIntent, string> = {
77
- default: "text-[var(--bh-content-subtle)]",
78
- danger:
79
- "rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-danger-subtle)] text-[var(--bh-content-danger-default)]",
80
- warning:
81
- "rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-warning-subtle)] text-[var(--bh-content-warning-default)]",
82
- }
83
-
84
- const modalTitleClassName =
85
- "min-w-0 flex-1 text-start text-[length:var(--bh-text-heading-sm-semibold-font-size)] font-[var(--bh-text-heading-sm-semibold-font-weight)] leading-[var(--bh-text-heading-sm-semibold-line-height)] tracking-[var(--bh-text-heading-sm-semibold-letter-spacing)] text-[var(--bh-content-default)]"
86
-
87
- const modalDescriptionClassName =
88
- "min-w-0 flex-1 text-start text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
89
-
90
- type ModalRootProps = React.ComponentProps<typeof DialogPrimitive.Root>
91
- type ModalContentProps = React.ComponentPropsWithoutRef<
92
- typeof DialogPrimitive.Content
93
- > &
94
- VariantProps<typeof modalContentVariants> & {
95
- overlayClassName?: string
96
- }
97
- type ModalOverlayProps = React.ComponentPropsWithoutRef<
98
- typeof DialogPrimitive.Overlay
99
- >
100
- type ModalSurfaceProps = React.ComponentProps<"section"> &
101
- VariantProps<typeof modalSurfaceVariants>
102
- type ModalHeaderProps = React.ComponentProps<"header">
103
- type ModalBodyProps = React.ComponentProps<"div">
104
- type ModalFooterProps = React.ComponentProps<"footer">
105
- type ModalActionsProps = React.ComponentProps<"div">
106
- type ModalHeaderIconProps = React.ComponentProps<"span"> & {
107
- intent?: ModalIntent
108
- }
109
- type ModalCloseButtonProps = React.ComponentProps<"button"> & {
110
- label?: string
111
- }
112
- type ConfirmModalContentProps = AlertDialogContentProps
113
- type ModalFooterCheckboxProps = Omit<React.ComponentProps<"div">, "children"> & {
114
- checked?: CheckboxProps["checked"]
115
- defaultChecked?: CheckboxProps["defaultChecked"]
116
- disabled?: CheckboxProps["disabled"]
117
- label?: React.ReactNode
118
- onCheckedChange?: CheckboxProps["onCheckedChange"]
119
- }
120
- type ModalActionButtonProps = ButtonProps
121
-
122
- type ModalProps = Omit<ModalRootProps, "children"> &
123
- VariantProps<typeof modalContentVariants> & {
124
- checkboxLabel?: React.ReactNode
125
- children?: React.ReactNode
126
- className?: string
127
- closeLabel?: string
128
- description?: React.ReactNode
129
- dir?: "ltr" | "rtl"
130
- footer?: React.ReactNode
131
- messages?: Partial<ModalMessages>
132
- overlayClassName?: string
133
- primaryActionLabel?: React.ReactNode
134
- secondaryActionLabel?: React.ReactNode
135
- showCheckbox?: boolean
136
- showCloseButton?: boolean
137
- showDivider?: boolean
138
- showFooter?: boolean
139
- showIcon?: boolean
140
- title: React.ReactNode
141
- }
142
-
143
- type ConfirmModalProps = Omit<
144
- ModalProps,
145
- | "checkboxLabel"
146
- | "footer"
147
- | "messages"
148
- | "primaryActionLabel"
149
- | "secondaryActionLabel"
150
- | "showCheckbox"
151
- > & {
152
- cancelButtonProps?: ModalActionButtonProps
153
- cancelLabel?: React.ReactNode
154
- closeOnCancel?: boolean
155
- closeOnConfirm?: boolean
156
- confirmButtonProps?: ModalActionButtonProps
157
- confirmDisabled?: boolean
158
- confirmLabel?: React.ReactNode
159
- confirmLoading?: boolean
160
- confirmLoadingLabel?: React.ReactNode
161
- confirmVariant?: ModalActionButtonProps["variant"]
162
- intent?: ModalIntent
163
- messages?: Partial<ConfirmModalMessages>
164
- onCancel?: React.MouseEventHandler<HTMLButtonElement>
165
- onConfirm?: React.MouseEventHandler<HTMLButtonElement>
166
- onEscapeKeyDown?: ModalContentProps["onEscapeKeyDown"]
167
- onOpenAutoFocus?: ModalContentProps["onOpenAutoFocus"]
168
- preventEscapeDismiss?: boolean
169
- showCancelButton?: boolean
170
- }
171
-
172
- const defaultModalMessages: ModalMessages = {
173
- checkboxLabel: "Don't show me again",
174
- closeLabel: "Close modal",
175
- primaryActionLabel: "Action",
176
- secondaryActionLabel: "Action",
177
- }
178
-
179
- const arabicModalMessages: ModalMessages = {
180
- checkboxLabel: "\u0627\u0644\u062a\u0633\u0645\u064a\u0629",
181
- closeLabel: "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0646\u0645\u0648\u0630\u062c",
182
- primaryActionLabel: "\u0627\u0646\u0642\u0631 \u0647\u0646\u0627",
183
- secondaryActionLabel: "\u0627\u0646\u0642\u0631 \u0647\u0646\u0627",
184
- }
185
-
186
- const defaultConfirmModalMessages: ConfirmModalMessages = {
187
- cancelLabel: "Cancel",
188
- closeLabel: "Close modal",
189
- confirmLabel: "Confirm",
190
- confirmLoadingLabel: "Confirming",
191
- }
192
-
193
- const arabicConfirmModalMessages: ConfirmModalMessages = {
194
- cancelLabel: "\u0625\u0644\u063a\u0627\u0621",
195
- closeLabel: "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0646\u0645\u0648\u0630\u062c",
196
- confirmLabel: "\u062a\u0623\u0643\u064a\u062f",
197
- confirmLoadingLabel: "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0623\u0643\u064a\u062f",
198
- }
199
-
200
- const ModalRoot = DialogPrimitive.Root
201
- const ModalTrigger = DialogPrimitive.Trigger
202
- const ModalPortal = DialogPrimitive.Portal
203
- const ModalClose = DialogPrimitive.Close
204
-
205
- const ModalOverlay = React.forwardRef<
206
- React.ElementRef<typeof DialogPrimitive.Overlay>,
207
- ModalOverlayProps
208
- >(function ModalOverlay({ className, ...props }, ref) {
209
- return (
210
- <DialogPrimitive.Overlay
211
- data-slot="modal-overlay"
212
- ref={ref}
213
- className={cn("fixed inset-0 z-[var(--bh-z-overlay)] bg-[var(--bh-bg-backdrop)]", className)}
214
- {...props}
215
- />
216
- )
217
- })
218
-
219
- const ModalContent = React.forwardRef<
220
- React.ElementRef<typeof DialogPrimitive.Content>,
221
- ModalContentProps
222
- >(function ModalContent(
223
- { children, className, dir, overlayClassName, size, ...props },
224
- ref
225
- ) {
226
- const contentRef = React.useRef<
227
- React.ElementRef<typeof DialogPrimitive.Content> | null
228
- >(null)
229
- const resolvedDirection = useResolvedDirection(
230
- dir === "ltr" || dir === "rtl" ? dir : undefined,
231
- contentRef
232
- )
233
- const setContentRef = React.useCallback(
234
- (node: React.ElementRef<typeof DialogPrimitive.Content> | null) => {
235
- contentRef.current = node
236
- if (typeof ref === "function") ref(node)
237
- else if (ref) ref.current = node
238
- },
239
- [ref]
240
- )
241
-
242
- return (
243
- <ModalPortal>
244
- <ModalOverlay className={overlayClassName} />
245
- <DialogPrimitive.Content
246
- data-size={size ?? "sm"}
247
- data-slot="modal-content"
248
- dir={resolvedDirection}
249
- ref={setContentRef}
250
- className={cn(modalContentVariants({ size, className }))}
251
- {...props}
252
- >
253
- {children}
254
- </DialogPrimitive.Content>
255
- </ModalPortal>
256
- )
257
- })
258
-
259
- const ModalSurface = React.forwardRef<HTMLElement, ModalSurfaceProps>(
260
- function ModalSurface({ className, size, ...props }, ref) {
261
- return (
262
- <section
263
- data-size={size ?? "sm"}
264
- data-slot="modal-surface"
265
- ref={ref}
266
- className={cn(modalSurfaceVariants({ size, className }))}
267
- {...props}
268
- />
269
- )
270
- }
271
- )
272
-
273
- const ModalHeader = React.forwardRef<HTMLElement, ModalHeaderProps>(function ModalHeader({
274
- className,
275
- ...props
276
- }, ref) {
277
- return (
278
- <header
279
- data-slot="modal-header"
280
- ref={ref}
281
- className={cn(
282
- "relative flex w-full shrink-0 items-center gap-[var(--bh-space-sm-6)] px-[var(--bh-space-4xl-20)] pb-[var(--bh-space-md-8)] pe-[var(--bh-space-9xl-64)] pt-[var(--bh-space-4xl-20)] text-start",
283
- className
284
- )}
285
- {...props}
286
- />
287
- )
288
- })
289
-
290
- const ModalTitle = React.forwardRef<
291
- React.ElementRef<typeof DialogPrimitive.Title>,
292
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
293
- >(function ModalTitle({ className, dir = "auto", ...props }, ref) {
294
- return (
295
- <DialogPrimitive.Title
296
- data-slot="modal-title"
297
- dir={dir}
298
- ref={ref}
299
- className={cn(modalTitleClassName, className)}
300
- {...props}
301
- />
302
- )
303
- })
304
-
305
- const ModalDescription = React.forwardRef<
306
- React.ElementRef<typeof DialogPrimitive.Description>,
307
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
308
- >(function ModalDescription({ className, dir = "auto", ...props }, ref) {
309
- return (
310
- <DialogPrimitive.Description
311
- data-slot="modal-description"
312
- dir={dir}
313
- ref={ref}
314
- className={cn(modalDescriptionClassName, className)}
315
- {...props}
316
- />
317
- )
318
- })
319
-
320
- // One close affordance, two dismissal wrappers. Dialog uses ModalClose and
321
- // AlertDialog uses AlertDialogCancel, but the button itself is identical — so
322
- // an a11y or focus-ring fix here reaches both instead of one.
323
- const ModalCloseButtonSurface = React.forwardRef<
324
- HTMLButtonElement,
325
- ModalCloseButtonProps
326
- >(function ModalCloseButtonSurface(
327
- { children, className, label = "Close modal", type = "button", ...props },
328
- ref
329
- ) {
330
- return (
331
- <button
332
- aria-label={label}
333
- data-slot="modal-close"
334
- ref={ref}
335
- type={type}
336
- className={cn(
337
- "absolute end-[var(--bh-space-3xl-16)] top-1/2 inline-flex size-[var(--bh-space-6xl-32)] -translate-y-1/2 items-center justify-center rounded-[var(--bh-radius-full)] border-0 bg-transparent p-0 text-[var(--bh-content-subtle)] outline-none transition-[background-color,box-shadow,color] hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)] focus-visible:shadow-[var(--shadow-button-focus)]",
338
- className
339
- )}
340
- {...props}
341
- >
342
- {children ?? (
343
- <XIcon
344
- aria-hidden="true"
345
- className="size-[var(--bh-space-5xl-24)]"
346
- strokeWidth="var(--bh-icon-stroke-200)"
347
- />
348
- )}
349
- </button>
350
- )
351
- })
352
-
353
- const ConfirmModalCloseButton = React.forwardRef<
354
- HTMLButtonElement,
355
- ModalCloseButtonProps
356
- >(function ConfirmModalCloseButton(props, ref) {
357
- return (
358
- <AlertDialogCancel asChild>
359
- <ModalCloseButtonSurface ref={ref} {...props} />
360
- </AlertDialogCancel>
361
- )
362
- })
363
-
364
- const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(function ModalBody({
365
- className,
366
- ...props
367
- }, ref) {
368
- return (
369
- <div
370
- data-slot="modal-body"
371
- ref={ref}
372
- className={cn(
373
- "flex w-full shrink-0 items-start px-[var(--bh-space-4xl-20)] pb-[var(--bh-space-4xl-20)] pt-[var(--bh-space-lg-10)]",
374
- className
375
- )}
376
- {...props}
377
- />
378
- )
379
- })
380
-
381
- const ModalDivider = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
382
- function ModalDivider({ className, role = "separator", ...props }, ref) {
383
- return (
384
- <div
385
- data-slot="modal-divider"
386
- ref={ref}
387
- role={role}
388
- className={cn("w-full border-t border-[var(--bh-border-subtle)]", className)}
389
- {...props}
390
- />
391
- )
392
- }
393
- )
394
-
395
- const ModalFooter = React.forwardRef<HTMLElement, ModalFooterProps>(function ModalFooter({
396
- className,
397
- ...props
398
- }, ref) {
399
- return (
400
- <footer
401
- data-slot="modal-footer"
402
- ref={ref}
403
- className={cn(
404
- "flex w-full shrink-0 items-center justify-end gap-[var(--bh-space-lg-10)] px-[var(--bh-space-4xl-20)] py-[var(--bh-space-3xl-16)]",
405
- className
406
- )}
407
- {...props}
408
- />
409
- )
410
- })
411
-
412
- function ModalHeaderIcon({
413
- children,
414
- className,
415
- intent = "default",
416
- ...props
417
- }: ModalHeaderIconProps) {
418
- return (
419
- <span
420
- aria-hidden="true"
421
- data-intent={intent}
422
- data-slot="modal-header-icon"
423
- className={cn(
424
- "inline-flex size-[var(--bh-space-5xl-24)] shrink-0 items-center justify-center",
425
- modalHeaderIconIntentClassNames[intent],
426
- className
427
- )}
428
- {...props}
429
- >
430
- {children ?? (
431
- <InfoIcon className="size-[var(--bh-space-3xl-16)]" strokeWidth="var(--bh-icon-stroke-200)" />
432
- )}
433
- </span>
434
- )
435
- }
436
-
437
- const ModalCloseButton = React.forwardRef<HTMLButtonElement, ModalCloseButtonProps>(
438
- function ModalCloseButton(props, ref) {
439
- return (
440
- <ModalClose asChild>
441
- <ModalCloseButtonSurface ref={ref} {...props} />
442
- </ModalClose>
443
- )
444
- }
445
- )
446
-
447
- function ModalFooterCheckbox({
448
- checked,
449
- className,
450
- defaultChecked,
451
- disabled,
452
- label = defaultModalMessages.checkboxLabel,
453
- onCheckedChange,
454
- ...props
455
- }: ModalFooterCheckboxProps) {
456
- const generatedId = React.useId()
457
- const labelId = `${generatedId}-label`
458
-
459
- return (
460
- <div
461
- data-slot="modal-checkbox"
462
- className={cn(
463
- "flex min-w-0 flex-1 items-center gap-[var(--bh-space-md-8)] text-start",
464
- className
465
- )}
466
- {...props}
467
- >
468
- <Checkbox
469
- aria-labelledby={labelId}
470
- checked={checked}
471
- defaultChecked={defaultChecked}
472
- disabled={disabled}
473
- onCheckedChange={onCheckedChange}
474
- />
475
- <span
476
- data-slot="modal-checkbox-label"
477
- dir="auto"
478
- id={labelId}
479
- className="min-w-0 flex-1 text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-default)]"
480
- >
481
- {label}
482
- </span>
483
- </div>
484
- )
485
- }
486
-
487
- function ModalActions({ className, ...props }: ModalActionsProps) {
488
- return (
489
- <div
490
- data-slot="modal-actions"
491
- className={cn(
492
- "flex shrink-0 items-center justify-end gap-[var(--bh-space-xs-4)]",
493
- className
494
- )}
495
- {...props}
496
- />
497
- )
498
- }
499
-
500
- const ModalActionButton = React.forwardRef<HTMLButtonElement, ModalActionButtonProps>(
501
- function ModalActionButton(
502
- { children = "Action", size = "sm", type = "button", variant = "default", ...props },
503
- ref
504
- ) {
505
- return (
506
- <Button
507
- data-slot="modal-action"
508
- ref={ref}
509
- size={size}
510
- type={type}
511
- variant={variant}
512
- {...props}
513
- >
514
- {children}
515
- </Button>
516
- )
517
- }
518
- )
519
-
520
- function Modal({
521
- checkboxLabel,
522
- children,
523
- className,
524
- closeLabel,
525
- defaultOpen = false,
526
- description,
527
- dir,
528
- footer,
529
- messages: messageOverrides,
530
- modal = true,
531
- onOpenChange,
532
- open,
533
- overlayClassName,
534
- primaryActionLabel,
535
- secondaryActionLabel,
536
- showCheckbox = true,
537
- showCloseButton = true,
538
- showDivider = true,
539
- showFooter = true,
540
- showIcon = true,
541
- size = "sm",
542
- title,
543
- ...props
544
- }: ModalProps) {
545
- const rootProps =
546
- open === undefined
547
- ? { defaultOpen, modal, onOpenChange }
548
- : { modal, onOpenChange, open }
549
- const messages = { ...defaultModalMessages, ...messageOverrides }
550
- const resolvedTitle = title
551
- const resolvedDescription = description
552
- const resolvedCheckboxLabel =
553
- checkboxLabel ?? messages.checkboxLabel
554
- const resolvedPrimaryAction =
555
- primaryActionLabel ?? messages.primaryActionLabel
556
- const resolvedSecondaryAction =
557
- secondaryActionLabel ?? messages.secondaryActionLabel
558
- const resolvedCloseLabel = closeLabel ?? messages.closeLabel
559
-
560
- return (
561
- <ModalRoot {...rootProps}>
562
- <ModalContent
563
- data-slot="modal"
564
- dir={dir}
565
- overlayClassName={overlayClassName}
566
- size={size}
567
- className={className}
568
- {...props}
569
- >
570
- <ModalHeader>
571
- {showIcon ? <ModalHeaderIcon /> : null}
572
- <ModalTitle>{resolvedTitle}</ModalTitle>
573
- {showCloseButton ? <ModalCloseButton label={resolvedCloseLabel} /> : null}
574
- </ModalHeader>
575
- <ModalBody>
576
- {children ?? <ModalDescription>{resolvedDescription}</ModalDescription>}
577
- </ModalBody>
578
- {showDivider ? <ModalDivider /> : null}
579
- {showFooter ? (
580
- footer ?? (
581
- <ModalFooter>
582
- {showCheckbox ? (
583
- <ModalFooterCheckbox label={resolvedCheckboxLabel} />
584
- ) : null}
585
- <ModalActions>
586
- <ModalActionButton variant="soft">
587
- {resolvedSecondaryAction}
588
- </ModalActionButton>
589
- <ModalActionButton>{resolvedPrimaryAction}</ModalActionButton>
590
- </ModalActions>
591
- </ModalFooter>
592
- )
593
- ) : null}
594
- </ModalContent>
595
- </ModalRoot>
596
- )
597
- }
598
-
599
- function ConfirmModal({
600
- cancelButtonProps,
601
- cancelLabel,
602
- children,
603
- className,
604
- closeLabel,
605
- closeOnCancel = true,
606
- closeOnConfirm = true,
607
- confirmButtonProps,
608
- confirmDisabled = false,
609
- confirmLabel,
610
- confirmLoading = false,
611
- confirmLoadingLabel,
612
- confirmVariant,
613
- defaultOpen = false,
614
- description,
615
- dir,
616
- intent = "danger",
617
- messages: messageOverrides,
618
- modal = true,
619
- onCancel,
620
- onConfirm,
621
- onEscapeKeyDown,
622
- onOpenAutoFocus,
623
- onOpenChange,
624
- open,
625
- overlayClassName,
626
- preventEscapeDismiss = false,
627
- showCancelButton = true,
628
- showCloseButton = false,
629
- showDivider = true,
630
- showFooter = true,
631
- showIcon = true,
632
- size = "sm",
633
- title,
634
- ...props
635
- }: ConfirmModalProps) {
636
- const cancelButtonRef = React.useRef<HTMLButtonElement>(null)
637
- const confirmButtonRef = React.useRef<HTMLButtonElement>(null)
638
- const directionBoundaryRef = React.useRef<
639
- React.ElementRef<typeof AlertDialogContent> | null
640
- >(null)
641
- const resolvedDirection = useResolvedDirection(dir, directionBoundaryRef)
642
- const rootProps =
643
- open === undefined
644
- ? { defaultOpen, modal, onOpenChange }
645
- : { modal, onOpenChange, open }
646
- const messages = { ...defaultConfirmModalMessages, ...messageOverrides }
647
- const resolvedTitle = title
648
- const resolvedDescription = description
649
- const resolvedCancelLabel =
650
- cancelLabel ?? messages.cancelLabel
651
- const resolvedConfirmLabel =
652
- confirmLabel ?? messages.confirmLabel
653
- const resolvedConfirmLoadingLabel =
654
- confirmLoadingLabel ?? messages.confirmLoadingLabel
655
- const resolvedCloseLabel = closeLabel ?? messages.closeLabel
656
- const {
657
- children: cancelButtonChildren,
658
- onClick: cancelButtonOnClick,
659
- variant: cancelButtonVariant = "soft",
660
- ...cancelActionProps
661
- } = cancelButtonProps ?? {}
662
- const {
663
- children: confirmButtonChildren,
664
- disabled: confirmButtonDisabled,
665
- onClick: confirmButtonOnClick,
666
- variant: confirmButtonVariant = confirmVariant ?? getConfirmActionVariant(intent),
667
- ...confirmActionProps
668
- } = confirmButtonProps ?? {}
669
- const isConfirmDisabled = confirmDisabled || confirmLoading || confirmButtonDisabled
670
-
671
- const handleOpenAutoFocus: NonNullable<
672
- ConfirmModalContentProps["onOpenAutoFocus"]
673
- > = React.useCallback(
674
- (event) => {
675
- onOpenAutoFocus?.(event)
676
- if (event.defaultPrevented) return
677
-
678
- const focusTarget = showCancelButton
679
- ? cancelButtonRef.current
680
- : confirmButtonRef.current
681
- if (!focusTarget) return
682
-
683
- event.preventDefault()
684
- focusTarget.focus()
685
- },
686
- [onOpenAutoFocus, showCancelButton]
687
- )
688
-
689
- const handleEscapeKeyDown: NonNullable<
690
- ConfirmModalContentProps["onEscapeKeyDown"]
691
- > = React.useCallback(
692
- (event) => {
693
- onEscapeKeyDown?.(event)
694
- if (!event.defaultPrevented && preventEscapeDismiss) {
695
- event.preventDefault()
696
- }
697
- },
698
- [onEscapeKeyDown, preventEscapeDismiss]
699
- )
700
- const handleCancel: React.MouseEventHandler<HTMLButtonElement> = (event) => {
701
- cancelButtonOnClick?.(event)
702
- if (!event.defaultPrevented) {
703
- onCancel?.(event)
704
- }
705
- }
706
-
707
- const handleConfirm: React.MouseEventHandler<HTMLButtonElement> = (event) => {
708
- confirmButtonOnClick?.(event)
709
- if (!event.defaultPrevented) {
710
- onConfirm?.(event)
711
- }
712
- }
713
-
714
- const cancelAction = showCancelButton ? (
715
- <ModalActionButton
716
- ref={cancelButtonRef}
717
- variant={cancelButtonVariant}
718
- onClick={handleCancel}
719
- {...cancelActionProps}
720
- >
721
- {cancelButtonChildren ?? resolvedCancelLabel}
722
- </ModalActionButton>
723
- ) : null
724
- const confirmAction = (
725
- <ModalActionButton
726
- aria-busy={confirmLoading || undefined}
727
- disabled={isConfirmDisabled}
728
- ref={confirmButtonRef}
729
- variant={confirmButtonVariant}
730
- onClick={handleConfirm}
731
- {...confirmActionProps}
732
- >
733
- {confirmButtonChildren ??
734
- (confirmLoading ? (
735
- <>
736
- <Spinner className="size-[var(--bh-space-3xl-16)]" />
737
- <span data-slot="button-label" dir="auto" className="min-w-0 px-[var(--bh-modal-action-label-padding-x)]">
738
- {resolvedConfirmLoadingLabel}
739
- </span>
740
- </>
741
- ) : (
742
- resolvedConfirmLabel
743
- ))}
744
- </ModalActionButton>
745
- )
746
-
747
- return (
748
- <AlertDialog {...rootProps}>
749
- <AlertDialogContent
750
- data-intent={intent}
751
- data-size={size}
752
- data-slot="confirm-modal"
753
- dir={resolvedDirection}
754
- ref={directionBoundaryRef}
755
- className={cn(modalContentVariants({ size, className }))}
756
- overlayClassName={overlayClassName}
757
- onEscapeKeyDown={handleEscapeKeyDown}
758
- onOpenAutoFocus={handleOpenAutoFocus}
759
- {...props}
760
- >
761
- <ModalHeader>
762
- {showIcon ? (
763
- <ModalHeaderIcon intent={intent}>
764
- {intent === "default" ? (
765
- <InfoIcon
766
- className="size-[var(--bh-space-3xl-16)]"
767
- strokeWidth="var(--bh-icon-stroke-200)"
768
- />
769
- ) : (
770
- <TriangleAlertIcon
771
- className="size-[var(--bh-space-3xl-16)]"
772
- strokeWidth="var(--bh-icon-stroke-200)"
773
- />
774
- )}
775
- </ModalHeaderIcon>
776
- ) : null}
777
- <AlertDialogTitle
778
- data-slot="modal-title"
779
- className={modalTitleClassName}
780
- >
781
- {resolvedTitle}
782
- </AlertDialogTitle>
783
- {showCloseButton ? (
784
- <ConfirmModalCloseButton label={resolvedCloseLabel} />
785
- ) : null}
786
- </ModalHeader>
787
- <ModalBody>
788
- {children ?? (
789
- <AlertDialogDescription
790
- data-slot="modal-description"
791
- className={modalDescriptionClassName}
792
- >
793
- {resolvedDescription}
794
- </AlertDialogDescription>
795
- )}
796
- </ModalBody>
797
- {showDivider ? <ModalDivider /> : null}
798
- {showFooter ? (
799
- <ModalFooter>
800
- <ModalActions>
801
- {cancelAction && closeOnCancel ? (
802
- <AlertDialogCancel asChild>{cancelAction}</AlertDialogCancel>
803
- ) : (
804
- cancelAction
805
- )}
806
- {closeOnConfirm ? (
807
- <AlertDialogAction asChild>{confirmAction}</AlertDialogAction>
808
- ) : (
809
- confirmAction
810
- )}
811
- </ModalActions>
812
- </ModalFooter>
813
- ) : null}
814
- </AlertDialogContent>
815
- </AlertDialog>
816
- )
817
- }
818
-
819
- function getConfirmActionVariant(
820
- intent: ModalIntent
821
- ): ModalActionButtonProps["variant"] {
822
- if (intent === "danger") return "danger"
823
- if (intent === "warning") return "warning"
824
- return "default"
825
- }
826
-
827
- export {
828
- arabicConfirmModalMessages,
829
- arabicModalMessages,
830
- ConfirmModal,
831
- Modal,
832
- ModalActionButton,
833
- ModalActions,
834
- ModalBody,
835
- ModalClose,
836
- ModalCloseButton,
837
- ModalContent,
838
- ModalDescription,
839
- ModalDivider,
840
- ModalFooter,
841
- ModalFooterCheckbox,
842
- ModalHeader,
843
- ModalHeaderIcon,
844
- ModalOverlay,
845
- ModalPortal,
846
- ModalRoot,
847
- ModalSurface,
848
- ModalTitle,
849
- ModalTrigger,
850
- modalContentVariants,
851
- modalSurfaceVariants,
852
- }
853
- export type {
854
- ConfirmModalMessages,
855
- ConfirmModalProps,
856
- ModalActionButtonProps,
857
- ModalActionsProps,
858
- ModalBodyProps,
859
- ModalCloseButtonProps,
860
- ModalContentProps,
861
- ModalFooterCheckboxProps,
862
- ModalFooterProps,
863
- ModalHeaderIconProps,
864
- ModalHeaderProps,
865
- ModalIntent,
866
- ModalMessages,
867
- ModalOverlayProps,
868
- ModalProps,
869
- ModalRootProps,
870
- ModalSize,
871
- ModalSurfaceProps,
872
- }
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { InfoIcon, TriangleAlertIcon, XIcon } from "lucide-react"
7
+
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogTitle,
15
+ type AlertDialogContentProps,
16
+ } from "@/components/ui/alert-dialog"
17
+ import { Button, type ButtonProps } from "@/components/ui/button"
18
+ import { Checkbox, type CheckboxProps } from "@/components/ui/checkbox"
19
+ import { Spinner } from "@/components/ui/spinner"
20
+ import { cn } from "@/lib/utils"
21
+ import { useResolvedDirection } from "@/hooks/use-direction"
22
+
23
+ type ModalSize = "sm" | "default" | "lg"
24
+ type ModalIntent = "default" | "warning" | "danger"
25
+
26
+ type ModalMessages = {
27
+ checkboxLabel: React.ReactNode
28
+ closeLabel: string
29
+ primaryActionLabel: React.ReactNode
30
+ secondaryActionLabel: React.ReactNode
31
+ }
32
+
33
+ type ConfirmModalMessages = {
34
+ cancelLabel: React.ReactNode
35
+ closeLabel: string
36
+ confirmLabel: React.ReactNode
37
+ confirmLoadingLabel: React.ReactNode
38
+ }
39
+
40
+ const modalSurfaceBaseClassName = [
41
+ "flex max-h-[calc(100vh-(var(--bh-space-8xl-48)*2))] w-[var(--bh-modal-width)] max-w-[calc(100vw-(var(--bh-space-4xl-20)*2))] flex-col overflow-hidden rounded-[var(--bh-radius-2xl-12)] bg-[var(--bh-bg-raised)] text-[var(--bh-content-default)] shadow-[var(--elevation-modal)] outline-none",
42
+ "[--bh-modal-border:var(--bh-border-subtle)] [--elevation-modal:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-modal-border),var(--elevation-06)]",
43
+ ]
44
+
45
+ const modalSizeVariants = {
46
+ sm: "[--bh-modal-width:calc(var(--bh-space-19xl-384)+var(--bh-space-8xl-48)+var(--bh-space-md-8))]",
47
+ default:
48
+ "[--bh-modal-width:calc(var(--bh-space-19xl-384)+var(--bh-space-12xl-128)+var(--bh-space-8xl-48))]",
49
+ lg: "[--bh-modal-width:calc(var(--bh-space-19xl-384)+var(--bh-space-16xl-256)+var(--bh-space-9xl-64)-var(--bh-space-xs-4))]",
50
+ } as const
51
+
52
+ const modalSurfaceVariants = cva(modalSurfaceBaseClassName, {
53
+ variants: {
54
+ size: modalSizeVariants,
55
+ },
56
+ defaultVariants: {
57
+ size: "sm",
58
+ },
59
+ })
60
+
61
+ const modalContentVariants = cva(
62
+ [
63
+ "fixed left-1/2 top-1/2 z-[var(--bh-z-overlay)] -translate-x-1/2 -translate-y-1/2",
64
+ ...modalSurfaceBaseClassName,
65
+ ],
66
+ {
67
+ variants: {
68
+ size: modalSizeVariants,
69
+ },
70
+ defaultVariants: {
71
+ size: "sm",
72
+ },
73
+ }
74
+ )
75
+
76
+ const modalHeaderIconIntentClassNames: Record<ModalIntent, string> = {
77
+ default: "text-[var(--bh-content-subtle)]",
78
+ danger:
79
+ "rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-danger-subtle)] text-[var(--bh-content-danger-default)]",
80
+ warning:
81
+ "rounded-[var(--bh-radius-full)] bg-[var(--bh-bg-warning-subtle)] text-[var(--bh-content-warning-default)]",
82
+ }
83
+
84
+ const modalTitleClassName =
85
+ "min-w-0 flex-1 text-start text-[length:var(--bh-text-heading-sm-semibold-font-size)] font-[var(--bh-text-heading-sm-semibold-font-weight)] leading-[var(--bh-text-heading-sm-semibold-line-height)] tracking-[var(--bh-text-heading-sm-semibold-letter-spacing)] text-[var(--bh-content-default)]"
86
+
87
+ const modalDescriptionClassName =
88
+ "min-w-0 flex-1 text-start text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
89
+
90
+ type ModalRootProps = React.ComponentProps<typeof DialogPrimitive.Root>
91
+ type ModalContentProps = React.ComponentPropsWithoutRef<
92
+ typeof DialogPrimitive.Content
93
+ > &
94
+ VariantProps<typeof modalContentVariants> & {
95
+ overlayClassName?: string
96
+ }
97
+ type ModalOverlayProps = React.ComponentPropsWithoutRef<
98
+ typeof DialogPrimitive.Overlay
99
+ >
100
+ type ModalSurfaceProps = React.ComponentProps<"section"> &
101
+ VariantProps<typeof modalSurfaceVariants>
102
+ type ModalHeaderProps = React.ComponentProps<"header">
103
+ type ModalBodyProps = React.ComponentProps<"div">
104
+ type ModalFooterProps = React.ComponentProps<"footer">
105
+ type ModalActionsProps = React.ComponentProps<"div">
106
+ type ModalHeaderIconProps = React.ComponentProps<"span"> & {
107
+ intent?: ModalIntent
108
+ }
109
+ type ModalCloseButtonProps = React.ComponentProps<"button"> & {
110
+ label?: string
111
+ }
112
+ type ConfirmModalContentProps = AlertDialogContentProps
113
+ type ModalFooterCheckboxProps = Omit<React.ComponentProps<"div">, "children"> & {
114
+ checked?: CheckboxProps["checked"]
115
+ defaultChecked?: CheckboxProps["defaultChecked"]
116
+ disabled?: CheckboxProps["disabled"]
117
+ label?: React.ReactNode
118
+ onCheckedChange?: CheckboxProps["onCheckedChange"]
119
+ }
120
+ type ModalActionButtonProps = ButtonProps
121
+
122
+ type ModalProps = Omit<ModalRootProps, "children"> &
123
+ VariantProps<typeof modalContentVariants> & {
124
+ checkboxLabel?: React.ReactNode
125
+ children?: React.ReactNode
126
+ className?: string
127
+ closeLabel?: string
128
+ description?: React.ReactNode
129
+ dir?: "ltr" | "rtl"
130
+ footer?: React.ReactNode
131
+ messages?: Partial<ModalMessages>
132
+ overlayClassName?: string
133
+ primaryActionLabel?: React.ReactNode
134
+ secondaryActionLabel?: React.ReactNode
135
+ showCheckbox?: boolean
136
+ showCloseButton?: boolean
137
+ showDivider?: boolean
138
+ showFooter?: boolean
139
+ showIcon?: boolean
140
+ title: React.ReactNode
141
+ }
142
+
143
+ type ConfirmModalProps = Omit<
144
+ ModalProps,
145
+ | "checkboxLabel"
146
+ | "footer"
147
+ | "messages"
148
+ | "primaryActionLabel"
149
+ | "secondaryActionLabel"
150
+ | "showCheckbox"
151
+ > & {
152
+ cancelButtonProps?: ModalActionButtonProps
153
+ cancelLabel?: React.ReactNode
154
+ closeOnCancel?: boolean
155
+ closeOnConfirm?: boolean
156
+ confirmButtonProps?: ModalActionButtonProps
157
+ confirmDisabled?: boolean
158
+ confirmLabel?: React.ReactNode
159
+ confirmLoading?: boolean
160
+ confirmLoadingLabel?: React.ReactNode
161
+ confirmVariant?: ModalActionButtonProps["variant"]
162
+ intent?: ModalIntent
163
+ messages?: Partial<ConfirmModalMessages>
164
+ onCancel?: React.MouseEventHandler<HTMLButtonElement>
165
+ onConfirm?: React.MouseEventHandler<HTMLButtonElement>
166
+ onEscapeKeyDown?: ModalContentProps["onEscapeKeyDown"]
167
+ onOpenAutoFocus?: ModalContentProps["onOpenAutoFocus"]
168
+ preventEscapeDismiss?: boolean
169
+ showCancelButton?: boolean
170
+ }
171
+
172
+ const defaultModalMessages: ModalMessages = {
173
+ checkboxLabel: "Don't show me again",
174
+ closeLabel: "Close modal",
175
+ primaryActionLabel: "Action",
176
+ secondaryActionLabel: "Action",
177
+ }
178
+
179
+ const arabicModalMessages: ModalMessages = {
180
+ checkboxLabel: "\u0627\u0644\u062a\u0633\u0645\u064a\u0629",
181
+ closeLabel: "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0646\u0645\u0648\u0630\u062c",
182
+ primaryActionLabel: "\u0627\u0646\u0642\u0631 \u0647\u0646\u0627",
183
+ secondaryActionLabel: "\u0627\u0646\u0642\u0631 \u0647\u0646\u0627",
184
+ }
185
+
186
+ const defaultConfirmModalMessages: ConfirmModalMessages = {
187
+ cancelLabel: "Cancel",
188
+ closeLabel: "Close modal",
189
+ confirmLabel: "Confirm",
190
+ confirmLoadingLabel: "Confirming",
191
+ }
192
+
193
+ const arabicConfirmModalMessages: ConfirmModalMessages = {
194
+ cancelLabel: "\u0625\u0644\u063a\u0627\u0621",
195
+ closeLabel: "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0646\u0645\u0648\u0630\u062c",
196
+ confirmLabel: "\u062a\u0623\u0643\u064a\u062f",
197
+ confirmLoadingLabel: "\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0623\u0643\u064a\u062f",
198
+ }
199
+
200
+ const ModalRoot = DialogPrimitive.Root
201
+ const ModalTrigger = DialogPrimitive.Trigger
202
+ const ModalPortal = DialogPrimitive.Portal
203
+ const ModalClose = DialogPrimitive.Close
204
+
205
+ const ModalOverlay = React.forwardRef<
206
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
207
+ ModalOverlayProps
208
+ >(function ModalOverlay({ className, ...props }, ref) {
209
+ return (
210
+ <DialogPrimitive.Overlay
211
+ data-slot="modal-overlay"
212
+ ref={ref}
213
+ className={cn("fixed inset-0 z-[var(--bh-z-overlay)] bg-[var(--bh-bg-backdrop)]", className)}
214
+ {...props}
215
+ />
216
+ )
217
+ })
218
+
219
+ const ModalContent = React.forwardRef<
220
+ React.ElementRef<typeof DialogPrimitive.Content>,
221
+ ModalContentProps
222
+ >(function ModalContent(
223
+ { children, className, dir, overlayClassName, size, ...props },
224
+ ref
225
+ ) {
226
+ const contentRef = React.useRef<
227
+ React.ElementRef<typeof DialogPrimitive.Content> | null
228
+ >(null)
229
+ const resolvedDirection = useResolvedDirection(
230
+ dir === "ltr" || dir === "rtl" ? dir : undefined,
231
+ contentRef
232
+ )
233
+ const setContentRef = React.useCallback(
234
+ (node: React.ElementRef<typeof DialogPrimitive.Content> | null) => {
235
+ contentRef.current = node
236
+ if (typeof ref === "function") ref(node)
237
+ else if (ref) ref.current = node
238
+ },
239
+ [ref]
240
+ )
241
+
242
+ return (
243
+ <ModalPortal>
244
+ <ModalOverlay className={overlayClassName} />
245
+ <DialogPrimitive.Content
246
+ data-size={size ?? "sm"}
247
+ data-slot="modal-content"
248
+ dir={resolvedDirection}
249
+ ref={setContentRef}
250
+ className={cn(modalContentVariants({ size, className }))}
251
+ {...props}
252
+ >
253
+ {children}
254
+ </DialogPrimitive.Content>
255
+ </ModalPortal>
256
+ )
257
+ })
258
+
259
+ const ModalSurface = React.forwardRef<HTMLElement, ModalSurfaceProps>(
260
+ function ModalSurface({ className, size, ...props }, ref) {
261
+ return (
262
+ <section
263
+ data-size={size ?? "sm"}
264
+ data-slot="modal-surface"
265
+ ref={ref}
266
+ className={cn(modalSurfaceVariants({ size, className }))}
267
+ {...props}
268
+ />
269
+ )
270
+ }
271
+ )
272
+
273
+ const ModalHeader = React.forwardRef<HTMLElement, ModalHeaderProps>(function ModalHeader({
274
+ className,
275
+ ...props
276
+ }, ref) {
277
+ return (
278
+ <header
279
+ data-slot="modal-header"
280
+ ref={ref}
281
+ className={cn(
282
+ "relative flex w-full shrink-0 items-center gap-[var(--bh-space-sm-6)] px-[var(--bh-space-4xl-20)] pb-[var(--bh-space-md-8)] pe-[var(--bh-space-9xl-64)] pt-[var(--bh-space-4xl-20)] text-start",
283
+ className
284
+ )}
285
+ {...props}
286
+ />
287
+ )
288
+ })
289
+
290
+ const ModalTitle = React.forwardRef<
291
+ React.ElementRef<typeof DialogPrimitive.Title>,
292
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
293
+ >(function ModalTitle({ className, dir = "auto", ...props }, ref) {
294
+ return (
295
+ <DialogPrimitive.Title
296
+ data-slot="modal-title"
297
+ dir={dir}
298
+ ref={ref}
299
+ className={cn(modalTitleClassName, className)}
300
+ {...props}
301
+ />
302
+ )
303
+ })
304
+
305
+ const ModalDescription = React.forwardRef<
306
+ React.ElementRef<typeof DialogPrimitive.Description>,
307
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
308
+ >(function ModalDescription({ className, dir = "auto", ...props }, ref) {
309
+ return (
310
+ <DialogPrimitive.Description
311
+ data-slot="modal-description"
312
+ dir={dir}
313
+ ref={ref}
314
+ className={cn(modalDescriptionClassName, className)}
315
+ {...props}
316
+ />
317
+ )
318
+ })
319
+
320
+ // One close affordance, two dismissal wrappers. Dialog uses ModalClose and
321
+ // AlertDialog uses AlertDialogCancel, but the button itself is identical — so
322
+ // an a11y or focus-ring fix here reaches both instead of one.
323
+ const ModalCloseButtonSurface = React.forwardRef<
324
+ HTMLButtonElement,
325
+ ModalCloseButtonProps
326
+ >(function ModalCloseButtonSurface(
327
+ { children, className, label = "Close modal", type = "button", ...props },
328
+ ref
329
+ ) {
330
+ return (
331
+ <button
332
+ aria-label={label}
333
+ data-slot="modal-close"
334
+ ref={ref}
335
+ type={type}
336
+ className={cn(
337
+ "absolute end-[var(--bh-space-3xl-16)] top-1/2 inline-flex size-[var(--bh-space-6xl-32)] -translate-y-1/2 items-center justify-center rounded-[var(--bh-radius-full)] border-0 bg-transparent p-0 text-[var(--bh-content-subtle)] outline-none transition-[background-color,box-shadow,color] hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)] focus-visible:shadow-[var(--elevation-button-focus)]",
338
+ className
339
+ )}
340
+ {...props}
341
+ >
342
+ {children ?? (
343
+ <XIcon
344
+ aria-hidden="true"
345
+ className="size-[var(--bh-space-5xl-24)]"
346
+ strokeWidth="var(--bh-icon-stroke-200)"
347
+ />
348
+ )}
349
+ </button>
350
+ )
351
+ })
352
+
353
+ const ConfirmModalCloseButton = React.forwardRef<
354
+ HTMLButtonElement,
355
+ ModalCloseButtonProps
356
+ >(function ConfirmModalCloseButton(props, ref) {
357
+ return (
358
+ <AlertDialogCancel asChild>
359
+ <ModalCloseButtonSurface ref={ref} {...props} />
360
+ </AlertDialogCancel>
361
+ )
362
+ })
363
+
364
+ const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(function ModalBody({
365
+ className,
366
+ ...props
367
+ }, ref) {
368
+ return (
369
+ <div
370
+ data-slot="modal-body"
371
+ ref={ref}
372
+ className={cn(
373
+ "flex w-full shrink-0 items-start px-[var(--bh-space-4xl-20)] pb-[var(--bh-space-4xl-20)] pt-[var(--bh-space-lg-10)]",
374
+ className
375
+ )}
376
+ {...props}
377
+ />
378
+ )
379
+ })
380
+
381
+ const ModalDivider = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
382
+ function ModalDivider({ className, role = "separator", ...props }, ref) {
383
+ return (
384
+ <div
385
+ data-slot="modal-divider"
386
+ ref={ref}
387
+ role={role}
388
+ className={cn("w-full border-t border-[var(--bh-border-subtle)]", className)}
389
+ {...props}
390
+ />
391
+ )
392
+ }
393
+ )
394
+
395
+ const ModalFooter = React.forwardRef<HTMLElement, ModalFooterProps>(function ModalFooter({
396
+ className,
397
+ ...props
398
+ }, ref) {
399
+ return (
400
+ <footer
401
+ data-slot="modal-footer"
402
+ ref={ref}
403
+ className={cn(
404
+ "flex w-full shrink-0 items-center justify-end gap-[var(--bh-space-lg-10)] px-[var(--bh-space-4xl-20)] py-[var(--bh-space-3xl-16)]",
405
+ className
406
+ )}
407
+ {...props}
408
+ />
409
+ )
410
+ })
411
+
412
+ function ModalHeaderIcon({
413
+ children,
414
+ className,
415
+ intent = "default",
416
+ ...props
417
+ }: ModalHeaderIconProps) {
418
+ return (
419
+ <span
420
+ aria-hidden="true"
421
+ data-intent={intent}
422
+ data-slot="modal-header-icon"
423
+ className={cn(
424
+ "inline-flex size-[var(--bh-space-5xl-24)] shrink-0 items-center justify-center",
425
+ modalHeaderIconIntentClassNames[intent],
426
+ className
427
+ )}
428
+ {...props}
429
+ >
430
+ {children ?? (
431
+ <InfoIcon className="size-[var(--bh-space-3xl-16)]" strokeWidth="var(--bh-icon-stroke-200)" />
432
+ )}
433
+ </span>
434
+ )
435
+ }
436
+
437
+ const ModalCloseButton = React.forwardRef<HTMLButtonElement, ModalCloseButtonProps>(
438
+ function ModalCloseButton(props, ref) {
439
+ return (
440
+ <ModalClose asChild>
441
+ <ModalCloseButtonSurface ref={ref} {...props} />
442
+ </ModalClose>
443
+ )
444
+ }
445
+ )
446
+
447
+ function ModalFooterCheckbox({
448
+ checked,
449
+ className,
450
+ defaultChecked,
451
+ disabled,
452
+ label = defaultModalMessages.checkboxLabel,
453
+ onCheckedChange,
454
+ ...props
455
+ }: ModalFooterCheckboxProps) {
456
+ const generatedId = React.useId()
457
+ const labelId = `${generatedId}-label`
458
+
459
+ return (
460
+ <div
461
+ data-slot="modal-checkbox"
462
+ className={cn(
463
+ "flex min-w-0 flex-1 items-center gap-[var(--bh-space-md-8)] text-start",
464
+ className
465
+ )}
466
+ {...props}
467
+ >
468
+ <Checkbox
469
+ aria-labelledby={labelId}
470
+ checked={checked}
471
+ defaultChecked={defaultChecked}
472
+ disabled={disabled}
473
+ onCheckedChange={onCheckedChange}
474
+ />
475
+ <span
476
+ data-slot="modal-checkbox-label"
477
+ dir="auto"
478
+ id={labelId}
479
+ className="min-w-0 flex-1 text-[length:var(--bh-text-body-md-regular-font-size)] font-[var(--bh-text-body-md-regular-font-weight)] leading-[var(--bh-text-body-md-regular-line-height)] tracking-[var(--bh-text-body-md-regular-letter-spacing)] text-[var(--bh-content-default)]"
480
+ >
481
+ {label}
482
+ </span>
483
+ </div>
484
+ )
485
+ }
486
+
487
+ function ModalActions({ className, ...props }: ModalActionsProps) {
488
+ return (
489
+ <div
490
+ data-slot="modal-actions"
491
+ className={cn(
492
+ "flex shrink-0 items-center justify-end gap-[var(--bh-space-xs-4)]",
493
+ className
494
+ )}
495
+ {...props}
496
+ />
497
+ )
498
+ }
499
+
500
+ const ModalActionButton = React.forwardRef<HTMLButtonElement, ModalActionButtonProps>(
501
+ function ModalActionButton(
502
+ { children = "Action", size = "sm", type = "button", variant = "default", ...props },
503
+ ref
504
+ ) {
505
+ return (
506
+ <Button
507
+ data-slot="modal-action"
508
+ ref={ref}
509
+ size={size}
510
+ type={type}
511
+ variant={variant}
512
+ {...props}
513
+ >
514
+ {children}
515
+ </Button>
516
+ )
517
+ }
518
+ )
519
+
520
+ function Modal({
521
+ checkboxLabel,
522
+ children,
523
+ className,
524
+ closeLabel,
525
+ defaultOpen = false,
526
+ description,
527
+ dir,
528
+ footer,
529
+ messages: messageOverrides,
530
+ modal = true,
531
+ onOpenChange,
532
+ open,
533
+ overlayClassName,
534
+ primaryActionLabel,
535
+ secondaryActionLabel,
536
+ showCheckbox = true,
537
+ showCloseButton = true,
538
+ showDivider = true,
539
+ showFooter = true,
540
+ showIcon = true,
541
+ size = "sm",
542
+ title,
543
+ ...props
544
+ }: ModalProps) {
545
+ const rootProps =
546
+ open === undefined
547
+ ? { defaultOpen, modal, onOpenChange }
548
+ : { modal, onOpenChange, open }
549
+ const messages = { ...defaultModalMessages, ...messageOverrides }
550
+ const resolvedTitle = title
551
+ const resolvedDescription = description
552
+ const resolvedCheckboxLabel =
553
+ checkboxLabel ?? messages.checkboxLabel
554
+ const resolvedPrimaryAction =
555
+ primaryActionLabel ?? messages.primaryActionLabel
556
+ const resolvedSecondaryAction =
557
+ secondaryActionLabel ?? messages.secondaryActionLabel
558
+ const resolvedCloseLabel = closeLabel ?? messages.closeLabel
559
+
560
+ return (
561
+ <ModalRoot {...rootProps}>
562
+ <ModalContent
563
+ data-slot="modal"
564
+ dir={dir}
565
+ overlayClassName={overlayClassName}
566
+ size={size}
567
+ className={className}
568
+ {...props}
569
+ >
570
+ <ModalHeader>
571
+ {showIcon ? <ModalHeaderIcon /> : null}
572
+ <ModalTitle>{resolvedTitle}</ModalTitle>
573
+ {showCloseButton ? <ModalCloseButton label={resolvedCloseLabel} /> : null}
574
+ </ModalHeader>
575
+ <ModalBody>
576
+ {children ?? <ModalDescription>{resolvedDescription}</ModalDescription>}
577
+ </ModalBody>
578
+ {showDivider ? <ModalDivider /> : null}
579
+ {showFooter ? (
580
+ footer ?? (
581
+ <ModalFooter>
582
+ {showCheckbox ? (
583
+ <ModalFooterCheckbox label={resolvedCheckboxLabel} />
584
+ ) : null}
585
+ <ModalActions>
586
+ <ModalActionButton variant="soft">
587
+ {resolvedSecondaryAction}
588
+ </ModalActionButton>
589
+ <ModalActionButton>{resolvedPrimaryAction}</ModalActionButton>
590
+ </ModalActions>
591
+ </ModalFooter>
592
+ )
593
+ ) : null}
594
+ </ModalContent>
595
+ </ModalRoot>
596
+ )
597
+ }
598
+
599
+ function ConfirmModal({
600
+ cancelButtonProps,
601
+ cancelLabel,
602
+ children,
603
+ className,
604
+ closeLabel,
605
+ closeOnCancel = true,
606
+ closeOnConfirm = true,
607
+ confirmButtonProps,
608
+ confirmDisabled = false,
609
+ confirmLabel,
610
+ confirmLoading = false,
611
+ confirmLoadingLabel,
612
+ confirmVariant,
613
+ defaultOpen = false,
614
+ description,
615
+ dir,
616
+ intent = "danger",
617
+ messages: messageOverrides,
618
+ modal = true,
619
+ onCancel,
620
+ onConfirm,
621
+ onEscapeKeyDown,
622
+ onOpenAutoFocus,
623
+ onOpenChange,
624
+ open,
625
+ overlayClassName,
626
+ preventEscapeDismiss = false,
627
+ showCancelButton = true,
628
+ showCloseButton = false,
629
+ showDivider = true,
630
+ showFooter = true,
631
+ showIcon = true,
632
+ size = "sm",
633
+ title,
634
+ ...props
635
+ }: ConfirmModalProps) {
636
+ const cancelButtonRef = React.useRef<HTMLButtonElement>(null)
637
+ const confirmButtonRef = React.useRef<HTMLButtonElement>(null)
638
+ const directionBoundaryRef = React.useRef<
639
+ React.ElementRef<typeof AlertDialogContent> | null
640
+ >(null)
641
+ const resolvedDirection = useResolvedDirection(dir, directionBoundaryRef)
642
+ const rootProps =
643
+ open === undefined
644
+ ? { defaultOpen, modal, onOpenChange }
645
+ : { modal, onOpenChange, open }
646
+ const messages = { ...defaultConfirmModalMessages, ...messageOverrides }
647
+ const resolvedTitle = title
648
+ const resolvedDescription = description
649
+ const resolvedCancelLabel =
650
+ cancelLabel ?? messages.cancelLabel
651
+ const resolvedConfirmLabel =
652
+ confirmLabel ?? messages.confirmLabel
653
+ const resolvedConfirmLoadingLabel =
654
+ confirmLoadingLabel ?? messages.confirmLoadingLabel
655
+ const resolvedCloseLabel = closeLabel ?? messages.closeLabel
656
+ const {
657
+ children: cancelButtonChildren,
658
+ onClick: cancelButtonOnClick,
659
+ variant: cancelButtonVariant = "soft",
660
+ ...cancelActionProps
661
+ } = cancelButtonProps ?? {}
662
+ const {
663
+ children: confirmButtonChildren,
664
+ disabled: confirmButtonDisabled,
665
+ onClick: confirmButtonOnClick,
666
+ variant: confirmButtonVariant = confirmVariant ?? getConfirmActionVariant(intent),
667
+ ...confirmActionProps
668
+ } = confirmButtonProps ?? {}
669
+ const isConfirmDisabled = confirmDisabled || confirmLoading || confirmButtonDisabled
670
+
671
+ const handleOpenAutoFocus: NonNullable<
672
+ ConfirmModalContentProps["onOpenAutoFocus"]
673
+ > = React.useCallback(
674
+ (event) => {
675
+ onOpenAutoFocus?.(event)
676
+ if (event.defaultPrevented) return
677
+
678
+ const focusTarget = showCancelButton
679
+ ? cancelButtonRef.current
680
+ : confirmButtonRef.current
681
+ if (!focusTarget) return
682
+
683
+ event.preventDefault()
684
+ focusTarget.focus()
685
+ },
686
+ [onOpenAutoFocus, showCancelButton]
687
+ )
688
+
689
+ const handleEscapeKeyDown: NonNullable<
690
+ ConfirmModalContentProps["onEscapeKeyDown"]
691
+ > = React.useCallback(
692
+ (event) => {
693
+ onEscapeKeyDown?.(event)
694
+ if (!event.defaultPrevented && preventEscapeDismiss) {
695
+ event.preventDefault()
696
+ }
697
+ },
698
+ [onEscapeKeyDown, preventEscapeDismiss]
699
+ )
700
+ const handleCancel: React.MouseEventHandler<HTMLButtonElement> = (event) => {
701
+ cancelButtonOnClick?.(event)
702
+ if (!event.defaultPrevented) {
703
+ onCancel?.(event)
704
+ }
705
+ }
706
+
707
+ const handleConfirm: React.MouseEventHandler<HTMLButtonElement> = (event) => {
708
+ confirmButtonOnClick?.(event)
709
+ if (!event.defaultPrevented) {
710
+ onConfirm?.(event)
711
+ }
712
+ }
713
+
714
+ const cancelAction = showCancelButton ? (
715
+ <ModalActionButton
716
+ ref={cancelButtonRef}
717
+ variant={cancelButtonVariant}
718
+ onClick={handleCancel}
719
+ {...cancelActionProps}
720
+ >
721
+ {cancelButtonChildren ?? resolvedCancelLabel}
722
+ </ModalActionButton>
723
+ ) : null
724
+ const confirmAction = (
725
+ <ModalActionButton
726
+ aria-busy={confirmLoading || undefined}
727
+ disabled={isConfirmDisabled}
728
+ ref={confirmButtonRef}
729
+ variant={confirmButtonVariant}
730
+ onClick={handleConfirm}
731
+ {...confirmActionProps}
732
+ >
733
+ {confirmButtonChildren ??
734
+ (confirmLoading ? (
735
+ <>
736
+ <Spinner className="size-[var(--bh-space-3xl-16)]" />
737
+ <span data-slot="button-label" dir="auto" className="min-w-0 px-[var(--bh-modal-action-label-padding-x)]">
738
+ {resolvedConfirmLoadingLabel}
739
+ </span>
740
+ </>
741
+ ) : (
742
+ resolvedConfirmLabel
743
+ ))}
744
+ </ModalActionButton>
745
+ )
746
+
747
+ return (
748
+ <AlertDialog {...rootProps}>
749
+ <AlertDialogContent
750
+ data-intent={intent}
751
+ data-size={size}
752
+ data-slot="confirm-modal"
753
+ dir={resolvedDirection}
754
+ ref={directionBoundaryRef}
755
+ className={cn(modalContentVariants({ size, className }))}
756
+ overlayClassName={overlayClassName}
757
+ onEscapeKeyDown={handleEscapeKeyDown}
758
+ onOpenAutoFocus={handleOpenAutoFocus}
759
+ {...props}
760
+ >
761
+ <ModalHeader>
762
+ {showIcon ? (
763
+ <ModalHeaderIcon intent={intent}>
764
+ {intent === "default" ? (
765
+ <InfoIcon
766
+ className="size-[var(--bh-space-3xl-16)]"
767
+ strokeWidth="var(--bh-icon-stroke-200)"
768
+ />
769
+ ) : (
770
+ <TriangleAlertIcon
771
+ className="size-[var(--bh-space-3xl-16)]"
772
+ strokeWidth="var(--bh-icon-stroke-200)"
773
+ />
774
+ )}
775
+ </ModalHeaderIcon>
776
+ ) : null}
777
+ <AlertDialogTitle
778
+ data-slot="modal-title"
779
+ className={modalTitleClassName}
780
+ >
781
+ {resolvedTitle}
782
+ </AlertDialogTitle>
783
+ {showCloseButton ? (
784
+ <ConfirmModalCloseButton label={resolvedCloseLabel} />
785
+ ) : null}
786
+ </ModalHeader>
787
+ <ModalBody>
788
+ {children ?? (
789
+ <AlertDialogDescription
790
+ data-slot="modal-description"
791
+ className={modalDescriptionClassName}
792
+ >
793
+ {resolvedDescription}
794
+ </AlertDialogDescription>
795
+ )}
796
+ </ModalBody>
797
+ {showDivider ? <ModalDivider /> : null}
798
+ {showFooter ? (
799
+ <ModalFooter>
800
+ <ModalActions>
801
+ {cancelAction && closeOnCancel ? (
802
+ <AlertDialogCancel asChild>{cancelAction}</AlertDialogCancel>
803
+ ) : (
804
+ cancelAction
805
+ )}
806
+ {closeOnConfirm ? (
807
+ <AlertDialogAction asChild>{confirmAction}</AlertDialogAction>
808
+ ) : (
809
+ confirmAction
810
+ )}
811
+ </ModalActions>
812
+ </ModalFooter>
813
+ ) : null}
814
+ </AlertDialogContent>
815
+ </AlertDialog>
816
+ )
817
+ }
818
+
819
+ function getConfirmActionVariant(
820
+ intent: ModalIntent
821
+ ): ModalActionButtonProps["variant"] {
822
+ if (intent === "danger") return "danger"
823
+ if (intent === "warning") return "warning"
824
+ return "default"
825
+ }
826
+
827
+ export {
828
+ arabicConfirmModalMessages,
829
+ arabicModalMessages,
830
+ ConfirmModal,
831
+ Modal,
832
+ ModalActionButton,
833
+ ModalActions,
834
+ ModalBody,
835
+ ModalClose,
836
+ ModalCloseButton,
837
+ ModalContent,
838
+ ModalDescription,
839
+ ModalDivider,
840
+ ModalFooter,
841
+ ModalFooterCheckbox,
842
+ ModalHeader,
843
+ ModalHeaderIcon,
844
+ ModalOverlay,
845
+ ModalPortal,
846
+ ModalRoot,
847
+ ModalSurface,
848
+ ModalTitle,
849
+ ModalTrigger,
850
+ modalContentVariants,
851
+ modalSurfaceVariants,
852
+ }
853
+ export type {
854
+ ConfirmModalMessages,
855
+ ConfirmModalProps,
856
+ ModalActionButtonProps,
857
+ ModalActionsProps,
858
+ ModalBodyProps,
859
+ ModalCloseButtonProps,
860
+ ModalContentProps,
861
+ ModalFooterCheckboxProps,
862
+ ModalFooterProps,
863
+ ModalHeaderIconProps,
864
+ ModalHeaderProps,
865
+ ModalIntent,
866
+ ModalMessages,
867
+ ModalOverlayProps,
868
+ ModalProps,
869
+ ModalRootProps,
870
+ ModalSize,
871
+ ModalSurfaceProps,
872
+ }