banhaten 0.5.0 → 0.6.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 (290) hide show
  1. package/CHANGELOG.md +177 -139
  2. package/CONTRIBUTING.md +46 -46
  3. package/README.md +192 -192
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -13
  7. package/docs/design-system/README.md +22 -22
  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 -245
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -225
  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 +91 -90
  20. package/docs/design-system/components/accordion.md +52 -52
  21. package/docs/design-system/components/activity-feed.md +93 -93
  22. package/docs/design-system/components/activity-heatmap.md +56 -56
  23. package/docs/design-system/components/alert-dialog.md +71 -71
  24. package/docs/design-system/components/alert.md +103 -97
  25. package/docs/design-system/components/app-header.md +97 -0
  26. package/docs/design-system/components/aspect-ratio.md +45 -45
  27. package/docs/design-system/components/attribute.md +101 -89
  28. package/docs/design-system/components/autocomplete.md +75 -75
  29. package/docs/design-system/components/avatar.md +53 -53
  30. package/docs/design-system/components/badge.md +55 -55
  31. package/docs/design-system/components/banner.md +86 -86
  32. package/docs/design-system/components/breadcrumbs.md +181 -177
  33. package/docs/design-system/components/button-group.md +97 -97
  34. package/docs/design-system/components/button.md +80 -80
  35. package/docs/design-system/components/card.md +102 -88
  36. package/docs/design-system/components/carousel.md +46 -46
  37. package/docs/design-system/components/catalog-components.md +46 -46
  38. package/docs/design-system/components/checkbox.md +53 -53
  39. package/docs/design-system/components/collapsible.md +63 -63
  40. package/docs/design-system/components/command.md +61 -61
  41. package/docs/design-system/components/context-menu.md +45 -45
  42. package/docs/design-system/components/date-picker.md +80 -80
  43. package/docs/design-system/components/divider.md +102 -102
  44. package/docs/design-system/components/empty-state.md +56 -56
  45. package/docs/design-system/components/field.md +70 -70
  46. package/docs/design-system/components/file-upload.md +186 -186
  47. package/docs/design-system/components/hover-card.md +47 -47
  48. package/docs/design-system/components/icons.md +48 -48
  49. package/docs/design-system/components/input-group.md +57 -57
  50. package/docs/design-system/components/input-otp.md +56 -56
  51. package/docs/design-system/components/input.md +111 -109
  52. package/docs/design-system/components/item.md +58 -58
  53. package/docs/design-system/components/kbd.md +49 -49
  54. package/docs/design-system/components/label.md +49 -49
  55. package/docs/design-system/components/menu.md +94 -73
  56. package/docs/design-system/components/menubar.md +46 -46
  57. package/docs/design-system/components/modal.md +104 -104
  58. package/docs/design-system/components/native-select.md +53 -53
  59. package/docs/design-system/components/navigation-menu.md +49 -49
  60. package/docs/design-system/components/onboarding-step-list-item.md +81 -81
  61. package/docs/design-system/components/page-header.md +127 -93
  62. package/docs/design-system/components/pagination.md +56 -56
  63. package/docs/design-system/components/popover.md +59 -59
  64. package/docs/design-system/components/progress-slider.md +49 -49
  65. package/docs/design-system/components/progress.md +76 -76
  66. package/docs/design-system/components/radio-card.md +50 -50
  67. package/docs/design-system/components/radio-group.md +56 -56
  68. package/docs/design-system/components/resizable.md +43 -43
  69. package/docs/design-system/components/scroll-area.md +50 -50
  70. package/docs/design-system/components/segmented-control.md +54 -54
  71. package/docs/design-system/components/select.md +68 -64
  72. package/docs/design-system/components/sheet.md +66 -66
  73. package/docs/design-system/components/sidebar.md +167 -111
  74. package/docs/design-system/components/skeleton.md +74 -74
  75. package/docs/design-system/components/slideout.md +64 -64
  76. package/docs/design-system/components/slider.md +62 -62
  77. package/docs/design-system/components/social-button.md +49 -49
  78. package/docs/design-system/components/spinner.md +62 -62
  79. package/docs/design-system/components/steps.md +65 -65
  80. package/docs/design-system/components/table.md +499 -422
  81. package/docs/design-system/components/tabs.md +74 -59
  82. package/docs/design-system/components/tag.md +79 -79
  83. package/docs/design-system/components/textarea.md +49 -49
  84. package/docs/design-system/components/timeline.md +82 -82
  85. package/docs/design-system/components/toast.md +57 -57
  86. package/docs/design-system/components/toggle.md +80 -80
  87. package/docs/design-system/components/toolbar.md +89 -89
  88. package/docs/design-system/components/tooltip.md +92 -92
  89. package/docs/design-system/components/typography.md +18 -18
  90. package/docs/design-system/density-and-elevation.md +65 -65
  91. package/docs/design-system/design-system-test-missing-items.md +368 -368
  92. package/docs/design-system/direction-and-localization.md +23 -23
  93. package/docs/design-system/form-recipes.md +73 -73
  94. package/docs/design-system/gap-promotion-policy.md +21 -21
  95. package/docs/design-system/icons.md +74 -74
  96. package/docs/design-system/installation.md +70 -70
  97. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  98. package/docs/design-system/registry-and-cli.md +64 -64
  99. package/docs/design-system/release-policy.md +30 -30
  100. package/docs/design-system/roadmap.md +14 -14
  101. package/docs/design-system/support-matrix.md +23 -23
  102. package/docs/design-system/tabs.md +54 -54
  103. package/docs/design-system/template-gap-audit.md +32 -32
  104. package/docs/design-system/templates.md +59 -59
  105. package/docs/design-system/token-governance.md +72 -72
  106. package/package.json +98 -98
  107. package/registry/component-contract.json +40 -40
  108. package/registry/components/accordion.tsx +155 -155
  109. package/registry/components/alert-dialog.tsx +297 -297
  110. package/registry/components/alert.tsx +309 -293
  111. package/registry/components/app-header.tsx +151 -0
  112. package/registry/components/aspect-ratio.tsx +30 -30
  113. package/registry/components/attribute.tsx +489 -454
  114. package/registry/components/autocomplete.tsx +598 -598
  115. package/registry/components/avatar.tsx +385 -385
  116. package/registry/components/badge.tsx +630 -629
  117. package/registry/components/button-group.tsx +565 -565
  118. package/registry/components/button.tsx +128 -127
  119. package/registry/components/card.tsx +651 -638
  120. package/registry/components/carousel.tsx +296 -296
  121. package/registry/components/chart.tsx +203 -203
  122. package/registry/components/checkbox.tsx +247 -247
  123. package/registry/components/collapsible.tsx +109 -109
  124. package/registry/components/command.tsx +208 -208
  125. package/registry/components/context-menu.tsx +236 -236
  126. package/registry/components/date-picker.tsx +1289 -1289
  127. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  128. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  129. package/registry/components/expanded/Banner.tsx +179 -179
  130. package/registry/components/expanded/Breadcrumbs.tsx +135 -131
  131. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  132. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  133. package/registry/components/expanded/EmptyState.tsx +155 -155
  134. package/registry/components/expanded/FileUpload.tsx +543 -543
  135. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -60
  136. package/registry/components/expanded/PageHeader.tsx +209 -179
  137. package/registry/components/expanded/Slideout.tsx +108 -108
  138. package/registry/components/expanded/Steps.tsx +402 -402
  139. package/registry/components/expanded/Table.tsx +1109 -1092
  140. package/registry/components/expanded/Timeline.tsx +203 -203
  141. package/registry/components/expanded/activityFeed.css +248 -248
  142. package/registry/components/expanded/activityHeatmap.css +66 -66
  143. package/registry/components/expanded/banner.css +260 -260
  144. package/registry/components/expanded/breadcrumbs.css +157 -140
  145. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  146. package/registry/components/expanded/divider.css +75 -75
  147. package/registry/components/expanded/emptyState.css +111 -111
  148. package/registry/components/expanded/fileUpload.css +457 -457
  149. package/registry/components/expanded/onboardingStepListItem.css +135 -135
  150. package/registry/components/expanded/pageHeader.css +284 -248
  151. package/registry/components/expanded/slideout.css +276 -276
  152. package/registry/components/expanded/steps.css +356 -356
  153. package/registry/components/expanded/table.css +655 -606
  154. package/registry/components/expanded/timeline.css +286 -286
  155. package/registry/components/field.tsx +283 -283
  156. package/registry/components/hover-card.tsx +48 -48
  157. package/registry/components/input-content.ts +107 -107
  158. package/registry/components/input-group.tsx +130 -130
  159. package/registry/components/input-otp.tsx +584 -584
  160. package/registry/components/input.tsx +992 -990
  161. package/registry/components/item.tsx +189 -189
  162. package/registry/components/kbd.tsx +246 -246
  163. package/registry/components/label.tsx +78 -78
  164. package/registry/components/menu.tsx +1091 -978
  165. package/registry/components/menubar.tsx +226 -226
  166. package/registry/components/modal.tsx +872 -872
  167. package/registry/components/native-select.tsx +205 -205
  168. package/registry/components/navigation-menu.tsx +171 -171
  169. package/registry/components/pagination.css +36 -36
  170. package/registry/components/pagination.tsx +560 -560
  171. package/registry/components/popover.tsx +338 -338
  172. package/registry/components/progress-slider.tsx +216 -216
  173. package/registry/components/radio-card.tsx +676 -676
  174. package/registry/components/radio-group.tsx +570 -570
  175. package/registry/components/resizable.tsx +108 -108
  176. package/registry/components/scroll-area.tsx +91 -91
  177. package/registry/components/segmented-control.tsx +87 -87
  178. package/registry/components/select-content.tsx +218 -218
  179. package/registry/components/select.tsx +937 -937
  180. package/registry/components/sheet.tsx +305 -305
  181. package/registry/components/sidebar.tsx +1535 -1433
  182. package/registry/components/skeleton.css +54 -54
  183. package/registry/components/skeleton.tsx +482 -482
  184. package/registry/components/slider.tsx +380 -380
  185. package/registry/components/social-button.tsx +387 -387
  186. package/registry/components/spinner.tsx +78 -78
  187. package/registry/components/table-elements.tsx +287 -128
  188. package/registry/components/tabs.css +246 -222
  189. package/registry/components/tabs.tsx +245 -241
  190. package/registry/components/tag.tsx +423 -423
  191. package/registry/components/textarea.tsx +610 -610
  192. package/registry/components/toast.tsx +136 -136
  193. package/registry/components/toggle.tsx +256 -256
  194. package/registry/components/toolbar.tsx +539 -539
  195. package/registry/components/tooltip.tsx +433 -433
  196. package/registry/examples/alert-demo.tsx +34 -34
  197. package/registry/examples/alert-dialog-demo.tsx +42 -42
  198. package/registry/examples/app-header-demo.tsx +40 -0
  199. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  200. package/registry/examples/attribute-demo.tsx +65 -65
  201. package/registry/examples/autocomplete-demo.tsx +109 -109
  202. package/registry/examples/avatar-demo.tsx +132 -132
  203. package/registry/examples/badge-demo.tsx +69 -69
  204. package/registry/examples/button-demo.tsx +86 -86
  205. package/registry/examples/button-group-demo.tsx +63 -63
  206. package/registry/examples/card-demo.tsx +86 -86
  207. package/registry/examples/carousel-demo.tsx +43 -43
  208. package/registry/examples/chart-demo.tsx +18 -18
  209. package/registry/examples/checkbox-demo.tsx +62 -62
  210. package/registry/examples/collapsible-demo.tsx +16 -16
  211. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  212. package/registry/examples/command-demo.tsx +42 -42
  213. package/registry/examples/command-palette-demo.tsx +235 -235
  214. package/registry/examples/context-menu-demo.tsx +29 -29
  215. package/registry/examples/control-density-contract-demo.tsx +65 -65
  216. package/registry/examples/date-picker-demo.tsx +127 -127
  217. package/registry/examples/divider-demo.tsx +17 -17
  218. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  219. package/registry/examples/expanded/banner-demo.tsx +48 -48
  220. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  221. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  222. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  223. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  224. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  225. package/registry/examples/expanded/steps-demo.tsx +29 -29
  226. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  227. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  228. package/registry/examples/field-demo.tsx +51 -51
  229. package/registry/examples/hover-card-demo.tsx +23 -23
  230. package/registry/examples/input-actions-demo.tsx +36 -36
  231. package/registry/examples/input-demo.tsx +87 -87
  232. package/registry/examples/input-group-demo.tsx +16 -16
  233. package/registry/examples/input-otp-demo.tsx +72 -72
  234. package/registry/examples/item-demo.tsx +46 -46
  235. package/registry/examples/kbd-demo.tsx +12 -12
  236. package/registry/examples/label-demo.tsx +20 -20
  237. package/registry/examples/menu-demo.tsx +122 -122
  238. package/registry/examples/menubar-demo.tsx +34 -34
  239. package/registry/examples/modal-demo.tsx +74 -74
  240. package/registry/examples/native-select-demo.tsx +16 -16
  241. package/registry/examples/navigation-menu-demo.tsx +29 -29
  242. package/registry/examples/pagination-demo.tsx +20 -20
  243. package/registry/examples/popover-demo.tsx +547 -547
  244. package/registry/examples/progress-demo.tsx +37 -37
  245. package/registry/examples/progress-slider-demo.tsx +29 -29
  246. package/registry/examples/radio-card-demo.tsx +51 -51
  247. package/registry/examples/radio-group-demo.tsx +62 -62
  248. package/registry/examples/resizable-demo.tsx +22 -22
  249. package/registry/examples/results-toolbar-demo.tsx +44 -44
  250. package/registry/examples/scroll-area-demo.tsx +15 -15
  251. package/registry/examples/segmented-control-demo.tsx +39 -39
  252. package/registry/examples/select-demo.tsx +87 -87
  253. package/registry/examples/sheet-demo.tsx +47 -47
  254. package/registry/examples/sidebar-demo.tsx +101 -101
  255. package/registry/examples/slider-demo.tsx +31 -31
  256. package/registry/examples/social-button-demo.tsx +61 -61
  257. package/registry/examples/spinner-demo.tsx +59 -59
  258. package/registry/examples/tabs-demo.tsx +13 -13
  259. package/registry/examples/tag-demo.tsx +29 -29
  260. package/registry/examples/textarea-demo.tsx +79 -79
  261. package/registry/examples/toast-demo.tsx +35 -35
  262. package/registry/examples/toggle-demo.tsx +59 -59
  263. package/registry/examples/tooltip-demo.tsx +115 -115
  264. package/registry/guidance/USAGE.md +27 -27
  265. package/registry/hooks/use-direction.ts +207 -207
  266. package/registry/index.json +387 -17
  267. package/registry/styles/globals.css +46 -0
  268. package/registry.json +24 -2
  269. package/schema/config.schema.json +51 -51
  270. package/schema/registry.schema.json +194 -194
  271. package/schema/tokens.schema.json +167 -167
  272. package/src/cli/command-line.js +194 -194
  273. package/src/cli/config-editors.js +9 -9
  274. package/src/cli/doctor.js +399 -399
  275. package/src/cli/file-transaction.js +302 -302
  276. package/src/cli/global-css.js +153 -153
  277. package/src/cli/index.js +1002 -1002
  278. package/src/cli/install-plan.js +592 -592
  279. package/src/cli/install-receipt.js +171 -171
  280. package/src/cli/jsonc.js +203 -203
  281. package/src/cli/package-install-state.js +254 -254
  282. package/src/cli/package-manager.js +238 -238
  283. package/src/cli/package-plan.js +185 -185
  284. package/src/cli/package-version.js +97 -97
  285. package/src/cli/registry-query.js +207 -207
  286. package/src/cli/reporting.js +55 -55
  287. package/src/cli/schema-validation.js +19 -19
  288. package/src/cli/source-scanner.js +686 -686
  289. package/src/cli/vite-config.js +478 -478
  290. package/tokens/banhaten.tokens.json +184 -0
@@ -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(--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
- }
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
+ }