@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,74 @@
1
+ import React, { ButtonHTMLAttributes, forwardRef, useEffect, useRef } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { useForkedRef } from '../../hooks'
5
+ import { markPointerClick } from '../../utils/pointerInteraction'
6
+ import { Icon } from '../icon'
7
+ import { useCarouselContext } from './context'
8
+
9
+ interface CarouselControlButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
10
+ /**
11
+ * A string of all className you want applied to the component.
12
+ */
13
+ className?: string
14
+ /**
15
+ * Which direction this control navigates, and which end-of-track state disables it.
16
+ */
17
+ direction: 'prev' | 'next'
18
+ /**
19
+ * The accessible label announced by assistive technology.
20
+ */
21
+ label?: string
22
+ }
23
+
24
+ // Shared prev/next control, used by both CarouselControlPrev and CarouselControlNext — mirrors
25
+ // how CalendarNavButton.tsx factors the same shape for Calendar/RangeCalendar.
26
+ export const CarouselControlButton = forwardRef<HTMLButtonElement, CarouselControlButtonProps>(
27
+ ({ children, className, direction, disabled, label, onClick, ...rest }, ref) => {
28
+ const { atEnd, atStart, ends, next, prev, registerControl } = useCarouselContext()
29
+ const buttonRef = useRef<HTMLButtonElement>(null)
30
+ const forkedRef = useForkedRef(ref, buttonRef)
31
+
32
+ useEffect(() => {
33
+ const element = buttonRef.current
34
+ if (!element) return undefined
35
+ return registerControl(direction, element)
36
+ }, [registerControl, direction])
37
+
38
+ const atBoundary = direction === 'next' ? atEnd : atStart
39
+ const navigate = direction === 'next' ? next : prev
40
+
41
+ // Compose rather than let a caller's `onClick`/`disabled` silently replace navigation: the
42
+ // carousel's own end-of-track state can only ever add a disable, and the click handler always
43
+ // still navigates the carousel alongside whatever the caller's handler does.
44
+ const isDisabled = disabled || (ends === 'stop' && atBoundary)
45
+ const handleClick: typeof onClick = (event) => {
46
+ onClick?.(event)
47
+ if (event.detail !== 0) markPointerClick(event.currentTarget)
48
+ navigate()
49
+ }
50
+
51
+ return (
52
+ <button
53
+ type="button"
54
+ className={classNames('button small icon-only', className)}
55
+ disabled={isDisabled}
56
+ onClick={handleClick}
57
+ {...rest}
58
+ ref={forkedRef}
59
+ >
60
+ {children ?? (
61
+ <>
62
+ <Icon
63
+ name={direction === 'next' ? 'chevron-right-outline' : 'chevron-left-outline'}
64
+ className="directional-icon"
65
+ />
66
+ <span className="visually-hidden">{label}</span>
67
+ </>
68
+ )}
69
+ </button>
70
+ )
71
+ }
72
+ )
73
+
74
+ CarouselControlButton.displayName = 'CarouselControlButton'
@@ -0,0 +1,22 @@
1
+ import React, { ButtonHTMLAttributes, forwardRef } from 'react'
2
+
3
+ import { CarouselControlButton } from './CarouselControlButton'
4
+
5
+ export interface CarouselControlNextProps extends ButtonHTMLAttributes<HTMLButtonElement> {
6
+ /**
7
+ * A string of all className you want applied to the component.
8
+ */
9
+ className?: string
10
+ /**
11
+ * The accessible label announced by assistive technology.
12
+ */
13
+ label?: string
14
+ }
15
+
16
+ export const CarouselControlNext = forwardRef<HTMLButtonElement, CarouselControlNextProps>(
17
+ ({ label = 'Next slide', ...rest }, ref) => (
18
+ <CarouselControlButton {...rest} direction="next" label={label} ref={ref} />
19
+ )
20
+ )
21
+
22
+ CarouselControlNext.displayName = 'CarouselControlNext'
@@ -0,0 +1,22 @@
1
+ import React, { ButtonHTMLAttributes, forwardRef } from 'react'
2
+
3
+ import { CarouselControlButton } from './CarouselControlButton'
4
+
5
+ export interface CarouselControlPrevProps extends ButtonHTMLAttributes<HTMLButtonElement> {
6
+ /**
7
+ * A string of all className you want applied to the component.
8
+ */
9
+ className?: string
10
+ /**
11
+ * The accessible label announced by assistive technology.
12
+ */
13
+ label?: string
14
+ }
15
+
16
+ export const CarouselControlPrev = forwardRef<HTMLButtonElement, CarouselControlPrevProps>(
17
+ ({ label = 'Previous slide', ...rest }, ref) => (
18
+ <CarouselControlButton {...rest} direction="prev" label={label} ref={ref} />
19
+ )
20
+ )
21
+
22
+ CarouselControlPrev.displayName = 'CarouselControlPrev'
@@ -0,0 +1,44 @@
1
+ import React, { forwardRef, HTMLAttributes } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { useCarouselContext } from './context'
5
+
6
+ export interface CarouselIndicatorsProps extends HTMLAttributes<HTMLOListElement> {
7
+ /**
8
+ * A string of all className you want applied to the component.
9
+ */
10
+ className?: string
11
+ /**
12
+ * Accessible label for each indicator button. Receives the slide's 1-based position.
13
+ */
14
+ label?: (position: number) => string
15
+ }
16
+
17
+ export const CarouselIndicators = forwardRef<HTMLOListElement, CarouselIndicatorsProps>(
18
+ ({ className, label = (position) => `Slide ${position}`, ...rest }, ref) => {
19
+ const { activeIndex, itemCount, itemsVisible, to } = useCarouselContext()
20
+ const _className = classNames('carousel-indicators', className)
21
+ // One indicator per reachable position, not per item — mirrors `navigate()`'s own clamping so
22
+ // an indicator never targets an index the carousel can't actually land on (e.g. with
23
+ // `items > 1`, the last few slides only ever appear alongside an earlier one).
24
+ const indicatorCount = itemCount === 0 ? 0 : Math.max(1, itemCount - itemsVisible + 1)
25
+
26
+ return (
27
+ <ol className={_className} {...rest} ref={ref}>
28
+ {Array.from({ length: indicatorCount }, (_, index) => index).map((index) => (
29
+ <li key={index}>
30
+ <button
31
+ type="button"
32
+ className={index === activeIndex ? 'active' : undefined}
33
+ aria-current={index === activeIndex ? 'true' : undefined}
34
+ aria-label={label(index + 1)}
35
+ onClick={() => to(index)}
36
+ />
37
+ </li>
38
+ ))}
39
+ </ol>
40
+ )
41
+ }
42
+ )
43
+
44
+ CarouselIndicators.displayName = 'CarouselIndicators'
@@ -0,0 +1,56 @@
1
+ import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { useForkedRef } from '../../hooks'
5
+ import {
6
+ createPolymorphicComponent,
7
+ PolymorphicComponentProps,
8
+ PolymorphicRef
9
+ } from '../../utils/polymorphic'
10
+ import { useCarouselContext } from './context'
11
+
12
+ type CarouselInnerOwnProps<C extends ElementType> = {
13
+ /**
14
+ * A string of all className you want applied to the component.
15
+ */
16
+ className?: string
17
+ /**
18
+ * Component used for the root node. Either a string to use a HTML element or a component.
19
+ */
20
+ component?: C
21
+ }
22
+
23
+ export type CarouselInnerProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
24
+ C,
25
+ CarouselInnerOwnProps<C>
26
+ >
27
+
28
+ type CarouselInnerComponent = (<C extends ElementType = 'div'>(
29
+ props: CarouselInnerProps<C> & { ref?: PolymorphicRef<C> }
30
+ ) => ReactElement | null) & { displayName?: string }
31
+
32
+ /**
33
+ * The scroll viewport — a real horizontally-scrolling container using CSS scroll-snap. Wrap
34
+ * CarouselItem children in this rather than passing them directly to Carousel, so controls and
35
+ * indicators can sit alongside it (above, below, or overlaid) instead of inside the scroll track.
36
+ */
37
+ function CarouselInnerRender<C extends ElementType = 'div'>(
38
+ { children, className, component, ...rest }: CarouselInnerProps<C>,
39
+ ref: PolymorphicRef<C>
40
+ ) {
41
+ const { registerViewport } = useCarouselContext()
42
+ const Component = component ?? 'div'
43
+ const forkedRef = useForkedRef(ref, registerViewport)
44
+ const _className = classNames('carousel-inner', className)
45
+
46
+ return (
47
+ <Component className={_className} {...rest} ref={forkedRef}>
48
+ {children}
49
+ </Component>
50
+ )
51
+ }
52
+
53
+ export const CarouselInner = createPolymorphicComponent<CarouselInnerComponent>(
54
+ CarouselInnerRender as ForwardRefRenderFunction<Element, CarouselInnerProps<ElementType>>,
55
+ 'CarouselInner'
56
+ )
@@ -0,0 +1,51 @@
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 CarouselItemOwnProps<C extends ElementType> = {
11
+ /**
12
+ * A string of all className you want applied to the 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
+ * Milliseconds to wait before autoplay advances past this slide, overriding the carousel's own `interval`.
21
+ */
22
+ interval?: number
23
+ }
24
+
25
+ export type CarouselItemProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
26
+ C,
27
+ CarouselItemOwnProps<C>
28
+ >
29
+
30
+ type CarouselItemComponent = (<C extends ElementType = 'div'>(
31
+ props: CarouselItemProps<C> & { ref?: PolymorphicRef<C> }
32
+ ) => ReactElement | null) & { displayName?: string }
33
+
34
+ function CarouselItemRender<C extends ElementType = 'div'>(
35
+ { children, className, component, interval, ...rest }: CarouselItemProps<C>,
36
+ ref: PolymorphicRef<C>
37
+ ) {
38
+ const Component = component ?? 'div'
39
+ const _className = classNames('carousel-item', className)
40
+
41
+ return (
42
+ <Component className={_className} data-interval={interval} {...rest} ref={ref}>
43
+ {children}
44
+ </Component>
45
+ )
46
+ }
47
+
48
+ export const CarouselItem = createPolymorphicComponent<CarouselItemComponent>(
49
+ CarouselItemRender as ForwardRefRenderFunction<Element, CarouselItemProps<ElementType>>,
50
+ 'CarouselItem'
51
+ )
@@ -0,0 +1,51 @@
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 CarouselOverlayOwnProps<C extends ElementType> = {
11
+ /**
12
+ * A string of all className you want applied to the 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 CarouselOverlayProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
22
+ C,
23
+ CarouselOverlayOwnProps<C>
24
+ >
25
+
26
+ type CarouselOverlayComponent = (<C extends ElementType = 'div'>(
27
+ props: CarouselOverlayProps<C> & { ref?: PolymorphicRef<C> }
28
+ ) => ReactElement | null) & { displayName?: string }
29
+
30
+ /**
31
+ * Overlays its children (typically controls and indicators) on top of the slides instead of
32
+ * stacking them in the flow.
33
+ */
34
+ function CarouselOverlayRender<C extends ElementType = 'div'>(
35
+ { children, className, component, ...rest }: CarouselOverlayProps<C>,
36
+ ref: PolymorphicRef<C>
37
+ ) {
38
+ const Component = component ?? 'div'
39
+ const _className = classNames('carousel-overlay', className)
40
+
41
+ return (
42
+ <Component className={_className} {...rest} ref={ref}>
43
+ {children}
44
+ </Component>
45
+ )
46
+ }
47
+
48
+ export const CarouselOverlay = createPolymorphicComponent<CarouselOverlayComponent>(
49
+ CarouselOverlayRender as ForwardRefRenderFunction<Element, CarouselOverlayProps<ElementType>>,
50
+ 'CarouselOverlay'
51
+ )
@@ -0,0 +1,61 @@
1
+ import React, { ButtonHTMLAttributes, forwardRef } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { Icon } from '../icon'
5
+ import { useCarouselContext } from './context'
6
+
7
+ export interface CarouselPlayPauseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /**
9
+ * A string of all className you want applied to the component.
10
+ */
11
+ className?: string
12
+ /**
13
+ * The accessible label announced while autoplay is running.
14
+ */
15
+ pauseLabel?: string
16
+ /**
17
+ * The accessible label announced while autoplay is stopped.
18
+ */
19
+ playLabel?: string
20
+ }
21
+
22
+ /**
23
+ * A discoverable toggle so a viewer can stop an autoplaying carousel, as required by WCAG 2.2
24
+ * Success Criterion 2.2.2 (Pause, Stop, Hide). Reflects the current state automatically — a pause
25
+ * icon while playing, a play icon once stopped.
26
+ */
27
+ export const CarouselPlayPause = forwardRef<HTMLButtonElement, CarouselPlayPauseProps>(
28
+ ({ children, className, onClick, pauseLabel = 'Pause', playLabel = 'Play', ...rest }, ref) => {
29
+ const { playing, togglePlayPause } = useCarouselContext()
30
+ const _className = classNames(
31
+ 'carousel-control-play-pause button small icon-only',
32
+ { paused: !playing },
33
+ className
34
+ )
35
+ // Compose rather than let a caller's `onClick` silently replace the toggle.
36
+ const handleClick: typeof onClick = (event) => {
37
+ onClick?.(event)
38
+ togglePlayPause()
39
+ }
40
+
41
+ return (
42
+ <button
43
+ type="button"
44
+ className={_className}
45
+ aria-label={playing ? pauseLabel : playLabel}
46
+ onClick={handleClick}
47
+ {...rest}
48
+ ref={ref}
49
+ >
50
+ {children ?? (
51
+ <>
52
+ <Icon name="pause-solid" className="carousel-icon-pause" aria-hidden="true" />
53
+ <Icon name="play-solid" className="carousel-icon-play" aria-hidden="true" />
54
+ </>
55
+ )}
56
+ </button>
57
+ )
58
+ }
59
+ )
60
+
61
+ CarouselPlayPause.displayName = 'CarouselPlayPause'
@@ -0,0 +1,170 @@
1
+ // Internal helpers shared by the carousel family. Not exported from the component's public
2
+ // barrel (see CONVENTIONS.md's helper-module carve-out).
3
+
4
+ export const ITEM_SELECTOR = '.carousel-item:not(.carousel-item-clone)'
5
+
6
+ export function getCarouselItems(viewport: HTMLElement): HTMLElement[] {
7
+ return Array.from(viewport.querySelectorAll<HTMLElement>(`:scope > ${ITEM_SELECTOR}`))
8
+ }
9
+
10
+ export function isRTL(element: Element): boolean {
11
+ return getComputedStyle(element).direction === 'rtl'
12
+ }
13
+
14
+ export function prefersReducedMotion(): boolean {
15
+ return (
16
+ typeof window !== 'undefined' &&
17
+ typeof window.matchMedia === 'function' &&
18
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
19
+ )
20
+ }
21
+
22
+ export function isElementVisible(element: Element | null): boolean {
23
+ if (!element) return false
24
+ const rect = element.getBoundingClientRect()
25
+ return (
26
+ rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
27
+ rect.bottom > 0 &&
28
+ rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
29
+ rect.right > 0
30
+ )
31
+ }
32
+
33
+ // Horizontal distance to scroll `viewport` so `target` rests where the active slide belongs —
34
+ // centered when `center` is set, otherwise flush with the scroll-padding (peek) offset, which is
35
+ // exactly where scroll-snap settles. Scrolling the viewport itself (rather than
36
+ // `target.scrollIntoView()`) keeps an off-screen carousel from yanking the whole page to itself.
37
+ export function scrollDeltaFor(
38
+ viewport: HTMLElement,
39
+ target: HTMLElement,
40
+ center: boolean
41
+ ): number {
42
+ const viewportRect = viewport.getBoundingClientRect()
43
+ const rect = target.getBoundingClientRect()
44
+
45
+ if (center) {
46
+ return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2)
47
+ }
48
+
49
+ const padStart = Number.parseFloat(getComputedStyle(viewport).scrollPaddingInlineStart) || 0
50
+
51
+ return isRTL(viewport)
52
+ ? rect.right - (viewportRect.right - padStart)
53
+ : rect.left - (viewportRect.left + padStart)
54
+ }
55
+
56
+ export function isViewportScrollable(viewport: HTMLElement): boolean {
57
+ return viewport.scrollWidth - viewport.clientWidth > 0
58
+ }
59
+
60
+ const SCROLL_DURATION_MS = 300
61
+ // Safety net above the animation's own duration, in case `requestAnimationFrame` stalls (a
62
+ // backgrounded tab, a frozen/suspended webview) and never drives the animation to completion.
63
+ // `setTimeout` still fires in that situation — throttled, but not indefinitely suspended the way
64
+ // rAF can be — so this guarantees the scroll lands on target and callers relying on completion to
65
+ // release a lock (e.g. re-enabling the IntersectionObserver) can never get stuck forever.
66
+ const SCROLL_SAFETY_MS = SCROLL_DURATION_MS + 200
67
+
68
+ const easeInOutCubic = (progress: number) =>
69
+ progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2
70
+
71
+ // Animates `viewport.scrollLeft` to `targetLeft` by stepping the position by hand each frame,
72
+ // resolving deterministically once the animation completes — not by waiting on the `scrollend`
73
+ // event or a fixed timeout as the *primary* signal. Both of those only approximate when a native
74
+ // `scrollTo({behavior:'smooth'})` actually finishes, and firing early restores CSS scroll-snap
75
+ // mid-flight, which snaps the track back to the nearest point instead of the intended one rather
76
+ // than letting it arrive. Using `behavior: 'instant'` for every step keeps this animation from
77
+ // fighting the viewport's own CSS `scroll-behavior: smooth`. Returns a canceller so a caller that
78
+ // starts a newer scroll before this one finishes can stop it instead of both fighting over the
79
+ // position.
80
+ export function animateScrollTo(
81
+ viewport: HTMLElement,
82
+ targetLeft: number,
83
+ onSettled: () => void
84
+ ): () => void {
85
+ if (prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') {
86
+ viewport.scrollTo({ left: targetLeft, behavior: 'instant' })
87
+ onSettled()
88
+ return () => {}
89
+ }
90
+
91
+ const startLeft = viewport.scrollLeft
92
+ const distance = targetLeft - startLeft
93
+ let startTime: number | null = null
94
+ let frame: number | null = null
95
+ let done = false
96
+
97
+ const finish = () => {
98
+ if (done) return
99
+ done = true
100
+ if (frame !== null) cancelAnimationFrame(frame)
101
+ clearTimeout(safetyTimer)
102
+ viewport.scrollTo({ left: targetLeft, behavior: 'instant' })
103
+ onSettled()
104
+ }
105
+
106
+ const step = (now: number) => {
107
+ if (done) return
108
+ if (startTime === null) startTime = now
109
+
110
+ const progress = Math.min((now - startTime) / SCROLL_DURATION_MS, 1)
111
+ viewport.scrollTo({
112
+ left: startLeft + distance * easeInOutCubic(progress),
113
+ behavior: 'instant'
114
+ })
115
+
116
+ if (progress < 1) {
117
+ frame = requestAnimationFrame(step)
118
+ return
119
+ }
120
+
121
+ finish()
122
+ }
123
+
124
+ const safetyTimer = setTimeout(finish, SCROLL_SAFETY_MS)
125
+ frame = requestAnimationFrame(step)
126
+
127
+ return () => {
128
+ if (done) return
129
+ done = true
130
+ if (frame !== null) cancelAnimationFrame(frame)
131
+ clearTimeout(safetyTimer)
132
+ }
133
+ }
134
+
135
+ export function normalizeIndex(index: number, length: number, wraps: boolean): number | null {
136
+ if (Number.isNaN(index) || length === 0) return null
137
+ if (index < 0) return wraps ? length - 1 : null
138
+ if (index > length - 1) return wraps ? 0 : null
139
+ return index
140
+ }
141
+
142
+ export type CarouselDirection = 'left' | 'right'
143
+
144
+ export function directionBetween(from: number, to: number, rtl: boolean): CarouselDirection {
145
+ const isNext = to > from
146
+ if (rtl) return isNext ? 'right' : 'left'
147
+ return isNext ? 'left' : 'right'
148
+ }
149
+
150
+ export function loopDirection(isNext: boolean, rtl: boolean): CarouselDirection {
151
+ if (rtl) return isNext ? 'right' : 'left'
152
+ return isNext ? 'left' : 'right'
153
+ }
154
+
155
+ // Seamless looping only reads right for the plain single-slide scroll layout — multi-item, peek,
156
+ // centered, and variable-width layouts fall back to a `wrap` jump instead (see `canLoop`).
157
+ export function canLoop(
158
+ carouselElement: HTMLElement,
159
+ itemCount: number,
160
+ isFade: boolean,
161
+ center: boolean,
162
+ auto: boolean
163
+ ): boolean {
164
+ if (isFade || itemCount < 2 || center || auto) return false
165
+
166
+ const styles = getComputedStyle(carouselElement)
167
+ const num = (name: string) => Number.parseFloat(styles.getPropertyValue(name)) || 0
168
+
169
+ return (num('--cx-carousel-items') || 1) === 1 && num('--cx-carousel-items-peek') === 0
170
+ }
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export type CarouselEnds = 'loop' | 'wrap' | 'stop'
4
+
5
+ export interface CarouselContextProps {
6
+ activeIndex: number
7
+ atEnd: boolean
8
+ atStart: boolean
9
+ ends: CarouselEnds
10
+ itemCount: number
11
+ itemsVisible: number
12
+ next: () => void
13
+ playing: boolean
14
+ prev: () => void
15
+ registerControl: (kind: 'prev' | 'next', element: HTMLButtonElement) => () => void
16
+ registerViewport: (node: HTMLDivElement | null) => void
17
+ to: (index: number) => void
18
+ togglePlayPause: () => void
19
+ }
20
+
21
+ export const CarouselContext = createContext<CarouselContextProps | null>(null)
22
+
23
+ export const useCarouselContext = (): CarouselContextProps => {
24
+ const context = useContext(CarouselContext)
25
+ if (!context) {
26
+ throw new Error('Carousel sub-components must be rendered inside a Carousel')
27
+ }
28
+ return context
29
+ }
@@ -0,0 +1,21 @@
1
+ export { Carousel } from './Carousel'
2
+ export type {
3
+ CarouselEnds,
4
+ CarouselProps,
5
+ CarouselSlideDetail,
6
+ CarouselTransition
7
+ } from './Carousel'
8
+ export { CarouselControlNext } from './CarouselControlNext'
9
+ export type { CarouselControlNextProps } from './CarouselControlNext'
10
+ export { CarouselControlPrev } from './CarouselControlPrev'
11
+ export type { CarouselControlPrevProps } from './CarouselControlPrev'
12
+ export { CarouselIndicators } from './CarouselIndicators'
13
+ export type { CarouselIndicatorsProps } from './CarouselIndicators'
14
+ export { CarouselInner } from './CarouselInner'
15
+ export type { CarouselInnerProps } from './CarouselInner'
16
+ export { CarouselItem } from './CarouselItem'
17
+ export type { CarouselItemProps } from './CarouselItem'
18
+ export { CarouselOverlay } from './CarouselOverlay'
19
+ export type { CarouselOverlayProps } from './CarouselOverlay'
20
+ export { CarouselPlayPause } from './CarouselPlayPause'
21
+ export type { CarouselPlayPauseProps } from './CarouselPlayPause'