@komensal/ui 0.1.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 (42) hide show
  1. package/package.json +48 -0
  2. package/src/components/alert.tsx +45 -0
  3. package/src/components/auth-header.tsx +26 -0
  4. package/src/components/button.tsx +121 -0
  5. package/src/components/command.tsx +125 -0
  6. package/src/components/dialog.tsx +118 -0
  7. package/src/components/form-fields/index.ts +5 -0
  8. package/src/components/form-fields/input-field.tsx +131 -0
  9. package/src/components/form-fields/label-field.tsx +20 -0
  10. package/src/components/form-fields/map-picker-field.tsx +49 -0
  11. package/src/components/form-fields/otp-field.tsx +76 -0
  12. package/src/components/form-fields/select-field.tsx +77 -0
  13. package/src/components/form.tsx +138 -0
  14. package/src/components/google-map-picker.tsx +243 -0
  15. package/src/components/input.tsx +40 -0
  16. package/src/components/label.tsx +17 -0
  17. package/src/components/loading.tsx +19 -0
  18. package/src/components/otp-input.tsx +62 -0
  19. package/src/components/password-checklist.tsx +37 -0
  20. package/src/components/popover.tsx +31 -0
  21. package/src/components/select-input.tsx +236 -0
  22. package/src/components/select.tsx +143 -0
  23. package/src/components/spinner.tsx +61 -0
  24. package/src/components/step-header.tsx +16 -0
  25. package/src/components/stepper.tsx +69 -0
  26. package/src/components/toast/index.ts +2 -0
  27. package/src/components/toast/toast-provider.tsx +123 -0
  28. package/src/components/toast/toast.tsx +106 -0
  29. package/src/components/tooltip.tsx +56 -0
  30. package/src/components/wrapper.tsx +12 -0
  31. package/src/index.ts +27 -0
  32. package/src/locales/es/ui.json +28 -0
  33. package/src/locales/index.ts +7 -0
  34. package/src/styles/index.css +1 -0
  35. package/src/styles/theme.css +105 -0
  36. package/src/types/password.ts +12 -0
  37. package/src/types/toast.ts +19 -0
  38. package/src/utils/cn.ts +6 -0
  39. package/src/utils/index.ts +3 -0
  40. package/src/utils/styles.ts +19 -0
  41. package/src/utils/toast-emitter.ts +37 -0
  42. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,62 @@
1
+ import { OTPInput, OTPInputContext } from 'input-otp'
2
+ import React from 'react'
3
+
4
+ import { cn } from '../utils'
5
+
6
+ function InputOTP({
7
+ className,
8
+ containerClassName,
9
+ ...props
10
+ }: React.ComponentProps<typeof OTPInput> & {
11
+ containerClassName?: string
12
+ }) {
13
+ return (
14
+ <OTPInput
15
+ className={cn('disabled:cursor-not-allowed', className)}
16
+ containerClassName={cn('flex items-center gap-2 has-disabled:opacity-50', containerClassName)}
17
+ data-slot='input-otp'
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
24
+ return <div className={cn('flex items-center gap-2', className)} data-slot='input-otp-group' {...props} />
25
+ }
26
+
27
+ function InputOTPSlot({
28
+ className,
29
+ index,
30
+ ...props
31
+ }: React.ComponentProps<'div'> & {
32
+ index: number
33
+ }) {
34
+ const inputOTPContext = React.useContext(OTPInputContext) as {
35
+ slots: { char: string | null; hasFakeCaret: boolean; isActive: boolean }[]
36
+ }
37
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] ?? {}
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ 'border-border text-foreground relative flex h-14 w-12 items-center justify-center rounded-[10px] border text-xl font-medium transition-all outline-none',
43
+ char ? 'bg-brand-subtle' : 'bg-transparent',
44
+ 'data-[active=true]:border-primary data-[active=true]:ring-ring data-[active=true]:z-10 data-[active=true]:ring-2',
45
+ 'aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/40',
46
+ className
47
+ )}
48
+ data-active={isActive}
49
+ data-slot='input-otp-slot'
50
+ {...props}
51
+ >
52
+ {char}
53
+ {hasFakeCaret && (
54
+ <div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
55
+ <div className='animate-caret-blink bg-foreground h-6 w-px duration-1000' />
56
+ </div>
57
+ )}
58
+ </div>
59
+ )
60
+ }
61
+
62
+ export { InputOTP, InputOTPGroup, InputOTPSlot }
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+ import { IoMdCheckmarkCircle, IoMdCloseCircle } from 'react-icons/io'
3
+
4
+ import { PasswordRule } from '../types/password'
5
+ import { cn } from '../utils'
6
+
7
+ interface PasswordChecklistProps {
8
+ className?: string
9
+ rules: PasswordRule[]
10
+ value: string
11
+ }
12
+
13
+ export const PasswordChecklist = ({ className, rules, value }: PasswordChecklistProps) => {
14
+ if (rules.length === 0) return null
15
+
16
+ return (
17
+ <ul className={cn('flex flex-col gap-1', className)}>
18
+ {rules.map((rule) => {
19
+ const isMet = rule.test(value)
20
+ const Icon = (isMet ? IoMdCheckmarkCircle : IoMdCloseCircle) as React.FC<React.SVGProps<SVGSVGElement>>
21
+
22
+ return (
23
+ <li
24
+ className={cn(
25
+ 'flex items-center gap-2 text-xs transition-colors',
26
+ isMet ? 'text-success' : 'text-inactive-text'
27
+ )}
28
+ key={rule.key}
29
+ >
30
+ <Icon aria-hidden='true' className='h-4 w-4 shrink-0' />
31
+ <span>{rule.label}</span>
32
+ </li>
33
+ )
34
+ })}
35
+ </ul>
36
+ )
37
+ }
@@ -0,0 +1,31 @@
1
+ import * as PopoverPrimitive from '@radix-ui/react-popover'
2
+ import * as React from 'react'
3
+
4
+ import { cn } from '../utils'
5
+
6
+ const Popover = PopoverPrimitive.Root
7
+
8
+ const PopoverTrigger = PopoverPrimitive.Trigger
9
+
10
+ const PopoverAnchor = PopoverPrimitive.Anchor
11
+
12
+ const PopoverContent = React.forwardRef<
13
+ React.ElementRef<typeof PopoverPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
+ >(({ align = 'center', className, sideOffset = 4, ...props }, ref) => (
16
+ <PopoverPrimitive.Portal>
17
+ <PopoverPrimitive.Content
18
+ align={align}
19
+ className={cn(
20
+ 'text-foreground bg-surface data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none',
21
+ className
22
+ )}
23
+ ref={ref}
24
+ sideOffset={sideOffset}
25
+ {...props}
26
+ />
27
+ </PopoverPrimitive.Portal>
28
+ ))
29
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
30
+
31
+ export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
@@ -0,0 +1,236 @@
1
+ import { ChevronDown, ChevronUp, Clock, XIcon } from 'lucide-react'
2
+ import React, { cloneElement, JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
+ import { FieldError, Noop } from 'react-hook-form'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import { cn, inputBase, inputDisabled, inputFocus, inputPlaceholder } from '../utils'
7
+ import { Button } from './button'
8
+ import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from './command'
9
+ import { Input } from './input'
10
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
11
+ import { Spinner } from './spinner'
12
+
13
+ interface SelectInputProps<Label extends string, Value extends string> {
14
+ ableDeleteOption?: boolean
15
+ className?: string
16
+ disabled?: boolean
17
+ error?: FieldError
18
+ items?: null | Record<Label | Value, number | string>[]
19
+ labelKey?: string
20
+ leftIcon?: JSX.Element
21
+ loading?: boolean
22
+ onBlur?: Noop
23
+ onChange?: (...event: unknown[]) => void
24
+ onValueChangeCallBack?: (item: { label: string; value: string }) => void
25
+ placeholder?: string
26
+ placeholderClassName?: string
27
+ search?: boolean
28
+ timeField?: boolean
29
+ value?: string
30
+ valueKey?: string
31
+ }
32
+
33
+ export const SelectInput = <Label extends string, Value extends string>(props: SelectInputProps<Label, Value>) => {
34
+ const { t } = useTranslation('ui')
35
+ const {
36
+ ableDeleteOption,
37
+ className,
38
+ disabled = false,
39
+ error,
40
+ items,
41
+ labelKey = 'label',
42
+ leftIcon,
43
+ loading = false,
44
+ onBlur,
45
+ onChange,
46
+ onValueChangeCallBack = () => undefined,
47
+ placeholder,
48
+ placeholderClassName = '',
49
+ search = false,
50
+ timeField = false,
51
+ value = '',
52
+ valueKey = 'value'
53
+ } = props
54
+
55
+ const resolvedPlaceholder = placeholder ?? t('selectInput.placeholder')
56
+
57
+ const divRef = useRef<HTMLButtonElement | null>(null)
58
+ const [open, setOpen] = useState<boolean>(false)
59
+ const [width, setWidth] = useState<number>(0)
60
+ const [query, setQuery] = useState<string>('')
61
+ const [valueItem, setValueItem] = useState(value)
62
+
63
+ const normalizeString = (str: string): string => str.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
64
+
65
+ const normalizedItems = useMemo(() => {
66
+ if (!items) return [] as Record<Label | Value, string>[]
67
+ return items.map((it) => ({
68
+ ...it,
69
+ [labelKey]: String(it[labelKey as Label]),
70
+ [valueKey]: String(it[valueKey as Value])
71
+ })) as Record<Label | Value, string>[]
72
+ }, [items, labelKey, valueKey])
73
+
74
+ const filteredOptions = useMemo(() => {
75
+ if (!query) return normalizedItems
76
+ return normalizedItems.filter((el) =>
77
+ normalizeString(el[labelKey as Label] as string).includes(normalizeString(query))
78
+ )
79
+ }, [normalizedItems, labelKey, query])
80
+
81
+ const onValueChange = useCallback(
82
+ (val: string) => {
83
+ if (!normalizedItems.length) return
84
+ const item = normalizedItems.find((i) => i[valueKey as Value] === val)
85
+ if (item) {
86
+ onValueChangeCallBack({
87
+ label: item[labelKey as Label],
88
+ value: item[valueKey as Value]
89
+ })
90
+ setValueItem(val)
91
+ }
92
+ },
93
+ [normalizedItems, labelKey, valueKey, onValueChangeCallBack]
94
+ )
95
+
96
+ const buttonClassName = useMemo(
97
+ () =>
98
+ cn(
99
+ inputBase,
100
+ inputFocus,
101
+ 'hover:bg-surface',
102
+ disabled && inputDisabled,
103
+ error && 'border-destructive',
104
+ className
105
+ ),
106
+ [disabled, error, className]
107
+ )
108
+
109
+ const updateWidth = () => {
110
+ if (divRef.current) {
111
+ const { width } = divRef.current.getBoundingClientRect()
112
+ setWidth(width)
113
+ }
114
+ }
115
+
116
+ useEffect(() => {
117
+ updateWidth()
118
+ window.addEventListener('resize', updateWidth)
119
+ return () => window.removeEventListener('resize', updateWidth)
120
+ }, [])
121
+
122
+ useEffect(() => {
123
+ setValueItem(value)
124
+ }, [value])
125
+
126
+ return (
127
+ <div className='flex flex-col gap-1'>
128
+ <div className='relative flex flex-col justify-center'>
129
+ <Popover onOpenChange={setOpen} open={open}>
130
+ <PopoverTrigger asChild>
131
+ <Button
132
+ aria-expanded={open}
133
+ className={buttonClassName}
134
+ disabled={disabled}
135
+ isLoading={loading}
136
+ onBlur={onBlur}
137
+ ref={divRef}
138
+ role='combobox'
139
+ variant='primary'
140
+ >
141
+ {loading ? (
142
+ <>
143
+ {t('selectInput.loading')} <Spinner />
144
+ </>
145
+ ) : (
146
+ <div className='relative flex h-5 w-full items-center'>
147
+ <div className='flex w-full max-w-[90%] items-center truncate'>
148
+ {leftIcon && (
149
+ <span className='mr-2 shrink-0'>{cloneElement(leftIcon, { size: 20, strokeWidth: 1 })}</span>
150
+ )}
151
+ {valueItem ? (
152
+ filteredOptions.find((i) => i[valueKey as Value] === valueItem)?.[labelKey as Label]
153
+ ) : (
154
+ <span className={cn(inputPlaceholder, placeholderClassName)}>{resolvedPlaceholder}</span>
155
+ )}
156
+ </div>
157
+ {timeField ? (
158
+ <div className='absolute top-0.5 right-0'>
159
+ <Clock className='ml-2 h-4 w-4 shrink-0 opacity-50' />
160
+ </div>
161
+ ) : (
162
+ <div className='absolute top-0.5 right-0 flex gap-2'>
163
+ {ableDeleteOption && (
164
+ <XIcon
165
+ className='z-100 h-4 w-4 shrink-0 opacity-50'
166
+ onClick={(e: React.MouseEvent<SVGSVGElement>) => {
167
+ e.preventDefault()
168
+ e.stopPropagation()
169
+ onChange?.('')
170
+ onValueChange('')
171
+ setValueItem('')
172
+ setQuery('')
173
+ setOpen(false)
174
+ }}
175
+ />
176
+ )}
177
+ {!open ? (
178
+ <ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
179
+ ) : (
180
+ <ChevronUp className='ml-2 h-4 w-4 shrink-0 opacity-50' />
181
+ )}
182
+ </div>
183
+ )}
184
+ </div>
185
+ )}
186
+ </Button>
187
+ </PopoverTrigger>
188
+ <PopoverContent
189
+ className='bg-surface border-border z-100 rounded-sm p-1'
190
+ onOpenAutoFocus={(e) => e.preventDefault()}
191
+ style={{ maxWidth: 'auto', minWidth: `${width}px`, width: `${width}px` }}
192
+ >
193
+ <Command className='flex w-full flex-col gap-2'>
194
+ {search && (
195
+ <Input
196
+ className='mb-1 rounded-sm'
197
+ onChange={(e) => setQuery(e.target.value)}
198
+ placeholder={t('selectInput.search')}
199
+ value={query}
200
+ />
201
+ )}
202
+ <CommandEmpty className='text-center'>{t('selectInput.noResults')}</CommandEmpty>
203
+ <CommandList>
204
+ <CommandGroup className='p-0'>
205
+ {filteredOptions.map((item) => {
206
+ const labelValue = item[labelKey as Label] as string
207
+ const valueValue = item[valueKey as Value] as string
208
+
209
+ return (
210
+ <CommandItem
211
+ className={cn(
212
+ 'hover:bg-hover mt-0.5 mb-0.5 flex cursor-pointer items-center gap-2 rounded-sm py-2 pl-3',
213
+ valueValue === value && 'bg-inactive'
214
+ )}
215
+ key={valueValue}
216
+ onSelect={(currentValue: string) => {
217
+ onChange?.(currentValue)
218
+ onValueChange(currentValue)
219
+ setOpen(false)
220
+ setTimeout(() => setQuery(''), 100)
221
+ }}
222
+ value={valueValue}
223
+ >
224
+ {labelValue.trim()}
225
+ </CommandItem>
226
+ )
227
+ })}
228
+ </CommandGroup>
229
+ </CommandList>
230
+ </Command>
231
+ </PopoverContent>
232
+ </Popover>
233
+ </div>
234
+ </div>
235
+ )
236
+ }
@@ -0,0 +1,143 @@
1
+ import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
2
+ import * as SelectPrimitive from '@radix-ui/react-select'
3
+ import * as React from 'react'
4
+
5
+ import { cn } from '../utils'
6
+
7
+ const Select = SelectPrimitive.Root
8
+
9
+ const SelectGroup = SelectPrimitive.Group
10
+
11
+ const SelectValue = SelectPrimitive.Value
12
+
13
+ const SelectTrigger = React.forwardRef<
14
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
15
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16
+ >(({ children, className, ...props }, ref) => (
17
+ <SelectPrimitive.Trigger
18
+ className={cn(
19
+ 'placeholder:text-placeholder border-border ring-offset-surface focus:ring-ring flex h-9 w-full items-center justify-between rounded-xl border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-sm focus:ring-0 focus:outline-none disabled:cursor-not-allowed [&>span]:line-clamp-1',
20
+ className
21
+ )}
22
+ ref={ref}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <SelectPrimitive.Icon asChild>
27
+ <CaretSortIcon className='h-4 w-4 opacity-50' />
28
+ </SelectPrimitive.Icon>
29
+ </SelectPrimitive.Trigger>
30
+ ))
31
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32
+
33
+ const SelectScrollUpButton = React.forwardRef<
34
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
35
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
36
+ >(({ className, ...props }, ref) => (
37
+ <SelectPrimitive.ScrollUpButton
38
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
39
+ ref={ref}
40
+ {...props}
41
+ >
42
+ <ChevronUpIcon />
43
+ </SelectPrimitive.ScrollUpButton>
44
+ ))
45
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
46
+
47
+ const SelectScrollDownButton = React.forwardRef<
48
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
49
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
50
+ >(({ className, ...props }, ref) => (
51
+ <SelectPrimitive.ScrollDownButton
52
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
53
+ ref={ref}
54
+ {...props}
55
+ >
56
+ <ChevronDownIcon />
57
+ </SelectPrimitive.ScrollDownButton>
58
+ ))
59
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
60
+
61
+ const SelectContent = React.forwardRef<
62
+ React.ElementRef<typeof SelectPrimitive.Content>,
63
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
64
+ >(({ children, className, position = 'popper', ...props }, ref) => (
65
+ <SelectPrimitive.Portal>
66
+ <SelectPrimitive.Content
67
+ className={cn(
68
+ 'border-border text-foreground bg-surface data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-lg border shadow-md',
69
+ position === 'popper' &&
70
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
71
+ className
72
+ )}
73
+ position={position}
74
+ ref={ref}
75
+ {...props}
76
+ >
77
+ <SelectScrollUpButton />
78
+ <SelectPrimitive.Viewport
79
+ className={cn(
80
+ 'p-1',
81
+ position === 'popper' && 'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)'
82
+ )}
83
+ >
84
+ {children}
85
+ </SelectPrimitive.Viewport>
86
+ <SelectScrollDownButton />
87
+ </SelectPrimitive.Content>
88
+ </SelectPrimitive.Portal>
89
+ ))
90
+ SelectContent.displayName = SelectPrimitive.Content.displayName
91
+
92
+ const SelectLabel = React.forwardRef<
93
+ React.ElementRef<typeof SelectPrimitive.Label>,
94
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
95
+ >(({ className, ...props }, ref) => (
96
+ <SelectPrimitive.Label className={cn('px-2 py-1.5 text-sm font-semibold', className)} ref={ref} {...props} />
97
+ ))
98
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
99
+
100
+ const SelectItem = React.forwardRef<
101
+ React.ElementRef<typeof SelectPrimitive.Item>,
102
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
103
+ >(({ children, className, ...props }, ref) => (
104
+ <SelectPrimitive.Item
105
+ className={cn(
106
+ 'focus:text-foreground focus:bg-hover relative flex w-full cursor-default items-center rounded-md py-1.5 pr-8 pl-2 text-sm outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-50',
107
+ className
108
+ )}
109
+ ref={ref}
110
+ {...props}
111
+ >
112
+ <span className='absolute right-2 flex h-3.5 w-3.5 items-center justify-center'>
113
+ <SelectPrimitive.ItemIndicator>
114
+ <CheckIcon className='h-4 w-4' />
115
+ </SelectPrimitive.ItemIndicator>
116
+ </span>
117
+ <SelectPrimitive.ItemText>
118
+ <span>{children}</span>
119
+ </SelectPrimitive.ItemText>
120
+ </SelectPrimitive.Item>
121
+ ))
122
+ SelectItem.displayName = SelectPrimitive.Item.displayName
123
+
124
+ const SelectSeparator = React.forwardRef<
125
+ React.ElementRef<typeof SelectPrimitive.Separator>,
126
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
127
+ >(({ className, ...props }, ref) => (
128
+ <SelectPrimitive.Separator className={cn('bg-border -mx-1 my-1 h-px', className)} ref={ref} {...props} />
129
+ ))
130
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
131
+
132
+ export {
133
+ Select,
134
+ SelectContent,
135
+ SelectGroup,
136
+ SelectItem,
137
+ SelectLabel,
138
+ SelectScrollDownButton,
139
+ SelectScrollUpButton,
140
+ SelectSeparator,
141
+ SelectTrigger,
142
+ SelectValue
143
+ }
@@ -0,0 +1,61 @@
1
+ import clsx from 'clsx'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ const sizes = {
5
+ lg: '70',
6
+ md: '50',
7
+ sm: '20',
8
+ xl: '100',
9
+ xs: '5'
10
+ }
11
+
12
+ const variants = {
13
+ light: 'var(--color-spinner-light)',
14
+ primary: 'var(--color-spinner-primary)',
15
+ secondary: 'var(--color-spinner-secondary)'
16
+ }
17
+
18
+ export interface SpinnerProps {
19
+ className?: string
20
+ size?: keyof typeof sizes
21
+ variant?: keyof typeof variants
22
+ }
23
+
24
+ export const Spinner = ({ className = '', size = 'md', variant = 'primary' }: SpinnerProps) => {
25
+ const { t } = useTranslation('ui')
26
+ return (
27
+ <>
28
+ <svg
29
+ fill='currentColor'
30
+ height={sizes[size]}
31
+ stroke={variants[variant]}
32
+ viewBox='0 0 100 100'
33
+ width={sizes[size]}
34
+ xmlns='http://www.w3.org/2000/svg'
35
+ >
36
+ <g className={clsx('spinner_V8m1', sizes[size], variants[variant], className)}>
37
+ <circle cx='50' cy='50' fill='none' opacity='0.25' r='35' stroke='currentColor' strokeWidth='10' />
38
+ <circle
39
+ cx='50'
40
+ cy='50'
41
+ fill='none'
42
+ r='35'
43
+ stroke='currentColor'
44
+ strokeDasharray='164.93361431346415 56.97787143782138'
45
+ strokeWidth='10'
46
+ >
47
+ <animateTransform
48
+ attributeName='transform'
49
+ dur='1s'
50
+ keyTimes='0;1'
51
+ repeatCount='indefinite'
52
+ type='rotate'
53
+ values='0 50 50;360 50 50'
54
+ />
55
+ </circle>
56
+ </g>
57
+ </svg>
58
+ <span className='sr-only'>{t('spinner.loading')}</span>
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { cn } from '../utils'
4
+
5
+ interface StepHeaderProps {
6
+ subtitle?: ReactNode
7
+ subtitleClassName?: string
8
+ title: string
9
+ }
10
+
11
+ export const StepHeader = ({ subtitle, subtitleClassName, title }: StepHeaderProps) => (
12
+ <div className='flex flex-col gap-1'>
13
+ <h1 className='text-title text-2xl font-bold'>{title}</h1>
14
+ {subtitle !== undefined && <p className={cn('text-subtitle text-sm', subtitleClassName)}>{subtitle}</p>}
15
+ </div>
16
+ )
@@ -0,0 +1,69 @@
1
+ import { CheckIcon } from 'lucide-react'
2
+
3
+ import { cn } from '../utils'
4
+
5
+ export interface StepperStep {
6
+ label: string
7
+ }
8
+
9
+ interface StepperProps {
10
+ activeIndex: number
11
+ className?: string
12
+ intermediate?: boolean
13
+ steps: StepperStep[]
14
+ }
15
+
16
+ export const Stepper = ({ activeIndex, className, intermediate = false, steps }: StepperProps) => {
17
+ return (
18
+ <div className={cn('flex w-full items-start', className)}>
19
+ {steps.map((step, index) => {
20
+ const isCompleted = intermediate ? index <= activeIndex : index < activeIndex
21
+ const isActive = !intermediate && index === activeIndex
22
+
23
+ return (
24
+ <div className='flex flex-1 flex-col items-center' key={step.label}>
25
+ <div className='flex w-full items-center'>
26
+ <div
27
+ className={cn(
28
+ 'h-0.75 flex-1 self-center',
29
+ isCompleted || isActive ? 'bg-primary' : 'bg-inactive',
30
+ index === 0 && 'rounded-l-full'
31
+ )}
32
+ />
33
+ <div
34
+ aria-current={isActive ? 'step' : undefined}
35
+ className={cn(
36
+ 'flex h-5.5 w-5.5 shrink-0 items-center justify-center rounded-full border text-xs font-semibold transition-colors md:h-6.5 md:w-6.5',
37
+ isCompleted && 'bg-primary border-primary text-inverse',
38
+ isActive && 'bg-primary border-primary text-inverse',
39
+ !isCompleted && !isActive && 'bg-inactive border-inactive text-surface'
40
+ )}
41
+ >
42
+ {isCompleted ? <CheckIcon className='h-4 w-4' /> : index + 1}
43
+ </div>
44
+ <div
45
+ className={cn(
46
+ 'relative h-0.75 flex-1 self-center',
47
+ isCompleted ? 'bg-primary' : 'bg-inactive',
48
+ index === steps.length - 1 && 'rounded-r-full'
49
+ )}
50
+ >
51
+ {index === 0 && (
52
+ <div
53
+ className={cn(
54
+ 'absolute top-1/2 right-0 h-2.25 w-2.25 translate-x-1/2 -translate-y-1/2 rounded-full transition-colors',
55
+ isCompleted ? 'bg-primary' : 'bg-inactive'
56
+ )}
57
+ />
58
+ )}
59
+ </div>
60
+ </div>
61
+ <span className={cn('mt-1 text-xs font-normal', isCompleted || isActive ? 'text-primary' : 'text-muted')}>
62
+ {step.label}
63
+ </span>
64
+ </div>
65
+ )
66
+ })}
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,2 @@
1
+ export { Toast } from './toast'
2
+ export { ToastProvider } from './toast-provider'