@clicktap/ui 0.14.12 → 0.14.13

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 (191) hide show
  1. package/components/Accordion/Accordion.tsx +82 -0
  2. package/components/Accordion/index.ts +3 -0
  3. package/components/Avatar/Avatar.stories.tsx +99 -0
  4. package/components/Avatar/Avatar.tsx +120 -0
  5. package/components/Avatar/Avatar.types.ts +3 -0
  6. package/components/Avatar/AvatarGroup/AvatarGroup.tsx +32 -0
  7. package/components/Avatar/AvatarGroup/AvatarGroup.types.ts +8 -0
  8. package/components/Avatar/index.ts +4 -0
  9. package/components/Badge/Badge.stories.tsx +72 -0
  10. package/components/Badge/Badge.tsx +169 -0
  11. package/components/Badge/Badge.types.ts +3 -0
  12. package/components/Badge/index.ts +2 -0
  13. package/components/Breadcrumbs/BreadcrumbEllipsis.tsx +47 -0
  14. package/components/Breadcrumbs/BreadcrumbEllipsis.types.ts +5 -0
  15. package/components/Breadcrumbs/BreadcrumbItem.tsx +23 -0
  16. package/components/Breadcrumbs/BreadcrumbItem.types.ts +3 -0
  17. package/components/Breadcrumbs/BreadcrumbLink.tsx +30 -0
  18. package/components/Breadcrumbs/BreadcrumbLink.types.ts +3 -0
  19. package/components/Breadcrumbs/BreadcrumbSeparator.tsx +41 -0
  20. package/components/Breadcrumbs/BreadcrumbSeparator.types.ts +9 -0
  21. package/components/Breadcrumbs/Breadcrumbs.tsx +28 -0
  22. package/components/Breadcrumbs/Breadcrumbs.types.ts +6 -0
  23. package/components/Breadcrumbs/index.ts +10 -0
  24. package/components/Button/Button.tsx +72 -0
  25. package/components/Button/Button.types.ts +7 -0
  26. package/components/Button/index.ts +2 -0
  27. package/components/Card/Card.tsx +15 -0
  28. package/components/Card/Card.types.ts +3 -0
  29. package/components/Card/index.ts +2 -0
  30. package/components/Checkbox/Checkbox.tsx +122 -0
  31. package/components/Checkbox/Checkbox.types.ts +15 -0
  32. package/components/Checkbox/index.ts +2 -0
  33. package/components/Collapsible/Collapsible.tsx +34 -0
  34. package/components/Collapsible/Collapsible.types.ts +5 -0
  35. package/components/Collapsible/CollapsibleTrigger.tsx +57 -0
  36. package/components/Collapsible/CollapsibleTrigger.types.ts +14 -0
  37. package/components/Collapsible/index.ts +10 -0
  38. package/components/Container/Container.tsx +26 -0
  39. package/components/Container/Container.types.ts +3 -0
  40. package/components/Container/index.ts +2 -0
  41. package/components/ContextMenu/ContextMenu.tsx +74 -0
  42. package/components/ContextMenu/ContextMenu.types.ts +17 -0
  43. package/components/ContextMenu/index.ts +2 -0
  44. package/components/CreditCardExpirationInput/CreditCardExpirationInput.tsx +115 -0
  45. package/components/CreditCardExpirationInput/CreditCardExpirationInput.types.ts +10 -0
  46. package/components/CreditCardExpirationInput/index.ts +2 -0
  47. package/components/CreditCardInput/CreditCardInput.tsx +147 -0
  48. package/components/CreditCardInput/CreditCardInput.types.ts +12 -0
  49. package/components/CreditCardInput/index.ts +2 -0
  50. package/components/DateInput/DateInput.tsx +81 -0
  51. package/components/DateInput/DateInput.types.ts +15 -0
  52. package/components/DateInput/index.ts +2 -0
  53. package/components/DateTimeFormat/DateTimeFormat.tsx +16 -0
  54. package/components/DateTimeFormat/DateTimeFormat.types.ts +7 -0
  55. package/components/DateTimeFormat/index.ts +2 -0
  56. package/components/Dialog/Dialog.tsx +65 -0
  57. package/components/Dialog/Dialog.types.ts +9 -0
  58. package/components/Dialog/index.ts +2 -0
  59. package/components/DialogTrigger/DialogTrigger.tsx +45 -0
  60. package/components/DialogTrigger/DialogTrigger.types.ts +6 -0
  61. package/components/DialogTrigger/index.ts +5 -0
  62. package/components/Divider/Divider.stories.tsx +37 -0
  63. package/components/Divider/Divider.tsx +34 -0
  64. package/components/Divider/Divider.types.ts +5 -0
  65. package/components/Divider/index.ts +2 -0
  66. package/components/DobInput/DobInput.tsx +120 -0
  67. package/components/DobInput/index.ts +2 -0
  68. package/components/Drawer/Drawer.tsx +126 -0
  69. package/components/Drawer/Drawer.types.ts +11 -0
  70. package/components/Drawer/index.ts +2 -0
  71. package/components/Icon/Account.tsx +50 -0
  72. package/components/Icon/Cart.tsx +43 -0
  73. package/components/Icon/Checkmark.tsx +34 -0
  74. package/components/Icon/Cross.tsx +36 -0
  75. package/components/Icon/DownArrow.tsx +23 -0
  76. package/components/Icon/Hamburger.tsx +23 -0
  77. package/components/Icon/Icon.types.ts +8 -0
  78. package/components/Icon/LinkArrow.tsx +32 -0
  79. package/components/Icon/Minus.tsx +20 -0
  80. package/components/Icon/Plus.tsx +20 -0
  81. package/components/Icon/Search.tsx +36 -0
  82. package/components/Icon/Trash.tsx +27 -0
  83. package/components/Icon/Verified.tsx +20 -0
  84. package/components/Icon/index.ts +14 -0
  85. package/components/Image/Image.tsx +32 -0
  86. package/components/Image/index.ts +2 -0
  87. package/components/Input/Input.tsx +109 -0
  88. package/components/Input/Input.types.ts +17 -0
  89. package/components/Input/index.ts +2 -0
  90. package/components/Link/Link.stories.tsx +96 -0
  91. package/components/Link/Link.tsx +34 -0
  92. package/components/Link/Link.types.ts +3 -0
  93. package/components/Link/index.ts +2 -0
  94. package/components/Loader/CircularEasing.tsx +66 -0
  95. package/components/Loader/CircularEasing.types.ts +8 -0
  96. package/components/Loader/Pulse.tsx +45 -0
  97. package/components/Loader/Pulse.types.ts +5 -0
  98. package/components/Loader/index.ts +4 -0
  99. package/components/Menu/ContextMenu.tsx +83 -0
  100. package/components/Menu/Menu.tsx +143 -0
  101. package/components/Menu/Menu.types.ts +44 -0
  102. package/components/Menu/index.ts +4 -0
  103. package/components/Meter/Meter.stories.tsx +111 -0
  104. package/components/Meter/Meter.tsx +68 -0
  105. package/components/Meter/Meter.types.ts +10 -0
  106. package/components/Meter/index.ts +2 -0
  107. package/components/Modal/Modal.tsx +16 -0
  108. package/components/Modal/Modal.types.ts +6 -0
  109. package/components/Modal/index.ts +2 -0
  110. package/components/ModalOverlay/ModalOverlay.tsx +121 -0
  111. package/components/ModalOverlay/ModalOverlay.types.ts +18 -0
  112. package/components/ModalOverlay/index.ts +2 -0
  113. package/components/NumberFormat/NumberFormat.tsx +19 -0
  114. package/components/NumberFormat/NumberFormat.types.ts +8 -0
  115. package/components/NumberFormat/index.ts +2 -0
  116. package/components/NumberInput/NumberInput.tsx +164 -0
  117. package/components/NumberInput/NumberInput.types.ts +22 -0
  118. package/components/NumberInput/index.ts +2 -0
  119. package/components/NumberTicker/DigitResolver.tsx +119 -0
  120. package/components/NumberTicker/DigitResolver.types.ts +18 -0
  121. package/components/NumberTicker/NumberTicker.tsx +56 -0
  122. package/components/NumberTicker/NumberTicker.types.ts +96 -0
  123. package/components/NumberTicker/hooks/useColumnTransition.ts +36 -0
  124. package/components/NumberTicker/hooks/useNumberDelta.ts +19 -0
  125. package/components/NumberTicker/hooks/useNumberTicker.ts +36 -0
  126. package/components/NumberTicker/index.ts +10 -0
  127. package/components/Pagination/Pagination.tsx +44 -0
  128. package/components/Pagination/index.ts +2 -0
  129. package/components/PasswordCheck/PasswordCheck.tsx +59 -0
  130. package/components/PasswordCheck/PasswordCheck.types.ts +4 -0
  131. package/components/PasswordCheck/PasswordCheck.utils.ts +47 -0
  132. package/components/PasswordCheck/index.ts +2 -0
  133. package/components/PhoneInput/PhoneInput.tsx +191 -0
  134. package/components/PhoneInput/index.ts +2 -0
  135. package/components/PinInput/PinInput.tsx +314 -0
  136. package/components/PinInput/PinInput.types.ts +21 -0
  137. package/components/PinInput/index.ts +2 -0
  138. package/components/Progressbar/CircularProgressbar.tsx +71 -0
  139. package/components/Progressbar/CircularProgressbar.types.ts +10 -0
  140. package/components/Progressbar/LinearProgressbar.tsx +75 -0
  141. package/components/Progressbar/LinearProgressbar.types.ts +11 -0
  142. package/components/Progressbar/index.ts +4 -0
  143. package/components/Radio/Radio.tsx +88 -0
  144. package/components/Radio/Radio.types.ts +16 -0
  145. package/components/Radio/index.ts +2 -0
  146. package/components/RadioGroup/RadioGroup.tsx +49 -0
  147. package/components/RadioGroup/RadioGroup.types.ts +7 -0
  148. package/components/RadioGroup/index.ts +2 -0
  149. package/components/Select/Option.tsx +32 -0
  150. package/components/Select/Option.types.ts +3 -0
  151. package/components/Select/Select.tsx +253 -0
  152. package/components/Select/Select.types.ts +42 -0
  153. package/components/Select/index.ts +8 -0
  154. package/components/Skeleton/Skeleton.tsx +15 -0
  155. package/components/Skeleton/Skeleton.types.ts +3 -0
  156. package/components/Skeleton/index.ts +2 -0
  157. package/components/Slider/Slider.tsx +110 -0
  158. package/components/Slider/Slider.types.ts +11 -0
  159. package/components/Slider/index.ts +2 -0
  160. package/components/Switch/Switch.tsx +63 -0
  161. package/components/Switch/Switch.types.ts +8 -0
  162. package/components/Switch/index.ts +2 -0
  163. package/components/Table/Table.tsx +52 -0
  164. package/components/Table/Table.types.ts +22 -0
  165. package/components/Table/index.ts +2 -0
  166. package/components/Tabs/Tab.tsx +118 -0
  167. package/components/Tabs/Tab.types.ts +10 -0
  168. package/components/Tabs/TabList.tsx +51 -0
  169. package/components/Tabs/TabList.types.ts +12 -0
  170. package/components/Tabs/TabPanel.tsx +19 -0
  171. package/components/Tabs/TabPanel.types.ts +3 -0
  172. package/components/Tabs/Tabs.context.tsx +9 -0
  173. package/components/Tabs/Tabs.tsx +39 -0
  174. package/components/Tabs/Tabs.types.ts +3 -0
  175. package/components/Tabs/index.ts +9 -0
  176. package/components/TimeInput/TimeInput.stories.tsx +125 -0
  177. package/components/TimeInput/TimeInput.tsx +81 -0
  178. package/components/TimeInput/TimeInput.types.ts +15 -0
  179. package/components/TimeInput/index.ts +2 -0
  180. package/components/ToggleButton/ToggleButton.stories.tsx +89 -0
  181. package/components/ToggleButton/ToggleButton.tsx +69 -0
  182. package/components/ToggleButton/ToggleButton.types.ts +6 -0
  183. package/components/ToggleButton/index.ts +2 -0
  184. package/components/Tooltip/Tooltip.tsx +59 -0
  185. package/components/Tooltip/Tooltip.types.ts +3 -0
  186. package/components/Tooltip/index.ts +2 -0
  187. package/components/UploadImage/UploadImage.tsx +206 -0
  188. package/components/UploadImage/UploadImage.types.ts +15 -0
  189. package/components/UploadImage/index.ts +2 -0
  190. package/package.json +1 -1
  191. package/tailwind.config.js +3 -1
@@ -0,0 +1,191 @@
1
+ 'use client';
2
+
3
+ import { useFilter } from 'react-aria';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import {
6
+ CountryIso2,
7
+ defaultCountries,
8
+ parseCountry,
9
+ usePhoneInput,
10
+ } from 'react-international-phone';
11
+ import {
12
+ Button,
13
+ ListBox,
14
+ ListBoxItem,
15
+ Input as AriaInput,
16
+ DialogTrigger,
17
+ } from 'react-aria-components';
18
+ import type { TextFieldProps } from 'react-aria-components';
19
+ import { cn } from '../../utils/cn';
20
+ import { Input } from '../Input/Input';
21
+ import type { SlotsToClasses } from '../../types/SlotsToClasses';
22
+
23
+ interface PhoneInputProps extends Omit<TextFieldProps, 'type'> {
24
+ label?: string;
25
+ description?: string;
26
+ errorMessage?: string;
27
+ placeholder?: string;
28
+ classNames?: SlotsToClasses<'label' | 'input' | 'description' | 'error'>;
29
+ defaultCountry?: CountryIso2;
30
+ }
31
+
32
+ const options = defaultCountries.map(parseCountry);
33
+
34
+ export function PhoneInput({
35
+ value,
36
+ onChange,
37
+ defaultCountry = 'us',
38
+ ...props
39
+ }: PhoneInputProps) {
40
+ const triggerRef = useRef<HTMLDivElement>(null);
41
+ const searchRef = useRef<HTMLInputElement>(null);
42
+ const { inputValue, handlePhoneValueChange, inputRef, country, setCountry } =
43
+ usePhoneInput({
44
+ defaultCountry,
45
+ value,
46
+ countries: defaultCountries,
47
+ onChange: (data) => {
48
+ onChange?.(data.phone);
49
+ },
50
+ forceDialCode: true,
51
+ });
52
+ const dialCodeWithPrefix = `+${country.dialCode}`;
53
+
54
+ // eslint-disable-next-line @typescript-eslint/unbound-method
55
+ const { startsWith } = useFilter({ sensitivity: 'base' });
56
+ const [filterValue, setFilterValue] = useState('');
57
+ const [isOpen, onOpenChange] = useState(false);
58
+
59
+ const filteredItems = useMemo(
60
+ () => options.filter((item) => startsWith(item.name, filterValue)),
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ [filterValue]
63
+ );
64
+
65
+ useEffect(() => {
66
+ if (isOpen) {
67
+ searchRef.current?.focus();
68
+ }
69
+ }, [isOpen]);
70
+
71
+ return (
72
+ <div
73
+ className={cn('flex relative', props.label ? 'pt-4' : '')}
74
+ aria-label={props['aria-label']}
75
+ >
76
+ <DialogTrigger isOpen={isOpen} onOpenChange={onOpenChange}>
77
+ <Button
78
+ aria-label="Select Country"
79
+ className="flex px-4 h-10 text-sm items-center rounded-l-md bg-slate-100 gap-2 uppercase [&[aria-expanded=false]_svg]:rotate-180 border border-r-0 border-slate-300"
80
+ excludeFromTabOrder
81
+ >
82
+ {country.iso2}
83
+ <svg
84
+ width="15"
85
+ height="8"
86
+ viewBox="0 0 15 8"
87
+ fill="none"
88
+ xmlns="http://www.w3.org/2000/svg"
89
+ >
90
+ <path d="M1 7L7.5 0.999999L14 7" stroke="currentColor" />
91
+ </svg>
92
+ </Button>
93
+
94
+ <div
95
+ className={cn(
96
+ 'w-full grid absolute left-0 top-[calc(100%+.25rem)] bg-white z-50 rounded-md text-slate-600 border border-slate-300',
97
+ 'shadow-inner',
98
+ isOpen ? 'block' : 'hidden'
99
+ )}
100
+ ref={triggerRef}
101
+ >
102
+ <svg
103
+ xmlns="http://www.w3.org/2000/svg"
104
+ width="16"
105
+ height="16"
106
+ fill="none"
107
+ viewBox="0 0 17 16"
108
+ className="absolute top-4 left-[1.325rem]"
109
+ >
110
+ <path
111
+ fill="currentColor"
112
+ fillRule="evenodd"
113
+ d="M12.3 6.9a4.9 4.9 0 1 1-9.8 0 4.9 4.9 0 0 1 9.8 0Zm-1.1 5.7c-1.1.7-2.4 1.2-3.8 1.2a6.9 6.9 0 1 1 5.3-2.5l3 3-1.4 1.4-3.1-3Z"
114
+ clipRule="evenodd"
115
+ />
116
+ </svg>
117
+
118
+ <AriaInput
119
+ aria-label="Search for countries"
120
+ placeholder="Search for countries"
121
+ className={cn(
122
+ 'pl-12 pr-5 py-4 w-full m-0 border-0 text-slate-950 transition-all duration-[0.25s] ease outline-0 bg-transparent',
123
+ 'placeholder:text-slate-950 focus:outline-0',
124
+ 'border-b border-slate-200'
125
+ )}
126
+ onChange={(event) => setFilterValue(event.target.value)}
127
+ onBlur={() => {
128
+ onOpenChange(false);
129
+ inputRef.current?.focus();
130
+ }}
131
+ value={filterValue}
132
+ ref={searchRef}
133
+ />
134
+
135
+ <div className="p-2.5">
136
+ <ListBox
137
+ className="text-slate-950 max-h-60 overflow-y-auto"
138
+ selectionMode="single"
139
+ aria-label="Countries List"
140
+ onSelectionChange={(selection) => {
141
+ setCountry(String([...selection][0]));
142
+ setFilterValue('');
143
+ onOpenChange(false);
144
+ }}
145
+ >
146
+ {(isOpen ? filteredItems : []).map((parsedCountry) => (
147
+ <ListBoxItem
148
+ id={parsedCountry.iso2}
149
+ key={parsedCountry.iso2}
150
+ value={parsedCountry}
151
+ textValue={parsedCountry.iso2}
152
+ className="p-3 text-slate-900 data-[hovered]:bg-slate-100 rounded-md"
153
+ >
154
+ {parsedCountry.name} (+{parsedCountry.dialCode})
155
+ </ListBoxItem>
156
+ ))}
157
+ </ListBox>
158
+ </div>
159
+ </div>
160
+ </DialogTrigger>
161
+
162
+ <Input
163
+ // eslint-disable-next-line react/jsx-props-no-spreading
164
+ {...props}
165
+ value={inputValue}
166
+ inputProps={{
167
+ onChange: handlePhoneValueChange,
168
+ }}
169
+ ref={inputRef}
170
+ classNames={{
171
+ input: cn(
172
+ 'rounded-l-none',
173
+ inputValue.trim() === dialCodeWithPrefix ? 'text-slate-600' : ''
174
+ ),
175
+ label: 'absolute top-0 left-0',
176
+ }}
177
+ />
178
+ </div>
179
+ );
180
+ }
181
+
182
+ PhoneInput.defaultProps = {
183
+ label: undefined,
184
+ description: undefined,
185
+ errorMessage: undefined,
186
+ placeholder: undefined,
187
+ classNames: undefined,
188
+ defaultCountry: undefined,
189
+ };
190
+
191
+ export default PhoneInput;
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export { PhoneInput } from './PhoneInput';
@@ -0,0 +1,314 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import type { ChangeEvent, ClipboardEvent, KeyboardEvent } from 'react';
5
+ import {
6
+ FieldError,
7
+ Group,
8
+ Input,
9
+ Label,
10
+ Text,
11
+ TextField,
12
+ } from 'react-aria-components';
13
+ import { cn } from '../../utils/cn';
14
+ import type { PinInputProps } from './PinInput.types';
15
+
16
+ /** based on https://github.com/chakra-ui/chakra-ui/blob/main/packages/components/src/pin-input/use-pin-input.ts */
17
+ export function PinInput({
18
+ description,
19
+ errorMessage,
20
+ isDisabled = false,
21
+ isInvalid = false,
22
+ isMasked = false,
23
+ isRequired = true,
24
+ label,
25
+ length = 6,
26
+ name,
27
+ onChange: controlledOnChange,
28
+ value = '',
29
+ type = 'numeric',
30
+ validationBehavior = 'native',
31
+ className,
32
+ classNames,
33
+ ...props
34
+ }: PinInputProps) {
35
+ // const [focusedIndex, setFocusedIndex] = useState(-1);
36
+ const [joinedValue, setJoinedValue] = useState(value);
37
+ const [values, setValues] = useState<string[]>(Array(length).fill(''));
38
+ const inputRefs = useRef<HTMLInputElement[]>([]);
39
+
40
+ useEffect(() => {
41
+ inputRefs.current = inputRefs.current.slice(0, length);
42
+ }, [length]);
43
+
44
+ useEffect(() => {
45
+ const updatedValue = values.join('');
46
+ setJoinedValue(updatedValue);
47
+ if (controlledOnChange) controlledOnChange(updatedValue);
48
+ }, [controlledOnChange, values]);
49
+
50
+ // const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
51
+ // const eventValue = event.target.value;
52
+ // const currentValue = values[index];
53
+ // const nextValue = getNextValue(currentValue, eventValue);
54
+ // const allItems = getAllItems(containerRef.current);
55
+
56
+ // // if the value was removed using backspace
57
+ // if (nextValue === '') {
58
+ // setValue('', index);
59
+ // return;
60
+ // }
61
+
62
+ // // in the case of an autocomplete or copy and paste
63
+ // if (eventValue.length > 2) {
64
+ // // see if we can use the string to fill out our values
65
+ // if (validate(eventValue, type)) {
66
+ // // Ensure the value matches the number of inputs
67
+ // const nextValue = eventValue
68
+ // .split('')
69
+ // .filter((_, index) => index < allItems.length);
70
+
71
+ // setValues(nextValue);
72
+ // // if pasting fills the entire input fields, trigger `onComplete`
73
+ // if (nextValue.length === allItems.length) {
74
+ // onComplete?.(nextValue.join(''));
75
+ // }
76
+ // }
77
+ // } else {
78
+ // // only set if the new value is a number
79
+ // if (validate(nextValue, type)) {
80
+ // setValue(nextValue, index);
81
+ // }
82
+
83
+ // setMoveFocus(true);
84
+ // }
85
+ // };
86
+ const onChange = (event: ChangeEvent<HTMLInputElement>) => {
87
+ const inputValue = event.target.value;
88
+ const inputIndex = Number(
89
+ event.target.getAttribute('data-pin-input-index')
90
+ );
91
+
92
+ if (inputValue !== '' && inputIndex < values.length - 1) {
93
+ inputRefs.current?.[inputIndex + 1]?.focus();
94
+ }
95
+
96
+ const updatedValues = values.map((v, i) =>
97
+ i === inputIndex ? inputValue : v
98
+ );
99
+ setValues(updatedValues);
100
+
101
+ // return setValues(updatedValues);
102
+ };
103
+
104
+ const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
105
+ // const inputValue = event.currentTarget.value;
106
+ const inputIndex = Number(
107
+ event.currentTarget.getAttribute('data-pin-input-index')
108
+ );
109
+
110
+ // Allow standard keyboard shortcuts
111
+ if (event.ctrlKey || event.metaKey) {
112
+ return;
113
+ }
114
+
115
+ /** @todo is there a better way to pass through control keys? */
116
+ const allowedKeys = [
117
+ 'Backspace',
118
+ 'Delete',
119
+ 'ArrowLeft',
120
+ 'ArrowRight',
121
+ 'ArrowUp',
122
+ 'ArrowDown',
123
+ 'Control',
124
+ 'Alt',
125
+ 'Meta',
126
+ 'Shift',
127
+ 'Tab',
128
+ 'Enter',
129
+ 'Escape',
130
+ ];
131
+
132
+ const regex = {
133
+ alpha: /^[a-z]$/i,
134
+ alphanumeric: /^[a-z0-9]$/i,
135
+ numeric: /^[0-9]$/i,
136
+ };
137
+
138
+ if (!event.key.match(regex[type]) && !allowedKeys.includes(event.key)) {
139
+ event.preventDefault();
140
+ }
141
+
142
+ if (
143
+ values[inputIndex] === '' &&
144
+ inputIndex > 0 &&
145
+ event.key === 'Backspace'
146
+ ) {
147
+ const updatedValues = values.map((v, i) =>
148
+ i === inputIndex - 1 ? '' : v
149
+ );
150
+ setValues(updatedValues);
151
+ inputRefs.current?.[inputIndex - 1]?.focus();
152
+ event.preventDefault();
153
+ }
154
+
155
+ if (
156
+ values[inputIndex] === '' &&
157
+ inputIndex < values.length - 1 &&
158
+ event.key === 'Delete'
159
+ ) {
160
+ const updatedValues = values.map((v, i) =>
161
+ i === inputIndex + 1 ? '' : v
162
+ );
163
+ setValues(updatedValues);
164
+ inputRefs.current?.[inputIndex + 1]?.focus();
165
+ event.preventDefault();
166
+ }
167
+
168
+ // const prevValues = value;
169
+ // prevValues[inputIndex] += inputValue;
170
+ // setValue(prevValues);
171
+ };
172
+
173
+ const onPaste = (event: ClipboardEvent<HTMLInputElement>) => {
174
+ const inputIndex = Number(
175
+ event.currentTarget.getAttribute('data-pin-input-index')
176
+ );
177
+
178
+ event.preventDefault();
179
+
180
+ const pasteData = event.clipboardData?.getData('text');
181
+ if (!pasteData) return;
182
+
183
+ /** @todo update to fail paste if contains invalid chars? */
184
+ const splitValue = pasteData.split('').filter((char) => {
185
+ switch (type) {
186
+ case 'alpha':
187
+ return /^[a-zA-Z]$/.test(char);
188
+ case 'alphanumeric':
189
+ return /^[a-z0-9]$/i.test(char);
190
+ case 'numeric':
191
+ default:
192
+ return /^[0-9]$/.test(char);
193
+ }
194
+ });
195
+
196
+ if (splitValue.length === 0) {
197
+ return;
198
+ }
199
+
200
+ const updatedValues = [...values];
201
+
202
+ let focusIndex = inputIndex;
203
+
204
+ // eslint-disable-next-line no-plusplus
205
+ for (let i = 0; i < splitValue.length && inputIndex + i < length; i++) {
206
+ updatedValues[inputIndex + i] = splitValue[i];
207
+ focusIndex = inputIndex + i;
208
+ }
209
+
210
+ setValues(updatedValues);
211
+
212
+ const nextInputIndex =
213
+ focusIndex + 1 < length ? focusIndex + 1 : length - 1;
214
+ inputRefs.current[nextInputIndex]?.focus();
215
+ };
216
+
217
+ return (
218
+ <Group
219
+ className={cn('flex flex-wrap gap-2', className)}
220
+ aria-label={label}
221
+ // eslint-disable-next-line react/jsx-props-no-spreading
222
+ {...props}
223
+ >
224
+ <Label
225
+ className={cn(
226
+ 'flex text-slate-500 text-sm grow shrink-0 basis-full',
227
+ classNames?.label
228
+ )}
229
+ >
230
+ {label}
231
+ </Label>
232
+
233
+ {values.map((v, i) => (
234
+ <TextField
235
+ className={cn(
236
+ 'flex flex-col w-full flex-1 text-slate-900',
237
+ classNames?.inputWrap
238
+ )}
239
+ // eslint-disable-next-line react/no-array-index-key
240
+ key={`pin-input-${i}`}
241
+ aria-label={`Pin Input Digit ${i + 1}`}
242
+ isDisabled={isDisabled}
243
+ isInvalid={isInvalid}
244
+ isRequired={isRequired}
245
+ validationBehavior={validationBehavior}
246
+ >
247
+ <Input
248
+ className={cn(
249
+ 'border-solid border border-slate-300 rounded-md',
250
+ 'text-sm text-slate-900 placeholder-slate-400 text-center',
251
+ 'h-10 px-2 py-0 m-0 w-full',
252
+ 'bg-white',
253
+ 'transition-all duration-200 ease-in-out',
254
+ 'hover:border-slate-400',
255
+ 'focus:outline-2 focus:outline focus:outline-slate-200 focus:border-slate-400',
256
+ 'disabled:border-slate-200 disabled:bg-slate-100',
257
+ 'invalid:border-red-500 invalid:bg-red-100 invalid:text-red-600',
258
+ 'invalid:hover:border-red-600 invalid:focus:border-red-600 invalid:focus:outline-red-200',
259
+ classNames?.input
260
+ )}
261
+ onChange={onChange}
262
+ onKeyDown={onKeyDown}
263
+ onPaste={onPaste}
264
+ type={isMasked ? 'password' : 'text'}
265
+ // eslint-disable-next-line no-return-assign
266
+ ref={(el) => (el ? (inputRefs.current[i] = el) : null)}
267
+ value={v}
268
+ maxLength={1}
269
+ name={name && `${name}-${i}`}
270
+ // pattern="[0-9]*"
271
+ data-pin-input-index={i}
272
+ />
273
+ {/* {description && (
274
+ <StyledText slot="description">{description}</StyledText>
275
+ )} */}
276
+ </TextField>
277
+ ))}
278
+ <TextField
279
+ className={cn(
280
+ 'flex flex-row flex-wrap grow shrink-0 basis-full',
281
+ classNames?.textWrap
282
+ )}
283
+ aria-label="Pin Input"
284
+ isDisabled={isDisabled}
285
+ isInvalid={isInvalid}
286
+ isRequired={isRequired}
287
+ validationBehavior={validationBehavior}
288
+ >
289
+ {name && <Input type="hidden" name={name} value={joinedValue} />}
290
+ {description && (
291
+ <Text
292
+ className={cn(
293
+ 'flex text-slate-500 text-sm grow shrink-0 basis-full',
294
+ classNames?.description
295
+ )}
296
+ slot="description"
297
+ >
298
+ {description}
299
+ </Text>
300
+ )}
301
+ <FieldError
302
+ className={cn(
303
+ 'flex text-red-500 text-sm grow shrink-0 basis-full',
304
+ classNames?.error
305
+ )}
306
+ >
307
+ {errorMessage}
308
+ </FieldError>
309
+ </TextField>
310
+ </Group>
311
+ );
312
+ }
313
+
314
+ export default PinInput;
@@ -0,0 +1,21 @@
1
+ import type { GroupProps, ValidationResult } from 'react-aria-components';
2
+ import type { SlotsToClasses } from '../../types/SlotsToClasses';
3
+
4
+ /** @todo extend certain textfield props like name, validationBehavior and isRequired */
5
+ export interface PinInputProps extends GroupProps {
6
+ description?: string;
7
+ errorMessage?: string | ((validation: ValidationResult) => string);
8
+ label?: string;
9
+ length?: number;
10
+ name?: string;
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ onChange?: (...event: any[]) => void;
13
+ value?: string;
14
+ isMasked?: boolean;
15
+ isRequired?: boolean;
16
+ type?: 'alpha' | 'alphanumeric' | 'numeric';
17
+ validationBehavior?: 'native' | 'aria';
18
+ classNames?: SlotsToClasses<
19
+ 'label' | 'input' | 'description' | 'error' | 'inputWrap' | 'textWrap'
20
+ >;
21
+ }
@@ -0,0 +1,2 @@
1
+ export { PinInput } from './PinInput';
2
+ export type { PinInputProps } from './PinInput.types';
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import { ProgressBar, Label } from 'react-aria-components';
4
+ import { cn } from '../../utils/cn';
5
+ import type { CircularProgressbarProps } from './CircularProgressbar.types';
6
+
7
+ export function CircularProgressbar({
8
+ label,
9
+ value = 0,
10
+ showValue = true,
11
+ size = 60,
12
+ strokeWidth = 6,
13
+ className,
14
+ classNames,
15
+ ...props
16
+ }: CircularProgressbarProps) {
17
+ const center = size / 2;
18
+ const radius = center - strokeWidth;
19
+ const circumference = 2 * Math.PI * radius;
20
+
21
+ return (
22
+ <ProgressBar
23
+ aria-label="Loading..."
24
+ className={cn('flex', className)}
25
+ // eslint-disable-next-line react/jsx-props-no-spreading
26
+ {...props}
27
+ value={value}
28
+ >
29
+ {({ percentage = 0 }) => (
30
+ <div className={cn('flex items-center flex-col', classNames?.base)}>
31
+ <Label className={cn('text-sm', classNames?.label)}>{label}</Label>
32
+ <svg width={size} height={size} fill="none">
33
+ <circle
34
+ className={cn('stroke-slate-300')}
35
+ cx={center}
36
+ cy={center}
37
+ r={radius}
38
+ strokeWidth={strokeWidth}
39
+ />
40
+ <circle
41
+ className={cn(
42
+ 'stroke-slate-800 transition-stroke-dashoffset duration-500 ease-in-out'
43
+ )}
44
+ cx={center}
45
+ cy={center}
46
+ r={radius}
47
+ strokeWidth={strokeWidth}
48
+ strokeDasharray={`${circumference} ${circumference}`}
49
+ strokeDashoffset={((100 - percentage) / 100) * circumference}
50
+ strokeLinecap="round"
51
+ transform={`rotate(-90 ${center} ${center})`}
52
+ />
53
+ {showValue && (
54
+ <text
55
+ className={cn('text-sm fill-slate-800', classNames?.value)}
56
+ x="50%"
57
+ y="50%"
58
+ alignmentBaseline="middle"
59
+ textAnchor="middle"
60
+ >
61
+ {percentage}%
62
+ </text>
63
+ )}
64
+ </svg>
65
+ </div>
66
+ )}
67
+ </ProgressBar>
68
+ );
69
+ }
70
+
71
+ export default CircularProgressbar;
@@ -0,0 +1,10 @@
1
+ import type { ProgressBarProps } from 'react-aria-components';
2
+ import type { SlotsToClasses } from '../../types/SlotsToClasses';
3
+
4
+ export interface CircularProgressbarProps extends ProgressBarProps {
5
+ label?: string;
6
+ showValue?: boolean;
7
+ size?: number;
8
+ strokeWidth?: number;
9
+ classNames?: SlotsToClasses<'label' | 'base' | 'value'>;
10
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import { ProgressBar, Label } from 'react-aria-components';
4
+ import { motion } from 'framer-motion';
5
+ import { cn } from '../../utils/cn';
6
+ import type { LinearProgressbarProps } from './LinearProgressbar.types';
7
+
8
+ export function LinearProgressbar({
9
+ label,
10
+ isIndeterminate,
11
+ showValue = true,
12
+ width,
13
+ className,
14
+ classNames,
15
+ ...props
16
+ }: LinearProgressbarProps) {
17
+ return (
18
+ <ProgressBar
19
+ style={{ width: width || '100%' }}
20
+ className={cn('overflow-hidden', className)}
21
+ // eslint-disable-next-line react/jsx-props-no-spreading
22
+ {...props}
23
+ >
24
+ {({ percentage, valueText }) => (
25
+ <>
26
+ <div
27
+ className={cn(
28
+ 'flex items-center justify-between gap-2 mb-1',
29
+ classNames?.labelWrapper
30
+ )}
31
+ >
32
+ <Label className={cn('text-sm', classNames?.label)}>{label}</Label>
33
+ {showValue && (
34
+ <span className={cn('text-sm', classNames?.value)}>
35
+ {valueText}
36
+ </span>
37
+ )}
38
+ </div>
39
+ <div
40
+ className={cn(
41
+ 'overflow-hidden forced-color-adjust-none h-2.5 rounded will-change-transform bg-slate-300',
42
+ classNames?.trackWrapper
43
+ )}
44
+ >
45
+ <motion.div
46
+ style={{
47
+ width:
48
+ !isIndeterminate && typeof percentage === 'number'
49
+ ? `${percentage}%`
50
+ : '',
51
+ }}
52
+ // eslint-disable-next-line react/jsx-props-no-spreading
53
+ {...(isIndeterminate && {
54
+ animate: {
55
+ x: ['-100%', '250px'],
56
+ },
57
+ transition: { repeat: Infinity, duration: 1.5 },
58
+ })}
59
+ className={cn(
60
+ 'h-full bg-slate-800 rounded transition-width duration-500 ease-in-out',
61
+ {
62
+ 'w-1/2': isIndeterminate,
63
+ 'transition-none': isIndeterminate,
64
+ },
65
+ classNames?.track
66
+ )}
67
+ />
68
+ </div>
69
+ </>
70
+ )}
71
+ </ProgressBar>
72
+ );
73
+ }
74
+
75
+ export default LinearProgressbar;