@moontra/moonui 2.3.10 → 2.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "2.3.10",
3
+ "version": "2.4.1",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -17,6 +17,10 @@ export {
17
17
  AlertDescription as MoonUIAlertDescription,
18
18
  } from "./alert";
19
19
 
20
+ export type {
21
+ AlertProps as MoonUIAlertProps,
22
+ } from "./alert";
23
+
20
24
  // AspectRatio
21
25
  export { AspectRatio as MoonUIAspectRatio } from "./aspect-ratio";
22
26
 
@@ -28,12 +32,20 @@ export {
28
32
  AvatarGroup as MoonUIAvatarGroup,
29
33
  } from "./avatar";
30
34
 
35
+ export type {
36
+ AvatarProps as MoonUIAvatarProps,
37
+ } from "./avatar";
38
+
31
39
  // Badge
32
40
  export {
33
41
  Badge as MoonUIBadge,
34
42
  badgeVariants as moonUIBadgeVariants,
35
43
  } from "./badge";
36
44
 
45
+ export type {
46
+ BadgeProps as MoonUIBadgeProps,
47
+ } from "./badge";
48
+
37
49
  // Breadcrumb
38
50
  export {
39
51
  Breadcrumb as MoonUIBreadcrumb,
@@ -51,6 +63,10 @@ export {
51
63
  buttonVariants as moonUIButtonVariants,
52
64
  } from "./button";
53
65
 
66
+ export type {
67
+ ButtonProps as MoonUIButtonProps,
68
+ } from "./button";
69
+
54
70
  // Card
55
71
  export {
56
72
  Card as MoonUICard,
@@ -61,6 +77,19 @@ export {
61
77
  CardContent as MoonUICardContent,
62
78
  } from "./card";
63
79
 
80
+ export type {
81
+ CardProps as MoonUICardProps,
82
+ } from "./card";
83
+
84
+ // Calendar
85
+ export {
86
+ Calendar as MoonUICalendar,
87
+ } from "./calendar";
88
+
89
+ export type {
90
+ CalendarProps as MoonUICalendarProps,
91
+ } from "./calendar";
92
+
64
93
  // Card Input
65
94
  export {
66
95
  CardNumberInput as MoonUICardNumberInput,
@@ -77,6 +106,10 @@ export {
77
106
  CheckboxLabel as MoonUICheckboxLabel,
78
107
  } from "./checkbox";
79
108
 
109
+ export type {
110
+ CheckboxProps as MoonUICheckboxProps,
111
+ } from "./checkbox";
112
+
80
113
  // Collapsible
81
114
  export {
82
115
  Collapsible as MoonUICollapsible,
@@ -91,6 +124,10 @@ export {
91
124
  GradientPicker as MoonUIGradientPicker,
92
125
  } from "./color-picker";
93
126
 
127
+ export type {
128
+ ColorPickerProps as MoonUIColorPickerProps,
129
+ } from "./color-picker";
130
+
94
131
  // Command
95
132
  export {
96
133
  Command as MoonUICommand,
@@ -117,6 +154,10 @@ export {
117
154
  MonthPicker as MoonUIMonthPicker,
118
155
  } from "./date-picker";
119
156
 
157
+ export type {
158
+ DatePickerProps as MoonUIDatePickerProps,
159
+ } from "./date-picker";
160
+
120
161
  // Dialog
121
162
  export {
122
163
  Dialog as MoonUIDialog,
@@ -132,6 +173,10 @@ export {
132
173
  // DraggableList
133
174
  export { DraggableList as MoonUIDraggableList } from "./draggable-list";
134
175
 
176
+ export type {
177
+ DraggableListProps as MoonUIDraggableListProps,
178
+ } from "./draggable-list";
179
+
135
180
  // DropdownMenu
136
181
  export {
137
182
  DropdownMenu as MoonUIDropdownMenu,
@@ -154,15 +199,27 @@ export {
154
199
  // FileUpload
155
200
  export { FileUpload as MoonUIFileUpload } from "./file-upload";
156
201
 
202
+ export type {
203
+ FileUploadProps as MoonUIFileUploadProps,
204
+ } from "./file-upload";
205
+
157
206
  // GestureDrawer
158
207
  export { GestureDrawer as MoonUIGestureDrawer } from "./gesture-drawer";
159
208
 
209
+ export type {
210
+ GestureDrawerProps as MoonUIGestureDrawerProps,
211
+ } from "./gesture-drawer";
212
+
160
213
  // GitHubStars
161
214
  export { GitHubStars as MoonUIGitHubStars } from "./github-stars";
162
215
 
163
216
  // Input
164
217
  export { Input as MoonUIInput } from "./input";
165
218
 
219
+ export type {
220
+ InputProps as MoonUIInputProps,
221
+ } from "./input";
222
+
166
223
  // Label
167
224
  export { Label as MoonUILabel } from "./label";
168
225
 
@@ -201,6 +258,10 @@ export {
201
258
  // Progress
202
259
  export { Progress as MoonUIProgress } from "./progress";
203
260
 
261
+ export type {
262
+ ProgressProps as MoonUIProgressProps,
263
+ } from "./progress";
264
+
204
265
  // RadioGroup
205
266
  export {
206
267
  RadioGroup as MoonUIRadioGroup,
@@ -209,6 +270,10 @@ export {
209
270
  RadioItemWithLabel as MoonUIRadioItemWithLabel,
210
271
  } from "./radio-group";
211
272
 
273
+ export type {
274
+ RadioGroupProps as MoonUIRadioGroupProps,
275
+ } from "./radio-group";
276
+
212
277
  // RichTextEditor
213
278
  export { RichTextEditor as MoonUIRichTextEditor } from "./rich-text-editor";
214
279
 
@@ -254,6 +319,10 @@ export {
254
319
  skeletonVariants as moonUISkeletonVariants,
255
320
  } from "./skeleton";
256
321
 
322
+ export type {
323
+ SkeletonProps as MoonUISkeletonProps,
324
+ } from "./skeleton";
325
+
257
326
  // Slider
258
327
  export { Slider as MoonUISlider } from "./slider";
259
328
 
@@ -263,6 +332,10 @@ export { SwipeableCard as MoonUISwipeableCard } from "./swipeable-card";
263
332
  // Switch
264
333
  export { Switch as MoonUISwitch } from "./switch";
265
334
 
335
+ export type {
336
+ SwitchProps as MoonUISwitchProps,
337
+ } from "./switch";
338
+
266
339
  // Table
267
340
  export {
268
341
  Table as MoonUITable,
@@ -289,6 +362,10 @@ export { TagsInput as MoonUITagsInput } from "./tags-input";
289
362
  // Textarea
290
363
  export { Textarea as MoonUITextarea } from "./textarea";
291
364
 
365
+ export type {
366
+ TextareaProps as MoonUITextareaProps,
367
+ } from "./textarea";
368
+
292
369
  // Toast
293
370
  export {
294
371
  Toast as MoonUIToast,
@@ -39,10 +39,10 @@ const inputVariants = cva(
39
39
  {
40
40
  variants: {
41
41
  variant: {
42
- standard: "border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 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:bg-gray-900/60 dark:shadow-inner dark:shadow-gray-950/10",
43
- filled: "border border-transparent bg-gray-100 dark:bg-gray-800/90 rounded-md px-3 py-2 hover:bg-gray-200 dark:hover:bg-gray-700/90 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 dark:shadow-inner dark:shadow-gray-950/10",
42
+ standard: "border border-gray-300 dark:border-gray-700 rounded-md px-3 py-2 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:bg-gray-800/80 dark:shadow-inner dark:shadow-gray-950/10",
43
+ filled: "border border-transparent bg-gray-100 dark:bg-gray-800 rounded-md px-3 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 dark:shadow-inner dark:shadow-gray-950/10",
44
44
  ghost: "border-none bg-transparent shadow-none px-1 dark:text-gray-300 dark:placeholder:text-gray-500 hover:bg-gray-100/50 dark:hover:bg-gray-800/30 focus-visible:bg-transparent",
45
- underline: "border-t-0 border-l-0 border-r-0 border-b border-gray-300 dark:border-gray-600 rounded-none px-0 py-2 hover:border-gray-400 dark:hover:border-gray-500 focus-visible:ring-0 focus-visible:border-b-2 focus-visible:border-primary dark:focus-visible:border-primary/80 dark:text-gray-300",
45
+ underline: "border-t-0 border-l-0 border-r-0 border-b border-gray-300 dark:border-gray-600 rounded-none px-0 py-2 hover:border-gray-400 dark:hover:border-gray-500 focus-visible:ring-0 focus-visible:border-b-2 focus-visible:border-primary dark:focus-visible:border-primary/80 dark:text-gray-300 dark:bg-transparent",
46
46
  },
47
47
  size: {
48
48
  sm: "h-8 text-xs",
@@ -90,6 +90,12 @@ const inputVariants = cva(
90
90
  export interface InputProps
91
91
  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
92
92
  Omit<VariantProps<typeof inputVariants>, "isDisabled" | "hasLeftIcon" | "hasRightIcon" | "hasRightButton"> {
93
+ /** Label metni */
94
+ label?: string;
95
+ /** Floating label animasyonu (Material Design pattern) */
96
+ floatingLabel?: boolean;
97
+ /** Floating label için ek sınıflar */
98
+ floatingLabelClassName?: string;
93
99
  /** Hata mesajı */
94
100
  error?: string;
95
101
  /** Başarı mesajı */
@@ -125,14 +131,14 @@ export interface InputProps
125
131
  * @param props.alwaysShowMessage - Mesajın her zaman görünür olması
126
132
  */
127
133
  const Input = React.forwardRef<HTMLInputElement, InputProps>(
128
- ({
129
- className,
134
+ ({
135
+ className,
130
136
  wrapperClassName,
131
137
  messageClassName,
132
- variant,
133
- size,
138
+ variant,
139
+ size,
134
140
  isError,
135
- disabled,
141
+ disabled,
136
142
  error,
137
143
  success,
138
144
  loading,
@@ -140,8 +146,48 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
140
146
  rightIcon,
141
147
  rightButton,
142
148
  alwaysShowMessage = false,
143
- ...props
149
+ label,
150
+ floatingLabel = false,
151
+ floatingLabelClassName,
152
+ value,
153
+ defaultValue,
154
+ onChange,
155
+ onFocus,
156
+ onBlur,
157
+ placeholder,
158
+ ...props
144
159
  }, ref) => {
160
+ // Floating label state management
161
+ const [isFocused, setIsFocused] = React.useState(false);
162
+ const [internalValue, setInternalValue] = React.useState(value || defaultValue || "");
163
+
164
+ // Floating label aktif mi kontrolü
165
+ const hasValue = internalValue !== "" && internalValue !== null && internalValue !== undefined;
166
+ const isLabelActive = isFocused || hasValue;
167
+
168
+ // Event handler'lar
169
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
170
+ setIsFocused(true);
171
+ onFocus?.(e);
172
+ };
173
+
174
+ const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
175
+ setIsFocused(false);
176
+ onBlur?.(e);
177
+ };
178
+
179
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
180
+ setInternalValue(e.target.value);
181
+ onChange?.(e);
182
+ };
183
+
184
+ // Value değişikliklerini takip et
185
+ React.useEffect(() => {
186
+ if (value !== undefined) {
187
+ setInternalValue(value);
188
+ }
189
+ }, [value]);
190
+
145
191
  // Mesajı göster/gizle
146
192
  const showMessage = alwaysShowMessage || error || success;
147
193
  const messageType = error ? "error" : success ? "success" : "normal";
@@ -149,18 +195,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
149
195
  return (
150
196
  <div className="space-y-1.5 w-full">
151
197
  <div className={cn("moonui-theme", inputWrapperVariants({ size }), wrapperClassName)}>
198
+ {/* Floating Label */}
199
+ {floatingLabel && label && (
200
+ <label
201
+ htmlFor={props.id}
202
+ className={cn(
203
+ "absolute transition-all duration-200 pointer-events-none",
204
+ leftIcon || loading ? "left-10" : "left-3",
205
+ "text-gray-500 dark:text-gray-400",
206
+ isLabelActive
207
+ ? "top-0 -translate-y-1/2 text-xs bg-background dark:bg-gray-800 px-1"
208
+ : "top-1/2 -translate-y-1/2 text-sm",
209
+ isFocused && "text-primary dark:text-primary",
210
+ !!error && "text-error",
211
+ disabled && "opacity-50",
212
+ floatingLabelClassName
213
+ )}
214
+ >
215
+ {label}
216
+ </label>
217
+ )}
218
+
152
219
  {leftIcon && (
153
220
  <div className="absolute left-3 text-gray-500 flex items-center justify-center pointer-events-none">
154
221
  {loading ? <Loader2 className="h-4 w-4 animate-spin" /> : leftIcon}
155
222
  </div>
156
223
  )}
157
-
224
+
158
225
  <input
159
226
  className={cn(
160
- inputVariants({
161
- variant,
162
- size,
163
- isError: !!error || isError,
227
+ inputVariants({
228
+ variant,
229
+ size,
230
+ isError: !!error || isError,
164
231
  isSuccess: !!success,
165
232
  isDisabled: disabled || loading,
166
233
  hasLeftIcon: !!leftIcon || loading,
@@ -176,30 +243,36 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
176
243
  data-success={!!success ? "" : undefined}
177
244
  aria-invalid={!!error || !!isError || undefined}
178
245
  aria-describedby={error ? `${props.id || ''}-error` : success ? `${props.id || ''}-success` : undefined}
246
+ value={value}
247
+ defaultValue={defaultValue}
248
+ onChange={handleChange}
249
+ onFocus={handleFocus}
250
+ onBlur={handleBlur}
251
+ placeholder={floatingLabel ? (isLabelActive ? placeholder : "") : placeholder}
179
252
  {...props}
180
253
  />
181
-
254
+
182
255
  {rightIcon && !loading && (
183
256
  <div className="absolute right-3 text-gray-500 flex items-center justify-center pointer-events-none">
184
257
  {rightIcon}
185
258
  </div>
186
259
  )}
187
-
260
+
188
261
  {rightButton && (
189
262
  <div className="absolute right-3">
190
263
  {rightButton}
191
264
  </div>
192
265
  )}
193
-
266
+
194
267
  {loading && !leftIcon && (
195
268
  <div className="absolute left-3 text-gray-500 flex items-center justify-center pointer-events-none">
196
269
  <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
197
270
  </div>
198
271
  )}
199
272
  </div>
200
-
273
+
201
274
  {showMessage && (
202
- <p
275
+ <p
203
276
  className={cn(
204
277
  "text-xs transition-all",
205
278
  messageType === "error" && "text-error",
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { cn } from "../../lib/utils"
4
+ import { cn } from "@/lib/utils"
5
5
 
6
6
  interface MoonLogoProps extends React.SVGProps<SVGSVGElement> {
7
7
  variant?: "default" | "monochrome" | "gradient"
@@ -16,9 +16,10 @@ export function MoonLogo({
16
16
  }: MoonLogoProps) {
17
17
  const logoId = React.useId()
18
18
  const gradientId = `moon-gradient-${logoId}`
19
+ const maskId = `moon-mask-${logoId}`
19
20
 
20
21
  return (
21
- <div className={cn("moonui-theme", "flex items-center gap-2", className)}>
22
+ <div className={cn("flex items-center gap-2", className)}>
22
23
  {/* Moon Icon */}
23
24
  <svg
24
25
  width="32"
@@ -29,32 +30,28 @@ export function MoonLogo({
29
30
  className="flex-shrink-0"
30
31
  {...props}
31
32
  >
32
- {variant === "gradient" && (
33
- <defs>
33
+ <defs>
34
+ {variant === "gradient" && (
34
35
  <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="100%">
35
36
  <stop offset="0%" stopColor="#3B82F6" />
36
37
  <stop offset="50%" stopColor="#8B5CF6" />
37
38
  <stop offset="100%" stopColor="#EC4899" />
38
39
  </linearGradient>
39
- </defs>
40
- )}
40
+ )}
41
+ <mask id={maskId}>
42
+ <rect width="32" height="32" fill="white"/>
43
+ <circle cx="22" cy="10" r="10" fill="black"/>
44
+ </mask>
45
+ </defs>
41
46
 
42
- {/* Moon Circle */}
47
+ {/* Moon Circle with transparent crescent */}
43
48
  <circle
44
49
  cx="16"
45
50
  cy="16"
46
51
  r="14"
47
52
  fill={variant === "gradient" ? `url(#${gradientId})` : "currentColor"}
48
53
  className={variant === "default" ? "fill-primary" : ""}
49
- />
50
-
51
- {/* Crescent cutout */}
52
- <circle
53
- cx="22"
54
- cy="10"
55
- r="10"
56
- fill="white"
57
- className="dark:fill-background"
54
+ mask={`url(#${maskId})`}
58
55
  />
59
56
  </svg>
60
57
 
@@ -37,7 +37,7 @@ PaginationItem.displayName = "PaginationItem"
37
37
 
38
38
  type PaginationLinkProps = {
39
39
  isActive?: boolean
40
- } & Pick<ButtonProps, "size"> &
40
+ } & Partial<Pick<ButtonProps, "size">> &
41
41
  React.ComponentProps<"a">
42
42
 
43
43
  const PaginationLink = ({
@@ -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
  )