myoperator-mcp 0.2.328 → 0.2.330

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 (2) hide show
  1. package/dist/index.js +722 -302
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2520,17 +2520,10 @@ import { cva, type VariantProps } from "class-variance-authority";
2520
2520
  import { ChevronLeft, ChevronRight, Clock2, X } from "lucide-react";
2521
2521
 
2522
2522
  import { cn } from "@/lib/utils";
2523
- import {
2524
- Select,
2525
- SelectContent,
2526
- SelectItem,
2527
- SelectTrigger,
2528
- SelectValue,
2529
- } from "./select";
2530
2523
 
2531
2524
  const DEFAULT_START_TIME = "10:30:00";
2532
2525
  const DEFAULT_END_TIME = "12:30:00";
2533
- const DEFAULT_PLACEHOLDER = "--/--/-- -- : --";
2526
+ const DEFAULT_PLACEHOLDER = "--/--/---- --:-- --";
2534
2527
  const POPOVER_WIDTH = 336;
2535
2528
  const POPOVER_MARGIN = 8;
2536
2529
  const POPOVER_GAP = 4;
@@ -2540,23 +2533,8 @@ const POPOVER_WIDTH_VAR = "--date-time-picker-popover-width";
2540
2533
  const CALENDAR_PLACEMENT: Placement = "bottom-start";
2541
2534
  const YEAR_RANGE_BEFORE = 100;
2542
2535
  const YEAR_RANGE_AFTER = 10;
2543
- const CALENDAR_SELECT_CONTENT_SELECTOR =
2544
- "[data-date-time-picker-calendar-select]";
2545
- const CALENDAR_SELECT_TRIGGER_SELECTOR =
2546
- "[data-date-time-picker-calendar-select-trigger]";
2547
- const CALENDAR_SELECT_TRIGGER_CLASS = "!w-[90px] !min-w-[90px] !gap-[6px] !px-3";
2548
- const CALENDAR_SELECT_CONTENT_CLASS =
2549
- "z-[10060] w-[var(--radix-select-trigger-width)] min-w-[var(--radix-select-trigger-width)] max-h-[min(16rem,var(--radix-select-content-available-height))]";
2550
- type CalendarSelect = "month" | "year";
2551
- const DATE_TIME_INPUT_SEGMENT_RANGES = {
2552
- day: [0, 2],
2553
- month: [3, 5],
2554
- year: [6, 10],
2555
- hour: [11, 13],
2556
- minute: [14, 16],
2557
- meridiem: [17, 19],
2558
- } as const;
2559
-
2536
+ const CALENDAR_DROPDOWN_TRIGGER_CLASS =
2537
+ "h-9 min-w-[90px] rounded-md border border-solid border-semantic-border-input bg-semantic-bg-primary px-3 text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]";
2560
2538
  const weekDays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
2561
2539
  const monthNames = Array.from({ length: 12 }, (_, monthIndex) =>
2562
2540
  new Intl.DateTimeFormat("en-US", { month: "short" }).format(
@@ -2570,6 +2548,11 @@ const monthFormatter = new Intl.DateTimeFormat("en-US", {
2570
2548
 
2571
2549
  const dateTimePickerVariants = cva("relative inline-block w-full max-w-full", {
2572
2550
  variants: {
2551
+ variant: {
2552
+ "date-time": "",
2553
+ "date-only": "",
2554
+ "time-only": "",
2555
+ },
2573
2556
  size: {
2574
2557
  sm: "sm:w-[280px]",
2575
2558
  default: "sm:w-[336px]",
@@ -2577,6 +2560,7 @@ const dateTimePickerVariants = cva("relative inline-block w-full max-w-full", {
2577
2560
  },
2578
2561
  },
2579
2562
  defaultVariants: {
2563
+ variant: "date-time",
2580
2564
  size: "default",
2581
2565
  },
2582
2566
  });
@@ -2622,6 +2606,7 @@ export interface DateTimePickerProps
2622
2606
  readOnly?: boolean;
2623
2607
  name?: string;
2624
2608
  showEndTime?: boolean;
2609
+ showSeconds?: boolean;
2625
2610
  showClear?: boolean;
2626
2611
  closeOnSelect?: boolean;
2627
2612
  startTimeLabel?: string;
@@ -2635,6 +2620,17 @@ export interface DateTimePickerProps
2635
2620
  portalContainer?: HTMLElement | null;
2636
2621
  }
2637
2622
 
2623
+ type DateTimePickerVariant = NonNullable<
2624
+ VariantProps<typeof dateTimePickerVariants>["variant"]
2625
+ >;
2626
+ type CalendarDropdownKind = "month" | "year";
2627
+
2628
+ interface CalendarDropdownOption {
2629
+ value: string;
2630
+ label: string;
2631
+ disabled?: boolean;
2632
+ }
2633
+
2638
2634
  function normalizeValue(
2639
2635
  value?: Partial<DateTimePickerValue>
2640
2636
  ): DateTimePickerValue {
@@ -2694,6 +2690,13 @@ function clampMonth(date: Date, minDate?: Date, maxDate?: Date) {
2694
2690
  return startOfMonth(date);
2695
2691
  }
2696
2692
 
2693
+ function isSelectableDay(date: Date, minDate?: Date, maxDate?: Date) {
2694
+ return (
2695
+ !(minDate && isBeforeDay(date, minDate)) &&
2696
+ !(maxDate && isAfterDay(date, maxDate))
2697
+ );
2698
+ }
2699
+
2697
2700
  function getYearOptions(visibleMonth: Date, minDate?: Date, maxDate?: Date) {
2698
2701
  const currentYear = new Date().getFullYear();
2699
2702
  const selectedYear = visibleMonth.getFullYear();
@@ -2742,32 +2745,84 @@ function isPointerInsideElement(
2742
2745
  return false;
2743
2746
  }
2744
2747
 
2745
- function isPointerInsideSelector(event: MouseEvent, selector: string) {
2746
- const target = event.target;
2747
-
2748
- return target instanceof Element && target.closest(selector) !== null;
2748
+ function timeHasVisibleSeconds(time?: string) {
2749
+ const [, , second = "00"] = (time ?? "").split(":");
2750
+ return /^\\d{1,2}$/.test(second) && Number(second) !== 0;
2749
2751
  }
2750
2752
 
2751
- function formatTimeForDisplay(time: string) {
2752
- const [hour = "0", minute = "0"] = time.split(":");
2753
+ function formatTimeForDisplay(time: string, showSeconds = false) {
2754
+ const [hour = "0", minute = "0", second = "00"] = time.split(":");
2753
2755
  const hourNumber = Number(hour);
2754
2756
  const suffix = hourNumber >= 12 ? "PM" : "AM";
2755
2757
  const hour12 = hourNumber % 12 || 12;
2758
+ const normalizedSecond = second.padStart(2, "0");
2759
+ const formattedTime = showSeconds
2760
+ ? \`\${hour12.toString().padStart(2, "0")}:\${minute.padStart(2, "0")}:\${normalizedSecond}\`
2761
+ : \`\${hour12.toString().padStart(2, "0")}:\${minute.padStart(2, "0")}\`;
2756
2762
 
2757
- return \`\${hour12.toString().padStart(2, "0")}:\${minute.padStart(2, "0")} \${suffix}\`;
2763
+ return \`\${formattedTime} \${suffix}\`;
2758
2764
  }
2759
2765
 
2760
- function formatDateForDisplay(date?: Date, time?: string) {
2766
+ function formatDateForDisplay(date?: Date, time?: string, showSeconds = false) {
2761
2767
  if (!date) return "";
2762
2768
 
2763
2769
  const month = (date.getMonth() + 1).toString().padStart(2, "0");
2764
2770
  const day = date.getDate().toString().padStart(2, "0");
2765
2771
  const year = date.getFullYear();
2766
- const formattedTime = formatTimeForDisplay(time ?? DEFAULT_START_TIME);
2772
+ const formattedTime = formatTimeForDisplay(
2773
+ time ?? DEFAULT_START_TIME,
2774
+ showSeconds
2775
+ );
2767
2776
 
2768
2777
  return \`\${day}/\${month}/\${year} \${formattedTime}\`;
2769
2778
  }
2770
2779
 
2780
+ function formatDateOnlyForDisplay(date?: Date) {
2781
+ if (!date) return "";
2782
+
2783
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
2784
+ const day = date.getDate().toString().padStart(2, "0");
2785
+ const year = date.getFullYear();
2786
+
2787
+ return \`\${day}/\${month}/\${year}\`;
2788
+ }
2789
+
2790
+ function formatValueForDisplay(
2791
+ value: DateTimePickerValue,
2792
+ variant: DateTimePickerVariant,
2793
+ showEndTime: boolean,
2794
+ hasTimeValue: boolean,
2795
+ showSeconds: boolean
2796
+ ) {
2797
+ if (variant === "date-only") {
2798
+ return formatDateOnlyForDisplay(value.date);
2799
+ }
2800
+
2801
+ if (variant === "time-only") {
2802
+ if (!hasTimeValue) return "";
2803
+
2804
+ return showEndTime
2805
+ ? \`\${formatTimeForDisplay(value.startTime, showSeconds)} - \${formatTimeForDisplay(value.endTime, showSeconds)}\`
2806
+ : formatTimeForDisplay(value.startTime, showSeconds);
2807
+ }
2808
+
2809
+ return formatDateForDisplay(value.date, value.startTime, showSeconds);
2810
+ }
2811
+
2812
+ function getDefaultPlaceholder(
2813
+ variant: DateTimePickerVariant,
2814
+ showSeconds: boolean
2815
+ ) {
2816
+ if (variant === "date-only") return "--/--/----";
2817
+ if (variant === "time-only") {
2818
+ return showSeconds ? "--:--:-- --" : "--:-- --";
2819
+ }
2820
+
2821
+ if (showSeconds) return "--/--/---- --:--:-- --";
2822
+
2823
+ return DEFAULT_PLACEHOLDER;
2824
+ }
2825
+
2771
2826
  function parseDatePart(datePart: string) {
2772
2827
  const isoMatch = datePart.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/);
2773
2828
  const dayFirstMatch = datePart.match(/^(\\d{1,2})[/-](\\d{1,2})[/-](\\d{4})$/);
@@ -2979,16 +3034,20 @@ function splitTypedDateInput(value: string) {
2979
3034
  };
2980
3035
  }
2981
3036
 
2982
- function isPotentiallyValidTimeDigits(timeDigits: string) {
3037
+ function isPotentiallyValidTimeDigits(timeDigits: string, showSeconds: boolean) {
2983
3038
  const hourValue = timeDigits.slice(0, 2);
2984
3039
  const minuteValue = timeDigits.slice(2, 4);
3040
+ const secondValue = showSeconds ? timeDigits.slice(4, 6) : "";
2985
3041
  const hour = Number(hourValue);
2986
3042
  const minute = Number(minuteValue);
3043
+ const second = Number(secondValue);
2987
3044
 
2988
3045
  if (hourValue.length === 1 && hour > 1) return false;
2989
3046
  if (hourValue.length === 2 && (hour < 1 || hour > 12)) return false;
2990
3047
  if (minuteValue.length === 1 && minute > 5) return false;
2991
3048
  if (minuteValue.length === 2 && minute > 59) return false;
3049
+ if (showSeconds && secondValue.length === 1 && second > 5) return false;
3050
+ if (showSeconds && secondValue.length === 2 && second > 59) return false;
2992
3051
 
2993
3052
  return true;
2994
3053
  }
@@ -3001,26 +3060,65 @@ function formatMeridiemInput(letters: string) {
3001
3060
  return null;
3002
3061
  }
3003
3062
 
3004
- function formatTimeInput(restValue: string) {
3063
+ function formatTimeInput(restValue: string, showSeconds: boolean) {
3005
3064
  const normalizedValue = restValue.toUpperCase();
3006
- const timeDigits = normalizedValue.replace(/\\D/g, "").slice(0, 4);
3065
+ const maxTimeDigits = showSeconds ? 6 : 4;
3066
+ const timeDigits = normalizedValue.replace(/\\D/g, "").slice(0, maxTimeDigits);
3007
3067
  const meridiemLetters = normalizedValue.replace(/[^A-Z]/g, "");
3008
3068
  const meridiem = formatMeridiemInput(meridiemLetters.slice(0, 2));
3009
3069
 
3010
3070
  if (!timeDigits && !meridiem) return "";
3011
- if (!isPotentiallyValidTimeDigits(timeDigits) || meridiem === null) {
3071
+ if (!isPotentiallyValidTimeDigits(timeDigits, showSeconds) || meridiem === null) {
3012
3072
  return null;
3013
3073
  }
3014
3074
 
3015
- const timeValue =
3016
- timeDigits.length <= 2
3017
- ? timeDigits
3018
- : \`\${timeDigits.slice(0, 2)}:\${timeDigits.slice(2, 4)}\`;
3075
+ let timeValue = timeDigits.slice(0, 2);
3076
+ if (timeDigits.length > 2) {
3077
+ timeValue = \`\${timeValue}:\${timeDigits.slice(2, 4)}\`;
3078
+ }
3079
+ if (showSeconds && timeDigits.length > 4) {
3080
+ timeValue = \`\${timeValue}:\${timeDigits.slice(4, 6)}\`;
3081
+ }
3019
3082
 
3020
3083
  return [timeValue, meridiem].filter(Boolean).join(" ");
3021
3084
  }
3022
3085
 
3023
- function sanitizeTypedDateTimeInput(value: string, previousValue: string) {
3086
+ function sanitizeTypedDateInput(value: string, previousValue: string) {
3087
+ const trimmedValue = value.trimStart();
3088
+ if (/^[A-Za-z]/.test(trimmedValue)) return previousValue;
3089
+
3090
+ const normalizedValue = value.toUpperCase().replace(/[^0-9\\s/-]/g, "");
3091
+ const { dateDigits, dateValue, isValid } = splitTypedDateInput(normalizedValue);
3092
+ const limitedDateDigits = dateDigits.slice(0, 8);
3093
+
3094
+ if (!limitedDateDigits) return "";
3095
+ if (!isValid) return previousValue;
3096
+
3097
+ return dateValue;
3098
+ }
3099
+
3100
+ function sanitizeTypedTimeInput(
3101
+ value: string,
3102
+ previousValue: string,
3103
+ showSeconds: boolean
3104
+ ) {
3105
+ const trimmedValue = value.trimStart();
3106
+ if (/^[A-Za-z]/.test(trimmedValue) && !/^[AP]/i.test(trimmedValue)) {
3107
+ return previousValue;
3108
+ }
3109
+
3110
+ const normalizedValue = value.toUpperCase().replace(/[^0-9\\s:APM]/g, "");
3111
+ const formattedTime = formatTimeInput(normalizedValue, showSeconds);
3112
+ if (formattedTime === null) return previousValue;
3113
+
3114
+ return formattedTime;
3115
+ }
3116
+
3117
+ function sanitizeTypedDateTimeInput(
3118
+ value: string,
3119
+ previousValue: string,
3120
+ showSeconds: boolean
3121
+ ) {
3024
3122
  const trimmedValue = value.trimStart();
3025
3123
  if (/^[A-Za-z]/.test(trimmedValue)) return previousValue;
3026
3124
 
@@ -3034,22 +3132,59 @@ function sanitizeTypedDateTimeInput(value: string, previousValue: string) {
3034
3132
  if (!limitedDateDigits) return "";
3035
3133
  if (!isValid) return previousValue;
3036
3134
 
3037
- const formattedTime = isComplete ? formatTimeInput(restValue) : "";
3135
+ const formattedTime = isComplete ? formatTimeInput(restValue, showSeconds) : "";
3038
3136
  if (formattedTime === null) return previousValue;
3039
3137
 
3040
3138
  return [dateValue, formattedTime].filter(Boolean).join(" ");
3041
3139
  }
3042
3140
 
3043
- type DateTimeInputSegment = keyof typeof DATE_TIME_INPUT_SEGMENT_RANGES;
3044
-
3045
- function getDateTimeInputSegment(cursorPosition: number): DateTimeInputSegment {
3046
- if (cursorPosition <= DATE_TIME_INPUT_SEGMENT_RANGES.day[1]) return "day";
3047
- if (cursorPosition <= DATE_TIME_INPUT_SEGMENT_RANGES.month[1]) return "month";
3048
- if (cursorPosition <= DATE_TIME_INPUT_SEGMENT_RANGES.year[1]) return "year";
3049
- if (cursorPosition <= DATE_TIME_INPUT_SEGMENT_RANGES.hour[1]) return "hour";
3050
- if (cursorPosition <= DATE_TIME_INPUT_SEGMENT_RANGES.minute[1]) {
3141
+ type DateTimeInputSegment =
3142
+ | "day"
3143
+ | "month"
3144
+ | "year"
3145
+ | "hour"
3146
+ | "minute"
3147
+ | "second"
3148
+ | "meridiem";
3149
+
3150
+ function getDateTimeInputSegmentRanges(showSeconds: boolean) {
3151
+ return showSeconds
3152
+ ? ({
3153
+ day: [0, 2],
3154
+ month: [3, 5],
3155
+ year: [6, 10],
3156
+ hour: [11, 13],
3157
+ minute: [14, 16],
3158
+ second: [17, 19],
3159
+ meridiem: [20, 22],
3160
+ } satisfies Record<DateTimeInputSegment, readonly [number, number]>)
3161
+ : ({
3162
+ day: [0, 2],
3163
+ month: [3, 5],
3164
+ year: [6, 10],
3165
+ hour: [11, 13],
3166
+ minute: [14, 16],
3167
+ second: [17, 19],
3168
+ meridiem: [17, 19],
3169
+ } satisfies Record<DateTimeInputSegment, readonly [number, number]>);
3170
+ }
3171
+
3172
+ function getDateTimeInputSegment(
3173
+ cursorPosition: number,
3174
+ showSeconds: boolean
3175
+ ): DateTimeInputSegment {
3176
+ const ranges = getDateTimeInputSegmentRanges(showSeconds);
3177
+
3178
+ if (cursorPosition <= ranges.day[1]) return "day";
3179
+ if (cursorPosition <= ranges.month[1]) return "month";
3180
+ if (cursorPosition <= ranges.year[1]) return "year";
3181
+ if (cursorPosition <= ranges.hour[1]) return "hour";
3182
+ if (cursorPosition <= ranges.minute[1]) {
3051
3183
  return "minute";
3052
3184
  }
3185
+ if (showSeconds && cursorPosition <= ranges.second[1]) {
3186
+ return "second";
3187
+ }
3053
3188
 
3054
3189
  return "meridiem";
3055
3190
  }
@@ -3062,12 +3197,13 @@ function stepDateTimeInputValue(
3062
3197
  value: string,
3063
3198
  cursorPosition: number,
3064
3199
  direction: 1 | -1,
3065
- fallbackTime: string
3200
+ fallbackTime: string,
3201
+ showSeconds: boolean
3066
3202
  ) {
3067
3203
  const typedDateTime = parseTypedDateTime(value);
3068
3204
  if (!typedDateTime) return null;
3069
3205
 
3070
- const segment = getDateTimeInputSegment(cursorPosition);
3206
+ const segment = getDateTimeInputSegment(cursorPosition, showSeconds);
3071
3207
  const nextDate = new Date(typedDateTime.date);
3072
3208
  const [hourValue = "0", minuteValue = "0", secondValue = "00"] = (
3073
3209
  typedDateTime.startTime ?? fallbackTime
@@ -3104,6 +3240,8 @@ function stepDateTimeInputValue(
3104
3240
  nextDate.setHours(nextDate.getHours() + direction);
3105
3241
  } else if (segment === "minute") {
3106
3242
  nextDate.setMinutes(nextDate.getMinutes() + direction);
3243
+ } else if (segment === "second") {
3244
+ nextDate.setSeconds(nextDate.getSeconds() + direction);
3107
3245
  } else {
3108
3246
  nextDate.setHours(nextDate.getHours() + 12 * direction);
3109
3247
  }
@@ -3126,6 +3264,34 @@ function stepDateTimeInputValue(
3126
3264
  };
3127
3265
  }
3128
3266
 
3267
+ function formatTimeForTimeInput(time: string, showSeconds: boolean) {
3268
+ const normalizedTime = parseTimePart(time) ?? DEFAULT_START_TIME;
3269
+ const [hour = "00", minute = "00", second = "00"] = normalizedTime.split(":");
3270
+
3271
+ return showSeconds ? \`\${hour}:\${minute}:\${second}\` : \`\${hour}:\${minute}\`;
3272
+ }
3273
+
3274
+ function normalizeTimeInputValue(time: string, fallbackTime: string) {
3275
+ return parseTimePart(time) ?? parseTimePart(fallbackTime) ?? DEFAULT_START_TIME;
3276
+ }
3277
+
3278
+ function openNativeTimePicker(input: HTMLInputElement | null) {
3279
+ if (!input) return;
3280
+
3281
+ input.focus();
3282
+
3283
+ const showPicker = (
3284
+ input as HTMLInputElement & { showPicker?: () => void }
3285
+ ).showPicker;
3286
+ if (!showPicker) return;
3287
+
3288
+ try {
3289
+ showPicker.call(input);
3290
+ } catch {
3291
+ input.focus();
3292
+ }
3293
+ }
3294
+
3129
3295
  function parseTypedDateTime(value: string) {
3130
3296
  const typedValue = value.trim();
3131
3297
  if (!typedValue) return undefined;
@@ -3144,9 +3310,40 @@ function parseTypedDateTime(value: string) {
3144
3310
  return { date, startTime };
3145
3311
  }
3146
3312
 
3147
- function formatHiddenValue(value: DateTimePickerValue) {
3313
+ function parseTypedDate(value: string) {
3314
+ const typedValue = value.trim();
3315
+ if (!typedValue) return undefined;
3316
+
3317
+ const typedMatch = typedValue.match(
3318
+ /^(\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[/-]\\d{1,2}[/-]\\d{4})$/
3319
+ );
3320
+ if (!typedMatch) return null;
3321
+
3322
+ return parseDatePart(typedValue);
3323
+ }
3324
+
3325
+ function formatHiddenValue(
3326
+ value: DateTimePickerValue,
3327
+ variant: DateTimePickerVariant,
3328
+ showEndTime: boolean,
3329
+ hasTimeValue: boolean
3330
+ ) {
3331
+ if (variant === "time-only") {
3332
+ if (!hasTimeValue) return "";
3333
+
3334
+ return showEndTime ? \`\${value.startTime}/\${value.endTime}\` : value.startTime;
3335
+ }
3336
+
3148
3337
  if (!value.date) return "";
3149
3338
 
3339
+ if (variant === "date-only") {
3340
+ const month = (value.date.getMonth() + 1).toString().padStart(2, "0");
3341
+ const day = value.date.getDate().toString().padStart(2, "0");
3342
+ const year = value.date.getFullYear();
3343
+
3344
+ return \`\${year}-\${month}-\${day}\`;
3345
+ }
3346
+
3150
3347
  const month = (value.date.getMonth() + 1).toString().padStart(2, "0");
3151
3348
  const day = value.date.getDate().toString().padStart(2, "0");
3152
3349
  const year = value.date.getFullYear();
@@ -3174,20 +3371,101 @@ function FigmaCalendarIcon({ className }: { className?: string }) {
3174
3371
  );
3175
3372
  }
3176
3373
 
3374
+ function CalendarDropdown({
3375
+ id,
3376
+ label,
3377
+ value,
3378
+ options,
3379
+ open,
3380
+ onOpenChange,
3381
+ onValueChange,
3382
+ }: {
3383
+ id: string;
3384
+ label: string;
3385
+ value: string;
3386
+ options: CalendarDropdownOption[];
3387
+ open: boolean;
3388
+ onOpenChange: (open: boolean) => void;
3389
+ onValueChange: (value: string) => void;
3390
+ }) {
3391
+ const selectedOption = options.find((option) => option.value === value);
3392
+
3393
+ return (
3394
+ <div className="relative">
3395
+ <label className="sr-only" htmlFor={id}>
3396
+ {label}
3397
+ </label>
3398
+ <button
3399
+ id={id}
3400
+ type="button"
3401
+ role="combobox"
3402
+ aria-label={label}
3403
+ aria-expanded={open}
3404
+ aria-controls={\`\${id}-options\`}
3405
+ data-value={value}
3406
+ className={cn(
3407
+ CALENDAR_DROPDOWN_TRIGGER_CLASS,
3408
+ "inline-flex items-center justify-between gap-2"
3409
+ )}
3410
+ onClick={() => onOpenChange(!open)}
3411
+ >
3412
+ <span>{selectedOption?.label ?? value}</span>
3413
+ <ChevronRight
3414
+ className={cn("size-3 transition-transform", open && "-rotate-90")}
3415
+ aria-hidden="true"
3416
+ />
3417
+ </button>
3418
+ {open && (
3419
+ <div
3420
+ id={\`\${id}-options\`}
3421
+ role="listbox"
3422
+ aria-label={\`\${label} options\`}
3423
+ className="absolute left-0 top-full z-10 mt-1 max-h-52 min-w-full overflow-y-auto rounded-md border border-solid border-semantic-border-layout bg-semantic-bg-primary p-1 shadow-lg"
3424
+ >
3425
+ {options.map((option) => (
3426
+ <button
3427
+ key={option.value}
3428
+ type="button"
3429
+ role="option"
3430
+ aria-label={option.label}
3431
+ aria-selected={option.value === value}
3432
+ disabled={option.disabled}
3433
+ className={cn(
3434
+ "flex w-full items-center rounded px-2 py-1.5 text-left text-sm text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover disabled:cursor-not-allowed disabled:opacity-40",
3435
+ option.value === value && "bg-semantic-primary text-semantic-text-inverted"
3436
+ )}
3437
+ onClick={() => {
3438
+ if (option.disabled) return;
3439
+
3440
+ onValueChange(option.value);
3441
+ onOpenChange(false);
3442
+ }}
3443
+ >
3444
+ {option.label}
3445
+ </button>
3446
+ ))}
3447
+ </div>
3448
+ )}
3449
+ </div>
3450
+ );
3451
+ }
3452
+
3177
3453
  const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3178
3454
  (
3179
3455
  {
3180
3456
  className,
3457
+ variant,
3181
3458
  size,
3182
3459
  state,
3183
3460
  value,
3184
3461
  defaultValue,
3185
3462
  onValueChange,
3186
- placeholder = DEFAULT_PLACEHOLDER,
3463
+ placeholder: placeholderProp,
3187
3464
  disabled = false,
3188
3465
  readOnly = false,
3189
3466
  name,
3190
3467
  showEndTime = true,
3468
+ showSeconds,
3191
3469
  showClear = true,
3192
3470
  closeOnSelect = false,
3193
3471
  startTimeLabel = "Start Time",
@@ -3206,28 +3484,52 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3206
3484
  ) => {
3207
3485
  const generatedId = React.useId();
3208
3486
  const triggerId = id ?? generatedId;
3487
+ const pickerVariant = variant ?? "date-time";
3488
+ const showCalendar = pickerVariant !== "time-only";
3489
+ const showTimeFields = pickerVariant !== "date-only";
3490
+ const resolvedShowEndTime = showTimeFields && showEndTime;
3209
3491
  const isValueControlled = value !== undefined;
3210
3492
  const isOpenControlled = controlledOpen !== undefined;
3211
3493
  const [internalValue, setInternalValue] = React.useState(() =>
3212
3494
  normalizeValue(defaultValue)
3213
3495
  );
3496
+ const [internalHasTimeValue, setInternalHasTimeValue] = React.useState(() =>
3497
+ Boolean(defaultValue?.date || defaultValue?.startTime || defaultValue?.endTime)
3498
+ );
3214
3499
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
3215
3500
  const currentValue = normalizeValue(
3216
3501
  isValueControlled ? value : internalValue
3217
3502
  );
3503
+ const hasTimeValue = isValueControlled
3504
+ ? Boolean(value?.date || value?.startTime || value?.endTime)
3505
+ : internalHasTimeValue;
3506
+ const resolvedShowSeconds =
3507
+ showSeconds ??
3508
+ (timeHasVisibleSeconds(currentValue.startTime) ||
3509
+ (resolvedShowEndTime && timeHasVisibleSeconds(currentValue.endTime)));
3510
+ const placeholder =
3511
+ placeholderProp ?? getDefaultPlaceholder(pickerVariant, resolvedShowSeconds);
3218
3512
  const open = isOpenControlled ? controlledOpen : internalOpen;
3219
3513
  const [visibleMonth, setVisibleMonth] = React.useState(() =>
3220
3514
  startOfMonth(currentValue.date ?? new Date())
3221
3515
  );
3222
3516
  const [dateInputValue, setDateInputValue] = React.useState(() =>
3223
- formatDateForDisplay(currentValue.date, currentValue.startTime)
3517
+ formatValueForDisplay(
3518
+ currentValue,
3519
+ pickerVariant,
3520
+ resolvedShowEndTime,
3521
+ hasTimeValue,
3522
+ resolvedShowSeconds
3523
+ )
3224
3524
  );
3225
3525
  const [isDateInputFocused, setIsDateInputFocused] = React.useState(false);
3226
- const [openCalendarSelect, setOpenCalendarSelect] =
3227
- React.useState<CalendarSelect | null>(null);
3228
3526
  const rootRef = React.useRef<HTMLDivElement | null>(null);
3229
3527
  const triggerRef = React.useRef<HTMLDivElement | null>(null);
3230
3528
  const popoverRef = React.useRef<HTMLDivElement | null>(null);
3529
+ const startTimeInputRef = React.useRef<HTMLInputElement | null>(null);
3530
+ const endTimeInputRef = React.useRef<HTMLInputElement | null>(null);
3531
+ const [openCalendarDropdown, setOpenCalendarDropdown] =
3532
+ React.useState<CalendarDropdownKind | null>(null);
3231
3533
  const usesContainerPortal = portalContainer !== undefined;
3232
3534
  const floatingStrategy: Strategy = usesContainerPortal
3233
3535
  ? "absolute"
@@ -3270,9 +3572,12 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3270
3572
  () => getCalendarDays(visibleMonth),
3271
3573
  [visibleMonth]
3272
3574
  );
3273
- const displayValue = formatDateForDisplay(
3274
- currentValue.date,
3275
- currentValue.startTime
3575
+ const displayValue = formatValueForDisplay(
3576
+ currentValue,
3577
+ pickerVariant,
3578
+ resolvedShowEndTime,
3579
+ hasTimeValue,
3580
+ resolvedShowSeconds
3276
3581
  );
3277
3582
  const effectiveMinDate = React.useMemo(() => {
3278
3583
  if (!disablePastDates) return minDate;
@@ -3291,12 +3596,12 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3291
3596
 
3292
3597
  const setOpen = React.useCallback(
3293
3598
  (nextOpen: boolean) => {
3294
- if (!isOpenControlled) {
3295
- setInternalOpen(nextOpen);
3599
+ if (!nextOpen) {
3600
+ setOpenCalendarDropdown(null);
3296
3601
  }
3297
3602
 
3298
- if (!nextOpen) {
3299
- setOpenCalendarSelect(null);
3603
+ if (!isOpenControlled) {
3604
+ setInternalOpen(nextOpen);
3300
3605
  }
3301
3606
 
3302
3607
  onOpenChange?.(nextOpen);
@@ -3304,30 +3609,6 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3304
3609
  [isOpenControlled, onOpenChange]
3305
3610
  );
3306
3611
 
3307
- const handleCalendarSelectOpenChange = React.useCallback(
3308
- (select: CalendarSelect, nextOpen: boolean) => {
3309
- setOpenCalendarSelect((currentSelect) => {
3310
- if (nextOpen) return select;
3311
-
3312
- return currentSelect === select ? null : currentSelect;
3313
- });
3314
- },
3315
- []
3316
- );
3317
-
3318
- const handleCalendarSelectTriggerPointerDown = React.useCallback(
3319
- (
3320
- select: CalendarSelect,
3321
- event: React.PointerEvent<HTMLButtonElement>
3322
- ) => {
3323
- if (openCalendarSelect !== select) return;
3324
-
3325
- event.preventDefault();
3326
- setOpenCalendarSelect(null);
3327
- },
3328
- [openCalendarSelect]
3329
- );
3330
-
3331
3612
  const setTriggerRef = React.useCallback(
3332
3613
  (node: HTMLDivElement | null) => {
3333
3614
  triggerRef.current = node;
@@ -3356,29 +3637,20 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3356
3637
  }
3357
3638
  }, [displayValue, isDateInputFocused]);
3358
3639
 
3359
- React.useEffect(() => {
3360
- if (!open) {
3361
- setOpenCalendarSelect(null);
3362
- }
3363
- }, [open]);
3364
-
3365
3640
  React.useEffect(() => {
3366
3641
  if (!open) return;
3367
3642
 
3368
3643
  const handlePointerDown = (event: MouseEvent) => {
3369
3644
  if (
3370
3645
  !isPointerInsideElement(event, rootRef.current) &&
3371
- !isPointerInsideElement(event, popoverRef.current) &&
3372
- !isPointerInsideSelector(event, CALENDAR_SELECT_CONTENT_SELECTOR)
3646
+ !isPointerInsideElement(event, popoverRef.current)
3373
3647
  ) {
3374
- setOpenCalendarSelect(null);
3375
3648
  setOpen(false);
3376
3649
  }
3377
3650
  };
3378
3651
 
3379
3652
  const handleKeyDown = (event: KeyboardEvent) => {
3380
3653
  if (event.key === "Escape") {
3381
- setOpenCalendarSelect(null);
3382
3654
  setOpen(false);
3383
3655
  }
3384
3656
  };
@@ -3393,9 +3665,15 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3393
3665
  }, [open, setOpen]);
3394
3666
 
3395
3667
  const updateValue = React.useCallback(
3396
- (nextValue: DateTimePickerValue) => {
3668
+ (
3669
+ nextValue: DateTimePickerValue,
3670
+ options?: { hasTimeValue?: boolean }
3671
+ ) => {
3397
3672
  if (!isValueControlled) {
3398
3673
  setInternalValue(nextValue);
3674
+ if (options?.hasTimeValue !== undefined) {
3675
+ setInternalHasTimeValue(options.hasTimeValue);
3676
+ }
3399
3677
  }
3400
3678
 
3401
3679
  onValueChange?.(nextValue);
@@ -3412,7 +3690,7 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3412
3690
  date: undefined,
3413
3691
  startTime: currentValue.startTime,
3414
3692
  endTime: currentValue.endTime,
3415
- });
3693
+ }, { hasTimeValue: false });
3416
3694
  };
3417
3695
 
3418
3696
  const yearOptions = React.useMemo(
@@ -3427,12 +3705,134 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3427
3705
  [effectiveMinDate, maxDate]
3428
3706
  );
3429
3707
 
3708
+ const syncCalendarMonthAndValue = React.useCallback(
3709
+ (nextMonth: Date) => {
3710
+ const clampedMonth = clampMonth(nextMonth, effectiveMinDate, maxDate);
3711
+ setVisibleMonth(clampedMonth);
3712
+
3713
+ if (pickerVariant === "time-only" || !currentValue.date) return;
3714
+
3715
+ const selectedDay = currentValue.date.getDate();
3716
+ const daysInTargetMonth = getDaysInMonth(
3717
+ clampedMonth.getFullYear(),
3718
+ clampedMonth.getMonth() + 1
3719
+ );
3720
+
3721
+ if (selectedDay > daysInTargetMonth) {
3722
+ const fallbackDate = new Date(
3723
+ clampedMonth.getFullYear(),
3724
+ clampedMonth.getMonth(),
3725
+ 1
3726
+ );
3727
+
3728
+ if (!isSelectableDay(fallbackDate, effectiveMinDate, maxDate)) {
3729
+ updateValue({ ...currentValue, date: undefined });
3730
+ return;
3731
+ }
3732
+
3733
+ updateValue({ ...currentValue, date: fallbackDate });
3734
+ return;
3735
+ }
3736
+
3737
+ const nextSelectedDate = new Date(
3738
+ clampedMonth.getFullYear(),
3739
+ clampedMonth.getMonth(),
3740
+ selectedDay
3741
+ );
3742
+
3743
+ if (!isSelectableDay(nextSelectedDate, effectiveMinDate, maxDate)) {
3744
+ updateValue({ ...currentValue, date: undefined });
3745
+ return;
3746
+ }
3747
+
3748
+ if (!isSameDay(currentValue.date, nextSelectedDate)) {
3749
+ updateValue({ ...currentValue, date: nextSelectedDate });
3750
+ }
3751
+ },
3752
+ [
3753
+ currentValue,
3754
+ effectiveMinDate,
3755
+ maxDate,
3756
+ pickerVariant,
3757
+ updateValue,
3758
+ ]
3759
+ );
3760
+
3430
3761
  const handleTypedDateChange = (
3431
3762
  event: React.ChangeEvent<HTMLInputElement>
3432
3763
  ) => {
3764
+ if (pickerVariant === "time-only") {
3765
+ const nextInputValue = sanitizeTypedTimeInput(
3766
+ event.target.value,
3767
+ dateInputValue,
3768
+ resolvedShowSeconds
3769
+ );
3770
+ setDateInputValue(nextInputValue);
3771
+
3772
+ if (
3773
+ nextInputValue === dateInputValue &&
3774
+ event.target.value !== dateInputValue
3775
+ ) {
3776
+ event.currentTarget.value = nextInputValue;
3777
+ return;
3778
+ }
3779
+
3780
+ const typedTime = parseTimePart(nextInputValue);
3781
+ if (typedTime === undefined) {
3782
+ updateValue({ ...currentValue, date: undefined }, { hasTimeValue: false });
3783
+ return;
3784
+ }
3785
+
3786
+ if (typedTime) {
3787
+ updateValue(
3788
+ {
3789
+ ...currentValue,
3790
+ startTime: typedTime,
3791
+ },
3792
+ { hasTimeValue: true }
3793
+ );
3794
+ }
3795
+
3796
+ return;
3797
+ }
3798
+
3799
+ if (pickerVariant === "date-only") {
3800
+ const nextInputValue = sanitizeTypedDateInput(
3801
+ event.target.value,
3802
+ dateInputValue
3803
+ );
3804
+ setDateInputValue(nextInputValue);
3805
+
3806
+ if (
3807
+ nextInputValue === dateInputValue &&
3808
+ event.target.value !== dateInputValue
3809
+ ) {
3810
+ event.currentTarget.value = nextInputValue;
3811
+ return;
3812
+ }
3813
+
3814
+ const typedDate = parseTypedDate(nextInputValue);
3815
+ if (typedDate === undefined) {
3816
+ updateValue({ ...currentValue, date: undefined });
3817
+ return;
3818
+ }
3819
+
3820
+ if (
3821
+ typedDate &&
3822
+ !(effectiveMinDate && isBeforeDay(typedDate, effectiveMinDate)) &&
3823
+ !(maxDate && isAfterDay(typedDate, maxDate))
3824
+ ) {
3825
+ updateValue({ ...currentValue, date: typedDate });
3826
+ updateVisibleMonth(typedDate);
3827
+ }
3828
+
3829
+ return;
3830
+ }
3831
+
3433
3832
  const nextInputValue = sanitizeTypedDateTimeInput(
3434
3833
  event.target.value,
3435
- dateInputValue
3834
+ dateInputValue,
3835
+ resolvedShowSeconds
3436
3836
  );
3437
3837
  setDateInputValue(nextInputValue);
3438
3838
 
@@ -3469,6 +3869,7 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3469
3869
  const handleTypedDateKeyDown = (
3470
3870
  event: React.KeyboardEvent<HTMLInputElement>
3471
3871
  ) => {
3872
+ if (pickerVariant === "time-only") return;
3472
3873
  if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
3473
3874
 
3474
3875
  const direction = event.key === "ArrowUp" ? 1 : -1;
@@ -3479,7 +3880,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3479
3880
  dateInputValue,
3480
3881
  cursorPosition,
3481
3882
  direction,
3482
- currentValue.startTime
3883
+ currentValue.startTime,
3884
+ resolvedShowSeconds
3483
3885
  );
3484
3886
 
3485
3887
  if (!steppedDateTime) return;
@@ -3495,16 +3897,26 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3495
3897
 
3496
3898
  const nextInputValue = formatDateForDisplay(
3497
3899
  steppedDateTime.date,
3498
- steppedDateTime.startTime
3900
+ steppedDateTime.startTime,
3901
+ resolvedShowSeconds
3499
3902
  );
3903
+ const dateTimeInputSegmentRanges =
3904
+ getDateTimeInputSegmentRanges(resolvedShowSeconds);
3500
3905
  const [selectionStart, selectionEnd] =
3501
- DATE_TIME_INPUT_SEGMENT_RANGES[steppedDateTime.segment];
3906
+ dateTimeInputSegmentRanges[steppedDateTime.segment];
3907
+ const resolvedInputValue =
3908
+ pickerVariant === "date-only"
3909
+ ? formatDateOnlyForDisplay(steppedDateTime.date)
3910
+ : nextInputValue;
3502
3911
 
3503
- setDateInputValue(nextInputValue);
3912
+ setDateInputValue(resolvedInputValue);
3504
3913
  updateValue({
3505
3914
  ...currentValue,
3506
3915
  date: steppedDateTime.date,
3507
- startTime: steppedDateTime.startTime,
3916
+ startTime:
3917
+ pickerVariant === "date-only"
3918
+ ? currentValue.startTime
3919
+ : steppedDateTime.startTime,
3508
3920
  });
3509
3921
  updateVisibleMonth(steppedDateTime.date);
3510
3922
 
@@ -3516,7 +3928,13 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3516
3928
  const handleTypedDateBlur = () => {
3517
3929
  setIsDateInputFocused(false);
3518
3930
  setDateInputValue(
3519
- formatDateForDisplay(currentValue.date, currentValue.startTime)
3931
+ formatValueForDisplay(
3932
+ currentValue,
3933
+ pickerVariant,
3934
+ resolvedShowEndTime,
3935
+ hasTimeValue,
3936
+ resolvedShowSeconds
3937
+ )
3520
3938
  );
3521
3939
  };
3522
3940
 
@@ -3530,7 +3948,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3530
3948
  ref={setPopoverRef}
3531
3949
  role="dialog"
3532
3950
  aria-modal="false"
3533
- aria-labelledby={\`\${triggerId}-calendar-heading\`}
3951
+ aria-labelledby={showCalendar ? \`\${triggerId}-calendar-heading\` : undefined}
3952
+ aria-label={showCalendar ? undefined : "Time picker"}
3534
3953
  className={cn(
3535
3954
  "rounded-lg border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-lg flex flex-col min-h-0 overflow-y-auto overflow-x-hidden overscroll-contain pointer-events-auto",
3536
3955
  "[scrollbar-gutter:stable] [scrollbar-width:thin] [scrollbar-color:var(--semantic-border-secondary)_transparent]",
@@ -3545,66 +3964,34 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3545
3964
  zIndex: 10050,
3546
3965
  visibility: isPositioned ? undefined : "hidden",
3547
3966
  }}
3548
- onPointerDown={(event) => {
3549
- if (
3550
- event.target instanceof Element &&
3551
- !event.target.closest(CALENDAR_SELECT_TRIGGER_SELECTOR)
3552
- ) {
3553
- setOpenCalendarSelect(null);
3554
- }
3555
-
3556
- event.stopPropagation();
3557
- }}
3967
+ onPointerDown={(event) => event.stopPropagation()}
3558
3968
  onMouseDown={(event) => event.stopPropagation()}
3559
3969
  onWheel={(event) => event.stopPropagation()}
3560
3970
  onTouchMove={(event) => event.stopPropagation()}
3561
3971
  >
3562
- <div className="p-3 touch-pan-y">
3563
- <div className="mb-3 flex items-center justify-between gap-2">
3564
- <button
3565
- type="button"
3566
- aria-label="Previous month"
3567
- className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
3568
- onClick={() => updateVisibleMonth(addMonths(visibleMonth, -1))}
3569
- >
3570
- <ChevronLeft className="size-4" aria-hidden="true" />
3571
- </button>
3572
- <div className="flex min-w-0 items-center gap-1.5">
3573
- <label className="sr-only" htmlFor={\`\${triggerId}-month\`}>
3574
- Month
3575
- </label>
3576
- <Select
3577
- open={openCalendarSelect === "month"}
3578
- onOpenChange={(nextOpen) =>
3579
- handleCalendarSelectOpenChange("month", nextOpen)
3580
- }
3581
- value={visibleMonth.getMonth().toString()}
3582
- onValueChange={(nextMonth) =>
3583
- updateVisibleMonth(
3584
- new Date(
3585
- visibleMonth.getFullYear(),
3586
- Number(nextMonth),
3587
- 1
3588
- )
3589
- )
3972
+ {showCalendar && (
3973
+ <div className="p-3 touch-pan-y">
3974
+ <div className="mb-3 flex items-center justify-between gap-2">
3975
+ <button
3976
+ type="button"
3977
+ aria-label="Previous month"
3978
+ className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
3979
+ onClick={() =>
3980
+ syncCalendarMonthAndValue(addMonths(visibleMonth, -1))
3590
3981
  }
3591
3982
  >
3592
- <SelectTrigger
3983
+ <ChevronLeft className="size-4" aria-hidden="true" />
3984
+ </button>
3985
+ <div className="flex min-w-0 items-center gap-1.5">
3986
+ <CalendarDropdown
3593
3987
  id={\`\${triggerId}-month\`}
3594
- data-date-time-picker-calendar-select-trigger=""
3595
- aria-label="Month"
3596
- className={CALENDAR_SELECT_TRIGGER_CLASS}
3597
- onPointerDown={(event) =>
3598
- handleCalendarSelectTriggerPointerDown("month", event)
3599
- }
3600
- >
3601
- <SelectValue />
3602
- </SelectTrigger>
3603
- <SelectContent
3604
- data-date-time-picker-calendar-select=""
3605
- className={CALENDAR_SELECT_CONTENT_CLASS}
3606
- >
3607
- {monthNames.map((monthName, monthIndex) => {
3988
+ label="Month"
3989
+ value={visibleMonth.getMonth().toString()}
3990
+ open={openCalendarDropdown === "month"}
3991
+ onOpenChange={(nextOpen) =>
3992
+ setOpenCalendarDropdown(nextOpen ? "month" : null)
3993
+ }
3994
+ options={monthNames.map((monthName, monthIndex) => {
3608
3995
  const optionMonth = new Date(
3609
3996
  visibleMonth.getFullYear(),
3610
3997
  monthIndex,
@@ -3615,134 +4002,129 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3615
4002
  isMonthBefore(optionMonth, effectiveMinDate)) ||
3616
4003
  (maxDate && isMonthAfter(optionMonth, maxDate));
3617
4004
 
3618
- return (
3619
- <SelectItem
3620
- key={monthName}
3621
- value={monthIndex.toString()}
3622
- disabled={!!isDisabled}
3623
- >
3624
- {monthName}
3625
- </SelectItem>
3626
- );
4005
+ return {
4006
+ value: monthIndex.toString(),
4007
+ label: monthName,
4008
+ disabled: !!isDisabled,
4009
+ };
3627
4010
  })}
3628
- </SelectContent>
3629
- </Select>
3630
- <label className="sr-only" htmlFor={\`\${triggerId}-year\`}>
3631
- Year
3632
- </label>
3633
- <Select
3634
- open={openCalendarSelect === "year"}
3635
- onOpenChange={(nextOpen) =>
3636
- handleCalendarSelectOpenChange("year", nextOpen)
3637
- }
3638
- value={visibleMonth.getFullYear().toString()}
3639
- onValueChange={(nextYear) =>
3640
- updateVisibleMonth(
3641
- new Date(
3642
- Number(nextYear),
3643
- visibleMonth.getMonth(),
3644
- 1
3645
- )
3646
- )
3647
- }
3648
- >
3649
- <SelectTrigger
4011
+ onValueChange={(nextMonth) => {
4012
+ syncCalendarMonthAndValue(
4013
+ new Date(
4014
+ visibleMonth.getFullYear(),
4015
+ Number(nextMonth),
4016
+ 1
4017
+ )
4018
+ );
4019
+ }}
4020
+ />
4021
+ <CalendarDropdown
3650
4022
  id={\`\${triggerId}-year\`}
3651
- data-date-time-picker-calendar-select-trigger=""
3652
- aria-label="Year"
3653
- className={CALENDAR_SELECT_TRIGGER_CLASS}
3654
- onPointerDown={(event) =>
3655
- handleCalendarSelectTriggerPointerDown("year", event)
3656
- }
3657
- >
3658
- <SelectValue />
3659
- </SelectTrigger>
3660
- <SelectContent
3661
- data-date-time-picker-calendar-select=""
3662
- className={CALENDAR_SELECT_CONTENT_CLASS}
3663
- >
3664
- {yearOptions.map((year) => (
3665
- <SelectItem key={year} value={year.toString()}>
3666
- {year}
3667
- </SelectItem>
3668
- ))}
3669
- </SelectContent>
3670
- </Select>
3671
- <div id={\`\${triggerId}-calendar-heading\`} className="sr-only">
3672
- {monthFormatter.format(visibleMonth)}
4023
+ label="Year"
4024
+ value={visibleMonth.getFullYear().toString()}
4025
+ open={openCalendarDropdown === "year"}
4026
+ onOpenChange={(nextOpen) =>
4027
+ setOpenCalendarDropdown(nextOpen ? "year" : null)
4028
+ }
4029
+ options={yearOptions.map((year) => ({
4030
+ value: year.toString(),
4031
+ label: year.toString(),
4032
+ }))}
4033
+ onValueChange={(nextYear) => {
4034
+ syncCalendarMonthAndValue(
4035
+ new Date(
4036
+ Number(nextYear),
4037
+ visibleMonth.getMonth(),
4038
+ 1
4039
+ )
4040
+ );
4041
+ }}
4042
+ />
4043
+ <div id={\`\${triggerId}-calendar-heading\`} className="sr-only">
4044
+ {monthFormatter.format(visibleMonth)}
4045
+ </div>
3673
4046
  </div>
3674
- </div>
3675
- <button
3676
- type="button"
3677
- aria-label="Next month"
3678
- className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
3679
- onClick={() => updateVisibleMonth(addMonths(visibleMonth, 1))}
3680
- >
3681
- <ChevronRight className="size-4" aria-hidden="true" />
3682
- </button>
3683
- </div>
3684
-
3685
- <div className="grid grid-cols-7">
3686
- {weekDays.map((day) => (
3687
- <div
3688
- key={day}
3689
- className="flex size-8 items-center justify-center text-xs font-medium text-semantic-text-muted"
4047
+ <button
4048
+ type="button"
4049
+ aria-label="Next month"
4050
+ className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
4051
+ onClick={() =>
4052
+ syncCalendarMonthAndValue(addMonths(visibleMonth, 1))
4053
+ }
3690
4054
  >
3691
- {day}
3692
- </div>
3693
- ))}
3694
- {calendarDays.map((day) => {
3695
- const isCurrentMonth =
3696
- day.getMonth() === visibleMonth.getMonth();
3697
- const isSelected = isSameDay(day, currentValue.date);
3698
- const isToday = isSameDay(day, new Date());
3699
- const isDisabled =
3700
- (effectiveMinDate && isBeforeDay(day, effectiveMinDate)) ||
3701
- (maxDate && isAfterDay(day, maxDate));
3702
- const dayLabel = day.toLocaleDateString("en-US", {
3703
- month: "long",
3704
- day: "numeric",
3705
- year: "numeric",
3706
- });
3707
-
3708
- return (
3709
- <button
3710
- key={day.toISOString()}
3711
- type="button"
3712
- aria-label={dayLabel}
3713
- aria-pressed={isSelected}
3714
- aria-current={isToday ? "date" : undefined}
3715
- disabled={!!isDisabled}
3716
- className={cn(
3717
- "relative flex items-center justify-center size-8 mx-auto rounded-full text-xs transition-colors",
3718
- isSelected
3719
- ? "bg-semantic-primary text-semantic-text-inverted font-semibold"
3720
- : isCurrentMonth
3721
- ? "text-semantic-text-primary hover:bg-semantic-bg-hover"
3722
- : "text-semantic-text-muted hover:bg-semantic-bg-hover",
3723
- isDisabled &&
3724
- "opacity-40 cursor-not-allowed pointer-events-none"
3725
- )}
3726
- onClick={() => {
3727
- if (isDisabled) return;
4055
+ <ChevronRight className="size-4" aria-hidden="true" />
4056
+ </button>
4057
+ </div>
3728
4058
 
3729
- updateValue({ ...currentValue, date: day });
3730
- if (closeOnSelect) {
3731
- setOpen(false);
3732
- }
3733
- }}
4059
+ <div className="grid grid-cols-7">
4060
+ {weekDays.map((day) => (
4061
+ <div
4062
+ key={day}
4063
+ className="flex size-8 items-center justify-center text-xs font-medium text-semantic-text-muted"
3734
4064
  >
3735
- {day.getDate()}
3736
- {isToday && !isSelected && (
3737
- <span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-semantic-primary" />
3738
- )}
3739
- </button>
3740
- );
3741
- })}
4065
+ {day}
4066
+ </div>
4067
+ ))}
4068
+ {calendarDays.map((day) => {
4069
+ const isCurrentMonth =
4070
+ day.getMonth() === visibleMonth.getMonth();
4071
+ const isSelected = isSameDay(day, currentValue.date);
4072
+ const isToday = isSameDay(day, new Date());
4073
+ const isDisabled =
4074
+ (effectiveMinDate && isBeforeDay(day, effectiveMinDate)) ||
4075
+ (maxDate && isAfterDay(day, maxDate));
4076
+ const dayLabel = day.toLocaleDateString("en-US", {
4077
+ month: "long",
4078
+ day: "numeric",
4079
+ year: "numeric",
4080
+ });
4081
+
4082
+ return (
4083
+ <button
4084
+ key={day.toISOString()}
4085
+ type="button"
4086
+ aria-label={dayLabel}
4087
+ aria-pressed={isSelected}
4088
+ aria-current={isToday ? "date" : undefined}
4089
+ disabled={!!isDisabled}
4090
+ className={cn(
4091
+ "relative flex items-center justify-center size-8 mx-auto rounded-full text-xs transition-colors",
4092
+ isSelected
4093
+ ? "bg-semantic-primary text-semantic-text-inverted font-semibold"
4094
+ : isCurrentMonth
4095
+ ? "text-semantic-text-primary hover:bg-semantic-bg-hover"
4096
+ : "text-semantic-text-muted hover:bg-semantic-bg-hover",
4097
+ isDisabled &&
4098
+ "opacity-40 cursor-not-allowed pointer-events-none"
4099
+ )}
4100
+ onClick={() => {
4101
+ if (isDisabled) return;
4102
+
4103
+ updateValue({ ...currentValue, date: day });
4104
+ if (closeOnSelect) {
4105
+ setOpen(false);
4106
+ }
4107
+ }}
4108
+ >
4109
+ {day.getDate()}
4110
+ {isToday && !isSelected && (
4111
+ <span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-semantic-primary" />
4112
+ )}
4113
+ </button>
4114
+ );
4115
+ })}
4116
+ </div>
3742
4117
  </div>
3743
- </div>
4118
+ )}
3744
4119
 
3745
- <div className="space-y-3 border-t border-solid border-semantic-border-layout bg-semantic-bg-primary p-3">
4120
+ {showTimeFields && (
4121
+ <div
4122
+ className={cn(
4123
+ "space-y-3 bg-semantic-bg-primary p-3",
4124
+ showCalendar &&
4125
+ "border-t border-solid border-semantic-border-layout"
4126
+ )}
4127
+ >
3746
4128
  <div className="flex flex-col gap-1.5">
3747
4129
  <label
3748
4130
  htmlFor={\`\${triggerId}-start-time\`}
@@ -3750,28 +4132,41 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3750
4132
  >
3751
4133
  {startTimeLabel}
3752
4134
  </label>
3753
- <div className="relative">
4135
+ <div
4136
+ className="relative cursor-pointer"
4137
+ onClick={() => openNativeTimePicker(startTimeInputRef.current)}
4138
+ >
3754
4139
  <Clock2
3755
4140
  className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-semantic-text-muted"
3756
4141
  aria-hidden="true"
3757
4142
  />
3758
4143
  <input
4144
+ ref={startTimeInputRef}
3759
4145
  id={\`\${triggerId}-start-time\`}
3760
4146
  type="time"
3761
- step="1"
3762
- value={currentValue.startTime}
4147
+ step={resolvedShowSeconds ? "1" : "60"}
4148
+ value={formatTimeForTimeInput(
4149
+ currentValue.startTime,
4150
+ resolvedShowSeconds
4151
+ )}
3763
4152
  className="h-8 w-full rounded-md border border-solid border-semantic-border-input bg-semantic-bg-primary pl-9 pr-3 text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
3764
4153
  onChange={(event) =>
3765
- updateValue({
3766
- ...currentValue,
3767
- startTime: event.target.value,
3768
- })
4154
+ updateValue(
4155
+ {
4156
+ ...currentValue,
4157
+ startTime: normalizeTimeInputValue(
4158
+ event.target.value,
4159
+ currentValue.startTime
4160
+ ),
4161
+ },
4162
+ { hasTimeValue: true }
4163
+ )
3769
4164
  }
3770
4165
  />
3771
4166
  </div>
3772
4167
  </div>
3773
4168
 
3774
- {showEndTime && (
4169
+ {resolvedShowEndTime && (
3775
4170
  <div className="flex flex-col gap-1.5">
3776
4171
  <label
3777
4172
  htmlFor={\`\${triggerId}-end-time\`}
@@ -3779,28 +4174,42 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3779
4174
  >
3780
4175
  {endTimeLabel}
3781
4176
  </label>
3782
- <div className="relative">
4177
+ <div
4178
+ className="relative cursor-pointer"
4179
+ onClick={() => openNativeTimePicker(endTimeInputRef.current)}
4180
+ >
3783
4181
  <Clock2
3784
4182
  className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-semantic-text-muted"
3785
4183
  aria-hidden="true"
3786
4184
  />
3787
4185
  <input
4186
+ ref={endTimeInputRef}
3788
4187
  id={\`\${triggerId}-end-time\`}
3789
4188
  type="time"
3790
- step="1"
3791
- value={currentValue.endTime}
4189
+ step={resolvedShowSeconds ? "1" : "60"}
4190
+ value={formatTimeForTimeInput(
4191
+ currentValue.endTime,
4192
+ resolvedShowSeconds
4193
+ )}
3792
4194
  className="h-8 w-full rounded-md border border-solid border-semantic-border-input bg-semantic-bg-primary pl-9 pr-3 text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
3793
4195
  onChange={(event) =>
3794
- updateValue({
3795
- ...currentValue,
3796
- endTime: event.target.value,
3797
- })
4196
+ updateValue(
4197
+ {
4198
+ ...currentValue,
4199
+ endTime: normalizeTimeInputValue(
4200
+ event.target.value,
4201
+ currentValue.endTime
4202
+ ),
4203
+ },
4204
+ { hasTimeValue: true }
4205
+ )
3798
4206
  }
3799
4207
  />
3800
4208
  </div>
3801
4209
  </div>
3802
4210
  )}
3803
- </div>
4211
+ </div>
4212
+ )}
3804
4213
  </div>,
3805
4214
  portalMount
3806
4215
  );
@@ -3824,7 +4233,12 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3824
4233
  <input
3825
4234
  type="hidden"
3826
4235
  name={name}
3827
- value={formatHiddenValue(currentValue)}
4236
+ value={formatHiddenValue(
4237
+ currentValue,
4238
+ pickerVariant,
4239
+ resolvedShowEndTime,
4240
+ hasTimeValue
4241
+ )}
3828
4242
  />
3829
4243
  )}
3830
4244
  <div
@@ -3846,7 +4260,13 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3846
4260
  placeholder={placeholder}
3847
4261
  aria-haspopup="dialog"
3848
4262
  aria-expanded={open}
3849
- aria-label="Date and time"
4263
+ aria-label={
4264
+ pickerVariant === "date-only"
4265
+ ? "Date"
4266
+ : pickerVariant === "time-only"
4267
+ ? "Time"
4268
+ : "Date and time"
4269
+ }
3850
4270
  className="min-w-0 flex-1 bg-transparent text-sm text-semantic-text-primary outline-none placeholder:text-semantic-text-placeholder disabled:cursor-not-allowed read-only:cursor-not-allowed"
3851
4271
  onFocus={() => {
3852
4272
  setIsDateInputFocused(true);
@@ -3870,7 +4290,7 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
3870
4290
  <button
3871
4291
  type="button"
3872
4292
  disabled={disabled || readOnly}
3873
- aria-label="Open calendar"
4293
+ aria-label={showCalendar ? "Open calendar" : "Open time picker"}
3874
4294
  className="inline-flex shrink-0 items-center justify-center rounded text-semantic-text-muted hover:bg-semantic-bg-hover hover:text-semantic-text-primary disabled:cursor-not-allowed"
3875
4295
  onClick={() => setOpen(!open)}
3876
4296
  >