@mci-ui/mci-ui 0.0.0 → 0.0.2

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.
@@ -0,0 +1,92 @@
1
+ import { X } from 'lucide-react'
2
+ import React from 'react'
3
+ import { cn } from '../../lib/utils.ts'
4
+
5
+ type TagProps = {
6
+ children: React.ReactNode
7
+ variant?: 'default' | 'secondary' | 'success' | 'error' | 'info' | 'accent'
8
+ size?: 'sm' | 'md' | 'lg'
9
+ icon?: React.ReactNode
10
+ iconPosition?: 'left' | 'right'
11
+ rounded?: boolean
12
+ closable?: boolean
13
+ onClose?: () => void
14
+ className?: string
15
+ }
16
+
17
+ const sizes = {
18
+ sm: 'text-xs px-2 py-0.5',
19
+ md: 'text-sm px-3 py-1',
20
+ lg: 'text-base px-4 py-1.5',
21
+ }
22
+
23
+ const iconSizes = {
24
+ sm: 'w-3 h-3',
25
+ md: 'w-4 h-4',
26
+ lg: 'w-5 h-5',
27
+ }
28
+
29
+ const variants = {
30
+ default:
31
+ 'bg-gray-100 text-gray-800 border border-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:border-gray-700',
32
+ secondary:
33
+ 'bg-secondary-100 text-secondary-800 border border-secondary-200 dark:bg-secondary-800 dark:text-secondary-50 dark:border-secondary-700',
34
+ success:
35
+ 'bg-success-100 text-success-800 border border-success-200 dark:bg-success-800 dark:text-success-50 dark:border-success-700',
36
+ error:
37
+ 'bg-error-100 text-error-800 border border-error-200 dark:bg-error-800 dark:text-error-50 dark:border-error-700',
38
+ info: 'bg-info-100 text-info-800 border border-info-200 dark:bg-info-800 dark:text-info-50 dark:border-info-700',
39
+ accent:
40
+ 'bg-accent-100 text-accent-800 border border-accent-200 dark:bg-accent-800 dark:text-accent-50 dark:border-accent-700',
41
+ }
42
+
43
+ export default function Tag({
44
+ children,
45
+ variant = 'default',
46
+ size = 'md',
47
+ icon,
48
+ iconPosition = 'left',
49
+ closable = false,
50
+ onClose,
51
+ className,
52
+ }: TagProps) {
53
+ return (
54
+ <span
55
+ className={cn(
56
+ 'inline-flex items-center font-medium animate-[fadeIn_0.3s_ease-out] rounded-[7px]',
57
+ sizes[size],
58
+ variants[variant],
59
+ icon && 'gap-1.5',
60
+ className
61
+ )}
62
+ >
63
+ {/* ICON LEFT */}
64
+ {icon && iconPosition === 'left' && (
65
+ <span className={`flex items-center ${cn(iconSizes[size])}`}>
66
+ {icon}
67
+ </span>
68
+ )}
69
+
70
+ {/* TEXT */}
71
+ <span>{children}</span>
72
+
73
+ {/* ICON RIGHT */}
74
+ {icon && iconPosition === 'right' && (
75
+ <span className={`flex items-center ${cn(iconSizes[size])}`}>
76
+ {icon}
77
+ </span>
78
+ )}
79
+
80
+ {/* CLOSE BUTTON */}
81
+ {closable && (
82
+ <button
83
+ type='button'
84
+ onClick={onClose}
85
+ className='ml-1 rounded-full p-0.5 hover:bg-black/10 dark:hover:bg-white/10 transition-colors'
86
+ >
87
+ <X className={cn(iconSizes[size])} />
88
+ </button>
89
+ )}
90
+ </span>
91
+ )
92
+ }
@@ -0,0 +1,72 @@
1
+ import { useState } from 'react'
2
+ import { cn } from '../../lib/utils.ts'
3
+
4
+ type TextareaProps = {
5
+ label?: string
6
+ placeholder?: string
7
+ value?: string
8
+ onChange?: (value: string) => void
9
+ required?: boolean
10
+ disabled?: boolean
11
+ error?: string
12
+ className?: string
13
+ rows?: number
14
+ }
15
+
16
+ export default function Textarea({
17
+ label,
18
+ placeholder,
19
+ value = '',
20
+ onChange,
21
+ required = false,
22
+ disabled = false,
23
+ error,
24
+ className,
25
+ rows = 4,
26
+ }: TextareaProps) {
27
+ const [focused, setFocused] = useState(false)
28
+
29
+ const isFloating = focused || value.length > 0
30
+
31
+ return (
32
+ <div className='relative w-full'>
33
+ {/* Label */}
34
+ {label && (
35
+ <label
36
+ className={cn(
37
+ 'absolute left-3 top-2 text-gray-500 dark:text-gray-400 transition-all duration-200 pointer-events-none',
38
+ isFloating
39
+ ? 'text-xs -translate-y-3 bg-white dark:bg-gray-900 px-1'
40
+ : 'text-sm translate-y-0'
41
+ )}
42
+ >
43
+ {label}
44
+ {required && <span className='text-error-500 ml-0.5'>*</span>}
45
+ </label>
46
+ )}
47
+
48
+ {/* Textarea */}
49
+ <textarea
50
+ rows={rows}
51
+ placeholder={label ? undefined : placeholder}
52
+ value={value}
53
+ onChange={e => onChange?.(e.target.value)}
54
+ onFocus={() => setFocused(true)}
55
+ onBlur={() => setFocused(false)}
56
+ disabled={disabled}
57
+ className={cn(
58
+ 'w-full rounded-md border border-gray-300 dark:border-gray-700 bg-transparent text-gray-900 dark:text-gray-100 px-3 py-2 outline-none transition-all resize-none',
59
+ 'focus:border-primary-500 focus:ring-1 focus:ring-primary-500',
60
+ disabled && 'opacity-50 cursor-not-allowed',
61
+ error &&
62
+ 'border-error-500 focus:ring-error-500 focus:border-error-500',
63
+ label && 'pt-5',
64
+ className
65
+ )}
66
+ />
67
+
68
+ {/* Error message */}
69
+ {error && <p className='mt-1 text-sm text-error-500'>{error}</p>}
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,150 @@
1
+ import {
2
+ AlertCircle,
3
+ CheckCircle2,
4
+ Info,
5
+ Loader2,
6
+ X,
7
+ XCircle,
8
+ } from 'lucide-react'
9
+ import { useEffect, useState } from 'react'
10
+ import { cn } from '../../lib/utils.ts'
11
+
12
+ // --- TYPES ---
13
+ type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading'
14
+
15
+ export type ToastProps = {
16
+ id?: string
17
+ title: string
18
+ description?: string
19
+ type?: ToastType
20
+ duration?: number
21
+ position?:
22
+ | 'top-right'
23
+ | 'top-left'
24
+ | 'bottom-right'
25
+ | 'bottom-left'
26
+ | 'top-center'
27
+ | 'bottom-center'
28
+ onClose?: (id: string) => void
29
+ action?: {
30
+ label: string
31
+ onClick: () => void
32
+ }
33
+ }
34
+
35
+ // --- MAIN TOAST COMPONENT ---
36
+ export function Toast({
37
+ id = Math.random().toString(36),
38
+ title,
39
+ description,
40
+ type = 'info',
41
+ duration = 5000,
42
+ position = 'top-right',
43
+ onClose,
44
+ action,
45
+ }: ToastProps) {
46
+ const [isVisible, setIsVisible] = useState(false)
47
+ const [isLeaving, setIsLeaving] = useState(false)
48
+
49
+ useEffect(() => {
50
+ const showTimer = setTimeout(() => setIsVisible(true), 100)
51
+ if (duration !== Infinity) {
52
+ const closeTimer = setTimeout(() => handleClose(), duration)
53
+ return () => {
54
+ clearTimeout(showTimer)
55
+ clearTimeout(closeTimer)
56
+ }
57
+ }
58
+ return () => clearTimeout(showTimer)
59
+ }, [duration])
60
+
61
+ const handleClose = () => {
62
+ setIsLeaving(true)
63
+ setTimeout(() => {
64
+ setIsVisible(false)
65
+ onClose?.(id)
66
+ }, 300)
67
+ }
68
+
69
+ const handleAction = () => {
70
+ action?.onClick()
71
+ handleClose()
72
+ }
73
+
74
+ const icons = {
75
+ success: <CheckCircle2 className='w-5 h-5' />,
76
+ error: <XCircle className='w-5 h-5' />,
77
+ warning: <AlertCircle className='w-5 h-5' />,
78
+ info: <Info className='w-5 h-5' />,
79
+ loading: <Loader2 className='w-5 h-5 animate-spin' />,
80
+ }
81
+
82
+ const variantClasses = {
83
+ success: 'bg-success-50 border-success-200 text-success-800',
84
+ error: 'bg-error-50 border-error-200 text-error-800',
85
+ warning: 'bg-secondary-50 border-secondary-200 text-secondary-800',
86
+ info: 'bg-info-50 border-info-200 text-info-800',
87
+ loading: 'bg-gray-50 border-gray-200 text-gray-800',
88
+ }
89
+
90
+ const iconColors = {
91
+ success: 'text-success-600',
92
+ error: 'text-error-600',
93
+ warning: 'text-secondary-600',
94
+ info: 'text-info-600',
95
+ loading: 'text-gray-600',
96
+ }
97
+
98
+ const positionClasses = {
99
+ 'top-right': 'top-4 right-4',
100
+ 'top-left': 'top-4 left-4',
101
+ 'bottom-right': 'bottom-4 right-4',
102
+ 'bottom-left': 'bottom-4 left-4',
103
+ 'top-center': 'top-4 left-1/2 -translate-x-1/2',
104
+ 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
105
+ }
106
+
107
+ if (!isVisible) return null
108
+
109
+ return (
110
+ <div
111
+ className={cn(
112
+ 'fixed z-50 transform transition-all duration-300',
113
+ positionClasses[position],
114
+ isLeaving ? 'opacity-0 scale-95' : 'opacity-100 scale-100',
115
+ position.includes('top') && !isLeaving && 'animate-slide-down',
116
+ position.includes('bottom') && !isLeaving && 'animate-slide-up'
117
+ )}
118
+ >
119
+ <div
120
+ className={cn(
121
+ 'flex items-start gap-3 p-4 rounded-lg border shadow-lg max-w-sm',
122
+ 'backdrop-blur-sm bg-white/95',
123
+ variantClasses[type]
124
+ )}
125
+ >
126
+ <div className={cn('flex-shrink-0 mt-0.5', iconColors[type])}>
127
+ {icons[type]}
128
+ </div>
129
+ <div className='flex-1 min-w-0'>
130
+ <h4 className='font-semibold text-sm mb-1'>{title}</h4>
131
+ {description && <p className='text-sm opacity-90'>{description}</p>}
132
+ {action && (
133
+ <button
134
+ onClick={handleAction}
135
+ className='mt-2 text-sm font-medium underline underline-offset-2 hover:no-underline'
136
+ >
137
+ {action.label}
138
+ </button>
139
+ )}
140
+ </div>
141
+ <button
142
+ onClick={handleClose}
143
+ className='flex-shrink-0 p-1 rounded-full hover:bg-black/5 transition-colors'
144
+ >
145
+ <X className='w-4 h-4' />
146
+ </button>
147
+ </div>
148
+ </div>
149
+ )
150
+ }
@@ -0,0 +1,19 @@
1
+ import { Toast, type ToastProps } from './Toast.tsx'
2
+
3
+ export function ToastContainer({
4
+ toasts,
5
+ onRemove,
6
+ position = 'top-right',
7
+ }: {
8
+ toasts: ToastProps[]
9
+ onRemove: (id: string) => void
10
+ position?: ToastProps['position']
11
+ }) {
12
+ return (
13
+ <>
14
+ {toasts.map(t => (
15
+ <Toast key={t.id} {...t} onClose={onRemove} position={position} />
16
+ ))}
17
+ </>
18
+ )
19
+ }
@@ -0,0 +1,58 @@
1
+ import React from 'react'
2
+ import { cn } from '../../lib/utils.ts'
3
+
4
+ type TooltipProps = {
5
+ content: React.ReactNode
6
+ children: React.ReactNode
7
+ position?: 'top' | 'bottom' | 'left' | 'right'
8
+ delay?: number
9
+ className?: string
10
+ }
11
+
12
+ export default function Tooltip({
13
+ content,
14
+ children,
15
+ position = 'top',
16
+ delay = 200,
17
+ className,
18
+ }: TooltipProps) {
19
+ const positionClasses = {
20
+ top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
21
+ bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
22
+ left: 'right-full top-1/2 -translate-y-1/2 mr-2',
23
+ right: 'left-full top-1/2 -translate-y-1/2 ml-2',
24
+ }
25
+
26
+ const arrowClasses = {
27
+ top: 'left-1/2 -translate-x-1/2 top-full',
28
+ bottom: 'left-1/2 -translate-x-1/2 bottom-full',
29
+ left: 'top-1/2 -translate-y-1/2 left-full',
30
+ right: 'top-1/2 -translate-y-1/2 right-full',
31
+ }
32
+
33
+ return (
34
+ <div className='relative inline-block group'>
35
+ {children}
36
+
37
+ <div
38
+ className={cn(
39
+ 'absolute z-50 whitespace-nowrap rounded-md bg-gray-800 text-white text-xs px-2 py-1 shadow-md dark:bg-gray-900',
40
+ 'transition-all opacity-0 scale-95 group-hover:opacity-100 group-hover:scale-100',
41
+ 'group-hover:delay-200 duration-200 ease-out',
42
+ positionClasses[position],
43
+ className
44
+ )}
45
+ style={{ transitionDelay: `${delay}ms` }}
46
+ >
47
+ {content}
48
+
49
+ <span
50
+ className={cn(
51
+ 'absolute w-2 h-2 bg-gray-800 dark:bg-gray-900 rotate-45',
52
+ arrowClasses[position]
53
+ )}
54
+ />
55
+ </div>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,36 @@
1
+ @import './tailwind-config.css';
2
+
3
+ * {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ html,
8
+ body {
9
+ font-family: system-ui, Avenir, sans-serif;
10
+ -webkit-font-smoothing: antialiased;
11
+ -moz-osx-font-smoothing: grayscale;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ button {
16
+ cursor: pointer;
17
+ }
18
+
19
+ a {
20
+ text-decoration: none;
21
+ color: inherit;
22
+ }
23
+
24
+ ul,
25
+ ol {
26
+ list-style: none;
27
+ padding: 0;
28
+ }
29
+
30
+ input,
31
+ textarea,
32
+ button,
33
+ select {
34
+ font: inherit;
35
+ outline: none;
36
+ }