banhaten 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +139 -73
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +98 -98
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +89 -89
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +177 -176
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -79
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +127 -76
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -61
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -69
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -60
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1277
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -255
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -126
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -603
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -555
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -677
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -358
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -236
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -11
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -57
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +458 -224
@@ -1,1277 +1,1289 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import {
5
- CalendarDaysIcon,
6
- ChevronDownIcon,
7
- ChevronLeftIcon,
8
- ChevronRightIcon,
9
- ChevronUpIcon,
10
- } from "lucide-react"
11
- import { cva } from "class-variance-authority"
12
-
13
- import { cn } from "@/lib/utils"
14
- import { Popover, PopoverContent, PopoverTrigger } from "./popover"
15
- import {
16
- addMonths,
17
- formatCalendarDate,
18
- formatCalendarRange,
19
- formatMonthTitle,
20
- getCalendarCells,
21
- getCalendarDayState,
22
- getCalendarKeyboardTarget,
23
- getNextRange,
24
- isSameMonth,
25
- normalizeRange,
26
- startOfMonth,
27
- toCalendarDateId,
28
- } from "./date-picker-state"
29
- import type {
30
- CalendarCell,
31
- CalendarMode,
32
- CalendarRange,
33
- CalendarView,
34
- DatePickerDirection,
35
- RangeCalendarType,
36
- } from "./date-picker-state"
37
-
38
- type ControlDensity = "compact" | "default" | "comfortable"
39
-
40
- type CalendarTimeValue = {
41
- hours?: string
42
- minutes?: string
43
- period?: string
44
- zone?: string
45
- }
46
-
47
- type CalendarMonthYearItem = {
48
- id?: string
49
- month: React.ReactNode
50
- year: React.ReactNode
51
- value?: Date
52
- selected?: boolean
53
- disabled?: boolean
54
- }
55
-
56
- type CalendarPreset = {
57
- id?: string
58
- label: React.ReactNode
59
- range?: CalendarRange
60
- disabled?: boolean
61
- }
62
-
63
- type CalendarProps = Omit<
64
- React.ComponentProps<"div">,
65
- "defaultValue" | "dir" | "onSelect"
66
- > & {
67
- defaultValue?: Date
68
- dir?: DatePickerDirection
69
- displayMonth?: Date
70
- events?: Date[]
71
- monthYearItems?: CalendarMonthYearItem[]
72
- nextLabel?: string
73
- onDisplayMonthChange?: (month: Date) => void
74
- onSelect?: (date: Date) => void
75
- onViewChange?: (view: CalendarView) => void
76
- previousLabel?: string
77
- selectedDate?: Date
78
- showTime?: boolean
79
- surfaceSize?: "calendar" | "picker"
80
- timeLabel?: React.ReactNode
81
- timeValue?: CalendarTimeValue
82
- title?: React.ReactNode
83
- value?: Date
84
- view?: CalendarView
85
- }
86
-
87
- type RangeCalendarProps = Omit<
88
- React.ComponentProps<"div">,
89
- "defaultValue" | "dir" | "onSelect"
90
- > & {
91
- defaultValue?: CalendarRange
92
- dir?: DatePickerDirection
93
- displayMonth?: Date
94
- nextLabel?: string
95
- onDisplayMonthChange?: (month: Date) => void
96
- onPresetSelect?: (preset: CalendarPreset) => void
97
- onSelect?: (range: CalendarRange) => void
98
- presets?: CalendarPreset[]
99
- previousLabel?: string
100
- showPresets?: boolean
101
- showTime?: boolean
102
- timeLabel?: React.ReactNode
103
- timeValue?: CalendarTimeValue
104
- type?: RangeCalendarType
105
- value?: CalendarRange
106
- }
107
-
108
- type DatePickerProps = Omit<
109
- React.ComponentProps<"div">,
110
- "defaultValue" | "dir" | "onSelect"
111
- > & {
112
- calendarProps?: Omit<
113
- CalendarProps,
114
- "defaultValue" | "selectedDate" | "value"
115
- >
116
- defaultOpen?: boolean
117
- defaultValue?: Date
118
- density?: ControlDensity
119
- dir?: DatePickerDirection
120
- disabled?: boolean
121
- icon?: React.ReactNode
122
- onOpenChange?: (open: boolean) => void
123
- onSelect?: (date: Date) => void
124
- open?: boolean
125
- placeholder?: React.ReactNode
126
- triggerClassName?: string
127
- triggerProps?: Omit<
128
- DatePickerTriggerProps,
129
- "density" | "icon" | "open" | "placeholder" | "value"
130
- >
131
- value?: Date
132
- }
133
-
134
- type DateRangePickerProps = Omit<
135
- React.ComponentProps<"div">,
136
- "defaultValue" | "dir" | "onSelect"
137
- > & {
138
- calendarProps?: Omit<
139
- RangeCalendarProps,
140
- "defaultValue" | "type" | "value"
141
- >
142
- defaultOpen?: boolean
143
- defaultValue?: CalendarRange
144
- density?: ControlDensity
145
- dir?: DatePickerDirection
146
- disabled?: boolean
147
- icon?: React.ReactNode
148
- onOpenChange?: (open: boolean) => void
149
- onSelect?: (range: CalendarRange) => void
150
- open?: boolean
151
- placeholder?: React.ReactNode
152
- triggerClassName?: string
153
- triggerProps?: Omit<
154
- DatePickerTriggerProps,
155
- "density" | "icon" | "open" | "placeholder" | "value"
156
- >
157
- type?: RangeCalendarType
158
- value?: CalendarRange
159
- }
160
-
161
- const defaultTimeValue = getCurrentTimeValue()
162
- const defaultDayLabels = [
163
- { full: "Sunday", short: "S" },
164
- { full: "Monday", short: "M" },
165
- { full: "Tuesday", short: "T" },
166
- { full: "Wednesday", short: "W" },
167
- { full: "Thursday", short: "T" },
168
- { full: "Friday", short: "F" },
169
- { full: "Saturday", short: "S" },
170
- ] as const
171
-
172
- const calendarSurface = cva(
173
- [
174
- "relative bg-[var(--bh-bg-raised)]",
175
- "[--bh-date-surface-border:var(--bh-border-subtle)]",
176
- "shadow-[var(--shadow-date-surface)]",
177
- ],
178
- {
179
- variants: {
180
- layout: {
181
- single:
182
- "flex flex-col items-start rounded-[var(--bh-radius-2xl-12)] p-[var(--bh-space-md-8)]",
183
- range:
184
- "inline-flex max-w-[var(--bh-date-range-surface-max-width)] items-start gap-[var(--bh-space-3xl-16)] overflow-auto rounded-[var(--bh-radius-2xl-12)] p-[var(--bh-space-3xl-16)]",
185
- },
186
- surfaceSize: {
187
- calendar: "w-[var(--bh-date-surface-width-calendar)]",
188
- picker: "w-[var(--bh-date-surface-width-picker)]",
189
- },
190
- },
191
- defaultVariants: {
192
- layout: "single",
193
- surfaceSize: "calendar",
194
- },
195
- }
196
- )
197
-
198
- const pickerTrigger = cva(
199
- [
200
- "group/date-trigger relative flex w-[var(--bh-date-trigger-width)] max-w-full items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-lg-8)]",
201
- "bg-[var(--bh-interactive-input-default)] text-start outline-none",
202
- "[--bh-date-trigger-border:var(--bh-border-input)] [--bh-date-trigger-shadow:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-date-trigger-border),var(--shadow-component-default)]",
203
- "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)]",
204
- "text-[var(--bh-content-default)] shadow-[var(--bh-date-trigger-shadow)] transition-[background-color,box-shadow]",
205
- "hover:bg-[var(--bh-bg-default-hover)] focus-visible:shadow-[var(--shadow-input-focus-ring)]",
206
- "disabled:pointer-events-none disabled:bg-[var(--bh-interactive-input-disabled)] disabled:text-[var(--bh-content-disabled)] disabled:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
207
- ],
208
- {
209
- variants: {
210
- density: {
211
- compact: "h-[var(--bh-button-xs-height)] px-[var(--bh-space-md-8)]",
212
- default: "h-[var(--bh-button-sm-height)] px-[var(--bh-space-lg-10)]",
213
- comfortable: "h-[var(--bh-date-trigger-height)] px-[var(--bh-space-xl-12)]",
214
- },
215
- },
216
- defaultVariants: {
217
- density: "comfortable",
218
- },
219
- }
220
- )
221
-
222
- const monthButton = cva(
223
- [
224
- "flex size-[var(--bh-date-nav-button-size)] shrink-0 items-center justify-center rounded-[var(--bh-control-default)]",
225
- "bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] outline-none transition-[background-color,box-shadow,color]",
226
- "hover:bg-[var(--bh-interactive-soft-hover)] focus-visible:shadow-[var(--shadow-button-focus)] disabled:opacity-[var(--bh-opacity-0)]",
227
- "[&_svg]:size-[var(--bh-date-nav-icon-size)]",
228
- ]
229
- )
230
-
231
- const calendarTitleButton = cva(
232
- [
233
- "flex h-[var(--bh-date-title-height)] min-w-0 flex-1 items-center justify-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)]",
234
- "px-[var(--bh-space-md-8)] text-center outline-none transition-[background-color,box-shadow]",
235
- "text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-content-default)]",
236
- "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--shadow-button-focus)]",
237
- ],
238
- {
239
- variants: {
240
- framed: {
241
- true:
242
- "shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-subtle)]",
243
- false: "",
244
- },
245
- active: {
246
- true: "bg-[var(--bh-interactive-ghost-hover)]",
247
- false: "",
248
- },
249
- },
250
- defaultVariants: {
251
- active: false,
252
- framed: false,
253
- },
254
- }
255
- )
256
-
257
- const dayCellButton = cva(
258
- [
259
- "relative flex h-[var(--bh-date-day-cell-height)] min-w-0 flex-1 items-center justify-center text-center outline-none",
260
- "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)]",
261
- "transition-[background-color,color,box-shadow] focus-visible:z-[var(--bh-z-raised)] focus-visible:shadow-[var(--shadow-button-focus)]",
262
- ],
263
- {
264
- variants: {
265
- state: {
266
- default:
267
- "rounded-[var(--bh-control-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-ghost-hover)]",
268
- outside:
269
- "rounded-[var(--bh-control-default)] text-[var(--bh-content-disabled)]",
270
- event:
271
- "rounded-[var(--bh-control-default)] text-[var(--bh-content-brand-default)] after:absolute after:bottom-[var(--bh-date-event-dot-offset-bottom)] after:left-1/2 after:size-[var(--bh-date-event-dot-size)] after:-translate-x-1/2 after:rounded-[var(--bh-radius-full)] after:bg-[var(--bh-content-brand-default)] rtl:after:translate-x-1/2",
272
- selected:
273
- "rounded-[var(--bh-control-default)] bg-[var(--bh-bg-brand-bold)] text-[var(--bh-content-on-brand)] hover:bg-[var(--bh-bg-brand-bold)] hover:shadow-[inset_0_0_0_var(--bh-border-width-default)_var(--bh-border-focus)]",
274
- range:
275
- "rounded-none bg-[var(--bh-bg-brand-subtle)] text-[var(--bh-content-brand-strong)] hover:bg-[var(--bh-bg-brand-subtle)]",
276
- },
277
- },
278
- defaultVariants: {
279
- state: "default",
280
- },
281
- }
282
- )
283
-
284
- const dateListItem = cva(
285
- [
286
- "flex h-[var(--bh-date-list-item-height)] w-full items-center rounded-[var(--bh-radius-lg-8)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-start outline-none",
287
- "text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)]",
288
- "text-[var(--bh-content-default)] transition-colors hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:bg-[var(--bh-interactive-ghost-hover)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
289
- ]
290
- )
291
-
292
- function Calendar({
293
- className,
294
- defaultValue,
295
- dir,
296
- displayMonth,
297
- events,
298
- monthYearItems,
299
- nextLabel = "Next month",
300
- onDisplayMonthChange,
301
- onSelect,
302
- onViewChange,
303
- previousLabel = "Previous month",
304
- selectedDate,
305
- showTime = false,
306
- surfaceSize = "calendar",
307
- timeLabel = "Time",
308
- timeValue = defaultTimeValue,
309
- title,
310
- value,
311
- view,
312
- ...props
313
- }: CalendarProps) {
314
- const controlledValue = value ?? selectedDate
315
- const [internalValue, setInternalValue] = React.useState(defaultValue)
316
- const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
317
- startOfMonth(displayMonth ?? controlledValue ?? defaultValue ?? new Date())
318
- )
319
- const [internalView, setInternalView] = React.useState<CalendarView>("days")
320
- const selected = controlledValue ?? internalValue
321
- const activeDisplayMonth = startOfMonth(displayMonth ?? internalDisplayMonth)
322
- const activeView = view ?? internalView
323
- const eventDates = events ?? []
324
- const resolvedMonthYearItems =
325
- monthYearItems ?? createMonthYearItems(activeDisplayMonth)
326
-
327
- function updateDisplayMonth(nextMonth: Date) {
328
- const normalized = startOfMonth(nextMonth)
329
- if (displayMonth === undefined) {
330
- setInternalDisplayMonth(normalized)
331
- }
332
- onDisplayMonthChange?.(normalized)
333
- }
334
-
335
- function updateView(nextView: CalendarView) {
336
- if (view === undefined) {
337
- setInternalView(nextView)
338
- }
339
- onViewChange?.(nextView)
340
- }
341
-
342
- function selectDate(nextDate: Date) {
343
- if (value === undefined && selectedDate === undefined) {
344
- setInternalValue(nextDate)
345
- }
346
- onSelect?.(nextDate)
347
- }
348
-
349
- return (
350
- <div
351
- data-slot="calendar"
352
- data-view={activeView}
353
- dir={dir}
354
- className={cn(calendarSurface({ layout: "single", surfaceSize }), className)}
355
- {...props}
356
- >
357
- <CalendarMonth
358
- displayMonth={activeDisplayMonth}
359
- eventDates={eventDates}
360
- mode="single"
361
- className="w-full"
362
- nextLabel={nextLabel}
363
- onDisplayMonthChange={updateDisplayMonth}
364
- onSelectDate={selectDate}
365
- onToggleView={() =>
366
- updateView(activeView === "days" ? "month-year" : "days")
367
- }
368
- previousLabel={previousLabel}
369
- selectedDate={selected}
370
- showTime={showTime && activeView === "days"}
371
- timeLabel={timeLabel}
372
- timeValue={timeValue}
373
- title={title}
374
- view={activeView}
375
- />
376
-
377
- {activeView === "month-year" ? (
378
- <>
379
- <div aria-hidden="true" className="h-[var(--bh-date-month-year-spacer-height)] w-full" />
380
- <MonthYearList
381
- items={resolvedMonthYearItems}
382
- onSelect={(item) => {
383
- if (!item.value) return
384
- updateDisplayMonth(item.value)
385
- updateView("days")
386
- }}
387
- />
388
- </>
389
- ) : null}
390
- </div>
391
- )
392
- }
393
-
394
- function RangeCalendar({
395
- className,
396
- defaultValue = {},
397
- dir,
398
- displayMonth,
399
- nextLabel = "Next month",
400
- onDisplayMonthChange,
401
- onPresetSelect,
402
- onSelect,
403
- presets,
404
- previousLabel = "Previous month",
405
- showPresets,
406
- showTime = false,
407
- timeLabel = "Time",
408
- timeValue = defaultTimeValue,
409
- type = "double",
410
- value,
411
- ...props
412
- }: RangeCalendarProps) {
413
- const [internalRange, setInternalRange] = React.useState(defaultValue)
414
- const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
415
- startOfMonth(displayMonth ?? defaultValue.from ?? new Date())
416
- )
417
- const [presetReferenceDate] = React.useState(() => startOfDay(new Date()))
418
- const selectedRange = normalizeRange(value ?? internalRange)
419
- const activeDisplayMonth = startOfMonth(displayMonth ?? internalDisplayMonth)
420
- const monthsToRender = type === "single" ? 1 : 2
421
- const shouldShowPresets = showPresets ?? type === "double-with-presets"
422
- const presetItems = presets ?? createDefaultPresets(presetReferenceDate)
423
- const monthNodes = Array.from({ length: monthsToRender }, (_, index) =>
424
- addMonths(activeDisplayMonth, index)
425
- )
426
-
427
- function updateDisplayMonth(nextMonth: Date) {
428
- const normalized = startOfMonth(nextMonth)
429
- if (displayMonth === undefined) {
430
- setInternalDisplayMonth(normalized)
431
- }
432
- onDisplayMonthChange?.(normalized)
433
- }
434
-
435
- function selectRangeDate(nextDate: Date) {
436
- const nextRange = getNextRange(selectedRange, nextDate)
437
- if (value === undefined) {
438
- setInternalRange(nextRange)
439
- }
440
- onSelect?.(nextRange)
441
- }
442
-
443
- function selectPreset(preset: CalendarPreset) {
444
- if (preset.disabled) return
445
- if (preset.range) {
446
- const nextRange = normalizeRange(preset.range)
447
- if (value === undefined) {
448
- setInternalRange(nextRange)
449
- }
450
- onSelect?.(nextRange)
451
- }
452
- onPresetSelect?.(preset)
453
- }
454
-
455
- return (
456
- <div
457
- data-slot="range-calendar"
458
- data-type={type}
459
- dir={dir}
460
- className={cn(calendarSurface({ layout: "range" }), className)}
461
- {...props}
462
- >
463
- {shouldShowPresets ? (
464
- <div
465
- data-slot="range-calendar-presets"
466
- className="grid w-[var(--bh-date-presets-width)] shrink-0 border-e border-[var(--bh-border-subtle)] pe-[var(--bh-space-md-8)]"
467
- >
468
- {presetItems.map((preset, index) => {
469
- const disabled =
470
- Boolean(preset.disabled) ||
471
- (!preset.range && onPresetSelect === undefined)
472
-
473
- return (
474
- <button
475
- aria-disabled={disabled || undefined}
476
- className={dateListItem()}
477
- data-slot="range-calendar-preset"
478
- disabled={disabled}
479
- key={preset.id || index}
480
- type="button"
481
- onClick={() => selectPreset(preset)}
482
- >
483
- <span dir="auto" className="min-w-0 truncate">
484
- {preset.label}
485
- </span>
486
- </button>
487
- )
488
- })}
489
- </div>
490
- ) : null}
491
-
492
- <div
493
- data-slot="range-calendar-months"
494
- className="flex min-w-max gap-[var(--bh-space-3xl-16)]"
495
- >
496
- {monthNodes.map((month, index) => (
497
- <CalendarMonth
498
- displayMonth={month}
499
- key={month.toISOString()}
500
- mode="range"
501
- className="w-[var(--bh-date-range-month-width)]"
502
- nextLabel={nextLabel}
503
- onDisplayMonthChange={updateDisplayMonth}
504
- onSelectDate={selectRangeDate}
505
- previousLabel={previousLabel}
506
- range={selectedRange}
507
- showNextButton={index === monthNodes.length - 1}
508
- showPreviousButton={index === 0}
509
- showTime={showTime}
510
- timeLabel={timeLabel}
511
- timeValue={timeValue}
512
- />
513
- ))}
514
- </div>
515
- </div>
516
- )
517
- }
518
-
519
- function DatePicker({
520
- calendarProps,
521
- className,
522
- defaultOpen = false,
523
- defaultValue,
524
- density,
525
- dir,
526
- disabled,
527
- icon,
528
- onOpenChange,
529
- onSelect,
530
- open,
531
- placeholder = "Select date",
532
- triggerClassName,
533
- triggerProps,
534
- value,
535
- ...props
536
- }: DatePickerProps) {
537
- const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
538
- const [internalValue, setInternalValue] = React.useState(defaultValue)
539
- const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
540
- startOfMonth(value ?? defaultValue ?? new Date())
541
- )
542
- const selected = value ?? internalValue
543
- const isOpen = open ?? internalOpen
544
- const selectedTime = selected?.getTime()
545
-
546
- React.useEffect(() => {
547
- if (!isOpen || calendarProps?.displayMonth || !selected) return
548
-
549
- setInternalDisplayMonth(startOfMonth(selected))
550
- }, [calendarProps?.displayMonth, isOpen, selectedTime])
551
-
552
- function updateOpen(nextOpen: boolean) {
553
- if (open === undefined) {
554
- setInternalOpen(nextOpen)
555
- }
556
- onOpenChange?.(nextOpen)
557
- }
558
-
559
- function selectDate(nextDate: Date) {
560
- if (value === undefined) {
561
- setInternalValue(nextDate)
562
- }
563
- onSelect?.(nextDate)
564
- }
565
-
566
- function updateDisplayMonth(nextMonth: Date) {
567
- const normalized = startOfMonth(nextMonth)
568
- if (calendarProps?.displayMonth === undefined) {
569
- setInternalDisplayMonth(normalized)
570
- }
571
- calendarProps?.onDisplayMonthChange?.(normalized)
572
- }
573
-
574
- return (
575
- <Popover
576
- open={isOpen}
577
- onOpenChange={updateOpen}
578
- data-open={isOpen ? "true" : "false"}
579
- data-slot="date-picker"
580
- dir={dir}
581
- className={cn("relative w-[var(--bh-date-picker-width)] max-w-full", className)}
582
- {...props}
583
- >
584
- <PopoverTrigger asChild>
585
- <DatePickerTrigger
586
- {...triggerProps}
587
- disabled={disabled ?? triggerProps?.disabled}
588
- density={density}
589
- icon={icon}
590
- open={isOpen}
591
- placeholder={placeholder}
592
- value={selected ? formatCalendarDate(selected, { month: "long" }) : undefined}
593
- className={cn(triggerClassName, triggerProps?.className)}
594
- />
595
- </PopoverTrigger>
596
-
597
- <PopoverContent
598
- align="center"
599
- showArrow={false}
600
- size="auto"
601
- className="w-auto bg-transparent p-0 shadow-none"
602
- >
603
- <Calendar
604
- {...calendarProps}
605
- defaultValue={defaultValue}
606
- displayMonth={calendarProps?.displayMonth ?? internalDisplayMonth}
607
- selectedDate={selected}
608
- surfaceSize="picker"
609
- className={calendarProps?.className}
610
- dir={calendarProps?.dir ?? dir}
611
- onDisplayMonthChange={updateDisplayMonth}
612
- onSelect={(nextDate) => {
613
- selectDate(nextDate)
614
- calendarProps?.onSelect?.(nextDate)
615
- }}
616
- />
617
- </PopoverContent>
618
- </Popover>
619
- )
620
- }
621
-
622
- function DateRangePicker({
623
- calendarProps,
624
- className,
625
- defaultOpen = false,
626
- defaultValue,
627
- density,
628
- dir,
629
- disabled,
630
- icon,
631
- onOpenChange,
632
- onSelect,
633
- open,
634
- placeholder = "Select date range",
635
- triggerClassName,
636
- triggerProps,
637
- type = "double",
638
- value,
639
- ...props
640
- }: DateRangePickerProps) {
641
- const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
642
- const [internalValue, setInternalValue] = React.useState(defaultValue)
643
- const selectedRange = normalizeRange(value ?? internalValue ?? {})
644
- const isOpen = open ?? internalOpen
645
- const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
646
- startOfMonth(selectedRange.from ?? defaultValue?.from ?? new Date())
647
- )
648
- const selectedRangeFromTime = selectedRange.from?.getTime()
649
-
650
- React.useEffect(() => {
651
- if (!isOpen || calendarProps?.displayMonth || !selectedRange.from) return
652
-
653
- setInternalDisplayMonth(startOfMonth(selectedRange.from))
654
- }, [calendarProps?.displayMonth, isOpen, selectedRangeFromTime])
655
-
656
- function updateOpen(nextOpen: boolean) {
657
- if (open === undefined) {
658
- setInternalOpen(nextOpen)
659
- }
660
- onOpenChange?.(nextOpen)
661
- }
662
-
663
- function selectRange(nextRange: CalendarRange) {
664
- if (value === undefined) {
665
- setInternalValue(nextRange)
666
- }
667
- onSelect?.(nextRange)
668
- }
669
-
670
- function updateDisplayMonth(nextMonth: Date) {
671
- const normalized = startOfMonth(nextMonth)
672
- if (calendarProps?.displayMonth === undefined) {
673
- setInternalDisplayMonth(normalized)
674
- }
675
- calendarProps?.onDisplayMonthChange?.(normalized)
676
- }
677
-
678
- return (
679
- <Popover
680
- open={isOpen}
681
- onOpenChange={updateOpen}
682
- data-open={isOpen ? "true" : "false"}
683
- data-slot="date-range-picker"
684
- data-type={type}
685
- dir={dir}
686
- className={cn("relative w-[var(--bh-date-picker-width)] max-w-full", className)}
687
- {...props}
688
- >
689
- <PopoverTrigger asChild>
690
- <DatePickerTrigger
691
- {...triggerProps}
692
- disabled={disabled ?? triggerProps?.disabled}
693
- density={density}
694
- icon={icon}
695
- open={isOpen}
696
- placeholder={placeholder}
697
- value={formatCalendarRange(selectedRange)}
698
- className={cn(triggerClassName, triggerProps?.className)}
699
- />
700
- </PopoverTrigger>
701
-
702
- <PopoverContent
703
- align="center"
704
- showArrow={false}
705
- size="auto"
706
- className="w-auto bg-transparent p-0 shadow-none"
707
- >
708
- <RangeCalendar
709
- {...calendarProps}
710
- defaultValue={defaultValue ?? {}}
711
- displayMonth={calendarProps?.displayMonth ?? internalDisplayMonth}
712
- type={type}
713
- value={selectedRange}
714
- className={calendarProps?.className}
715
- dir={calendarProps?.dir ?? dir}
716
- onDisplayMonthChange={updateDisplayMonth}
717
- onSelect={(nextRange) => {
718
- selectRange(nextRange)
719
- calendarProps?.onSelect?.(nextRange)
720
- }}
721
- />
722
- </PopoverContent>
723
- </Popover>
724
- )
725
- }
726
-
727
- type DatePickerTriggerProps = Omit<
728
- React.ComponentProps<"button">,
729
- "children" | "value"
730
- > & {
731
- density?: ControlDensity
732
- icon?: React.ReactNode
733
- open?: boolean
734
- placeholder: React.ReactNode
735
- value?: React.ReactNode
736
- }
737
-
738
- function DatePickerTrigger({
739
- className,
740
- density,
741
- disabled,
742
- icon,
743
- onClick,
744
- open,
745
- placeholder,
746
- type = "button",
747
- value,
748
- ...props
749
- }: DatePickerTriggerProps) {
750
- const hasValue = hasRenderableContent(value)
751
-
752
- return (
753
- <button
754
- aria-expanded={open || undefined}
755
- data-date-picker-trigger=""
756
- data-density={density ?? "comfortable"}
757
- data-slot="date-picker-trigger"
758
- disabled={disabled}
759
- type={type}
760
- className={cn(pickerTrigger({ density }), className)}
761
- onClick={onClick}
762
- {...props}
763
- >
764
- <span
765
- aria-hidden="true"
766
- data-slot="date-picker-trigger-icon"
767
- className="flex size-[var(--bh-date-trigger-icon-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)] group-disabled/date-trigger:text-[var(--bh-content-disabled)] [&_svg:not([class*='size-'])]:size-[var(--bh-date-trigger-icon-size)]"
768
- >
769
- {icon ?? <CalendarDaysIcon strokeWidth="var(--bh-icon-stroke-180)" />}
770
- </span>
771
- <span
772
- data-slot="date-picker-trigger-value"
773
- dir="auto"
774
- className={cn(
775
- "min-w-0 flex-1 truncate text-start",
776
- hasValue ? "text-[var(--bh-content-default)]" : "text-[var(--bh-content-muted)]",
777
- disabled && "text-[var(--bh-content-disabled)]"
778
- )}
779
- >
780
- {hasValue ? value : placeholder}
781
- </span>
782
- </button>
783
- )
784
- }
785
-
786
- function CalendarMonth({
787
- className,
788
- displayMonth,
789
- eventDates,
790
- mode,
791
- nextLabel,
792
- onDisplayMonthChange,
793
- onSelectDate,
794
- onToggleView,
795
- previousLabel,
796
- range,
797
- selectedDate,
798
- showNextButton = true,
799
- showPreviousButton = true,
800
- showTime,
801
- timeLabel,
802
- timeValue,
803
- title,
804
- view = "days",
805
- }: {
806
- className?: string
807
- displayMonth: Date
808
- eventDates?: Date[]
809
- mode: CalendarMode
810
- nextLabel: string
811
- onDisplayMonthChange: (month: Date) => void
812
- onSelectDate: (date: Date) => void
813
- onToggleView?: () => void
814
- previousLabel: string
815
- range?: CalendarRange
816
- selectedDate?: Date
817
- showNextButton?: boolean
818
- showPreviousButton?: boolean
819
- showTime?: boolean
820
- timeLabel: React.ReactNode
821
- timeValue: CalendarTimeValue
822
- title?: React.ReactNode
823
- view?: CalendarView
824
- }) {
825
- const cells = React.useMemo(() => getCalendarCells(displayMonth), [displayMonth])
826
- const gridRef = React.useRef<HTMLDivElement>(null)
827
- const titleId = React.useId()
828
- const pendingFocusDateRef = React.useRef<Date | undefined>(undefined)
829
- const monthTitle = title ?? formatMonthTitle(displayMonth)
830
- const hasMonthToggle = mode === "single"
831
-
832
- React.useLayoutEffect(() => {
833
- if (!pendingFocusDateRef.current) return
834
-
835
- focusCalendarDate(pendingFocusDateRef.current)
836
- pendingFocusDateRef.current = undefined
837
- }, [cells])
838
-
839
- function focusCalendarDate(date: Date) {
840
- const target = gridRef.current?.querySelector<HTMLButtonElement>(
841
- `[data-calendar-date="${toCalendarDateId(date)}"]`
842
- )
843
-
844
- target?.focus()
845
- }
846
-
847
- function handleKeyboardNavigateDate(nextDate: Date) {
848
- if (isSameMonth(nextDate, displayMonth)) {
849
- focusCalendarDate(nextDate)
850
- return
851
- }
852
-
853
- pendingFocusDateRef.current = nextDate
854
- onDisplayMonthChange(startOfMonth(nextDate))
855
- }
856
-
857
- return (
858
- <div
859
- data-slot="calendar-month"
860
- className={cn(
861
- "flex shrink-0 flex-col items-start gap-[var(--bh-space-md-8)]",
862
- className
863
- )}
864
- >
865
- <div
866
- data-slot="calendar-header"
867
- className="flex w-full items-center gap-[var(--bh-space-md-8)]"
868
- >
869
- <button
870
- aria-label={previousLabel}
871
- disabled={!showPreviousButton || view === "month-year"}
872
- type="button"
873
- className={monthButton()}
874
- onClick={() => onDisplayMonthChange(addMonths(displayMonth, -1))}
875
- >
876
- <ChevronLeftIcon aria-hidden="true" data-rtl-flip="true" />
877
- </button>
878
-
879
- {hasMonthToggle ? (
880
- <button
881
- aria-expanded={view === "month-year"}
882
- data-slot="calendar-title"
883
- id={titleId}
884
- type="button"
885
- className={calendarTitleButton({
886
- active: view === "month-year",
887
- framed: true,
888
- })}
889
- onClick={onToggleView}
890
- >
891
- <span className="min-w-0 truncate">{monthTitle}</span>
892
- {view === "month-year" ? (
893
- <ChevronUpIcon aria-hidden="true" className="size-[var(--bh-date-title-chevron-size)]" />
894
- ) : (
895
- <ChevronDownIcon aria-hidden="true" className="size-[var(--bh-date-title-chevron-size)]" />
896
- )}
897
- </button>
898
- ) : (
899
- <div
900
- data-slot="calendar-title"
901
- id={titleId}
902
- className={calendarTitleButton({ framed: false })}
903
- >
904
- <span className="min-w-0 truncate">{monthTitle}</span>
905
- </div>
906
- )}
907
-
908
- <button
909
- aria-label={nextLabel}
910
- disabled={!showNextButton || view === "month-year"}
911
- type="button"
912
- className={monthButton()}
913
- onClick={() => onDisplayMonthChange(addMonths(displayMonth, 1))}
914
- >
915
- <ChevronRightIcon aria-hidden="true" data-rtl-flip="true" />
916
- </button>
917
- </div>
918
-
919
- {view === "days" ? (
920
- <>
921
- <div
922
- aria-colcount={7}
923
- aria-labelledby={titleId}
924
- aria-rowcount={Math.ceil(cells.length / 7) + 1}
925
- data-slot="calendar-grid"
926
- role="grid"
927
- ref={gridRef}
928
- className="grid w-full gap-[var(--bh-space-md-8)]"
929
- >
930
- <div
931
- data-slot="calendar-weekdays"
932
- role="row"
933
- className="grid w-full grid-cols-7"
934
- >
935
- {defaultDayLabels.map((day) => (
936
- <div
937
- aria-label={day.full}
938
- className="flex h-[var(--bh-date-day-cell-height)] min-w-0 items-center justify-center rounded-[var(--bh-radius-lg-8)] text-center 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)]"
939
- key={day.full}
940
- role="columnheader"
941
- >
942
- {day.short}
943
- </div>
944
- ))}
945
- </div>
946
-
947
- <div role="rowgroup" className="grid w-full">
948
- {chunkCalendarRows(cells).map((row, rowIndex) => (
949
- <div
950
- className="grid w-full grid-cols-7"
951
- key={row[0]?.date.toISOString() ?? rowIndex}
952
- role="row"
953
- >
954
- {row.map((cell) => (
955
- <CalendarDayCell
956
- cell={cell}
957
- displayMonth={displayMonth}
958
- eventDates={eventDates}
959
- key={cell.date.toISOString()}
960
- mode={mode}
961
- onKeyboardNavigateDate={handleKeyboardNavigateDate}
962
- onSelectDate={onSelectDate}
963
- range={range}
964
- selectedDate={selectedDate}
965
- />
966
- ))}
967
- </div>
968
- ))}
969
- </div>
970
- </div>
971
-
972
- {showTime ? (
973
- <CalendarTime
974
- label={timeLabel}
975
- value={timeValue}
976
- />
977
- ) : null}
978
- </>
979
- ) : null}
980
- </div>
981
- )
982
- }
983
-
984
- function chunkCalendarRows(cells: CalendarCell[]) {
985
- const rows: CalendarCell[][] = []
986
-
987
- for (let index = 0; index < cells.length; index += 7) {
988
- rows.push(cells.slice(index, index + 7))
989
- }
990
-
991
- return rows
992
- }
993
-
994
- function CalendarDayCell({
995
- cell,
996
- displayMonth,
997
- eventDates,
998
- mode,
999
- onKeyboardNavigateDate,
1000
- onSelectDate,
1001
- range,
1002
- selectedDate,
1003
- }: {
1004
- cell: CalendarCell
1005
- displayMonth: Date
1006
- eventDates?: Date[]
1007
- mode: CalendarMode
1008
- onKeyboardNavigateDate: (date: Date) => void
1009
- onSelectDate: (date: Date) => void
1010
- range?: CalendarRange
1011
- selectedDate?: Date
1012
- }) {
1013
- const { inRange, outside, rangeEndpoint, selected, state } = getCalendarDayState({
1014
- date: cell.date,
1015
- displayMonth,
1016
- eventDates,
1017
- mode,
1018
- range,
1019
- selectedDate,
1020
- })
1021
-
1022
- function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
1023
- const nextDate = getCalendarKeyboardTarget(cell.date, event.key)
1024
-
1025
- if (!nextDate) return
1026
-
1027
- event.preventDefault()
1028
- onKeyboardNavigateDate(nextDate)
1029
- }
1030
-
1031
- return (
1032
- <button
1033
- aria-disabled={outside || undefined}
1034
- aria-label={formatCalendarDate(cell.date, { month: "long" })}
1035
- aria-selected={selected || undefined}
1036
- data-calendar-date={toCalendarDateId(cell.date)}
1037
- data-outside={outside ? "true" : undefined}
1038
- data-range-endpoint={rangeEndpoint ? "true" : undefined}
1039
- data-slot="calendar-cell"
1040
- disabled={outside}
1041
- role="gridcell"
1042
- type="button"
1043
- className={cn(
1044
- dayCellButton({ state }),
1045
- inRange && cell.weekday === 0 && "rounded-s-[var(--bh-control-default)]",
1046
- inRange && cell.weekday === 6 && "rounded-e-[var(--bh-control-default)]"
1047
- )}
1048
- onClick={() => onSelectDate(cell.date)}
1049
- onKeyDown={handleKeyDown}
1050
- >
1051
- {cell.date.getDate()}
1052
- </button>
1053
- )
1054
- }
1055
-
1056
- function CalendarTime({
1057
- label,
1058
- value,
1059
- }: {
1060
- label: React.ReactNode
1061
- value: CalendarTimeValue
1062
- }) {
1063
- const resolved = {
1064
- ...defaultTimeValue,
1065
- ...value,
1066
- }
1067
-
1068
- return (
1069
- <div
1070
- data-slot="calendar-time"
1071
- className="flex w-full items-center gap-[var(--bh-space-5xl-24)] ps-[var(--bh-space-md-8)]"
1072
- >
1073
- <span
1074
- data-slot="calendar-time-label"
1075
- dir="auto"
1076
- className="shrink-0 text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
1077
- >
1078
- {label}
1079
- </span>
1080
- <span
1081
- data-slot="calendar-time-value"
1082
- className="flex min-h-[var(--bh-date-time-input-min-height)] min-w-0 flex-1 items-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-interactive-input-default)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] shadow-[var(--shadow-date-time-input)]"
1083
- >
1084
- <CalendarTimeSegment active>{resolved.hours}</CalendarTimeSegment>
1085
- <span className="text-[var(--bh-content-subtle)]">:</span>
1086
- <CalendarTimeSegment>{resolved.minutes}</CalendarTimeSegment>
1087
- <CalendarTimeSegment>{resolved.period}</CalendarTimeSegment>
1088
- <CalendarTimeSegment subtle>{resolved.zone}</CalendarTimeSegment>
1089
- </span>
1090
- </div>
1091
- )
1092
- }
1093
-
1094
- function CalendarTimeSegment({
1095
- active,
1096
- children,
1097
- subtle,
1098
- }: {
1099
- active?: boolean
1100
- children: React.ReactNode
1101
- subtle?: boolean
1102
- }) {
1103
- return (
1104
- <span
1105
- data-active={active ? "true" : undefined}
1106
- className={cn(
1107
- "inline-flex min-w-0 items-center justify-center rounded-[var(--bh-radius-sm-4)] px-[var(--bh-space-xxs-2)] text-[length:var(--bh-text-body-2xs-medium-font-size)] font-[var(--bh-text-body-2xs-medium-font-weight)] leading-[var(--bh-text-body-2xs-medium-line-height)] tracking-[var(--bh-text-body-2xs-medium-letter-spacing)]",
1108
- active && "bg-[var(--bh-interactive-ghost-hover)]",
1109
- subtle ? "text-[var(--bh-content-subtle)]" : "text-[var(--bh-content-default)]"
1110
- )}
1111
- >
1112
- {children}
1113
- </span>
1114
- )
1115
- }
1116
-
1117
- function MonthYearList({
1118
- items,
1119
- onSelect,
1120
- }: {
1121
- items: CalendarMonthYearItem[]
1122
- onSelect: (item: CalendarMonthYearItem) => void
1123
- }) {
1124
- return (
1125
- <div
1126
- data-slot="calendar-month-year-list"
1127
- className="absolute bottom-[var(--bh-space-md-8)] left-1/2 top-[var(--bh-date-month-year-list-top)] flex w-[var(--bh-date-month-year-list-width)] -translate-x-1/2 flex-col justify-center overflow-hidden rounded-[var(--bh-radius-md-6)] bg-[var(--bh-bg-default)] p-[var(--bh-space-md-8)] shadow-[var(--shadow-date-month-year-list)] rtl:translate-x-1/2"
1128
- >
1129
- {items.map((item, index) => (
1130
- <button
1131
- className={cn(
1132
- "grid h-[var(--bh-date-month-year-item-height)] grid-cols-2 items-center rounded-[var(--bh-radius-md-6)] px-[var(--bh-space-md-8)] py-[var(--bh-space-xs-4)] text-center outline-none transition-colors",
1133
- "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)]",
1134
- "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:bg-[var(--bh-interactive-ghost-hover)] disabled:text-[var(--bh-content-disabled)]",
1135
- item.selected && "bg-[var(--bh-interactive-ghost-hover)]"
1136
- )}
1137
- data-selected={item.selected ? "true" : undefined}
1138
- data-slot="calendar-month-year-item"
1139
- disabled={item.disabled || !item.value}
1140
- key={item.id || index}
1141
- onClick={() => onSelect(item)}
1142
- type="button"
1143
- >
1144
- <span className="truncate">{item.month}</span>
1145
- <span className="truncate">{item.year}</span>
1146
- </button>
1147
- ))}
1148
- </div>
1149
- )
1150
- }
1151
-
1152
- function startOfDay(date: Date) {
1153
- return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1154
- }
1155
-
1156
- function addDays(date: Date, amount: number) {
1157
- const next = startOfDay(date)
1158
- next.setDate(next.getDate() + amount)
1159
- return next
1160
- }
1161
-
1162
- function createDefaultPresets(today: Date): CalendarPreset[] {
1163
- const quarterStartMonth = Math.floor(today.getMonth() / 3) * 3
1164
-
1165
- return [
1166
- { id: "today", label: "Today", range: { from: today, to: today } },
1167
- {
1168
- id: "last-7-days",
1169
- label: "Last 7 days",
1170
- range: { from: addDays(today, -6), to: today },
1171
- },
1172
- {
1173
- id: "last-14-days",
1174
- label: "Last 14 days",
1175
- range: { from: addDays(today, -13), to: today },
1176
- },
1177
- {
1178
- id: "last-30-days",
1179
- label: "Last 30 days",
1180
- range: { from: addDays(today, -29), to: today },
1181
- },
1182
- {
1183
- id: "last-3-months",
1184
- label: "Last 3 months",
1185
- range: { from: addMonths(today, -3), to: today },
1186
- },
1187
- {
1188
- id: "last-12-months",
1189
- label: "Last 12 months",
1190
- range: { from: addMonths(today, -12), to: today },
1191
- },
1192
- {
1193
- id: "quarter-to-date",
1194
- label: "Quarter to date",
1195
- range: {
1196
- from: new Date(today.getFullYear(), quarterStartMonth, 1),
1197
- to: today,
1198
- },
1199
- },
1200
- {
1201
- id: "year-to-date",
1202
- label: "Year to date",
1203
- range: { from: new Date(today.getFullYear(), 0, 1), to: today },
1204
- },
1205
- ]
1206
- }
1207
-
1208
- function createMonthYearItems(
1209
- displayMonth: Date
1210
- ): CalendarMonthYearItem[] {
1211
- return Array.from({ length: 7 }, (_, index) => {
1212
- const value = addMonths(displayMonth, index - 3)
1213
-
1214
- return {
1215
- id: `${value.getFullYear()}-${value.getMonth()}`,
1216
- month: new Intl.DateTimeFormat(undefined, { month: "long" }).format(value),
1217
- selected: index === 3,
1218
- value,
1219
- year: String(value.getFullYear()),
1220
- }
1221
- })
1222
- }
1223
-
1224
- function getCurrentTimeValue(): Required<CalendarTimeValue> {
1225
- const now = new Date()
1226
- const timeParts = new Intl.DateTimeFormat("en-US", {
1227
- hour: "2-digit",
1228
- hour12: true,
1229
- minute: "2-digit",
1230
- }).formatToParts(now)
1231
- const zoneParts = new Intl.DateTimeFormat("en-US", {
1232
- timeZoneName: "short",
1233
- }).formatToParts(now)
1234
- const getPart = (parts: Intl.DateTimeFormatPart[], type: string) =>
1235
- parts.find((part) => part.type === type)?.value ?? ""
1236
-
1237
- return {
1238
- hours: getPart(timeParts, "hour"),
1239
- minutes: getPart(timeParts, "minute"),
1240
- period: getPart(timeParts, "dayPeriod"),
1241
- zone: getPart(zoneParts, "timeZoneName") || "UTC",
1242
- }
1243
- }
1244
-
1245
- function hasRenderableContent(content: React.ReactNode) {
1246
- return (
1247
- content !== undefined &&
1248
- content !== null &&
1249
- content !== false &&
1250
- content !== ""
1251
- )
1252
- }
1253
-
1254
- export {
1255
- Calendar,
1256
- DatePicker,
1257
- DatePickerTrigger,
1258
- DateRangePicker,
1259
- RangeCalendar,
1260
- calendarSurface,
1261
- pickerTrigger,
1262
- }
1263
- export type {
1264
- CalendarMonthYearItem,
1265
- CalendarPreset,
1266
- CalendarProps,
1267
- CalendarRange,
1268
- CalendarTimeValue,
1269
- CalendarView,
1270
- DatePickerDirection,
1271
- DatePickerProps,
1272
- DatePickerTriggerProps,
1273
- DateRangePickerProps,
1274
- ControlDensity,
1275
- RangeCalendarProps,
1276
- RangeCalendarType,
1277
- }
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ CalendarDaysIcon,
6
+ ChevronDownIcon,
7
+ ChevronLeftIcon,
8
+ ChevronRightIcon,
9
+ ChevronUpIcon,
10
+ } from "lucide-react"
11
+ import { cva } from "class-variance-authority"
12
+
13
+ import { cn } from "@/lib/utils"
14
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover"
15
+ import {
16
+ addMonths,
17
+ formatCalendarDate,
18
+ formatCalendarRange,
19
+ formatMonthTitle,
20
+ getCalendarCells,
21
+ getCalendarDayState,
22
+ getCalendarKeyboardTarget,
23
+ getNextRange,
24
+ isSameMonth,
25
+ normalizeRange,
26
+ startOfMonth,
27
+ toCalendarDateId,
28
+ } from "./date-picker-state"
29
+ import type {
30
+ CalendarCell,
31
+ CalendarMode,
32
+ CalendarRange,
33
+ CalendarView,
34
+ DatePickerDirection,
35
+ RangeCalendarType,
36
+ } from "./date-picker-state"
37
+
38
+ type ControlDensity = "compact" | "default" | "comfortable"
39
+
40
+ type CalendarTimeValue = {
41
+ hours?: string
42
+ minutes?: string
43
+ period?: string
44
+ zone?: string
45
+ }
46
+
47
+ type CalendarMonthYearItem = {
48
+ id?: string
49
+ month: React.ReactNode
50
+ year: React.ReactNode
51
+ value?: Date
52
+ selected?: boolean
53
+ disabled?: boolean
54
+ }
55
+
56
+ type CalendarPreset = {
57
+ id?: string
58
+ label: React.ReactNode
59
+ range?: CalendarRange
60
+ disabled?: boolean
61
+ }
62
+
63
+ type CalendarProps = Omit<
64
+ React.ComponentProps<"div">,
65
+ "defaultValue" | "dir" | "onSelect"
66
+ > & {
67
+ defaultValue?: Date
68
+ dir?: DatePickerDirection
69
+ displayMonth?: Date
70
+ events?: Date[]
71
+ monthYearItems?: CalendarMonthYearItem[]
72
+ nextLabel?: string
73
+ onDisplayMonthChange?: (month: Date) => void
74
+ onSelect?: (date: Date) => void
75
+ onViewChange?: (view: CalendarView) => void
76
+ previousLabel?: string
77
+ selectedDate?: Date
78
+ showTime?: boolean
79
+ surfaceSize?: "calendar" | "picker"
80
+ timeLabel?: React.ReactNode
81
+ timeValue?: CalendarTimeValue
82
+ title?: React.ReactNode
83
+ value?: Date
84
+ view?: CalendarView
85
+ }
86
+
87
+ type RangeCalendarProps = Omit<
88
+ React.ComponentProps<"div">,
89
+ "defaultValue" | "dir" | "onSelect"
90
+ > & {
91
+ defaultValue?: CalendarRange
92
+ dir?: DatePickerDirection
93
+ displayMonth?: Date
94
+ nextLabel?: string
95
+ onDisplayMonthChange?: (month: Date) => void
96
+ onPresetSelect?: (preset: CalendarPreset) => void
97
+ onSelect?: (range: CalendarRange) => void
98
+ presets?: CalendarPreset[]
99
+ previousLabel?: string
100
+ showPresets?: boolean
101
+ showTime?: boolean
102
+ timeLabel?: React.ReactNode
103
+ timeValue?: CalendarTimeValue
104
+ type?: RangeCalendarType
105
+ value?: CalendarRange
106
+ }
107
+
108
+ type DatePickerProps = Omit<
109
+ React.ComponentProps<"div">,
110
+ "defaultValue" | "dir" | "onSelect"
111
+ > & {
112
+ calendarProps?: Omit<
113
+ CalendarProps,
114
+ "defaultValue" | "selectedDate" | "value"
115
+ >
116
+ defaultOpen?: boolean
117
+ defaultValue?: Date
118
+ density?: ControlDensity
119
+ dir?: DatePickerDirection
120
+ disabled?: boolean
121
+ icon?: React.ReactNode
122
+ onOpenChange?: (open: boolean) => void
123
+ onSelect?: (date: Date) => void
124
+ open?: boolean
125
+ placeholder?: React.ReactNode
126
+ triggerClassName?: string
127
+ triggerProps?: Omit<
128
+ DatePickerTriggerProps,
129
+ "density" | "icon" | "open" | "placeholder" | "value"
130
+ >
131
+ value?: Date
132
+ }
133
+
134
+ type DateRangePickerProps = Omit<
135
+ React.ComponentProps<"div">,
136
+ "defaultValue" | "dir" | "onSelect"
137
+ > & {
138
+ calendarProps?: Omit<
139
+ RangeCalendarProps,
140
+ "defaultValue" | "type" | "value"
141
+ >
142
+ defaultOpen?: boolean
143
+ defaultValue?: CalendarRange
144
+ density?: ControlDensity
145
+ dir?: DatePickerDirection
146
+ disabled?: boolean
147
+ icon?: React.ReactNode
148
+ onOpenChange?: (open: boolean) => void
149
+ onSelect?: (range: CalendarRange) => void
150
+ open?: boolean
151
+ placeholder?: React.ReactNode
152
+ triggerClassName?: string
153
+ triggerProps?: Omit<
154
+ DatePickerTriggerProps,
155
+ "density" | "icon" | "open" | "placeholder" | "value"
156
+ >
157
+ type?: RangeCalendarType
158
+ value?: CalendarRange
159
+ }
160
+
161
+ const defaultTimeValue = getCurrentTimeValue()
162
+ const defaultDayLabels = [
163
+ { full: "Sunday", short: "S" },
164
+ { full: "Monday", short: "M" },
165
+ { full: "Tuesday", short: "T" },
166
+ { full: "Wednesday", short: "W" },
167
+ { full: "Thursday", short: "T" },
168
+ { full: "Friday", short: "F" },
169
+ { full: "Saturday", short: "S" },
170
+ ] as const
171
+
172
+ const calendarSurface = cva(
173
+ [
174
+ "relative bg-[var(--bh-bg-raised)]",
175
+ "[--bh-date-surface-border:var(--bh-border-subtle)]",
176
+ "shadow-[var(--elevation-date-surface)]",
177
+ ],
178
+ {
179
+ variants: {
180
+ layout: {
181
+ single:
182
+ "flex flex-col items-start rounded-[var(--bh-radius-2xl-12)] p-[var(--bh-space-md-8)]",
183
+ range:
184
+ "inline-flex max-w-[var(--bh-date-range-surface-max-width)] items-start gap-[var(--bh-space-3xl-16)] overflow-auto rounded-[var(--bh-radius-2xl-12)] p-[var(--bh-space-3xl-16)]",
185
+ },
186
+ surfaceSize: {
187
+ calendar: "w-[var(--bh-date-surface-width-calendar)]",
188
+ picker: "w-[var(--bh-date-surface-width-picker)]",
189
+ },
190
+ },
191
+ defaultVariants: {
192
+ layout: "single",
193
+ surfaceSize: "calendar",
194
+ },
195
+ }
196
+ )
197
+
198
+ const pickerTrigger = cva(
199
+ [
200
+ "group/date-trigger relative flex w-[var(--bh-date-trigger-width)] max-w-full items-center gap-[var(--bh-space-md-8)] rounded-[var(--bh-radius-lg-8)]",
201
+ "bg-[var(--bh-interactive-input-default)] text-start outline-none",
202
+ "[--bh-date-trigger-border:var(--bh-border-input)] [--bh-date-trigger-elevation:inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-date-trigger-border),var(--elevation-component-default)]",
203
+ "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)]",
204
+ "text-[var(--bh-content-default)] shadow-[var(--bh-date-trigger-elevation)] transition-[background-color,box-shadow]",
205
+ "hover:bg-[var(--bh-bg-default-hover)] focus-visible:shadow-[var(--elevation-input-focus-ring)]",
206
+ "disabled:pointer-events-none disabled:bg-[var(--bh-interactive-input-disabled)] disabled:text-[var(--bh-content-disabled)] disabled:shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-disabled)]",
207
+ ],
208
+ {
209
+ variants: {
210
+ density: {
211
+ compact: "h-[var(--bh-button-xs-height)] px-[var(--bh-space-md-8)]",
212
+ default: "h-[var(--bh-button-sm-height)] px-[var(--bh-space-lg-10)]",
213
+ comfortable: "h-[var(--bh-date-trigger-height)] px-[var(--bh-space-xl-12)]",
214
+ },
215
+ },
216
+ defaultVariants: {
217
+ density: "comfortable",
218
+ },
219
+ }
220
+ )
221
+
222
+ const monthButton = cva(
223
+ [
224
+ "flex size-[var(--bh-date-nav-button-size)] shrink-0 items-center justify-center rounded-[var(--bh-control-default)]",
225
+ "bg-[var(--bh-interactive-soft-default)] text-[var(--bh-content-default)] outline-none transition-[background-color,box-shadow,color]",
226
+ "hover:bg-[var(--bh-interactive-soft-hover)] focus-visible:shadow-[var(--elevation-button-focus)] disabled:opacity-[var(--bh-opacity-0)]",
227
+ "[&_[data-slot=date-picker-nav-icon]>svg]:size-[var(--bh-date-nav-icon-size)]",
228
+ ]
229
+ )
230
+
231
+ const calendarTitleButton = cva(
232
+ [
233
+ "flex h-[var(--bh-date-title-height)] min-w-0 flex-1 items-center justify-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)]",
234
+ "px-[var(--bh-space-md-8)] text-center outline-none transition-[background-color,box-shadow]",
235
+ "text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)] text-[var(--bh-content-default)]",
236
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:shadow-[var(--elevation-button-focus)]",
237
+ ],
238
+ {
239
+ variants: {
240
+ framed: {
241
+ true:
242
+ "shadow-[inset_0px_0px_0px_var(--bh-border-width-default)_var(--bh-border-subtle)]",
243
+ false: "",
244
+ },
245
+ active: {
246
+ true: "bg-[var(--bh-interactive-ghost-hover)]",
247
+ false: "",
248
+ },
249
+ },
250
+ defaultVariants: {
251
+ active: false,
252
+ framed: false,
253
+ },
254
+ }
255
+ )
256
+
257
+ const dayCellButton = cva(
258
+ [
259
+ "relative flex h-[var(--bh-date-day-cell-height)] min-w-0 flex-1 items-center justify-center text-center outline-none",
260
+ "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)]",
261
+ "transition-[background-color,color,box-shadow] focus-visible:z-[var(--bh-z-raised)] focus-visible:shadow-[var(--elevation-button-focus)]",
262
+ ],
263
+ {
264
+ variants: {
265
+ state: {
266
+ default:
267
+ "rounded-[var(--bh-control-default)] text-[var(--bh-content-default)] hover:bg-[var(--bh-interactive-ghost-hover)]",
268
+ outside:
269
+ "rounded-[var(--bh-control-default)] text-[var(--bh-content-disabled)]",
270
+ event:
271
+ "rounded-[var(--bh-control-default)] text-[var(--bh-content-brand-default)] after:absolute after:bottom-[var(--bh-date-event-dot-offset-bottom)] after:left-1/2 after:size-[var(--bh-date-event-dot-size)] after:-translate-x-1/2 after:rounded-[var(--bh-radius-full)] after:bg-[var(--bh-content-brand-default)] rtl:after:translate-x-1/2",
272
+ selected:
273
+ "rounded-[var(--bh-control-default)] bg-[var(--bh-bg-brand-bold)] text-[var(--bh-content-on-brand)] hover:bg-[var(--bh-bg-brand-bold)] hover:shadow-[inset_0_0_0_var(--bh-border-width-default)_var(--bh-border-focus)]",
274
+ range:
275
+ "rounded-none bg-[var(--bh-bg-brand-subtle)] text-[var(--bh-content-brand-strong)] hover:bg-[var(--bh-bg-brand-subtle)]",
276
+ },
277
+ },
278
+ defaultVariants: {
279
+ state: "default",
280
+ },
281
+ }
282
+ )
283
+
284
+ const dateListItem = cva(
285
+ [
286
+ "flex h-[var(--bh-date-list-item-height)] w-full items-center rounded-[var(--bh-radius-lg-8)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] text-start outline-none",
287
+ "text-[length:var(--bh-text-body-md-medium-font-size)] font-[var(--bh-text-body-md-medium-font-weight)] leading-[var(--bh-text-body-md-medium-line-height)] tracking-[var(--bh-text-body-md-medium-letter-spacing)]",
288
+ "text-[var(--bh-content-default)] transition-colors hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:bg-[var(--bh-interactive-ghost-hover)] disabled:pointer-events-none disabled:text-[var(--bh-content-disabled)]",
289
+ ]
290
+ )
291
+
292
+ function Calendar({
293
+ className,
294
+ defaultValue,
295
+ dir,
296
+ displayMonth,
297
+ events,
298
+ monthYearItems,
299
+ nextLabel = "Next month",
300
+ onDisplayMonthChange,
301
+ onSelect,
302
+ onViewChange,
303
+ previousLabel = "Previous month",
304
+ selectedDate,
305
+ showTime = false,
306
+ surfaceSize = "calendar",
307
+ timeLabel = "Time",
308
+ timeValue = defaultTimeValue,
309
+ title,
310
+ value,
311
+ view,
312
+ ...props
313
+ }: CalendarProps) {
314
+ const controlledValue = value ?? selectedDate
315
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
316
+ const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
317
+ startOfMonth(displayMonth ?? controlledValue ?? defaultValue ?? new Date())
318
+ )
319
+ const [internalView, setInternalView] = React.useState<CalendarView>("days")
320
+ const selected = controlledValue ?? internalValue
321
+ const activeDisplayMonth = startOfMonth(displayMonth ?? internalDisplayMonth)
322
+ const activeView = view ?? internalView
323
+ const eventDates = events ?? []
324
+ const resolvedMonthYearItems =
325
+ monthYearItems ?? createMonthYearItems(activeDisplayMonth)
326
+
327
+ function updateDisplayMonth(nextMonth: Date) {
328
+ const normalized = startOfMonth(nextMonth)
329
+ if (displayMonth === undefined) {
330
+ setInternalDisplayMonth(normalized)
331
+ }
332
+ onDisplayMonthChange?.(normalized)
333
+ }
334
+
335
+ function updateView(nextView: CalendarView) {
336
+ if (view === undefined) {
337
+ setInternalView(nextView)
338
+ }
339
+ onViewChange?.(nextView)
340
+ }
341
+
342
+ function selectDate(nextDate: Date) {
343
+ if (value === undefined && selectedDate === undefined) {
344
+ setInternalValue(nextDate)
345
+ }
346
+ onSelect?.(nextDate)
347
+ }
348
+
349
+ return (
350
+ <div
351
+ data-slot="calendar"
352
+ data-view={activeView}
353
+ dir={dir}
354
+ className={cn(calendarSurface({ layout: "single", surfaceSize }), className)}
355
+ {...props}
356
+ >
357
+ <CalendarMonth
358
+ displayMonth={activeDisplayMonth}
359
+ eventDates={eventDates}
360
+ mode="single"
361
+ className="w-full"
362
+ nextLabel={nextLabel}
363
+ onDisplayMonthChange={updateDisplayMonth}
364
+ onSelectDate={selectDate}
365
+ onToggleView={() =>
366
+ updateView(activeView === "days" ? "month-year" : "days")
367
+ }
368
+ previousLabel={previousLabel}
369
+ selectedDate={selected}
370
+ showTime={showTime && activeView === "days"}
371
+ timeLabel={timeLabel}
372
+ timeValue={timeValue}
373
+ title={title}
374
+ view={activeView}
375
+ />
376
+
377
+ {activeView === "month-year" ? (
378
+ <>
379
+ <div aria-hidden="true" className="h-[var(--bh-date-month-year-spacer-height)] w-full" />
380
+ <MonthYearList
381
+ items={resolvedMonthYearItems}
382
+ onSelect={(item) => {
383
+ if (!item.value) return
384
+ updateDisplayMonth(item.value)
385
+ updateView("days")
386
+ }}
387
+ />
388
+ </>
389
+ ) : null}
390
+ </div>
391
+ )
392
+ }
393
+
394
+ function RangeCalendar({
395
+ className,
396
+ defaultValue = {},
397
+ dir,
398
+ displayMonth,
399
+ nextLabel = "Next month",
400
+ onDisplayMonthChange,
401
+ onPresetSelect,
402
+ onSelect,
403
+ presets,
404
+ previousLabel = "Previous month",
405
+ showPresets,
406
+ showTime = false,
407
+ timeLabel = "Time",
408
+ timeValue = defaultTimeValue,
409
+ type = "double",
410
+ value,
411
+ ...props
412
+ }: RangeCalendarProps) {
413
+ const [internalRange, setInternalRange] = React.useState(defaultValue)
414
+ const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
415
+ startOfMonth(displayMonth ?? defaultValue.from ?? new Date())
416
+ )
417
+ const [presetReferenceDate] = React.useState(() => startOfDay(new Date()))
418
+ const selectedRange = normalizeRange(value ?? internalRange)
419
+ const activeDisplayMonth = startOfMonth(displayMonth ?? internalDisplayMonth)
420
+ const monthsToRender = type === "single" ? 1 : 2
421
+ const shouldShowPresets = showPresets ?? type === "double-with-presets"
422
+ const presetItems = presets ?? createDefaultPresets(presetReferenceDate)
423
+ const monthNodes = Array.from({ length: monthsToRender }, (_, index) =>
424
+ addMonths(activeDisplayMonth, index)
425
+ )
426
+
427
+ function updateDisplayMonth(nextMonth: Date) {
428
+ const normalized = startOfMonth(nextMonth)
429
+ if (displayMonth === undefined) {
430
+ setInternalDisplayMonth(normalized)
431
+ }
432
+ onDisplayMonthChange?.(normalized)
433
+ }
434
+
435
+ function selectRangeDate(nextDate: Date) {
436
+ const nextRange = getNextRange(selectedRange, nextDate)
437
+ if (value === undefined) {
438
+ setInternalRange(nextRange)
439
+ }
440
+ onSelect?.(nextRange)
441
+ }
442
+
443
+ function selectPreset(preset: CalendarPreset) {
444
+ if (preset.disabled) return
445
+ if (preset.range) {
446
+ const nextRange = normalizeRange(preset.range)
447
+ if (value === undefined) {
448
+ setInternalRange(nextRange)
449
+ }
450
+ onSelect?.(nextRange)
451
+ }
452
+ onPresetSelect?.(preset)
453
+ }
454
+
455
+ return (
456
+ <div
457
+ data-slot="range-calendar"
458
+ data-type={type}
459
+ dir={dir}
460
+ className={cn(calendarSurface({ layout: "range" }), className)}
461
+ {...props}
462
+ >
463
+ {shouldShowPresets ? (
464
+ <div
465
+ data-slot="range-calendar-presets"
466
+ className="grid w-[var(--bh-date-presets-width)] shrink-0 border-e border-[var(--bh-border-subtle)] pe-[var(--bh-space-md-8)]"
467
+ >
468
+ {presetItems.map((preset, index) => {
469
+ const disabled =
470
+ Boolean(preset.disabled) ||
471
+ (!preset.range && onPresetSelect === undefined)
472
+
473
+ return (
474
+ <button
475
+ aria-disabled={disabled || undefined}
476
+ className={dateListItem()}
477
+ data-slot="range-calendar-preset"
478
+ disabled={disabled}
479
+ key={preset.id || index}
480
+ type="button"
481
+ onClick={() => selectPreset(preset)}
482
+ >
483
+ <span dir="auto" className="min-w-0 truncate">
484
+ {preset.label}
485
+ </span>
486
+ </button>
487
+ )
488
+ })}
489
+ </div>
490
+ ) : null}
491
+
492
+ <div
493
+ data-slot="range-calendar-months"
494
+ className="flex min-w-max gap-[var(--bh-space-3xl-16)]"
495
+ >
496
+ {monthNodes.map((month, index) => (
497
+ <CalendarMonth
498
+ displayMonth={month}
499
+ key={month.toISOString()}
500
+ mode="range"
501
+ className="w-[var(--bh-date-range-month-width)]"
502
+ nextLabel={nextLabel}
503
+ onDisplayMonthChange={updateDisplayMonth}
504
+ onSelectDate={selectRangeDate}
505
+ previousLabel={previousLabel}
506
+ range={selectedRange}
507
+ showNextButton={index === monthNodes.length - 1}
508
+ showPreviousButton={index === 0}
509
+ showTime={showTime}
510
+ timeLabel={timeLabel}
511
+ timeValue={timeValue}
512
+ />
513
+ ))}
514
+ </div>
515
+ </div>
516
+ )
517
+ }
518
+
519
+ function DatePicker({
520
+ calendarProps,
521
+ className,
522
+ defaultOpen = false,
523
+ defaultValue,
524
+ density,
525
+ dir,
526
+ disabled,
527
+ icon,
528
+ onOpenChange,
529
+ onSelect,
530
+ open,
531
+ placeholder = "Select date",
532
+ triggerClassName,
533
+ triggerProps,
534
+ value,
535
+ ...props
536
+ }: DatePickerProps) {
537
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
538
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
539
+ const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
540
+ startOfMonth(value ?? defaultValue ?? new Date())
541
+ )
542
+ const selected = value ?? internalValue
543
+ const isOpen = open ?? internalOpen
544
+ const selectedTime = selected?.getTime()
545
+
546
+ React.useEffect(() => {
547
+ if (!isOpen || calendarProps?.displayMonth || !selected) return
548
+
549
+ setInternalDisplayMonth(startOfMonth(selected))
550
+ }, [calendarProps?.displayMonth, isOpen, selectedTime])
551
+
552
+ function updateOpen(nextOpen: boolean) {
553
+ if (open === undefined) {
554
+ setInternalOpen(nextOpen)
555
+ }
556
+ onOpenChange?.(nextOpen)
557
+ }
558
+
559
+ function selectDate(nextDate: Date) {
560
+ if (value === undefined) {
561
+ setInternalValue(nextDate)
562
+ }
563
+ onSelect?.(nextDate)
564
+ }
565
+
566
+ function updateDisplayMonth(nextMonth: Date) {
567
+ const normalized = startOfMonth(nextMonth)
568
+ if (calendarProps?.displayMonth === undefined) {
569
+ setInternalDisplayMonth(normalized)
570
+ }
571
+ calendarProps?.onDisplayMonthChange?.(normalized)
572
+ }
573
+
574
+ return (
575
+ <Popover
576
+ open={isOpen}
577
+ onOpenChange={updateOpen}
578
+ data-open={isOpen ? "true" : "false"}
579
+ data-slot="date-picker"
580
+ dir={dir}
581
+ className={cn("relative w-[var(--bh-date-picker-width)] max-w-full", className)}
582
+ {...props}
583
+ >
584
+ <PopoverTrigger asChild>
585
+ <DatePickerTrigger
586
+ {...triggerProps}
587
+ disabled={disabled ?? triggerProps?.disabled}
588
+ density={density}
589
+ icon={icon}
590
+ open={isOpen}
591
+ placeholder={placeholder}
592
+ value={selected ? formatCalendarDate(selected, { month: "long" }) : undefined}
593
+ className={cn(triggerClassName, triggerProps?.className)}
594
+ />
595
+ </PopoverTrigger>
596
+
597
+ <PopoverContent
598
+ align="center"
599
+ showArrow={false}
600
+ size="auto"
601
+ className="w-auto bg-transparent p-0 shadow-none"
602
+ >
603
+ <Calendar
604
+ {...calendarProps}
605
+ defaultValue={defaultValue}
606
+ displayMonth={calendarProps?.displayMonth ?? internalDisplayMonth}
607
+ selectedDate={selected}
608
+ surfaceSize="picker"
609
+ className={calendarProps?.className}
610
+ dir={calendarProps?.dir ?? dir}
611
+ onDisplayMonthChange={updateDisplayMonth}
612
+ onSelect={(nextDate) => {
613
+ selectDate(nextDate)
614
+ calendarProps?.onSelect?.(nextDate)
615
+ }}
616
+ />
617
+ </PopoverContent>
618
+ </Popover>
619
+ )
620
+ }
621
+
622
+ function DateRangePicker({
623
+ calendarProps,
624
+ className,
625
+ defaultOpen = false,
626
+ defaultValue,
627
+ density,
628
+ dir,
629
+ disabled,
630
+ icon,
631
+ onOpenChange,
632
+ onSelect,
633
+ open,
634
+ placeholder = "Select date range",
635
+ triggerClassName,
636
+ triggerProps,
637
+ type = "double",
638
+ value,
639
+ ...props
640
+ }: DateRangePickerProps) {
641
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
642
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
643
+ const selectedRange = normalizeRange(value ?? internalValue ?? {})
644
+ const isOpen = open ?? internalOpen
645
+ const [internalDisplayMonth, setInternalDisplayMonth] = React.useState(() =>
646
+ startOfMonth(selectedRange.from ?? defaultValue?.from ?? new Date())
647
+ )
648
+ const selectedRangeFromTime = selectedRange.from?.getTime()
649
+
650
+ React.useEffect(() => {
651
+ if (!isOpen || calendarProps?.displayMonth || !selectedRange.from) return
652
+
653
+ setInternalDisplayMonth(startOfMonth(selectedRange.from))
654
+ }, [calendarProps?.displayMonth, isOpen, selectedRangeFromTime])
655
+
656
+ function updateOpen(nextOpen: boolean) {
657
+ if (open === undefined) {
658
+ setInternalOpen(nextOpen)
659
+ }
660
+ onOpenChange?.(nextOpen)
661
+ }
662
+
663
+ function selectRange(nextRange: CalendarRange) {
664
+ if (value === undefined) {
665
+ setInternalValue(nextRange)
666
+ }
667
+ onSelect?.(nextRange)
668
+ }
669
+
670
+ function updateDisplayMonth(nextMonth: Date) {
671
+ const normalized = startOfMonth(nextMonth)
672
+ if (calendarProps?.displayMonth === undefined) {
673
+ setInternalDisplayMonth(normalized)
674
+ }
675
+ calendarProps?.onDisplayMonthChange?.(normalized)
676
+ }
677
+
678
+ return (
679
+ <Popover
680
+ open={isOpen}
681
+ onOpenChange={updateOpen}
682
+ data-open={isOpen ? "true" : "false"}
683
+ data-slot="date-range-picker"
684
+ data-type={type}
685
+ dir={dir}
686
+ className={cn("relative w-[var(--bh-date-picker-width)] max-w-full", className)}
687
+ {...props}
688
+ >
689
+ <PopoverTrigger asChild>
690
+ <DatePickerTrigger
691
+ {...triggerProps}
692
+ disabled={disabled ?? triggerProps?.disabled}
693
+ density={density}
694
+ icon={icon}
695
+ open={isOpen}
696
+ placeholder={placeholder}
697
+ value={formatCalendarRange(selectedRange)}
698
+ className={cn(triggerClassName, triggerProps?.className)}
699
+ />
700
+ </PopoverTrigger>
701
+
702
+ <PopoverContent
703
+ align="center"
704
+ showArrow={false}
705
+ size="auto"
706
+ className="w-auto bg-transparent p-0 shadow-none"
707
+ >
708
+ <RangeCalendar
709
+ {...calendarProps}
710
+ defaultValue={defaultValue ?? {}}
711
+ displayMonth={calendarProps?.displayMonth ?? internalDisplayMonth}
712
+ type={type}
713
+ value={selectedRange}
714
+ className={calendarProps?.className}
715
+ dir={calendarProps?.dir ?? dir}
716
+ onDisplayMonthChange={updateDisplayMonth}
717
+ onSelect={(nextRange) => {
718
+ selectRange(nextRange)
719
+ calendarProps?.onSelect?.(nextRange)
720
+ }}
721
+ />
722
+ </PopoverContent>
723
+ </Popover>
724
+ )
725
+ }
726
+
727
+ type DatePickerTriggerProps = Omit<
728
+ React.ComponentProps<"button">,
729
+ "children" | "value"
730
+ > & {
731
+ density?: ControlDensity
732
+ icon?: React.ReactNode
733
+ open?: boolean
734
+ placeholder: React.ReactNode
735
+ value?: React.ReactNode
736
+ }
737
+
738
+ function DatePickerTrigger({
739
+ className,
740
+ density,
741
+ disabled,
742
+ icon,
743
+ onClick,
744
+ open,
745
+ placeholder,
746
+ type = "button",
747
+ value,
748
+ ...props
749
+ }: DatePickerTriggerProps) {
750
+ const hasValue = hasRenderableContent(value)
751
+
752
+ return (
753
+ <button
754
+ aria-expanded={open || undefined}
755
+ data-date-picker-trigger=""
756
+ data-density={density ?? "comfortable"}
757
+ data-slot="date-picker-trigger"
758
+ disabled={disabled}
759
+ type={type}
760
+ className={cn(pickerTrigger({ density }), className)}
761
+ onClick={onClick}
762
+ {...props}
763
+ >
764
+ <span
765
+ aria-hidden="true"
766
+ data-slot="date-picker-trigger-icon"
767
+ className="flex size-[var(--bh-date-trigger-icon-slot-size)] shrink-0 items-center justify-center text-[var(--bh-content-muted)] group-disabled/date-trigger:text-[var(--bh-content-disabled)] [&_svg:not([class*='size-'])]:size-[var(--bh-date-trigger-icon-size)]"
768
+ >
769
+ {icon ?? <CalendarDaysIcon strokeWidth="var(--bh-icon-stroke-180)" />}
770
+ </span>
771
+ <span
772
+ data-slot="date-picker-trigger-value"
773
+ dir="auto"
774
+ className={cn(
775
+ "min-w-0 flex-1 truncate text-start",
776
+ hasValue ? "text-[var(--bh-content-default)]" : "text-[var(--bh-content-muted)]",
777
+ disabled && "text-[var(--bh-content-disabled)]"
778
+ )}
779
+ >
780
+ {hasValue ? value : placeholder}
781
+ </span>
782
+ </button>
783
+ )
784
+ }
785
+
786
+ function CalendarMonth({
787
+ className,
788
+ displayMonth,
789
+ eventDates,
790
+ mode,
791
+ nextLabel,
792
+ onDisplayMonthChange,
793
+ onSelectDate,
794
+ onToggleView,
795
+ previousLabel,
796
+ range,
797
+ selectedDate,
798
+ showNextButton = true,
799
+ showPreviousButton = true,
800
+ showTime,
801
+ timeLabel,
802
+ timeValue,
803
+ title,
804
+ view = "days",
805
+ }: {
806
+ className?: string
807
+ displayMonth: Date
808
+ eventDates?: Date[]
809
+ mode: CalendarMode
810
+ nextLabel: string
811
+ onDisplayMonthChange: (month: Date) => void
812
+ onSelectDate: (date: Date) => void
813
+ onToggleView?: () => void
814
+ previousLabel: string
815
+ range?: CalendarRange
816
+ selectedDate?: Date
817
+ showNextButton?: boolean
818
+ showPreviousButton?: boolean
819
+ showTime?: boolean
820
+ timeLabel: React.ReactNode
821
+ timeValue: CalendarTimeValue
822
+ title?: React.ReactNode
823
+ view?: CalendarView
824
+ }) {
825
+ const cells = React.useMemo(() => getCalendarCells(displayMonth), [displayMonth])
826
+ const gridRef = React.useRef<HTMLDivElement>(null)
827
+ const titleId = React.useId()
828
+ const pendingFocusDateRef = React.useRef<Date | undefined>(undefined)
829
+ const monthTitle = title ?? formatMonthTitle(displayMonth)
830
+ const hasMonthToggle = mode === "single"
831
+
832
+ React.useLayoutEffect(() => {
833
+ if (!pendingFocusDateRef.current) return
834
+
835
+ focusCalendarDate(pendingFocusDateRef.current)
836
+ pendingFocusDateRef.current = undefined
837
+ }, [cells])
838
+
839
+ function focusCalendarDate(date: Date) {
840
+ const target = gridRef.current?.querySelector<HTMLButtonElement>(
841
+ `[data-calendar-date="${toCalendarDateId(date)}"]`
842
+ )
843
+
844
+ target?.focus()
845
+ }
846
+
847
+ function handleKeyboardNavigateDate(nextDate: Date) {
848
+ if (isSameMonth(nextDate, displayMonth)) {
849
+ focusCalendarDate(nextDate)
850
+ return
851
+ }
852
+
853
+ pendingFocusDateRef.current = nextDate
854
+ onDisplayMonthChange(startOfMonth(nextDate))
855
+ }
856
+
857
+ return (
858
+ <div
859
+ data-slot="calendar-month"
860
+ className={cn(
861
+ "flex shrink-0 flex-col items-start gap-[var(--bh-space-md-8)]",
862
+ className
863
+ )}
864
+ >
865
+ <div
866
+ data-slot="calendar-header"
867
+ className="flex w-full items-center gap-[var(--bh-space-md-8)]"
868
+ >
869
+ <button
870
+ aria-label={previousLabel}
871
+ disabled={!showPreviousButton || view === "month-year"}
872
+ type="button"
873
+ className={monthButton()}
874
+ onClick={() => onDisplayMonthChange(addMonths(displayMonth, -1))}
875
+ >
876
+ <span
877
+ aria-hidden="true"
878
+ data-slot="date-picker-nav-icon"
879
+ className="inline-flex size-[var(--bh-date-nav-icon-slot-size)] items-center justify-center"
880
+ >
881
+ <ChevronLeftIcon data-rtl-flip="true" />
882
+ </span>
883
+ </button>
884
+
885
+ {hasMonthToggle ? (
886
+ <button
887
+ aria-expanded={view === "month-year"}
888
+ data-slot="calendar-title"
889
+ id={titleId}
890
+ type="button"
891
+ className={calendarTitleButton({
892
+ active: view === "month-year",
893
+ framed: true,
894
+ })}
895
+ onClick={onToggleView}
896
+ >
897
+ <span className="min-w-0 truncate">{monthTitle}</span>
898
+ {view === "month-year" ? (
899
+ <ChevronUpIcon aria-hidden="true" className="size-[var(--bh-date-title-chevron-size)]" />
900
+ ) : (
901
+ <ChevronDownIcon aria-hidden="true" className="size-[var(--bh-date-title-chevron-size)]" />
902
+ )}
903
+ </button>
904
+ ) : (
905
+ <div
906
+ data-slot="calendar-title"
907
+ id={titleId}
908
+ className={calendarTitleButton({ framed: false })}
909
+ >
910
+ <span className="min-w-0 truncate">{monthTitle}</span>
911
+ </div>
912
+ )}
913
+
914
+ <button
915
+ aria-label={nextLabel}
916
+ disabled={!showNextButton || view === "month-year"}
917
+ type="button"
918
+ className={monthButton()}
919
+ onClick={() => onDisplayMonthChange(addMonths(displayMonth, 1))}
920
+ >
921
+ <span
922
+ aria-hidden="true"
923
+ data-slot="date-picker-nav-icon"
924
+ className="inline-flex size-[var(--bh-date-nav-icon-slot-size)] items-center justify-center"
925
+ >
926
+ <ChevronRightIcon data-rtl-flip="true" />
927
+ </span>
928
+ </button>
929
+ </div>
930
+
931
+ {view === "days" ? (
932
+ <>
933
+ <div
934
+ aria-colcount={7}
935
+ aria-labelledby={titleId}
936
+ aria-rowcount={Math.ceil(cells.length / 7) + 1}
937
+ data-slot="calendar-grid"
938
+ role="grid"
939
+ ref={gridRef}
940
+ className="grid w-full gap-[var(--bh-space-md-8)]"
941
+ >
942
+ <div
943
+ data-slot="calendar-weekdays"
944
+ role="row"
945
+ className="grid w-full grid-cols-7"
946
+ >
947
+ {defaultDayLabels.map((day) => (
948
+ <div
949
+ aria-label={day.full}
950
+ className="flex h-[var(--bh-date-day-cell-height)] min-w-0 items-center justify-center rounded-[var(--bh-radius-lg-8)] text-center 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)]"
951
+ key={day.full}
952
+ role="columnheader"
953
+ >
954
+ {day.short}
955
+ </div>
956
+ ))}
957
+ </div>
958
+
959
+ <div role="rowgroup" className="grid w-full">
960
+ {chunkCalendarRows(cells).map((row, rowIndex) => (
961
+ <div
962
+ className="grid w-full grid-cols-7"
963
+ key={row[0]?.date.toISOString() ?? rowIndex}
964
+ role="row"
965
+ >
966
+ {row.map((cell) => (
967
+ <CalendarDayCell
968
+ cell={cell}
969
+ displayMonth={displayMonth}
970
+ eventDates={eventDates}
971
+ key={cell.date.toISOString()}
972
+ mode={mode}
973
+ onKeyboardNavigateDate={handleKeyboardNavigateDate}
974
+ onSelectDate={onSelectDate}
975
+ range={range}
976
+ selectedDate={selectedDate}
977
+ />
978
+ ))}
979
+ </div>
980
+ ))}
981
+ </div>
982
+ </div>
983
+
984
+ {showTime ? (
985
+ <CalendarTime
986
+ label={timeLabel}
987
+ value={timeValue}
988
+ />
989
+ ) : null}
990
+ </>
991
+ ) : null}
992
+ </div>
993
+ )
994
+ }
995
+
996
+ function chunkCalendarRows(cells: CalendarCell[]) {
997
+ const rows: CalendarCell[][] = []
998
+
999
+ for (let index = 0; index < cells.length; index += 7) {
1000
+ rows.push(cells.slice(index, index + 7))
1001
+ }
1002
+
1003
+ return rows
1004
+ }
1005
+
1006
+ function CalendarDayCell({
1007
+ cell,
1008
+ displayMonth,
1009
+ eventDates,
1010
+ mode,
1011
+ onKeyboardNavigateDate,
1012
+ onSelectDate,
1013
+ range,
1014
+ selectedDate,
1015
+ }: {
1016
+ cell: CalendarCell
1017
+ displayMonth: Date
1018
+ eventDates?: Date[]
1019
+ mode: CalendarMode
1020
+ onKeyboardNavigateDate: (date: Date) => void
1021
+ onSelectDate: (date: Date) => void
1022
+ range?: CalendarRange
1023
+ selectedDate?: Date
1024
+ }) {
1025
+ const { inRange, outside, rangeEndpoint, selected, state } = getCalendarDayState({
1026
+ date: cell.date,
1027
+ displayMonth,
1028
+ eventDates,
1029
+ mode,
1030
+ range,
1031
+ selectedDate,
1032
+ })
1033
+
1034
+ function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
1035
+ const nextDate = getCalendarKeyboardTarget(cell.date, event.key)
1036
+
1037
+ if (!nextDate) return
1038
+
1039
+ event.preventDefault()
1040
+ onKeyboardNavigateDate(nextDate)
1041
+ }
1042
+
1043
+ return (
1044
+ <button
1045
+ aria-disabled={outside || undefined}
1046
+ aria-label={formatCalendarDate(cell.date, { month: "long" })}
1047
+ aria-selected={selected || undefined}
1048
+ data-calendar-date={toCalendarDateId(cell.date)}
1049
+ data-outside={outside ? "true" : undefined}
1050
+ data-range-endpoint={rangeEndpoint ? "true" : undefined}
1051
+ data-slot="calendar-cell"
1052
+ disabled={outside}
1053
+ role="gridcell"
1054
+ type="button"
1055
+ className={cn(
1056
+ dayCellButton({ state }),
1057
+ inRange && cell.weekday === 0 && "rounded-s-[var(--bh-control-default)]",
1058
+ inRange && cell.weekday === 6 && "rounded-e-[var(--bh-control-default)]"
1059
+ )}
1060
+ onClick={() => onSelectDate(cell.date)}
1061
+ onKeyDown={handleKeyDown}
1062
+ >
1063
+ {cell.date.getDate()}
1064
+ </button>
1065
+ )
1066
+ }
1067
+
1068
+ function CalendarTime({
1069
+ label,
1070
+ value,
1071
+ }: {
1072
+ label: React.ReactNode
1073
+ value: CalendarTimeValue
1074
+ }) {
1075
+ const resolved = {
1076
+ ...defaultTimeValue,
1077
+ ...value,
1078
+ }
1079
+
1080
+ return (
1081
+ <div
1082
+ data-slot="calendar-time"
1083
+ className="flex w-full items-center gap-[var(--bh-space-5xl-24)] ps-[var(--bh-space-md-8)]"
1084
+ >
1085
+ <span
1086
+ data-slot="calendar-time-label"
1087
+ dir="auto"
1088
+ className="shrink-0 text-[length:var(--bh-text-body-2xs-regular-font-size)] font-[var(--bh-text-body-2xs-regular-font-weight)] leading-[var(--bh-text-body-2xs-regular-line-height)] tracking-[var(--bh-text-body-2xs-regular-letter-spacing)] text-[var(--bh-content-subtle)]"
1089
+ >
1090
+ {label}
1091
+ </span>
1092
+ <span
1093
+ data-slot="calendar-time-value"
1094
+ className="flex min-h-[var(--bh-date-time-input-min-height)] min-w-0 flex-1 items-center gap-[var(--bh-space-xs-4)] rounded-[var(--bh-radius-lg-8)] bg-[var(--bh-interactive-input-default)] px-[var(--bh-space-md-8)] py-[var(--bh-space-sm-6)] shadow-[var(--elevation-date-time-input)]"
1095
+ >
1096
+ <CalendarTimeSegment active>{resolved.hours}</CalendarTimeSegment>
1097
+ <span className="text-[var(--bh-content-subtle)]">:</span>
1098
+ <CalendarTimeSegment>{resolved.minutes}</CalendarTimeSegment>
1099
+ <CalendarTimeSegment>{resolved.period}</CalendarTimeSegment>
1100
+ <CalendarTimeSegment subtle>{resolved.zone}</CalendarTimeSegment>
1101
+ </span>
1102
+ </div>
1103
+ )
1104
+ }
1105
+
1106
+ function CalendarTimeSegment({
1107
+ active,
1108
+ children,
1109
+ subtle,
1110
+ }: {
1111
+ active?: boolean
1112
+ children: React.ReactNode
1113
+ subtle?: boolean
1114
+ }) {
1115
+ return (
1116
+ <span
1117
+ data-active={active ? "true" : undefined}
1118
+ className={cn(
1119
+ "inline-flex min-w-0 items-center justify-center rounded-[var(--bh-radius-sm-4)] px-[var(--bh-space-xxs-2)] text-[length:var(--bh-text-body-2xs-medium-font-size)] font-[var(--bh-text-body-2xs-medium-font-weight)] leading-[var(--bh-text-body-2xs-medium-line-height)] tracking-[var(--bh-text-body-2xs-medium-letter-spacing)]",
1120
+ active && "bg-[var(--bh-interactive-ghost-hover)]",
1121
+ subtle ? "text-[var(--bh-content-subtle)]" : "text-[var(--bh-content-default)]"
1122
+ )}
1123
+ >
1124
+ {children}
1125
+ </span>
1126
+ )
1127
+ }
1128
+
1129
+ function MonthYearList({
1130
+ items,
1131
+ onSelect,
1132
+ }: {
1133
+ items: CalendarMonthYearItem[]
1134
+ onSelect: (item: CalendarMonthYearItem) => void
1135
+ }) {
1136
+ return (
1137
+ <div
1138
+ data-slot="calendar-month-year-list"
1139
+ className="absolute bottom-[var(--bh-space-md-8)] left-1/2 top-[var(--bh-date-month-year-list-top)] flex w-[var(--bh-date-month-year-list-width)] -translate-x-1/2 flex-col justify-center overflow-hidden rounded-[var(--bh-radius-md-6)] bg-[var(--bh-bg-default)] p-[var(--bh-space-md-8)] shadow-[var(--elevation-date-month-year-list)] rtl:translate-x-1/2"
1140
+ >
1141
+ {items.map((item, index) => (
1142
+ <button
1143
+ className={cn(
1144
+ "grid h-[var(--bh-date-month-year-item-height)] grid-cols-2 items-center rounded-[var(--bh-radius-md-6)] px-[var(--bh-space-md-8)] py-[var(--bh-space-xs-4)] text-center outline-none transition-colors",
1145
+ "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)]",
1146
+ "hover:bg-[var(--bh-interactive-ghost-hover)] focus-visible:bg-[var(--bh-interactive-ghost-hover)] disabled:text-[var(--bh-content-disabled)]",
1147
+ item.selected && "bg-[var(--bh-interactive-ghost-hover)]"
1148
+ )}
1149
+ data-selected={item.selected ? "true" : undefined}
1150
+ data-slot="calendar-month-year-item"
1151
+ disabled={item.disabled || !item.value}
1152
+ key={item.id || index}
1153
+ onClick={() => onSelect(item)}
1154
+ type="button"
1155
+ >
1156
+ <span className="truncate">{item.month}</span>
1157
+ <span className="truncate">{item.year}</span>
1158
+ </button>
1159
+ ))}
1160
+ </div>
1161
+ )
1162
+ }
1163
+
1164
+ function startOfDay(date: Date) {
1165
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1166
+ }
1167
+
1168
+ function addDays(date: Date, amount: number) {
1169
+ const next = startOfDay(date)
1170
+ next.setDate(next.getDate() + amount)
1171
+ return next
1172
+ }
1173
+
1174
+ function createDefaultPresets(today: Date): CalendarPreset[] {
1175
+ const quarterStartMonth = Math.floor(today.getMonth() / 3) * 3
1176
+
1177
+ return [
1178
+ { id: "today", label: "Today", range: { from: today, to: today } },
1179
+ {
1180
+ id: "last-7-days",
1181
+ label: "Last 7 days",
1182
+ range: { from: addDays(today, -6), to: today },
1183
+ },
1184
+ {
1185
+ id: "last-14-days",
1186
+ label: "Last 14 days",
1187
+ range: { from: addDays(today, -13), to: today },
1188
+ },
1189
+ {
1190
+ id: "last-30-days",
1191
+ label: "Last 30 days",
1192
+ range: { from: addDays(today, -29), to: today },
1193
+ },
1194
+ {
1195
+ id: "last-3-months",
1196
+ label: "Last 3 months",
1197
+ range: { from: addMonths(today, -3), to: today },
1198
+ },
1199
+ {
1200
+ id: "last-12-months",
1201
+ label: "Last 12 months",
1202
+ range: { from: addMonths(today, -12), to: today },
1203
+ },
1204
+ {
1205
+ id: "quarter-to-date",
1206
+ label: "Quarter to date",
1207
+ range: {
1208
+ from: new Date(today.getFullYear(), quarterStartMonth, 1),
1209
+ to: today,
1210
+ },
1211
+ },
1212
+ {
1213
+ id: "year-to-date",
1214
+ label: "Year to date",
1215
+ range: { from: new Date(today.getFullYear(), 0, 1), to: today },
1216
+ },
1217
+ ]
1218
+ }
1219
+
1220
+ function createMonthYearItems(
1221
+ displayMonth: Date
1222
+ ): CalendarMonthYearItem[] {
1223
+ return Array.from({ length: 7 }, (_, index) => {
1224
+ const value = addMonths(displayMonth, index - 3)
1225
+
1226
+ return {
1227
+ id: `${value.getFullYear()}-${value.getMonth()}`,
1228
+ month: new Intl.DateTimeFormat(undefined, { month: "long" }).format(value),
1229
+ selected: index === 3,
1230
+ value,
1231
+ year: String(value.getFullYear()),
1232
+ }
1233
+ })
1234
+ }
1235
+
1236
+ function getCurrentTimeValue(): Required<CalendarTimeValue> {
1237
+ const now = new Date()
1238
+ const timeParts = new Intl.DateTimeFormat("en-US", {
1239
+ hour: "2-digit",
1240
+ hour12: true,
1241
+ minute: "2-digit",
1242
+ }).formatToParts(now)
1243
+ const zoneParts = new Intl.DateTimeFormat("en-US", {
1244
+ timeZoneName: "short",
1245
+ }).formatToParts(now)
1246
+ const getPart = (parts: Intl.DateTimeFormatPart[], type: string) =>
1247
+ parts.find((part) => part.type === type)?.value ?? ""
1248
+
1249
+ return {
1250
+ hours: getPart(timeParts, "hour"),
1251
+ minutes: getPart(timeParts, "minute"),
1252
+ period: getPart(timeParts, "dayPeriod"),
1253
+ zone: getPart(zoneParts, "timeZoneName") || "UTC",
1254
+ }
1255
+ }
1256
+
1257
+ function hasRenderableContent(content: React.ReactNode) {
1258
+ return (
1259
+ content !== undefined &&
1260
+ content !== null &&
1261
+ content !== false &&
1262
+ content !== ""
1263
+ )
1264
+ }
1265
+
1266
+ export {
1267
+ Calendar,
1268
+ DatePicker,
1269
+ DatePickerTrigger,
1270
+ DateRangePicker,
1271
+ RangeCalendar,
1272
+ calendarSurface,
1273
+ pickerTrigger,
1274
+ }
1275
+ export type {
1276
+ CalendarMonthYearItem,
1277
+ CalendarPreset,
1278
+ CalendarProps,
1279
+ CalendarRange,
1280
+ CalendarTimeValue,
1281
+ CalendarView,
1282
+ DatePickerDirection,
1283
+ DatePickerProps,
1284
+ DatePickerTriggerProps,
1285
+ DateRangePickerProps,
1286
+ ControlDensity,
1287
+ RangeCalendarProps,
1288
+ RangeCalendarType,
1289
+ }