@campxdev/react-blueprint 3.0.0-alpha.3 → 3.0.0-alpha.5

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.
Files changed (60) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/cjs/types/src/components/Input/DatePicker/DatePicker.d.ts +2 -1
  3. package/dist/cjs/types/src/components/Input/DatePicker/components/DatePickerFilter.d.ts +2 -0
  4. package/dist/cjs/types/src/components/Input/DatePicker/components/DatePickerInput.d.ts +2 -0
  5. package/dist/cjs/types/src/components/Input/DateTimePicker/DateTimePicker.d.ts +2 -1
  6. package/dist/cjs/types/src/components/Input/DateTimePicker/components/DateTimePickerFilter.d.ts +2 -0
  7. package/dist/cjs/types/src/components/Input/DateTimePicker/components/DateTimePickerInput.d.ts +2 -0
  8. package/dist/cjs/types/src/components/Input/MultiSelect/MultiSelect.d.ts +2 -0
  9. package/dist/cjs/types/src/components/Input/MultiSelect/components/MultiSelectFilter.d.ts +1 -1
  10. package/dist/cjs/types/src/components/Input/MultiSelect/components/MultiSelectInput.d.ts +1 -1
  11. package/dist/cjs/types/src/components/Input/Select/Select.d.ts +2 -1
  12. package/dist/cjs/types/src/components/Input/SingleSelect/SingleSelect.d.ts +2 -0
  13. package/dist/cjs/types/src/components/Input/SingleSelect/components/SingleFilter.d.ts +1 -1
  14. package/dist/cjs/types/src/components/Input/SingleSelect/components/SingleInput.d.ts +1 -1
  15. package/dist/cjs/types/src/shadcn-components/Input/Select/Select.d.ts +2 -2
  16. package/dist/esm/index.js +2 -2
  17. package/dist/esm/types/src/components/Input/DatePicker/DatePicker.d.ts +2 -1
  18. package/dist/esm/types/src/components/Input/DatePicker/components/DatePickerFilter.d.ts +2 -0
  19. package/dist/esm/types/src/components/Input/DatePicker/components/DatePickerInput.d.ts +2 -0
  20. package/dist/esm/types/src/components/Input/DateTimePicker/DateTimePicker.d.ts +2 -1
  21. package/dist/esm/types/src/components/Input/DateTimePicker/components/DateTimePickerFilter.d.ts +2 -0
  22. package/dist/esm/types/src/components/Input/DateTimePicker/components/DateTimePickerInput.d.ts +2 -0
  23. package/dist/esm/types/src/components/Input/MultiSelect/MultiSelect.d.ts +2 -0
  24. package/dist/esm/types/src/components/Input/MultiSelect/components/MultiSelectFilter.d.ts +1 -1
  25. package/dist/esm/types/src/components/Input/MultiSelect/components/MultiSelectInput.d.ts +1 -1
  26. package/dist/esm/types/src/components/Input/Select/Select.d.ts +2 -1
  27. package/dist/esm/types/src/components/Input/SingleSelect/SingleSelect.d.ts +2 -0
  28. package/dist/esm/types/src/components/Input/SingleSelect/components/SingleFilter.d.ts +1 -1
  29. package/dist/esm/types/src/components/Input/SingleSelect/components/SingleInput.d.ts +1 -1
  30. package/dist/esm/types/src/shadcn-components/Input/Select/Select.d.ts +2 -2
  31. package/dist/index.d.ts +13 -6
  32. package/dist/styles.css +372 -4
  33. package/package.json +1 -1
  34. package/src/components/DataDisplay/DataTable/components/TableView.tsx +31 -5
  35. package/src/components/Feedback/Tooltip/Tooltip.tsx +17 -3
  36. package/src/components/Input/Button/ButtonLoader.css +2 -2
  37. package/src/components/Input/DatePicker/DatePicker.tsx +9 -188
  38. package/src/components/Input/DatePicker/components/DatePickerFilter.tsx +178 -0
  39. package/src/components/Input/DatePicker/components/DatePickerInput.tsx +192 -0
  40. package/src/components/Input/DateTimePicker/DateTimePicker.tsx +8 -294
  41. package/src/components/Input/DateTimePicker/components/DateTimePickerFilter.tsx +292 -0
  42. package/src/components/Input/DateTimePicker/components/DateTimePickerInput.tsx +297 -0
  43. package/src/components/Input/MultiSelect/MultiSelect.tsx +2 -0
  44. package/src/components/Input/MultiSelect/components/MultiSelectFilter.tsx +7 -3
  45. package/src/components/Input/MultiSelect/components/MultiSelectInput.tsx +8 -3
  46. package/src/components/Input/Select/Select.tsx +22 -12
  47. package/src/components/Input/SingleSelect/SingleSelect.tsx +2 -0
  48. package/src/components/Input/SingleSelect/components/SingleFilter.tsx +7 -3
  49. package/src/components/Input/SingleSelect/components/SingleInput.tsx +8 -3
  50. package/src/components/Layout/AppLayout/components/Sidebar/MenuItem.tsx +2 -2
  51. package/src/components/Navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
  52. package/src/components/Navigation/DialogButton/DialogButton.tsx +6 -1
  53. package/src/components/Navigation/DropDownMenu/DropDownMenu.tsx +1 -1
  54. package/src/components/Navigation/TabsContainer/TabsContainer.tsx +1 -1
  55. package/src/shadcn-components/DataDisplay/Dialog/Dialog.tsx +2 -2
  56. package/src/shadcn-components/Input/Popover/Popover.tsx +1 -1
  57. package/src/shadcn-components/Input/Select/Select.tsx +8 -8
  58. package/src/shadcn-components/Navigation/DropdownMenu/DropdownMenu.tsx +2 -2
  59. package/src/styles/globals.css +4 -2
  60. package/src/styles/index.css +5 -0
@@ -87,11 +87,13 @@ const TableView = React.forwardRef<HTMLDivElement, TableViewProps>(
87
87
  index === 0 && 'pl-4 rounded-tl-md',
88
88
  index === headerGroup.headers.length - 1 &&
89
89
  'rounded-tr-md',
90
+ (header.column.columnDef.maxSize || header.column.columnDef.size) && 'overflow-hidden text-ellipsis whitespace-nowrap',
90
91
  cellClassName,
91
92
  )}
92
93
  style={{
93
- width:
94
- header.getSize() !== 150 ? header.getSize() : undefined,
94
+ width: header.column.columnDef.size ?? (header.getSize() !== 150 ? header.getSize() : undefined),
95
+ maxWidth: header.column.columnDef.maxSize ?? undefined,
96
+ minWidth: header.column.columnDef.minSize ?? undefined,
95
97
  }}
96
98
  >
97
99
  {header.isPlaceholder ? null : (
@@ -135,10 +137,19 @@ const TableView = React.forwardRef<HTMLDivElement, TableViewProps>(
135
137
  {/* Use visible header columns count to render correct number of skeleton cells */}
136
138
  {table
137
139
  .getHeaderGroups()[0]
138
- ?.headers.map((_: any, cellIndex: number) => (
140
+ ?.headers.map((header: any, cellIndex: number) => (
139
141
  <TableCell
140
142
  key={`skeleton-cell-${rowIndex}-${cellIndex}`}
141
- className={cn(cellIndex === 0 && 'pl-4', cellClassName)}
143
+ className={cn(
144
+ cellIndex === 0 && 'pl-4',
145
+ (header.column.columnDef.maxSize || header.column.columnDef.size) && 'overflow-hidden text-ellipsis whitespace-nowrap',
146
+ cellClassName
147
+ )}
148
+ style={{
149
+ width: header.column.columnDef.size ?? (header.getSize() !== 150 ? header.getSize() : undefined),
150
+ maxWidth: header.column.columnDef.maxSize ?? undefined,
151
+ minWidth: header.column.columnDef.minSize ?? undefined,
152
+ }}
142
153
  >
143
154
  <Skeleton className="h-4 w-full" />
144
155
  </TableCell>
@@ -162,7 +173,22 @@ const TableView = React.forwardRef<HTMLDivElement, TableViewProps>(
162
173
  {row.getVisibleCells().map((cell: any, index: number) => (
163
174
  <TableCell
164
175
  key={cell.id}
165
- className={cn(index === 0 && 'pl-4', cellClassName)}
176
+ className={cn(
177
+ index === 0 && 'pl-4',
178
+ (cell.column.columnDef.maxSize || cell.column.columnDef.size) && 'overflow-hidden text-ellipsis whitespace-nowrap',
179
+ cellClassName
180
+ )}
181
+ style={{
182
+ width: cell.column.columnDef.size ?? (cell.column.getSize() !== 150 ? cell.column.getSize() : undefined),
183
+ maxWidth: cell.column.columnDef.maxSize ?? undefined,
184
+ minWidth: cell.column.columnDef.minSize ?? undefined,
185
+ }}
186
+ onClick={(e) => {
187
+ // Prevent row click when clicking on checkbox column
188
+ if (cell.column.id === 'select') {
189
+ e.stopPropagation();
190
+ }
191
+ }}
166
192
  >
167
193
  {flexRender(
168
194
  cell.column.columnDef.cell,
@@ -1,6 +1,10 @@
1
- import { ReactNode } from 'react';
1
+ import { ReactNode, useEffect, useRef, useState } from 'react';
2
2
 
3
- import { Tooltip as ShadcnTooltip, TooltipContent, TooltipTrigger } from '@/shadcn-components/Feedback/Tooltip/Tooltip';
3
+ import {
4
+ Tooltip as ShadcnTooltip,
5
+ TooltipContent,
6
+ TooltipTrigger,
7
+ } from '@/shadcn-components/Feedback/Tooltip/Tooltip';
4
8
 
5
9
  export type TooltipProps = {
6
10
  children: ReactNode;
@@ -22,6 +26,14 @@ export const Tooltip = ({
22
26
  onOpenChange,
23
27
  delayDuration,
24
28
  }: TooltipProps) => {
29
+ const wrapperRef = useRef<HTMLSpanElement>(null);
30
+
31
+ // Force re-render on mount to ensure positioning works
32
+ const [, forceUpdate] = useState({});
33
+ useEffect(() => {
34
+ forceUpdate({});
35
+ }, []);
36
+
25
37
  return (
26
38
  <ShadcnTooltip
27
39
  open={open}
@@ -29,7 +41,9 @@ export const Tooltip = ({
29
41
  onOpenChange={onOpenChange}
30
42
  delayDuration={delayDuration}
31
43
  >
32
- <TooltipTrigger asChild>{children}</TooltipTrigger>
44
+ <span ref={wrapperRef}>
45
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
46
+ </span>
33
47
  <TooltipContent side={placement}>{title}</TooltipContent>
34
48
  </ShadcnTooltip>
35
49
  );
@@ -4,9 +4,9 @@
4
4
  padding: 10px 20px;
5
5
  font-size: '14px';
6
6
  aspect-ratio: 2.5;
7
- --_g: no-repeat radial-gradient(farthest-side, #fff 90%, #fff);
7
+ --_g: no-repeat radial-gradient(circle, currentColor 48%, #0000);
8
8
  background: var(--_g), var(--_g), var(--_g), var(--_g);
9
- background-size: 20% 50%;
9
+ background-size: 15% 40%;
10
10
  animation: l44 1s infinite linear alternate;
11
11
  scale: 0.5;
12
12
  }
@@ -1,16 +1,5 @@
1
- import { cn } from '@/lib/utils';
2
- import { Calendar } from '@/shadcn-components/Input/Calendar/Calendar';
3
- import {
4
- Popover,
5
- PopoverContent,
6
- PopoverTrigger,
7
- } from '@/shadcn-components/Input/Popover/Popover';
8
- import { format as DateFnsFormat } from 'date-fns';
9
- import { CalendarDays } from 'lucide-react';
10
- import { cloneElement, useEffect, useRef, useState } from 'react';
11
- import { Typography } from '../../DataDisplay/Typography/Typography';
12
- import { Button } from '../Button/Button';
13
- import { LabelWrapper } from '../LabelWrapper/LabelWrapper';
1
+ import { DatePickerFilter } from './components/DatePickerFilter';
2
+ import { DatePickerInput } from './components/DatePickerInput';
14
3
 
15
4
  type DateFormat = 'yyyy' | 'MMMM yyyy' | 'dd MMMM yyyy' | 'dd/MM/yyyy';
16
5
 
@@ -40,185 +29,17 @@ export type DatePickerProps = {
40
29
  onClose?: () => void;
41
30
  onBlur?: React.FocusEventHandler;
42
31
  fullWidth?: boolean;
32
+ type?: 'input' | 'filter';
43
33
  [key: string]: any;
44
34
  };
45
35
 
46
36
  export const DatePicker = ({
47
- label,
48
- name,
49
- value,
50
- onChange,
51
- required = false,
52
- format = 'dd/MM/yyyy',
53
- helperText,
54
- placeholder = 'Pick a date',
55
- shortcutsItems = [],
56
- openPickerIcon: Icon = <CalendarDays />,
57
- containerProps,
58
- error,
59
- disabled = false,
60
- minDate,
61
- maxDate,
62
- disablePast = false,
63
- disableFuture = false,
64
- shouldDisableDate,
65
- shouldDisableMonth,
66
- shouldDisableYear,
67
- className,
68
- onOpen,
69
- onClose,
70
- fullWidth,
71
- ...rest
37
+ type = 'input',
38
+ ...props
72
39
  }: DatePickerProps) => {
73
- const [open, setOpen] = useState(false);
74
- const [date, setDate] = useState<Date | undefined>(value);
75
- const wrapperRef = useRef<HTMLSpanElement>(null);
40
+ if (type === 'filter') {
41
+ return <DatePickerFilter {...props} />;
42
+ }
76
43
 
77
- const formatDateString = (date: Date | undefined) => {
78
- if (!date) return '';
79
-
80
- try {
81
- return DateFnsFormat(date, format);
82
- } catch {
83
- return DateFnsFormat(date, 'dd/MM/yyyy');
84
- }
85
- };
86
-
87
- const handleOpenChange = (newOpen: boolean) => {
88
- setOpen(newOpen);
89
- if (newOpen && onOpen) {
90
- onOpen();
91
- } else if (!newOpen && onClose) {
92
- onClose();
93
- }
94
- };
95
-
96
- const [, forceUpdate] = useState({});
97
- useEffect(() => {
98
- forceUpdate({});
99
- }, []);
100
-
101
- return (
102
- <LabelWrapper
103
- label={label}
104
- required={required}
105
- name={name}
106
- containerProps={{
107
- ...(fullWidth && { style: { width: '100%' } }),
108
- ...containerProps,
109
- }}
110
- >
111
- <Popover open={open} onOpenChange={handleOpenChange}>
112
- <PopoverTrigger asChild>
113
- <span
114
- ref={wrapperRef}
115
- style={{ display: fullWidth ? 'block' : 'inline-block' }}
116
- >
117
- <Button
118
- variant="input"
119
- className={cn(
120
- 'justify-between text-left font-normal',
121
- !date && 'text-muted-foreground',
122
- error && 'border-destructive',
123
- fullWidth ? 'w-full' : 'w-auto',
124
- className,
125
- )}
126
- disabled={disabled}
127
- {...rest}
128
- >
129
- <span>{date ? formatDateString(date) : placeholder}</span>
130
- {Icon &&
131
- cloneElement(Icon as React.ReactElement, {
132
- className: 'ml-2 h-4 w-4',
133
- })}
134
- </Button>
135
- </span>
136
- </PopoverTrigger>
137
- <PopoverContent className="w-auto p-0" align="start">
138
- <div className="flex flex-col">
139
- {/* Shortcuts */}
140
- {shortcutsItems.length > 0 && (
141
- <div className="border-b p-2">
142
- <div className="flex flex-wrap gap-1">
143
- {shortcutsItems.map((shortcut, index) => (
144
- <Button
145
- key={index}
146
- variant="outline"
147
- size="sm"
148
- onClick={() => {
149
- const newDate = shortcut.getValue();
150
- onChange?.(newDate);
151
- setDate(newDate);
152
- setOpen(false);
153
- }}
154
- >
155
- {shortcut.label}
156
- </Button>
157
- ))}
158
- </div>
159
- </div>
160
- )}
161
-
162
- {/* Calendar */}
163
- <Calendar
164
- mode="single"
165
- selected={date}
166
- onSelect={(date) => {
167
- onChange?.(date);
168
- setDate(date);
169
- }}
170
- disabled={(date) => {
171
- // Start of today for comparison
172
- const today = new Date();
173
- today.setHours(0, 0, 0, 0);
174
- const compareDate = new Date(date);
175
- compareDate.setHours(0, 0, 0, 0);
176
-
177
- // Check min/max dates
178
- if (minDate) {
179
- const min = new Date(minDate);
180
- min.setHours(0, 0, 0, 0);
181
- if (compareDate < min) return true;
182
- }
183
- if (maxDate) {
184
- const max = new Date(maxDate);
185
- max.setHours(0, 0, 0, 0);
186
- if (compareDate > max) return true;
187
- }
188
-
189
- // Check disablePast
190
- if (disablePast && compareDate < today) return true;
191
-
192
- // Check disableFuture
193
- if (disableFuture && compareDate > today) return true;
194
-
195
- // Check custom date disable function
196
- if (shouldDisableDate && shouldDisableDate(date)) return true;
197
-
198
- // Check month disable function
199
- if (shouldDisableMonth && shouldDisableMonth(date)) return true;
200
-
201
- // Check year disable function
202
- if (shouldDisableYear && shouldDisableYear(date)) return true;
203
-
204
- return false;
205
- }}
206
- captionLayout={'dropdown'}
207
- defaultMonth={date}
208
- />
209
- </div>
210
- </PopoverContent>
211
- </Popover>
212
-
213
- {/* Helper Text / Error */}
214
- {(helperText || error) && (
215
- <Typography
216
- variant="small"
217
- className={cn('ml-1 mt-1', error && 'text-destructive')}
218
- >
219
- {typeof error === 'string' ? error : error?.message || helperText}
220
- </Typography>
221
- )}
222
- </LabelWrapper>
223
- );
44
+ return <DatePickerInput {...props} />;
224
45
  };
@@ -0,0 +1,178 @@
1
+ import { Separator } from '@/components/DataDisplay/Separator/Separator';
2
+ import { cn } from '@/lib/utils';
3
+ import { Calendar } from '@/shadcn-components/Input/Calendar/Calendar';
4
+ import {
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from '@/shadcn-components/Input/Popover/Popover';
9
+ import { format as DateFnsFormat } from 'date-fns';
10
+ import { CalendarDays } from 'lucide-react';
11
+ import { useEffect, useRef, useState } from 'react';
12
+ import { Button } from '../../Button/Button';
13
+ import { DatePickerProps } from '../DatePicker';
14
+
15
+ export const DatePickerFilter = ({
16
+ label,
17
+ value,
18
+ onChange,
19
+ format = 'dd/MM/yyyy',
20
+ placeholder = 'Pick a date',
21
+ shortcutsItems = [],
22
+ openPickerIcon: Icon = <CalendarDays />,
23
+ disabled = false,
24
+ minDate,
25
+ maxDate,
26
+ disablePast = false,
27
+ disableFuture = false,
28
+ shouldDisableDate,
29
+ shouldDisableMonth,
30
+ shouldDisableYear,
31
+ className,
32
+ onOpen,
33
+ onClose,
34
+ fullWidth,
35
+ type,
36
+ ...rest
37
+ }: DatePickerProps) => {
38
+ const [open, setOpen] = useState(false);
39
+ const wrapperRef = useRef<HTMLSpanElement>(null);
40
+
41
+ const formatDateString = (date: Date | undefined) => {
42
+ if (!date) return '';
43
+
44
+ try {
45
+ return DateFnsFormat(date, format);
46
+ } catch {
47
+ return DateFnsFormat(date, 'dd/MM/yyyy');
48
+ }
49
+ };
50
+
51
+ const handleOpenChange = (newOpen: boolean) => {
52
+ setOpen(newOpen);
53
+ if (newOpen && onOpen) {
54
+ onOpen();
55
+ } else if (!newOpen && onClose) {
56
+ onClose();
57
+ }
58
+ };
59
+
60
+ const handleClear = () => {
61
+ onChange?.(undefined);
62
+ setOpen(false);
63
+ };
64
+
65
+ // Force re-render on mount to ensure positioning works
66
+ const [, forceUpdate] = useState({});
67
+ useEffect(() => {
68
+ forceUpdate({});
69
+ }, []);
70
+
71
+ const hasValue = !!value;
72
+ const displayText = hasValue ? `${label} = ${formatDateString(value)}` : label;
73
+
74
+ return (
75
+ <Popover open={open} onOpenChange={handleOpenChange}>
76
+ <PopoverTrigger asChild>
77
+ <span
78
+ ref={wrapperRef}
79
+ style={{ display: fullWidth ? 'block' : 'inline-block' }}
80
+ >
81
+ <Button
82
+ variant={hasValue ? 'default' : 'input'}
83
+ className={cn(
84
+ 'justify-between',
85
+ fullWidth ? 'w-full' : '',
86
+ className,
87
+ )}
88
+ disabled={disabled}
89
+ {...rest}
90
+ >
91
+ {displayText || placeholder}
92
+ <CalendarDays className="ml-2 h-4 w-4 shrink-0 opacity-50" />
93
+ </Button>
94
+ </span>
95
+ </PopoverTrigger>
96
+ <PopoverContent className="w-auto p-0" align="start">
97
+ <div className="flex flex-col">
98
+ {/* Shortcuts */}
99
+ {shortcutsItems.length > 0 && (
100
+ <div className="border-b p-2">
101
+ <div className="flex flex-wrap gap-1">
102
+ {shortcutsItems.map((shortcut, index) => (
103
+ <Button
104
+ key={index}
105
+ variant="outline"
106
+ size="sm"
107
+ onClick={() => {
108
+ const newDate = shortcut.getValue();
109
+ onChange?.(newDate);
110
+ setOpen(false);
111
+ }}
112
+ >
113
+ {shortcut.label}
114
+ </Button>
115
+ ))}
116
+ </div>
117
+ </div>
118
+ )}
119
+
120
+ {/* Calendar */}
121
+ <Calendar
122
+ mode="single"
123
+ selected={value}
124
+ onSelect={(date) => {
125
+ onChange?.(date);
126
+ }}
127
+ disabled={(date) => {
128
+ // Start of today for comparison
129
+ const today = new Date();
130
+ today.setHours(0, 0, 0, 0);
131
+ const compareDate = new Date(date);
132
+ compareDate.setHours(0, 0, 0, 0);
133
+
134
+ // Check min/max dates
135
+ if (minDate) {
136
+ const min = new Date(minDate);
137
+ min.setHours(0, 0, 0, 0);
138
+ if (compareDate < min) return true;
139
+ }
140
+ if (maxDate) {
141
+ const max = new Date(maxDate);
142
+ max.setHours(0, 0, 0, 0);
143
+ if (compareDate > max) return true;
144
+ }
145
+
146
+ // Check disablePast
147
+ if (disablePast && compareDate < today) return true;
148
+
149
+ // Check disableFuture
150
+ if (disableFuture && compareDate > today) return true;
151
+
152
+ // Check custom date disable function
153
+ if (shouldDisableDate && shouldDisableDate(date)) return true;
154
+
155
+ // Check month disable function
156
+ if (shouldDisableMonth && shouldDisableMonth(date)) return true;
157
+
158
+ // Check year disable function
159
+ if (shouldDisableYear && shouldDisableYear(date)) return true;
160
+
161
+ return false;
162
+ }}
163
+ captionLayout={'dropdown'}
164
+ defaultMonth={value}
165
+ />
166
+
167
+ {/* Clear Button */}
168
+ <Separator />
169
+ <div className="flex flex-row items-center justify-left">
170
+ <Button variant="link" onClick={handleClear} className="min-w-0">
171
+ Clear
172
+ </Button>
173
+ </div>
174
+ </div>
175
+ </PopoverContent>
176
+ </Popover>
177
+ );
178
+ };
@@ -0,0 +1,192 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { Calendar } from '@/shadcn-components/Input/Calendar/Calendar';
3
+ import {
4
+ Popover,
5
+ PopoverContent,
6
+ PopoverTrigger,
7
+ } from '@/shadcn-components/Input/Popover/Popover';
8
+ import { format as DateFnsFormat } from 'date-fns';
9
+ import { CalendarDays } from 'lucide-react';
10
+ import { cloneElement, useEffect, useRef, useState } from 'react';
11
+ import { Typography } from '../../../DataDisplay/Typography/Typography';
12
+ import { Button } from '../../Button/Button';
13
+ import { LabelWrapper } from '../../LabelWrapper/LabelWrapper';
14
+ import { DatePickerProps } from '../DatePicker';
15
+
16
+ export const DatePickerInput = ({
17
+ label,
18
+ name,
19
+ value,
20
+ onChange,
21
+ required = false,
22
+ format = 'dd/MM/yyyy',
23
+ helperText,
24
+ placeholder = 'Pick a date',
25
+ shortcutsItems = [],
26
+ openPickerIcon: Icon = <CalendarDays />,
27
+ containerProps,
28
+ error,
29
+ disabled = false,
30
+ minDate,
31
+ maxDate,
32
+ disablePast = false,
33
+ disableFuture = false,
34
+ shouldDisableDate,
35
+ shouldDisableMonth,
36
+ shouldDisableYear,
37
+ className,
38
+ onOpen,
39
+ onClose,
40
+ fullWidth,
41
+ type,
42
+ ...rest
43
+ }: DatePickerProps) => {
44
+ const [open, setOpen] = useState(false);
45
+ const wrapperRef = useRef<HTMLSpanElement>(null);
46
+
47
+ const formatDateString = (date: Date | undefined) => {
48
+ if (!date) return '';
49
+
50
+ try {
51
+ return DateFnsFormat(date, format);
52
+ } catch {
53
+ return DateFnsFormat(date, 'dd/MM/yyyy');
54
+ }
55
+ };
56
+
57
+ const handleOpenChange = (newOpen: boolean) => {
58
+ setOpen(newOpen);
59
+ if (newOpen && onOpen) {
60
+ onOpen();
61
+ } else if (!newOpen && onClose) {
62
+ onClose();
63
+ }
64
+ };
65
+
66
+ const [, forceUpdate] = useState({});
67
+ useEffect(() => {
68
+ forceUpdate({});
69
+ }, []);
70
+
71
+ return (
72
+ <LabelWrapper
73
+ label={label}
74
+ required={required}
75
+ name={name}
76
+ containerProps={{
77
+ ...(fullWidth && { style: { width: '100%' } }),
78
+ ...containerProps,
79
+ }}
80
+ >
81
+ <Popover open={open} onOpenChange={handleOpenChange}>
82
+ <PopoverTrigger asChild>
83
+ <span
84
+ ref={wrapperRef}
85
+ style={{ display: fullWidth ? 'block' : 'inline-block' }}
86
+ >
87
+ <Button
88
+ variant="input"
89
+ className={cn(
90
+ 'justify-between text-left font-normal',
91
+ !value && 'text-muted-foreground',
92
+ error && 'border-destructive',
93
+ fullWidth ? 'w-full' : 'w-auto',
94
+ className,
95
+ )}
96
+ disabled={disabled}
97
+ {...rest}
98
+ >
99
+ <span>{value ? formatDateString(value) : placeholder}</span>
100
+ {Icon &&
101
+ cloneElement(Icon as React.ReactElement, {
102
+ className: 'ml-2 h-4 w-4',
103
+ })}
104
+ </Button>
105
+ </span>
106
+ </PopoverTrigger>
107
+ <PopoverContent className="w-auto p-0" align="start">
108
+ <div className="flex flex-col">
109
+ {/* Shortcuts */}
110
+ {shortcutsItems.length > 0 && (
111
+ <div className="border-b p-2">
112
+ <div className="flex flex-wrap gap-1">
113
+ {shortcutsItems.map((shortcut, index) => (
114
+ <Button
115
+ key={index}
116
+ variant="outline"
117
+ size="sm"
118
+ onClick={() => {
119
+ const newDate = shortcut.getValue();
120
+ onChange?.(newDate);
121
+ setOpen(false);
122
+ }}
123
+ >
124
+ {shortcut.label}
125
+ </Button>
126
+ ))}
127
+ </div>
128
+ </div>
129
+ )}
130
+
131
+ {/* Calendar */}
132
+ <Calendar
133
+ mode="single"
134
+ selected={value}
135
+ onSelect={(date) => {
136
+ onChange?.(date);
137
+ }}
138
+ disabled={(date) => {
139
+ // Start of today for comparison
140
+ const today = new Date();
141
+ today.setHours(0, 0, 0, 0);
142
+ const compareDate = new Date(date);
143
+ compareDate.setHours(0, 0, 0, 0);
144
+
145
+ // Check min/max dates
146
+ if (minDate) {
147
+ const min = new Date(minDate);
148
+ min.setHours(0, 0, 0, 0);
149
+ if (compareDate < min) return true;
150
+ }
151
+ if (maxDate) {
152
+ const max = new Date(maxDate);
153
+ max.setHours(0, 0, 0, 0);
154
+ if (compareDate > max) return true;
155
+ }
156
+
157
+ // Check disablePast
158
+ if (disablePast && compareDate < today) return true;
159
+
160
+ // Check disableFuture
161
+ if (disableFuture && compareDate > today) return true;
162
+
163
+ // Check custom date disable function
164
+ if (shouldDisableDate && shouldDisableDate(date)) return true;
165
+
166
+ // Check month disable function
167
+ if (shouldDisableMonth && shouldDisableMonth(date)) return true;
168
+
169
+ // Check year disable function
170
+ if (shouldDisableYear && shouldDisableYear(date)) return true;
171
+
172
+ return false;
173
+ }}
174
+ captionLayout={'dropdown'}
175
+ defaultMonth={value}
176
+ />
177
+ </div>
178
+ </PopoverContent>
179
+ </Popover>
180
+
181
+ {/* Helper Text / Error */}
182
+ {(helperText || error) && (
183
+ <Typography
184
+ variant="small"
185
+ className={cn('ml-1 mt-1', error && 'text-destructive')}
186
+ >
187
+ {typeof error === 'string' ? error : error?.message || helperText}
188
+ </Typography>
189
+ )}
190
+ </LabelWrapper>
191
+ );
192
+ };