@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,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,7 @@
1
+ import uiEs from './es/ui.json'
2
+
3
+ export const uiLocales = {
4
+ es: {
5
+ ui: uiEs
6
+ }
7
+ }
@@ -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'
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,3 @@
1
+ export { cn } from './cn'
2
+ export { inputBase, inputDisabled, inputFocus, inputPlaceholder } from './styles'
3
+ export { registerToastEmitter, toast } from './toast-emitter'
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ interface ImportMeta {
2
+ readonly env: Record<string, string | undefined>
3
+ }