@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.
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/types/src/components/Input/DatePicker/DatePicker.d.ts +2 -1
- package/dist/cjs/types/src/components/Input/DatePicker/components/DatePickerFilter.d.ts +2 -0
- package/dist/cjs/types/src/components/Input/DatePicker/components/DatePickerInput.d.ts +2 -0
- package/dist/cjs/types/src/components/Input/DateTimePicker/DateTimePicker.d.ts +2 -1
- package/dist/cjs/types/src/components/Input/DateTimePicker/components/DateTimePickerFilter.d.ts +2 -0
- package/dist/cjs/types/src/components/Input/DateTimePicker/components/DateTimePickerInput.d.ts +2 -0
- package/dist/cjs/types/src/components/Input/MultiSelect/MultiSelect.d.ts +2 -0
- package/dist/cjs/types/src/components/Input/MultiSelect/components/MultiSelectFilter.d.ts +1 -1
- package/dist/cjs/types/src/components/Input/MultiSelect/components/MultiSelectInput.d.ts +1 -1
- package/dist/cjs/types/src/components/Input/Select/Select.d.ts +2 -1
- package/dist/cjs/types/src/components/Input/SingleSelect/SingleSelect.d.ts +2 -0
- package/dist/cjs/types/src/components/Input/SingleSelect/components/SingleFilter.d.ts +1 -1
- package/dist/cjs/types/src/components/Input/SingleSelect/components/SingleInput.d.ts +1 -1
- package/dist/cjs/types/src/shadcn-components/Input/Select/Select.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/types/src/components/Input/DatePicker/DatePicker.d.ts +2 -1
- package/dist/esm/types/src/components/Input/DatePicker/components/DatePickerFilter.d.ts +2 -0
- package/dist/esm/types/src/components/Input/DatePicker/components/DatePickerInput.d.ts +2 -0
- package/dist/esm/types/src/components/Input/DateTimePicker/DateTimePicker.d.ts +2 -1
- package/dist/esm/types/src/components/Input/DateTimePicker/components/DateTimePickerFilter.d.ts +2 -0
- package/dist/esm/types/src/components/Input/DateTimePicker/components/DateTimePickerInput.d.ts +2 -0
- package/dist/esm/types/src/components/Input/MultiSelect/MultiSelect.d.ts +2 -0
- package/dist/esm/types/src/components/Input/MultiSelect/components/MultiSelectFilter.d.ts +1 -1
- package/dist/esm/types/src/components/Input/MultiSelect/components/MultiSelectInput.d.ts +1 -1
- package/dist/esm/types/src/components/Input/Select/Select.d.ts +2 -1
- package/dist/esm/types/src/components/Input/SingleSelect/SingleSelect.d.ts +2 -0
- package/dist/esm/types/src/components/Input/SingleSelect/components/SingleFilter.d.ts +1 -1
- package/dist/esm/types/src/components/Input/SingleSelect/components/SingleInput.d.ts +1 -1
- package/dist/esm/types/src/shadcn-components/Input/Select/Select.d.ts +2 -2
- package/dist/index.d.ts +13 -6
- package/dist/styles.css +372 -4
- package/package.json +1 -1
- package/src/components/DataDisplay/DataTable/components/TableView.tsx +31 -5
- package/src/components/Feedback/Tooltip/Tooltip.tsx +17 -3
- package/src/components/Input/Button/ButtonLoader.css +2 -2
- package/src/components/Input/DatePicker/DatePicker.tsx +9 -188
- package/src/components/Input/DatePicker/components/DatePickerFilter.tsx +178 -0
- package/src/components/Input/DatePicker/components/DatePickerInput.tsx +192 -0
- package/src/components/Input/DateTimePicker/DateTimePicker.tsx +8 -294
- package/src/components/Input/DateTimePicker/components/DateTimePickerFilter.tsx +292 -0
- package/src/components/Input/DateTimePicker/components/DateTimePickerInput.tsx +297 -0
- package/src/components/Input/MultiSelect/MultiSelect.tsx +2 -0
- package/src/components/Input/MultiSelect/components/MultiSelectFilter.tsx +7 -3
- package/src/components/Input/MultiSelect/components/MultiSelectInput.tsx +8 -3
- package/src/components/Input/Select/Select.tsx +22 -12
- package/src/components/Input/SingleSelect/SingleSelect.tsx +2 -0
- package/src/components/Input/SingleSelect/components/SingleFilter.tsx +7 -3
- package/src/components/Input/SingleSelect/components/SingleInput.tsx +8 -3
- package/src/components/Layout/AppLayout/components/Sidebar/MenuItem.tsx +2 -2
- package/src/components/Navigation/Breadcrumbs/Breadcrumbs.tsx +1 -1
- package/src/components/Navigation/DialogButton/DialogButton.tsx +6 -1
- package/src/components/Navigation/DropDownMenu/DropDownMenu.tsx +1 -1
- package/src/components/Navigation/TabsContainer/TabsContainer.tsx +1 -1
- package/src/shadcn-components/DataDisplay/Dialog/Dialog.tsx +2 -2
- package/src/shadcn-components/Input/Popover/Popover.tsx +1 -1
- package/src/shadcn-components/Input/Select/Select.tsx +8 -8
- package/src/shadcn-components/Navigation/DropdownMenu/DropdownMenu.tsx +2 -2
- package/src/styles/globals.css +4 -2
- 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
|
-
|
|
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((
|
|
140
|
+
?.headers.map((header: any, cellIndex: number) => (
|
|
139
141
|
<TableCell
|
|
140
142
|
key={`skeleton-cell-${rowIndex}-${cellIndex}`}
|
|
141
|
-
className={cn(
|
|
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(
|
|
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 {
|
|
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
|
-
<
|
|
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(
|
|
7
|
+
--_g: no-repeat radial-gradient(circle, currentColor 48%, #0000);
|
|
8
8
|
background: var(--_g), var(--_g), var(--_g), var(--_g);
|
|
9
|
-
background-size:
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
40
|
+
if (type === 'filter') {
|
|
41
|
+
return <DatePickerFilter {...props} />;
|
|
42
|
+
}
|
|
76
43
|
|
|
77
|
-
|
|
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
|
+
};
|