banhaten 0.1.2 → 0.2.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 (365) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +44 -0
  3. package/README.md +145 -328
  4. package/SECURITY.md +15 -0
  5. package/banhaten.config.example.json +1 -3
  6. package/docs/design-system/README.md +20 -0
  7. package/docs/design-system/adoption-quality-remediation.md +87 -0
  8. package/docs/design-system/agent-usage.md +13 -0
  9. package/docs/design-system/appearance-presets.md +189 -0
  10. package/docs/design-system/appearances/default.md +94 -0
  11. package/docs/design-system/appearances/rounded.md +95 -0
  12. package/docs/design-system/appearances/sharp.md +95 -0
  13. package/docs/design-system/component-docs-pattern.md +81 -0
  14. package/docs/design-system/component-showcase-consistency-report.md +217 -0
  15. package/docs/design-system/component-token-consistency-audit.md +173 -0
  16. package/docs/design-system/components/README.md +75 -0
  17. package/docs/design-system/components/accordion.md +67 -0
  18. package/docs/design-system/components/activity-feed.md +106 -0
  19. package/docs/design-system/components/alert-dialog.md +87 -0
  20. package/docs/design-system/components/alert.md +111 -0
  21. package/docs/design-system/components/aspect-ratio.md +59 -0
  22. package/docs/design-system/components/attribute.md +103 -0
  23. package/docs/design-system/components/autocomplete.md +93 -0
  24. package/docs/design-system/components/avatar.md +70 -0
  25. package/docs/design-system/components/badge.md +72 -0
  26. package/docs/design-system/components/banner.md +99 -0
  27. package/docs/design-system/components/breadcrumbs.md +188 -0
  28. package/docs/design-system/components/button-group.md +117 -0
  29. package/docs/design-system/components/button.md +95 -0
  30. package/docs/design-system/components/card.md +95 -0
  31. package/docs/design-system/components/carousel.md +61 -0
  32. package/docs/design-system/components/catalog-components.md +59 -0
  33. package/docs/design-system/components/chart.md +58 -0
  34. package/docs/design-system/components/checkbox.md +68 -0
  35. package/docs/design-system/components/collapsible.md +63 -0
  36. package/docs/design-system/components/command.md +75 -0
  37. package/docs/design-system/components/context-menu.md +59 -0
  38. package/docs/design-system/components/date-picker.md +97 -0
  39. package/docs/design-system/components/divider.md +117 -0
  40. package/docs/design-system/components/empty-state.md +72 -0
  41. package/docs/design-system/components/field.md +86 -0
  42. package/docs/design-system/components/file-upload.md +199 -0
  43. package/docs/design-system/components/hover-card.md +61 -0
  44. package/docs/design-system/components/icons.md +48 -0
  45. package/docs/design-system/components/input-group.md +73 -0
  46. package/docs/design-system/components/input-otp.md +73 -0
  47. package/docs/design-system/components/input.md +84 -0
  48. package/docs/design-system/components/item.md +72 -0
  49. package/docs/design-system/components/kbd.md +62 -0
  50. package/docs/design-system/components/label.md +64 -0
  51. package/docs/design-system/components/menu.md +76 -0
  52. package/docs/design-system/components/menubar.md +60 -0
  53. package/docs/design-system/components/modal.md +119 -0
  54. package/docs/design-system/components/native-select.md +70 -0
  55. package/docs/design-system/components/navigation-menu.md +63 -0
  56. package/docs/design-system/components/onboarding-step-list-item.md +94 -0
  57. package/docs/design-system/components/page-header.md +98 -0
  58. package/docs/design-system/components/pagination.md +71 -0
  59. package/docs/design-system/components/popover.md +77 -0
  60. package/docs/design-system/components/progress-slider.md +66 -0
  61. package/docs/design-system/components/progress.md +92 -0
  62. package/docs/design-system/components/radio-card.md +65 -0
  63. package/docs/design-system/components/radio-group.md +72 -0
  64. package/docs/design-system/components/resizable.md +57 -0
  65. package/docs/design-system/components/scroll-area.md +60 -0
  66. package/docs/design-system/components/segmented-control.md +71 -0
  67. package/docs/design-system/components/select.md +87 -0
  68. package/docs/design-system/components/sheet.md +82 -0
  69. package/docs/design-system/components/sidebar.md +83 -0
  70. package/docs/design-system/components/skeleton.md +91 -0
  71. package/docs/design-system/components/slideout.md +77 -0
  72. package/docs/design-system/components/slider.md +81 -0
  73. package/docs/design-system/components/social-button.md +65 -0
  74. package/docs/design-system/components/spinner.md +77 -0
  75. package/docs/design-system/components/steps.md +77 -0
  76. package/docs/design-system/components/table.md +411 -0
  77. package/docs/design-system/components/tabs.md +69 -0
  78. package/docs/design-system/components/tag.md +95 -0
  79. package/docs/design-system/components/textarea.md +64 -0
  80. package/docs/design-system/components/timeline.md +95 -0
  81. package/docs/design-system/components/toast.md +73 -0
  82. package/docs/design-system/components/toggle.md +96 -0
  83. package/docs/design-system/components/toolbar.md +105 -0
  84. package/docs/design-system/components/tooltip.md +107 -0
  85. package/docs/design-system/components/typography.md +18 -0
  86. package/docs/design-system/density-and-elevation.md +44 -0
  87. package/docs/design-system/design-system-test-missing-items.md +368 -0
  88. package/docs/design-system/direction-and-localization.md +23 -0
  89. package/docs/design-system/form-recipes.md +73 -0
  90. package/docs/design-system/gap-promotion-policy.md +21 -0
  91. package/docs/design-system/gap-promotion-report.md +166 -0
  92. package/docs/design-system/icons.md +69 -0
  93. package/docs/design-system/installation.md +68 -0
  94. package/docs/design-system/launch-comparison-shadcn.md +98 -0
  95. package/docs/design-system/registry-and-cli.md +64 -0
  96. package/docs/design-system/release-policy.md +30 -0
  97. package/docs/design-system/roadmap.md +14 -0
  98. package/docs/design-system/support-matrix.md +23 -0
  99. package/docs/design-system/tabs.md +53 -0
  100. package/docs/design-system/token-governance.md +60 -0
  101. package/package.json +97 -65
  102. package/registry/component-contract.json +40 -0
  103. package/registry/components/alert-dialog.tsx +297 -0
  104. package/registry/components/alert.tsx +86 -65
  105. package/registry/components/aspect-ratio.tsx +30 -0
  106. package/registry/components/autocomplete.tsx +23 -9
  107. package/registry/components/avatar.tsx +7 -7
  108. package/registry/components/badge.tsx +1 -1
  109. package/registry/components/button-group.tsx +321 -4
  110. package/registry/components/button.tsx +38 -17
  111. package/registry/components/card.tsx +68 -59
  112. package/registry/components/carousel.tsx +282 -0
  113. package/registry/components/chart.tsx +203 -0
  114. package/registry/components/checkbox.tsx +3 -3
  115. package/registry/components/collapsible.tsx +69 -0
  116. package/registry/components/command.tsx +174 -0
  117. package/registry/components/context-menu.tsx +236 -0
  118. package/registry/components/date-picker.tsx +246 -105
  119. package/registry/components/expanded/ActivityFeed.tsx +6 -15
  120. package/registry/components/expanded/Banner.tsx +3 -3
  121. package/registry/components/expanded/Breadcrumbs.tsx +5 -5
  122. package/registry/components/expanded/CatalogComponentsShowcase.tsx +13 -13
  123. package/registry/components/expanded/CatalogDivider.tsx +1 -1
  124. package/registry/components/expanded/EmptyState.tsx +1 -1
  125. package/registry/components/expanded/FileUpload.tsx +9 -2
  126. package/registry/components/expanded/OnboardingStepListItem.tsx +2 -4
  127. package/registry/components/expanded/PageHeader.tsx +14 -14
  128. package/registry/components/expanded/Slideout.tsx +15 -415
  129. package/registry/components/expanded/Steps.tsx +4 -4
  130. package/registry/components/expanded/Table.tsx +116 -69
  131. package/registry/components/expanded/Timeline.tsx +3 -3
  132. package/registry/components/expanded/activityFeed.css +1 -1
  133. package/registry/components/expanded/breadcrumbs.css +139 -139
  134. package/registry/components/expanded/catalogComponentsShowcase.css +83 -83
  135. package/registry/components/expanded/divider.css +1 -1
  136. package/registry/components/expanded/emptyState.css +1 -1
  137. package/registry/components/expanded/fileUpload.css +6 -6
  138. package/registry/components/expanded/slideout.css +9 -685
  139. package/registry/components/expanded/steps.css +274 -274
  140. package/registry/components/expanded/table.css +2 -2
  141. package/registry/components/expanded/timeline.css +264 -264
  142. package/registry/components/field.tsx +230 -0
  143. package/registry/components/hover-card.tsx +48 -0
  144. package/registry/components/input-content.ts +1 -0
  145. package/registry/components/input-group.tsx +130 -0
  146. package/registry/components/input-otp.tsx +13 -3
  147. package/registry/components/input.tsx +70 -45
  148. package/registry/components/item.tsx +189 -0
  149. package/registry/components/kbd.tsx +151 -0
  150. package/registry/components/label.tsx +78 -0
  151. package/registry/components/menu.tsx +34 -246
  152. package/registry/components/menubar.tsx +226 -0
  153. package/registry/components/modal.tsx +216 -143
  154. package/registry/components/native-select.tsx +205 -0
  155. package/registry/components/navigation-menu.tsx +171 -0
  156. package/registry/components/pagination.tsx +80 -58
  157. package/registry/components/popover.tsx +162 -664
  158. package/registry/components/radio-card.tsx +116 -87
  159. package/registry/components/radio-group.tsx +1 -1
  160. package/registry/components/resizable.tsx +74 -0
  161. package/registry/components/scroll-area.tsx +67 -0
  162. package/registry/components/segmented-control.tsx +87 -0
  163. package/registry/components/select-content.tsx +2 -2
  164. package/registry/components/select.tsx +54 -20
  165. package/registry/components/sheet.tsx +305 -0
  166. package/registry/components/sidebar.tsx +352 -0
  167. package/registry/components/skeleton.css +0 -3
  168. package/registry/components/social-button.tsx +116 -30
  169. package/registry/components/spinner.tsx +18 -55
  170. package/registry/components/{expanded/tabs.css → tabs.css} +127 -106
  171. package/registry/components/tabs.tsx +246 -0
  172. package/registry/components/tag.tsx +2 -2
  173. package/registry/components/textarea.tsx +4 -4
  174. package/registry/components/toast.tsx +136 -0
  175. package/registry/components/toggle.tsx +2 -2
  176. package/registry/components/toolbar.tsx +89 -17
  177. package/registry/components/tooltip.tsx +1 -1
  178. package/registry/examples/alert-demo.tsx +25 -5
  179. package/registry/examples/alert-dialog-demo.tsx +42 -0
  180. package/registry/examples/aspect-ratio-demo.tsx +11 -0
  181. package/registry/examples/attribute-demo.tsx +1 -1
  182. package/registry/examples/autocomplete-demo.tsx +3 -3
  183. package/registry/examples/avatar-demo.tsx +7 -7
  184. package/registry/examples/badge-demo.tsx +1 -1
  185. package/registry/examples/button-demo.tsx +6 -6
  186. package/registry/examples/button-group-demo.tsx +23 -2
  187. package/registry/examples/card-demo.tsx +46 -8
  188. package/registry/examples/carousel-demo.tsx +43 -0
  189. package/registry/examples/chart-demo.tsx +33 -0
  190. package/registry/examples/checkbox-demo.tsx +5 -5
  191. package/registry/examples/collapsible-demo.tsx +16 -0
  192. package/registry/examples/collection-filter-bar-demo.tsx +135 -0
  193. package/registry/examples/command-demo.tsx +42 -0
  194. package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
  195. package/registry/examples/context-menu-demo.tsx +29 -0
  196. package/registry/examples/control-density-contract-demo.tsx +61 -0
  197. package/registry/examples/date-picker-demo.tsx +12 -12
  198. package/registry/examples/divider-demo.tsx +3 -3
  199. package/registry/examples/expanded/banner-demo.tsx +1 -1
  200. package/registry/examples/expanded/breadcrumbs-demo.tsx +1 -1
  201. package/registry/examples/expanded/empty-state-demo.tsx +1 -1
  202. package/registry/examples/expanded/file-upload-demo.tsx +1 -1
  203. package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
  204. package/registry/examples/expanded/slideout-demo.tsx +2 -2
  205. package/registry/examples/expanded/steps-demo.tsx +1 -1
  206. package/registry/examples/expanded/tabs-demo.tsx +2 -2
  207. package/registry/examples/expanded/timeline-demo.tsx +1 -1
  208. package/registry/examples/field-demo.tsx +51 -0
  209. package/registry/examples/hover-card-demo.tsx +23 -0
  210. package/registry/examples/input-demo.tsx +6 -6
  211. package/registry/examples/input-group-demo.tsx +16 -0
  212. package/registry/examples/input-otp-demo.tsx +4 -4
  213. package/registry/examples/item-demo.tsx +46 -0
  214. package/registry/examples/kbd-demo.tsx +11 -0
  215. package/registry/examples/label-demo.tsx +20 -0
  216. package/registry/examples/menu-demo.tsx +1 -1
  217. package/registry/examples/menubar-demo.tsx +34 -0
  218. package/registry/examples/modal-demo.tsx +67 -9
  219. package/registry/examples/native-select-demo.tsx +16 -0
  220. package/registry/examples/navigation-menu-demo.tsx +29 -0
  221. package/registry/examples/pagination-demo.tsx +6 -3
  222. package/registry/examples/popover-demo.tsx +19 -19
  223. package/registry/examples/progress-demo.tsx +1 -1
  224. package/registry/examples/progress-slider-demo.tsx +1 -1
  225. package/registry/examples/radio-card-demo.tsx +4 -4
  226. package/registry/examples/radio-group-demo.tsx +2 -2
  227. package/registry/examples/resizable-demo.tsx +22 -0
  228. package/registry/examples/results-toolbar-demo.tsx +44 -0
  229. package/registry/examples/scroll-area-demo.tsx +15 -0
  230. package/registry/examples/segmented-control-demo.tsx +39 -0
  231. package/registry/examples/select-demo.tsx +1 -1
  232. package/registry/examples/sheet-demo.tsx +47 -0
  233. package/registry/examples/sidebar-demo.tsx +55 -0
  234. package/registry/examples/slider-demo.tsx +3 -3
  235. package/registry/examples/social-button-demo.tsx +18 -8
  236. package/registry/examples/spinner-demo.tsx +13 -13
  237. package/registry/examples/tabs-demo.tsx +13 -0
  238. package/registry/examples/tag-demo.tsx +1 -1
  239. package/registry/examples/textarea-demo.tsx +4 -4
  240. package/registry/examples/toast-demo.tsx +35 -0
  241. package/registry/examples/toggle-demo.tsx +6 -6
  242. package/registry/examples/tooltip-demo.tsx +4 -4
  243. package/registry/guidance/USAGE.md +27 -0
  244. package/registry/hooks/use-direction.ts +193 -13
  245. package/registry/index.json +6010 -432
  246. package/registry/styles/globals.css +3112 -4690
  247. package/registry.json +1582 -0
  248. package/schema/config.schema.json +51 -0
  249. package/schema/registry.schema.json +190 -0
  250. package/schema/tokens.schema.json +163 -0
  251. package/src/cli/command-line.js +194 -0
  252. package/src/cli/config-editors.js +9 -0
  253. package/src/cli/doctor.js +399 -0
  254. package/src/cli/file-transaction.js +295 -0
  255. package/src/cli/global-css.js +153 -0
  256. package/src/cli/index.js +639 -1391
  257. package/src/cli/install-plan.js +592 -0
  258. package/src/cli/install-receipt.js +171 -0
  259. package/src/cli/jsonc.js +203 -0
  260. package/src/cli/package-install-state.js +254 -0
  261. package/src/cli/package-manager.js +238 -0
  262. package/src/cli/package-plan.js +185 -0
  263. package/src/cli/package-version.js +97 -0
  264. package/src/cli/registry-query.js +207 -0
  265. package/src/cli/reporting.js +55 -0
  266. package/src/cli/schema-validation.js +19 -0
  267. package/src/cli/source-scanner.js +686 -0
  268. package/src/cli/vite-config.js +478 -0
  269. package/tokens/banhaten.tokens.json +4313 -310
  270. package/registry/assets/avatars/avatar-02.jpg +0 -0
  271. package/registry/assets/avatars/avatar-03.jpg +0 -0
  272. package/registry/assets/avatars/avatar-04.jpg +0 -0
  273. package/registry/assets/avatars/avatar-05.jpg +0 -0
  274. package/registry/assets/avatars/avatar-06.jpg +0 -0
  275. package/registry/assets/avatars/avatar-07.jpg +0 -0
  276. package/registry/assets/avatars/avatar-08.jpg +0 -0
  277. package/registry/assets/avatars/avatar-09.jpg +0 -0
  278. package/registry/assets/avatars/avatar-10.jpg +0 -0
  279. package/registry/assets/avatars/avatar-11.jpg +0 -0
  280. package/registry/assets/avatars/avatar-12.jpg +0 -0
  281. package/registry/assets/avatars/avatar-13.jpg +0 -0
  282. package/registry/assets/avatars/avatar-14.jpg +0 -0
  283. package/registry/assets/avatars/avatar-15.jpg +0 -0
  284. package/registry/assets/avatars/avatar-16.jpg +0 -0
  285. package/registry/assets/avatars/avatar-17.jpg +0 -0
  286. package/registry/assets/avatars/avatar-18.jpg +0 -0
  287. package/registry/assets/avatars/avatar-19.jpg +0 -0
  288. package/registry/assets/avatars/avatar-20.jpg +0 -0
  289. package/registry/assets/avatars/avatar-21.jpg +0 -0
  290. package/registry/assets/avatars/avatar-22.jpg +0 -0
  291. package/registry/assets/avatars/avatar-23.jpg +0 -0
  292. package/registry/assets/avatars/avatar-24.jpg +0 -0
  293. package/registry/assets/avatars/avatar-25.jpg +0 -0
  294. package/registry/assets/avatars/avatar-26.jpg +0 -0
  295. package/registry/assets/avatars/avatar-27.jpg +0 -0
  296. package/registry/assets/avatars/avatar-28.jpg +0 -0
  297. package/registry/assets/avatars/avatar-29.jpg +0 -0
  298. package/registry/assets/avatars/avatar-30.jpg +0 -0
  299. package/registry/assets/avatars/avatar-31.jpg +0 -0
  300. package/registry/assets/avatars/avatar-32.jpg +0 -0
  301. package/registry/assets/avatars/avatar-33.jpg +0 -0
  302. package/registry/assets/avatars/avatar-34.jpg +0 -0
  303. package/registry/assets/avatars/avatar-35.jpg +0 -0
  304. package/registry/assets/image-assets.json +0 -744
  305. package/registry/assets/images/art-02.jpg +0 -0
  306. package/registry/assets/images/art-03.jpg +0 -0
  307. package/registry/assets/images/art-04.jpg +0 -0
  308. package/registry/assets/images/art-05.jpg +0 -0
  309. package/registry/assets/images/art-06.jpg +0 -0
  310. package/registry/assets/images/art-07.jpg +0 -0
  311. package/registry/assets/images/art-08.jpg +0 -0
  312. package/registry/assets/images/art-09.jpg +0 -0
  313. package/registry/assets/images/art-10.jpg +0 -0
  314. package/registry/assets/images/art-11.jpg +0 -0
  315. package/registry/assets/images/art-12.jpg +0 -0
  316. package/registry/assets/images/art-13.jpg +0 -0
  317. package/registry/assets/images/art-14.jpg +0 -0
  318. package/registry/assets/images/art-15.jpg +0 -0
  319. package/registry/assets/images/art-16.jpg +0 -0
  320. package/registry/assets/images/art-17.jpg +0 -0
  321. package/registry/assets/images/art-18.jpg +0 -0
  322. package/registry/assets/images/art-19.jpg +0 -0
  323. package/registry/assets/images/art-20.jpg +0 -0
  324. package/registry/assets/images/art-21.jpg +0 -0
  325. package/registry/assets/images/art-22.jpg +0 -0
  326. package/registry/assets/images/art-23.jpg +0 -0
  327. package/registry/assets/images/art-24.jpg +0 -0
  328. package/registry/assets/images/art-25.jpg +0 -0
  329. package/registry/assets/images/art-26.jpg +0 -0
  330. package/registry/assets/images/art-27.jpg +0 -0
  331. package/registry/assets/images/nature-01.jpg +0 -0
  332. package/registry/assets/images/nature-02.jpg +0 -0
  333. package/registry/assets/images/nature-03.jpg +0 -0
  334. package/registry/assets/images/nature-04.jpg +0 -0
  335. package/registry/assets/images/nature-05.jpg +0 -0
  336. package/registry/assets/images/nature-06.jpg +0 -0
  337. package/registry/assets/images/nature-07.jpg +0 -0
  338. package/registry/assets/images/nature-08.jpg +0 -0
  339. package/registry/assets/images/nature-09.jpg +0 -0
  340. package/registry/assets/images/nature-10.jpg +0 -0
  341. package/registry/assets/images/nature-11.jpg +0 -0
  342. package/registry/assets/images/nature-12.jpg +0 -0
  343. package/registry/assets/images/nature-13.jpg +0 -0
  344. package/registry/assets/images/nature-14.jpg +0 -0
  345. package/registry/assets/images/nature-15.jpg +0 -0
  346. package/registry/assets/images/nature-16.jpg +0 -0
  347. package/registry/assets/images/nature-17.jpg +0 -0
  348. package/registry/assets/images/nature-18.jpg +0 -0
  349. package/registry/assets/images/nature-19.jpg +0 -0
  350. package/registry/assets/images/nature-20.jpg +0 -0
  351. package/registry/components/expanded/BannerBoard.tsx +0 -225
  352. package/registry/components/expanded/CatalogTag.tsx +0 -85
  353. package/registry/components/expanded/CommandBar.tsx +0 -386
  354. package/registry/components/expanded/IconExplorer.tsx +0 -612
  355. package/registry/components/expanded/Tabs.tsx +0 -86
  356. package/registry/components/expanded/TimelineShowcase.tsx +0 -158
  357. package/registry/components/expanded/commandBar.css +0 -470
  358. package/registry/components/expanded/iconExplorer.css +0 -764
  359. package/registry/components/expanded/iconPacks.ts +0 -866
  360. package/registry/components/expanded/tag.css +0 -148
  361. package/registry/examples/expanded/icons-demo.tsx +0 -5
  362. package/tokens/Color mode.zip +0 -0
  363. package/tokens/Numbers.zip +0 -0
  364. package/tokens/Radius.zip +0 -0
  365. package/tokens/Theme.zip +0 -0
@@ -1,48 +1,32 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
3
5
  import { XIcon } from "lucide-react"
4
6
  import { cva, type VariantProps } from "class-variance-authority"
5
7
 
6
8
  import { cn } from "@/lib/utils"
9
+ import { useResolvedDirection, type Direction } from "@/hooks/use-direction"
7
10
 
8
11
  type PopoverSide = "top" | "right" | "bottom" | "left"
9
12
  type PopoverAlign = "start" | "center" | "end"
10
13
  type PopoverSize = "sm" | "md" | "lg" | "auto"
11
14
  type PopoverOffset = "tight" | "default" | "loose"
12
- type PopoverArrowSide = "top" | "right" | "bottom" | "left"
13
- type PopoverCollisionShift = {
14
- x: number
15
- y: number
16
- }
17
- type PopoverResizeObserverConstructor = new (
18
- callback: () => void
19
- ) => {
20
- disconnect: () => void
21
- observe: (target: Element) => void
22
- }
23
-
24
- type PopoverContextValue = {
25
- contentId: string
26
- modal: boolean
27
- open: boolean
28
- setOpen: (open: boolean) => void
29
- triggerId: string
30
- }
31
15
 
32
- const PopoverContext = React.createContext<PopoverContextValue | null>(null)
33
- const zeroPopoverCollisionShift: PopoverCollisionShift = { x: 0, y: 0 }
34
- // Viewport collision padding mirrors --bh-space-md-8.
35
16
  const POPOVER_COLLISION_PADDING_PX = 8
36
- const useSafeLayoutEffect =
37
- typeof window === "undefined" ? React.useEffect : React.useLayoutEffect
17
+ const popoverOffsetPixels: Record<PopoverOffset, number> = {
18
+ tight: 4,
19
+ default: 8,
20
+ loose: 16,
21
+ }
38
22
 
39
23
  const popoverContentVariants = cva(
40
24
  [
41
- "absolute z-[var(--bh-z-popover)] grid max-w-[var(--bh-popover-max-width)] gap-[var(--bh-popover-content-gap)]",
25
+ "z-[var(--bh-z-popover)] grid max-w-[var(--bh-popover-max-width)] gap-[var(--bh-popover-content-gap)]",
42
26
  "rounded-[var(--bh-popover-radius)] bg-[var(--bh-popover-bg)] p-[var(--bh-popover-padding)]",
43
27
  "text-start text-[var(--bh-content-default)] shadow-[var(--shadow-popover)] outline-none",
44
28
  "font-[var(--bh-font-family)] tracking-[var(--bh-text-base-letter-spacing)]",
45
- "data-[state=closed]:pointer-events-none data-[state=closed]:opacity-[var(--bh-opacity-0)]",
29
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
46
30
  ],
47
31
  {
48
32
  variants: {
@@ -73,16 +57,14 @@ type PopoverProps = React.ComponentProps<"div"> & {
73
57
  openOnHover?: boolean
74
58
  }
75
59
 
76
- type PopoverAnchorProps = React.ComponentProps<"span"> & {
77
- asChild?: boolean
78
- }
79
-
80
- type PopoverTriggerProps = React.ComponentProps<"button"> & {
81
- asChild?: boolean
60
+ type PopoverAnchorProps = React.ComponentProps<typeof PopoverPrimitive.Anchor>
61
+ type PopoverTriggerProps = React.ComponentProps<typeof PopoverPrimitive.Trigger> & {
82
62
  openOnFocus?: boolean
83
63
  }
84
-
85
- type PopoverContentProps = React.ComponentProps<"div"> &
64
+ type PopoverContentProps = Omit<
65
+ React.ComponentProps<typeof PopoverPrimitive.Content>,
66
+ "align" | "collisionPadding" | "forceMount" | "side" | "sideOffset"
67
+ > &
86
68
  VariantProps<typeof popoverContentVariants> & {
87
69
  align?: PopoverAlign
88
70
  avoidCollisions?: boolean
@@ -91,7 +73,6 @@ type PopoverContentProps = React.ComponentProps<"div"> &
91
73
  showArrow?: boolean
92
74
  side?: PopoverSide
93
75
  }
94
-
95
76
  type PopoverHeaderProps = React.ComponentProps<"div">
96
77
  type PopoverBodyProps = React.ComponentProps<"div">
97
78
  type PopoverTitleProps = React.ComponentProps<"h3">
@@ -102,11 +83,19 @@ type PopoverCloseProps = React.ComponentProps<"button"> & {
102
83
  label?: string
103
84
  }
104
85
 
86
+ type PopoverOpenContextValue = {
87
+ direction: Direction
88
+ setOpen: (open: boolean) => void
89
+ }
90
+
91
+ const PopoverOpenContext = React.createContext<PopoverOpenContextValue | null>(null)
92
+
105
93
  const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(function Popover(
106
94
  {
107
95
  children,
108
96
  className,
109
97
  defaultOpen = false,
98
+ dir,
110
99
  modal = false,
111
100
  onOpenChange,
112
101
  onPointerEnter,
@@ -118,694 +107,203 @@ const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(function Popover(
118
107
  ref
119
108
  ) {
120
109
  const rootRef = React.useRef<HTMLDivElement | null>(null)
121
- const contentId = React.useId()
122
- const triggerId = React.useId()
123
- const [isOpen, setIsOpen] = useControllablePopoverState({
124
- defaultOpen,
125
- onOpenChange,
126
- open,
127
- })
128
-
129
- React.useEffect(() => {
130
- if (!isOpen) return
131
-
132
- const ownerDocument = rootRef.current?.ownerDocument ?? document
133
-
134
- function handlePointerDown(event: PointerEvent) {
135
- const root = rootRef.current
136
- if (!root || root.contains(event.target as Node)) return
137
- setIsOpen(false)
138
- }
139
-
140
- function handleKeyDown(event: KeyboardEvent) {
141
- if (event.key === "Escape") {
142
- setIsOpen(false)
143
- }
144
- }
145
-
146
- ownerDocument.addEventListener("pointerdown", handlePointerDown)
147
- ownerDocument.addEventListener("keydown", handleKeyDown)
110
+ const explicitDirection = dir === "ltr" || dir === "rtl" ? dir : undefined
111
+ const direction = useResolvedDirection(explicitDirection, rootRef)
112
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
113
+ const isControlled = open !== undefined
114
+ const resolvedOpen = isControlled ? open : uncontrolledOpen
148
115
 
149
- return () => {
150
- ownerDocument.removeEventListener("pointerdown", handlePointerDown)
151
- ownerDocument.removeEventListener("keydown", handleKeyDown)
152
- }
153
- }, [isOpen, setIsOpen])
116
+ const setOpen = React.useCallback(
117
+ (nextOpen: boolean) => {
118
+ if (!isControlled) setUncontrolledOpen(nextOpen)
119
+ onOpenChange?.(nextOpen)
120
+ },
121
+ [isControlled, onOpenChange]
122
+ )
154
123
 
155
124
  function setRootRef(node: HTMLDivElement | null) {
156
125
  rootRef.current = node
157
-
158
- if (typeof ref === "function") {
159
- ref(node)
160
- } else if (ref) {
161
- ref.current = node
162
- }
126
+ if (typeof ref === "function") ref(node)
127
+ else if (ref) ref.current = node
163
128
  }
164
129
 
165
130
  return (
166
- <PopoverContext.Provider
167
- value={{
168
- contentId,
169
- modal,
170
- open: isOpen,
171
- setOpen: setIsOpen,
172
- triggerId,
173
- }}
131
+ <PopoverPrimitive.Root
132
+ modal={modal}
133
+ open={resolvedOpen}
134
+ onOpenChange={setOpen}
174
135
  >
175
- <div
176
- data-slot="popover"
177
- data-state={isOpen ? "open" : "closed"}
178
- ref={setRootRef}
179
- className={cn("relative inline-flex max-w-full", className)}
180
- onPointerEnter={(event) => {
181
- onPointerEnter?.(event)
182
- if (openOnHover && !event.defaultPrevented) {
183
- setIsOpen(true)
184
- }
185
- }}
186
- onPointerLeave={(event) => {
187
- onPointerLeave?.(event)
188
- if (openOnHover && !event.defaultPrevented) {
189
- setIsOpen(false)
190
- }
191
- }}
192
- {...props}
193
- >
194
- {children}
195
- </div>
196
- </PopoverContext.Provider>
136
+ <PopoverOpenContext.Provider value={{ direction, setOpen }}>
137
+ <div
138
+ className={cn("relative inline-flex max-w-full", className)}
139
+ data-slot="popover"
140
+ data-state={resolvedOpen ? "open" : "closed"}
141
+ dir={dir}
142
+ ref={setRootRef}
143
+ onPointerEnter={(event) => {
144
+ onPointerEnter?.(event)
145
+ if (openOnHover && !event.defaultPrevented) setOpen(true)
146
+ }}
147
+ onPointerLeave={(event) => {
148
+ onPointerLeave?.(event)
149
+ if (openOnHover && !event.defaultPrevented) setOpen(false)
150
+ }}
151
+ {...props}
152
+ >
153
+ {children}
154
+ </div>
155
+ </PopoverOpenContext.Provider>
156
+ </PopoverPrimitive.Root>
197
157
  )
198
158
  })
199
159
 
200
- function PopoverAnchor({
201
- asChild = false,
202
- className,
203
- ...props
204
- }: PopoverAnchorProps) {
205
- const Comp = asChild ? Slot : "span"
206
-
160
+ const PopoverAnchor = React.forwardRef<
161
+ React.ElementRef<typeof PopoverPrimitive.Anchor>,
162
+ PopoverAnchorProps
163
+ >(function PopoverAnchor({ className, ...props }, ref) {
207
164
  return (
208
- <Comp
209
- data-slot="popover-anchor"
165
+ <PopoverPrimitive.Anchor
210
166
  className={cn("relative inline-flex max-w-full", className)}
167
+ data-slot="popover-anchor"
168
+ ref={ref}
211
169
  {...props}
212
170
  />
213
171
  )
214
- }
215
-
216
- const PopoverTrigger = React.forwardRef<HTMLButtonElement, PopoverTriggerProps>(
217
- function PopoverTrigger(
218
- {
219
- asChild = false,
220
- className,
221
- disabled,
222
- onClick,
223
- onFocus,
224
- onKeyDown,
225
- openOnFocus = false,
226
- type,
227
- ...props
228
- },
229
- ref
230
- ) {
231
- const context = usePopoverContext("PopoverTrigger")
232
- const Comp = asChild ? Slot : "button"
233
- const isDisabled = Boolean(disabled)
234
-
235
- return (
236
- <Comp
237
- aria-controls={context.open ? context.contentId : undefined}
238
- aria-expanded={context.open}
239
- aria-haspopup="dialog"
240
- data-slot="popover-trigger"
241
- data-state={context.open ? "open" : "closed"}
242
- disabled={isDisabled}
243
- id={context.triggerId}
244
- ref={ref}
245
- type={asChild ? undefined : type || "button"}
246
- className={cn(
247
- "inline-flex min-w-0 items-center justify-center gap-[var(--bh-popover-trigger-gap)]",
248
- "rounded-[var(--bh-control-default)] text-start outline-none transition-[background-color,border-color,box-shadow]",
249
- "focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
250
- className
251
- )}
252
- onClick={(event) => {
253
- onClick?.(event)
254
- if (event.defaultPrevented || isDisabled) return
255
- context.setOpen(!context.open)
256
- }}
257
- onFocus={(event) => {
258
- onFocus?.(event)
259
- if (openOnFocus && !event.defaultPrevented && !isDisabled) {
260
- context.setOpen(true)
261
- }
262
- }}
263
- onKeyDown={(event) => {
264
- onKeyDown?.(event)
265
- if (event.defaultPrevented || isDisabled) return
266
-
267
- if (event.key === "ArrowDown") {
268
- event.preventDefault()
269
- context.setOpen(true)
270
- }
271
- }}
272
- {...props}
273
- />
274
- )
275
- }
276
- )
277
-
278
- const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
279
- function PopoverContent(
280
- {
281
- align = "start",
282
- avoidCollisions = true,
283
- children,
284
- className,
285
- collisionPadding = POPOVER_COLLISION_PADDING_PX,
286
- forceMount = false,
287
- offset,
288
- role = "dialog",
289
- showArrow = true,
290
- side = "bottom",
291
- size,
292
- style,
293
- ...props
294
- },
295
- ref
296
- ) {
297
- const context = usePopoverContext("PopoverContent")
298
- const isOpen = context.open
299
- const contentRef = React.useRef<HTMLDivElement | null>(null)
300
- const collisionShiftRef = React.useRef(zeroPopoverCollisionShift)
301
- const [collisionShift, setCollisionShift] = React.useState(
302
- zeroPopoverCollisionShift
303
- )
304
-
305
- function updateCollisionShift(nextShift: PopoverCollisionShift) {
306
- collisionShiftRef.current = nextShift
307
- setCollisionShift((currentShift) =>
308
- isSamePopoverCollisionShift(currentShift, nextShift)
309
- ? currentShift
310
- : nextShift
311
- )
312
- }
313
-
314
- function setContentRef(node: HTMLDivElement | null) {
315
- contentRef.current = node
316
-
317
- if (typeof ref === "function") {
318
- ref(node)
319
- } else if (ref) {
320
- ref.current = node
321
- }
322
- }
323
-
324
- useSafeLayoutEffect(() => {
325
- if (!isOpen || !avoidCollisions) {
326
- updateCollisionShift(zeroPopoverCollisionShift)
327
- return
328
- }
329
-
330
- const node = contentRef.current
331
- if (!node) return
332
- const contentNode: HTMLDivElement = node
333
-
334
- const ownerWindow = contentNode.ownerDocument.defaultView
335
- if (!ownerWindow) return
336
- const contentWindow: Window = ownerWindow
337
-
338
- let animationFrame = 0
339
- const resolvedCollisionPadding = Math.max(0, collisionPadding)
340
- const requestFrame =
341
- typeof contentWindow.requestAnimationFrame === "function"
342
- ? contentWindow.requestAnimationFrame.bind(contentWindow)
343
- : (callback: FrameRequestCallback) =>
344
- contentWindow.setTimeout(
345
- () => callback(contentWindow.performance.now()),
346
- 0
347
- )
348
- const cancelFrame =
349
- typeof contentWindow.cancelAnimationFrame === "function"
350
- ? contentWindow.cancelAnimationFrame.bind(contentWindow)
351
- : contentWindow.clearTimeout.bind(contentWindow)
352
-
353
- function measureCollisionShift() {
354
- animationFrame = 0
355
-
356
- const rect = contentNode.getBoundingClientRect()
357
- const currentShift = collisionShiftRef.current
358
- const unshiftedRect = {
359
- bottom: rect.bottom - currentShift.y,
360
- left: rect.left - currentShift.x,
361
- right: rect.right - currentShift.x,
362
- top: rect.top - currentShift.y,
363
- }
364
-
365
- updateCollisionShift(
366
- getPopoverCollisionShift({
367
- collisionPadding: resolvedCollisionPadding,
368
- rect: unshiftedRect,
369
- viewportHeight: contentWindow.innerHeight,
370
- viewportWidth: contentWindow.innerWidth,
371
- })
372
- )
373
- }
374
-
375
- function queueCollisionShiftMeasure() {
376
- if (animationFrame) {
377
- cancelFrame(animationFrame)
378
- }
379
-
380
- animationFrame = requestFrame(measureCollisionShift)
381
- }
172
+ })
382
173
 
383
- queueCollisionShiftMeasure()
384
- contentWindow.addEventListener("resize", queueCollisionShiftMeasure)
385
- contentWindow.addEventListener("scroll", queueCollisionShiftMeasure, true)
174
+ const PopoverTrigger = React.forwardRef<
175
+ React.ElementRef<typeof PopoverPrimitive.Trigger>,
176
+ PopoverTriggerProps
177
+ >(function PopoverTrigger(
178
+ { className, onFocus, onKeyDown, openOnFocus = false, ...props },
179
+ ref
180
+ ) {
181
+ const context = React.useContext(PopoverOpenContext)
386
182
 
387
- const ResizeObserver = (
388
- contentWindow as Window & {
389
- ResizeObserver?: PopoverResizeObserverConstructor
183
+ return (
184
+ <PopoverPrimitive.Trigger
185
+ className={cn(
186
+ "inline-flex min-w-0 items-center justify-center gap-[var(--bh-popover-trigger-gap)]",
187
+ "rounded-[var(--bh-control-default)] text-start outline-none transition-[background-color,border-color,box-shadow]",
188
+ "focus-visible:shadow-[var(--shadow-button-focus)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
189
+ className
190
+ )}
191
+ data-slot="popover-trigger"
192
+ ref={ref}
193
+ onFocus={(event) => {
194
+ onFocus?.(event)
195
+ if (openOnFocus && !event.defaultPrevented && !props.disabled) {
196
+ context?.setOpen(true)
390
197
  }
391
- ).ResizeObserver
392
- const resizeObserver = ResizeObserver
393
- ? new ResizeObserver(queueCollisionShiftMeasure)
394
- : undefined
395
- resizeObserver?.observe(contentNode)
396
-
397
- return () => {
398
- if (animationFrame) {
399
- cancelFrame(animationFrame)
198
+ }}
199
+ onKeyDown={(event) => {
200
+ onKeyDown?.(event)
201
+ if (event.key === "ArrowDown" && !event.defaultPrevented && !props.disabled) {
202
+ event.preventDefault()
203
+ context?.setOpen(true)
400
204
  }
205
+ }}
206
+ {...props}
207
+ />
208
+ )
209
+ })
401
210
 
402
- contentWindow.removeEventListener("resize", queueCollisionShiftMeasure)
403
- contentWindow.removeEventListener("scroll", queueCollisionShiftMeasure, true)
404
- resizeObserver?.disconnect()
405
- }
406
- }, [align, avoidCollisions, collisionPadding, isOpen, offset, side, size])
407
-
408
- if (!forceMount && !isOpen) {
409
- return null
410
- }
211
+ const PopoverContent = React.forwardRef<
212
+ React.ElementRef<typeof PopoverPrimitive.Content>,
213
+ PopoverContentProps
214
+ >(function PopoverContent(
215
+ {
216
+ align = "start",
217
+ avoidCollisions = true,
218
+ children,
219
+ className,
220
+ collisionPadding = POPOVER_COLLISION_PADDING_PX,
221
+ forceMount,
222
+ offset = "default",
223
+ role = "dialog",
224
+ showArrow = true,
225
+ side = "bottom",
226
+ size,
227
+ ...props
228
+ },
229
+ ref
230
+ ) {
231
+ const context = React.useContext(PopoverOpenContext)
411
232
 
412
- return (
413
- <div
414
- aria-modal={context.modal || undefined}
415
- data-align={align}
416
- data-side={side}
233
+ return (
234
+ <PopoverPrimitive.Portal forceMount={forceMount ? true : undefined}>
235
+ <PopoverPrimitive.Content
236
+ align={align}
237
+ avoidCollisions={avoidCollisions}
238
+ collisionPadding={collisionPadding}
417
239
  data-slot="popover-content"
418
- data-state={isOpen ? "open" : "closed"}
419
- hidden={!isOpen}
420
- id={context.contentId}
421
- ref={setContentRef}
240
+ dir={props.dir ?? context?.direction}
241
+ forceMount={forceMount ? true : undefined}
242
+ ref={ref}
422
243
  role={role}
423
- style={{
424
- ...style,
425
- transform: mergePopoverTransforms(
426
- getPopoverContentTransform(side, align, collisionShift),
427
- style?.transform
428
- ),
429
- }}
430
- className={cn(
431
- popoverContentVariants({ offset, size }),
432
- popoverPositionClasses[side][align],
433
- className
434
- )}
244
+ side={side}
245
+ sideOffset={popoverOffsetPixels[offset ?? "default"]}
246
+ className={cn(popoverContentVariants({ offset, size }), className)}
435
247
  {...props}
436
248
  >
437
- {showArrow ? <PopoverArrow align={align} side={side} /> : null}
249
+ {showArrow ? (
250
+ <PopoverPrimitive.Arrow
251
+ className="h-[var(--bh-popover-arrow-depth)] w-[var(--bh-popover-arrow-width)] fill-[var(--bh-popover-bg)] stroke-[var(--bh-popover-border)]"
252
+ data-slot="popover-arrow"
253
+ />
254
+ ) : null}
438
255
  {children}
439
- </div>
440
- )
441
- }
442
- )
256
+ </PopoverPrimitive.Content>
257
+ </PopoverPrimitive.Portal>
258
+ )
259
+ })
443
260
 
444
261
  function PopoverHeader({ className, ...props }: PopoverHeaderProps) {
445
- return (
446
- <div
447
- data-slot="popover-header"
448
- className={cn(
449
- "flex min-w-0 items-start justify-between gap-[var(--bh-popover-header-gap)]",
450
- className
451
- )}
452
- {...props}
453
- />
454
- )
262
+ return <div data-slot="popover-header" className={cn("flex min-w-0 items-start justify-between gap-[var(--bh-popover-header-gap)]", className)} {...props} />
455
263
  }
456
264
 
457
265
  function PopoverBody({ className, ...props }: PopoverBodyProps) {
458
- return (
459
- <div
460
- data-slot="popover-body"
461
- className={cn("grid min-w-0 gap-[var(--bh-popover-body-gap)]", className)}
462
- {...props}
463
- />
464
- )
266
+ return <div data-slot="popover-body" className={cn("grid min-w-0 gap-[var(--bh-popover-body-gap)]", className)} {...props} />
465
267
  }
466
268
 
467
269
  function PopoverTitle({ className, dir = "auto", ...props }: PopoverTitleProps) {
468
- return (
469
- <h3
470
- data-slot="popover-title"
471
- dir={dir}
472
- className={cn(
473
- "m-0 min-w-0 text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)]",
474
- className
475
- )}
476
- {...props}
477
- />
478
- )
270
+ return <h3 data-slot="popover-title" dir={dir} className={cn("m-0 min-w-0 text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)]", className)} {...props} />
479
271
  }
480
272
 
481
- function PopoverDescription({
482
- className,
483
- dir = "auto",
484
- ...props
485
- }: PopoverDescriptionProps) {
486
- return (
487
- <p
488
- data-slot="popover-description"
489
- dir={dir}
490
- className={cn(
491
- "m-0 min-w-0 text-start text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]",
492
- className
493
- )}
494
- {...props}
495
- />
496
- )
273
+ function PopoverDescription({ className, dir = "auto", ...props }: PopoverDescriptionProps) {
274
+ return <p data-slot="popover-description" dir={dir} className={cn("m-0 min-w-0 text-start text-[length:var(--bh-text-body-xs-regular-font-size)] font-[var(--bh-text-body-xs-regular-font-weight)] leading-[var(--bh-text-body-xs-regular-line-height)] tracking-[var(--bh-text-body-xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]", className)} {...props} />
497
275
  }
498
276
 
499
277
  function PopoverFooter({ className, ...props }: PopoverFooterProps) {
500
- return (
501
- <div
502
- data-slot="popover-footer"
503
- className={cn(
504
- "flex min-w-0 flex-wrap items-center justify-end gap-[var(--bh-popover-footer-gap)]",
505
- className
506
- )}
507
- {...props}
508
- />
509
- )
278
+ return <div data-slot="popover-footer" className={cn("flex min-w-0 flex-wrap items-center justify-end gap-[var(--bh-popover-footer-gap)]", className)} {...props} />
510
279
  }
511
280
 
512
281
  const PopoverClose = React.forwardRef<HTMLButtonElement, PopoverCloseProps>(
513
282
  function PopoverClose(
514
- {
515
- asChild = false,
516
- children,
517
- className,
518
- label = "Close popover",
519
- onClick,
520
- type,
521
- ...props
522
- },
283
+ { asChild = false, children, className, label = "Close popover", type, ...props },
523
284
  ref
524
285
  ) {
525
- const context = usePopoverContext("PopoverClose")
526
- const Comp = asChild ? Slot : "button"
527
-
528
286
  return (
529
- <Comp
287
+ <PopoverPrimitive.Close
530
288
  aria-label={label}
531
- data-slot="popover-close"
532
- ref={ref}
533
- type={asChild ? undefined : type || "button"}
289
+ asChild={asChild}
534
290
  className={cn(
535
291
  "inline-flex size-[var(--bh-popover-close-size)] shrink-0 items-center justify-center rounded-[var(--bh-radius-full)]",
536
292
  "border-0 bg-transparent p-0 text-[var(--bh-content-subtle)] outline-none transition-[background-color,box-shadow,color]",
537
293
  "hover:bg-[var(--bh-interactive-ghost-hover)] hover:text-[var(--bh-content-default)] focus-visible:shadow-[var(--shadow-button-focus)]",
538
294
  className
539
295
  )}
540
- onClick={(event) => {
541
- onClick?.(event)
542
- if (!event.defaultPrevented) {
543
- context.setOpen(false)
544
- }
545
- }}
296
+ data-slot="popover-close"
297
+ ref={ref}
298
+ type={asChild ? undefined : type || "button"}
546
299
  {...props}
547
300
  >
548
- {children ?? (
549
- <XIcon
550
- aria-hidden="true"
551
- className="size-[var(--bh-popover-close-icon-size)]"
552
- strokeWidth={2}
553
- />
554
- )}
555
- </Comp>
301
+ {children ?? <XIcon aria-hidden="true" className="size-[var(--bh-popover-close-icon-size)]" strokeWidth="var(--bh-icon-stroke-200)" />}
302
+ </PopoverPrimitive.Close>
556
303
  )
557
304
  }
558
305
  )
559
306
 
560
- function PopoverArrow({
561
- align,
562
- side,
563
- }: {
564
- align: PopoverAlign
565
- side: PopoverSide
566
- }) {
567
- const arrowSide = popoverArrowSideByContentSide[side]
568
-
569
- return (
570
- <span
571
- aria-hidden="true"
572
- data-arrow-side={arrowSide}
573
- data-slot="popover-arrow"
574
- className={cn(
575
- "pointer-events-none absolute z-[var(--bh-z-raised)]",
576
- getPopoverArrowPlacement(side, align)
577
- )}
578
- >
579
- <svg
580
- className="block h-full w-full overflow-visible"
581
- focusable="false"
582
- viewBox={popoverArrowShape[arrowSide].viewBox}
583
- >
584
- <path d={popoverArrowShape[arrowSide].fillPath} fill="var(--bh-popover-bg)" />
585
- <path
586
- d={popoverArrowShape[arrowSide].strokePath}
587
- fill="none"
588
- stroke="var(--bh-popover-border)"
589
- strokeLinecap="round"
590
- strokeLinejoin="round"
591
- strokeWidth="var(--bh-border-width-default)"
592
- vectorEffect="non-scaling-stroke"
593
- />
594
- </svg>
595
- </span>
596
- )
597
- }
598
-
599
- function usePopoverContext(componentName: string) {
600
- const context = React.useContext(PopoverContext)
601
-
602
- if (!context) {
603
- throw new Error(`${componentName} must be used within Popover`)
604
- }
605
-
606
- return context
607
- }
608
-
609
- function useControllablePopoverState({
610
- defaultOpen,
611
- onOpenChange,
612
- open,
613
- }: {
614
- defaultOpen: boolean
615
- onOpenChange?: (open: boolean) => void
616
- open?: boolean
617
- }) {
618
- const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
619
- const isControlled = open !== undefined
620
- const resolvedOpen = isControlled ? Boolean(open) : uncontrolledOpen
621
-
622
- const setOpen = React.useCallback(
623
- (nextOpen: boolean) => {
624
- if (!isControlled) {
625
- setUncontrolledOpen(nextOpen)
626
- }
627
-
628
- onOpenChange?.(nextOpen)
629
- },
630
- [isControlled, onOpenChange]
631
- )
632
-
633
- return [resolvedOpen, setOpen] as const
634
- }
635
-
636
- function getPopoverArrowPlacement(side: PopoverSide, align: PopoverAlign) {
637
- if (side === "bottom" || side === "top") {
638
- const edge =
639
- side === "bottom"
640
- ? "top-[var(--bh-popover-arrow-offset)]"
641
- : "bottom-[var(--bh-popover-arrow-offset)]"
642
- const size =
643
- "h-[calc(var(--bh-popover-arrow-depth)+var(--bh-popover-arrow-overlap))] w-[var(--bh-popover-arrow-width)]"
644
-
645
- if (align === "center") {
646
- return `${edge} left-1/2 ${size} -translate-x-1/2`
647
- }
648
-
649
- if (align === "end") {
650
- return `${edge} ${size} [inset-inline-end:var(--bh-popover-arrow-inline-offset)]`
651
- }
652
-
653
- return `${edge} ${size} [inset-inline-start:var(--bh-popover-arrow-inline-offset)]`
654
- }
655
-
656
- const edge =
657
- side === "right"
658
- ? "left-[var(--bh-popover-arrow-offset)]"
659
- : "right-[var(--bh-popover-arrow-offset)]"
660
- const size =
661
- "h-[var(--bh-popover-arrow-width)] w-[calc(var(--bh-popover-arrow-depth)+var(--bh-popover-arrow-overlap))]"
662
-
663
- if (align === "center") {
664
- return `${edge} top-1/2 ${size} -translate-y-1/2`
665
- }
666
-
667
- if (align === "end") {
668
- return `${edge} bottom-[var(--bh-popover-arrow-inline-offset)] ${size}`
669
- }
670
-
671
- return `${edge} top-[var(--bh-popover-arrow-inline-offset)] ${size}`
672
- }
673
-
674
- function getPopoverCollisionShift({
675
- collisionPadding,
676
- rect,
677
- viewportHeight,
678
- viewportWidth,
679
- }: {
680
- collisionPadding: number
681
- rect: Pick<DOMRect, "bottom" | "left" | "right" | "top">
682
- viewportHeight: number
683
- viewportWidth: number
684
- }): PopoverCollisionShift {
685
- let x = 0
686
- let y = 0
687
- const minX = collisionPadding
688
- const minY = collisionPadding
689
- const maxX = viewportWidth - collisionPadding
690
- const maxY = viewportHeight - collisionPadding
691
-
692
- if (rect.left < minX) {
693
- x = minX - rect.left
694
- } else if (rect.right > maxX) {
695
- x = maxX - rect.right
696
- }
697
-
698
- if (rect.top < minY) {
699
- y = minY - rect.top
700
- } else if (rect.bottom > maxY) {
701
- y = maxY - rect.bottom
702
- }
703
-
704
- return { x: Math.round(x), y: Math.round(y) }
705
- }
706
-
707
- function getPopoverContentTransform(
708
- side: PopoverSide,
709
- align: PopoverAlign,
710
- shift: PopoverCollisionShift
711
- ) {
712
- const shiftX = formatPopoverTranslateValue(shift.x)
713
- const shiftY = formatPopoverTranslateValue(shift.y)
714
-
715
- if ((side === "bottom" || side === "top") && align === "center") {
716
- return `translate(${formatPopoverTranslateCalc("-50%", shift.x)}, ${shiftY})`
717
- }
718
-
719
- if ((side === "left" || side === "right") && align === "center") {
720
- return `translate(${shiftX}, ${formatPopoverTranslateCalc("-50%", shift.y)})`
721
- }
722
-
723
- return `translate(${shiftX}, ${shiftY})`
724
- }
725
-
726
- function formatPopoverTranslateValue(value: number) {
727
- return `${Math.round(value)}px`
728
- }
729
-
730
- function formatPopoverTranslateCalc(base: string, value: number) {
731
- const roundedValue = Math.round(value)
732
- if (roundedValue === 0) return base
733
-
734
- return `calc(${base} ${roundedValue < 0 ? "-" : "+"} ${Math.abs(roundedValue)}px)`
735
- }
736
-
737
- function mergePopoverTransforms(
738
- contentTransform: string,
739
- customTransform: string | undefined
740
- ) {
741
- return customTransform
742
- ? `${contentTransform} ${customTransform}`
743
- : contentTransform
744
- }
745
-
746
- function isSamePopoverCollisionShift(
747
- currentShift: PopoverCollisionShift,
748
- nextShift: PopoverCollisionShift
749
- ) {
750
- return currentShift.x === nextShift.x && currentShift.y === nextShift.y
751
- }
752
-
753
- const popoverPositionClasses: Record<
754
- PopoverSide,
755
- Record<PopoverAlign, string>
756
- > = {
757
- bottom: {
758
- start: "top-[calc(100%+var(--bh-popover-offset))] [inset-inline-start:0]",
759
- center: "top-[calc(100%+var(--bh-popover-offset))] left-1/2",
760
- end: "top-[calc(100%+var(--bh-popover-offset))] [inset-inline-end:0]",
761
- },
762
- top: {
763
- start: "bottom-[calc(100%+var(--bh-popover-offset))] [inset-inline-start:0]",
764
- center: "bottom-[calc(100%+var(--bh-popover-offset))] left-1/2",
765
- end: "bottom-[calc(100%+var(--bh-popover-offset))] [inset-inline-end:0]",
766
- },
767
- right: {
768
- start: "left-[calc(100%+var(--bh-popover-offset))] top-0",
769
- center: "left-[calc(100%+var(--bh-popover-offset))] top-1/2",
770
- end: "bottom-0 left-[calc(100%+var(--bh-popover-offset))]",
771
- },
772
- left: {
773
- start: "right-[calc(100%+var(--bh-popover-offset))] top-0",
774
- center: "right-[calc(100%+var(--bh-popover-offset))] top-1/2",
775
- end: "bottom-0 right-[calc(100%+var(--bh-popover-offset))]",
776
- },
777
- }
778
-
779
- const popoverArrowSideByContentSide: Record<PopoverSide, PopoverArrowSide> = {
780
- bottom: "top",
781
- left: "right",
782
- right: "left",
783
- top: "bottom",
784
- }
785
-
786
- const popoverArrowShape = {
787
- top: {
788
- viewBox: "0 0 12 9",
789
- fillPath: "M0 9V8L6 0L12 8V9Z",
790
- strokePath: "M0 8L6 0L12 8",
791
- },
792
- bottom: {
793
- viewBox: "0 0 12 9",
794
- fillPath: "M0 0H12V1L6 9L0 1Z",
795
- strokePath: "M0 1L6 9L12 1",
796
- },
797
- left: {
798
- viewBox: "0 0 9 12",
799
- fillPath: "M9 0H8L0 6L8 12H9Z",
800
- strokePath: "M8 0L0 6L8 12",
801
- },
802
- right: {
803
- viewBox: "0 0 9 12",
804
- fillPath: "M0 0H1L9 6L1 12H0Z",
805
- strokePath: "M1 0L9 6L1 12",
806
- },
807
- } as const
808
-
809
307
  const PopoverRoot = Popover
810
308
 
811
309
  export {