@sixthshift/design-system 0.1.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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/styles.css +2 -0
- package/dist/theme.generated.css +438 -0
- package/package.json +238 -0
- package/src/components/Avatar/Avatar.tsx +19 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BarChart/BarChart.tsx +51 -0
- package/src/components/BarChart/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
- package/src/components/Breadcrumb/index.ts +5 -0
- package/src/components/Button/Button.tsx +157 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/Calendar.tsx +433 -0
- package/src/components/Calendar/calendar.hooks.ts +329 -0
- package/src/components/Calendar/calendar.types.ts +117 -0
- package/src/components/Calendar/index.ts +28 -0
- package/src/components/Card/Card.tsx +47 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +144 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
- package/src/components/CheckboxGroup/index.ts +5 -0
- package/src/components/Code/Editor/Editor.tsx +161 -0
- package/src/components/Code/Editor/index.ts +2 -0
- package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
- package/src/components/Code/Editor/themes.ts +112 -0
- package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
- package/src/components/Code/ValidationStatus/index.ts +1 -0
- package/src/components/Code/Workspace/StatusBar.tsx +47 -0
- package/src/components/Code/Workspace/Toolbar.tsx +47 -0
- package/src/components/Code/Workspace/Workspace.tsx +109 -0
- package/src/components/Code/Workspace/index.ts +3 -0
- package/src/components/ColorDot/ColorDot.tsx +50 -0
- package/src/components/ColorDot/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.tsx +236 -0
- package/src/components/DatePicker/datepicker.hooks.ts +381 -0
- package/src/components/DatePicker/datepicker.types.ts +140 -0
- package/src/components/DatePicker/index.ts +12 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
- package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
- package/src/components/DateRangePicker/index.ts +2 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
- package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
- package/src/components/DateTimePicker/index.ts +2 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
- package/src/components/DateTimeRangePicker/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +29 -0
- package/src/components/EmptyState/index.ts +1 -0
- package/src/components/Field/Field.tsx +60 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +55 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
- package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
- package/src/components/HeatMap/heat-map.utils.ts +15 -0
- package/src/components/HeatMap/index.ts +3 -0
- package/src/components/HoverCard/HoverCard.tsx +83 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
- package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
- package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
- package/src/components/HoverCard/index.ts +3 -0
- package/src/components/Input/Input.tsx +48 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/LineChart/LineChart.tsx +251 -0
- package/src/components/LineChart/index.ts +1 -0
- package/src/components/Markdown/Markdown.tsx +40 -0
- package/src/components/Markdown/index.ts +1 -0
- package/src/components/Message/Message.tsx +59 -0
- package/src/components/Message/components/MessageBody.tsx +9 -0
- package/src/components/Message/components/MessageDescription.tsx +9 -0
- package/src/components/Message/components/MessageIcon.tsx +13 -0
- package/src/components/Message/components/MessageTitle.tsx +9 -0
- package/src/components/Message/index.ts +10 -0
- package/src/components/MetricList/MetricList.tsx +13 -0
- package/src/components/MetricList/MetricRow.tsx +30 -0
- package/src/components/MetricList/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +146 -0
- package/src/components/Modal/components/ModalBody.tsx +9 -0
- package/src/components/Modal/components/ModalContext.tsx +27 -0
- package/src/components/Modal/components/ModalFooter.tsx +9 -0
- package/src/components/Modal/components/ModalHeader.tsx +29 -0
- package/src/components/Modal/components/index.ts +9 -0
- package/src/components/Modal/index.ts +13 -0
- package/src/components/NavBottom/NavBottom.tsx +53 -0
- package/src/components/NavBottom/index.ts +3 -0
- package/src/components/NavSide/NavItem.tsx +39 -0
- package/src/components/NavSide/NavSide.tsx +40 -0
- package/src/components/NavSide/index.ts +2 -0
- package/src/components/NavSide/types.ts +36 -0
- package/src/components/Pagination/Pagination.tsx +86 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Popover/Popover.tsx +68 -0
- package/src/components/Popover/components/PopoverBody.tsx +32 -0
- package/src/components/Popover/components/PopoverClose.tsx +26 -0
- package/src/components/Popover/components/PopoverContext.tsx +24 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
- package/src/components/Popover/index.ts +10 -0
- package/src/components/ProgressBar/ProgressBar.tsx +42 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.tsx +89 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
- package/src/components/RadioButtonGroup/index.ts +5 -0
- package/src/components/SearchInput/SearchInput.tsx +45 -0
- package/src/components/SearchInput/index.ts +2 -0
- package/src/components/Select/Select.tsx +289 -0
- package/src/components/Select/SelectDropdown.tsx +90 -0
- package/src/components/Select/SelectTrigger.tsx +130 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/useSelectKeyboard.ts +111 -0
- package/src/components/Separator/Separator.tsx +20 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +136 -0
- package/src/components/Sheet/components/SheetBody.tsx +9 -0
- package/src/components/Sheet/components/SheetContext.tsx +23 -0
- package/src/components/Sheet/components/SheetFooter.tsx +9 -0
- package/src/components/Sheet/components/SheetHeader.tsx +29 -0
- package/src/components/Sheet/components/index.ts +9 -0
- package/src/components/Sheet/index.ts +13 -0
- package/src/components/Skeleton/Skeleton.tsx +7 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Sparkline/Sparkline.tsx +93 -0
- package/src/components/Sparkline/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +41 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/StatsCard/StatsCard.tsx +40 -0
- package/src/components/StatsCard/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +128 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +49 -0
- package/src/components/Tabs/components/TabsContext.tsx +35 -0
- package/src/components/Tabs/components/TabsList.tsx +98 -0
- package/src/components/Tabs/components/TabsPanels.tsx +26 -0
- package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/TagChip/TagChip.tsx +46 -0
- package/src/components/TagChip/index.ts +1 -0
- package/src/components/TagInput/TagInput.tsx +60 -0
- package/src/components/TagInput/index.ts +1 -0
- package/src/components/Text/Text.tsx +35 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextInline/TextInline.tsx +35 -0
- package/src/components/TextInline/index.ts +1 -0
- package/src/components/Textarea/Textarea.tsx +20 -0
- package/src/components/Textarea/index.ts +1 -0
- package/src/components/TimePicker/PeriodSelector.tsx +28 -0
- package/src/components/TimePicker/TimeColumn.tsx +62 -0
- package/src/components/TimePicker/TimePicker.tsx +323 -0
- package/src/components/TimePicker/index.ts +7 -0
- package/src/components/TimePicker/timepicker.hooks.ts +173 -0
- package/src/components/TimePicker/timepicker.types.ts +101 -0
- package/src/components/Toast/Toast.tsx +111 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Toggle/Toggle.tsx +72 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
- package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
- package/src/components/Tooltip/Tooltip.tsx +66 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
- package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
- package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/date-time/arithmetic.ts +101 -0
- package/src/date-time/boundaries.ts +91 -0
- package/src/date-time/compare.ts +85 -0
- package/src/date-time/duration.ts +63 -0
- package/src/date-time/elapsed.ts +18 -0
- package/src/date-time/format.ts +722 -0
- package/src/date-time/index.ts +279 -0
- package/src/date-time/now.ts +70 -0
- package/src/date-time/parse.ts +170 -0
- package/src/date-time/serialize.ts +90 -0
- package/src/date-time/timezone.ts +47 -0
- package/src/date-time/validate.ts +278 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCollapsible.ts +16 -0
- package/src/hooks/useControllableState.ts +54 -0
- package/src/hooks/useDebouncedCallback.ts +21 -0
- package/src/hooks/useDebouncedState.ts +20 -0
- package/src/hooks/useDeviceFormFactor.ts +49 -0
- package/src/hooks/useLocalStorage.ts +67 -0
- package/src/hooks/useMergedFloatingRef.ts +29 -0
- package/src/hooks/usePresence.ts +127 -0
- package/src/hooks/useStack.ts +99 -0
- package/src/hooks/useTheme.ts +105 -0
- package/src/internal/Slot.tsx +79 -0
- package/src/internal/index.ts +2 -0
- package/src/internal/rovingFocus.ts +39 -0
- package/src/internal/types.ts +10 -0
- package/src/lib/EmptyBoundary.tsx +12 -0
- package/src/lib/ErrorBoundary.tsx +48 -0
- package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
- package/src/lib/contexts/ComponentsContext/index.ts +1 -0
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
- package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
- package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
- package/src/lib/contexts/OverlayContext/index.ts +6 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/utils.ts +10 -0
- package/src/lib/withEmpty.tsx +14 -0
- package/src/lib/withErrorBoundary.tsx +16 -0
- package/src/lib/withSuspense.tsx +16 -0
- package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
- package/src/styles/base.css +41 -0
- package/src/theme/index.ts +37 -0
- package/src/theme/palette.json +100 -0
- package/src/theme/palette.schema.json +40 -0
- package/src/theme/schema.ts +122 -0
- package/src/theme/theme.json +349 -0
- package/src/theme/theme.schema.json +876 -0
- package/src/theme/typography.json +7 -0
- package/src/theme/typography.schema.json +36 -0
- package/src/typography/Body.tsx +8 -0
- package/src/typography/Caption.tsx +8 -0
- package/src/typography/Code.tsx +8 -0
- package/src/typography/Display.tsx +8 -0
- package/src/typography/Emphasis.tsx +8 -0
- package/src/typography/Heading.tsx +23 -0
- package/src/typography/Label.tsx +10 -0
- package/src/typography/Lead.tsx +8 -0
- package/src/typography/Mono.tsx +8 -0
- package/src/typography/Muted.tsx +8 -0
- package/src/typography/SectionTitle.tsx +8 -0
- package/src/typography/Subtitle.tsx +8 -0
- package/src/typography/Timestamp.tsx +8 -0
- package/src/typography/index.ts +10 -0
- package/tailwind.config.ts +261 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { autoUpdate, FloatingPortal, flip, offset, shift, useClick, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
|
|
2
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
3
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
4
|
+
import { Calendar as CalendarIcon, X } from "lucide-react";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { useCallback, useId, useMemo, useState } from "react";
|
|
7
|
+
import { now, Temporal, today } from "../../date-time";
|
|
8
|
+
|
|
9
|
+
import { Button } from "../Button";
|
|
10
|
+
import { Calendar } from "../Calendar";
|
|
11
|
+
import type { DateRangeValue } from "../Calendar/calendar.types";
|
|
12
|
+
import { Separator } from "../Separator";
|
|
13
|
+
import { PeriodSelector } from "../TimePicker/PeriodSelector";
|
|
14
|
+
import { TimeColumn } from "../TimePicker/TimeColumn";
|
|
15
|
+
import {
|
|
16
|
+
generateHours,
|
|
17
|
+
generateMinutes,
|
|
18
|
+
generateSeconds,
|
|
19
|
+
getCurrentTime,
|
|
20
|
+
isTimeDisabled,
|
|
21
|
+
temporalToParsed,
|
|
22
|
+
to12Hour,
|
|
23
|
+
to24Hour,
|
|
24
|
+
} from "../TimePicker/timepicker.hooks";
|
|
25
|
+
import type { ParsedTime, TimePeriod } from "../TimePicker/timepicker.types";
|
|
26
|
+
import type { DateTimeRangePickerProps, DateTimeRangePresetOption, DateTimeRangeValue } from "./datetimerangepicker.types";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format an Instant for display
|
|
30
|
+
*/
|
|
31
|
+
function formatInstantDisplay(instant: Temporal.Instant, clockFormat: "12h" | "24h", showSeconds: boolean, timeZone?: string): string {
|
|
32
|
+
const tz = timeZone ?? Temporal.Now.timeZoneId();
|
|
33
|
+
const zoned = instant.toZonedDateTimeISO(tz);
|
|
34
|
+
const plainDateTime = zoned.toPlainDateTime();
|
|
35
|
+
|
|
36
|
+
// Format date part
|
|
37
|
+
const datePart = plainDateTime.toLocaleString("en-US", {
|
|
38
|
+
month: "long",
|
|
39
|
+
day: "numeric",
|
|
40
|
+
year: "numeric",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Format time part based on props
|
|
44
|
+
const timeOptions: Intl.DateTimeFormatOptions = {
|
|
45
|
+
hour: "numeric",
|
|
46
|
+
minute: "2-digit",
|
|
47
|
+
hour12: clockFormat === "12h",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (showSeconds) {
|
|
51
|
+
timeOptions.second = "2-digit";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timePart = plainDateTime.toLocaleString("en-US", timeOptions);
|
|
55
|
+
|
|
56
|
+
return `${datePart}, ${timePart}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format an Instant range for display
|
|
61
|
+
*/
|
|
62
|
+
function formatRangeDisplay(range: DateTimeRangeValue | undefined, clockFormat: "12h" | "24h", showSeconds: boolean): string {
|
|
63
|
+
if (!range?.from && !range?.to) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (range.from && !range.to) {
|
|
68
|
+
return `${formatInstantDisplay(range.from, clockFormat, showSeconds)} – ...`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!range.from && range.to) {
|
|
72
|
+
return `... – ${formatInstantDisplay(range.to, clockFormat, showSeconds)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `${formatInstantDisplay(range.from!, clockFormat, showSeconds)} – ${formatInstantDisplay(range.to!, clockFormat, showSeconds)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default preset ranges for DateTimeRangePicker
|
|
80
|
+
*
|
|
81
|
+
* Uses clean hour boundaries and returns Instant values
|
|
82
|
+
*/
|
|
83
|
+
function getDefaultPresets(): DateTimeRangePresetOption[] {
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
label: "Last hour",
|
|
87
|
+
value: () => {
|
|
88
|
+
const end = now().round({ smallestUnit: "hour", roundingMode: "ceil" });
|
|
89
|
+
const start = end.subtract({ hours: 1 });
|
|
90
|
+
return { from: start, to: end };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Last 24 hours",
|
|
95
|
+
value: () => {
|
|
96
|
+
const end = now().round({ smallestUnit: "hour", roundingMode: "ceil" });
|
|
97
|
+
const start = end.subtract({ hours: 24 });
|
|
98
|
+
return { from: start, to: end };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: "Last 7 days",
|
|
103
|
+
value: () => {
|
|
104
|
+
const end = now().round({ smallestUnit: "hour", roundingMode: "ceil" });
|
|
105
|
+
const start = end.subtract({ hours: 7 * 24 });
|
|
106
|
+
return { from: start, to: end };
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
label: "Last 30 days",
|
|
111
|
+
value: () => {
|
|
112
|
+
const end = now().round({ smallestUnit: "hour", roundingMode: "ceil" });
|
|
113
|
+
const start = end.subtract({ hours: 30 * 24 });
|
|
114
|
+
return { from: start, to: end };
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: "This month",
|
|
119
|
+
value: () => {
|
|
120
|
+
const end = now().round({ smallestUnit: "hour", roundingMode: "ceil" });
|
|
121
|
+
const tz = Temporal.Now.timeZoneId();
|
|
122
|
+
const zonedNow = end.toZonedDateTimeISO(tz);
|
|
123
|
+
const startOfMonth = zonedNow.toPlainDate().with({ day: 1 }).toPlainDateTime(Temporal.PlainTime.from("00:00:00")).toZonedDateTime(tz).toInstant();
|
|
124
|
+
return { from: startOfMonth, to: end };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* DateTimeRangePicker - A component for selecting a datetime range
|
|
132
|
+
*
|
|
133
|
+
* Combines date range selection with separate time pickers for start and end times.
|
|
134
|
+
* Uses Temporal.PlainDateTime for all values.
|
|
135
|
+
*/
|
|
136
|
+
export const DateTimeRangePicker = (props: DateTimeRangePickerProps) => {
|
|
137
|
+
const {
|
|
138
|
+
value,
|
|
139
|
+
defaultValue,
|
|
140
|
+
onChange,
|
|
141
|
+
presets: customPresets,
|
|
142
|
+
showPresets = true,
|
|
143
|
+
minDate,
|
|
144
|
+
maxDate,
|
|
145
|
+
disabledDates,
|
|
146
|
+
minTime,
|
|
147
|
+
maxTime,
|
|
148
|
+
minuteStep = 1,
|
|
149
|
+
clockFormat = "12h",
|
|
150
|
+
showSeconds = false,
|
|
151
|
+
placeholder = "Select datetime range...",
|
|
152
|
+
weekStartsOn = 0,
|
|
153
|
+
name,
|
|
154
|
+
isDisabled = false,
|
|
155
|
+
isInvalid = false,
|
|
156
|
+
className,
|
|
157
|
+
align = "end",
|
|
158
|
+
clearable = true,
|
|
159
|
+
} = props;
|
|
160
|
+
|
|
161
|
+
// Open state
|
|
162
|
+
const [open, setOpen] = useState(false);
|
|
163
|
+
|
|
164
|
+
// Controllable value state (external/committed value)
|
|
165
|
+
const [committedValue, setCommittedValue] = useControllableState({
|
|
166
|
+
value,
|
|
167
|
+
defaultValue,
|
|
168
|
+
onChange,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Draft state (date range + time parts for both start and end)
|
|
172
|
+
// Convert Instant to ZonedDateTime for editing
|
|
173
|
+
const userTimeZone = Temporal.Now.timeZoneId();
|
|
174
|
+
const zonedFrom = committedValue?.from?.toZonedDateTimeISO(userTimeZone);
|
|
175
|
+
const zonedTo = committedValue?.to?.toZonedDateTimeISO(userTimeZone);
|
|
176
|
+
|
|
177
|
+
const [draftDateRange, setDraftDateRange] = useState<DateRangeValue | undefined>(() => ({
|
|
178
|
+
from: zonedFrom?.toPlainDate(),
|
|
179
|
+
to: zonedTo?.toPlainDate(),
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
// Start time state
|
|
183
|
+
const [draftStartHour, setDraftStartHour] = useState<number>(12);
|
|
184
|
+
const [draftStartMinute, setDraftStartMinute] = useState<number>(0);
|
|
185
|
+
const [draftStartSecond, setDraftStartSecond] = useState<number>(0);
|
|
186
|
+
const [draftStartPeriod, setDraftStartPeriod] = useState<TimePeriod>("AM");
|
|
187
|
+
|
|
188
|
+
// End time state
|
|
189
|
+
const [draftEndHour, setDraftEndHour] = useState<number>(12);
|
|
190
|
+
const [draftEndMinute, setDraftEndMinute] = useState<number>(0);
|
|
191
|
+
const [draftEndSecond, setDraftEndSecond] = useState<number>(0);
|
|
192
|
+
const [draftEndPeriod, setDraftEndPeriod] = useState<TimePeriod>("PM");
|
|
193
|
+
|
|
194
|
+
// Month navigation state (for calendar)
|
|
195
|
+
const [month, setMonth] = useState<Temporal.PlainDate>(() => {
|
|
196
|
+
if (committedValue?.from) {
|
|
197
|
+
const tz = Temporal.Now.timeZoneId();
|
|
198
|
+
const zoned = committedValue.from.toZonedDateTimeISO(tz);
|
|
199
|
+
return zoned.toPlainDate();
|
|
200
|
+
}
|
|
201
|
+
return today();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Compute presets
|
|
205
|
+
const presetOptions = useMemo(() => {
|
|
206
|
+
if (!showPresets) return undefined;
|
|
207
|
+
const presetsToUse = customPresets || getDefaultPresets();
|
|
208
|
+
return presetsToUse.map((preset) => ({
|
|
209
|
+
label: preset.label,
|
|
210
|
+
value: preset.value(),
|
|
211
|
+
}));
|
|
212
|
+
}, [customPresets, showPresets]);
|
|
213
|
+
|
|
214
|
+
// Sync draft when popup opens
|
|
215
|
+
const handleOpenChange = useCallback(
|
|
216
|
+
(newOpen: boolean) => {
|
|
217
|
+
if (newOpen) {
|
|
218
|
+
const tz = Temporal.Now.timeZoneId();
|
|
219
|
+
if (committedValue?.from && committedValue?.to) {
|
|
220
|
+
// Convert Instant to ZonedDateTime for editing
|
|
221
|
+
const zonedFrom = committedValue.from.toZonedDateTimeISO(tz);
|
|
222
|
+
const zonedTo = committedValue.to.toZonedDateTimeISO(tz);
|
|
223
|
+
const dateTimeFrom = zonedFrom.toPlainDateTime();
|
|
224
|
+
const dateTimeTo = zonedTo.toPlainDateTime();
|
|
225
|
+
|
|
226
|
+
// Set draft from committed value
|
|
227
|
+
setDraftDateRange({
|
|
228
|
+
from: dateTimeFrom.toPlainDate(),
|
|
229
|
+
to: dateTimeTo.toPlainDate(),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const startParsed = temporalToParsed(dateTimeFrom.toPlainTime());
|
|
233
|
+
setDraftStartHour(clockFormat === "12h" ? to12Hour(startParsed.hour).hour12 : startParsed.hour);
|
|
234
|
+
setDraftStartMinute(startParsed.minute);
|
|
235
|
+
setDraftStartSecond(startParsed.second);
|
|
236
|
+
setDraftStartPeriod(to12Hour(startParsed.hour).period);
|
|
237
|
+
|
|
238
|
+
const endParsed = temporalToParsed(dateTimeTo.toPlainTime());
|
|
239
|
+
setDraftEndHour(clockFormat === "12h" ? to12Hour(endParsed.hour).hour12 : endParsed.hour);
|
|
240
|
+
setDraftEndMinute(endParsed.minute);
|
|
241
|
+
setDraftEndSecond(endParsed.second);
|
|
242
|
+
setDraftEndPeriod(to12Hour(endParsed.hour).period);
|
|
243
|
+
|
|
244
|
+
setMonth(dateTimeFrom.toPlainDate());
|
|
245
|
+
} else if (committedValue?.from) {
|
|
246
|
+
// Only start is set
|
|
247
|
+
const zonedFrom = committedValue.from.toZonedDateTimeISO(tz);
|
|
248
|
+
const dateTimeFrom = zonedFrom.toPlainDateTime();
|
|
249
|
+
|
|
250
|
+
setDraftDateRange({
|
|
251
|
+
from: dateTimeFrom.toPlainDate(),
|
|
252
|
+
to: undefined,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const startParsed = temporalToParsed(dateTimeFrom.toPlainTime());
|
|
256
|
+
setDraftStartHour(clockFormat === "12h" ? to12Hour(startParsed.hour).hour12 : startParsed.hour);
|
|
257
|
+
setDraftStartMinute(startParsed.minute);
|
|
258
|
+
setDraftStartSecond(startParsed.second);
|
|
259
|
+
setDraftStartPeriod(to12Hour(startParsed.hour).period);
|
|
260
|
+
|
|
261
|
+
setMonth(dateTimeFrom.toPlainDate());
|
|
262
|
+
} else {
|
|
263
|
+
// No committed value - start fresh with empty range
|
|
264
|
+
const now = getCurrentTime();
|
|
265
|
+
|
|
266
|
+
setDraftDateRange({ from: undefined, to: undefined });
|
|
267
|
+
|
|
268
|
+
const currentHour = now.hour;
|
|
269
|
+
setDraftStartHour(clockFormat === "12h" ? to12Hour(currentHour).hour12 : currentHour);
|
|
270
|
+
setDraftStartMinute(now.minute);
|
|
271
|
+
setDraftStartSecond(now.second);
|
|
272
|
+
setDraftStartPeriod(to12Hour(currentHour).period);
|
|
273
|
+
|
|
274
|
+
setDraftEndHour(12);
|
|
275
|
+
setDraftEndMinute(0);
|
|
276
|
+
setDraftEndSecond(0);
|
|
277
|
+
setDraftEndPeriod("PM");
|
|
278
|
+
|
|
279
|
+
setMonth(today());
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
setOpen(newOpen);
|
|
283
|
+
},
|
|
284
|
+
[committedValue, clockFormat]
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Get the current draft times as parsed times (24-hour)
|
|
288
|
+
const getDraftStartTime = useCallback((): ParsedTime => {
|
|
289
|
+
const hour24 = clockFormat === "12h" ? to24Hour(draftStartHour, draftStartPeriod) : draftStartHour;
|
|
290
|
+
return {
|
|
291
|
+
hour: hour24,
|
|
292
|
+
minute: draftStartMinute,
|
|
293
|
+
second: draftStartSecond,
|
|
294
|
+
};
|
|
295
|
+
}, [clockFormat, draftStartHour, draftStartMinute, draftStartSecond, draftStartPeriod]);
|
|
296
|
+
|
|
297
|
+
const getDraftEndTime = useCallback((): ParsedTime => {
|
|
298
|
+
const hour24 = clockFormat === "12h" ? to24Hour(draftEndHour, draftEndPeriod) : draftEndHour;
|
|
299
|
+
return {
|
|
300
|
+
hour: hour24,
|
|
301
|
+
minute: draftEndMinute,
|
|
302
|
+
second: draftEndSecond,
|
|
303
|
+
};
|
|
304
|
+
}, [clockFormat, draftEndHour, draftEndMinute, draftEndSecond, draftEndPeriod]);
|
|
305
|
+
|
|
306
|
+
// Combine draft dates and times into DateTimeRangeValue
|
|
307
|
+
const getDraftRange = useCallback((): DateTimeRangeValue => {
|
|
308
|
+
const tz = Temporal.Now.timeZoneId();
|
|
309
|
+
|
|
310
|
+
const from = draftDateRange?.from
|
|
311
|
+
? draftDateRange.from
|
|
312
|
+
.toPlainDateTime({
|
|
313
|
+
hour: getDraftStartTime().hour,
|
|
314
|
+
minute: getDraftStartTime().minute,
|
|
315
|
+
second: getDraftStartTime().second,
|
|
316
|
+
})
|
|
317
|
+
.toZonedDateTime(tz)
|
|
318
|
+
.toInstant()
|
|
319
|
+
: undefined;
|
|
320
|
+
|
|
321
|
+
const to = draftDateRange?.to
|
|
322
|
+
? draftDateRange.to
|
|
323
|
+
.toPlainDateTime({
|
|
324
|
+
hour: getDraftEndTime().hour,
|
|
325
|
+
minute: getDraftEndTime().minute,
|
|
326
|
+
second: getDraftEndTime().second,
|
|
327
|
+
})
|
|
328
|
+
.toZonedDateTime(tz)
|
|
329
|
+
.toInstant()
|
|
330
|
+
: undefined;
|
|
331
|
+
|
|
332
|
+
return { from, to };
|
|
333
|
+
}, [draftDateRange, getDraftStartTime, getDraftEndTime]);
|
|
334
|
+
|
|
335
|
+
// Apply changes
|
|
336
|
+
const handleApply = useCallback(() => {
|
|
337
|
+
const range = getDraftRange();
|
|
338
|
+
|
|
339
|
+
// Validate that end is after start
|
|
340
|
+
if (range.from && range.to && Temporal.Instant.compare(range.from, range.to) > 0) {
|
|
341
|
+
// Swap them
|
|
342
|
+
setCommittedValue({ from: range.to, to: range.from });
|
|
343
|
+
} else {
|
|
344
|
+
setCommittedValue(range.from || range.to ? range : undefined);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
setOpen(false);
|
|
348
|
+
}, [getDraftRange, setCommittedValue]);
|
|
349
|
+
|
|
350
|
+
// Cancel changes
|
|
351
|
+
const handleCancel = useCallback(() => {
|
|
352
|
+
const tz = Temporal.Now.timeZoneId();
|
|
353
|
+
// Revert to committed value
|
|
354
|
+
if (committedValue?.from) {
|
|
355
|
+
const zonedFrom = committedValue.from.toZonedDateTimeISO(tz);
|
|
356
|
+
const dateTimeFrom = zonedFrom.toPlainDateTime();
|
|
357
|
+
|
|
358
|
+
setDraftDateRange({
|
|
359
|
+
from: dateTimeFrom.toPlainDate(),
|
|
360
|
+
to: committedValue.to ? committedValue.to.toZonedDateTimeISO(tz).toPlainDateTime().toPlainDate() : undefined,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const startParsed = temporalToParsed(dateTimeFrom.toPlainTime());
|
|
364
|
+
setDraftStartHour(clockFormat === "12h" ? to12Hour(startParsed.hour).hour12 : startParsed.hour);
|
|
365
|
+
setDraftStartMinute(startParsed.minute);
|
|
366
|
+
setDraftStartSecond(startParsed.second);
|
|
367
|
+
setDraftStartPeriod(to12Hour(startParsed.hour).period);
|
|
368
|
+
}
|
|
369
|
+
if (committedValue?.to) {
|
|
370
|
+
const zonedTo = committedValue.to.toZonedDateTimeISO(tz);
|
|
371
|
+
const dateTimeTo = zonedTo.toPlainDateTime();
|
|
372
|
+
|
|
373
|
+
const endParsed = temporalToParsed(dateTimeTo.toPlainTime());
|
|
374
|
+
setDraftEndHour(clockFormat === "12h" ? to12Hour(endParsed.hour).hour12 : endParsed.hour);
|
|
375
|
+
setDraftEndMinute(endParsed.minute);
|
|
376
|
+
setDraftEndSecond(endParsed.second);
|
|
377
|
+
setDraftEndPeriod(to12Hour(endParsed.hour).period);
|
|
378
|
+
}
|
|
379
|
+
setOpen(false);
|
|
380
|
+
}, [committedValue, clockFormat]);
|
|
381
|
+
|
|
382
|
+
// Clear value
|
|
383
|
+
const handleClear = useCallback(
|
|
384
|
+
(e: React.MouseEvent) => {
|
|
385
|
+
e.stopPropagation();
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
setCommittedValue(undefined);
|
|
388
|
+
},
|
|
389
|
+
[setCommittedValue]
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Apply preset
|
|
393
|
+
const _handlePresetClick = useCallback(
|
|
394
|
+
(preset: { label: string; value: DateTimeRangeValue }) => {
|
|
395
|
+
setCommittedValue(preset.value);
|
|
396
|
+
setOpen(false);
|
|
397
|
+
},
|
|
398
|
+
[setCommittedValue]
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Floating UI setup
|
|
402
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
403
|
+
open,
|
|
404
|
+
onOpenChange: handleOpenChange,
|
|
405
|
+
placement: align === "start" ? "bottom-start" : "bottom-end",
|
|
406
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
407
|
+
whileElementsMounted: autoUpdate,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const click = useClick(context);
|
|
411
|
+
const dismiss = useDismiss(context);
|
|
412
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
|
|
413
|
+
|
|
414
|
+
// IDs for accessibility
|
|
415
|
+
const id = useId();
|
|
416
|
+
const contentId = `datetimerangepicker-content-${id}`;
|
|
417
|
+
const inputId = `datetimerangepicker-input-${id}`;
|
|
418
|
+
|
|
419
|
+
// Display value (from committed value)
|
|
420
|
+
const displayValue = committedValue ? formatRangeDisplay(committedValue, clockFormat, showSeconds) : "";
|
|
421
|
+
const hasValue = Boolean(committedValue?.from || committedValue?.to);
|
|
422
|
+
|
|
423
|
+
// Time selection handlers - Start time
|
|
424
|
+
const handleStartHourChange = (hour: number) => setDraftStartHour(hour);
|
|
425
|
+
const handleStartMinuteChange = (minute: number) => setDraftStartMinute(minute);
|
|
426
|
+
const handleStartSecondChange = (second: number) => setDraftStartSecond(second);
|
|
427
|
+
const handleStartPeriodChange = (period: TimePeriod) => setDraftStartPeriod(period);
|
|
428
|
+
|
|
429
|
+
// Time selection handlers - End time
|
|
430
|
+
const handleEndHourChange = (hour: number) => setDraftEndHour(hour);
|
|
431
|
+
const handleEndMinuteChange = (minute: number) => setDraftEndMinute(minute);
|
|
432
|
+
const handleEndSecondChange = (second: number) => setDraftEndSecond(second);
|
|
433
|
+
const handleEndPeriodChange = (period: TimePeriod) => setDraftEndPeriod(period);
|
|
434
|
+
|
|
435
|
+
// Generate time options
|
|
436
|
+
const hours = generateHours(clockFormat);
|
|
437
|
+
const minutes = generateMinutes(minuteStep);
|
|
438
|
+
const seconds = generateSeconds();
|
|
439
|
+
|
|
440
|
+
// Get current draft times
|
|
441
|
+
const draftStartTime = getDraftStartTime();
|
|
442
|
+
const draftEndTime = getDraftEndTime();
|
|
443
|
+
|
|
444
|
+
// Check if a time is disabled
|
|
445
|
+
const checkTimeDisabled = useCallback(
|
|
446
|
+
(hour: number, minute: number, second: number): boolean => {
|
|
447
|
+
const temporal = Temporal.PlainTime.from({ hour, minute, second });
|
|
448
|
+
return isTimeDisabled(temporal, minTime, maxTime);
|
|
449
|
+
},
|
|
450
|
+
[minTime, maxTime]
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<>
|
|
455
|
+
{/* Trigger Input */}
|
|
456
|
+
<div className="relative flex min-w-125 items-center">
|
|
457
|
+
<CalendarIcon className="pointer-events-none absolute left-3 h-4 w-4 text-fg-subtle" />
|
|
458
|
+
<input
|
|
459
|
+
ref={refs.setReference as React.Ref<HTMLInputElement>}
|
|
460
|
+
id={inputId}
|
|
461
|
+
type="text"
|
|
462
|
+
readOnly
|
|
463
|
+
value={displayValue}
|
|
464
|
+
placeholder={placeholder}
|
|
465
|
+
disabled={isDisabled}
|
|
466
|
+
role="combobox"
|
|
467
|
+
aria-invalid={isInvalid}
|
|
468
|
+
aria-haspopup="dialog"
|
|
469
|
+
aria-expanded={open}
|
|
470
|
+
aria-controls={open ? contentId : undefined}
|
|
471
|
+
className={cn(
|
|
472
|
+
`flex h-9 w-full min-w-125 cursor-pointer rounded-md border border-border-normal bg-transparent py-1 pl-9 text-sm shadow-xs transition-colors placeholder:text-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`,
|
|
473
|
+
clearable && hasValue ? "pr-9" : "pr-3",
|
|
474
|
+
isInvalid && "border-border-danger",
|
|
475
|
+
className
|
|
476
|
+
)}
|
|
477
|
+
{...getReferenceProps()}
|
|
478
|
+
/>
|
|
479
|
+
{clearable && hasValue && !isDisabled && (
|
|
480
|
+
<button
|
|
481
|
+
type="button"
|
|
482
|
+
onClick={handleClear}
|
|
483
|
+
className="absolute right-3 rounded-sm p-0.5 text-fg-subtle hover:bg-bg-subtle hover:text-fg-normal focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring"
|
|
484
|
+
aria-label="Clear datetime range"
|
|
485
|
+
>
|
|
486
|
+
<X className="h-3.5 w-3.5" />
|
|
487
|
+
</button>
|
|
488
|
+
)}
|
|
489
|
+
|
|
490
|
+
{/* Hidden inputs for form submission */}
|
|
491
|
+
{name && committedValue?.from && <input type="hidden" name={`${name}.from`} value={committedValue.from.toString()} />}
|
|
492
|
+
{name && committedValue?.to && <input type="hidden" name={`${name}.to`} value={committedValue.to.toString()} />}
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
{/* Popover Content */}
|
|
496
|
+
{open && (
|
|
497
|
+
<FloatingPortal>
|
|
498
|
+
<div
|
|
499
|
+
ref={refs.setFloating}
|
|
500
|
+
id={contentId}
|
|
501
|
+
role="dialog"
|
|
502
|
+
aria-modal="true"
|
|
503
|
+
style={floatingStyles}
|
|
504
|
+
className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
|
|
505
|
+
{...getFloatingProps()}
|
|
506
|
+
>
|
|
507
|
+
{/* Top row: Presets + Calendar */}
|
|
508
|
+
<div className="flex gap-4">
|
|
509
|
+
{presetOptions && presetOptions.length > 0 && (
|
|
510
|
+
<>
|
|
511
|
+
<div className="flex flex-col gap-1">
|
|
512
|
+
<div className="mb-2 font-semibold text-sm">Presets</div>
|
|
513
|
+
<div className="flex flex-col gap-1">
|
|
514
|
+
{presetOptions.map((preset) => (
|
|
515
|
+
<button
|
|
516
|
+
key={preset.label}
|
|
517
|
+
type="button"
|
|
518
|
+
onClick={() => _handlePresetClick(preset)}
|
|
519
|
+
className={cn(
|
|
520
|
+
"rounded-md px-3 py-1.5 text-left text-sm transition-colors",
|
|
521
|
+
"hover:bg-bg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset"
|
|
522
|
+
)}
|
|
523
|
+
>
|
|
524
|
+
{preset.label}
|
|
525
|
+
</button>
|
|
526
|
+
))}
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
<Separator orientation="vertical" />
|
|
530
|
+
</>
|
|
531
|
+
)}
|
|
532
|
+
|
|
533
|
+
{/* Date Calendar */}
|
|
534
|
+
<Calendar
|
|
535
|
+
mode="range"
|
|
536
|
+
value={draftDateRange}
|
|
537
|
+
onSelect={setDraftDateRange as (range: DateRangeValue | undefined) => void}
|
|
538
|
+
month={month}
|
|
539
|
+
onMonthChange={setMonth}
|
|
540
|
+
minDate={minDate}
|
|
541
|
+
maxDate={maxDate}
|
|
542
|
+
disabled={disabledDates}
|
|
543
|
+
weekStartsOn={weekStartsOn}
|
|
544
|
+
showFooter={false}
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<Separator className="my-3" />
|
|
549
|
+
|
|
550
|
+
{/* Bottom row: Time Pickers */}
|
|
551
|
+
<div className="flex gap-4">
|
|
552
|
+
{/* Start Time Picker */}
|
|
553
|
+
<div className="flex flex-1 flex-col gap-3">
|
|
554
|
+
<div className="font-semibold text-sm">Start Time</div>
|
|
555
|
+
|
|
556
|
+
{/* Time columns */}
|
|
557
|
+
<div className="flex gap-2">
|
|
558
|
+
{/* Hours */}
|
|
559
|
+
<div className="flex flex-col gap-1">
|
|
560
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Hour</div>
|
|
561
|
+
<TimeColumn
|
|
562
|
+
values={hours}
|
|
563
|
+
selected={draftStartHour}
|
|
564
|
+
onSelect={handleStartHourChange}
|
|
565
|
+
disabled={(hour: number) => {
|
|
566
|
+
const hour24 = clockFormat === "12h" ? to24Hour(hour, draftStartPeriod) : hour;
|
|
567
|
+
return checkTimeDisabled(hour24, draftStartMinute, draftStartSecond);
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
{/* Minutes */}
|
|
573
|
+
<div className="flex flex-col gap-1">
|
|
574
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Min</div>
|
|
575
|
+
<TimeColumn
|
|
576
|
+
values={minutes}
|
|
577
|
+
selected={draftStartMinute}
|
|
578
|
+
onSelect={handleStartMinuteChange}
|
|
579
|
+
disabled={(minute: number) => checkTimeDisabled(draftStartTime.hour, minute, draftStartSecond)}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{/* Seconds (optional) */}
|
|
584
|
+
{showSeconds && (
|
|
585
|
+
<div className="flex flex-col gap-1">
|
|
586
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Sec</div>
|
|
587
|
+
<TimeColumn
|
|
588
|
+
values={seconds}
|
|
589
|
+
selected={draftStartSecond}
|
|
590
|
+
onSelect={handleStartSecondChange}
|
|
591
|
+
disabled={(second: number) => checkTimeDisabled(draftStartTime.hour, draftStartMinute, second)}
|
|
592
|
+
/>
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
|
|
596
|
+
{/* AM/PM (12h format only) */}
|
|
597
|
+
{clockFormat === "12h" && (
|
|
598
|
+
<div className="flex flex-col gap-1">
|
|
599
|
+
<div className="text-center font-medium text-fg-subtle text-xs"> </div>
|
|
600
|
+
<PeriodSelector value={draftStartPeriod} onChange={handleStartPeriodChange} />
|
|
601
|
+
</div>
|
|
602
|
+
)}
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<Separator orientation="vertical" />
|
|
607
|
+
|
|
608
|
+
{/* End Time Picker */}
|
|
609
|
+
<div className="flex flex-1 flex-col gap-3">
|
|
610
|
+
<div className="font-semibold text-sm">End Time</div>
|
|
611
|
+
|
|
612
|
+
{/* Time columns */}
|
|
613
|
+
<div className="flex gap-2">
|
|
614
|
+
{/* Hours */}
|
|
615
|
+
<div className="flex flex-col gap-1">
|
|
616
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Hour</div>
|
|
617
|
+
<TimeColumn
|
|
618
|
+
values={hours}
|
|
619
|
+
selected={draftEndHour}
|
|
620
|
+
onSelect={handleEndHourChange}
|
|
621
|
+
disabled={(hour: number) => {
|
|
622
|
+
const hour24 = clockFormat === "12h" ? to24Hour(hour, draftEndPeriod) : hour;
|
|
623
|
+
return checkTimeDisabled(hour24, draftEndMinute, draftEndSecond);
|
|
624
|
+
}}
|
|
625
|
+
/>
|
|
626
|
+
</div>
|
|
627
|
+
|
|
628
|
+
{/* Minutes */}
|
|
629
|
+
<div className="flex flex-col gap-1">
|
|
630
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Min</div>
|
|
631
|
+
<TimeColumn
|
|
632
|
+
values={minutes}
|
|
633
|
+
selected={draftEndMinute}
|
|
634
|
+
onSelect={handleEndMinuteChange}
|
|
635
|
+
disabled={(minute: number) => checkTimeDisabled(draftEndTime.hour, minute, draftEndSecond)}
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
{/* Seconds (optional) */}
|
|
640
|
+
{showSeconds && (
|
|
641
|
+
<div className="flex flex-col gap-1">
|
|
642
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Sec</div>
|
|
643
|
+
<TimeColumn
|
|
644
|
+
values={seconds}
|
|
645
|
+
selected={draftEndSecond}
|
|
646
|
+
onSelect={handleEndSecondChange}
|
|
647
|
+
disabled={(second: number) => checkTimeDisabled(draftEndTime.hour, draftEndMinute, second)}
|
|
648
|
+
/>
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
|
|
652
|
+
{/* AM/PM (12h format only) */}
|
|
653
|
+
{clockFormat === "12h" && (
|
|
654
|
+
<div className="flex flex-col gap-1">
|
|
655
|
+
<div className="text-center font-medium text-fg-subtle text-xs"> </div>
|
|
656
|
+
<PeriodSelector value={draftEndPeriod} onChange={handleEndPeriodChange} />
|
|
657
|
+
</div>
|
|
658
|
+
)}
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<Separator className="my-3" />
|
|
664
|
+
|
|
665
|
+
{/* Footer with Cancel / Apply */}
|
|
666
|
+
<div className="flex items-center justify-end gap-2">
|
|
667
|
+
<Button type="button" variant="ghost" size="sm" onClick={handleCancel}>
|
|
668
|
+
Cancel
|
|
669
|
+
</Button>
|
|
670
|
+
<Button type="button" variant="solid" size="sm" onClick={handleApply}>
|
|
671
|
+
Apply
|
|
672
|
+
</Button>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</FloatingPortal>
|
|
676
|
+
)}
|
|
677
|
+
</>
|
|
678
|
+
);
|
|
679
|
+
};
|