@moontra/moonui 2.3.10 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "2.3.10",
3
+ "version": "2.4.0",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -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",
@@ -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
  )