@tuturuuu/ui 0.6.2 → 0.7.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/biome.json +1 -1
  3. package/package.json +8 -8
  4. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  5. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  6. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  7. package/src/components/ui/calendar.test.tsx +24 -0
  8. package/src/components/ui/calendar.tsx +1 -0
  9. package/src/components/ui/date-time-picker.tsx +352 -234
  10. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  11. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  12. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  13. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  14. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  15. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  16. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  17. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  18. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  19. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  20. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  21. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  22. package/src/components/ui/finance/transactions/form.tsx +116 -20
  23. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  24. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  25. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  26. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  27. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  28. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  29. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  30. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  33. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  34. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  35. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  36. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  37. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  38. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  39. package/src/components/ui/optional-time-picker.tsx +95 -0
  40. package/src/components/ui/quick-command-center.test.tsx +90 -0
  41. package/src/components/ui/quick-command-center.tsx +190 -0
  42. package/src/components/ui/storefront/cart-summary.tsx +18 -27
  43. package/src/components/ui/storefront/hero-panel.tsx +22 -13
  44. package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
  45. package/src/components/ui/storefront/storefront-surface.tsx +84 -41
  46. package/src/components/ui/storefront/types.ts +2 -0
  47. package/src/components/ui/storefront/utils.ts +21 -0
  48. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  49. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  50. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
@@ -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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
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 { useEffect, useMemo, useRef, useState } from 'react';
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
- // Inline content (used both for inline mode and popover content)
428
- const pickerContent = showTimeSelect ? (
429
- <Tabs defaultValue="date" className="w-full p-2">
430
- <TabsList className="grid w-full grid-cols-2">
431
- <TabsTrigger
432
- value="date"
433
- className="flex items-center gap-1"
434
- aria-label="Select date"
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
- <CalendarIcon className="h-3 w-3" />
437
- Date
438
- </TabsTrigger>
439
- <TabsTrigger
440
- value="time"
441
- className="flex items-center gap-1"
442
- disabled={noValidTimes}
443
- aria-label="Select time"
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
- <Clock className="h-3 w-3" />
446
- Time
447
- </TabsTrigger>
448
- </TabsList>
449
-
450
- <Separator />
451
-
452
- <TabsContent value="date" className="mt-0 p-0">
453
- <Calendar
454
- mode="single"
455
- selected={date}
456
- onSelect={handleSelect}
457
- onSubmit={(date) => {
458
- handleSelect(date);
459
- }}
460
- autoFocus
461
- disabled={calendarDisabled}
462
- preferences={preferences}
463
- aria-label="Calendar selector"
464
- />
465
- </TabsContent>
466
-
467
- <TabsContent value="time" className="mt-0 min-w-xs p-0">
468
- <div className="space-y-4 p-4">
469
- <div className="flex items-center justify-between">
470
- <div className="flex items-center gap-2">
471
- <Clock className="h-4 w-4 text-muted-foreground" />
472
- <span className="font-medium text-sm">Select time</span>
473
- </div>
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
- {isManualTimeEntry ? (
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
- <Input
486
- value={manualTimeInput}
487
- onChange={(e) => setManualTimeInput(e.target.value)}
488
- onKeyDown={handleManualTimeKeyDown}
489
- placeholder="HH:MM"
490
- className="flex-1"
491
- aria-label="Enter time manually in HH:MM"
492
- />
493
- <Button
494
- size="sm"
495
- variant="ghost"
496
- onClick={() => handleManualTimeSubmit()}
497
- aria-label="Confirm manual time"
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
- <Select
505
- value={
506
- noValidTimes
507
- ? undefined
508
- : date
509
- ? (() => {
510
- if (tz !== null) {
511
- const p = getDatePartsInTimezone(date, tz);
512
- return `${p.hour.toString().padStart(2, '0')}:${p.minute.toString().padStart(2, '0')}`;
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
- size="sm"
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
- setSelectedDate(undefined);
566
- updateDate(undefined);
704
+ if (isManualTimeEntry) {
705
+ handleManualTimeSubmit();
706
+ }
567
707
  setIsCalendarOpen(false);
568
708
  }}
569
- aria-label="Clear selection"
709
+ aria-label="Done"
570
710
  >
571
- Clear
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
- </div>
715
+ </>
608
716
  )}
609
- </Tabs>
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
- <PopoverTrigger asChild>
642
- <Button
643
- ref={pickerButtonRef}
644
- variant="outline"
778
+ {timeToggle ? (
779
+ <div
645
780
  className={cn(
646
- 'w-full justify-start text-left font-normal',
647
- !date && 'text-muted-foreground'
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
- <CalendarIcon className="mr-2 h-4 w-4" />
665
- {date ? (
666
- <div className="flex items-center gap-2">
667
- <span>
668
- {tz !== null
669
- ? formatInTimezone(date, tz, 'MMM D, YYYY')
670
- : format(date, 'PPP')}
671
- </span>
672
- {showTimeSelect && (
673
- <>
674
- <span className="text-muted-foreground">•</span>
675
- <span className="text-muted-foreground">
676
- {tz !== null
677
- ? formatInTimezone(
678
- date,
679
- tz,
680
- timeFormat === '24h' ? 'HH:mm' : 'h:mm A'
681
- )
682
- : format(date, timePattern)}
683
- </span>
684
- </>
685
- )}
686
- </div>
687
- ) : (
688
- <span>Pick a date{showTimeSelect ? ' and time' : ''}</span>
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}