@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/dist/index.d.mts +50 -16
- package/dist/index.d.ts +50 -16
- package/dist/index.global.js +538 -37
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +858 -220
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +857 -219
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/input.tsx +92 -19
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/textarea.tsx +194 -20
package/package.json
CHANGED
|
@@ -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-
|
|
43
|
-
filled: "border border-transparent bg-gray-100 dark:bg-gray-800
|
|
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
|
-
|
|
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,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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
36
|
-
className=
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
)
|