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.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/bin/cli.js +379 -0
- package/package.json +52 -0
- package/registry.json +350 -0
- package/templates/button/Button.tsx +156 -0
- package/templates/button/index.ts +2 -0
- package/templates/button/tooltip.tsx +124 -0
- package/templates/form/amount-input.tsx +252 -0
- package/templates/form/checkbox-group.tsx +235 -0
- package/templates/form/checkbox.tsx +148 -0
- package/templates/form/date-picker.tsx +647 -0
- package/templates/form/date-range-picker.tsx +1039 -0
- package/templates/form/email-input.tsx +55 -0
- package/templates/form/file-input.tsx +380 -0
- package/templates/form/index.ts +22 -0
- package/templates/form/input.tsx +255 -0
- package/templates/form/number-input.tsx +186 -0
- package/templates/form/password-input.tsx +233 -0
- package/templates/form/phone-input.tsx +82 -0
- package/templates/form/radio-group.tsx +191 -0
- package/templates/form/radio.tsx +157 -0
- package/templates/form/range-slider.tsx +210 -0
- package/templates/form/switch.tsx +134 -0
- package/templates/form/textarea.tsx +253 -0
- package/templates/form/time-picker.tsx +435 -0
- package/templates/form/time-range-picker.tsx +526 -0
- package/templates/form/url-input.tsx +81 -0
- package/templates/select-dropdown/index.ts +4 -0
- package/templates/select-dropdown/multiselect-input.tsx +687 -0
- package/templates/select-dropdown/select-input.tsx +565 -0
- package/utils/cn.ts +6 -0
|
@@ -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";
|