@campxdev/react-blueprint 3.0.0-alpha.4 → 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 +12 -5
- 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/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
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Input } from '@/shadcn-components/Input/Input/Input';
|
|
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 { cloneElement, useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
-
import { Typography } from '../../DataDisplay/Typography/Typography';
|
|
13
|
-
import { Button } from '../Button/Button';
|
|
14
|
-
import { LabelWrapper } from '../LabelWrapper/LabelWrapper';
|
|
1
|
+
import { DateTimePickerFilter } from './components/DateTimePickerFilter';
|
|
2
|
+
import { DateTimePickerInput } from './components/DateTimePickerInput';
|
|
15
3
|
|
|
16
4
|
type DateTimePickerViews =
|
|
17
5
|
| 'year'
|
|
@@ -69,291 +57,17 @@ export type DateTimePickerProps = {
|
|
|
69
57
|
onClose?: () => void;
|
|
70
58
|
onBlur?: React.FocusEventHandler;
|
|
71
59
|
fullWidth?: boolean;
|
|
60
|
+
type?: 'input' | 'filter';
|
|
72
61
|
[key: string]: any;
|
|
73
62
|
};
|
|
74
63
|
|
|
75
64
|
export const DateTimePicker = ({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
value,
|
|
79
|
-
onChange,
|
|
80
|
-
required = false,
|
|
81
|
-
format = 'dd MMM yyyy hh:mm a',
|
|
82
|
-
views = ['year', 'month', 'day', 'hours', 'minutes', 'seconds'],
|
|
83
|
-
helperText,
|
|
84
|
-
placeholder = 'Pick a date and time',
|
|
85
|
-
shortcutsItems = [],
|
|
86
|
-
openPickerIcon: Icon = <CalendarDays />,
|
|
87
|
-
containerProps,
|
|
88
|
-
error,
|
|
89
|
-
disabled = false,
|
|
90
|
-
minDate,
|
|
91
|
-
maxDate,
|
|
92
|
-
disablePast = false,
|
|
93
|
-
disableFuture = false,
|
|
94
|
-
shouldDisableDate,
|
|
95
|
-
shouldDisableMonth,
|
|
96
|
-
shouldDisableYear,
|
|
97
|
-
className,
|
|
98
|
-
onOpen,
|
|
99
|
-
onClose,
|
|
100
|
-
fullWidth,
|
|
101
|
-
...rest
|
|
65
|
+
type = 'input',
|
|
66
|
+
...props
|
|
102
67
|
}: DateTimePickerProps) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const [timeValue, setTimeValue] = useState<string>('');
|
|
106
|
-
const wrapperRef = useRef<HTMLSpanElement>(null);
|
|
107
|
-
|
|
108
|
-
// Determine view configurations based on views prop
|
|
109
|
-
const viewConfig = useMemo(() => {
|
|
110
|
-
const hasTime =
|
|
111
|
-
views.includes('hours') ||
|
|
112
|
-
views.includes('minutes') ||
|
|
113
|
-
views.includes('seconds');
|
|
114
|
-
const hasDate =
|
|
115
|
-
views.includes('day') ||
|
|
116
|
-
views.includes('month') ||
|
|
117
|
-
views.includes('year');
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
showTime: hasTime,
|
|
121
|
-
showDate: hasDate,
|
|
122
|
-
};
|
|
123
|
-
}, [views]);
|
|
124
|
-
|
|
125
|
-
// Update time value when dateTime changes
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
if (dateTime && viewConfig.showTime) {
|
|
128
|
-
const hours = dateTime.getHours(),
|
|
129
|
-
minutes = dateTime.getMinutes(),
|
|
130
|
-
seconds = dateTime.getSeconds(),
|
|
131
|
-
timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(
|
|
132
|
-
2,
|
|
133
|
-
'0',
|
|
134
|
-
)}:${String(seconds).padStart(2, '0')}`;
|
|
135
|
-
setTimeValue(timeStr);
|
|
136
|
-
}
|
|
137
|
-
}, [dateTime, viewConfig.showTime]);
|
|
138
|
-
|
|
139
|
-
const formatDateTimeString = (date: Date | undefined) => {
|
|
140
|
-
if (!date) return '';
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
return DateFnsFormat(date, format);
|
|
144
|
-
} catch {
|
|
145
|
-
return DateFnsFormat(date, 'dd/MM/yyyy hh:mm a');
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const handleOpenChange = (newOpen: boolean) => {
|
|
150
|
-
setOpen(newOpen);
|
|
151
|
-
if (newOpen && onOpen) {
|
|
152
|
-
onOpen();
|
|
153
|
-
} else if (!newOpen && onClose) {
|
|
154
|
-
onClose();
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const handleDateSelect = (date: Date | undefined) => {
|
|
159
|
-
if (date) {
|
|
160
|
-
const newDateTime = new Date(date);
|
|
161
|
-
// Preserve existing time if we have one
|
|
162
|
-
if (dateTime) {
|
|
163
|
-
newDateTime.setHours(
|
|
164
|
-
dateTime.getHours(),
|
|
165
|
-
dateTime.getMinutes(),
|
|
166
|
-
dateTime.getSeconds(),
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
setDateTime(newDateTime);
|
|
170
|
-
onChange?.(newDateTime);
|
|
171
|
-
} else {
|
|
172
|
-
setDateTime(undefined);
|
|
173
|
-
onChange?.(undefined);
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
178
|
-
const timeStr = e.target.value;
|
|
179
|
-
setTimeValue(timeStr);
|
|
180
|
-
|
|
181
|
-
if (timeStr) {
|
|
182
|
-
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
|
183
|
-
const newDateTime = dateTime ? new Date(dateTime) : new Date();
|
|
184
|
-
newDateTime.setHours(hours || 0, minutes || 0, seconds || 0);
|
|
185
|
-
setDateTime(newDateTime);
|
|
186
|
-
onChange?.(newDateTime);
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const [, forceUpdate] = useState({});
|
|
191
|
-
useEffect(() => {
|
|
192
|
-
forceUpdate({});
|
|
193
|
-
}, []);
|
|
194
|
-
|
|
195
|
-
// If only time selection (no date), render native time input directly
|
|
196
|
-
if (viewConfig.showTime && !viewConfig.showDate) {
|
|
197
|
-
return (
|
|
198
|
-
<LabelWrapper
|
|
199
|
-
label={label}
|
|
200
|
-
required={required}
|
|
201
|
-
name={name}
|
|
202
|
-
containerProps={containerProps}
|
|
203
|
-
>
|
|
204
|
-
<Input
|
|
205
|
-
type="time"
|
|
206
|
-
step="1"
|
|
207
|
-
value={timeValue}
|
|
208
|
-
onChange={handleTimeChange}
|
|
209
|
-
disabled={disabled}
|
|
210
|
-
placeholder={placeholder}
|
|
211
|
-
className={cn(
|
|
212
|
-
'bg-input-background border-none text-muted-foreground',
|
|
213
|
-
error && 'border-destructive',
|
|
214
|
-
className,
|
|
215
|
-
)}
|
|
216
|
-
{...rest}
|
|
217
|
-
/>
|
|
218
|
-
|
|
219
|
-
{/* Helper Text / Error */}
|
|
220
|
-
{(helperText || error) && (
|
|
221
|
-
<Typography
|
|
222
|
-
variant="small"
|
|
223
|
-
className={cn('ml-1 mt-1', error && 'text-destructive')}
|
|
224
|
-
>
|
|
225
|
-
{typeof error === 'string' ? error : error?.message || helperText}
|
|
226
|
-
</Typography>
|
|
227
|
-
)}
|
|
228
|
-
</LabelWrapper>
|
|
229
|
-
);
|
|
68
|
+
if (type === 'filter') {
|
|
69
|
+
return <DateTimePickerFilter {...props} />;
|
|
230
70
|
}
|
|
231
71
|
|
|
232
|
-
return
|
|
233
|
-
<LabelWrapper
|
|
234
|
-
label={label}
|
|
235
|
-
required={required}
|
|
236
|
-
name={name}
|
|
237
|
-
containerProps={{
|
|
238
|
-
...(fullWidth && { style: { width: '100%' } }),
|
|
239
|
-
...containerProps,
|
|
240
|
-
}}
|
|
241
|
-
>
|
|
242
|
-
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
243
|
-
<PopoverTrigger asChild>
|
|
244
|
-
<span
|
|
245
|
-
ref={wrapperRef}
|
|
246
|
-
style={{ display: fullWidth ? 'block' : 'inline-block' }}
|
|
247
|
-
>
|
|
248
|
-
<Button
|
|
249
|
-
variant="input"
|
|
250
|
-
className={cn(
|
|
251
|
-
'w-full justify-between text-left font-normal',
|
|
252
|
-
!dateTime && 'text-muted-foreground',
|
|
253
|
-
error && 'border-destructive',
|
|
254
|
-
fullWidth ? 'w-full' : 'w-auto',
|
|
255
|
-
className,
|
|
256
|
-
)}
|
|
257
|
-
disabled={disabled}
|
|
258
|
-
{...rest}
|
|
259
|
-
>
|
|
260
|
-
<span>
|
|
261
|
-
{dateTime ? formatDateTimeString(dateTime) : placeholder}
|
|
262
|
-
</span>
|
|
263
|
-
{Icon &&
|
|
264
|
-
cloneElement(Icon as React.ReactElement, {
|
|
265
|
-
className: 'ml-2 h-4 w-4 ',
|
|
266
|
-
})}
|
|
267
|
-
</Button>
|
|
268
|
-
</span>
|
|
269
|
-
</PopoverTrigger>
|
|
270
|
-
<PopoverContent className="w-auto p-0" align="start">
|
|
271
|
-
<div className="flex flex-col">
|
|
272
|
-
{/* Shortcuts */}
|
|
273
|
-
{shortcutsItems.length > 0 && (
|
|
274
|
-
<div className="border-b p-2">
|
|
275
|
-
<div className="flex flex-wrap gap-1">
|
|
276
|
-
{shortcutsItems.map((shortcut, index) => (
|
|
277
|
-
<Button
|
|
278
|
-
key={index}
|
|
279
|
-
variant="outline"
|
|
280
|
-
size="sm"
|
|
281
|
-
onClick={() => {
|
|
282
|
-
const newDate = shortcut.getValue();
|
|
283
|
-
onChange?.(newDate);
|
|
284
|
-
setDateTime(newDate);
|
|
285
|
-
setOpen(false);
|
|
286
|
-
}}
|
|
287
|
-
>
|
|
288
|
-
{shortcut.label}
|
|
289
|
-
</Button>
|
|
290
|
-
))}
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
)}
|
|
294
|
-
|
|
295
|
-
{/* Calendar */}
|
|
296
|
-
{viewConfig.showDate && (
|
|
297
|
-
<Calendar
|
|
298
|
-
mode="single"
|
|
299
|
-
selected={dateTime}
|
|
300
|
-
onSelect={handleDateSelect}
|
|
301
|
-
disabled={(date) => {
|
|
302
|
-
const today = new Date();
|
|
303
|
-
today.setHours(0, 0, 0, 0);
|
|
304
|
-
const compareDate = new Date(date);
|
|
305
|
-
compareDate.setHours(0, 0, 0, 0);
|
|
306
|
-
|
|
307
|
-
if (minDate) {
|
|
308
|
-
const min = new Date(minDate);
|
|
309
|
-
min.setHours(0, 0, 0, 0);
|
|
310
|
-
if (compareDate < min) return true;
|
|
311
|
-
}
|
|
312
|
-
if (maxDate) {
|
|
313
|
-
const max = new Date(maxDate);
|
|
314
|
-
max.setHours(0, 0, 0, 0);
|
|
315
|
-
if (compareDate > max) return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (disablePast && compareDate < today) return true;
|
|
319
|
-
if (disableFuture && compareDate > today) return true;
|
|
320
|
-
if (shouldDisableDate && shouldDisableDate(date)) return true;
|
|
321
|
-
if (shouldDisableMonth && shouldDisableMonth(date))
|
|
322
|
-
return true;
|
|
323
|
-
if (shouldDisableYear && shouldDisableYear(date)) return true;
|
|
324
|
-
|
|
325
|
-
return false;
|
|
326
|
-
}}
|
|
327
|
-
captionLayout={'dropdown'}
|
|
328
|
-
defaultMonth={dateTime}
|
|
329
|
-
/>
|
|
330
|
-
)}
|
|
331
|
-
|
|
332
|
-
{/* Time Picker */}
|
|
333
|
-
{viewConfig.showTime && (
|
|
334
|
-
<div className={cn('p-3', viewConfig.showDate && 'border-t')}>
|
|
335
|
-
<Input
|
|
336
|
-
type="time"
|
|
337
|
-
step="1"
|
|
338
|
-
value={timeValue}
|
|
339
|
-
onChange={handleTimeChange}
|
|
340
|
-
className="bg-input-background border-none text-muted-foreground appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
|
341
|
-
/>
|
|
342
|
-
</div>
|
|
343
|
-
)}
|
|
344
|
-
</div>
|
|
345
|
-
</PopoverContent>
|
|
346
|
-
</Popover>
|
|
347
|
-
|
|
348
|
-
{/* Helper Text / Error */}
|
|
349
|
-
{(helperText || error) && (
|
|
350
|
-
<Typography
|
|
351
|
-
variant="small"
|
|
352
|
-
className={cn('ml-1 mt-1', error && 'text-destructive')}
|
|
353
|
-
>
|
|
354
|
-
{typeof error === 'string' ? error : error?.message || helperText}
|
|
355
|
-
</Typography>
|
|
356
|
-
)}
|
|
357
|
-
</LabelWrapper>
|
|
358
|
-
);
|
|
72
|
+
return <DateTimePickerInput {...props} />;
|
|
359
73
|
};
|
|
@@ -0,0 +1,292 @@
|
|
|
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 { Input } from '@/shadcn-components/Input/Input/Input';
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverTrigger,
|
|
9
|
+
} from '@/shadcn-components/Input/Popover/Popover';
|
|
10
|
+
import { format as DateFnsFormat } from 'date-fns';
|
|
11
|
+
import { CalendarDays } from 'lucide-react';
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import { Button } from '../../Button/Button';
|
|
14
|
+
import { DateTimePickerProps } from '../DateTimePicker';
|
|
15
|
+
|
|
16
|
+
export const DateTimePickerFilter = ({
|
|
17
|
+
label,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
format = 'dd MMM yyyy hh:mm a',
|
|
21
|
+
views = ['year', 'month', 'day', 'hours', 'minutes', 'seconds'],
|
|
22
|
+
placeholder = 'Pick a date and time',
|
|
23
|
+
shortcutsItems = [],
|
|
24
|
+
disabled = false,
|
|
25
|
+
minDate,
|
|
26
|
+
maxDate,
|
|
27
|
+
disablePast = false,
|
|
28
|
+
disableFuture = false,
|
|
29
|
+
shouldDisableDate,
|
|
30
|
+
shouldDisableMonth,
|
|
31
|
+
shouldDisableYear,
|
|
32
|
+
className,
|
|
33
|
+
onOpen,
|
|
34
|
+
onClose,
|
|
35
|
+
fullWidth,
|
|
36
|
+
type,
|
|
37
|
+
...rest
|
|
38
|
+
}: DateTimePickerProps) => {
|
|
39
|
+
const [open, setOpen] = useState(false);
|
|
40
|
+
const [timeValue, setTimeValue] = useState<string>('');
|
|
41
|
+
const wrapperRef = useRef<HTMLSpanElement>(null);
|
|
42
|
+
|
|
43
|
+
// Determine view configurations based on views prop
|
|
44
|
+
const viewConfig = useMemo(() => {
|
|
45
|
+
const hasTime =
|
|
46
|
+
views.includes('hours') ||
|
|
47
|
+
views.includes('minutes') ||
|
|
48
|
+
views.includes('seconds');
|
|
49
|
+
const hasDate =
|
|
50
|
+
views.includes('day') ||
|
|
51
|
+
views.includes('month') ||
|
|
52
|
+
views.includes('year');
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
showTime: hasTime,
|
|
56
|
+
showDate: hasDate,
|
|
57
|
+
};
|
|
58
|
+
}, [views]);
|
|
59
|
+
|
|
60
|
+
// Update time value when value changes
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (value && viewConfig.showTime) {
|
|
63
|
+
const hours = value.getHours(),
|
|
64
|
+
minutes = value.getMinutes(),
|
|
65
|
+
seconds = value.getSeconds(),
|
|
66
|
+
timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(
|
|
67
|
+
2,
|
|
68
|
+
'0',
|
|
69
|
+
)}:${String(seconds).padStart(2, '0')}`;
|
|
70
|
+
setTimeValue(timeStr);
|
|
71
|
+
}
|
|
72
|
+
}, [value, viewConfig.showTime]);
|
|
73
|
+
|
|
74
|
+
const formatDateTimeString = (date: Date | undefined) => {
|
|
75
|
+
if (!date) return '';
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return DateFnsFormat(date, format);
|
|
79
|
+
} catch {
|
|
80
|
+
return DateFnsFormat(date, 'dd/MM/yyyy hh:mm a');
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
85
|
+
setOpen(newOpen);
|
|
86
|
+
if (newOpen && onOpen) {
|
|
87
|
+
onOpen();
|
|
88
|
+
} else if (!newOpen && onClose) {
|
|
89
|
+
onClose();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleDateSelect = (date: Date | undefined) => {
|
|
94
|
+
if (date) {
|
|
95
|
+
const newDateTime = new Date(date);
|
|
96
|
+
// Preserve existing time if we have one
|
|
97
|
+
if (value) {
|
|
98
|
+
newDateTime.setHours(
|
|
99
|
+
value.getHours(),
|
|
100
|
+
value.getMinutes(),
|
|
101
|
+
value.getSeconds(),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
onChange?.(newDateTime);
|
|
105
|
+
} else {
|
|
106
|
+
onChange?.(undefined);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
111
|
+
const timeStr = e.target.value;
|
|
112
|
+
setTimeValue(timeStr);
|
|
113
|
+
|
|
114
|
+
if (timeStr) {
|
|
115
|
+
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
|
|
116
|
+
const newDateTime = value ? new Date(value) : new Date();
|
|
117
|
+
newDateTime.setHours(hours || 0, minutes || 0, seconds || 0);
|
|
118
|
+
onChange?.(newDateTime);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleClear = () => {
|
|
123
|
+
setTimeValue('');
|
|
124
|
+
onChange?.(undefined);
|
|
125
|
+
setOpen(false);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Force re-render on mount to ensure positioning works
|
|
129
|
+
const [, forceUpdate] = useState({});
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
forceUpdate({});
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const hasValue = !!value;
|
|
135
|
+
const displayText = hasValue
|
|
136
|
+
? `${label} = ${formatDateTimeString(value)}`
|
|
137
|
+
: label;
|
|
138
|
+
|
|
139
|
+
// If only time selection (no date), render simplified filter for time only
|
|
140
|
+
if (viewConfig.showTime && !viewConfig.showDate) {
|
|
141
|
+
return (
|
|
142
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
143
|
+
<PopoverTrigger asChild>
|
|
144
|
+
<span
|
|
145
|
+
ref={wrapperRef}
|
|
146
|
+
style={{ display: fullWidth ? 'block' : 'inline-block' }}
|
|
147
|
+
>
|
|
148
|
+
<Button
|
|
149
|
+
variant={hasValue ? 'default' : 'input'}
|
|
150
|
+
className={cn(
|
|
151
|
+
'justify-between',
|
|
152
|
+
fullWidth ? 'w-full' : '',
|
|
153
|
+
className,
|
|
154
|
+
)}
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
{...rest}
|
|
157
|
+
>
|
|
158
|
+
{displayText || placeholder}
|
|
159
|
+
<CalendarDays className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
160
|
+
</Button>
|
|
161
|
+
</span>
|
|
162
|
+
</PopoverTrigger>
|
|
163
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
164
|
+
<div className="flex flex-col">
|
|
165
|
+
<div className="p-3">
|
|
166
|
+
<Input
|
|
167
|
+
type="time"
|
|
168
|
+
step="1"
|
|
169
|
+
value={timeValue}
|
|
170
|
+
onChange={handleTimeChange}
|
|
171
|
+
className="bg-input-background border-none text-muted-foreground appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
<Separator />
|
|
175
|
+
<div className="flex flex-row items-center justify-left">
|
|
176
|
+
<Button variant="link" onClick={handleClear} className="min-w-0">
|
|
177
|
+
Clear
|
|
178
|
+
</Button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</PopoverContent>
|
|
182
|
+
</Popover>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
188
|
+
<PopoverTrigger asChild>
|
|
189
|
+
<span
|
|
190
|
+
ref={wrapperRef}
|
|
191
|
+
style={{ display: fullWidth ? 'block' : 'inline-block' }}
|
|
192
|
+
>
|
|
193
|
+
<Button
|
|
194
|
+
variant={hasValue ? 'default' : 'input'}
|
|
195
|
+
className={cn(
|
|
196
|
+
'justify-between',
|
|
197
|
+
fullWidth ? 'w-full' : '',
|
|
198
|
+
className,
|
|
199
|
+
)}
|
|
200
|
+
disabled={disabled}
|
|
201
|
+
{...rest}
|
|
202
|
+
>
|
|
203
|
+
{displayText || placeholder}
|
|
204
|
+
<CalendarDays className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
205
|
+
</Button>
|
|
206
|
+
</span>
|
|
207
|
+
</PopoverTrigger>
|
|
208
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
209
|
+
<div className="flex flex-col">
|
|
210
|
+
{/* Shortcuts */}
|
|
211
|
+
{shortcutsItems.length > 0 && (
|
|
212
|
+
<div className="border-b p-2">
|
|
213
|
+
<div className="flex flex-wrap gap-1">
|
|
214
|
+
{shortcutsItems.map((shortcut, index) => (
|
|
215
|
+
<Button
|
|
216
|
+
key={index}
|
|
217
|
+
variant="outline"
|
|
218
|
+
size="sm"
|
|
219
|
+
onClick={() => {
|
|
220
|
+
const newDate = shortcut.getValue();
|
|
221
|
+
onChange?.(newDate);
|
|
222
|
+
setOpen(false);
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
{shortcut.label}
|
|
226
|
+
</Button>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Calendar */}
|
|
233
|
+
{viewConfig.showDate && (
|
|
234
|
+
<Calendar
|
|
235
|
+
mode="single"
|
|
236
|
+
selected={value}
|
|
237
|
+
onSelect={handleDateSelect}
|
|
238
|
+
disabled={(date) => {
|
|
239
|
+
const today = new Date();
|
|
240
|
+
today.setHours(0, 0, 0, 0);
|
|
241
|
+
const compareDate = new Date(date);
|
|
242
|
+
compareDate.setHours(0, 0, 0, 0);
|
|
243
|
+
|
|
244
|
+
if (minDate) {
|
|
245
|
+
const min = new Date(minDate);
|
|
246
|
+
min.setHours(0, 0, 0, 0);
|
|
247
|
+
if (compareDate < min) return true;
|
|
248
|
+
}
|
|
249
|
+
if (maxDate) {
|
|
250
|
+
const max = new Date(maxDate);
|
|
251
|
+
max.setHours(0, 0, 0, 0);
|
|
252
|
+
if (compareDate > max) return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (disablePast && compareDate < today) return true;
|
|
256
|
+
if (disableFuture && compareDate > today) return true;
|
|
257
|
+
if (shouldDisableDate && shouldDisableDate(date)) return true;
|
|
258
|
+
if (shouldDisableMonth && shouldDisableMonth(date)) return true;
|
|
259
|
+
if (shouldDisableYear && shouldDisableYear(date)) return true;
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
}}
|
|
263
|
+
captionLayout={'dropdown'}
|
|
264
|
+
defaultMonth={value}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{/* Time Picker */}
|
|
269
|
+
{viewConfig.showTime && (
|
|
270
|
+
<div className={cn('p-3', viewConfig.showDate && 'border-t')}>
|
|
271
|
+
<Input
|
|
272
|
+
type="time"
|
|
273
|
+
step="1"
|
|
274
|
+
value={timeValue}
|
|
275
|
+
onChange={handleTimeChange}
|
|
276
|
+
className="bg-input-background border-none text-muted-foreground appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{/* Clear Button */}
|
|
282
|
+
<Separator />
|
|
283
|
+
<div className="flex flex-row items-center justify-left">
|
|
284
|
+
<Button variant="link" onClick={handleClear} className="min-w-0">
|
|
285
|
+
Clear
|
|
286
|
+
</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</PopoverContent>
|
|
290
|
+
</Popover>
|
|
291
|
+
);
|
|
292
|
+
};
|