@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.
- package/package.json +48 -0
- package/src/components/alert.tsx +45 -0
- package/src/components/auth-header.tsx +26 -0
- package/src/components/button.tsx +121 -0
- package/src/components/command.tsx +125 -0
- package/src/components/dialog.tsx +118 -0
- package/src/components/form-fields/index.ts +5 -0
- package/src/components/form-fields/input-field.tsx +131 -0
- package/src/components/form-fields/label-field.tsx +20 -0
- package/src/components/form-fields/map-picker-field.tsx +49 -0
- package/src/components/form-fields/otp-field.tsx +76 -0
- package/src/components/form-fields/select-field.tsx +77 -0
- package/src/components/form.tsx +138 -0
- package/src/components/google-map-picker.tsx +243 -0
- package/src/components/input.tsx +40 -0
- package/src/components/label.tsx +17 -0
- package/src/components/loading.tsx +19 -0
- package/src/components/otp-input.tsx +62 -0
- package/src/components/password-checklist.tsx +37 -0
- package/src/components/popover.tsx +31 -0
- package/src/components/select-input.tsx +236 -0
- package/src/components/select.tsx +143 -0
- package/src/components/spinner.tsx +61 -0
- package/src/components/step-header.tsx +16 -0
- package/src/components/stepper.tsx +69 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast-provider.tsx +123 -0
- package/src/components/toast/toast.tsx +106 -0
- package/src/components/tooltip.tsx +56 -0
- package/src/components/wrapper.tsx +12 -0
- package/src/index.ts +27 -0
- package/src/locales/es/ui.json +28 -0
- package/src/locales/index.ts +7 -0
- package/src/styles/index.css +1 -0
- package/src/styles/theme.css +105 -0
- package/src/types/password.ts +12 -0
- package/src/types/toast.ts +19 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/styles.ts +19 -0
- package/src/utils/toast-emitter.ts +37 -0
- package/src/vite-env.d.ts +3 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { ToastOptions, ToastPosition, ToastType } from '../../types/toast'
|
|
4
|
+
import { registerToastEmitter } from '../../utils/toast-emitter'
|
|
5
|
+
import { Toast } from './toast'
|
|
6
|
+
|
|
7
|
+
interface ToastItem extends ToastOptions {
|
|
8
|
+
id: string
|
|
9
|
+
type: ToastType
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ToastProvider() {
|
|
13
|
+
const [toasts, setToasts] = useState<ToastItem[]>([])
|
|
14
|
+
const itemRefs = useRef(new Map<string, HTMLDivElement>())
|
|
15
|
+
const prevRects = useRef(new Map<string, DOMRect>())
|
|
16
|
+
|
|
17
|
+
const showToast = useCallback((type: ToastType, options: ToastOptions) => {
|
|
18
|
+
setToasts((prev) => [
|
|
19
|
+
...prev,
|
|
20
|
+
{
|
|
21
|
+
duration: 2500,
|
|
22
|
+
id: crypto.randomUUID(),
|
|
23
|
+
position: 'top-right',
|
|
24
|
+
type,
|
|
25
|
+
...options
|
|
26
|
+
}
|
|
27
|
+
])
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const removeToast = useCallback((id: string) => {
|
|
31
|
+
setToasts((prev) => prev.filter((t) => t.id !== id))
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
registerToastEmitter(showToast)
|
|
36
|
+
return () => registerToastEmitter(null)
|
|
37
|
+
}, [showToast])
|
|
38
|
+
|
|
39
|
+
const grouped = useMemo(() => {
|
|
40
|
+
return toasts.reduce<Partial<Record<ToastPosition, ToastItem[]>>>((acc, toast) => {
|
|
41
|
+
const pos = toast.position ?? 'top'
|
|
42
|
+
acc[pos] = [...(acc[pos] ?? []), toast]
|
|
43
|
+
return acc
|
|
44
|
+
}, {})
|
|
45
|
+
}, [toasts])
|
|
46
|
+
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
const newRects = new Map<string, DOMRect>()
|
|
49
|
+
|
|
50
|
+
itemRefs.current.forEach((el, id) => {
|
|
51
|
+
newRects.set(id, el.getBoundingClientRect())
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
newRects.forEach((newRect, id) => {
|
|
55
|
+
const prevRect = prevRects.current.get(id)
|
|
56
|
+
const el = itemRefs.current.get(id)
|
|
57
|
+
if (!el || !prevRect) return
|
|
58
|
+
|
|
59
|
+
const dy = prevRect.top - newRect.top
|
|
60
|
+
if (dy === 0) return
|
|
61
|
+
|
|
62
|
+
el.style.transform = `translateY(${dy}px)`
|
|
63
|
+
el.style.transition = 'transform 0s'
|
|
64
|
+
|
|
65
|
+
requestAnimationFrame(() => {
|
|
66
|
+
el.style.transition = 'transform 200ms ease-out'
|
|
67
|
+
el.style.transform = ''
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
prevRects.current = newRects
|
|
72
|
+
}, [toasts])
|
|
73
|
+
|
|
74
|
+
const setItemRef = useCallback((id: string) => {
|
|
75
|
+
return (el: HTMLDivElement | null) => {
|
|
76
|
+
if (!el) {
|
|
77
|
+
itemRefs.current.delete(id)
|
|
78
|
+
prevRects.current.delete(id)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
itemRefs.current.set(id, el)
|
|
82
|
+
}
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
{Object.entries(grouped).map(([position, list]) => (
|
|
88
|
+
<div className={`fixed z-4000 ${getPositionClass(position as ToastPosition)}`} key={position}>
|
|
89
|
+
<div className='relative flex flex-col gap-3'>
|
|
90
|
+
{list.map((toast) => (
|
|
91
|
+
<div key={toast.id} ref={setItemRef(toast.id)}>
|
|
92
|
+
<Toast
|
|
93
|
+
duration={toast.duration}
|
|
94
|
+
id={toast.id}
|
|
95
|
+
onClose={removeToast}
|
|
96
|
+
subtitle={toast.subtitle}
|
|
97
|
+
title={toast.title}
|
|
98
|
+
type={toast.type}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
))}
|
|
105
|
+
</>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getPositionClass(position: ToastPosition) {
|
|
110
|
+
const map: Record<ToastPosition, string> = {
|
|
111
|
+
bottom: 'bottom-6 left-1/2 -translate-x-1/2',
|
|
112
|
+
'bottom-left': 'bottom-6 left-6',
|
|
113
|
+
'bottom-right': 'bottom-6 right-6',
|
|
114
|
+
center: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
|
|
115
|
+
'middle-bottom': 'bottom-20 left-1/2 -translate-x-1/2',
|
|
116
|
+
'middle-top': 'top-20 left-1/2 -translate-x-1/2',
|
|
117
|
+
top: 'top-6 left-1/2 -translate-x-1/2',
|
|
118
|
+
'top-left': 'top-6 left-6',
|
|
119
|
+
'top-right': 'top-6 right-6'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return map[position]
|
|
123
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AlertTriangle, Check, X } from 'lucide-react'
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { IoDocumentLockOutline, IoInformationCircle } from 'react-icons/io5'
|
|
4
|
+
|
|
5
|
+
import { ToastType } from '../../types/toast'
|
|
6
|
+
|
|
7
|
+
interface ToastProps {
|
|
8
|
+
duration?: number
|
|
9
|
+
id: string
|
|
10
|
+
onClose: (id: string) => void
|
|
11
|
+
subtitle?: string
|
|
12
|
+
title: string
|
|
13
|
+
type: ToastType
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EXIT_ANIMATION_MS = 200
|
|
17
|
+
|
|
18
|
+
export function Toast({ duration = 2500, id, onClose, subtitle, title, type }: ToastProps) {
|
|
19
|
+
const [leaving, setLeaving] = useState(false)
|
|
20
|
+
const startedAtRef = useRef<number>(Date.now())
|
|
21
|
+
const timeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null)
|
|
22
|
+
const exitRef = useRef<null | ReturnType<typeof setTimeout>>(null)
|
|
23
|
+
|
|
24
|
+
const Icon = useMemo(() => {
|
|
25
|
+
if (type === 'success') return Check
|
|
26
|
+
if (type === 'error') return X
|
|
27
|
+
if (type === 'warning') return AlertTriangle
|
|
28
|
+
if (type === 'info') return IoInformationCircle
|
|
29
|
+
return IoDocumentLockOutline
|
|
30
|
+
}, [type])
|
|
31
|
+
|
|
32
|
+
const styles = useMemo(() => {
|
|
33
|
+
if (type === 'success') {
|
|
34
|
+
return { border: 'border-success', icon: 'text-success', iconBorder: 'border-success' }
|
|
35
|
+
}
|
|
36
|
+
if (type === 'error') {
|
|
37
|
+
return { border: 'border-destructive', icon: 'text-destructive', iconBorder: 'border-destructive' }
|
|
38
|
+
}
|
|
39
|
+
if (type === 'warning') {
|
|
40
|
+
return { border: 'border-warning', icon: 'text-warning', iconBorder: 'border-warning' }
|
|
41
|
+
}
|
|
42
|
+
if (type === 'info') {
|
|
43
|
+
return { border: 'border-info', icon: 'text-info', iconBorder: 'border-info' }
|
|
44
|
+
}
|
|
45
|
+
return { border: 'border-wait', icon: 'text-wait', iconBorder: 'border-wait' }
|
|
46
|
+
}, [type])
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
50
|
+
if (exitRef.current) clearTimeout(exitRef.current)
|
|
51
|
+
|
|
52
|
+
const elapsed = Date.now() - startedAtRef.current
|
|
53
|
+
const remaining = Math.max(0, duration - elapsed)
|
|
54
|
+
|
|
55
|
+
timeoutRef.current = setTimeout(() => {
|
|
56
|
+
setLeaving(true)
|
|
57
|
+
exitRef.current = setTimeout(() => onClose(id), EXIT_ANIMATION_MS)
|
|
58
|
+
}, remaining)
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
62
|
+
if (exitRef.current) clearTimeout(exitRef.current)
|
|
63
|
+
}
|
|
64
|
+
}, [id, duration, onClose])
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
aria-live='polite'
|
|
69
|
+
className={[
|
|
70
|
+
'relative',
|
|
71
|
+
leaving ? 'absolute top-0 right-0 left-0' : '',
|
|
72
|
+
'flex w-108.5 max-w-[calc(100vw-2rem)] items-center justify-between',
|
|
73
|
+
'min-h-16',
|
|
74
|
+
'bg-surface rounded-[10px] border-2',
|
|
75
|
+
'px-5 py-4.5',
|
|
76
|
+
'shadow-[0_4px_20px_rgba(0,0,0,0.05)]',
|
|
77
|
+
'transition-all duration-200 ease-out',
|
|
78
|
+
styles.border,
|
|
79
|
+
leaving ? 'translate-y-2 opacity-0' : 'translate-y-0 opacity-100'
|
|
80
|
+
].join(' ')}
|
|
81
|
+
role='status'
|
|
82
|
+
>
|
|
83
|
+
<div className='min-w-0 pr-4'>
|
|
84
|
+
<h4 className={['text-[16px] leading-normal font-semibold', 'text-foreground', 'line-clamp-2'].join(' ')}>
|
|
85
|
+
{title}
|
|
86
|
+
</h4>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{subtitle ? (
|
|
90
|
+
<div className='shrink-0 pr-4 text-left'>
|
|
91
|
+
<div className={['max-w-28 text-[14px] leading-[19.5px] font-normal', 'text-body'].join(' ')}>{subtitle}</div>
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
|
|
95
|
+
<div
|
|
96
|
+
className={[
|
|
97
|
+
'shrink-0',
|
|
98
|
+
'flex h-8 w-8 min-w-8 items-center justify-center rounded-full border-2',
|
|
99
|
+
styles.iconBorder
|
|
100
|
+
].join(' ')}
|
|
101
|
+
>
|
|
102
|
+
<Icon className={['h-5 w-5', styles.icon].join(' ')} />
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../utils'
|
|
5
|
+
|
|
6
|
+
interface TooltipProps {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
childrenClassName?: string
|
|
9
|
+
className?: string
|
|
10
|
+
message: string
|
|
11
|
+
sideTooltip?: 'bottom' | 'left' | 'right' | 'top'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
15
|
+
const TooltipRoot = TooltipPrimitive.Root
|
|
16
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
17
|
+
|
|
18
|
+
const TooltipContent = React.forwardRef<
|
|
19
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
21
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
22
|
+
<TooltipPrimitive.Portal>
|
|
23
|
+
<TooltipPrimitive.Content
|
|
24
|
+
className={cn(
|
|
25
|
+
'bg-tooltip text-tooltip-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 overflow-hidden rounded-md px-3 py-1.5 text-sm font-semibold shadow-sm',
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
ref={ref}
|
|
29
|
+
sideOffset={sideOffset}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
</TooltipPrimitive.Portal>
|
|
33
|
+
))
|
|
34
|
+
|
|
35
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
36
|
+
|
|
37
|
+
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
|
38
|
+
({ children, childrenClassName, className, message, sideTooltip = 'top' }, ref) => {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipProvider>
|
|
41
|
+
<TooltipRoot>
|
|
42
|
+
<TooltipTrigger asChild>
|
|
43
|
+
<div className={cn('', childrenClassName)} ref={ref}>
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
</TooltipTrigger>
|
|
47
|
+
<TooltipContent className={cn('', className)} side={sideTooltip}>
|
|
48
|
+
<p>{message}</p>
|
|
49
|
+
</TooltipContent>
|
|
50
|
+
</TooltipRoot>
|
|
51
|
+
</TooltipProvider>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Tooltip.displayName = 'Tooltip'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../utils'
|
|
4
|
+
|
|
5
|
+
export interface WrapperProps {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Wrapper = ({ children, className }: WrapperProps) => {
|
|
11
|
+
return <div className={cn('mx-auto h-full w-full', className)}>{children}</div>
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export * from './components/alert'
|
|
2
|
+
export * from './components/auth-header'
|
|
3
|
+
export * from './components/button'
|
|
4
|
+
export * from './components/command'
|
|
5
|
+
export * from './components/dialog'
|
|
6
|
+
export * from './components/form'
|
|
7
|
+
export * from './components/form-fields'
|
|
8
|
+
export * from './components/google-map-picker'
|
|
9
|
+
export * from './components/input'
|
|
10
|
+
export * from './components/label'
|
|
11
|
+
export * from './components/loading'
|
|
12
|
+
export * from './components/otp-input'
|
|
13
|
+
export * from './components/password-checklist'
|
|
14
|
+
export * from './components/popover'
|
|
15
|
+
export * from './components/select'
|
|
16
|
+
export * from './components/select-input'
|
|
17
|
+
export * from './components/spinner'
|
|
18
|
+
export * from './components/step-header'
|
|
19
|
+
export * from './components/stepper'
|
|
20
|
+
export * from './components/toast'
|
|
21
|
+
export * from './components/tooltip'
|
|
22
|
+
export * from './components/wrapper'
|
|
23
|
+
|
|
24
|
+
export * from './types/toast'
|
|
25
|
+
export * from './types/password'
|
|
26
|
+
|
|
27
|
+
export { cn, inputBase, inputDisabled, inputFocus, inputPlaceholder, registerToastEmitter, toast } from './utils'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"otpField": {
|
|
3
|
+
"description": "Se ha enviado un código OTP a tu correo . Revisa tu bandeja de entrada."
|
|
4
|
+
},
|
|
5
|
+
"inputField": {
|
|
6
|
+
"showPassword": "Mostrar contraseña",
|
|
7
|
+
"hidePassword": "Ocultar contraseña",
|
|
8
|
+
"clear": "Limpiar"
|
|
9
|
+
},
|
|
10
|
+
"dialog": {
|
|
11
|
+
"close": "Cerrar"
|
|
12
|
+
},
|
|
13
|
+
"selectInput": {
|
|
14
|
+
"placeholder": "Selecciona...",
|
|
15
|
+
"loading": "Cargando...",
|
|
16
|
+
"search": "Buscar...",
|
|
17
|
+
"noResults": "Sin resultados."
|
|
18
|
+
},
|
|
19
|
+
"passwordChecklist": {
|
|
20
|
+
"minLength": "Mínimo {{min}} caracteres",
|
|
21
|
+
"hasUpperAndLower": "Una mayúscula y minúscula",
|
|
22
|
+
"hasNumber": "Un número",
|
|
23
|
+
"hasSpecialChar": "Un carácter especial"
|
|
24
|
+
},
|
|
25
|
+
"spinner": {
|
|
26
|
+
"loading": "Cargando..."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import './theme.css';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* ── Capa 1: Primitivos ────────────────────────────────────────────────────
|
|
2
|
+
Valores reales del design system. Solo esta capa cambia cuando
|
|
3
|
+
UI/UX entregue actualizaciones — los tokens semánticos no se tocan.
|
|
4
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
5
|
+
:root {
|
|
6
|
+
/* Brand — Naranja */
|
|
7
|
+
--brand-100: #fdf0ea;
|
|
8
|
+
--brand-200: #facdb4;
|
|
9
|
+
--brand-300: #f5a478;
|
|
10
|
+
--brand-400: #ec7a45;
|
|
11
|
+
--brand-500: #e25822;
|
|
12
|
+
--brand-600: #b84418;
|
|
13
|
+
--brand-700: #8a3011;
|
|
14
|
+
|
|
15
|
+
/* Neutral */
|
|
16
|
+
--neutral-0: #ffffff;
|
|
17
|
+
--neutral-50: #f9f8f7;
|
|
18
|
+
--neutral-100: #f2f0ee;
|
|
19
|
+
--neutral-200: #e4e1dc;
|
|
20
|
+
--neutral-300: #c8c3bb;
|
|
21
|
+
--neutral-400: #999999;
|
|
22
|
+
--neutral-500: #6b6560;
|
|
23
|
+
--neutral-600: #4a4540;
|
|
24
|
+
--neutral-900: #1e1e1e;
|
|
25
|
+
--neutral-950: #000000;
|
|
26
|
+
|
|
27
|
+
/* Feedback */
|
|
28
|
+
--success-500: #2d9b6f;
|
|
29
|
+
--success-100: #e8f7f1;
|
|
30
|
+
--warning-500: #d4820a;
|
|
31
|
+
--warning-100: #fef3e2;
|
|
32
|
+
--error-500: #d63b3b;
|
|
33
|
+
--error-100: #fdecec;
|
|
34
|
+
--info-500: #2f7ec4;
|
|
35
|
+
--info-100: #eaf3fb;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ── Capa 2: Semánticos ────────────────────────────────────────────────────
|
|
39
|
+
Los componentes solo conocen estos tokens, nunca los primitivos.
|
|
40
|
+
@theme inline genera las utilidades de Tailwind (bg-*, text-*, etc.)
|
|
41
|
+
Naming: sin prefijo bg-/text- porque Tailwind lo agrega en la clase.
|
|
42
|
+
bg-page, bg-surface, bg-subtle, bg-hover, bg-brand, bg-brand-subtle
|
|
43
|
+
text-primary → text-foreground (evita colisión con brand primary)
|
|
44
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
45
|
+
@theme {
|
|
46
|
+
/* ── Brand / Primary ── */
|
|
47
|
+
--color-primary: var(--brand-500); /* bg-brand, text-brand */
|
|
48
|
+
--color-primary-hover: var(--brand-400); /* hover de botón primario */
|
|
49
|
+
--color-primary-dark: var(--brand-600); /* pressed/active de botón */
|
|
50
|
+
--color-brand-subtle: var(--brand-100); /* chips, tags de marca */
|
|
51
|
+
--color-brand-200: var(--brand-200); /* active card background */
|
|
52
|
+
--color-brand-border: var(--brand-300); /* bordes activos, focus ring */
|
|
53
|
+
|
|
54
|
+
/* ── Surfaces ── */
|
|
55
|
+
--color-page: var(--neutral-0); /* bg-page → fondo de página */
|
|
56
|
+
--color-surface: var(--neutral-0); /* bg-surface → cards, modales */
|
|
57
|
+
--color-subtle: var(--neutral-50); /* bg-subtle → sidebars, alternas */
|
|
58
|
+
--color-hover: var(--neutral-100); /* bg-hover → filas, list items */
|
|
59
|
+
|
|
60
|
+
/* ── Text ── */
|
|
61
|
+
--color-foreground: var(--neutral-950); /* text-foreground → títulos, cuerpo */
|
|
62
|
+
--color-title: var(--neutral-900); /* text-title → titulos */
|
|
63
|
+
--color-subtitle: var(--neutral-400); /* text-subtitle → párrafos */
|
|
64
|
+
--color-body: var(--neutral-600); /* text-body → párrafos */
|
|
65
|
+
--color-muted: var(--neutral-500); /* text-muted → descripciones, meta */
|
|
66
|
+
--color-disabled: var(--neutral-300); /* text-disabled → inputs off */
|
|
67
|
+
--color-inverse: var(--neutral-0); /* text-inverse → sobre fondos oscuros */
|
|
68
|
+
|
|
69
|
+
/* ── Estados de componentes interactivos ── */
|
|
70
|
+
--color-inactive: var(--neutral-200); /* bg disabled */
|
|
71
|
+
--color-inactive-text: var(--neutral-400); /* text disabled */
|
|
72
|
+
|
|
73
|
+
/* ── Feedback ── */
|
|
74
|
+
--color-success: var(--success-500);
|
|
75
|
+
--color-success-subtle: var(--success-100);
|
|
76
|
+
--color-warning: var(--warning-500);
|
|
77
|
+
--color-warning-subtle: var(--warning-100);
|
|
78
|
+
--color-destructive: var(--error-500);
|
|
79
|
+
--color-destructive-subtle: var(--error-100);
|
|
80
|
+
--color-info: var(--info-500);
|
|
81
|
+
--color-info-subtle: var(--info-100);
|
|
82
|
+
--color-wait: var(--info-500);
|
|
83
|
+
|
|
84
|
+
/* ── UI Components ── */
|
|
85
|
+
--color-border: var(--neutral-300); /* bordes de inputs */
|
|
86
|
+
--color-input: var(--neutral-0); /* fondo de inputs = blanco */
|
|
87
|
+
--color-placeholder: var(--neutral-300); /* placeholder text */
|
|
88
|
+
--color-icon-muted: var(--neutral-400); /* iconos inactivos */
|
|
89
|
+
--color-tooltip: var(--neutral-900);
|
|
90
|
+
--color-tooltip-foreground: var(--neutral-0);
|
|
91
|
+
|
|
92
|
+
/* ── Spinner ── */
|
|
93
|
+
--color-spinner-light: var(--neutral-50);
|
|
94
|
+
--color-spinner-primary: var(--brand-500);
|
|
95
|
+
--color-spinner-secondary: var(--neutral-900);
|
|
96
|
+
|
|
97
|
+
/* ── Sistema ── */
|
|
98
|
+
--color-ring: var(--brand-300); /* focus outline — brand-300, 2px per spec */
|
|
99
|
+
|
|
100
|
+
/* ── Fuentes ──
|
|
101
|
+
DM Sans es la tipografía global (definida en global.css).
|
|
102
|
+
Poppins es excepción puntual — solo donde UX lo indique (p. ej. título del login).
|
|
103
|
+
*/
|
|
104
|
+
--font-poppins: 'Poppins', sans-serif;
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PasswordRule {
|
|
2
|
+
key: string
|
|
3
|
+
label: string
|
|
4
|
+
test: (value: string) => boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PasswordRulesConfig {
|
|
8
|
+
hasNumber?: boolean
|
|
9
|
+
hasSpecialChar?: boolean
|
|
10
|
+
hasUpperAndLower?: boolean
|
|
11
|
+
minLength?: false | number
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ToastOptions {
|
|
2
|
+
duration?: number
|
|
3
|
+
position?: ToastPosition
|
|
4
|
+
subtitle?: string
|
|
5
|
+
title: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ToastPosition =
|
|
9
|
+
| 'bottom'
|
|
10
|
+
| 'bottom-left'
|
|
11
|
+
| 'bottom-right'
|
|
12
|
+
| 'center'
|
|
13
|
+
| 'middle-bottom'
|
|
14
|
+
| 'middle-top'
|
|
15
|
+
| 'top'
|
|
16
|
+
| 'top-left'
|
|
17
|
+
| 'top-right'
|
|
18
|
+
|
|
19
|
+
export type ToastType = 'error' | 'info' | 'success' | 'wait' | 'warning'
|
package/src/utils/cn.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cn } from './cn'
|
|
2
|
+
|
|
3
|
+
export const inputBase = cn(
|
|
4
|
+
'flex w-full !h-[50px] rounded-[10px] px-3 gap-[10px] text-base font-normal justify-between items-center',
|
|
5
|
+
'border border-border bg-surface text-foreground',
|
|
6
|
+
'transition-all duration-200 ease-in-out',
|
|
7
|
+
'placeholder:text-icon-muted placeholder:text-sm placeholder:font-normal placeholder:leading-none',
|
|
8
|
+
'disabled:cursor-not-allowed disabled:bg-inactive disabled:text-inactive-text disabled:border-inactive disabled:placeholder:text-inactive-text'
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export const inputPlaceholder = cn('text-icon-muted text-sm font-normal leading-none')
|
|
12
|
+
|
|
13
|
+
export const inputDisabled = cn('cursor-not-allowed opacity-50')
|
|
14
|
+
|
|
15
|
+
export const inputFocus = cn(
|
|
16
|
+
'focus:outline-none focus-visible:outline-none',
|
|
17
|
+
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-primary',
|
|
18
|
+
'data-[state=open]:ring-2 data-[state=open]:ring-ring data-[state=open]:border-primary'
|
|
19
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ToastOptions, ToastType } from '../types/toast'
|
|
2
|
+
|
|
3
|
+
type ToastEmitter = ((type: ToastType, options: ToastOptions) => void) | null
|
|
4
|
+
|
|
5
|
+
let emitToast: ToastEmitter = null
|
|
6
|
+
|
|
7
|
+
export const toast = {
|
|
8
|
+
error(options: string | ToastOptions) {
|
|
9
|
+
emit('error', options)
|
|
10
|
+
},
|
|
11
|
+
info(options: string | ToastOptions) {
|
|
12
|
+
emit('info', options)
|
|
13
|
+
},
|
|
14
|
+
success(options: string | ToastOptions) {
|
|
15
|
+
emit('success', options)
|
|
16
|
+
},
|
|
17
|
+
wait(options: string | ToastOptions) {
|
|
18
|
+
emit('wait', options)
|
|
19
|
+
},
|
|
20
|
+
warning(options: string | ToastOptions) {
|
|
21
|
+
emit('warning', options)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerToastEmitter(fn: ToastEmitter) {
|
|
26
|
+
emitToast = fn
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function emit(type: ToastType, options: string | ToastOptions) {
|
|
30
|
+
if (!emitToast) return
|
|
31
|
+
|
|
32
|
+
if (typeof options === 'string') {
|
|
33
|
+
emitToast(type, { title: options })
|
|
34
|
+
} else {
|
|
35
|
+
emitToast(type, options)
|
|
36
|
+
}
|
|
37
|
+
}
|