pejay-ui 1.0.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.
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { Input } from "./input";
3
+ import { Mail, CheckCircle2, AlertCircle } from "lucide-react";
4
+
5
+ /*
6
+ * ============================================================================
7
+ * Types & Interfaces
8
+ * ============================================================================
9
+ */
10
+
11
+ interface EmailInputProps extends React.ComponentProps<typeof Input> {
12
+ /* Controls visibility of validation indicator icons (Check/Alert) */
13
+ showValidationIcon?: boolean;
14
+ /* Represents the validity status calculated by the parent form state */
15
+ isValid?: boolean;
16
+ }
17
+
18
+ /*
19
+ * ============================================================================
20
+ * EmailInput Component
21
+ * ============================================================================
22
+ */
23
+
24
+ export const EmailInput = ({
25
+ showValidationIcon = true,
26
+ isValid = false,
27
+ onChange,
28
+ className,
29
+ value,
30
+ ...props
31
+ }: EmailInputProps) => {
32
+ return (
33
+ <Input
34
+ autoComplete="email"
35
+ type="email"
36
+ leftIcon={<Mail size={18} />}
37
+ {...props}
38
+ value={value}
39
+ onChange={onChange}
40
+ rightIcon={
41
+ /* Render check circle on success, alert circle on format errors */
42
+ showValidationIcon && value ? (
43
+ isValid ? (
44
+ <CheckCircle2 size={18} className="text-green-500" />
45
+ ) : (
46
+ <AlertCircle size={18} className="text-amber-500" />
47
+ )
48
+ ) : (
49
+ props.rightIcon
50
+ )
51
+ }
52
+ className={className}
53
+ />
54
+ );
55
+ };
@@ -0,0 +1,380 @@
1
+ import React, { useState, useRef } from "react";
2
+ import { Upload, X, FileText, Image as ImageIcon } from "lucide-react";
3
+ import { cn } from "@/utils/cn";
4
+ import { Input } from "./input";
5
+
6
+ /*
7
+ * ============================================================================
8
+ * Types & Interfaces
9
+ * ============================================================================
10
+ */
11
+
12
+ /* Prop configuration for the FileInput component */
13
+ interface FileInputProps extends Omit<
14
+ React.InputHTMLAttributes<HTMLInputElement>,
15
+ "type"
16
+ > {
17
+ /* Title label of the file input */
18
+ label?: string;
19
+ /* Validation error message */
20
+ error?: string;
21
+ /* Visual template variant */
22
+ variant?: "field" | "dropzone" | "field-2";
23
+ /* Sizing shape template for the dropzone variants */
24
+ dropzoneVariant?: "rectangle" | "square" | "narrow";
25
+ /* Maximum allowed file size limit (in MB) */
26
+ maxFileSize?: number;
27
+ }
28
+
29
+ /*
30
+ * ============================================================================
31
+ * FileInput Component
32
+ * ============================================================================
33
+ */
34
+
35
+ export const FileInput = ({
36
+ label,
37
+ error,
38
+ variant = "dropzone",
39
+ dropzoneVariant = "rectangle",
40
+ maxFileSize,
41
+ className,
42
+ onChange,
43
+ accept,
44
+ ...props
45
+ }: FileInputProps) => {
46
+ /* Tracks the currently selected File object */
47
+ const [file, setFile] = useState<File | null>(null);
48
+ /* Manages dragging state overlay during drop events */
49
+ const [isDragging, setIsDragging] = useState(false);
50
+ /* Stores any local file format or size validation errors */
51
+ const [internalError, setInternalError] = useState<string | undefined>(
52
+ undefined,
53
+ );
54
+ /* References the hidden native file input element */
55
+ const fileInputRef = useRef<HTMLInputElement>(null);
56
+
57
+ /* Processes selected files and enforces file size constraints */
58
+ const handleFiles = (files: FileList | null) => {
59
+ const selectedFile = files?.[0];
60
+ setInternalError(undefined);
61
+
62
+ if (selectedFile) {
63
+ if (maxFileSize && selectedFile.size > maxFileSize * 1024 * 1024) {
64
+ setInternalError(`File size exceeds ${maxFileSize}MB limit`);
65
+ return;
66
+ }
67
+ setFile(selectedFile);
68
+ }
69
+ };
70
+
71
+ /* Invokes file processor on native file change event */
72
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
73
+ handleFiles(e.target.files);
74
+ onChange?.(e);
75
+ };
76
+
77
+ /* Drag over callback enabling drop interactions */
78
+ const handleDragOver = (e: React.DragEvent) => {
79
+ e.preventDefault();
80
+ setIsDragging(true);
81
+ };
82
+
83
+ /* Drag leave callback removing dragging state background highlights */
84
+ const handleDragLeave = () => {
85
+ setIsDragging(false);
86
+ };
87
+
88
+ /* Drop handler capturing dropped file instances */
89
+ const handleDrop = (e: React.DragEvent) => {
90
+ e.preventDefault();
91
+ setIsDragging(false);
92
+ handleFiles(e.dataTransfer.files);
93
+ };
94
+
95
+ /* Paste handler capturing file streams from system clipboards */
96
+ const handlePaste = (e: React.ClipboardEvent) => {
97
+ const items = e.clipboardData.items;
98
+ const files: File[] = [];
99
+
100
+ for (let i = 0; i < items.length; i++) {
101
+ if (items[i].kind === "file") {
102
+ const file = items[i].getAsFile();
103
+ if (file) files.push(file);
104
+ }
105
+ }
106
+
107
+ if (files.length > 0) {
108
+ handleFiles(files as unknown as FileList);
109
+ }
110
+ };
111
+
112
+ /* Resets current selection and clears native input buffers */
113
+ const handleClear = (e: React.MouseEvent) => {
114
+ e.stopPropagation();
115
+ setFile(null);
116
+ setInternalError(undefined);
117
+ if (fileInputRef.current) {
118
+ fileInputRef.current.value = "";
119
+ }
120
+ };
121
+
122
+ const displayError = error || internalError;
123
+
124
+ /* Simulates mouse click event on native hidden input tag */
125
+ const triggerInput = () => {
126
+ fileInputRef.current?.click();
127
+ };
128
+
129
+ const inputId = React.useId();
130
+
131
+ /* Template option mapping for traditional Input fields and double action bars */
132
+ if (variant === "field" || variant === "field-2") {
133
+ return (
134
+ <div
135
+ className="flex flex-col w-full gap-1.5 outline-none"
136
+ onPaste={handlePaste}
137
+ tabIndex={0}
138
+ onKeyDown={e => {
139
+ if ((e.key === "Delete" || e.key === "Backspace") && file) {
140
+ e.preventDefault();
141
+ handleClear(e as unknown as React.MouseEvent);
142
+ } else if (e.key === "Enter" || e.key === " ") {
143
+ e.preventDefault();
144
+ triggerInput();
145
+ }
146
+ }}
147
+ >
148
+ <input
149
+ {...props}
150
+ ref={fileInputRef}
151
+ type="file"
152
+ accept={accept}
153
+ className="hidden"
154
+ onChange={handleFileChange}
155
+ />
156
+
157
+ {/* Template variant 1: Custom read-only base input mimicking file paths */}
158
+ {variant === "field" ? (
159
+ <Input
160
+ label={label}
161
+ error={displayError}
162
+ readOnly
163
+ placeholder={props.placeholder || "Click to upload file..."}
164
+ value={file ? file.name : ""}
165
+ onClick={triggerInput}
166
+ leftIcon={<Upload size={18} />}
167
+ rightIcon={
168
+ file && (
169
+ <button
170
+ onClick={handleClear}
171
+ className="bg-black text-white hover:bg-red-500 rounded-full p-1 hover:text-white transition-colors cursor-pointer"
172
+ >
173
+ <X size={14} strokeWidth={2.5} />
174
+ </button>
175
+ )
176
+ }
177
+ />
178
+ ) : (
179
+ /* Template variant 2: split-action bar with inline selection button */
180
+ <div className="flex flex-col gap-1.5">
181
+ {label && (
182
+ <label className="text-sm font-medium text-black ml-1">
183
+ {label}
184
+ {props.required && (
185
+ <span className="text-red-500 ml-1">*</span>
186
+ )}
187
+ </label>
188
+ )}
189
+ <div
190
+ className={cn(
191
+ "flex items-center w-full h-10 rounded-xl border-[1.5px] overflow-hidden transition-all duration-200 focus-within:ring-2 focus-within:ring-sky-500/20 bg-black focus-within:border-sky-500",
192
+ displayError ? "border-red-500" : "border-gray-800",
193
+ className,
194
+ )}
195
+ >
196
+ <div
197
+
198
+ className="flex-1 h-full flex items-center px-3 truncate text-sm text-white"
199
+ >
200
+ {file ? (
201
+ <span className="truncate">{file.name}</span>
202
+ ) : (
203
+ <span className="text-white">
204
+ {props.placeholder || "No file selected"}
205
+ </span>
206
+ )}
207
+ </div>
208
+
209
+ <div className="flex items-center h-full">
210
+ {file && (
211
+ <button
212
+ onClick={handleClear}
213
+ className="p-1 bg-white rounded-full hover:text-red-500 transition-colors mr-1.5 cursor-pointer"
214
+ >
215
+ <X size={14} strokeWidth={2.5} />
216
+ </button>
217
+ )}
218
+
219
+ <button
220
+ type="button"
221
+ onClick={triggerInput}
222
+ className="h-full px-4 flex items-center text-xs font-medium border-l-[1.5px] border-gray-800 bg-white text-black cursor-pointer"
223
+ >
224
+ {file ? "Replace" : "Select a File"}
225
+ </button>
226
+ </div>
227
+ </div>
228
+ {displayError && (
229
+ <span className="text-xs text-red-500 ml-1 font-medium">
230
+ {displayError}
231
+ </span>
232
+ )}
233
+ </div>
234
+ )}
235
+ </div>
236
+ );
237
+ }
238
+
239
+ /* Default template dropzone layout variant supporting drag, drop, and paste actions */
240
+ return (
241
+ <div className="flex flex-col w-full gap-1.5">
242
+ {label && (
243
+ <label className="text-sm font-medium text-black ml-1">
244
+ {label}
245
+ {props.required && <span className="text-red-500 ml-1">*</span>}
246
+ </label>
247
+ )}
248
+
249
+ <div
250
+ onClick={triggerInput}
251
+ onDragOver={handleDragOver}
252
+ onDragLeave={handleDragLeave}
253
+ onDrop={handleDrop}
254
+ onPaste={handlePaste}
255
+ tabIndex={0}
256
+ onKeyDown={e => {
257
+ if (e.key === "Enter" || e.key === " ") {
258
+ e.preventDefault();
259
+ triggerInput();
260
+ } else if ((e.key === "Delete" || e.key === "Backspace") && file) {
261
+ e.preventDefault();
262
+ handleClear(e as unknown as React.MouseEvent);
263
+ }
264
+ }}
265
+ className={cn(
266
+ "group relative flex flex-col items-center justify-center w-full p-4 rounded-2xl border transition-all cursor-pointer outline-none focus-within:ring-2 focus-within:ring-sky-500/20 focus-within:border-sky-500 bg-black",
267
+ file && "border-sky-500 bg-sky-500/10",
268
+ isDragging && "border-sky-500 bg-sky-500/10",
269
+ displayError && "border-red-500 bg-red-500/10",
270
+ /* Sizing templates */
271
+ dropzoneVariant === "rectangle" && "min-h-[120px]",
272
+ dropzoneVariant === "square" && "aspect-square",
273
+ dropzoneVariant === "narrow" && "min-h-[48px] p-2",
274
+ className,
275
+ )}
276
+ >
277
+ <input
278
+ {...props}
279
+ ref={fileInputRef}
280
+ type="file"
281
+ id={inputId}
282
+ accept={accept}
283
+ className="hidden"
284
+ onChange={handleFileChange}
285
+ />
286
+
287
+ {/* Selected file info card display view */}
288
+ {file ? (
289
+ <div
290
+ className={cn(
291
+ "flex items-center w-full gap-4",
292
+ dropzoneVariant === "square" &&
293
+ "flex-col justify-center text-center gap-2",
294
+ dropzoneVariant === "narrow" && "gap-2",
295
+ )}
296
+ >
297
+ <div
298
+ className={cn(
299
+ "flex items-center justify-center rounded-xl shrink-0 text-sky-500 bg-black",
300
+ dropzoneVariant === "narrow" ? "w-8 h-8" : "w-12 h-12",
301
+ )}
302
+ >
303
+ {file.type.startsWith("image/") ? (
304
+ <ImageIcon size={dropzoneVariant === "narrow" ? 16 : 24} />
305
+ ) : (
306
+ <FileText size={dropzoneVariant === "narrow" ? 16 : 24} />
307
+ )}
308
+ </div>
309
+
310
+ <div
311
+ className={cn(
312
+ "flex flex-col flex-1 min-w-0",
313
+ dropzoneVariant === "square"
314
+ ? "w-full px-2 items-center"
315
+ : "items-start",
316
+ )}
317
+ >
318
+ <span
319
+ className={cn(
320
+ "truncate block w-full text-sm font-medium text-black",
321
+ dropzoneVariant === "square" ? "text-center" : "text-left",
322
+ )}
323
+ >
324
+ {file.name}
325
+ </span>
326
+
327
+ <span className="text-xs text-black">
328
+ {(file.size / 1024 / 1024).toFixed(2)} MB
329
+ </span>
330
+ </div>
331
+
332
+ <button
333
+ type="button"
334
+ onClick={handleClear}
335
+ className="p-2 rounded-full transition-all cursor-pointer bg-black text-white hover:bg-red-500"
336
+ >
337
+ <X size={14} />
338
+ </button>
339
+ </div>
340
+ ) : (
341
+ /* Empty / Unselected state display view */
342
+ <div
343
+ className={cn(
344
+ "flex items-center gap-2 pointer-events-none transition-all",
345
+ dropzoneVariant === "narrow" ? "flex-row w-full" : "flex-col",
346
+ )}
347
+ >
348
+ <div
349
+ className={cn(
350
+ "rounded-full flex items-center justify-center group-hover:scale-110 transition-transform shrink-0 text-black bg-white",
351
+ dropzoneVariant === "narrow" ? "w-8 h-8" : "w-10 h-10",
352
+ )}
353
+ >
354
+ <Upload size={dropzoneVariant === "narrow" ? 16 : 20} />
355
+ </div>
356
+ <div
357
+ className={cn(
358
+ "flex flex-col min-w-0",
359
+ dropzoneVariant === "narrow" ? "items-start" : "items-center",
360
+ )}
361
+ >
362
+ <span className="text-sm font-medium text-white">
363
+ {isDragging ? "Drop your file here" : "Click or drag to upload"}
364
+ </span>
365
+ <span className="text-xs text-white">
366
+ {accept ? `Supports: ${accept}` : "All file types supported"}
367
+ </span>
368
+ </div>
369
+ </div>
370
+ )}
371
+ </div>
372
+
373
+ {displayError && (
374
+ <span className="text-xs text-red-500 ml-1 font-medium">
375
+ {displayError}
376
+ </span>
377
+ )}
378
+ </div>
379
+ );
380
+ };
@@ -0,0 +1,22 @@
1
+ export * from "./input";
2
+ export * from "./checkbox";
3
+ export * from "./textarea";
4
+ export * from "./radio";
5
+ export * from "./radio-group";
6
+ export * from "./switch";
7
+ export * from "./checkbox-group";
8
+ export * from "./range-slider";
9
+ export * from "./password-input";
10
+
11
+ export * from "./date-picker";
12
+ export * from "./time-picker";
13
+ export * from "./date-range-picker";
14
+ export * from "./time-range-picker";
15
+
16
+ export * from "./number-input";
17
+ export * from "./amount-input";
18
+ export * from "./phone-input";
19
+ export * from "./url-input";
20
+
21
+ export * from "./email-input";
22
+ export * from "./file-input";