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,1039 @@
1
+ import React, { useState, useMemo } from "react";
2
+ import { cn } from "@/utils/cn";
3
+ import {
4
+ Calendar as CalendarIcon,
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ ChevronDown,
8
+ PanelLeftClose,
9
+ PanelLeft,
10
+ } from "lucide-react";
11
+ import { SelectInput } from "../select-dropdown/select-input";
12
+ const DateUtils = {};
13
+ import {
14
+ useFloating,
15
+ autoUpdate,
16
+ offset,
17
+ flip,
18
+ shift,
19
+ size,
20
+ useClick,
21
+ useDismiss,
22
+ useRole,
23
+ useInteractions,
24
+ FloatingPortal,
25
+ FloatingFocusManager,
26
+ } from "@floating-ui/react";
27
+
28
+ interface DateRange {
29
+ from: Date | undefined;
30
+ to: Date | undefined;
31
+ }
32
+
33
+ type StaticPresetId =
34
+ | "yesterday"
35
+ | "today"
36
+ | "tomorrow"
37
+ | "last-week"
38
+ | "this-week"
39
+ | "next-week"
40
+ | "last-month"
41
+ | "this-month"
42
+ | "next-month"
43
+ | "last-year"
44
+ | "this-year"
45
+ | "next-year";
46
+
47
+ type DynamicPresetId =
48
+ | `last-${number}-weeks`
49
+ | `last-${number}-months`
50
+ | `last-${number}-years`
51
+ | `next-${number}-weeks`
52
+ | `next-${number}-months`
53
+ | `next-${number}-years`;
54
+
55
+ /**
56
+ * Available static and dynamic presets for the DateRangePicker sidebar.
57
+ *
58
+ * Static presets:
59
+ * - "today", "yesterday", "tomorrow"
60
+ * - "this-week", "last-week", "next-week"
61
+ * - "this-month", "last-month", "next-month"
62
+ * - "this-year", "last-year", "next-year"
63
+ *
64
+ * Dynamic presets (replace 'X' with a number):
65
+ * - "last-X-weeks", "next-X-weeks" (Max X: 52)
66
+ * - "last-X-months", "next-X-months" (Max X: 12)
67
+ * - "last-X-years", "next-X-years" (Max X: 10)
68
+ */
69
+ type PresetId = StaticPresetId | DynamicPresetId | string;
70
+
71
+ interface DateRangePickerProps {
72
+ label?: string;
73
+ description?: string;
74
+ error?: string;
75
+ value?: DateRange;
76
+ onChange?: (range: DateRange) => void;
77
+ presets?: PresetId[];
78
+ minYear?: number;
79
+ maxYear?: number;
80
+ placeholder?: string;
81
+ defaultToToday?: boolean;
82
+ formatStr?: string;
83
+ disableBefore?: Date;
84
+ disableAfter?: Date;
85
+ isTypeable?: boolean;
86
+ typeableFormat?: string;
87
+ variant?: "rounded" | "curved" | "square";
88
+ labelPlacement?: "top" | "left" | "right";
89
+ labelWidth?: string;
90
+ "labelAlign-X"?: "left" | "center" | "right";
91
+ "labelAlign-Y"?: "top" | "middle" | "bottom";
92
+ className?: string;
93
+ }
94
+
95
+ export const DateRangePicker = ({
96
+ label,
97
+ description,
98
+ error,
99
+ value,
100
+ onChange,
101
+ presets: enabledPresets,
102
+ minYear = 1900,
103
+ maxYear = 2100,
104
+ placeholder,
105
+ defaultToToday = false,
106
+ formatStr = "dd/mm/yyyy",
107
+ disableBefore,
108
+ disableAfter,
109
+ isTypeable = false,
110
+ typeableFormat = "dd/mm/yyyy",
111
+ variant = "curved",
112
+ labelPlacement = "top",
113
+ labelWidth = "w-32",
114
+ "labelAlign-X": labelAlignX,
115
+ "labelAlign-Y": labelAlignY = "middle",
116
+ className,
117
+ }: DateRangePickerProps) => {
118
+ const [isOpen, setIsOpen] = useState(false);
119
+ const [range, setRange] = useState<DateRange>(
120
+ value || { from: undefined, to: undefined },
121
+ );
122
+ const [inputValue, setInputValue] = useState("");
123
+ const inputRef = React.useRef<HTMLInputElement>(null);
124
+ const [cursorPos, setCursorPos] = useState<number | null>(null);
125
+ const [hoverDate, setHoverDate] = useState<Date | undefined>(undefined);
126
+ const [viewDate, setViewDate] = useState<Date>(range.from || new Date());
127
+ const [activeBox, setActiveBox] = useState<"from" | "to">("from");
128
+ const [isPanelVisible, setIsPanelVisible] = useState(true);
129
+ const [isFocused, setIsFocused] = useState(false);
130
+
131
+ /* Core Utilities from root.config */
132
+ const {
133
+ format: baseFormat = (d: Date) => d.toDateString(),
134
+ addMonths = (d: Date, n: number) =>
135
+ new Date(d.getFullYear(), d.getMonth() + n, 1),
136
+ subMonths = (d: Date, n: number) =>
137
+ new Date(d.getFullYear(), d.getMonth() - n, 1),
138
+ startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1),
139
+ endOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth() + 1, 0),
140
+ startOfWeek = (d: Date) => {
141
+ const date = new Date(d);
142
+ const day = date.getDay();
143
+ const diff = date.getDate() - day;
144
+ return new Date(date.setDate(diff));
145
+ },
146
+ endOfWeek = (d: Date) => {
147
+ const date = new Date(d);
148
+ const day = date.getDay();
149
+ const diff = date.getDate() + (6 - day);
150
+ return new Date(date.setDate(diff));
151
+ },
152
+ eachDayOfInterval = ({ start, end }: { start: Date; end: Date }) => {
153
+ const days = [];
154
+ let current = new Date(start);
155
+ while (current <= end) {
156
+ days.push(new Date(current));
157
+ current.setDate(current.getDate() + 1);
158
+ }
159
+ return days;
160
+ },
161
+ isSameDay = (d1: Date, d2: Date) => d1.toDateString() === d2.toDateString(),
162
+ addDays = (d: Date, n: number) => {
163
+ const r = new Date(d);
164
+ r.setDate(r.getDate() + n);
165
+ return r;
166
+ },
167
+ subDays = (d: Date, n: number) => {
168
+ const r = new Date(d);
169
+ r.setDate(r.getDate() - n);
170
+ return r;
171
+ },
172
+ startOfYear = (d: Date) => new Date(d.getFullYear(), 0, 1),
173
+ endOfYear = (d: Date) => new Date(d.getFullYear(), 11, 31),
174
+ addYears = (d: Date, n: number) =>
175
+ new Date(d.getFullYear() + n, d.getMonth(), d.getDate()),
176
+ subYears = (d: Date, n: number) =>
177
+ new Date(d.getFullYear() - n, d.getMonth(), d.getDate()),
178
+ } = DateUtils as any;
179
+
180
+ const format = (d: Date, fmtStr: string) => {
181
+ const normalized = fmtStr.replace(/mm/g, "MM");
182
+ return baseFormat(d, normalized);
183
+ };
184
+
185
+ /* Local helper functions for missing exports */
186
+ const isBeforeDate = (d1: Date, d2: Date) => d1.getTime() < d2.getTime();
187
+ const isAfterDate = (d1: Date, d2: Date) => d1.getTime() > d2.getTime();
188
+
189
+ const isDateDisabled = (date: Date) => {
190
+ if (
191
+ disableBefore &&
192
+ isBeforeDate(date, disableBefore) &&
193
+ !isSameDay(date, disableBefore)
194
+ )
195
+ return true;
196
+ if (
197
+ disableAfter &&
198
+ isAfterDate(date, disableAfter) &&
199
+ !isSameDay(date, disableAfter)
200
+ )
201
+ return true;
202
+ return false;
203
+ };
204
+
205
+ React.useEffect(() => {
206
+ if (range.from && range.to && isTypeable && !isFocused) {
207
+ setInputValue(
208
+ `${format(range.from, typeableFormat)} to ${format(range.to, typeableFormat)}`,
209
+ );
210
+ } else if (!range.from && !range.to && !isFocused) {
211
+ setInputValue("");
212
+ }
213
+ }, [range, isTypeable, typeableFormat, isFocused, format]);
214
+
215
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
216
+ let rawVal = e.target.value;
217
+ let cPos = e.target.selectionStart || 0;
218
+ let nativeEvent = e.nativeEvent as InputEvent;
219
+ let isDeleting = nativeEvent.inputType === "deleteContentBackward";
220
+
221
+ const rawBeforeCursor = rawVal.slice(0, cPos);
222
+ const valueCharsBefore = (rawBeforeCursor.match(/[0-9]/g) || []).length;
223
+
224
+ let digits = rawVal.replace(/\D/g, "").slice(0, 16);
225
+ let oldDigits = inputValue.replace(/\D/g, "");
226
+
227
+ /* If they backspaced a delimiter, force remove a digit */
228
+ if (isDeleting && oldDigits.length === digits.length && digits.length > 0) {
229
+ /* Find which digit to remove based on cursor position.
230
+ A simple fallback: just remove the last digit if we can't pinpoint it,
231
+ but actually, since they deleted a delimiter, the mask will just put it back.
232
+ To actually delete the digit before the delimiter, we can just slice the digits. */
233
+ digits = digits.slice(0, -1);
234
+ cPos--;
235
+ }
236
+
237
+ let masked = "";
238
+
239
+ /* Date 1 */
240
+ if (digits.length > 0) masked += digits.slice(0, 2);
241
+ if (digits.length > 2) masked += "/" + digits.slice(2, 4);
242
+ if (digits.length > 4) masked += "/" + digits.slice(4, 8);
243
+
244
+ if (digits.length > 8) masked += " to ";
245
+
246
+ /* Date 2 */
247
+ if (digits.length > 8) masked += digits.slice(8, 10);
248
+ if (digits.length > 10) masked += "/" + digits.slice(10, 12);
249
+ if (digits.length > 12) masked += "/" + digits.slice(12, 16);
250
+
251
+ /* Calculate full mask for cursor */
252
+ let baseTemplate = typeableFormat.replace(/[a-zA-Z]/g, "_");
253
+ let template = `${baseTemplate} to ${baseTemplate}`;
254
+ let fullMask = "";
255
+ for (let i = 0; i < template.length; i++) {
256
+ if (masked[i]) fullMask += masked[i];
257
+ else fullMask += template[i];
258
+ }
259
+
260
+ let newCPos = 0;
261
+ let count = 0;
262
+ for (let i = 0; i < fullMask.length; i++) {
263
+ if (/[0-9]/.test(fullMask[i])) count++;
264
+ if (count === valueCharsBefore) {
265
+ newCPos = i + 1;
266
+ break;
267
+ }
268
+ }
269
+ if (valueCharsBefore === 0) newCPos = 0;
270
+ else if (count < valueCharsBefore) newCPos = fullMask.length;
271
+
272
+ if (!isDeleting) {
273
+ while (fullMask[newCPos] && /[^0-9A-P_]/.test(fullMask[newCPos])) {
274
+ newCPos++;
275
+ }
276
+ } else {
277
+ while (
278
+ newCPos > 0 &&
279
+ fullMask[newCPos - 1] &&
280
+ /[^0-9A-P_]/.test(fullMask[newCPos - 1])
281
+ ) {
282
+ newCPos--;
283
+ }
284
+ }
285
+
286
+ setCursorPos(newCPos);
287
+ setInputValue(masked);
288
+
289
+ if (digits.length === 16) {
290
+ const p1 = masked.split(" to ")[0];
291
+ const p2 = masked.split(" to ")[1];
292
+ if (p1 && p2) {
293
+ const d1 = new Date(p1);
294
+ const d2 = new Date(p2);
295
+ if (!isNaN(d1.getTime()) && !isNaN(d2.getTime())) {
296
+ setRange({ from: d1, to: d2 });
297
+ onChange?.({ from: d1, to: d2 });
298
+ }
299
+ }
300
+ } else if (digits.length === 0) {
301
+ setRange({ from: undefined, to: undefined });
302
+ onChange?.({ from: undefined, to: undefined });
303
+ }
304
+ };
305
+
306
+ const getFullMaskedValue = () => {
307
+ if (!isFocused && !inputValue) return "";
308
+ let current = inputValue;
309
+ let baseTemplate = typeableFormat.replace(/[a-zA-Z]/g, "_");
310
+ let template = `${baseTemplate} to ${baseTemplate}`;
311
+
312
+ let res = "";
313
+ for (let i = 0; i < template.length; i++) {
314
+ if (current[i]) res += current[i];
315
+ else res += template[i];
316
+ }
317
+ return res;
318
+ };
319
+
320
+ const handleFocus = () => {
321
+ setIsFocused(true);
322
+ const pos = inputValue.length;
323
+ setTimeout(() => inputRef.current?.setSelectionRange(pos, pos), 0);
324
+ };
325
+
326
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
327
+ if (e.ctrlKey || e.metaKey || e.altKey || e.key.length > 1) return;
328
+ if (!/[0-9 \-:/TO]/.test(e.key.toUpperCase())) {
329
+ e.preventDefault();
330
+ }
331
+ };
332
+
333
+ React.useLayoutEffect(() => {
334
+ if (isFocused && inputRef.current && cursorPos !== null) {
335
+ inputRef.current.setSelectionRange(cursorPos, cursorPos);
336
+ }
337
+ }, [inputValue, isFocused, cursorPos]);
338
+
339
+ const { refs, floatingStyles, context } = useFloating({
340
+ open: !isTypeable && isOpen,
341
+ onOpenChange: setIsOpen,
342
+ middleware: [
343
+ offset(10),
344
+ flip({ fallbackAxisSideDirection: "start" }),
345
+ shift(),
346
+ size({
347
+ apply({ availableHeight, elements }) {
348
+ Object.assign(elements.floating.style, {
349
+ maxHeight: `${Math.max(400, availableHeight - 20)}px`,
350
+ });
351
+ },
352
+ }),
353
+ ],
354
+ whileElementsMounted: autoUpdate,
355
+ });
356
+
357
+ const click = useClick(context, { enabled: !isTypeable });
358
+ const dismiss = useDismiss(context, { enabled: !isTypeable });
359
+ const role = useRole(context);
360
+ const { getReferenceProps, getFloatingProps } = useInteractions([
361
+ click,
362
+ dismiss,
363
+ role,
364
+ ]);
365
+
366
+ const calendarDays = useMemo(() => {
367
+ try {
368
+ const start = startOfWeek(startOfMonth(viewDate));
369
+ const end = endOfWeek(endOfMonth(viewDate));
370
+ return eachDayOfInterval({ start, end }) as Date[];
371
+ } catch (e) {
372
+ return [] as Date[];
373
+ }
374
+ }, [
375
+ viewDate,
376
+ startOfWeek,
377
+ startOfMonth,
378
+ endOfWeek,
379
+ endOfMonth,
380
+ eachDayOfInterval,
381
+ ]);
382
+
383
+ const allStaticPresets = [
384
+ {
385
+ id: "yesterday",
386
+ label: "Yesterday",
387
+ getValue: () => {
388
+ const d = subDays(new Date(), 1);
389
+ return { from: d, to: d };
390
+ },
391
+ },
392
+ {
393
+ id: "today",
394
+ label: "Today",
395
+ getValue: () => ({ from: new Date(), to: new Date() }),
396
+ },
397
+ {
398
+ id: "tomorrow",
399
+ label: "Tomorrow",
400
+ getValue: () => {
401
+ const d = addDays(new Date(), 1);
402
+ return { from: d, to: d };
403
+ },
404
+ },
405
+ {
406
+ id: "last-week",
407
+ label: "Last Week",
408
+ getValue: () => {
409
+ const d = subDays(new Date(), 7);
410
+ return { from: startOfWeek(d), to: endOfWeek(d) };
411
+ },
412
+ },
413
+ {
414
+ id: "this-week",
415
+ label: "This Week",
416
+ getValue: () => ({
417
+ from: startOfWeek(new Date()),
418
+ to: endOfWeek(new Date()),
419
+ }),
420
+ },
421
+ {
422
+ id: "next-week",
423
+ label: "Next Week",
424
+ getValue: () => {
425
+ const d = addDays(new Date(), 7);
426
+ return { from: startOfWeek(d), to: endOfWeek(d) };
427
+ },
428
+ },
429
+ {
430
+ id: "last-month",
431
+ label: "Last Month",
432
+ getValue: () => {
433
+ const d = subMonths(new Date(), 1);
434
+ return { from: startOfMonth(d), to: endOfMonth(d) };
435
+ },
436
+ },
437
+ {
438
+ id: "this-month",
439
+ label: "This Month",
440
+ getValue: () => ({
441
+ from: startOfMonth(new Date()),
442
+ to: endOfMonth(new Date()),
443
+ }),
444
+ },
445
+ {
446
+ id: "next-month",
447
+ label: "Next Month",
448
+ getValue: () => {
449
+ const d = addMonths(new Date(), 1);
450
+ return { from: startOfMonth(d), to: endOfMonth(d) };
451
+ },
452
+ },
453
+ {
454
+ id: "last-year",
455
+ label: "Last Year",
456
+ getValue: () => {
457
+ const d = subYears(new Date(), 1);
458
+ return { from: startOfYear(d), to: endOfYear(d) };
459
+ },
460
+ },
461
+ {
462
+ id: "this-year",
463
+ label: "This Year",
464
+ getValue: () => ({
465
+ from: startOfYear(new Date()),
466
+ to: endOfYear(new Date()),
467
+ }),
468
+ },
469
+ {
470
+ id: "next-year",
471
+ label: "Next Year",
472
+ getValue: () => {
473
+ const d = addYears(new Date(), 1);
474
+ return { from: startOfYear(d), to: endOfYear(d) };
475
+ },
476
+ },
477
+ ];
478
+
479
+ const presets = useMemo(() => {
480
+ if (!enabledPresets) return [];
481
+ return enabledPresets
482
+ .map(id => {
483
+ const staticMatch = allStaticPresets.find(p => p.id === id);
484
+ if (staticMatch) return staticMatch;
485
+ const parts = id.split("-");
486
+ if (
487
+ parts.length === 3 &&
488
+ (parts[0] === "last" || parts[0] === "next")
489
+ ) {
490
+ const isPast = parts[0] === "last";
491
+ const val = parseInt(parts[1]);
492
+ const unit = parts[2];
493
+ const today = new Date();
494
+ if (!isNaN(val)) {
495
+ if (unit === "weeks") {
496
+ const capped = Math.min(val, 52);
497
+ return {
498
+ id,
499
+ label: `${isPast ? "Last" : "Next"} ${capped} Weeks`,
500
+ getValue: () =>
501
+ isPast
502
+ ? { from: subDays(today, capped * 7), to: today }
503
+ : { from: today, to: addDays(today, capped * 7) },
504
+ };
505
+ }
506
+ if (unit === "months") {
507
+ const capped = Math.min(val, 12);
508
+ return {
509
+ id,
510
+ label: `${isPast ? "Last" : "Next"} ${capped} Months`,
511
+ getValue: () =>
512
+ isPast
513
+ ? { from: subMonths(today, capped), to: today }
514
+ : { from: today, to: addMonths(today, capped) },
515
+ };
516
+ }
517
+ if (unit === "years") {
518
+ const capped = Math.min(val, 10);
519
+ return {
520
+ id,
521
+ label: `${isPast ? "Last" : "Next"} ${capped} Years`,
522
+ getValue: () =>
523
+ isPast
524
+ ? { from: subYears(today, capped), to: today }
525
+ : { from: today, to: addYears(today, capped) },
526
+ };
527
+ }
528
+ }
529
+ }
530
+ return null;
531
+ })
532
+ .filter(p => p !== null) as {
533
+ id: string;
534
+ label: string;
535
+ getValue: () => DateRange;
536
+ }[];
537
+ }, [enabledPresets]);
538
+
539
+ const showSidebar = presets.length > 0;
540
+
541
+ const isPresetActive = (p: { getValue: () => DateRange }) => {
542
+ const pRange = p.getValue();
543
+ if (!range.from || !pRange.from || !range.to || !pRange.to) return false;
544
+ return isSameDay(range.from, pRange.from) && isSameDay(range.to, pRange.to);
545
+ };
546
+
547
+ const handleDateSelect = (date: Date) => {
548
+ if (activeBox === "from") {
549
+ setRange({
550
+ from: date,
551
+ to: range.to && isAfterDate(date, range.to) ? undefined : range.to,
552
+ });
553
+ setActiveBox("to");
554
+ } else {
555
+ if (range.from && isBeforeDate(date, range.from)) {
556
+ setRange({ from: date, to: range.from });
557
+ } else {
558
+ setRange({ ...range, to: date });
559
+ }
560
+ setActiveBox("from");
561
+ }
562
+ };
563
+
564
+ const handleBoxClick = (box: "from" | "to") => {
565
+ setActiveBox(box);
566
+ const dateToView = box === "from" ? range.from : range.to;
567
+ if (dateToView) setViewDate(dateToView);
568
+ };
569
+
570
+ const isInRange = (date: Date) => {
571
+ if (range.from && range.to) {
572
+ const start = isBeforeDate(range.from, range.to) ? range.from : range.to;
573
+ const end = isBeforeDate(range.from, range.to) ? range.to : range.from;
574
+ return (
575
+ (isAfterDate(date, start) && isBeforeDate(date, end)) ||
576
+ isSameDay(date, start) ||
577
+ isSameDay(date, end)
578
+ );
579
+ }
580
+ if (range.from && hoverDate) {
581
+ const start = isBeforeDate(hoverDate, range.from)
582
+ ? hoverDate
583
+ : range.from;
584
+ const end = isBeforeDate(hoverDate, range.from) ? range.from : hoverDate;
585
+ return (
586
+ (isAfterDate(date, start) && isBeforeDate(date, end)) ||
587
+ isSameDay(date, start) ||
588
+ isSameDay(date, end)
589
+ );
590
+ }
591
+ return false;
592
+ };
593
+
594
+ const months = [
595
+ "Jan",
596
+ "Feb",
597
+ "Mar",
598
+ "Apr",
599
+ "May",
600
+ "Jun",
601
+ "Jul",
602
+ "Aug",
603
+ "Sep",
604
+ "Oct",
605
+ "Nov",
606
+ "Dec",
607
+ ];
608
+ const years = Array.from(
609
+ { length: maxYear - minYear + 1 },
610
+ (_, i) => minYear + i,
611
+ );
612
+
613
+ const monthOptions = useMemo(
614
+ () =>
615
+ months.map((m, i) => ({
616
+ id: m,
617
+ label: m,
618
+ key: i.toString(),
619
+ })),
620
+ [months],
621
+ );
622
+
623
+ const yearOptions = useMemo(
624
+ () =>
625
+ years.map(y => ({
626
+ id: y.toString(),
627
+ label: y.toString(),
628
+ key: y.toString(),
629
+ })),
630
+ [years],
631
+ );
632
+
633
+ const getDisplayText = () => {
634
+ if (range.from) {
635
+ if (range.to && isSameDay(range.from, range.to))
636
+ return format(range.from, formatStr || "dd/mm/yyyy");
637
+
638
+ const isDiffYear =
639
+ range.to && range.from.getFullYear() !== range.to.getFullYear();
640
+ const shortF = formatStr || "MMM d";
641
+
642
+ if (isDiffYear && range.to) {
643
+ return `${format(range.from, "MMM d, yyyy")} - ${format(range.to, "MMM d, yyyy")}`;
644
+ }
645
+
646
+ const yearF = formatStr ? "" : format(range.to || range.from, ", yyyy");
647
+ return `${format(range.from, shortF)} - ${range.to ? format(range.to, shortF) : "..."}${yearF}`;
648
+ }
649
+ if (placeholder) return placeholder;
650
+ if (defaultToToday) {
651
+ const today = new Date();
652
+ return `${format(today, "MMM d")} - ${format(today, "MMM d, yyyy")}`;
653
+ }
654
+ return "";
655
+ };
656
+
657
+ const isSideLabel = labelPlacement === "left" || labelPlacement === "right";
658
+ const xAlignment =
659
+ labelAlignX ||
660
+ (labelPlacement === "left"
661
+ ? "left"
662
+ : labelPlacement === "right"
663
+ ? "right"
664
+ : "left");
665
+ const yAlignmentClass =
666
+ labelAlignY === "top"
667
+ ? "items-start"
668
+ : labelAlignY === "bottom"
669
+ ? "items-end"
670
+ : "items-center";
671
+
672
+ const selectionRadius =
673
+ variant === "square"
674
+ ? "rounded-none"
675
+ : variant === "curved"
676
+ ? "rounded-lg"
677
+ : "rounded-full";
678
+
679
+ const borderRadius =
680
+ variant === "square"
681
+ ? "rounded-none"
682
+ : variant === "curved"
683
+ ? "rounded-lg"
684
+ : "rounded-full";
685
+
686
+ return (
687
+ <div
688
+ className={cn(
689
+ "flex w-full",
690
+ labelPlacement === "top" && "flex-col gap-1.5",
691
+ labelPlacement === "left" && cn("flex-row gap-4", yAlignmentClass),
692
+ labelPlacement === "right" &&
693
+ cn("flex-row-reverse gap-4", yAlignmentClass),
694
+ className,
695
+ )}
696
+ >
697
+ {label && (
698
+ <div
699
+ className={cn(
700
+ "flex flex-col",
701
+ isSideLabel ? "shrink-0" : "w-full",
702
+ labelAlignY === "top" && isSideLabel && "mt-2.5",
703
+ )}
704
+ >
705
+ <div
706
+ className={cn(
707
+ isSideLabel ? labelWidth : "w-full",
708
+ "flex flex-col",
709
+ xAlignment === "left" && "items-start text-left",
710
+ xAlignment === "right" && "items-end text-right",
711
+ xAlignment === "center" && "items-center text-center",
712
+ )}
713
+ >
714
+ <span className="text-sm font-medium text-black">{label}</span>
715
+ {description && (
716
+ <span className="text-[11px] text-black font-medium mt-0.5">
717
+ {description}
718
+ </span>
719
+ )}
720
+ </div>
721
+ </div>
722
+ )}
723
+
724
+ <div className="flex-1 relative group">
725
+ {isTypeable ? (
726
+ <div className="relative flex items-center">
727
+ <input
728
+ ref={inputRef}
729
+ type="text"
730
+ value={getFullMaskedValue()}
731
+ onChange={handleInputChange}
732
+ onKeyDown={handleKeyDown}
733
+ onFocus={handleFocus}
734
+ onBlur={() => setIsFocused(false)}
735
+ placeholder={`${typeableFormat.toLowerCase()} to ${typeableFormat.toLowerCase()}`}
736
+ className={cn(
737
+ "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",
738
+ borderRadius,
739
+ isFocused
740
+ ? "border-sky-500 ring-4 ring-sky-500/10 shadow-lg"
741
+ : "hover:border-gray-800",
742
+ error && "border-red-500 ring-4 ring-red-500/10 text-red-500",
743
+ )}
744
+ />
745
+ <CalendarIcon
746
+ size={16}
747
+ className={cn(
748
+ "absolute left-3 transition-colors",
749
+ error ? "text-red-500" : "text-black",
750
+ )}
751
+ />
752
+ </div>
753
+ ) : (
754
+ <button
755
+ type="button"
756
+ ref={refs.setReference}
757
+ {...getReferenceProps()}
758
+ onFocus={() => setIsFocused(true)}
759
+ onBlur={() => setIsFocused(false)}
760
+ className={cn(
761
+ "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",
762
+ borderRadius,
763
+ isOpen
764
+ ? "border-sky-500 ring-4 ring-sky-500/10"
765
+ : "hover:border-gray-800",
766
+ error && "border-red-500 ring-4 ring-red-500/10",
767
+ )}
768
+ >
769
+ <div className="flex items-center pl-2.25 pr-2 shrink-0">
770
+ <CalendarIcon size={16} className="text-black" />
771
+ </div>
772
+ <span
773
+ className={cn(
774
+ "text-md flex-1 text-left truncate text-black",
775
+ !range.from && !defaultToToday && "text-gray-400",
776
+ )}
777
+ >
778
+ {range.from ? getDisplayText() : placeholder || ""}
779
+ </span>
780
+ <ChevronDown
781
+ size={14}
782
+ className={cn(
783
+ "text-black transition-transform duration-200",
784
+ isOpen && "rotate-180",
785
+ )}
786
+ />
787
+ </button>
788
+ )}
789
+
790
+ {isOpen && !isTypeable && (
791
+ <FloatingPortal>
792
+ <FloatingFocusManager context={context} modal={false}>
793
+ <div
794
+ ref={refs.setFloating}
795
+ style={floatingStyles}
796
+ {...getFloatingProps()}
797
+ className="z-[9999] flex bg-white border-[1.5px] border-black rounded-xl shadow-xl animate-in fade-in duration-200 max-w-[95vw] overflow-hidden"
798
+ >
799
+ {/* Sidebar Panel */}
800
+ {showSidebar && isPanelVisible && (
801
+ <div className="w-44 shrink-0 border-r border-black bg-black flex flex-col min-h-0">
802
+ <div className="flex items-center justify-between p-4 pb-2 border-b border-white/50">
803
+ <span className="text-xs font-semibold text-white uppercase">
804
+ Presets
805
+ </span>
806
+ </div>
807
+ <div className="flex-1 overflow-y-auto custom-scrollbar p-2.5 flex flex-col gap-0.5">
808
+ {presets.map(p => {
809
+ const active = isPresetActive(p);
810
+ return (
811
+ <button
812
+ key={p.id}
813
+ onClick={() => {
814
+ const r = p.getValue();
815
+ setRange(r);
816
+ if (r.from) setViewDate(r.from);
817
+ setActiveBox("from");
818
+ }}
819
+ className={cn(
820
+ "px-3 py-2 text-xs font-semibold text-left rounded-lg transition-all cursor-pointer truncate",
821
+ active
822
+ ? "bg-white text-black z-10"
823
+ : "text-white hover:bg-white/15 ",
824
+ )}
825
+ >
826
+ {p.label}
827
+ </button>
828
+ );
829
+ })}
830
+ </div>
831
+ </div>
832
+ )}
833
+
834
+ {/* Main Content */}
835
+ <div
836
+ className={cn(
837
+ "flex flex-col min-w-[340px] flex-1 min-h-0 bg-white",
838
+ )}
839
+ >
840
+ <div className="flex items-center gap-2 p-4 pb-0">
841
+ {showSidebar && (
842
+ <button
843
+ onClick={() => setIsPanelVisible(!isPanelVisible)}
844
+ className="p-1.5 bg-black/10 hover:bg-sky-500/10 hover:text-sky-500 rounded-lg text-black cursor-pointer transition-colors shrink-0"
845
+ >
846
+ {isPanelVisible ? (
847
+ <PanelLeftClose size={16} />
848
+ ) : (
849
+ <PanelLeft size={16} />
850
+ )}
851
+ </button>
852
+ )}
853
+ <button
854
+ onClick={() => handleBoxClick("from")}
855
+ className={cn(
856
+ "flex-1 p-1.5 border-[1.5px] transition-all rounded-lg text-center font-bold uppercase text-[11px] cursor-pointer truncate",
857
+ activeBox === "from"
858
+ ? "border-sky-500 bg-sky-500/10 text-sky-600"
859
+ : "border-black hover:border-gray-800 text-black",
860
+ )}
861
+ >
862
+ {range.from
863
+ ? format(range.from, formatStr || "dd/mm/yyyy")
864
+ : "START"}
865
+ </button>
866
+ <button
867
+ onClick={() => handleBoxClick("to")}
868
+ className={cn(
869
+ "flex-1 p-1.5 border-[1.5px] transition-all rounded-lg text-center font-bold uppercase text-[11px] cursor-pointer truncate",
870
+ activeBox === "to"
871
+ ? "border-sky-500 bg-sky-500/10 text-sky-600"
872
+ : "border-black hover:border-gray-800 text-black",
873
+ )}
874
+ >
875
+ {range.to
876
+ ? format(range.to, formatStr || "dd/mm/yyyy")
877
+ : "END"}
878
+ </button>
879
+ </div>
880
+
881
+ <div className="flex-1 overflow-y-auto custom-scrollbar">
882
+ <div className="flex items-center justify-between p-4">
883
+ <button
884
+ onClick={e => {
885
+ e.stopPropagation();
886
+ setViewDate(subMonths(viewDate, 1));
887
+ }}
888
+ className="h-9 w-9 flex items-center justify-center rounded-lg cursor-pointer text-black bg-black/10 hover:bg-sky-500/10 hover:text-sky-500 transition-all duration-150"
889
+ >
890
+ <ChevronLeft size={16} strokeWidth={2.5} />
891
+ </button>
892
+ <div className="flex items-center gap-2">
893
+ <SelectInput
894
+ options={monthOptions}
895
+ value={viewDate.getMonth().toString()}
896
+ onChange={key => {
897
+ setViewDate(
898
+ new Date(
899
+ viewDate.getFullYear(),
900
+ parseInt(key),
901
+ 1,
902
+ ),
903
+ );
904
+ }}
905
+ width="w-24"
906
+ />
907
+ <SelectInput
908
+ options={yearOptions}
909
+ value={viewDate.getFullYear().toString()}
910
+ onChange={key => {
911
+ setViewDate(
912
+ new Date(parseInt(key), viewDate.getMonth(), 1),
913
+ );
914
+ }}
915
+ width="w-24"
916
+ />
917
+ </div>
918
+ <button
919
+ onClick={e => {
920
+ e.stopPropagation();
921
+ setViewDate(addMonths(viewDate, 1));
922
+ }}
923
+ className="h-9 w-9 flex items-center justify-center rounded-lg cursor-pointer text-black bg-black/10 hover:bg-sky-500/10 hover:text-sky-500 transition-all duration-150"
924
+ >
925
+ <ChevronRight size={16} strokeWidth={2.5} />
926
+ </button>
927
+ </div>
928
+
929
+ <div className="px-4 pb-4">
930
+ <div className="grid grid-cols-7 gap-px">
931
+ {["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map(d => (
932
+ <div
933
+ key={d}
934
+ className="h-8 flex items-center justify-center text-xs font-semibold text-black uppercase"
935
+ >
936
+ {d}
937
+ </div>
938
+ ))}
939
+ </div>
940
+ <div className="grid grid-cols-7">
941
+ {calendarDays.map((date: Date, i: number) => {
942
+ const active = isInRange(date);
943
+ const isStart =
944
+ range.from && isSameDay(date, range.from);
945
+ const isEnd = range.to && isSameDay(date, range.to);
946
+ const isOutside =
947
+ format(date, "M") !== format(viewDate, "M");
948
+ const disabled = isDateDisabled(date);
949
+ const isToday = isSameDay(date, new Date());
950
+
951
+ return (
952
+ <div
953
+ key={i}
954
+ className="h-9 relative flex items-center justify-center"
955
+ onMouseEnter={() =>
956
+ !range.to && setHoverDate(date)
957
+ }
958
+ onMouseLeave={() => setHoverDate(undefined)}
959
+ >
960
+ {active && !isOutside && (
961
+ <div
962
+ className={cn(
963
+ "absolute inset-0 bg-black/10 z-0",
964
+ isStart &&
965
+ cn(
966
+ selectionRadius === "rounded-full"
967
+ ? "rounded-l-full"
968
+ : selectionRadius === "rounded-lg"
969
+ ? "rounded-l-lg"
970
+ : "rounded-none",
971
+ "ml-1",
972
+ ),
973
+ isEnd &&
974
+ cn(
975
+ selectionRadius === "rounded-full"
976
+ ? "rounded-r-full"
977
+ : selectionRadius === "rounded-lg"
978
+ ? "rounded-r-lg"
979
+ : "rounded-none",
980
+ "mr-1",
981
+ ),
982
+ !isStart && !isEnd && "mx-0",
983
+ )}
984
+ />
985
+ )}
986
+ <button
987
+ disabled={disabled}
988
+ onClick={() => handleDateSelect(date)}
989
+ className={cn(
990
+ "w-7 h-7 text-xs font-semibold relative z-10 transition-all",
991
+ selectionRadius,
992
+ isStart || isEnd
993
+ ? "bg-black text-white shadow-lg scale-110"
994
+ : !isOutside
995
+ ? "text-black hover:bg-black/10"
996
+ : "text-black/50",
997
+ isToday &&
998
+ !(isStart || isEnd) &&
999
+ "bg-sky-500/10 text-sky-500",
1000
+ disabled
1001
+ ? "opacity-20 cursor-not-allowed"
1002
+ : "cursor-pointer",
1003
+ )}
1004
+ >
1005
+ {format(date, "d")}
1006
+ </button>
1007
+ </div>
1008
+ );
1009
+ })}
1010
+ </div>
1011
+ </div>
1012
+ </div>
1013
+
1014
+ <div className="shrink-0 p-4 pt-0 flex items-center justify-end gap-3 bg-white z-50">
1015
+ <button
1016
+ onClick={() => setIsOpen(false)}
1017
+ className="px-4 py-2 text-[12px] font-semibold uppercase text-black/50 hover:text-black hover:bg-black/10 rounded-full cursor-pointer"
1018
+ >
1019
+ Cancel
1020
+ </button>
1021
+ <button
1022
+ onClick={() => {
1023
+ onChange?.(range);
1024
+ setIsOpen(false);
1025
+ }}
1026
+ className="px-8 py-2 bg-black text-white rounded-full text-[12px] font-semibold uppercase transition-all cursor-pointer "
1027
+ >
1028
+ Apply
1029
+ </button>
1030
+ </div>
1031
+ </div>
1032
+ </div>
1033
+ </FloatingFocusManager>
1034
+ </FloatingPortal>
1035
+ )}
1036
+ </div>
1037
+ </div>
1038
+ );
1039
+ };