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,526 @@
1
+ import React, { useState, useMemo } from "react";
2
+ import { cn } from "@/utils/cn";
3
+ import { Clock, ChevronDown } from "lucide-react";
4
+ const DateUtils = {};
5
+ import {
6
+ useFloating,
7
+ autoUpdate,
8
+ offset,
9
+ flip,
10
+ shift,
11
+ size,
12
+ useClick,
13
+ useDismiss,
14
+ useRole,
15
+ useInteractions,
16
+ FloatingPortal,
17
+ FloatingFocusManager,
18
+ } from "@floating-ui/react";
19
+
20
+ interface TimeRange {
21
+ from: Date | undefined;
22
+ to: Date | undefined;
23
+ }
24
+
25
+ interface TimeRangePickerProps {
26
+ label?: string;
27
+ description?: string;
28
+ error?: string;
29
+ value?: TimeRange;
30
+ onChange?: (range: TimeRange) => void;
31
+ timeFormat?: "12hr" | "24hr";
32
+ showSeconds?: boolean;
33
+ isTypeable?: boolean;
34
+ placeholder?: string;
35
+ variant?: "rounded" | "curved" | "square";
36
+ labelPlacement?: "top" | "left" | "right";
37
+ labelWidth?: string;
38
+ "labelAlign-X"?: "left" | "center" | "right";
39
+ "labelAlign-Y"?: "top" | "middle" | "bottom";
40
+ className?: string;
41
+ }
42
+
43
+ export const TimeRangePicker = ({
44
+ label,
45
+ description,
46
+ error,
47
+ value,
48
+ onChange,
49
+ timeFormat = "12hr",
50
+ showSeconds = false,
51
+ isTypeable = false,
52
+ placeholder,
53
+ variant = "curved",
54
+ labelPlacement = "top",
55
+ labelWidth = "w-32",
56
+ "labelAlign-X": labelAlignX,
57
+ "labelAlign-Y": labelAlignY = "middle",
58
+ className,
59
+ }: TimeRangePickerProps) => {
60
+ const [isOpen, setIsOpen] = useState(false);
61
+ const [inputValue, setInputValue] = useState("");
62
+ const inputRef = React.useRef<HTMLInputElement>(null);
63
+ const [cursorPos, setCursorPos] = useState<number | null>(null);
64
+ const [range, setRange] = useState<TimeRange>(value || { from: undefined, to: undefined });
65
+ const [activeBox, setActiveBox] = useState<"from" | "to">("from");
66
+ const [isFocused, setIsFocused] = useState(false);
67
+
68
+ const { format = (d: Date) => d.toLocaleTimeString() } = DateUtils as any;
69
+
70
+ const is12Hour = timeFormat === "12hr";
71
+
72
+ const displayFormat = useMemo(() => {
73
+ let f = is12Hour ? "hh:mm" : "HH:mm";
74
+ if (showSeconds) f += ":ss";
75
+ if (is12Hour) f += " aa";
76
+ return f;
77
+ }, [is12Hour, showSeconds]);
78
+
79
+ const { refs, floatingStyles, context } = useFloating({
80
+ open: !isTypeable && isOpen,
81
+ onOpenChange: setIsOpen,
82
+ middleware: [
83
+ offset(10),
84
+ flip({ fallbackAxisSideDirection: "start" }),
85
+ shift(),
86
+ size({
87
+ apply({ availableHeight, elements }) {
88
+ Object.assign(elements.floating.style, {
89
+ maxHeight: `${Math.max(400, availableHeight - 20)}px`,
90
+ });
91
+ },
92
+ }),
93
+ ],
94
+ whileElementsMounted: autoUpdate,
95
+ });
96
+
97
+ const click = useClick(context, { enabled: !isTypeable });
98
+ const dismiss = useDismiss(context, { enabled: !isTypeable });
99
+ const role = useRole(context);
100
+ const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
101
+
102
+ const handleBoxClick = (box: "from" | "to") => setActiveBox(box);
103
+
104
+ const activeDate = activeBox === "from" ? range.from : range.to;
105
+
106
+ const currentHour = activeDate ? (is12Hour ? (activeDate.getHours() % 12 || 12) : activeDate.getHours()) : null;
107
+ const currentMinute = activeDate ? activeDate.getMinutes() : null;
108
+ const currentSecond = activeDate ? activeDate.getSeconds() : null;
109
+ const currentPeriod = activeDate ? (activeDate.getHours() >= 12 ? "PM" : "AM") : "AM";
110
+
111
+ const hours = Array.from({ length: is12Hour ? 12 : 24 }, (_, i) => is12Hour ? i + 1 : i);
112
+ const minutes = Array.from({ length: 60 }, (_, i) => i);
113
+ const secs = Array.from({ length: 60 }, (_, i) => i);
114
+
115
+ const handleTimeSelect = (h: number, m: number, s: number, p?: string) => {
116
+ const baseDate = activeDate || new Date();
117
+ const newDate = new Date(baseDate);
118
+
119
+ let hour = h;
120
+ if (is12Hour) {
121
+ const isPM = p === "PM";
122
+ if (isPM && hour < 12) hour += 12;
123
+ if (!isPM && hour === 12) hour = 0;
124
+ }
125
+ newDate.setHours(hour, m, s, 0);
126
+
127
+ if (activeBox === "from") {
128
+ setRange({ from: newDate, to: range.to });
129
+ } else {
130
+ setRange({ ...range, to: newDate });
131
+ }
132
+ };
133
+
134
+ const getDisplayText = () => {
135
+ if (range.from) {
136
+ if (range.to) return `${format(range.from, displayFormat)} - ${format(range.to, displayFormat)}`;
137
+ return `${format(range.from, displayFormat)} - ...`;
138
+ }
139
+ return placeholder || "";
140
+ };
141
+
142
+ React.useEffect(() => {
143
+ if (range.from && range.to && isTypeable && !isFocused) {
144
+ const typeFormat = is12Hour ? (showSeconds ? "hh:mm:ss aa" : "hh:mm aa") : (showSeconds ? "HH:mm:ss" : "HH:mm");
145
+ setInputValue(`${format(range.from, typeFormat)} to ${format(range.to, typeFormat)}`);
146
+ } else if (!range.from && !range.to && !isFocused) {
147
+ setInputValue("");
148
+ }
149
+ }, [range, isTypeable, showSeconds, is12Hour, isFocused, format]);
150
+
151
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
152
+ let rawVal = e.target.value.toUpperCase();
153
+ let cPos = e.target.selectionStart || 0;
154
+ let nativeEvent = e.nativeEvent as InputEvent;
155
+ let isDeleting = nativeEvent.inputType === "deleteContentBackward";
156
+
157
+ const rawBeforeCursor = rawVal.slice(0, cPos);
158
+ const valueCharsBefore = (rawBeforeCursor.match(/[0-9AP]/g) || []).length;
159
+
160
+ let digits = rawVal.replace(/\D/g, "");
161
+ let oldDigits = inputValue.replace(/\D/g, "");
162
+
163
+ const digitLimit = showSeconds ? 12 : 8;
164
+
165
+ if (isDeleting && oldDigits.length === digits.length && digits.length > 0) {
166
+ digits = digits.slice(0, -1);
167
+ cPos--;
168
+ }
169
+
170
+ digits = digits.slice(0, digitLimit);
171
+ const midIdx = showSeconds ? 4 : 4;
172
+
173
+ let ampm1 = "";
174
+ let ampm2 = "";
175
+
176
+ if (is12Hour) {
177
+ const parts = rawVal.split("TO");
178
+ const p1 = parts[0] || "";
179
+ const p2 = parts[1] || "";
180
+ if (parts.length >= 2) {
181
+ const l1 = parts[0].replace(/[^AP]/g, "");
182
+ const l2 = parts[1].replace(/[^AP]/g, "");
183
+ if (l1.length > 0) ampm1 = l1[l1.length - 1] + "M";
184
+ if (l2.length > 0) ampm2 = l2[l2.length - 1] + "M";
185
+ } else {
186
+ const letters = rawVal.replace(/[^AP]/g, "");
187
+ if (digits.length <= midIdx) {
188
+ if (letters.length > 0) ampm1 = letters[letters.length - 1] + "M";
189
+ } else {
190
+ if (letters.length > 0) ampm1 = letters[0] + "M";
191
+ if (letters.length > 1) ampm2 = letters[letters.length - 1] + "M";
192
+ }
193
+ }
194
+
195
+ if (isDeleting) {
196
+ if (p1.trim().endsWith("A") || p1.trim().endsWith("P")) ampm1 = "";
197
+ if (p2.trim().endsWith("P") || p2.trim().endsWith("P")) ampm2 = "";
198
+ }
199
+
200
+ if (!ampm1 && digits.length > midIdx) {
201
+ digits = digits.slice(0, midIdx);
202
+ }
203
+ }
204
+
205
+ let masked = "";
206
+
207
+ // Time 1
208
+ if (digits.length > 0) masked += digits.slice(0, 2);
209
+ if (digits.length > 2) masked += ":" + digits.slice(2, 4);
210
+ if (showSeconds && digits.length > 4) masked += ":" + digits.slice(4, 6);
211
+
212
+ if (is12Hour && digits.length >= midIdx) {
213
+ if (ampm1) masked += " " + ampm1;
214
+ }
215
+
216
+ // Determine if we should show `to`
217
+ if (digits.length > midIdx || (is12Hour && (ampm1 === "AM" || ampm1 === "PM")) || (!is12Hour && digits.length === midIdx)) {
218
+ if (!masked.includes(" to ")) masked += " to ";
219
+ }
220
+
221
+ // Time 2
222
+ if (digits.length > midIdx) masked += digits.slice(midIdx, midIdx + 2);
223
+ if (digits.length > midIdx + 2) masked += ":" + digits.slice(midIdx + 2, midIdx + 4);
224
+ if (showSeconds && digits.length > midIdx + 4) masked += ":" + digits.slice(midIdx + 4, midIdx + 6);
225
+
226
+ if (is12Hour && digits.length >= digitLimit) {
227
+ if (ampm2) masked += " " + ampm2;
228
+ }
229
+
230
+ let baseTemplate = showSeconds ? "__:__:__" : "__:__";
231
+ if (is12Hour) baseTemplate += " __";
232
+ let template = `${baseTemplate} to ${baseTemplate}`;
233
+
234
+ let fullMask = "";
235
+ for (let i = 0; i < template.length; i++) {
236
+ if (masked[i]) fullMask += masked[i];
237
+ else fullMask += template[i];
238
+ }
239
+
240
+ let newCPos = 0;
241
+ let count = 0;
242
+ for (let i = 0; i < fullMask.length; i++) {
243
+ if (/[0-9AP]/.test(fullMask[i])) count++;
244
+ if (count === valueCharsBefore) {
245
+ newCPos = i + 1;
246
+ break;
247
+ }
248
+ }
249
+ if (valueCharsBefore === 0) newCPos = 0;
250
+ else if (count < valueCharsBefore) newCPos = fullMask.length;
251
+
252
+ if (!isDeleting) {
253
+ while (fullMask[newCPos] && /[^0-9A-P_]/.test(fullMask[newCPos])) {
254
+ // Prevent leaping into the second time box if AM/PM 1 is missing
255
+ if (is12Hour && !ampm1 && digits.length === midIdx && fullMask.substring(newCPos, newCPos + 4) === " to ") {
256
+ break;
257
+ }
258
+ if (fullMask[newCPos] === " " && fullMask[newCPos + 1] === "_") {
259
+ break;
260
+ }
261
+ newCPos++;
262
+ }
263
+ } else {
264
+ while (newCPos > 0 && fullMask[newCPos - 1] && /[^0-9A-P]/.test(fullMask[newCPos - 1])) {
265
+ newCPos--;
266
+ }
267
+ }
268
+
269
+ setCursorPos(newCPos);
270
+ setInputValue(masked);
271
+
272
+ const isComplete = is12Hour
273
+ ? digits.length === digitLimit && (masked.endsWith("AM") || masked.endsWith("PM")) && (masked.includes(" AM to ") || masked.includes(" PM to "))
274
+ : digits.length === digitLimit;
275
+
276
+ if (isComplete && masked.includes(" to ")) {
277
+ const p1 = masked.split(" to ")[0];
278
+ const p2 = masked.split(" to ")[1];
279
+ if (p1 && p2) {
280
+ const today = new Date();
281
+ const p1Parts = p1.split(" ");
282
+ const p2Parts = p2.split(" ");
283
+ const t1 = p1Parts[0].split(":");
284
+ const t2 = p2Parts[0].split(":");
285
+
286
+ let h1 = parseInt(t1[0]);
287
+ let h2 = parseInt(t2[0]);
288
+
289
+ if (is12Hour) {
290
+ if (p1Parts[1] === "PM" && h1 < 12) h1 += 12;
291
+ if (p1Parts[1] === "AM" && h1 === 12) h1 = 0;
292
+ if (p2Parts[1] === "PM" && h2 < 12) h2 += 12;
293
+ if (p2Parts[1] === "AM" && h2 === 12) h2 = 0;
294
+ }
295
+
296
+ const d1 = new Date(today.setHours(h1, parseInt(t1[1]), showSeconds ? parseInt(t1[2]) : 0, 0));
297
+ const today2 = new Date();
298
+ const d2 = new Date(today2.setHours(h2, parseInt(t2[1]), showSeconds ? parseInt(t2[2]) : 0, 0));
299
+
300
+ if (!isNaN(d1.getTime()) && !isNaN(d2.getTime())) {
301
+ setRange({ from: d1, to: d2 });
302
+ onChange?.({ from: d1, to: d2 });
303
+ }
304
+ }
305
+ } else if (digits.length === 0) {
306
+ setRange({ from: undefined, to: undefined });
307
+ onChange?.({ from: undefined, to: undefined });
308
+ }
309
+ };
310
+
311
+ const getFullMaskedValue = () => {
312
+ if (!isFocused && !inputValue) return "";
313
+ if (!isFocused && inputValue) return inputValue;
314
+
315
+ let current = inputValue;
316
+
317
+ let baseTemplate = showSeconds ? "__:__:__" : "__:__";
318
+ if (is12Hour) baseTemplate += " __";
319
+ let template = `${baseTemplate} to ${baseTemplate}`;
320
+
321
+ let res = "";
322
+ for (let i = 0; i < template.length; i++) {
323
+ if (current[i]) res += current[i];
324
+ else res += template[i];
325
+ }
326
+ return res;
327
+ };
328
+
329
+ const handleFocus = () => {
330
+ setIsFocused(true);
331
+ const pos = inputValue.length;
332
+ setTimeout(() => inputRef.current?.setSelectionRange(pos, pos), 0);
333
+ };
334
+
335
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
336
+ if (e.ctrlKey || e.metaKey || e.altKey || e.key.length > 1) return;
337
+ if (!/[0-9A-P: TO]/.test(e.key.toUpperCase())) {
338
+ e.preventDefault();
339
+ }
340
+ };
341
+
342
+ React.useLayoutEffect(() => {
343
+ if (isFocused && inputRef.current && cursorPos !== null) {
344
+ inputRef.current.setSelectionRange(cursorPos, cursorPos);
345
+ }
346
+ }, [inputValue, isFocused, cursorPos]);
347
+
348
+ const isSideLabel = labelPlacement === "left" || labelPlacement === "right";
349
+ const xAlignment = labelAlignX || (
350
+ labelPlacement === "left" ? "left" :
351
+ labelPlacement === "right" ? "right" : "left"
352
+ );
353
+ const yAlignmentClass =
354
+ labelAlignY === "top" ? "items-start" :
355
+ labelAlignY === "bottom" ? "items-end" : "items-center";
356
+
357
+ const borderRadius = variant === "square" ? "rounded-none" : variant === "curved" ? "rounded-lg" : "rounded-full";
358
+
359
+ return (
360
+ <div className={cn("flex w-full", labelPlacement === "top" && "flex-col gap-1.5", labelPlacement === "left" && cn("flex-row gap-4", yAlignmentClass), labelPlacement === "right" && cn("flex-row-reverse gap-4", yAlignmentClass), className)}>
361
+ {label && (
362
+ <div className={cn("flex flex-col", isSideLabel ? "shrink-0" : "w-full", labelAlignY === "top" && isSideLabel && "mt-2.5")}>
363
+ <div className={cn(
364
+ isSideLabel ? labelWidth : "w-full",
365
+ "flex flex-col",
366
+ xAlignment === "left" && "items-start text-left",
367
+ xAlignment === "right" && "items-end text-right",
368
+ xAlignment === "center" && "items-center text-center"
369
+ )}>
370
+ <span className="text-sm font-medium text-black">{label}</span>
371
+ {description && <span className="text-[11px] text-black font-medium mt-0.5">{description}</span>}
372
+ </div>
373
+ </div>
374
+ )}
375
+
376
+ <div className="flex-1 relative group">
377
+ {isTypeable ? (
378
+ <div className="relative flex items-center">
379
+ <input
380
+ ref={inputRef}
381
+ type="text"
382
+ value={getFullMaskedValue()}
383
+ onChange={handleInputChange}
384
+ onKeyDown={handleKeyDown}
385
+ onFocus={handleFocus}
386
+ onBlur={() => setIsFocused(false)}
387
+ placeholder={is12Hour ? (showSeconds ? "hh:mm:ss aa to hh:mm:ss aa" : "hh:mm aa to hh:mm aa") : (showSeconds ? "HH:mm:ss to HH:mm:ss" : "HH:mm to HH:mm")}
388
+ className={cn(
389
+ "flex items-center w-full pl-10 pr-2 h-9 border-[1.5px] border-black transition-all duration-200 bg-white text-md text-black outline-none placeholder:text-black/40 placeholder:text-sm placeholder:font-medium font-medium",
390
+ borderRadius,
391
+ isFocused ? "border-sky-500 ring-4 ring-sky-500/10 shadow-lg" : "hover:border-gray-800",
392
+ error && "border-red-500 ring-4 ring-red-500/10 text-red-500"
393
+ )}
394
+ />
395
+ <Clock size={16} className={cn("absolute left-3 transition-colors", error ? "text-red-500" : "text-black")} />
396
+ </div>
397
+ ) : (
398
+ <button
399
+ type="button"
400
+ ref={refs.setReference}
401
+ {...getReferenceProps()}
402
+ onFocus={() => setIsFocused(true)}
403
+ onBlur={() => setIsFocused(false)}
404
+ className={cn(
405
+ "flex items-center pr-2 w-full h-9 border-[1.5px] border-black transition-all duration-200 bg-white font-medium text-black cursor-pointer",
406
+ borderRadius,
407
+ isOpen ? "border-sky-500 ring-4 ring-sky-500/10" : "hover:border-gray-800",
408
+ error && "border-red-500 ring-4 ring-red-500/10"
409
+ )}
410
+ >
411
+ <div className="flex items-center pl-2.25 pr-2 shrink-0">
412
+ <Clock size={16} className="text-black" />
413
+ </div>
414
+ <span className={cn("text-md flex-1 text-left truncate text-black", !range.from && "text-gray-400")}>
415
+ {range.from ? getDisplayText() : (placeholder || "")}
416
+ </span>
417
+ <ChevronDown size={14} className={cn("text-black transition-transform duration-200", isOpen && "rotate-180")} />
418
+ </button>
419
+ )}
420
+
421
+ {isOpen && !isTypeable && (
422
+ <FloatingPortal>
423
+ <FloatingFocusManager context={context} modal={false}>
424
+ <div
425
+ ref={refs.setFloating}
426
+ style={floatingStyles}
427
+ {...getFloatingProps()}
428
+ className="z-[9999] flex flex-col min-w-[340px] bg-white border-[1.5px] border-black rounded-xl shadow-2xl animate-in fade-in duration-200 overflow-hidden"
429
+ >
430
+ <div className="flex items-center gap-2 p-4 pb-0">
431
+ <button onClick={() => handleBoxClick("from")} className={cn("flex-1 p-1.5 border-[1.5px] transition-all rounded-lg text-center font-bold uppercase text-[11px] cursor-pointer truncate", activeBox === "from" ? "border-sky-500 bg-sky-500/10 text-sky-600" : "border-black hover:border-gray-800 text-black")}>
432
+ {range.from ? format(range.from, displayFormat) : "START TIME"}
433
+ </button>
434
+ <button onClick={() => handleBoxClick("to")} className={cn("flex-1 p-1.5 border-[1.5px] transition-all rounded-lg text-center font-bold uppercase text-[11px] cursor-pointer truncate", activeBox === "to" ? "border-sky-500 bg-sky-500/10 text-sky-600" : "border-black hover:border-gray-800 text-black")}>
435
+ {range.to ? format(range.to, displayFormat) : "END TIME"}
436
+ </button>
437
+ </div>
438
+
439
+ <div className="flex p-4 justify-center gap-1 min-h-[220px] max-h-[300px]">
440
+ {/* Hour Column */}
441
+ <div className="flex flex-col overflow-y-auto custom-scrollbar px-1 flex-1">
442
+ <div className="text-[10px] font-semibold tracking-widest text-black uppercase p-2 sticky top-0 bg-white text-center">Hour</div>
443
+ {hours.map(h => (
444
+ <button
445
+ key={h}
446
+ onClick={() => handleTimeSelect(h, currentMinute || 0, currentSecond || 0, currentPeriod)}
447
+ className={cn(
448
+ "w-10 h-10 shrink-0 flex items-center justify-center rounded-lg text-sm font-bold transition-all cursor-pointer mx-auto",
449
+ currentHour === h ? "bg-black text-white shadow-lg" : "hover:bg-black/5 text-black"
450
+ )}
451
+ >
452
+ {h.toString().padStart(2, "0")}
453
+ </button>
454
+ ))}
455
+ </div>
456
+
457
+ {/* Minute Column */}
458
+ <div className="flex flex-col overflow-y-auto custom-scrollbar px-1 border-l border-black flex-1">
459
+ <div className="text-[10px] font-semibold tracking-widest text-black uppercase p-2 sticky top-0 bg-white text-center">Min</div>
460
+ {minutes.map(m => (
461
+ <button
462
+ key={m}
463
+ onClick={() => handleTimeSelect(currentHour || (is12Hour ? 12 : 0), m, currentSecond || 0, currentPeriod)}
464
+ className={cn(
465
+ "w-10 h-10 shrink-0 flex items-center justify-center rounded-lg text-sm font-bold transition-all cursor-pointer mx-auto",
466
+ currentMinute === m ? "bg-black text-white shadow-lg" : "hover:bg-black/5 text-black"
467
+ )}
468
+ >
469
+ {m.toString().padStart(2, "0")}
470
+ </button>
471
+ ))}
472
+ </div>
473
+
474
+ {/* Seconds Column */}
475
+ {showSeconds && (
476
+ <div className="flex flex-col overflow-y-auto custom-scrollbar px-1 border-l border-black flex-1">
477
+ <div className="text-[10px] font-semibold tracking-widest text-black uppercase p-2 sticky top-0 bg-white text-center">Sec</div>
478
+ {secs.map(s => (
479
+ <button
480
+ key={s}
481
+ onClick={() => handleTimeSelect(currentHour || (is12Hour ? 12 : 0), currentMinute || 0, s, currentPeriod)}
482
+ className={cn(
483
+ "w-10 h-10 shrink-0 flex items-center justify-center rounded-lg text-sm font-bold transition-all cursor-pointer mx-auto",
484
+ currentSecond === s ? "bg-black text-white shadow-lg" : "hover:bg-black/5 text-black"
485
+ )}
486
+ >
487
+ {s.toString().padStart(2, "0")}
488
+ </button>
489
+ ))}
490
+ </div>
491
+ )}
492
+
493
+ {/* AM/PM Column */}
494
+ {is12Hour && (
495
+ <div className="flex flex-col px-1 border-l border-black flex-1">
496
+ <div className="text-[10px] font-semibold tracking-widest text-black uppercase p-2 text-center">Per</div>
497
+ {["AM", "PM"].map(p => (
498
+ <button
499
+ key={p}
500
+ onClick={() => handleTimeSelect(currentHour || 12, currentMinute || 0, currentSecond || 0, p)}
501
+ className={cn(
502
+ "w-12 h-10 shrink-0 flex items-center justify-center rounded-lg text-[11px] font-bold tracking-widest transition-all cursor-pointer mx-auto",
503
+ currentPeriod === p ? "bg-black text-white shadow-lg" : "hover:bg-black/10 text-black"
504
+ )}
505
+ >
506
+ {p}
507
+ </button>
508
+ ))}
509
+ </div>
510
+ )}
511
+ </div>
512
+
513
+ <div className="shrink-0 p-4 pt-0 flex items-center justify-end gap-3 bg-white z-50">
514
+ <button onClick={() => setIsOpen(false)} className="px-4 py-2 text-[12px] font-semibold uppercase text-black/50 hover:text-black cursor-pointer">Cancel</button>
515
+ <button onClick={() => { onChange?.(range); setIsOpen(false); }} className="px-8 py-2 bg-black text-white rounded-full text-[12px] font-semibold uppercase transition-all cursor-pointer">Apply</button>
516
+ </div>
517
+ </div>
518
+ </FloatingFocusManager>
519
+ </FloatingPortal>
520
+ )}
521
+ </div>
522
+ </div>
523
+ );
524
+ };
525
+
526
+ TimeRangePicker.displayName = "TimeRangePicker";
@@ -0,0 +1,81 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Input } from "./input";
3
+ import { Link, CheckCircle2, AlertCircle } from "lucide-react";
4
+
5
+ interface URLInputProps extends React.ComponentProps<typeof Input> {
6
+ showValidationIcon?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Validates common URL formats:
11
+ * - https://www.google.com
12
+ * - http://example.com/path?query=1
13
+ * - example.com (supports missing protocol)
14
+ * - 127.0.0.1 (IP addresses)
15
+ * - sub.domain.co.uk/file.pdf
16
+ */
17
+ const validateURL = (url: string) => {
18
+ if (!url) return false;
19
+ try {
20
+ // Simple regex for URL validation
21
+ const pattern = new RegExp(
22
+ "^(https?:\\/\\/)?" + // protocol
23
+ "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
24
+ "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address
25
+ "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
26
+ "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
27
+ "(\\#[-a-z\\d_]*)?$", // fragment locator
28
+ "i"
29
+ );
30
+ return pattern.test(url);
31
+ } catch (e) {
32
+ return false;
33
+ }
34
+ };
35
+
36
+ export const URLInput = ({
37
+ showValidationIcon = true,
38
+ onChange,
39
+ className,
40
+ ...props
41
+ }: URLInputProps) => {
42
+ const [value, setValue] = useState((props.value as string) || "");
43
+ const [isValid, setIsValid] = useState(validateURL((props.value as string) || ""));
44
+
45
+ useEffect(() => {
46
+ const newVal = (props.value as string) || "";
47
+ setValue(newVal);
48
+ setIsValid(validateURL(newVal));
49
+ }, [props.value]);
50
+
51
+ const handleInternalChange = (e: React.ChangeEvent<HTMLInputElement>) => {
52
+ const val = e.target.value;
53
+ setValue(val);
54
+ setIsValid(validateURL(val));
55
+ onChange?.(e);
56
+ };
57
+
58
+ return (
59
+ <Input
60
+ autoComplete="url"
61
+ type="url"
62
+ leftIcon={<Link size={18} />}
63
+ {...props}
64
+ value={value}
65
+ onChange={handleInternalChange}
66
+ rightIcon={
67
+ showValidationIcon && value ? (
68
+ isValid ? (
69
+ <CheckCircle2 size={18} className="text-green-500" />
70
+ ) : (
71
+ <AlertCircle size={18} className="text-amber-500" />
72
+ )
73
+ ) : (
74
+ props.rightIcon
75
+ )
76
+ }
77
+ className={className}
78
+
79
+ />
80
+ );
81
+ };
@@ -0,0 +1,4 @@
1
+ export { SelectInput } from "./select-input";
2
+ export type { SelectInputProps, SelectOption } from "./select-input";
3
+ export { MultiSelectInput } from "./multiselect-input";
4
+ export type { MultiSelectInputProps } from "./multiselect-input";