@pago-sh/ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/.turbo/turbo-build.log +195 -0
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +22 -0
  4. package/components.json +17 -0
  5. package/dist/chunk-2RCGDEGJ.mjs +1 -0
  6. package/dist/chunk-3GDNSYGJ.mjs +1 -0
  7. package/dist/chunk-4TCUGB6E.mjs +1 -0
  8. package/dist/chunk-6XCXQA5L.mjs +1 -0
  9. package/dist/chunk-BBYR3CIF.mjs +1 -0
  10. package/dist/chunk-CFBUY7KY.mjs +1 -0
  11. package/dist/chunk-CHG2VB73.mjs +1 -0
  12. package/dist/chunk-GCVBVZU7.mjs +1 -0
  13. package/dist/chunk-HRGUR4R2.mjs +1 -0
  14. package/dist/chunk-IVU4SSCD.mjs +1 -0
  15. package/dist/chunk-N6WI5TGM.mjs +1 -0
  16. package/dist/chunk-OTKGECM3.mjs +1 -0
  17. package/dist/chunk-SRZFZUN4.mjs +1 -0
  18. package/dist/chunk-VPECET7P.mjs +1 -0
  19. package/dist/chunk-WQPSZXQS.mjs +1 -0
  20. package/dist/chunk-YXNT3UEO.mjs +1 -0
  21. package/dist/components/atoms/Accordion.d.mts +20 -0
  22. package/dist/components/atoms/Accordion.d.ts +20 -0
  23. package/dist/components/atoms/Accordion.js +1 -0
  24. package/dist/components/atoms/Accordion.mjs +1 -0
  25. package/dist/components/atoms/Card.d.mts +18 -0
  26. package/dist/components/atoms/Card.d.ts +18 -0
  27. package/dist/components/atoms/Card.js +1 -0
  28. package/dist/components/atoms/Card.mjs +1 -0
  29. package/dist/components/atoms/Combobox.d.mts +24 -0
  30. package/dist/components/atoms/Combobox.d.ts +24 -0
  31. package/dist/components/atoms/Combobox.js +1 -0
  32. package/dist/components/atoms/Combobox.mjs +1 -0
  33. package/dist/components/atoms/CopyToClipboardInput.d.mts +12 -0
  34. package/dist/components/atoms/CopyToClipboardInput.d.ts +12 -0
  35. package/dist/components/atoms/CopyToClipboardInput.js +1 -0
  36. package/dist/components/atoms/CopyToClipboardInput.mjs +1 -0
  37. package/dist/components/atoms/CountryPicker.d.mts +16 -0
  38. package/dist/components/atoms/CountryPicker.d.ts +16 -0
  39. package/dist/components/atoms/CountryPicker.js +1 -0
  40. package/dist/components/atoms/CountryPicker.mjs +1 -0
  41. package/dist/components/atoms/CountryStatePicker.d.mts +18 -0
  42. package/dist/components/atoms/CountryStatePicker.d.ts +18 -0
  43. package/dist/components/atoms/CountryStatePicker.js +1 -0
  44. package/dist/components/atoms/CountryStatePicker.mjs +1 -0
  45. package/dist/components/atoms/DateTimePicker.d.mts +10 -0
  46. package/dist/components/atoms/DateTimePicker.d.ts +10 -0
  47. package/dist/components/atoms/DateTimePicker.js +1 -0
  48. package/dist/components/atoms/DateTimePicker.mjs +1 -0
  49. package/dist/components/atoms/DropdownMenu.d.mts +22 -0
  50. package/dist/components/atoms/DropdownMenu.d.ts +22 -0
  51. package/dist/components/atoms/DropdownMenu.js +1 -0
  52. package/dist/components/atoms/DropdownMenu.mjs +1 -0
  53. package/dist/components/atoms/FormattedDateTime.d.mts +10 -0
  54. package/dist/components/atoms/FormattedDateTime.d.ts +10 -0
  55. package/dist/components/atoms/FormattedDateTime.js +1 -0
  56. package/dist/components/atoms/FormattedDateTime.mjs +1 -0
  57. package/dist/components/atoms/FormattedInterval.d.mts +9 -0
  58. package/dist/components/atoms/FormattedInterval.d.ts +9 -0
  59. package/dist/components/atoms/FormattedInterval.js +1 -0
  60. package/dist/components/atoms/FormattedInterval.mjs +1 -0
  61. package/dist/components/atoms/InputOTP.d.mts +4 -0
  62. package/dist/components/atoms/InputOTP.d.ts +4 -0
  63. package/dist/components/atoms/InputOTP.js +1 -0
  64. package/dist/components/atoms/InputOTP.mjs +1 -0
  65. package/dist/components/atoms/MoneyInput.d.mts +21 -0
  66. package/dist/components/atoms/MoneyInput.d.ts +21 -0
  67. package/dist/components/atoms/MoneyInput.js +1 -0
  68. package/dist/components/atoms/MoneyInput.mjs +1 -0
  69. package/dist/components/atoms/Paginator.d.mts +21 -0
  70. package/dist/components/atoms/Paginator.d.ts +21 -0
  71. package/dist/components/atoms/Paginator.js +1 -0
  72. package/dist/components/atoms/Paginator.mjs +1 -0
  73. package/dist/components/atoms/PagoTimeAgo.d.mts +8 -0
  74. package/dist/components/atoms/PagoTimeAgo.d.ts +8 -0
  75. package/dist/components/atoms/PagoTimeAgo.js +1 -0
  76. package/dist/components/atoms/PagoTimeAgo.mjs +1 -0
  77. package/dist/components/atoms/PercentageInput.d.mts +19 -0
  78. package/dist/components/atoms/PercentageInput.d.ts +19 -0
  79. package/dist/components/atoms/PercentageInput.js +1 -0
  80. package/dist/components/atoms/PercentageInput.mjs +1 -0
  81. package/dist/components/atoms/ShadowBox.d.mts +14 -0
  82. package/dist/components/atoms/ShadowBox.d.ts +14 -0
  83. package/dist/components/atoms/ShadowBox.js +1 -0
  84. package/dist/components/atoms/ShadowBox.mjs +1 -0
  85. package/dist/components/atoms/ShadowBoxOnMd.d.mts +6 -0
  86. package/dist/components/atoms/ShadowBoxOnMd.d.ts +6 -0
  87. package/dist/components/atoms/ShadowBoxOnMd.js +1 -0
  88. package/dist/components/atoms/ShadowBoxOnMd.mjs +1 -0
  89. package/dist/components/atoms/Sidebar.d.mts +11 -0
  90. package/dist/components/atoms/Sidebar.d.ts +11 -0
  91. package/dist/components/atoms/Sidebar.js +1 -0
  92. package/dist/components/atoms/Sidebar.mjs +1 -0
  93. package/dist/components/molecules/Banner.d.mts +11 -0
  94. package/dist/components/molecules/Banner.d.ts +11 -0
  95. package/dist/components/molecules/Banner.js +1 -0
  96. package/dist/components/molecules/Banner.mjs +1 -0
  97. package/dist/components/ui/accordion.d.mts +19 -0
  98. package/dist/components/ui/accordion.d.ts +19 -0
  99. package/dist/components/ui/accordion.js +1 -0
  100. package/dist/components/ui/accordion.mjs +1 -0
  101. package/dist/components/ui/alert-dialog.d.mts +41 -0
  102. package/dist/components/ui/alert-dialog.d.ts +41 -0
  103. package/dist/components/ui/alert-dialog.js +1 -0
  104. package/dist/components/ui/alert-dialog.mjs +1 -0
  105. package/dist/components/ui/calendar.d.mts +12 -0
  106. package/dist/components/ui/calendar.d.ts +12 -0
  107. package/dist/components/ui/calendar.js +1 -0
  108. package/dist/components/ui/calendar.mjs +1 -0
  109. package/dist/components/ui/card.d.mts +23 -0
  110. package/dist/components/ui/card.d.ts +23 -0
  111. package/dist/components/ui/card.js +1 -0
  112. package/dist/components/ui/card.mjs +1 -0
  113. package/dist/components/ui/chart.d.mts +55 -0
  114. package/dist/components/ui/chart.d.ts +55 -0
  115. package/dist/components/ui/chart.js +7 -0
  116. package/dist/components/ui/chart.mjs +7 -0
  117. package/dist/components/ui/checkbox.d.mts +2 -0
  118. package/dist/components/ui/checkbox.d.ts +2 -0
  119. package/dist/components/ui/checkbox.js +1 -0
  120. package/dist/components/ui/checkbox.mjs +1 -0
  121. package/dist/components/ui/command.d.mts +36 -0
  122. package/dist/components/ui/command.d.ts +36 -0
  123. package/dist/components/ui/command.js +1 -0
  124. package/dist/components/ui/command.mjs +1 -0
  125. package/dist/components/ui/dialog.d.mts +34 -0
  126. package/dist/components/ui/dialog.d.ts +34 -0
  127. package/dist/components/ui/dialog.js +1 -0
  128. package/dist/components/ui/dialog.mjs +1 -0
  129. package/dist/components/ui/dropdown-menu.d.mts +23 -0
  130. package/dist/components/ui/dropdown-menu.d.ts +23 -0
  131. package/dist/components/ui/dropdown-menu.js +1 -0
  132. package/dist/components/ui/dropdown-menu.mjs +1 -0
  133. package/dist/components/ui/form.d.mts +43 -0
  134. package/dist/components/ui/form.d.ts +43 -0
  135. package/dist/components/ui/form.js +1 -0
  136. package/dist/components/ui/form.mjs +1 -0
  137. package/dist/components/ui/input-otp.d.mts +21 -0
  138. package/dist/components/ui/input-otp.d.ts +21 -0
  139. package/dist/components/ui/input-otp.js +1 -0
  140. package/dist/components/ui/input-otp.mjs +1 -0
  141. package/dist/components/ui/label.d.mts +9 -0
  142. package/dist/components/ui/label.d.ts +9 -0
  143. package/dist/components/ui/label.js +1 -0
  144. package/dist/components/ui/label.mjs +1 -0
  145. package/dist/components/ui/popover.d.mts +12 -0
  146. package/dist/components/ui/popover.d.ts +12 -0
  147. package/dist/components/ui/popover.js +1 -0
  148. package/dist/components/ui/popover.mjs +1 -0
  149. package/dist/components/ui/radio-group.d.mts +14 -0
  150. package/dist/components/ui/radio-group.d.ts +14 -0
  151. package/dist/components/ui/radio-group.js +1 -0
  152. package/dist/components/ui/radio-group.mjs +1 -0
  153. package/dist/components/ui/separator.d.mts +10 -0
  154. package/dist/components/ui/separator.d.ts +10 -0
  155. package/dist/components/ui/separator.js +1 -0
  156. package/dist/components/ui/separator.mjs +1 -0
  157. package/dist/components/ui/sheet.d.mts +43 -0
  158. package/dist/components/ui/sheet.d.ts +43 -0
  159. package/dist/components/ui/sheet.js +1 -0
  160. package/dist/components/ui/sheet.mjs +1 -0
  161. package/dist/components/ui/sidebar.d.mts +149 -0
  162. package/dist/components/ui/sidebar.d.ts +149 -0
  163. package/dist/components/ui/sidebar.js +1 -0
  164. package/dist/components/ui/sidebar.mjs +1 -0
  165. package/dist/components/ui/tooltip.d.mts +1 -0
  166. package/dist/components/ui/tooltip.d.ts +1 -0
  167. package/dist/components/ui/tooltip.js +1 -0
  168. package/dist/components/ui/tooltip.mjs +1 -0
  169. package/dist/hooks/theming.d.mts +76 -0
  170. package/dist/hooks/theming.d.ts +76 -0
  171. package/dist/hooks/theming.js +1 -0
  172. package/dist/hooks/theming.mjs +1 -0
  173. package/dist/hooks/use-mobile.d.mts +3 -0
  174. package/dist/hooks/use-mobile.d.ts +3 -0
  175. package/dist/hooks/use-mobile.js +1 -0
  176. package/dist/hooks/use-mobile.mjs +1 -0
  177. package/dist/lib/meterUnit.d.mts +34 -0
  178. package/dist/lib/meterUnit.d.ts +34 -0
  179. package/dist/lib/meterUnit.js +1 -0
  180. package/dist/lib/meterUnit.mjs +1 -0
  181. package/dist/lib/utils.d.mts +5 -0
  182. package/dist/lib/utils.d.ts +5 -0
  183. package/dist/lib/utils.js +1 -0
  184. package/dist/lib/utils.mjs +1 -0
  185. package/eslint.config.mjs +18 -0
  186. package/package.json +74 -0
  187. package/src/components/atoms/Accordion.tsx +56 -0
  188. package/src/components/atoms/Card.tsx +52 -0
  189. package/src/components/atoms/Combobox.tsx +166 -0
  190. package/src/components/atoms/CopyToClipboardInput.tsx +72 -0
  191. package/src/components/atoms/CountryPicker.tsx +81 -0
  192. package/src/components/atoms/CountryStatePicker.tsx +155 -0
  193. package/src/components/atoms/DateTimePicker.tsx +69 -0
  194. package/src/components/atoms/DropdownMenu.tsx +68 -0
  195. package/src/components/atoms/FormattedDateTime.tsx +58 -0
  196. package/src/components/atoms/FormattedInterval.tsx +81 -0
  197. package/src/components/atoms/InputOTP.tsx +7 -0
  198. package/src/components/atoms/MoneyInput.tsx +297 -0
  199. package/src/components/atoms/Paginator.tsx +227 -0
  200. package/src/components/atoms/PagoTimeAgo.tsx +19 -0
  201. package/src/components/atoms/PercentageInput.tsx +97 -0
  202. package/src/components/atoms/ShadowBox.tsx +26 -0
  203. package/src/components/atoms/ShadowBoxOnMd.tsx +20 -0
  204. package/src/components/atoms/Sidebar.tsx +1 -0
  205. package/src/components/molecules/Banner.tsx +42 -0
  206. package/src/components/ui/accordion.tsx +63 -0
  207. package/src/components/ui/alert-dialog.tsx +147 -0
  208. package/src/components/ui/calendar.tsx +81 -0
  209. package/src/components/ui/card.tsx +47 -0
  210. package/src/components/ui/chart.tsx +372 -0
  211. package/src/components/ui/checkbox.tsx +3 -0
  212. package/src/components/ui/command.tsx +133 -0
  213. package/src/components/ui/dialog.tsx +127 -0
  214. package/src/components/ui/dropdown-menu.tsx +74 -0
  215. package/src/components/ui/form.tsx +180 -0
  216. package/src/components/ui/input-otp.tsx +68 -0
  217. package/src/components/ui/label.tsx +25 -0
  218. package/src/components/ui/popover.tsx +34 -0
  219. package/src/components/ui/radio-group.tsx +46 -0
  220. package/src/components/ui/separator.tsx +29 -0
  221. package/src/components/ui/sheet.tsx +149 -0
  222. package/src/components/ui/sidebar.tsx +742 -0
  223. package/src/components/ui/tooltip.tsx +8 -0
  224. package/src/hooks/theming.ts +87 -0
  225. package/src/hooks/use-mobile.ts +19 -0
  226. package/src/lib/meterUnit.ts +52 -0
  227. package/src/lib/utils.ts +6 -0
  228. package/tsconfig.json +15 -0
  229. package/tsup.config.ts +11 -0
@@ -0,0 +1,69 @@
1
+ import { Button } from '@pago-sh/orbit/ui/button'
2
+ import { Calendar } from '@/components/ui/calendar'
3
+ import { FormControl } from '@/components/ui/form'
4
+ import {
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from '@/components/ui/popover'
9
+ import { format } from 'date-fns'
10
+ import { CalendarDays } from 'lucide-react'
11
+ import { useCallback, useMemo } from 'react'
12
+ import { Matcher } from 'react-day-picker'
13
+ import { twMerge } from 'tailwind-merge'
14
+
15
+ interface DatePickerProps {
16
+ value: string | undefined
17
+ onChange: (value: string | undefined) => void
18
+ disabled?: Matcher | Matcher[]
19
+ }
20
+
21
+ const DatePicker: React.FC<DatePickerProps> = (props) => {
22
+ const { value, onChange: _onChange, disabled } = props
23
+ const internalValue = useMemo(
24
+ () => (value ? new Date(value as string) : undefined),
25
+ [value],
26
+ )
27
+
28
+ const onChange = useCallback(
29
+ (date: Date | undefined) => {
30
+ const value = date ? date.toISOString() : ''
31
+ _onChange(value)
32
+ },
33
+ [_onChange],
34
+ )
35
+
36
+ return (
37
+ <Popover>
38
+ <PopoverTrigger asChild>
39
+ <FormControl className="flex">
40
+ <Button
41
+ variant={'outline'}
42
+ className={twMerge(
43
+ 'dark:bg-pago-800 h-10 w-full justify-start rounded-xl bg-white text-left',
44
+ !value && 'text-muted-foreground',
45
+ )}
46
+ >
47
+ {internalValue ? (
48
+ format(internalValue, 'PPP')
49
+ ) : (
50
+ <span>Pick a date</span>
51
+ )}
52
+ <CalendarDays className="ml-auto h-4 w-4 opacity-50" />
53
+ </Button>
54
+ </FormControl>
55
+ </PopoverTrigger>
56
+ <PopoverContent className="w-auto p-0" align="start">
57
+ <Calendar
58
+ mode="single"
59
+ selected={internalValue}
60
+ onSelect={onChange}
61
+ disabled={disabled}
62
+ autoFocus
63
+ />
64
+ </PopoverContent>
65
+ </Popover>
66
+ )
67
+ }
68
+
69
+ export default DatePicker
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ import {
4
+ DropdownMenuContent as DropdownMenuContentPrimitive,
5
+ DropdownMenuItem as DropdownMenuItemPrimitive,
6
+ DropdownMenuPortal as DropdownMenuPortalPrimitive,
7
+ DropdownMenu as DropdownMenuPrimitive,
8
+ DropdownMenuSeparator as DropdownMenuSeparatorPrimitive,
9
+ DropdownMenuTrigger as DropdownMenuTriggerPrimitive,
10
+ } from '@/components/ui/dropdown-menu'
11
+ import * as React from 'react'
12
+ import { twMerge } from 'tailwind-merge'
13
+
14
+ const DropdownMenu = DropdownMenuPrimitive
15
+ const DropdownMenuPortal = DropdownMenuPortalPrimitive
16
+ const DropdownMenuTrigger = DropdownMenuTriggerPrimitive
17
+
18
+ const DropdownMenuContent = ({
19
+ ref,
20
+ className,
21
+ sideOffset = 4,
22
+ ...props
23
+ }: React.ComponentProps<typeof DropdownMenuContentPrimitive>) => (
24
+ <DropdownMenuPortal>
25
+ <DropdownMenuContentPrimitive
26
+ ref={ref}
27
+ sideOffset={sideOffset}
28
+ className={twMerge('', className)}
29
+ {...props}
30
+ />
31
+ </DropdownMenuPortal>
32
+ )
33
+ DropdownMenuContent.displayName = DropdownMenuContentPrimitive.displayName
34
+
35
+ const DropdownMenuItem = ({
36
+ ref,
37
+ className,
38
+ ...props
39
+ }: React.ComponentProps<typeof DropdownMenuItemPrimitive>) => (
40
+ <DropdownMenuItemPrimitive
41
+ ref={ref}
42
+ className={twMerge(props?.onClick ? 'cursor-pointer' : '', className)}
43
+ {...props}
44
+ />
45
+ )
46
+ DropdownMenuItem.displayName = DropdownMenuItemPrimitive.displayName
47
+
48
+ const DropdownMenuSeparator = ({
49
+ ref,
50
+ className,
51
+ ...props
52
+ }: React.ComponentProps<typeof DropdownMenuSeparatorPrimitive>) => (
53
+ <DropdownMenuSeparatorPrimitive
54
+ ref={ref}
55
+ className={twMerge('', className)}
56
+ {...props}
57
+ />
58
+ )
59
+ DropdownMenuSeparator.displayName = DropdownMenuSeparatorPrimitive.displayName
60
+
61
+ export {
62
+ DropdownMenu,
63
+ DropdownMenuContent,
64
+ DropdownMenuItem,
65
+ DropdownMenuPortal,
66
+ DropdownMenuSeparator,
67
+ DropdownMenuTrigger,
68
+ }
@@ -0,0 +1,58 @@
1
+ import { useMemo } from 'react'
2
+
3
+ interface FormattedDateTimeProps {
4
+ datetime: Date | string
5
+ locale?: string
6
+ dateStyle?: 'full' | 'long' | 'medium' | 'short'
7
+ timeStyle?: 'full' | 'long' | 'medium' | 'short'
8
+ resolution?: 'time' | 'day' | 'month'
9
+ }
10
+
11
+ const FormattedDateTime: React.FC<FormattedDateTimeProps> = ({
12
+ datetime,
13
+ locale = 'en-US',
14
+ dateStyle = 'medium',
15
+ timeStyle = 'short',
16
+ resolution = 'day',
17
+ }) => {
18
+ const parsedDate = useMemo(() => new Date(datetime), [datetime])
19
+ const isValidDate = useMemo(
20
+ () => !Number.isNaN(parsedDate.getTime()),
21
+ [parsedDate],
22
+ )
23
+ const formatted = useMemo(() => {
24
+ try {
25
+ if (!isValidDate) {
26
+ return 'Invalid date'
27
+ }
28
+
29
+ if (resolution === 'time') {
30
+ return parsedDate.toLocaleString(locale, { dateStyle, timeStyle })
31
+ }
32
+
33
+ if (resolution === 'month') {
34
+ return parsedDate.toLocaleDateString(locale, {
35
+ month: 'short',
36
+ year: 'numeric',
37
+ })
38
+ }
39
+
40
+ return parsedDate.toLocaleDateString(locale, {
41
+ dateStyle,
42
+ })
43
+ } catch {
44
+ return 'Invalid date or locale'
45
+ }
46
+ }, [parsedDate, isValidDate, locale, resolution, dateStyle, timeStyle])
47
+
48
+ return (
49
+ <time
50
+ suppressHydrationWarning
51
+ dateTime={isValidDate ? parsedDate.toISOString() : undefined}
52
+ >
53
+ {formatted}
54
+ </time>
55
+ )
56
+ }
57
+
58
+ export default FormattedDateTime
@@ -0,0 +1,81 @@
1
+ import { isSameDay } from 'date-fns/isSameDay'
2
+ import { useMemo } from 'react'
3
+
4
+ interface FormattedIntervalProps {
5
+ startDatetime: Date | string
6
+ endDatetime: Date | string
7
+ locale?: string
8
+ hideCurrentYear?: boolean
9
+ }
10
+
11
+ const FormattedInterval: React.FC<FormattedIntervalProps> = ({
12
+ startDatetime,
13
+ endDatetime,
14
+ locale = 'en-US',
15
+ hideCurrentYear = true,
16
+ }) => {
17
+ const formatted = useMemo(() => {
18
+ const startDate = new Date(startDatetime)
19
+ const endDate = new Date(endDatetime)
20
+ const currentYear = new Date().getFullYear()
21
+
22
+ const startYear = startDate.getFullYear()
23
+ const endYear = endDate.getFullYear()
24
+ const startMonth = startDate.getMonth()
25
+ const endMonth = endDate.getMonth()
26
+
27
+ const separator = '–'
28
+
29
+ const shouldHideYear =
30
+ hideCurrentYear && endYear === currentYear && startYear === endYear
31
+
32
+ if (isSameDay(startDate, endDate)) {
33
+ return endDate.toLocaleDateString(locale, {
34
+ month: 'short',
35
+ day: 'numeric',
36
+ ...(shouldHideYear ? {} : { year: 'numeric' }),
37
+ })
38
+ }
39
+
40
+ if (startYear === endYear && startMonth === endMonth) {
41
+ const startFormatted = startDate.toLocaleDateString(locale, {
42
+ month: 'short',
43
+ day: 'numeric',
44
+ })
45
+ const endFormatted = `${endDate.toLocaleDateString(locale, {
46
+ day: 'numeric',
47
+ })}${shouldHideYear ? '' : `, ${endYear}`}`
48
+ return `${startFormatted} ${separator} ${endFormatted}`
49
+ }
50
+
51
+ if (startYear === endYear) {
52
+ const startFormatted = startDate.toLocaleDateString(locale, {
53
+ month: 'short',
54
+ day: 'numeric',
55
+ })
56
+ const endFormatted = endDate.toLocaleDateString(locale, {
57
+ month: 'short',
58
+ day: 'numeric',
59
+ ...(shouldHideYear ? {} : { year: 'numeric' }),
60
+ })
61
+ return `${startFormatted} ${separator} ${endFormatted}`
62
+ }
63
+
64
+ const startFormatted = startDate.toLocaleDateString(locale, {
65
+ month: 'short',
66
+ day: 'numeric',
67
+ year: 'numeric',
68
+ })
69
+
70
+ const endFormatted = endDate.toLocaleDateString(locale, {
71
+ month: 'short',
72
+ day: 'numeric',
73
+ year: 'numeric',
74
+ })
75
+ return `${startFormatted} ${separator} ${endFormatted}`
76
+ }, [startDatetime, endDatetime, locale, hideCurrentYear])
77
+
78
+ return <>{formatted}</>
79
+ }
80
+
81
+ export default FormattedInterval
@@ -0,0 +1,7 @@
1
+ import {
2
+ InputOTP,
3
+ InputOTPGroup,
4
+ InputOTPSlot,
5
+ } from '@/components/ui/input-otp'
6
+
7
+ export { InputOTP, InputOTPGroup, InputOTPSlot }
@@ -0,0 +1,297 @@
1
+ import { getCurrencyDecimalFactor, isDecimalCurrency } from '@pago-sh/currency'
2
+ import { ChangeEvent, FocusEvent, useCallback, useMemo, useState } from 'react'
3
+ import { twMerge } from 'tailwind-merge'
4
+ import { Input } from '@pago-sh/orbit'
5
+
6
+ interface Props {
7
+ name: string
8
+ placeholder: number
9
+ currency: string
10
+ id?: string
11
+ onChange?: (value: number | null) => void
12
+ onBlur?: (e: ChangeEvent<HTMLInputElement>) => void
13
+ onFocus?: (e: FocusEvent<HTMLInputElement>) => void
14
+ value?: number | null
15
+ className?: string
16
+ disabled?: boolean
17
+ preSlot?: React.ReactNode
18
+ postSlot?: React.ReactNode
19
+ step?: number
20
+ }
21
+
22
+ const MoneyInput = (props: Props) => {
23
+ const {
24
+ id,
25
+ name,
26
+ value,
27
+ placeholder,
28
+ currency,
29
+ preSlot,
30
+ postSlot,
31
+ onChange: _onChange,
32
+ onBlur: _onBlur,
33
+ onFocus,
34
+ disabled,
35
+ step = 0.1,
36
+ } = props
37
+
38
+ const decimalFactor = useMemo(
39
+ () => getCurrencyDecimalFactor(currency),
40
+ [currency],
41
+ )
42
+ const isNonDecimalCurrency = !isDecimalCurrency(currency)
43
+
44
+ const getInternalValue = useCallback(
45
+ (value: number | null | undefined): string | undefined => {
46
+ if (value === undefined || value === null) {
47
+ return undefined
48
+ }
49
+ if (isNonDecimalCurrency) {
50
+ return value.toString()
51
+ }
52
+ return (value / decimalFactor).toFixed(2)
53
+ },
54
+ [decimalFactor, isNonDecimalCurrency],
55
+ )
56
+
57
+ const getUnits = useCallback(
58
+ (value: string): number => {
59
+ let newAmount = Number.parseFloat(value)
60
+ if (isNaN(newAmount)) {
61
+ newAmount = 0
62
+ }
63
+ if (isNonDecimalCurrency) {
64
+ return Math.round(newAmount)
65
+ }
66
+ // Round to avoid floating point errors
67
+ return Math.round(newAmount * decimalFactor)
68
+ },
69
+ [decimalFactor, isNonDecimalCurrency],
70
+ )
71
+
72
+ const [previousValue, setPreviousValue] = useState<number | null | undefined>(
73
+ value,
74
+ )
75
+ const [internalValue, setInternalValue] = useState<string | undefined>(
76
+ getInternalValue(value),
77
+ )
78
+
79
+ if (value !== previousValue) {
80
+ setPreviousValue(value)
81
+ setInternalValue(getInternalValue(value))
82
+ }
83
+
84
+ const updateValue = useCallback(
85
+ (newValue: string) => {
86
+ if (_onChange) {
87
+ if (!newValue || newValue.trim() === '') {
88
+ setPreviousValue(null)
89
+ _onChange(null)
90
+ } else {
91
+ const unitsValue = getUnits(newValue)
92
+ setPreviousValue(unitsValue)
93
+ _onChange(unitsValue)
94
+ }
95
+ }
96
+
97
+ setInternalValue(newValue)
98
+ },
99
+ [_onChange, getUnits],
100
+ )
101
+
102
+ const onChange = useCallback(
103
+ (e: ChangeEvent<HTMLInputElement>) => {
104
+ const input = e.target.value
105
+
106
+ // If input is completely empty, allow clearing the field
107
+ if (input === '') {
108
+ updateValue('')
109
+ return
110
+ }
111
+
112
+ // For non-decimal currencies, only allow whole numbers
113
+ if (isNonDecimalCurrency) {
114
+ const cleaned = input.replace(/[^0-9]/g, '')
115
+ updateValue(cleaned)
116
+ return
117
+ }
118
+
119
+ // Strip everything except numbers, commas, and periods
120
+ // (people can paste in anything, so the keydown handler is not enough)
121
+ const cleaned = input.replace(/[^0-9,.]/g, '')
122
+
123
+ // By default, parse the full value as a whole number, stripping out decimal separators
124
+ //
125
+ // Leave a trailing comma, otherwise people can't type in decimals
126
+ // (the onBlur handler will strip it if it's dangling on blur)
127
+ let newValue = cleaned.replace(/[,.](?!$)/g, '').replace(/,$/, '.')
128
+
129
+ // However, if we detect a decimal separator, round it to 2 decimal places
130
+ //
131
+ // We support period decimal separator (enforced when typing)
132
+ // but also support comma decimal separators (might be pasted in)
133
+ const decimalMatch = cleaned.match(/([.,])([0-9]+)$/)
134
+
135
+ if (decimalMatch) {
136
+ const maxDecimalPrecision = 2
137
+ const decimalPart = decimalMatch[2]
138
+ const integerPart = cleaned
139
+ .slice(0, -decimalMatch[0].length)
140
+ .replace(/[,.]/g, '')
141
+ const trimmedDecimalPart = decimalPart.slice(0, maxDecimalPrecision)
142
+
143
+ const parsedValue = Number.parseFloat(
144
+ `${integerPart}.${trimmedDecimalPart}`,
145
+ )
146
+
147
+ if (!Number.isNaN(parsedValue)) {
148
+ const decimalPlaces = Math.min(
149
+ maxDecimalPrecision,
150
+ decimalPart.length,
151
+ )
152
+ const formatted = parsedValue.toFixed(decimalPlaces)
153
+
154
+ // This covers when the user deletes the last integer part and prevents inserting a `0`
155
+ // in place of the last integer part that would make the caret jump to the end of the input
156
+ // This way the user can continue typing
157
+ newValue =
158
+ integerPart.length > 0
159
+ ? formatted
160
+ : formatted.replace(/^0(?=\.)/, '')
161
+ }
162
+ }
163
+
164
+ updateValue(newValue)
165
+ },
166
+ [updateValue, isNonDecimalCurrency],
167
+ )
168
+
169
+ const onBlur = useCallback(
170
+ (e: FocusEvent<HTMLInputElement>) => {
171
+ if (internalValue) {
172
+ let nextValue = internalValue
173
+
174
+ if (!isNonDecimalCurrency) {
175
+ // Add 0 as integer part if value starts with `.`
176
+ if (nextValue.startsWith('.')) {
177
+ nextValue = `0${nextValue}`
178
+ }
179
+
180
+ // Strip trailing decimal point
181
+ if (nextValue.endsWith('.')) {
182
+ nextValue = nextValue.replace(/\.$/, '')
183
+ }
184
+ }
185
+
186
+ if (nextValue !== internalValue) {
187
+ updateValue(nextValue)
188
+ }
189
+ }
190
+
191
+ if (_onBlur) {
192
+ _onBlur(e)
193
+ }
194
+ },
195
+ [_onBlur, internalValue, updateValue, isNonDecimalCurrency],
196
+ )
197
+
198
+ const onKeyDown = useCallback(
199
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
200
+ // For non-decimal currencies, only allow digits and control keys
201
+ if (isNonDecimalCurrency) {
202
+ if (
203
+ !/[0-9]/.test(e.key) &&
204
+ !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(
205
+ e.key,
206
+ ) &&
207
+ !e.ctrlKey &&
208
+ !e.metaKey
209
+ ) {
210
+ e.preventDefault()
211
+ }
212
+ return
213
+ }
214
+
215
+ // Allow only digits, decimal point, and control keys
216
+ if (
217
+ !/[0-9.,]/.test(e.key) &&
218
+ !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(
219
+ e.key,
220
+ ) &&
221
+ !e.ctrlKey &&
222
+ !e.metaKey
223
+ ) {
224
+ e.preventDefault()
225
+ }
226
+
227
+ if (e.key === 'ArrowUp') {
228
+ e.preventDefault()
229
+ const parsedValue = Number.parseFloat(e.currentTarget.value)
230
+
231
+ const newValue = (
232
+ !Number.isNaN(parsedValue) ? parsedValue + step : step
233
+ ).toFixed(2)
234
+
235
+ updateValue(newValue)
236
+ }
237
+
238
+ if (e.key === 'ArrowDown') {
239
+ const parsedValue = Number.parseFloat(e.currentTarget.value)
240
+
241
+ const newValue = Math.max(
242
+ 0,
243
+ !Number.isNaN(parsedValue) ? parsedValue - step : -step,
244
+ ).toFixed(2)
245
+
246
+ updateValue(newValue)
247
+ }
248
+
249
+ // Prevent multiple decimal points
250
+ if (
251
+ (e.key === '.' || e.key === ',') &&
252
+ e.currentTarget.value.includes('.')
253
+ ) {
254
+ e.preventDefault()
255
+ }
256
+ },
257
+ [step, updateValue, isNonDecimalCurrency],
258
+ )
259
+
260
+ const currencyLabel = (
261
+ <span className="dark:text-pago-500 text-sm font-medium text-gray-500">
262
+ {currency.toUpperCase()}
263
+ </span>
264
+ )
265
+
266
+ const placeholderValue = useMemo(() => {
267
+ if (!placeholder) return undefined
268
+ if (isNonDecimalCurrency) {
269
+ return placeholder.toLocaleString('en-US')
270
+ }
271
+ return (placeholder / decimalFactor).toLocaleString('en-US')
272
+ }, [placeholder, decimalFactor, isNonDecimalCurrency])
273
+
274
+ return (
275
+ <Input
276
+ type="text"
277
+ inputMode={isNonDecimalCurrency ? 'numeric' : 'decimal'}
278
+ id={id}
279
+ name={name}
280
+ className={twMerge(
281
+ 'dark:placeholder:text-pago-500 block w-full px-4 pl-14 text-base placeholder:text-gray-400',
282
+ props.className ?? '',
283
+ )}
284
+ value={internalValue}
285
+ onChange={onChange}
286
+ onKeyDown={onKeyDown}
287
+ placeholder={placeholderValue}
288
+ preSlot={preSlot ? preSlot : currencyLabel}
289
+ postSlot={postSlot}
290
+ onBlur={onBlur}
291
+ onFocus={onFocus}
292
+ disabled={disabled}
293
+ />
294
+ )
295
+ }
296
+
297
+ export default MoneyInput