@moontra/moonui 2.3.9 → 2.4.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.
@@ -1,45 +1,219 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
-
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "../../lib/utils"
6
+ import { Loader2 } from "lucide-react"
7
+
8
+ /**
9
+ * Textarea Variant Styles
10
+ */
11
+ const textareaVariants = cva(
12
+ [
13
+ "flex w-full rounded-md px-3 py-2 text-sm transition-all duration-200",
14
+ "text-foreground placeholder:text-muted-foreground dark:placeholder:text-gray-500",
15
+ "disabled:cursor-not-allowed disabled:opacity-50",
16
+ "focus-visible:outline-none dark:text-gray-200",
17
+ "resize-none" // Always disable manual resize, we control it
18
+ ],
19
+ {
20
+ variants: {
21
+ variant: {
22
+ default: "border border-gray-300 dark:border-gray-700 bg-background dark:bg-gray-800/80 hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 focus-visible:border-primary dark:focus-visible:border-primary/80 dark:shadow-inner dark:shadow-gray-950/10",
23
+ outline: "border-2 border-gray-300 dark:border-gray-700 bg-transparent hover:border-gray-400 dark:hover:border-gray-600 focus-visible:border-primary dark:focus-visible:border-primary/80",
24
+ ghost: "border-none bg-transparent hover:bg-gray-100/50 dark:hover:bg-gray-800/30 focus-visible:bg-transparent",
25
+ underline: "border-t-0 border-l-0 border-r-0 border-b-2 border-gray-300 dark:border-gray-600 rounded-none px-0 hover:border-gray-400 dark:hover:border-gray-500 focus-visible:ring-0 focus-visible:border-primary dark:focus-visible:border-primary/80 bg-transparent dark:bg-transparent"
26
+ },
27
+ size: {
28
+ sm: "min-h-[60px] text-xs",
29
+ md: "min-h-[80px] text-sm",
30
+ lg: "min-h-[120px] text-base"
31
+ },
32
+ isError: {
33
+ true: "border-error focus-visible:ring-error/30 focus-visible:border-error hover:border-error/80 dark:hover:border-error/80",
34
+ false: ""
35
+ },
36
+ isSuccess: {
37
+ true: "border-success focus-visible:ring-success/30 focus-visible:border-success hover:border-success/80 dark:hover:border-success/80",
38
+ false: ""
39
+ }
40
+ },
41
+ defaultVariants: {
42
+ variant: "default",
43
+ size: "md",
44
+ isError: false,
45
+ isSuccess: false
46
+ }
47
+ }
48
+ )
6
49
 
7
50
  export interface TextareaProps
8
- extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
9
- // Enhanced props that shouldn't be passed to DOM
10
- variant?: "default" | "outline" | "ghost" | "underline"
11
- size?: "sm" | "md" | "lg"
51
+ extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">,
52
+ Omit<VariantProps<typeof textareaVariants>, "isError" | "isSuccess"> {
53
+ /** Hata mesajı */
12
54
  error?: boolean | string
13
- success?: boolean
55
+ /** Başarı mesajı */
56
+ success?: boolean | string
57
+ /** Yükleniyor durumu */
14
58
  loading?: boolean
59
+ /** Otomatik yükseklik ayarlama */
15
60
  autoResize?: boolean
61
+ /** Maksimum yükseklik (px) */
16
62
  maxHeight?: number
63
+ /** Karakter sayacı göster */
17
64
  characterCount?: boolean
65
+ /** Wrapper için ek sınıflar */
66
+ wrapperClassName?: string
67
+ /** Mesaj için ek sınıflar */
68
+ messageClassName?: string
18
69
  }
19
70
 
71
+ /**
72
+ * Advanced Textarea Component
73
+ *
74
+ * Features:
75
+ * - Auto-resize based on content
76
+ * - Character count display
77
+ * - Max height constraint
78
+ * - Multiple variants
79
+ * - Error/Success states
80
+ * - Loading state
81
+ */
20
82
  const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
21
- ({
22
- className,
23
- // Extract enhanced props to prevent them from being passed to DOM
83
+ ({
84
+ className,
85
+ wrapperClassName,
86
+ messageClassName,
24
87
  variant,
25
88
  size,
26
89
  error,
27
90
  success,
28
91
  loading,
29
- autoResize,
92
+ autoResize = false,
30
93
  maxHeight,
31
- characterCount,
32
- ...props
94
+ characterCount = false,
95
+ disabled,
96
+ maxLength,
97
+ value,
98
+ defaultValue,
99
+ onChange,
100
+ ...props
33
101
  }, ref) => {
102
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null)
103
+ const [internalValue, setInternalValue] = React.useState(value || defaultValue || "")
104
+
105
+ // Merge refs
106
+ React.useImperativeHandle(ref, () => textareaRef.current!)
107
+
108
+ // Auto-resize logic
109
+ const adjustHeight = React.useCallback(() => {
110
+ const textarea = textareaRef.current
111
+ if (!textarea || !autoResize) return
112
+
113
+ // Reset height to get correct scrollHeight
114
+ textarea.style.height = 'auto'
115
+
116
+ // Set new height
117
+ const newHeight = textarea.scrollHeight
118
+ if (maxHeight && newHeight > maxHeight) {
119
+ textarea.style.height = `${maxHeight}px`
120
+ textarea.style.overflowY = 'auto'
121
+ } else {
122
+ textarea.style.height = `${newHeight}px`
123
+ textarea.style.overflowY = 'hidden'
124
+ }
125
+ }, [autoResize, maxHeight])
126
+
127
+ // Adjust height on value change
128
+ React.useEffect(() => {
129
+ adjustHeight()
130
+ }, [internalValue, adjustHeight])
131
+
132
+ // Handle value changes
133
+ React.useEffect(() => {
134
+ if (value !== undefined) {
135
+ setInternalValue(value)
136
+ }
137
+ }, [value])
138
+
139
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
140
+ setInternalValue(e.target.value)
141
+ onChange?.(e)
142
+ }
143
+
144
+ // Character count
145
+ const currentLength = String(internalValue).length
146
+ const showCharCount = characterCount && (maxLength !== undefined || currentLength > 0)
147
+
148
+ // Messages
149
+ const errorMessage = typeof error === "string" ? error : undefined
150
+ const successMessage = typeof success === "string" ? success : undefined
151
+ const showMessage = errorMessage || successMessage
152
+
34
153
  return (
35
- <textarea
36
- className={cn("moonui-theme",
37
- "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
38
- className
39
- )}
40
- ref={ref}
41
- {...props}
42
- />
154
+ <div className={cn("moonui-theme", "space-y-1.5 w-full", wrapperClassName)}>
155
+ <div className="relative">
156
+ <textarea
157
+ ref={textareaRef}
158
+ className={cn(
159
+ textareaVariants({
160
+ variant,
161
+ size,
162
+ isError: !!error,
163
+ isSuccess: !!success
164
+ }),
165
+ loading && "pr-10",
166
+ className
167
+ )}
168
+ disabled={disabled || loading}
169
+ value={value}
170
+ defaultValue={defaultValue}
171
+ onChange={handleChange}
172
+ maxLength={maxLength}
173
+ aria-invalid={!!error || undefined}
174
+ aria-describedby={
175
+ errorMessage ? `${props.id || ''}-error` :
176
+ successMessage ? `${props.id || ''}-success` :
177
+ undefined
178
+ }
179
+ {...props}
180
+ />
181
+
182
+ {loading && (
183
+ <div className="absolute top-3 right-3 text-gray-500">
184
+ <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
185
+ </div>
186
+ )}
187
+ </div>
188
+
189
+ <div className="flex items-center justify-between">
190
+ {/* Error/Success Message */}
191
+ {showMessage && (
192
+ <p
193
+ className={cn(
194
+ "text-xs transition-all",
195
+ errorMessage && "text-error",
196
+ successMessage && "text-success",
197
+ messageClassName
198
+ )}
199
+ id={
200
+ errorMessage ? `${props.id || ''}-error` :
201
+ successMessage ? `${props.id || ''}-success` :
202
+ undefined
203
+ }
204
+ >
205
+ {errorMessage || successMessage}
206
+ </p>
207
+ )}
208
+
209
+ {/* Character Count */}
210
+ {showCharCount && (
211
+ <p className="text-xs text-muted-foreground dark:text-gray-500 ml-auto">
212
+ {currentLength}{maxLength !== undefined && ` / ${maxLength}`}
213
+ </p>
214
+ )}
215
+ </div>
216
+ </div>
43
217
  )
44
218
  }
45
219
  )