@chassis-ui/react 0.0.0-20260831090525

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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/index.d.ts +5909 -0
  4. package/dist/index.js +2 -0
  5. package/dist/style.css +1 -0
  6. package/package.json +114 -0
  7. package/src/components/accordion/Accordion.tsx +190 -0
  8. package/src/components/accordion/AccordionBody.tsx +46 -0
  9. package/src/components/accordion/AccordionHeader.tsx +21 -0
  10. package/src/components/accordion/AccordionItem.tsx +59 -0
  11. package/src/components/accordion/context.ts +8 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/autocomplete/Autocomplete.tsx +464 -0
  14. package/src/components/autocomplete/AutocompleteGroup.tsx +20 -0
  15. package/src/components/autocomplete/AutocompleteItem.tsx +18 -0
  16. package/src/components/autocomplete/index.ts +6 -0
  17. package/src/components/avatar/Avatar.tsx +134 -0
  18. package/src/components/avatar/AvatarImage.tsx +19 -0
  19. package/src/components/avatar/AvatarStack.tsx +98 -0
  20. package/src/components/avatar/index.ts +6 -0
  21. package/src/components/badge/Badge.tsx +94 -0
  22. package/src/components/badge/index.ts +2 -0
  23. package/src/components/breadcrumb/Breadcrumb.tsx +59 -0
  24. package/src/components/breadcrumb/BreadcrumbItem.tsx +38 -0
  25. package/src/components/breadcrumb/index.ts +4 -0
  26. package/src/components/button/Button.tsx +229 -0
  27. package/src/components/button/index.ts +2 -0
  28. package/src/components/button-group/ButtonGroup.tsx +55 -0
  29. package/src/components/button-group/ButtonToolbar.tsx +47 -0
  30. package/src/components/button-group/index.ts +4 -0
  31. package/src/components/calendar/Calendar.scss +57 -0
  32. package/src/components/calendar/Calendar.tsx +324 -0
  33. package/src/components/calendar/CalendarMonthBlock.tsx +57 -0
  34. package/src/components/calendar/CalendarMonthGrid.tsx +92 -0
  35. package/src/components/calendar/CalendarMonthYearPicker.tsx +162 -0
  36. package/src/components/calendar/CalendarNavButton.tsx +24 -0
  37. package/src/components/calendar/CalendarWeekGrid.tsx +58 -0
  38. package/src/components/calendar/CalendarYearGrid.tsx +126 -0
  39. package/src/components/calendar/RangeCalendar.scss +95 -0
  40. package/src/components/calendar/RangeCalendar.tsx +361 -0
  41. package/src/components/calendar/index.ts +5 -0
  42. package/src/components/card/Card.tsx +173 -0
  43. package/src/components/card/CardBody.tsx +75 -0
  44. package/src/components/card/CardFooter.tsx +47 -0
  45. package/src/components/card/CardGroup.tsx +51 -0
  46. package/src/components/card/CardHeader.tsx +47 -0
  47. package/src/components/card/CardImage.tsx +70 -0
  48. package/src/components/card/CardImageOverlay.tsx +47 -0
  49. package/src/components/card/CardLink.tsx +40 -0
  50. package/src/components/card/CardSubtitle.tsx +47 -0
  51. package/src/components/card/CardText.tsx +47 -0
  52. package/src/components/card/CardTitle.tsx +47 -0
  53. package/src/components/card/index.ts +22 -0
  54. package/src/components/carousel/Carousel.tsx +742 -0
  55. package/src/components/carousel/CarouselControlButton.tsx +74 -0
  56. package/src/components/carousel/CarouselControlNext.tsx +22 -0
  57. package/src/components/carousel/CarouselControlPrev.tsx +22 -0
  58. package/src/components/carousel/CarouselIndicators.tsx +44 -0
  59. package/src/components/carousel/CarouselInner.tsx +56 -0
  60. package/src/components/carousel/CarouselItem.tsx +51 -0
  61. package/src/components/carousel/CarouselOverlay.tsx +51 -0
  62. package/src/components/carousel/CarouselPlayPause.tsx +61 -0
  63. package/src/components/carousel/carouselEngine.ts +170 -0
  64. package/src/components/carousel/context.ts +29 -0
  65. package/src/components/carousel/index.ts +21 -0
  66. package/src/components/checkbox/Checkbox.tsx +236 -0
  67. package/src/components/checkbox/CheckboxGroup.tsx +145 -0
  68. package/src/components/checkbox/context.ts +14 -0
  69. package/src/components/checkbox/index.ts +4 -0
  70. package/src/components/chip/Chip.tsx +183 -0
  71. package/src/components/chip/index.ts +2 -0
  72. package/src/components/chip-input/ChipInput.tsx +363 -0
  73. package/src/components/chip-input/ChipList.tsx +95 -0
  74. package/src/components/chip-input/index.ts +2 -0
  75. package/src/components/close-button/CloseButton.tsx +205 -0
  76. package/src/components/close-button/index.ts +2 -0
  77. package/src/components/collapse/Collapse.tsx +128 -0
  78. package/src/components/collapse/index.ts +2 -0
  79. package/src/components/color-input/ColorInput.tsx +106 -0
  80. package/src/components/color-input/index.ts +2 -0
  81. package/src/components/combobox/Combobox.tsx +320 -0
  82. package/src/components/combobox/ComboboxGroup.tsx +27 -0
  83. package/src/components/combobox/ComboboxItem.tsx +44 -0
  84. package/src/components/combobox/ComboboxListBox.tsx +98 -0
  85. package/src/components/combobox/index.ts +6 -0
  86. package/src/components/datepicker/CalendarToggleButton.tsx +58 -0
  87. package/src/components/datepicker/ClearButton.tsx +35 -0
  88. package/src/components/datepicker/DateField.tsx +54 -0
  89. package/src/components/datepicker/DatePicker.scss +51 -0
  90. package/src/components/datepicker/DatePicker.tsx +574 -0
  91. package/src/components/datepicker/DateRangePicker.scss +11 -0
  92. package/src/components/datepicker/DateRangePicker.tsx +343 -0
  93. package/src/components/datepicker/index.ts +4 -0
  94. package/src/components/datepicker/renderDatePickerShell.tsx +104 -0
  95. package/src/components/drawer/Drawer.tsx +211 -0
  96. package/src/components/drawer/DrawerBody.tsx +47 -0
  97. package/src/components/drawer/DrawerFooter.tsx +51 -0
  98. package/src/components/drawer/DrawerHeader.tsx +60 -0
  99. package/src/components/drawer/DrawerTitle.tsx +49 -0
  100. package/src/components/drawer/index.ts +10 -0
  101. package/src/components/file-input/FileInput.tsx +100 -0
  102. package/src/components/file-input/index.ts +2 -0
  103. package/src/components/flex/Flex.tsx +134 -0
  104. package/src/components/flex/index.ts +2 -0
  105. package/src/components/floating-input/FloatingInput.tsx +94 -0
  106. package/src/components/floating-input/index.ts +2 -0
  107. package/src/components/form/Form.tsx +26 -0
  108. package/src/components/form/FormFeedback.tsx +64 -0
  109. package/src/components/form/FormHelp.tsx +46 -0
  110. package/src/components/form/FormLabel.tsx +26 -0
  111. package/src/components/form/index.ts +8 -0
  112. package/src/components/form/renderFormCheck.tsx +86 -0
  113. package/src/components/form-field/FormField.tsx +84 -0
  114. package/src/components/form-field/index.ts +2 -0
  115. package/src/components/form-field/renderFormField.tsx +74 -0
  116. package/src/components/grid/Col.tsx +69 -0
  117. package/src/components/grid/Container.tsx +59 -0
  118. package/src/components/grid/Grid.tsx +86 -0
  119. package/src/components/grid/GridItem.tsx +123 -0
  120. package/src/components/grid/Row.tsx +71 -0
  121. package/src/components/grid/gap.ts +8 -0
  122. package/src/components/grid/index.ts +10 -0
  123. package/src/components/icon/Icon.tsx +74 -0
  124. package/src/components/icon/index.ts +2 -0
  125. package/src/components/input-adorn/InputAdorn.tsx +55 -0
  126. package/src/components/input-adorn/index.ts +2 -0
  127. package/src/components/input-group/InputGroup.tsx +50 -0
  128. package/src/components/input-group/InputGroupAddon.tsx +50 -0
  129. package/src/components/input-group/index.ts +4 -0
  130. package/src/components/link/Link.tsx +148 -0
  131. package/src/components/link/index.ts +2 -0
  132. package/src/components/list/List.tsx +193 -0
  133. package/src/components/list/ListItem.tsx +91 -0
  134. package/src/components/list/index.ts +4 -0
  135. package/src/components/menu/Menu.tsx +398 -0
  136. package/src/components/menu/MenuDivider.tsx +19 -0
  137. package/src/components/menu/MenuHeader.tsx +55 -0
  138. package/src/components/menu/MenuItem.tsx +85 -0
  139. package/src/components/menu/MenuItemDef.ts +83 -0
  140. package/src/components/menu/MenuList.tsx +142 -0
  141. package/src/components/menu/MenuSubmenu.tsx +284 -0
  142. package/src/components/menu/MenuSubmenuBack.tsx +39 -0
  143. package/src/components/menu/MenuText.tsx +47 -0
  144. package/src/components/menu/MenuToggle.tsx +148 -0
  145. package/src/components/menu/index.ts +19 -0
  146. package/src/components/menu/menuNavigation.ts +106 -0
  147. package/src/components/menu/submenuGroup.ts +43 -0
  148. package/src/components/modal/Modal.tsx +162 -0
  149. package/src/components/modal/ModalBody.tsx +47 -0
  150. package/src/components/modal/ModalFooter.tsx +51 -0
  151. package/src/components/modal/ModalHeader.tsx +60 -0
  152. package/src/components/modal/ModalTitle.tsx +49 -0
  153. package/src/components/modal/index.ts +10 -0
  154. package/src/components/nav/Nav.tsx +97 -0
  155. package/src/components/nav/NavItem.tsx +42 -0
  156. package/src/components/nav/NavLink.tsx +48 -0
  157. package/src/components/nav/NavTitle.tsx +22 -0
  158. package/src/components/nav/index.ts +7 -0
  159. package/src/components/navbar/Navbar.tsx +112 -0
  160. package/src/components/navbar/NavbarBrand.tsx +55 -0
  161. package/src/components/navbar/NavbarNav.tsx +47 -0
  162. package/src/components/navbar/NavbarText.tsx +47 -0
  163. package/src/components/navbar/NavbarToggler.tsx +34 -0
  164. package/src/components/navbar/index.ts +10 -0
  165. package/src/components/notification/Notification.scss +15 -0
  166. package/src/components/notification/Notification.tsx +187 -0
  167. package/src/components/notification/NotificationIcon.tsx +21 -0
  168. package/src/components/notification/NotificationStack.tsx +66 -0
  169. package/src/components/notification/NotificationText.tsx +47 -0
  170. package/src/components/notification/NotificationTitle.tsx +47 -0
  171. package/src/components/notification/context.ts +14 -0
  172. package/src/components/notification/index.ts +17 -0
  173. package/src/components/notification/notificationQueue.ts +48 -0
  174. package/src/components/otp-input/OtpBox.tsx +68 -0
  175. package/src/components/otp-input/OtpInput.tsx +341 -0
  176. package/src/components/otp-input/index.ts +2 -0
  177. package/src/components/pagination/Pagination.tsx +199 -0
  178. package/src/components/pagination/PaginationItem.tsx +133 -0
  179. package/src/components/pagination/index.ts +4 -0
  180. package/src/components/password-strength/PasswordStrength.tsx +155 -0
  181. package/src/components/password-strength/index.ts +2 -0
  182. package/src/components/password-strength/strengthScore.ts +79 -0
  183. package/src/components/placeholder/Placeholder.tsx +178 -0
  184. package/src/components/placeholder/index.ts +2 -0
  185. package/src/components/popover/Popover.tsx +243 -0
  186. package/src/components/popover/index.ts +2 -0
  187. package/src/components/progress/Progress.tsx +154 -0
  188. package/src/components/progress/ProgressBar.tsx +88 -0
  189. package/src/components/progress/index.ts +4 -0
  190. package/src/components/radio/Radio.tsx +88 -0
  191. package/src/components/radio/RadioGroup.tsx +142 -0
  192. package/src/components/radio/context.ts +14 -0
  193. package/src/components/radio/index.ts +4 -0
  194. package/src/components/range-input/RangeInput.tsx +109 -0
  195. package/src/components/range-input/index.ts +2 -0
  196. package/src/components/select/Select.tsx +277 -0
  197. package/src/components/select/index.ts +2 -0
  198. package/src/components/skeleton/Skeleton.tsx +89 -0
  199. package/src/components/skeleton/SkeletonLoader.tsx +91 -0
  200. package/src/components/skeleton/index.ts +4 -0
  201. package/src/components/spinner/Spinner.tsx +77 -0
  202. package/src/components/spinner/index.ts +2 -0
  203. package/src/components/stack/Stack.tsx +81 -0
  204. package/src/components/stack/index.ts +2 -0
  205. package/src/components/stepper/Stepper.tsx +172 -0
  206. package/src/components/stepper/StepperItem.tsx +66 -0
  207. package/src/components/stepper/index.ts +4 -0
  208. package/src/components/switch/Switch.tsx +246 -0
  209. package/src/components/switch/index.ts +2 -0
  210. package/src/components/table/Table.css +39 -0
  211. package/src/components/table/Table.tsx +390 -0
  212. package/src/components/table/TableBody.tsx +25 -0
  213. package/src/components/table/TableCell.tsx +27 -0
  214. package/src/components/table/TableColumn.tsx +31 -0
  215. package/src/components/table/TableHeader.tsx +26 -0
  216. package/src/components/table/TableRow.tsx +24 -0
  217. package/src/components/table/index.ts +12 -0
  218. package/src/components/tabs/Tab.tsx +36 -0
  219. package/src/components/tabs/TabList.tsx +76 -0
  220. package/src/components/tabs/TabPanel.tsx +68 -0
  221. package/src/components/tabs/Tabs.tsx +136 -0
  222. package/src/components/tabs/context.ts +24 -0
  223. package/src/components/tabs/index.ts +8 -0
  224. package/src/components/text-input/TextInput.tsx +188 -0
  225. package/src/components/text-input/index.ts +2 -0
  226. package/src/components/textarea/Textarea.tsx +145 -0
  227. package/src/components/textarea/index.ts +2 -0
  228. package/src/components/toast/Toast.tsx +219 -0
  229. package/src/components/toast/ToastBody.tsx +64 -0
  230. package/src/components/toast/ToastFooter.tsx +47 -0
  231. package/src/components/toast/ToastHeader.tsx +104 -0
  232. package/src/components/toast/ToastIcon.tsx +21 -0
  233. package/src/components/toast/Toaster.tsx +87 -0
  234. package/src/components/toast/context.ts +14 -0
  235. package/src/components/toast/index.ts +18 -0
  236. package/src/components/toast/toastQueue.ts +56 -0
  237. package/src/components/tooltip/Tooltip.tsx +155 -0
  238. package/src/components/tooltip/index.ts +2 -0
  239. package/src/global.d.ts +2 -0
  240. package/src/hooks/index.ts +57 -0
  241. package/src/hooks/useAutoDismiss.ts +88 -0
  242. package/src/hooks/useButtonSemantics.ts +33 -0
  243. package/src/hooks/useControllableState.ts +24 -0
  244. package/src/hooks/useDialogElement.ts +216 -0
  245. package/src/hooks/useDisabledAnchorGuard.ts +22 -0
  246. package/src/hooks/useDismissibleTransition.ts +126 -0
  247. package/src/hooks/useDrawer.ts +9 -0
  248. package/src/hooks/useFloatingOverlay.ts +115 -0
  249. package/src/hooks/useForkedRef.ts +50 -0
  250. package/src/hooks/useFormField.ts +89 -0
  251. package/src/hooks/useIsomorphicLayoutEffect.ts +9 -0
  252. package/src/hooks/useModal.ts +9 -0
  253. package/src/hooks/useNotification.ts +10 -0
  254. package/src/hooks/useOverlayPlacement.ts +93 -0
  255. package/src/hooks/usePagination.ts +85 -0
  256. package/src/hooks/useToast.ts +9 -0
  257. package/src/hooks/useToastRegionQueue.ts +27 -0
  258. package/src/index.ts +276 -0
  259. package/src/types.ts +133 -0
  260. package/src/utils/breakpoints.ts +43 -0
  261. package/src/utils/comboboxCollection.tsx +152 -0
  262. package/src/utils/dateRangePresets.ts +6 -0
  263. package/src/utils/dialogTransition.ts +39 -0
  264. package/src/utils/focusRedirect.ts +29 -0
  265. package/src/utils/isWholeUnitDisabled.ts +17 -0
  266. package/src/utils/mergeIsDateUnavailable.ts +15 -0
  267. package/src/utils/overlayPlacement.ts +93 -0
  268. package/src/utils/pointerInteraction.ts +21 -0
  269. package/src/utils/polymorphic.ts +36 -0
  270. package/src/utils/renderMenuItemContent.tsx +62 -0
  271. package/src/utils/setVisibleRangeStart.ts +64 -0
  272. package/src/utils/spacingClassName.ts +14 -0
  273. package/src/utils/suppressFocusRingGlobally.ts +69 -0
  274. package/src/utils/validationClassName.ts +8 -0
  275. package/src/utils/virtualFocusStyle.ts +16 -0
@@ -0,0 +1,361 @@
1
+ import React, { forwardRef, HTMLAttributes, useMemo, useRef, useState } from 'react'
2
+ import classNames from 'classnames'
3
+ import {
4
+ AriaRangeCalendarProps,
5
+ mergeProps,
6
+ RangeValue,
7
+ useCalendarCell,
8
+ useLocale,
9
+ useRangeCalendar
10
+ } from 'react-aria'
11
+ import { DateValue, RangeCalendarState, useRangeCalendarState } from 'react-stately'
12
+ import {
13
+ CalendarDate,
14
+ createCalendar,
15
+ getLocalTimeZone,
16
+ isSameDay,
17
+ isToday,
18
+ isWeekend
19
+ } from '@internationalized/date'
20
+
21
+ import { useForkedRef } from '../../hooks'
22
+ import { CalendarMonthBlock } from './CalendarMonthBlock'
23
+ import { CalendarNavButton } from './CalendarNavButton'
24
+ import { DateRangePreset } from '../../utils/dateRangePresets'
25
+ import { mergeIsDateUnavailable } from '../../utils/mergeIsDateUnavailable'
26
+ import { suppressFocusRing } from '../../utils/suppressFocusRingGlobally'
27
+ import './Calendar.scss'
28
+ import './RangeCalendar.scss'
29
+
30
+ export interface RangeCalendarProps extends Omit<
31
+ HTMLAttributes<HTMLDivElement>,
32
+ 'onChange' | 'defaultValue'
33
+ > {
34
+ /**
35
+ * An accessible label for the calendar, used when there's no visible label. Required for
36
+ * standalone use — a calendar grid has no other accessible name of its own.
37
+ */
38
+ 'aria-label'?: string
39
+ /**
40
+ * Identifies a visible label element for the calendar.
41
+ */
42
+ 'aria-labelledby'?: string
43
+ /**
44
+ * Whether to automatically focus the calendar when it mounts.
45
+ */
46
+ autoFocus?: boolean
47
+ /**
48
+ * A string of all className you want applied to the base component.
49
+ */
50
+ className?: string
51
+ /**
52
+ * The initial selected date range (uncontrolled).
53
+ */
54
+ defaultValue?: RangeValue<DateValue> | null
55
+ /**
56
+ * Prevents the calendar from being focused or interacted with.
57
+ */
58
+ disabled?: boolean
59
+ /**
60
+ * The day that starts the week, overriding the default set by the active locale.
61
+ *
62
+ * @default 'mon'
63
+ */
64
+ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
65
+ /**
66
+ * Callback that is called for each date in the calendar. If it returns `true`, that date is
67
+ * shown but cannot be selected.
68
+ */
69
+ isDateUnavailable?: (date: DateValue) => boolean
70
+ /**
71
+ * The maximum allowed date that a user may select.
72
+ */
73
+ maxValue?: DateValue | null
74
+ /**
75
+ * The minimum allowed date that a user may select.
76
+ */
77
+ minValue?: DateValue | null
78
+ /**
79
+ * Callback fired when a complete range is selected (both a start and end date).
80
+ */
81
+ onChange?: (value: RangeValue<DateValue>) => void
82
+ /**
83
+ * A list of quick-select range presets shown beside the calendar. Selecting a preset commits
84
+ * its range immediately, the same as picking a start and end date from the grid. The preset
85
+ * matching the current selection (if any) is marked selected. Omit to not show a preset list.
86
+ */
87
+ presets?: DateRangePreset[]
88
+ /**
89
+ * ISO 8601 dates (`YYYY-MM-DD`) to mark unselectable, as a convenience alternative to
90
+ * `isDateUnavailable` for data-driven cases (e.g. booked dates fetched from an API). Composed
91
+ * with `isDateUnavailable` when both are given — a date unavailable by either is unavailable.
92
+ */
93
+ unavailableDates?: string[]
94
+ /**
95
+ * The selected date range (controlled).
96
+ */
97
+ value?: RangeValue<DateValue> | null
98
+ /**
99
+ * Number of months to display side by side, sharing one selection. Wraps to multiple rows in
100
+ * a narrow container (e.g. a popover on a small screen) rather than overflowing — the wrap is
101
+ * driven by the calendar's own width, not the viewport, so it adapts correctly regardless of
102
+ * where the calendar is embedded.
103
+ *
104
+ * @default 1
105
+ */
106
+ visibleMonths?: number
107
+ }
108
+
109
+ // Range counterpart to `Calendar` — same dialog-agnostic composition boundary (see that
110
+ // component's own comment) and the same header/month-year-picker/grid visual language, reusing
111
+ // `Calendar.scss`'s shared classes directly. Kept as its own component rather than a `mode` prop
112
+ // on `Calendar`: the underlying react-stately/react-aria hooks are a genuinely different pair
113
+ // (`useRangeCalendarState`/`useRangeCalendar` vs `useCalendarState`/`useCalendar`), and the cell
114
+ // rendering has range-only concerns (start/end/in-between pill styling) with no single-date
115
+ // equivalent.
116
+ export const RangeCalendar = forwardRef<HTMLDivElement, RangeCalendarProps>(
117
+ (
118
+ {
119
+ autoFocus,
120
+ className,
121
+ defaultValue,
122
+ disabled,
123
+ firstDayOfWeek = 'mon',
124
+ isDateUnavailable,
125
+ maxValue,
126
+ minValue,
127
+ onChange,
128
+ presets,
129
+ unavailableDates,
130
+ value,
131
+ visibleMonths = 1,
132
+ ...rest
133
+ },
134
+ forwardedRef
135
+ ) => {
136
+ const { locale } = useLocale()
137
+ const internalRef = useRef<HTMLDivElement>(null)
138
+ const ref = useForkedRef(internalRef, forwardedRef)
139
+ const combinedIsDateUnavailable = useMemo(
140
+ () => mergeIsDateUnavailable(unavailableDates, isDateUnavailable),
141
+ [unavailableDates, isDateUnavailable]
142
+ )
143
+ const resolvedPresets = presets && presets.length > 0 ? presets : null
144
+ // Tracks each visible month block's own view, keyed by `monthIndex` — needed only to know
145
+ // whether *any* block has switched away from the day grid. `CalendarYearGrid` owns its own
146
+ // prev/next pager, which would otherwise sit right underneath the global `.datepicker-controls`
147
+ // overlay below (see its own comment) — and even in the month view, where there's no such
148
+ // literal overlap, the global pager would still page the day grid one block is no longer
149
+ // showing, which reads as it doing nothing.
150
+ const [monthViews, setMonthViews] = useState<Record<number, 'days' | 'months' | 'years'>>({})
151
+ const hasPickerView = Object.values(monthViews).some((view) => view !== 'days')
152
+
153
+ const state = useRangeCalendarState({
154
+ autoFocus,
155
+ createCalendar,
156
+ defaultValue,
157
+ firstDayOfWeek,
158
+ isDateUnavailable: combinedIsDateUnavailable,
159
+ isDisabled: disabled,
160
+ locale,
161
+ maxValue,
162
+ minValue,
163
+ onChange,
164
+ // Prev/next always slide the visible window by one month, regardless of `visibleMonths` —
165
+ // react-stately's own default (`pageBehavior: 'visible'`) instead pages by the *entire*
166
+ // visible span at once (e.g. jumping straight from Nov/Dec to Jan/Feb for `visibleMonths={2}`,
167
+ // skipping Dec/Jan entirely), which reads as broken next to `CalendarMonthYearDropdowns`'
168
+ // own one-month-at-a-time jumps.
169
+ pageBehavior: 'single',
170
+ value,
171
+ visibleDuration: { months: visibleMonths }
172
+ })
173
+
174
+ const ariaProps: AriaRangeCalendarProps<DateValue> = {
175
+ 'aria-label': rest['aria-label'],
176
+ 'aria-labelledby': rest['aria-labelledby'],
177
+ autoFocus,
178
+ defaultValue,
179
+ isDateUnavailable: combinedIsDateUnavailable,
180
+ isDisabled: disabled,
181
+ maxValue,
182
+ minValue,
183
+ onChange,
184
+ value
185
+ }
186
+
187
+ const { calendarProps, prevButtonProps, nextButtonProps } = useRangeCalendar(
188
+ ariaProps,
189
+ state,
190
+ internalRef
191
+ )
192
+ // A single pair of buttons, placed in exactly one spot depending on `visibleMonths` — inline
193
+ // in the (only) month's own header for a single month, or once as a global overlay for
194
+ // several — never both, so there's no risk of two DOM nodes fighting over one ref.
195
+ const prevButton = <CalendarNavButton buttonProps={prevButtonProps} direction="prev" />
196
+ const nextButton = <CalendarNavButton buttonProps={nextButtonProps} direction="next" />
197
+ const singleMonthArrows = visibleMonths === 1 ? { next: nextButton, prev: prevButton } : null
198
+
199
+ // Goes through the same `state.setValue` path a two-click grid selection does (rather than
200
+ // calling `onChange` directly), so it behaves identically whether `value` is controlled or
201
+ // uncontrolled — including `DateRangePicker`, which relies on this to auto-close its popover
202
+ // the same way completing a range in the grid already does.
203
+ const handlePresetSelect = (range: RangeValue<DateValue>) => {
204
+ state.setValue(range)
205
+ }
206
+
207
+ return (
208
+ <div
209
+ {...mergeProps(calendarProps, rest)}
210
+ className={classNames('datepicker', className)}
211
+ data-cx-inline="true"
212
+ ref={ref}
213
+ >
214
+ <div className="datepicker-grid">
215
+ {resolvedPresets && (
216
+ <DateRangePresets
217
+ onSelect={handlePresetSelect}
218
+ presets={resolvedPresets}
219
+ value={state.value}
220
+ />
221
+ )}
222
+ <div className="datepicker-body">
223
+ {visibleMonths > 1 && !hasPickerView && (
224
+ <div className="datepicker-controls">
225
+ {prevButton}
226
+ {nextButton}
227
+ </div>
228
+ )}
229
+ <div className="datepicker-grid">
230
+ {[...new Array(visibleMonths).keys()].map((monthIndex) => (
231
+ <CalendarMonthBlock
232
+ arrows={monthIndex === 0 ? singleMonthArrows : null}
233
+ firstDayOfWeek={firstDayOfWeek}
234
+ key={monthIndex}
235
+ monthIndex={monthIndex}
236
+ onViewChange={(view) =>
237
+ setMonthViews((prev) => ({ ...prev, [monthIndex]: view }))
238
+ }
239
+ renderCell={(date, i, week) => (
240
+ <CalendarCell
241
+ date={date}
242
+ isFirstInRow={i === 0}
243
+ isLastInRow={i === week.length - 1}
244
+ locale={locale}
245
+ state={state}
246
+ />
247
+ )}
248
+ state={state}
249
+ />
250
+ ))}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ )
256
+ }
257
+ )
258
+
259
+ RangeCalendar.displayName = 'RangeCalendar'
260
+
261
+ interface CalendarCellProps {
262
+ date: CalendarDate
263
+ isFirstInRow: boolean
264
+ isLastInRow: boolean
265
+ locale: string
266
+ state: RangeCalendarState
267
+ }
268
+
269
+ const CalendarCell = ({ date, isFirstInRow, isLastInRow, locale, state }: CalendarCellProps) => {
270
+ const ref = useRef<HTMLButtonElement>(null)
271
+ const {
272
+ cellProps,
273
+ buttonProps,
274
+ isSelected,
275
+ isOutsideVisibleRange,
276
+ isDisabled,
277
+ isUnavailable,
278
+ formattedDate
279
+ } = useCalendarCell({ date }, state, ref)
280
+
281
+ // `isSelected` is true for every day in the range, not just its endpoints — the pill's rounded
282
+ // caps belong only at the true start/end (or wherever a row wraps mid-range, so the band still
283
+ // reads as continuous week to week); everything else in between is a flush, square-edged band.
284
+ const { highlightedRange } = state
285
+ const isRangeStart = Boolean(highlightedRange && isSameDay(date, highlightedRange.start))
286
+ const isRangeEnd = Boolean(highlightedRange && isSameDay(date, highlightedRange.end))
287
+ const isCurrentDate = isToday(date, getLocalTimeZone())
288
+
289
+ return (
290
+ <div
291
+ {...cellProps}
292
+ aria-current={isCurrentDate ? 'date' : undefined}
293
+ className={classNames('datepicker-date', {
294
+ 'datepicker-date-today': isCurrentDate,
295
+ 'datepicker-date-in-range': isSelected,
296
+ 'datepicker-date-range-start': isSelected && (isRangeStart || isFirstInRow),
297
+ 'datepicker-date-range-end': isSelected && (isRangeEnd || isLastInRow),
298
+ 'datepicker-date-outside': isOutsideVisibleRange,
299
+ 'datepicker-date-disabled': isDisabled,
300
+ 'datepicker-date-unavailable': isUnavailable,
301
+ 'datepicker-date-weekend': isWeekend(date, locale)
302
+ })}
303
+ >
304
+ <button
305
+ {...mergeProps(buttonProps, {
306
+ // Hovering to preview a range's end date moves real DOM focus via `element.focus()`
307
+ // (`useCalendarCell`'s `onPointerEnter` -> `state.highlightDate`), which browsers can
308
+ // paint as a visible focus ring even though the input modality is a pointer, not a
309
+ // keyboard — suppress it the same way a mouse press does elsewhere in the library.
310
+ onPointerEnter: () => {
311
+ if (state.anchorDate && ref.current) suppressFocusRing(ref.current)
312
+ }
313
+ })}
314
+ type="button"
315
+ className={classNames('datepicker-date-btn', {
316
+ 'datepicker-date-range-start': isRangeStart,
317
+ 'datepicker-date-range-end': isRangeEnd
318
+ })}
319
+ ref={ref}
320
+ >
321
+ {formattedDate}
322
+ </button>
323
+ </div>
324
+ )
325
+ }
326
+
327
+ interface DateRangePresetsProps {
328
+ onSelect: (range: RangeValue<DateValue>) => void
329
+ presets: DateRangePreset[]
330
+ value: RangeValue<DateValue> | null
331
+ }
332
+
333
+ // A preset "matches" the current selection when both endpoints land on the same day — the same
334
+ // granularity `CalendarCell` already uses to compare dates, so a preset stays marked selected
335
+ // regardless of whether `value` is a `CalendarDate`, `CalendarDateTime`, or `ZonedDateTime`.
336
+ const isSameRange = (a: RangeValue<DateValue>, b: RangeValue<DateValue>) =>
337
+ isSameDay(a.start, b.start) && isSameDay(a.end, b.end)
338
+
339
+ // Plain buttons in a list, not a listbox — a group of independent actions (each one commits
340
+ // immediately) rather than a single-selection widget, so native Tab/Enter/Space is the right
341
+ // interaction model without extra roving-tabindex/arrow-key wiring.
342
+ const DateRangePresets = ({ onSelect, presets, value }: DateRangePresetsProps) => (
343
+ <ul className="datepicker-presets">
344
+ {presets.map((preset) => {
345
+ const isSelected = Boolean(value && isSameRange(value, preset.range))
346
+
347
+ return (
348
+ <li key={preset.label}>
349
+ <button
350
+ aria-current={isSelected ? 'true' : undefined}
351
+ className={classNames('datepicker-preset', { selected: isSelected })}
352
+ onClick={() => onSelect(preset.range)}
353
+ type="button"
354
+ >
355
+ {preset.label}
356
+ </button>
357
+ </li>
358
+ )
359
+ })}
360
+ </ul>
361
+ )
@@ -0,0 +1,5 @@
1
+ export { Calendar } from './Calendar'
2
+ export type { CalendarProps, CalendarSingleProps, CalendarMultipleProps } from './Calendar'
3
+ export { RangeCalendar } from './RangeCalendar'
4
+ export type { RangeCalendarProps } from './RangeCalendar'
5
+ export type { DateRangePreset } from '../../utils/dateRangePresets'
@@ -0,0 +1,173 @@
1
+ import React, { ElementType, ForwardRefRenderFunction, ReactElement, ReactNode } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { Breakpoint, ContextColor, ContextStyle, Sizing } from '../../types'
5
+ import {
6
+ buildResponsiveClassNames,
7
+ flexDirectionClassNames,
8
+ FlexDirection
9
+ } from '../../utils/breakpoints'
10
+ import {
11
+ createPolymorphicComponent,
12
+ PolymorphicComponentProps,
13
+ PolymorphicRef
14
+ } from '../../utils/polymorphic'
15
+ import { CardBody } from './CardBody'
16
+ import { CardFooter } from './CardFooter'
17
+ import { CardImage } from './CardImage'
18
+ import { CardSubtitle } from './CardSubtitle'
19
+ import { CardText } from './CardText'
20
+ import { CardTitle } from './CardTitle'
21
+
22
+ type CardOwnProps<C extends ElementType> = {
23
+ /**
24
+ * A string of all className you want applied to the base component.
25
+ */
26
+ className?: string
27
+ /**
28
+ * Component used for the root node. Either a string to use a HTML element or a component.
29
+ */
30
+ component?: C
31
+ /**
32
+ * Sets the color of the component to one of Chassis context colors.
33
+ */
34
+ color?: ContextColor
35
+ /**
36
+ * Switches the card from its default stacked (column) layout to a side-by-side (row) layout.
37
+ * Wrap the image and body in `Col` to control each side's width.
38
+ */
39
+ direction?: FlexDirection
40
+ /**
41
+ * Shorthand for a `CardFooter`, rendered after the image/title/subtitle/text/`children` block.
42
+ */
43
+ footer?: ReactNode
44
+ /**
45
+ * Shorthand for a `CardImage` — pass either a `src` URL (combine with `imageAlt`/
46
+ * `imageOrientation`) or a fully-formed element — a `<CardImage>`, `<Skeleton>`,
47
+ * `<Placeholder>`, or anything else — rendered as-is in its place. `imageAlt`/`imageOrientation`
48
+ * only apply to the `src` form; set them directly on your own element otherwise. For a
49
+ * horizontal layout or an overlay, omit this and compose `CardImage` directly as a child
50
+ * instead.
51
+ */
52
+ image?: ReactNode
53
+ /**
54
+ * Accessible alt text for `image`. Ignored unless `image` is set.
55
+ */
56
+ imageAlt?: string
57
+ /**
58
+ * Orientates `image` to the top (default) or bottom of the card.
59
+ */
60
+ imageOrientation?: 'top' | 'bottom'
61
+ /**
62
+ * Overrides `direction` at one or more breakpoints — e.g. `{ large: 'row' }` to lay the card
63
+ * out horizontally from `large` up while stacking below it.
64
+ */
65
+ responsive?: Partial<Record<Breakpoint, FlexDirection>>
66
+ /**
67
+ * Sets the size of the component to one of Chassis component sizes.
68
+ */
69
+ size?: Sizing
70
+ /**
71
+ * Shorthand for a `CardSubtitle`, rendered directly after `title`.
72
+ */
73
+ subtitle?: ReactNode
74
+ /**
75
+ * Shorthand for a `CardText`, rendered after `title`/`subtitle`.
76
+ */
77
+ text?: ReactNode
78
+ /**
79
+ * Shorthand for a `CardTitle`. Set alongside `subtitle`/`text` (or plain `children`, e.g. a
80
+ * `Button`) to build a standard card without composing `CardBody`/`CardTitle` yourself — all
81
+ * render together inside a single `CardBody`, in that order. Only takes effect when at least
82
+ * one of `image`/`title`/`subtitle`/`text`/`footer` is set; otherwise `children` render as-is,
83
+ * so full manual composition (`CardHeader`, lists, overlays, nav headers, …) keeps working
84
+ * unchanged. Mixing shorthand props with a manually-composed `children` tree isn't supported —
85
+ * pick one approach per card.
86
+ */
87
+ title?: ReactNode
88
+ /**
89
+ * Sets the context style of the component. `basic` (the default) renders with no extra class.
90
+ */
91
+ variant?: ContextStyle
92
+ }
93
+
94
+ export type CardProps<C extends ElementType = 'div'> = PolymorphicComponentProps<C, CardOwnProps<C>>
95
+
96
+ type CardComponent = (<C extends ElementType = 'div'>(
97
+ props: CardProps<C> & { ref?: PolymorphicRef<C> }
98
+ ) => ReactElement | null) & { displayName?: string }
99
+
100
+ function CardRender<C extends ElementType = 'div'>(
101
+ {
102
+ children,
103
+ className,
104
+ color,
105
+ component,
106
+ direction,
107
+ footer,
108
+ image,
109
+ imageAlt,
110
+ imageOrientation = 'top',
111
+ responsive,
112
+ size,
113
+ subtitle,
114
+ text,
115
+ title,
116
+ variant,
117
+ ...rest
118
+ }: CardProps<C>,
119
+ ref: PolymorphicRef<C>
120
+ ) {
121
+ const Component = component ?? 'div'
122
+ const _className = classNames(
123
+ 'card',
124
+ color,
125
+ {
126
+ context: !!color,
127
+ solid: variant === 'solid',
128
+ smooth: variant === 'smooth',
129
+ outline: variant === 'outline',
130
+ small: size === 'small',
131
+ large: size === 'large'
132
+ },
133
+ buildResponsiveClassNames(flexDirectionClassNames, direction, responsive),
134
+ className
135
+ )
136
+
137
+ const hasShorthand =
138
+ image != null || title != null || subtitle != null || footer != null || text != null
139
+ const hasBodyContent = title != null || subtitle != null || text != null || children != null
140
+ const renderedImage =
141
+ typeof image === 'string' ? (
142
+ <CardImage orientation={imageOrientation} src={image} alt={imageAlt ?? ''} />
143
+ ) : (
144
+ image
145
+ )
146
+
147
+ return (
148
+ <Component className={_className} {...rest} ref={ref}>
149
+ {hasShorthand ? (
150
+ <>
151
+ {image != null && imageOrientation === 'top' && renderedImage}
152
+ {hasBodyContent && (
153
+ <CardBody>
154
+ {title != null && <CardTitle>{title}</CardTitle>}
155
+ {subtitle != null && <CardSubtitle>{subtitle}</CardSubtitle>}
156
+ {text != null && <CardText>{text}</CardText>}
157
+ {children}
158
+ </CardBody>
159
+ )}
160
+ {image != null && imageOrientation === 'bottom' && renderedImage}
161
+ {footer != null && <CardFooter>{footer}</CardFooter>}
162
+ </>
163
+ ) : (
164
+ children
165
+ )}
166
+ </Component>
167
+ )
168
+ }
169
+
170
+ export const Card = createPolymorphicComponent<CardComponent>(
171
+ CardRender as ForwardRefRenderFunction<Element, CardProps<ElementType>>,
172
+ 'Card'
173
+ )
@@ -0,0 +1,75 @@
1
+ import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { Breakpoint, Spacing } from '../../types'
5
+ import {
6
+ buildResponsiveClassNames,
7
+ flexDirectionClassNames,
8
+ FlexDirection
9
+ } from '../../utils/breakpoints'
10
+ import {
11
+ createPolymorphicComponent,
12
+ PolymorphicComponentProps,
13
+ PolymorphicRef
14
+ } from '../../utils/polymorphic'
15
+ import { spacingClassName } from '../../utils/spacingClassName'
16
+
17
+ type CardBodyOwnProps<C extends ElementType> = {
18
+ /**
19
+ * A string of all className you want applied to the base component.
20
+ */
21
+ className?: string
22
+ /**
23
+ * Component used for the root node. Either a string to use a HTML element or a component.
24
+ */
25
+ component?: C
26
+ /**
27
+ * Switches the body from its default stacked (column) layout to a side-by-side (row) layout —
28
+ * for placing an image beside text within a single padded region. Wrap the image and text in
29
+ * `Col` to control each side's width, and nest another `CardBody` (with `.p-0`) for the text
30
+ * side so it doesn't receive double padding.
31
+ */
32
+ direction?: FlexDirection
33
+ /**
34
+ * Spacing between children, mapped to the `gap-*` utility classes. Overrides the card's default
35
+ * gap between body children.
36
+ */
37
+ gap?: Spacing | 0
38
+ /**
39
+ * Overrides `direction` at one or more breakpoints.
40
+ */
41
+ responsive?: Partial<Record<Breakpoint, FlexDirection>>
42
+ }
43
+
44
+ export type CardBodyProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
45
+ C,
46
+ CardBodyOwnProps<C>
47
+ >
48
+
49
+ type CardBodyComponent = (<C extends ElementType = 'div'>(
50
+ props: CardBodyProps<C> & { ref?: PolymorphicRef<C> }
51
+ ) => ReactElement | null) & { displayName?: string }
52
+
53
+ function CardBodyRender<C extends ElementType = 'div'>(
54
+ { children, className, component, direction, gap, responsive, ...rest }: CardBodyProps<C>,
55
+ ref: PolymorphicRef<C>
56
+ ) {
57
+ const Component = component ?? 'div'
58
+ const _className = classNames(
59
+ 'card-body',
60
+ buildResponsiveClassNames(flexDirectionClassNames, direction, responsive),
61
+ spacingClassName('gap', gap),
62
+ className
63
+ )
64
+
65
+ return (
66
+ <Component className={_className} {...rest} ref={ref}>
67
+ {children}
68
+ </Component>
69
+ )
70
+ }
71
+
72
+ export const CardBody = createPolymorphicComponent<CardBodyComponent>(
73
+ CardBodyRender as ForwardRefRenderFunction<Element, CardBodyProps<ElementType>>,
74
+ 'CardBody'
75
+ )
@@ -0,0 +1,47 @@
1
+ import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import {
5
+ createPolymorphicComponent,
6
+ PolymorphicComponentProps,
7
+ PolymorphicRef
8
+ } from '../../utils/polymorphic'
9
+
10
+ type CardFooterOwnProps<C extends ElementType> = {
11
+ /**
12
+ * A string of all className you want applied to the base component.
13
+ */
14
+ className?: string
15
+ /**
16
+ * Component used for the root node. Either a string to use a HTML element or a component.
17
+ */
18
+ component?: C
19
+ }
20
+
21
+ export type CardFooterProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
22
+ C,
23
+ CardFooterOwnProps<C>
24
+ >
25
+
26
+ type CardFooterComponent = (<C extends ElementType = 'div'>(
27
+ props: CardFooterProps<C> & { ref?: PolymorphicRef<C> }
28
+ ) => ReactElement | null) & { displayName?: string }
29
+
30
+ function CardFooterRender<C extends ElementType = 'div'>(
31
+ { children, className, component, ...rest }: CardFooterProps<C>,
32
+ ref: PolymorphicRef<C>
33
+ ) {
34
+ const Component = component ?? 'div'
35
+ const _className = classNames('card-footer', className)
36
+
37
+ return (
38
+ <Component className={_className} {...rest} ref={ref}>
39
+ {children}
40
+ </Component>
41
+ )
42
+ }
43
+
44
+ export const CardFooter = createPolymorphicComponent<CardFooterComponent>(
45
+ CardFooterRender as ForwardRefRenderFunction<Element, CardFooterProps<ElementType>>,
46
+ 'CardFooter'
47
+ )