@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,199 @@
1
+ import React, { forwardRef, HTMLAttributes } from 'react'
2
+ import { Icon } from '../icon'
3
+ import classNames from 'classnames'
4
+
5
+ import { PaginationItem } from './PaginationItem'
6
+ import { usePagination } from '../../hooks'
7
+
8
+ export interface PaginationProps extends HTMLAttributes<HTMLElement> {
9
+ /**
10
+ * Current active page (1-indexed). Used with `pages` for data-driven mode.
11
+ */
12
+ activePage?: number
13
+ /**
14
+ * Set the alignment of pagination components.
15
+ */
16
+ align?: 'start' | 'center' | 'end'
17
+ /**
18
+ * Accessible label for the pagination `<nav>` landmark. Override for non-English locales,
19
+ * or when multiple paginators appear on the same page.
20
+ *
21
+ * @default 'Pagination'
22
+ */
23
+ 'aria-label'?: string
24
+ /**
25
+ * A string of all className you want applied to the base component.
26
+ */
27
+ className?: string
28
+ /**
29
+ * Maximum number of visible page buttons (default: 5). Flanking pages are collapsed to ellipsis.
30
+ */
31
+ maxVisiblePages?: number
32
+ /**
33
+ * Accessible label for the "next page" control, used in smart pagination mode. Override for
34
+ * non-English locales.
35
+ *
36
+ * @default 'Next'
37
+ */
38
+ nextLabel?: string
39
+ /**
40
+ * Callback fired when the active page changes.
41
+ */
42
+ onActivePageChange?: (page: number) => void
43
+ /**
44
+ * Total number of pages. When provided alongside `activePage` and `onActivePageChange`,
45
+ * the component renders a fully-controlled smart paginator with Prev/Next and ellipsis.
46
+ */
47
+ pages?: number
48
+ /**
49
+ * Accessible label for the "previous page" control, used in smart pagination mode. Override
50
+ * for non-English locales.
51
+ *
52
+ * @default 'Previous'
53
+ */
54
+ previousLabel?: string
55
+ /**
56
+ * Show the numbered page buttons in smart pagination mode. Set `false` alongside
57
+ * `showPrevNext` to build a Prev/Next-only paginator.
58
+ *
59
+ * @default true
60
+ */
61
+ showPageNumbers?: boolean
62
+ /**
63
+ * Show the Prev/Next controls in smart pagination mode. Set `false` alongside
64
+ * `showPageNumbers` to build a page-numbers-only paginator.
65
+ *
66
+ * @default true
67
+ */
68
+ showPrevNext?: boolean
69
+ /**
70
+ * Size the component small or large.
71
+ */
72
+ size?: 'small' | 'large'
73
+ }
74
+
75
+ function getPageRange(activePage: number, pages: number, maxVisible: number): (number | '...')[] {
76
+ const safeMaxVisible = Math.max(1, maxVisible)
77
+
78
+ if (pages <= safeMaxVisible) {
79
+ return Array.from({ length: pages }, (_, i) => i + 1)
80
+ }
81
+
82
+ const half = Math.floor(safeMaxVisible / 2)
83
+ let start = Math.max(1, activePage - half)
84
+ const end = Math.min(pages, start + safeMaxVisible - 1)
85
+
86
+ if (end - start < safeMaxVisible - 1) {
87
+ start = Math.max(1, end - safeMaxVisible + 1)
88
+ }
89
+
90
+ const result: (number | '...')[] = []
91
+
92
+ if (start > 1) {
93
+ result.push(1)
94
+ if (start > 2) result.push('...')
95
+ }
96
+
97
+ for (let i = start; i <= end; i++) {
98
+ result.push(i)
99
+ }
100
+
101
+ if (end < pages) {
102
+ if (end < pages - 1) result.push('...')
103
+ result.push(pages)
104
+ }
105
+
106
+ return result
107
+ }
108
+
109
+ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
110
+ (
111
+ {
112
+ activePage = 1,
113
+ align,
114
+ 'aria-label': ariaLabel = 'Pagination',
115
+ children,
116
+ className,
117
+ maxVisiblePages = 5,
118
+ nextLabel = 'Next',
119
+ onActivePageChange,
120
+ pages,
121
+ previousLabel = 'Previous',
122
+ showPageNumbers = true,
123
+ showPrevNext = true,
124
+ size,
125
+ ...rest
126
+ },
127
+ ref
128
+ ) => {
129
+ const _className = classNames(
130
+ 'pagination',
131
+ size,
132
+ {
133
+ [`justify-content-${align}`]: align
134
+ },
135
+ className
136
+ )
137
+
138
+ const clampedActivePage = pages ? Math.min(Math.max(activePage, 1), pages) : activePage
139
+ const prevDisabled = pages ? clampedActivePage <= 1 : false
140
+ const nextDisabled = pages ? clampedActivePage >= pages : false
141
+
142
+ const { prevRef, nextRef, handlePrevClick, handleNextClick } = usePagination({
143
+ prevDisabled,
144
+ nextDisabled,
145
+ onPrev: () => onActivePageChange && onActivePageChange(clampedActivePage - 1),
146
+ onNext: () => onActivePageChange && onActivePageChange(clampedActivePage + 1)
147
+ })
148
+
149
+ const smartContent = pages ? (
150
+ <>
151
+ {showPrevNext && (
152
+ <PaginationItem
153
+ ref={prevRef}
154
+ disabled={prevDisabled}
155
+ onClick={handlePrevClick}
156
+ aria-label={previousLabel}
157
+ >
158
+ <Icon name="chevron-left-solid" className="directional-icon" />
159
+ </PaginationItem>
160
+ )}
161
+ {showPageNumbers &&
162
+ getPageRange(clampedActivePage, pages, maxVisiblePages).map((page, idx) =>
163
+ page === '...' ? (
164
+ // eslint-disable-next-line react/no-array-index-key
165
+ <PaginationItem key={`ellipsis-${idx}`} disabled component="span" aria-hidden="true">
166
+ &hellip;
167
+ </PaginationItem>
168
+ ) : (
169
+ <PaginationItem
170
+ key={page}
171
+ active={page === clampedActivePage}
172
+ onClick={() => onActivePageChange && onActivePageChange(page)}
173
+ >
174
+ {page}
175
+ </PaginationItem>
176
+ )
177
+ )}
178
+ {showPrevNext && (
179
+ <PaginationItem
180
+ ref={nextRef}
181
+ disabled={nextDisabled}
182
+ onClick={handleNextClick}
183
+ aria-label={nextLabel}
184
+ >
185
+ <Icon name="chevron-right-solid" className="directional-icon" />
186
+ </PaginationItem>
187
+ )}
188
+ </>
189
+ ) : null
190
+
191
+ return (
192
+ <nav aria-label={ariaLabel} ref={ref} {...rest}>
193
+ <ul className={_className}>{smartContent ?? children}</ul>
194
+ </nav>
195
+ )
196
+ }
197
+ )
198
+
199
+ Pagination.displayName = 'Pagination'
@@ -0,0 +1,133 @@
1
+ import React, {
2
+ ElementType,
3
+ ForwardRefRenderFunction,
4
+ MouseEvent,
5
+ MouseEventHandler,
6
+ ReactElement,
7
+ Ref
8
+ } from 'react'
9
+ import classNames from 'classnames'
10
+ import { mergeProps } from 'react-aria'
11
+
12
+ import { useButtonSemantics } from '../../hooks'
13
+ import {
14
+ createPolymorphicComponent,
15
+ PolymorphicComponentProps,
16
+ PolymorphicRef
17
+ } from '../../utils/polymorphic'
18
+
19
+ type PaginationItemOwnProps<C extends ElementType> = {
20
+ /**
21
+ * Toggle the active state for the component.
22
+ */
23
+ active?: boolean
24
+ /**
25
+ * Component used for the root node. Either a string to use a HTML element or a component.
26
+ */
27
+ component?: C
28
+ /**
29
+ * Toggle the disabled state for the component.
30
+ */
31
+ disabled?: boolean
32
+ /**
33
+ * The href attribute. When provided the item renders as an `<a>` tag; otherwise as a `<button>`.
34
+ */
35
+ href?: string
36
+ }
37
+
38
+ export type PaginationItemProps<C extends ElementType = 'button'> = PolymorphicComponentProps<
39
+ C,
40
+ PaginationItemOwnProps<C>
41
+ >
42
+
43
+ type PaginationItemComponent = (<C extends ElementType = 'button'>(
44
+ props: PaginationItemProps<C> & { ref?: PolymorphicRef<C> }
45
+ ) => ReactElement | null) & { displayName?: string }
46
+
47
+ function PaginationItemRender<C extends ElementType = 'button'>(
48
+ {
49
+ active,
50
+ children,
51
+ className,
52
+ component,
53
+ disabled,
54
+ href,
55
+ onClick,
56
+ ...rest
57
+ }: PaginationItemProps<C>,
58
+ ref: PolymorphicRef<C>
59
+ ) {
60
+ const _className = classNames(
61
+ 'pagination-item',
62
+ {
63
+ active,
64
+ disabled
65
+ },
66
+ className
67
+ )
68
+
69
+ const Component = (component ?? (active ? 'span' : href ? 'a' : 'button')) as ElementType
70
+
71
+ // `<a>` has no real `disabled` attribute, so a disabled anchor pagination item still fires
72
+ // click (and still navigates) unless it's blocked here, same guard `Button` applies.
73
+ const handleClick = (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
74
+ if (Component === 'a' && disabled) {
75
+ event.preventDefault()
76
+ return
77
+ }
78
+ onClick?.(event as never)
79
+ }
80
+
81
+ // A custom `component`/default (non-`button`/`a`) branch gets a raw `onClick` with no keyboard
82
+ // semantics otherwise — the same gap `Link` fills via `useButtonSemantics`.
83
+ const needsButtonSemantics = Component !== 'button' && Component !== 'a' && !!onClick
84
+ const { buttonProps, forkedRef } = useButtonSemantics<HTMLElement>(ref as Ref<HTMLElement>, {
85
+ disabled,
86
+ onClick: onClick as MouseEventHandler<HTMLElement> | undefined
87
+ })
88
+
89
+ return (
90
+ <li className={_className} {...(active && { 'aria-current': 'page' })}>
91
+ {Component === 'button' ? (
92
+ <button
93
+ {...(rest as Record<string, unknown>)}
94
+ className="pagination-link"
95
+ type="button"
96
+ disabled={disabled}
97
+ onClick={handleClick}
98
+ ref={ref as Ref<HTMLButtonElement>}
99
+ >
100
+ {children}
101
+ </button>
102
+ ) : Component === 'a' ? (
103
+ <a
104
+ {...(rest as Record<string, unknown>)}
105
+ className="pagination-link"
106
+ href={href}
107
+ onClick={handleClick}
108
+ {...(disabled && { 'aria-disabled': true, tabIndex: -1 })}
109
+ ref={ref as Ref<HTMLAnchorElement>}
110
+ >
111
+ {children}
112
+ </a>
113
+ ) : (
114
+ <Component
115
+ className="pagination-link"
116
+ {...(mergeProps(rest, needsButtonSemantics ? buttonProps : {}) as Record<
117
+ string,
118
+ unknown
119
+ >)}
120
+ {...(!needsButtonSemantics && { onClick: handleClick })}
121
+ ref={needsButtonSemantics ? forkedRef : ref}
122
+ >
123
+ {children}
124
+ </Component>
125
+ )}
126
+ </li>
127
+ )
128
+ }
129
+
130
+ export const PaginationItem = createPolymorphicComponent<PaginationItemComponent>(
131
+ PaginationItemRender as ForwardRefRenderFunction<Element, PaginationItemProps<ElementType>>,
132
+ 'PaginationItem'
133
+ )
@@ -0,0 +1,4 @@
1
+ export { Pagination } from './Pagination'
2
+ export type { PaginationProps } from './Pagination'
3
+ export { PaginationItem } from './PaginationItem'
4
+ export type { PaginationItemProps } from './PaginationItem'
@@ -0,0 +1,155 @@
1
+ import React, { Fragment, HTMLAttributes, useEffect, useMemo, useRef } from 'react'
2
+ import classNames from 'classnames'
3
+ import { useMeter } from 'react-aria'
4
+
5
+ import {
6
+ calculateScore,
7
+ defaultMessages,
8
+ defaultThresholds,
9
+ defaultWeights,
10
+ scoreToStrength,
11
+ STRENGTH_LEVELS,
12
+ StrengthLevel,
13
+ StrengthWeights
14
+ } from './strengthScore'
15
+
16
+ export interface PasswordStrengthProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
17
+ /**
18
+ * An accessible label for the meter. Defaults to `"Password strength"`.
19
+ */
20
+ 'aria-label'?: string
21
+ /**
22
+ * A string of all className you want applied to the base component.
23
+ */
24
+ className?: string
25
+ /**
26
+ * `id` forwarded to the meter element.
27
+ */
28
+ id?: string
29
+ /**
30
+ * The maximum possible score, used to compute the meter's `aria-valuemax`. Defaults to the sum
31
+ * of `weights`' values — set this explicitly when pairing a custom `scorer` whose scale doesn't
32
+ * match the built-in criteria weights, otherwise `aria-valuenow`/`aria-valuemax` get silently
33
+ * clamped to the built-in max.
34
+ */
35
+ maxScore?: number
36
+ /**
37
+ * Minimum password length required to earn the first strength point. Defaults to `8`.
38
+ */
39
+ minLength?: number
40
+ /**
41
+ * Feedback message shown for each strength level, when `showText` is `true`.
42
+ */
43
+ messages?: Partial<Record<StrengthLevel, string>>
44
+ /**
45
+ * Callback fired whenever the strength level changes, including once on mount with the
46
+ * initial `value`'s strength.
47
+ */
48
+ onStrengthChange?: (result: { score: number; strength: StrengthLevel | null }) => void
49
+ /**
50
+ * Custom scoring function overriding the built-in criteria. Receives the password and returns
51
+ * a numeric score.
52
+ */
53
+ scorer?: (password: string) => number
54
+ /**
55
+ * Renders a `.strength-text` element below the meter with the current level's message.
56
+ * Defaults to `true`.
57
+ */
58
+ showText?: boolean
59
+ /**
60
+ * Score boundaries `[weak, fair, good]` — scores above the last value are `"strong"`. Defaults
61
+ * to `[2, 4, 6]`.
62
+ */
63
+ thresholds?: [number, number, number]
64
+ /**
65
+ * The current password value to evaluate.
66
+ */
67
+ value: string
68
+ /**
69
+ * Render as four discrete segments, or a single growing bar. Defaults to `"segmented"`.
70
+ */
71
+ variant?: 'bar' | 'segmented'
72
+ /**
73
+ * Point values for each scoring criterion. Set a criterion to `0` to disable it. Merged with
74
+ * the built-in defaults.
75
+ */
76
+ weights?: Partial<StrengthWeights>
77
+ }
78
+
79
+ export const PasswordStrength = ({
80
+ 'aria-label': ariaLabel = 'Password strength',
81
+ className,
82
+ id,
83
+ maxScore,
84
+ messages,
85
+ minLength = 8,
86
+ onStrengthChange,
87
+ scorer,
88
+ showText = true,
89
+ thresholds = defaultThresholds,
90
+ value,
91
+ variant = 'segmented',
92
+ weights,
93
+ ...rest
94
+ }: PasswordStrengthProps) => {
95
+ const mergedWeights = useMemo(() => ({ ...defaultWeights, ...weights }), [weights])
96
+ const mergedMessages = useMemo(() => ({ ...defaultMessages, ...messages }), [messages])
97
+
98
+ const score = useMemo(
99
+ () => calculateScore(value, { minLength, scorer, weights: mergedWeights }),
100
+ [value, minLength, scorer, mergedWeights]
101
+ )
102
+ const strength = useMemo(() => scoreToStrength(score, thresholds), [score, thresholds])
103
+ const resolvedMaxScore = useMemo(
104
+ () => maxScore ?? Object.values(mergedWeights).reduce((total, weight) => total + weight, 0),
105
+ [maxScore, mergedWeights]
106
+ )
107
+
108
+ // Deliberately not initialized to `strength` — that would make the effect below skip firing
109
+ // for the mount-time value, and a consumer using this to gate e.g. a submit button needs the
110
+ // real initial state, not just subsequent changes.
111
+ const previousStrength = useRef<StrengthLevel | null | undefined>(undefined)
112
+ useEffect(() => {
113
+ if (previousStrength.current !== strength) {
114
+ previousStrength.current = strength
115
+ onStrengthChange?.({ score, strength })
116
+ }
117
+ }, [strength, score, onStrengthChange])
118
+
119
+ const { meterProps } = useMeter({
120
+ 'aria-label': ariaLabel,
121
+ maxValue: resolvedMaxScore,
122
+ minValue: 0,
123
+ value: score,
124
+ valueLabel: strength ? mergedMessages[strength] : undefined
125
+ })
126
+
127
+ const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1
128
+
129
+ return (
130
+ <Fragment>
131
+ <div
132
+ {...meterProps}
133
+ className={classNames(variant === 'bar' ? 'strength-bar' : 'strength', className)}
134
+ data-cx-strength={strength ?? undefined}
135
+ id={id}
136
+ {...rest}
137
+ >
138
+ {variant === 'segmented' &&
139
+ STRENGTH_LEVELS.map((level, index) => (
140
+ <div
141
+ className={classNames('strength-segment', { active: index <= strengthIndex })}
142
+ key={level}
143
+ />
144
+ ))}
145
+ </div>
146
+ {showText && (
147
+ <span aria-live="polite" className="strength-text" data-cx-strength={strength ?? undefined}>
148
+ {strength ? mergedMessages[strength] : ''}
149
+ </span>
150
+ )}
151
+ </Fragment>
152
+ )
153
+ }
154
+
155
+ PasswordStrength.displayName = 'PasswordStrength'
@@ -0,0 +1,2 @@
1
+ export { PasswordStrength } from './PasswordStrength'
2
+ export type { PasswordStrengthProps } from './PasswordStrength'
@@ -0,0 +1,79 @@
1
+ export type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong'
2
+
3
+ export const STRENGTH_LEVELS: StrengthLevel[] = ['weak', 'fair', 'good', 'strong']
4
+
5
+ export interface StrengthWeights {
6
+ extraLength: number
7
+ lowercase: number
8
+ longPassword: number
9
+ minLength: number
10
+ multipleSpecial: number
11
+ numbers: number
12
+ special: number
13
+ uppercase: number
14
+ }
15
+
16
+ export const defaultWeights: StrengthWeights = {
17
+ extraLength: 1,
18
+ lowercase: 1,
19
+ longPassword: 1,
20
+ minLength: 1,
21
+ multipleSpecial: 1,
22
+ numbers: 1,
23
+ special: 1,
24
+ uppercase: 1
25
+ }
26
+
27
+ export const defaultMessages: Record<StrengthLevel, string> = {
28
+ fair: 'Fair',
29
+ good: 'Good',
30
+ strong: 'Strong',
31
+ weak: 'Weak'
32
+ }
33
+
34
+ export const defaultThresholds: [number, number, number] = [2, 4, 6]
35
+
36
+ const SPECIAL_CHARS = /[!"#$%&()*,.:<>?@^{|}]/
37
+ const MULTIPLE_SPECIAL_CHARS = /[!"#$%&()*,.:<>?@^{|}].*[!"#$%&()*,.:<>?@^{|}]/
38
+
39
+ interface CalculateScoreOptions {
40
+ minLength: number
41
+ scorer?: (password: string) => number
42
+ weights: StrengthWeights
43
+ }
44
+
45
+ // Ported from chassis-css's vanilla strength.js `_calculateScore` — kept in lockstep with it so
46
+ // a password scores identically whether evaluated by the vanilla JS or this component.
47
+ export const calculateScore = (
48
+ password: string,
49
+ { minLength, scorer, weights }: CalculateScoreOptions
50
+ ): number => {
51
+ if (!password) return 0
52
+ if (scorer) return scorer(password)
53
+
54
+ let score = 0
55
+
56
+ if (password.length >= minLength) score += weights.minLength
57
+ if (password.length >= minLength + 4) score += weights.extraLength
58
+ if (/[a-z]/.test(password)) score += weights.lowercase
59
+ if (/[A-Z]/.test(password)) score += weights.uppercase
60
+ if (/\d/.test(password)) score += weights.numbers
61
+ if (SPECIAL_CHARS.test(password)) score += weights.special
62
+ if (MULTIPLE_SPECIAL_CHARS.test(password)) score += weights.multipleSpecial
63
+ if (password.length >= 16) score += weights.longPassword
64
+
65
+ return score
66
+ }
67
+
68
+ export const scoreToStrength = (
69
+ score: number,
70
+ thresholds: [number, number, number]
71
+ ): StrengthLevel | null => {
72
+ if (score === 0) return null
73
+
74
+ const [weak, fair, good] = thresholds
75
+ if (score <= weak) return 'weak'
76
+ if (score <= fair) return 'fair'
77
+ if (score <= good) return 'good'
78
+ return 'strong'
79
+ }