@tuturuuu/ui 0.6.2 → 0.8.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.
- package/CHANGELOG.md +66 -0
- package/biome.json +1 -1
- package/package.json +11 -11
- package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
- package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
- package/src/components/ui/calendar.test.tsx +24 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/currency-input.test.tsx +43 -0
- package/src/components/ui/currency-input.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
- package/src/components/ui/date-time-picker.tsx +352 -234
- package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
- package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
- package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
- package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
- package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
- package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
- package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
- package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
- package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
- package/src/components/ui/finance/transactions/form-types.ts +5 -0
- package/src/components/ui/finance/transactions/form.test.tsx +105 -22
- package/src/components/ui/finance/transactions/form.tsx +116 -20
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
- package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
- package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
- package/src/components/ui/money-input.test.tsx +64 -0
- package/src/components/ui/money-input.tsx +63 -0
- package/src/components/ui/optional-time-picker.tsx +95 -0
- package/src/components/ui/quick-command-center.test.tsx +90 -0
- package/src/components/ui/quick-command-center.tsx +190 -0
- package/src/components/ui/storefront/cart-summary.tsx +126 -50
- package/src/components/ui/storefront/checkout-overlay.tsx +27 -0
- package/src/components/ui/storefront/hero-panel.tsx +23 -20
- package/src/components/ui/storefront/image-panel.tsx +6 -0
- package/src/components/ui/storefront/index.ts +11 -0
- package/src/components/ui/storefront/listing-card.tsx +84 -22
- package/src/components/ui/storefront/product-detail.tsx +289 -0
- package/src/components/ui/storefront/product-dialog.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +132 -5
- package/src/components/ui/storefront/storefront-surface.tsx +371 -128
- package/src/components/ui/storefront/types.ts +25 -1
- package/src/components/ui/storefront/utils.ts +118 -13
- package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
- package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
- package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
- package/src/components/ui/text-editor/content-migration.ts +41 -18
- package/src/components/ui/text-editor/extensions.ts +1 -1
- package/src/components/ui/text-editor/image-extension.ts +40 -18
- package/src/components/ui/text-editor/video-extension.ts +11 -2
- package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +1 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +13 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +46 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +13 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +3 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +56 -2
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
- package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +34 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +39 -0
- package/src/components/ui/tu-do/shared/board-views.tsx +9 -7
- package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
- package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +42 -14
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
- package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
- package/src/hooks/useBoardRealtime.ts +6 -3
- package/src/hooks/useBoardRealtime.types.ts +11 -0
- package/src/hooks/useCursorTracking.ts +91 -27
- package/src/hooks/useTaskUserRealtime.ts +5 -3
|
@@ -4,6 +4,7 @@ import { CalendarIcon, Check, Clock, Edit } from '@tuturuuu/icons';
|
|
|
4
4
|
import { Button } from '@tuturuuu/ui/button';
|
|
5
5
|
import { Calendar } from '@tuturuuu/ui/calendar';
|
|
6
6
|
import { Input } from '@tuturuuu/ui/input';
|
|
7
|
+
import { Label } from '@tuturuuu/ui/label';
|
|
7
8
|
import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
|
|
8
9
|
import {
|
|
9
10
|
Select,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
SelectTrigger,
|
|
13
14
|
SelectValue,
|
|
14
15
|
} from '@tuturuuu/ui/select';
|
|
15
|
-
import {
|
|
16
|
+
import { Switch } from '@tuturuuu/ui/switch';
|
|
16
17
|
import { cn } from '@tuturuuu/utils/format';
|
|
17
18
|
import {
|
|
18
19
|
buildDateInTimezone,
|
|
@@ -22,7 +23,14 @@ import {
|
|
|
22
23
|
} from '@tuturuuu/utils/task-date-timezone';
|
|
23
24
|
import { getTimeFormatPattern } from '@tuturuuu/utils/time-helper';
|
|
24
25
|
import { format, parse } from 'date-fns';
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
type ReactNode,
|
|
28
|
+
useEffect,
|
|
29
|
+
useId,
|
|
30
|
+
useMemo,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
} from 'react';
|
|
26
34
|
import { Separator } from './separator';
|
|
27
35
|
|
|
28
36
|
interface DateTimePickerProps {
|
|
@@ -42,6 +50,12 @@ interface DateTimePickerProps {
|
|
|
42
50
|
collisionPadding?: number;
|
|
43
51
|
/** Render the picker inline without a popover (useful inside dialogs) */
|
|
44
52
|
inline?: boolean;
|
|
53
|
+
timeToggle?: {
|
|
54
|
+
checked: boolean;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
label: ReactNode;
|
|
57
|
+
onCheckedChange: (checked: boolean) => void;
|
|
58
|
+
};
|
|
45
59
|
preferences?: {
|
|
46
60
|
weekStartsOn?: 0 | 1 | 6;
|
|
47
61
|
timezone?: string;
|
|
@@ -82,6 +96,7 @@ export function DateTimePicker({
|
|
|
82
96
|
align = 'start',
|
|
83
97
|
collisionPadding = 16,
|
|
84
98
|
inline = false,
|
|
99
|
+
timeToggle,
|
|
85
100
|
preferences,
|
|
86
101
|
}: DateTimePickerProps) {
|
|
87
102
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(date);
|
|
@@ -91,12 +106,25 @@ export function DateTimePicker({
|
|
|
91
106
|
date ? format(date, 'HH:mm') : ''
|
|
92
107
|
);
|
|
93
108
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
109
|
+
const timeToggleId = useId();
|
|
94
110
|
|
|
95
111
|
const tz = useMemo(
|
|
96
112
|
() =>
|
|
97
113
|
preferences?.timezone ? resolveTaskTimezone(preferences.timezone) : null,
|
|
98
114
|
[preferences?.timezone]
|
|
99
115
|
);
|
|
116
|
+
const resolvedTimezoneLabel = useMemo(() => {
|
|
117
|
+
if (tz) return tz.replace(/_/g, ' ');
|
|
118
|
+
|
|
119
|
+
if (typeof Intl !== 'undefined') {
|
|
120
|
+
return (
|
|
121
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone?.replace(/_/g, ' ') ||
|
|
122
|
+
'Local time'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 'Local time';
|
|
127
|
+
}, [tz]);
|
|
100
128
|
|
|
101
129
|
// Update date directly without casting workaround
|
|
102
130
|
const updateDate = (next?: Date) => {
|
|
@@ -189,7 +217,7 @@ export function DateTimePicker({
|
|
|
189
217
|
}
|
|
190
218
|
setSelectedDate(next);
|
|
191
219
|
updateDate(next);
|
|
192
|
-
if (!inline) {
|
|
220
|
+
if (!inline && !showTimeSelect) {
|
|
193
221
|
setIsCalendarOpen(false);
|
|
194
222
|
}
|
|
195
223
|
};
|
|
@@ -394,6 +422,83 @@ export function DateTimePicker({
|
|
|
394
422
|
|
|
395
423
|
// If the filtered list is empty, show an error message
|
|
396
424
|
const noValidTimes = filteredTimeOptions.length === 0;
|
|
425
|
+
const selectedTimeValue = date
|
|
426
|
+
? (() => {
|
|
427
|
+
if (tz !== null) {
|
|
428
|
+
const p = getDatePartsInTimezone(date, tz);
|
|
429
|
+
return `${p.hour.toString().padStart(2, '0')}:${p.minute.toString().padStart(2, '0')}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return `${format(date, 'HH')}:${format(date, 'mm')}`;
|
|
433
|
+
})()
|
|
434
|
+
: undefined;
|
|
435
|
+
const selectedDateText = date
|
|
436
|
+
? tz !== null
|
|
437
|
+
? formatInTimezone(date, tz, 'MMM D, YYYY')
|
|
438
|
+
: format(date, 'PPP')
|
|
439
|
+
: null;
|
|
440
|
+
const selectedTimeText = date
|
|
441
|
+
? tz !== null
|
|
442
|
+
? formatInTimezone(date, tz, timeFormat === '24h' ? 'HH:mm' : 'h:mm A')
|
|
443
|
+
: format(date, timePattern)
|
|
444
|
+
: null;
|
|
445
|
+
|
|
446
|
+
const setSelectedValue = (next: Date | undefined) => {
|
|
447
|
+
setSelectedDate(next);
|
|
448
|
+
updateDate(next);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const handleSetNow = () => {
|
|
452
|
+
let next = new Date();
|
|
453
|
+
|
|
454
|
+
if (minDate && next < minDate) {
|
|
455
|
+
next = new Date(minDate);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (maxDate && next > maxDate) {
|
|
459
|
+
next = new Date(maxDate);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
setSelectedValue(next);
|
|
463
|
+
if (!inline) {
|
|
464
|
+
setIsCalendarOpen(false);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const handleSetToday = () => {
|
|
469
|
+
const now = new Date();
|
|
470
|
+
const baseDate = selectedDate ?? date ?? now;
|
|
471
|
+
let next: Date;
|
|
472
|
+
|
|
473
|
+
if (tz) {
|
|
474
|
+
const todayParts = getDatePartsInTimezone(now, tz);
|
|
475
|
+
const timeParts = showTimeSelect
|
|
476
|
+
? getDatePartsInTimezone(baseDate, tz)
|
|
477
|
+
: { hour: 0, minute: 0 };
|
|
478
|
+
|
|
479
|
+
next = buildDateInTimezone(
|
|
480
|
+
todayParts.year,
|
|
481
|
+
todayParts.month,
|
|
482
|
+
todayParts.day,
|
|
483
|
+
timeParts.hour,
|
|
484
|
+
timeParts.minute,
|
|
485
|
+
tz
|
|
486
|
+
);
|
|
487
|
+
} else {
|
|
488
|
+
next = new Date(now);
|
|
489
|
+
|
|
490
|
+
if (showTimeSelect) {
|
|
491
|
+
next.setHours(baseDate.getHours(), baseDate.getMinutes(), 0, 0);
|
|
492
|
+
} else {
|
|
493
|
+
next.setHours(0, 0, 0, 0);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
setSelectedValue(next);
|
|
498
|
+
if (!inline && !showTimeSelect) {
|
|
499
|
+
setIsCalendarOpen(false);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
397
502
|
|
|
398
503
|
// Shared calendar disabled dates config
|
|
399
504
|
const calendarDisabled =
|
|
@@ -424,205 +529,192 @@ export function DateTimePicker({
|
|
|
424
529
|
]
|
|
425
530
|
: undefined;
|
|
426
531
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
532
|
+
const timeControl = showTimeSelect ? (
|
|
533
|
+
<div className="flex min-w-0 flex-col gap-3 border-t p-3 sm:w-52 sm:border-t-0 sm:border-l">
|
|
534
|
+
<div className="space-y-1">
|
|
535
|
+
<div className="flex items-center gap-2 font-medium text-sm">
|
|
536
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
537
|
+
<span>Time</span>
|
|
538
|
+
</div>
|
|
539
|
+
<div className="truncate text-muted-foreground text-xs">
|
|
540
|
+
{resolvedTimezoneLabel}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
{isManualTimeEntry ? (
|
|
545
|
+
<div className="flex items-center gap-2">
|
|
546
|
+
<Input
|
|
547
|
+
value={manualTimeInput}
|
|
548
|
+
onChange={(e) => setManualTimeInput(e.target.value)}
|
|
549
|
+
onKeyDown={handleManualTimeKeyDown}
|
|
550
|
+
placeholder="HH:MM"
|
|
551
|
+
className="h-9 flex-1"
|
|
552
|
+
aria-label="Enter time manually in HH:MM"
|
|
553
|
+
/>
|
|
554
|
+
<Button
|
|
555
|
+
size="icon"
|
|
556
|
+
variant="ghost"
|
|
557
|
+
className="h-9 w-9"
|
|
558
|
+
onClick={() => handleManualTimeSubmit()}
|
|
559
|
+
aria-label="Confirm manual time"
|
|
560
|
+
>
|
|
561
|
+
<Check className="h-4 w-4" />
|
|
562
|
+
</Button>
|
|
563
|
+
</div>
|
|
564
|
+
) : (
|
|
565
|
+
<div className="flex items-center gap-2">
|
|
566
|
+
<Select
|
|
567
|
+
value={noValidTimes ? undefined : selectedTimeValue}
|
|
568
|
+
onValueChange={handleTimeChange}
|
|
569
|
+
disabled={noValidTimes}
|
|
570
|
+
aria-label="Time options"
|
|
571
|
+
>
|
|
572
|
+
<SelectTrigger className="h-9 flex-1">
|
|
573
|
+
<SelectValue
|
|
574
|
+
placeholder={
|
|
575
|
+
noValidTimes ? 'Invalid time selection' : 'Select time'
|
|
576
|
+
}
|
|
577
|
+
/>
|
|
578
|
+
</SelectTrigger>
|
|
579
|
+
<SelectContent className="max-h-64">
|
|
580
|
+
{filteredTimeOptions.map((time) => (
|
|
581
|
+
<SelectItem key={time.value} value={time.value}>
|
|
582
|
+
{time.display}
|
|
583
|
+
</SelectItem>
|
|
584
|
+
))}
|
|
585
|
+
</SelectContent>
|
|
586
|
+
</Select>
|
|
587
|
+
<Button
|
|
588
|
+
size="icon"
|
|
589
|
+
variant="ghost"
|
|
590
|
+
className="h-9 w-9"
|
|
591
|
+
onClick={() => setIsManualTimeEntry(true)}
|
|
592
|
+
title="Enter time manually"
|
|
593
|
+
aria-label="Switch to manual time entry"
|
|
594
|
+
>
|
|
595
|
+
<Edit className="h-4 w-4" />
|
|
596
|
+
</Button>
|
|
597
|
+
</div>
|
|
598
|
+
)}
|
|
599
|
+
|
|
600
|
+
<div className="grid grid-cols-2 gap-2">
|
|
601
|
+
<Button
|
|
602
|
+
type="button"
|
|
603
|
+
variant="secondary"
|
|
604
|
+
size="sm"
|
|
605
|
+
onClick={handleSetToday}
|
|
435
606
|
>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
aria-label="
|
|
607
|
+
Today
|
|
608
|
+
</Button>
|
|
609
|
+
<Button
|
|
610
|
+
type="button"
|
|
611
|
+
variant="secondary"
|
|
612
|
+
size="sm"
|
|
613
|
+
onClick={handleSetNow}
|
|
614
|
+
aria-label="Set to now"
|
|
444
615
|
>
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
<span className="text-muted-foreground text-xs">
|
|
475
|
-
{date
|
|
476
|
-
? tz !== null
|
|
477
|
-
? formatInTimezone(date, tz, 'MMM D, YYYY')
|
|
478
|
-
: format(date, 'MMM d, yyyy')
|
|
479
|
-
: ''}
|
|
480
|
-
</span>
|
|
616
|
+
Now
|
|
617
|
+
</Button>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
{noValidTimes && (
|
|
621
|
+
<div className="text-destructive text-xs">
|
|
622
|
+
No valid end times available. Please select an earlier start time or
|
|
623
|
+
check your time selection.
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
</div>
|
|
627
|
+
) : null;
|
|
628
|
+
|
|
629
|
+
// Inline content (used both for inline mode and popover content)
|
|
630
|
+
const pickerContent = (
|
|
631
|
+
<div className="overflow-hidden">
|
|
632
|
+
{date && (
|
|
633
|
+
<div className="border-b bg-muted/30 px-3 py-2">
|
|
634
|
+
<div className="flex min-w-0 items-center gap-2 text-sm">
|
|
635
|
+
<CalendarIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
636
|
+
<span className="truncate font-medium">{selectedDateText}</span>
|
|
637
|
+
{showTimeSelect && selectedTimeText && (
|
|
638
|
+
<>
|
|
639
|
+
<span className="shrink-0 text-muted-foreground">•</span>
|
|
640
|
+
<span className="truncate text-muted-foreground">
|
|
641
|
+
{selectedTimeText}
|
|
642
|
+
</span>
|
|
643
|
+
</>
|
|
644
|
+
)}
|
|
481
645
|
</div>
|
|
646
|
+
</div>
|
|
647
|
+
)}
|
|
482
648
|
|
|
483
|
-
|
|
649
|
+
<div className="flex flex-col sm:flex-row">
|
|
650
|
+
<div className="p-2">
|
|
651
|
+
<Calendar
|
|
652
|
+
mode="single"
|
|
653
|
+
selected={date}
|
|
654
|
+
onSelect={handleSelect}
|
|
655
|
+
onSubmit={(date) => {
|
|
656
|
+
handleSelect(date);
|
|
657
|
+
if (!inline && !showTimeSelect) {
|
|
658
|
+
setIsCalendarOpen(false);
|
|
659
|
+
}
|
|
660
|
+
}}
|
|
661
|
+
autoFocus
|
|
662
|
+
disabled={calendarDisabled}
|
|
663
|
+
preferences={preferences}
|
|
664
|
+
aria-label="Calendar selector"
|
|
665
|
+
/>
|
|
666
|
+
</div>
|
|
667
|
+
{timeControl}
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
{showFooterControls && !inline && (
|
|
671
|
+
<>
|
|
672
|
+
<Separator />
|
|
673
|
+
<div className="flex flex-wrap items-center justify-between gap-2 p-2">
|
|
484
674
|
<div className="flex items-center gap-2">
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
>
|
|
499
|
-
<Check className="h-4 w-4" />
|
|
500
|
-
</Button>
|
|
675
|
+
{allowClear && (date || selectedDate) && (
|
|
676
|
+
<Button
|
|
677
|
+
variant="ghost"
|
|
678
|
+
size="sm"
|
|
679
|
+
onClick={() => {
|
|
680
|
+
setSelectedValue(undefined);
|
|
681
|
+
setIsCalendarOpen(false);
|
|
682
|
+
}}
|
|
683
|
+
aria-label="Clear selection"
|
|
684
|
+
>
|
|
685
|
+
Clear
|
|
686
|
+
</Button>
|
|
687
|
+
)}
|
|
501
688
|
</div>
|
|
502
|
-
) : (
|
|
503
689
|
<div className="flex items-center gap-2">
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return `${format(date, 'HH')}:${format(date, 'mm')}`;
|
|
515
|
-
})()
|
|
516
|
-
: undefined
|
|
517
|
-
}
|
|
518
|
-
onValueChange={handleTimeChange}
|
|
519
|
-
disabled={noValidTimes}
|
|
520
|
-
aria-label="Time options"
|
|
521
|
-
>
|
|
522
|
-
<SelectTrigger className="flex-1">
|
|
523
|
-
<SelectValue
|
|
524
|
-
placeholder={
|
|
525
|
-
noValidTimes ? 'Invalid time selection' : 'Select time'
|
|
526
|
-
}
|
|
527
|
-
/>
|
|
528
|
-
</SelectTrigger>
|
|
529
|
-
<SelectContent className="max-h-50">
|
|
530
|
-
{filteredTimeOptions.map((time) => (
|
|
531
|
-
<SelectItem key={time.value} value={time.value}>
|
|
532
|
-
{time.display}
|
|
533
|
-
</SelectItem>
|
|
534
|
-
))}
|
|
535
|
-
</SelectContent>
|
|
536
|
-
</Select>
|
|
690
|
+
{!showTimeSelect && (
|
|
691
|
+
<Button
|
|
692
|
+
type="button"
|
|
693
|
+
variant="ghost"
|
|
694
|
+
size="sm"
|
|
695
|
+
onClick={handleSetToday}
|
|
696
|
+
>
|
|
697
|
+
Today
|
|
698
|
+
</Button>
|
|
699
|
+
)}
|
|
537
700
|
<Button
|
|
538
|
-
|
|
539
|
-
variant="ghost"
|
|
540
|
-
onClick={() => setIsManualTimeEntry(true)}
|
|
541
|
-
title="Enter time manually"
|
|
542
|
-
aria-label="Switch to manual time entry"
|
|
543
|
-
>
|
|
544
|
-
<Edit className="h-4 w-4" />
|
|
545
|
-
</Button>
|
|
546
|
-
</div>
|
|
547
|
-
)}
|
|
548
|
-
|
|
549
|
-
{noValidTimes && (
|
|
550
|
-
<div className="text-destructive text-xs">
|
|
551
|
-
No valid end times available. Please select an earlier start time
|
|
552
|
-
or check your time selection.
|
|
553
|
-
</div>
|
|
554
|
-
)}
|
|
555
|
-
</div>
|
|
556
|
-
</TabsContent>
|
|
557
|
-
{showFooterControls && !inline && (
|
|
558
|
-
<div className="flex items-center justify-between border-t p-2">
|
|
559
|
-
<div className="flex items-center gap-2">
|
|
560
|
-
{allowClear && (date || selectedDate) && (
|
|
561
|
-
<Button
|
|
562
|
-
variant="ghost"
|
|
701
|
+
variant="default"
|
|
563
702
|
size="sm"
|
|
564
703
|
onClick={() => {
|
|
565
|
-
|
|
566
|
-
|
|
704
|
+
if (isManualTimeEntry) {
|
|
705
|
+
handleManualTimeSubmit();
|
|
706
|
+
}
|
|
567
707
|
setIsCalendarOpen(false);
|
|
568
708
|
}}
|
|
569
|
-
aria-label="
|
|
709
|
+
aria-label="Done"
|
|
570
710
|
>
|
|
571
|
-
|
|
711
|
+
Done
|
|
572
712
|
</Button>
|
|
573
|
-
|
|
574
|
-
</div>
|
|
575
|
-
<div className="flex items-center gap-2">
|
|
576
|
-
<Button
|
|
577
|
-
variant="ghost"
|
|
578
|
-
size="sm"
|
|
579
|
-
onClick={() => {
|
|
580
|
-
const now = new Date();
|
|
581
|
-
let next = now;
|
|
582
|
-
if (minDate && now < minDate) {
|
|
583
|
-
next = new Date(minDate);
|
|
584
|
-
}
|
|
585
|
-
setSelectedDate(next);
|
|
586
|
-
setDate(next);
|
|
587
|
-
setIsCalendarOpen(false);
|
|
588
|
-
}}
|
|
589
|
-
aria-label="Set to now"
|
|
590
|
-
>
|
|
591
|
-
Now
|
|
592
|
-
</Button>
|
|
593
|
-
<Button
|
|
594
|
-
variant="default"
|
|
595
|
-
size="sm"
|
|
596
|
-
onClick={() => {
|
|
597
|
-
if (isManualTimeEntry) {
|
|
598
|
-
handleManualTimeSubmit();
|
|
599
|
-
}
|
|
600
|
-
setIsCalendarOpen(false);
|
|
601
|
-
}}
|
|
602
|
-
aria-label="Done"
|
|
603
|
-
>
|
|
604
|
-
Done
|
|
605
|
-
</Button>
|
|
713
|
+
</div>
|
|
606
714
|
</div>
|
|
607
|
-
|
|
715
|
+
</>
|
|
608
716
|
)}
|
|
609
|
-
</
|
|
610
|
-
) : (
|
|
611
|
-
<Calendar
|
|
612
|
-
mode="single"
|
|
613
|
-
selected={date}
|
|
614
|
-
onSelect={handleSelect}
|
|
615
|
-
onSubmit={(date) => {
|
|
616
|
-
handleSelect(date);
|
|
617
|
-
if (!inline) {
|
|
618
|
-
setIsCalendarOpen(false);
|
|
619
|
-
}
|
|
620
|
-
}}
|
|
621
|
-
autoFocus
|
|
622
|
-
disabled={calendarDisabled}
|
|
623
|
-
preferences={preferences}
|
|
624
|
-
aria-label="Calendar selector"
|
|
625
|
-
/>
|
|
717
|
+
</div>
|
|
626
718
|
);
|
|
627
719
|
|
|
628
720
|
// Inline mode: render content directly without popover
|
|
@@ -635,60 +727,86 @@ export function DateTimePicker({
|
|
|
635
727
|
}
|
|
636
728
|
|
|
637
729
|
// Popover mode (default)
|
|
730
|
+
const triggerButton = (
|
|
731
|
+
<Button
|
|
732
|
+
ref={pickerButtonRef}
|
|
733
|
+
variant={timeToggle ? 'ghost' : 'outline'}
|
|
734
|
+
className={cn(
|
|
735
|
+
'min-h-10 justify-start text-left font-normal',
|
|
736
|
+
timeToggle
|
|
737
|
+
? 'min-w-0 flex-1 rounded-none border-0 px-3 shadow-none hover:bg-transparent'
|
|
738
|
+
: 'w-full',
|
|
739
|
+
!date && 'text-muted-foreground'
|
|
740
|
+
)}
|
|
741
|
+
disabled={disabled}
|
|
742
|
+
aria-label={
|
|
743
|
+
date
|
|
744
|
+
? `Selected ${
|
|
745
|
+
tz !== null
|
|
746
|
+
? formatInTimezone(
|
|
747
|
+
date,
|
|
748
|
+
tz,
|
|
749
|
+
`MMM D, YYYY ${timeFormat === '24h' ? 'HH:mm' : 'h:mm A'}`
|
|
750
|
+
)
|
|
751
|
+
: format(date, `PPP ${timePattern}`)
|
|
752
|
+
}`
|
|
753
|
+
: 'Open date and time picker'
|
|
754
|
+
}
|
|
755
|
+
>
|
|
756
|
+
<CalendarIcon className="mr-2 h-4 w-4 shrink-0" />
|
|
757
|
+
{date ? (
|
|
758
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5">
|
|
759
|
+
<span className="truncate">{selectedDateText}</span>
|
|
760
|
+
{showTimeSelect && selectedTimeText && (
|
|
761
|
+
<>
|
|
762
|
+
<span className="text-muted-foreground">•</span>
|
|
763
|
+
<span className="truncate text-muted-foreground">
|
|
764
|
+
{selectedTimeText}
|
|
765
|
+
</span>
|
|
766
|
+
</>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
) : (
|
|
770
|
+
<span>Pick a date{showTimeSelect ? ' and time' : ''}</span>
|
|
771
|
+
)}
|
|
772
|
+
</Button>
|
|
773
|
+
);
|
|
774
|
+
|
|
638
775
|
return (
|
|
639
776
|
<div className="w-full" ref={popoverRef}>
|
|
640
777
|
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
|
641
|
-
|
|
642
|
-
<
|
|
643
|
-
ref={pickerButtonRef}
|
|
644
|
-
variant="outline"
|
|
778
|
+
{timeToggle ? (
|
|
779
|
+
<div
|
|
645
780
|
className={cn(
|
|
646
|
-
'w-full
|
|
647
|
-
|
|
781
|
+
'flex w-full overflow-hidden rounded-md border bg-background shadow-xs transition-colors',
|
|
782
|
+
disabled && 'cursor-not-allowed opacity-60'
|
|
648
783
|
)}
|
|
649
|
-
disabled={disabled}
|
|
650
|
-
aria-label={
|
|
651
|
-
date
|
|
652
|
-
? `Selected ${
|
|
653
|
-
tz !== null
|
|
654
|
-
? formatInTimezone(
|
|
655
|
-
date,
|
|
656
|
-
tz,
|
|
657
|
-
`MMM D, YYYY ${timeFormat === '24h' ? 'HH:mm' : 'h:mm A'}`
|
|
658
|
-
)
|
|
659
|
-
: format(date, `PPP ${timePattern}`)
|
|
660
|
-
}`
|
|
661
|
-
: 'Open date and time picker'
|
|
662
|
-
}
|
|
663
784
|
>
|
|
664
|
-
<
|
|
665
|
-
|
|
666
|
-
<
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
</span>
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
)}
|
|
690
|
-
</Button>
|
|
691
|
-
</PopoverTrigger>
|
|
785
|
+
<PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
|
|
786
|
+
<div className="flex min-w-36 shrink-0 items-center justify-between gap-2 border-l bg-muted/30 px-3">
|
|
787
|
+
<Label
|
|
788
|
+
htmlFor={timeToggleId}
|
|
789
|
+
className="flex min-w-0 items-center gap-2 font-medium text-sm"
|
|
790
|
+
>
|
|
791
|
+
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
792
|
+
<span className="truncate">{timeToggle.label}</span>
|
|
793
|
+
</Label>
|
|
794
|
+
<Switch
|
|
795
|
+
id={timeToggleId}
|
|
796
|
+
checked={timeToggle.checked}
|
|
797
|
+
onCheckedChange={timeToggle.onCheckedChange}
|
|
798
|
+
disabled={disabled || timeToggle.disabled}
|
|
799
|
+
aria-label={
|
|
800
|
+
typeof timeToggle.label === 'string'
|
|
801
|
+
? timeToggle.label
|
|
802
|
+
: undefined
|
|
803
|
+
}
|
|
804
|
+
/>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
) : (
|
|
808
|
+
<PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
|
|
809
|
+
)}
|
|
692
810
|
<PopoverContent
|
|
693
811
|
className="flex max-h-[85vh] w-auto max-w-[calc(100vw-1rem)] flex-col p-0 sm:max-w-[calc(100vw-2rem)]"
|
|
694
812
|
align={align}
|