@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,53 @@
|
|
|
1
|
+
import type { Temporal, WeekStartsOn } from "../../date-time";
|
|
2
|
+
|
|
3
|
+
export type DateRangeValue = {
|
|
4
|
+
from: Temporal.PlainDate | undefined;
|
|
5
|
+
to: Temporal.PlainDate | undefined;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type PresetOption = {
|
|
9
|
+
label: string;
|
|
10
|
+
value: () => DateRangeValue;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DisabledDateMatcher =
|
|
14
|
+
| Temporal.PlainDate
|
|
15
|
+
| Temporal.PlainDate[]
|
|
16
|
+
| ((date: Temporal.PlainDate) => boolean)
|
|
17
|
+
| { before: Temporal.PlainDate }
|
|
18
|
+
| { after: Temporal.PlainDate }
|
|
19
|
+
| { from: Temporal.PlainDate; to: Temporal.PlainDate };
|
|
20
|
+
|
|
21
|
+
export type DateRangePickerProps = {
|
|
22
|
+
// Value (controlled/uncontrolled)
|
|
23
|
+
value?: DateRangeValue;
|
|
24
|
+
defaultValue?: DateRangeValue;
|
|
25
|
+
onChange?: (range: DateRangeValue | undefined) => void;
|
|
26
|
+
|
|
27
|
+
// Constraints
|
|
28
|
+
minDate?: Temporal.PlainDate;
|
|
29
|
+
maxDate?: Temporal.PlainDate;
|
|
30
|
+
disabled?: DisabledDateMatcher | DisabledDateMatcher[];
|
|
31
|
+
|
|
32
|
+
// Presets
|
|
33
|
+
presets?: PresetOption[];
|
|
34
|
+
showPresets?: boolean; // Default: true
|
|
35
|
+
|
|
36
|
+
// Display
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
weekStartsOn?: WeekStartsOn;
|
|
39
|
+
|
|
40
|
+
// Form
|
|
41
|
+
name?: string;
|
|
42
|
+
|
|
43
|
+
// State
|
|
44
|
+
isDisabled?: boolean;
|
|
45
|
+
isInvalid?: boolean;
|
|
46
|
+
|
|
47
|
+
// Style
|
|
48
|
+
className?: string;
|
|
49
|
+
align?: "start" | "end";
|
|
50
|
+
|
|
51
|
+
/** Whether the clear button is shown when a value is present (default: true) */
|
|
52
|
+
clearable?: boolean;
|
|
53
|
+
};
|
|
@@ -0,0 +1,399 @@
|
|
|
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 { Clock, X } from "lucide-react";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { useCallback, useId, useState } from "react";
|
|
7
|
+
import { Temporal, today } from "../../date-time";
|
|
8
|
+
|
|
9
|
+
import { Button } from "../Button";
|
|
10
|
+
import { Calendar } from "../Calendar";
|
|
11
|
+
import { Separator } from "../Separator";
|
|
12
|
+
import { PeriodSelector } from "../TimePicker/PeriodSelector";
|
|
13
|
+
import { TimeColumn } from "../TimePicker/TimeColumn";
|
|
14
|
+
import {
|
|
15
|
+
generateHours,
|
|
16
|
+
generateMinutes,
|
|
17
|
+
generateSeconds,
|
|
18
|
+
getCurrentTime,
|
|
19
|
+
isTimeDisabled,
|
|
20
|
+
temporalToParsed,
|
|
21
|
+
to12Hour,
|
|
22
|
+
to24Hour,
|
|
23
|
+
} from "../TimePicker/timepicker.hooks";
|
|
24
|
+
import type { ParsedTime, TimePeriod } from "../TimePicker/timepicker.types";
|
|
25
|
+
import type { DateTimePickerProps } from "./datetimepicker.types";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format an Instant for display
|
|
29
|
+
*/
|
|
30
|
+
function formatInstantDisplay(instant: Temporal.Instant, clockFormat: "12h" | "24h", showSeconds: boolean, timeZone?: string): string {
|
|
31
|
+
const tz = timeZone ?? Temporal.Now.timeZoneId();
|
|
32
|
+
const zoned = instant.toZonedDateTimeISO(tz);
|
|
33
|
+
const plainDateTime = zoned.toPlainDateTime();
|
|
34
|
+
|
|
35
|
+
// Format date part
|
|
36
|
+
const datePart = plainDateTime.toLocaleString("en-US", {
|
|
37
|
+
month: "long",
|
|
38
|
+
day: "numeric",
|
|
39
|
+
year: "numeric",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Format time part based on props
|
|
43
|
+
const timeOptions: Intl.DateTimeFormatOptions = {
|
|
44
|
+
hour: "numeric",
|
|
45
|
+
minute: "2-digit",
|
|
46
|
+
hour12: clockFormat === "12h",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (showSeconds) {
|
|
50
|
+
timeOptions.second = "2-digit";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const timePart = plainDateTime.toLocaleString("en-US", timeOptions);
|
|
54
|
+
|
|
55
|
+
return `${datePart}, ${timePart}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* DateTimePicker - A component for selecting both date and time
|
|
60
|
+
*
|
|
61
|
+
* Combines date and time selection in a single popup with side-by-side layout.
|
|
62
|
+
* Uses Temporal.Instant for all values (timezone-aware).
|
|
63
|
+
*/
|
|
64
|
+
export const DateTimePicker = (props: DateTimePickerProps) => {
|
|
65
|
+
const {
|
|
66
|
+
value,
|
|
67
|
+
defaultValue,
|
|
68
|
+
onChange,
|
|
69
|
+
minDate,
|
|
70
|
+
maxDate,
|
|
71
|
+
disabledDates,
|
|
72
|
+
minTime,
|
|
73
|
+
maxTime,
|
|
74
|
+
minuteStep = 1,
|
|
75
|
+
clockFormat = "12h",
|
|
76
|
+
showSeconds = false,
|
|
77
|
+
placeholder = "Select date and time...",
|
|
78
|
+
weekStartsOn = 0,
|
|
79
|
+
name,
|
|
80
|
+
isDisabled = false,
|
|
81
|
+
isInvalid = false,
|
|
82
|
+
className,
|
|
83
|
+
align = "end",
|
|
84
|
+
clearable = true,
|
|
85
|
+
} = props;
|
|
86
|
+
|
|
87
|
+
// Open state
|
|
88
|
+
const [open, setOpen] = useState(false);
|
|
89
|
+
|
|
90
|
+
// Controllable value state (external/committed value)
|
|
91
|
+
const [committedValue, setCommittedValue] = useControllableState({
|
|
92
|
+
value,
|
|
93
|
+
defaultValue,
|
|
94
|
+
onChange,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Draft state (date + time parts)
|
|
98
|
+
// Convert Instant to ZonedDateTime for editing
|
|
99
|
+
const userTimeZone = Temporal.Now.timeZoneId();
|
|
100
|
+
const zonedValue = committedValue?.toZonedDateTimeISO(userTimeZone);
|
|
101
|
+
|
|
102
|
+
const [draftDate, setDraftDate] = useState<Temporal.PlainDate | undefined>(zonedValue?.toPlainDate());
|
|
103
|
+
const [draftHour, setDraftHour] = useState<number>(12);
|
|
104
|
+
const [draftMinute, setDraftMinute] = useState<number>(0);
|
|
105
|
+
const [draftSecond, setDraftSecond] = useState<number>(0);
|
|
106
|
+
const [draftPeriod, setDraftPeriod] = useState<TimePeriod>("PM");
|
|
107
|
+
|
|
108
|
+
// Month navigation state (for calendar)
|
|
109
|
+
const [month, setMonth] = useState<Temporal.PlainDate>(() => {
|
|
110
|
+
if (committedValue) {
|
|
111
|
+
const zoned = committedValue.toZonedDateTimeISO(userTimeZone);
|
|
112
|
+
return zoned.toPlainDate();
|
|
113
|
+
}
|
|
114
|
+
return today();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Sync draft when popup opens
|
|
118
|
+
const handleOpenChange = useCallback(
|
|
119
|
+
(newOpen: boolean) => {
|
|
120
|
+
if (newOpen) {
|
|
121
|
+
const tz = Temporal.Now.timeZoneId();
|
|
122
|
+
if (committedValue) {
|
|
123
|
+
// Convert Instant to ZonedDateTime for editing
|
|
124
|
+
const zoned = committedValue.toZonedDateTimeISO(tz);
|
|
125
|
+
const dateTime = zoned.toPlainDateTime();
|
|
126
|
+
|
|
127
|
+
setDraftDate(dateTime.toPlainDate());
|
|
128
|
+
const parsed = temporalToParsed(dateTime.toPlainTime());
|
|
129
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(parsed.hour).hour12 : parsed.hour);
|
|
130
|
+
setDraftMinute(parsed.minute);
|
|
131
|
+
setDraftSecond(parsed.second);
|
|
132
|
+
setDraftPeriod(to12Hour(parsed.hour).period);
|
|
133
|
+
setMonth(dateTime.toPlainDate());
|
|
134
|
+
} else {
|
|
135
|
+
// Default to today + current time
|
|
136
|
+
const todayDate = today();
|
|
137
|
+
const now = getCurrentTime();
|
|
138
|
+
setDraftDate(todayDate);
|
|
139
|
+
const currentHour = now.hour;
|
|
140
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(currentHour).hour12 : currentHour);
|
|
141
|
+
setDraftMinute(now.minute);
|
|
142
|
+
setDraftSecond(now.second);
|
|
143
|
+
setDraftPeriod(to12Hour(currentHour).period);
|
|
144
|
+
setMonth(todayDate);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
setOpen(newOpen);
|
|
148
|
+
},
|
|
149
|
+
[committedValue, clockFormat]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Get the current draft as a parsed time (24-hour)
|
|
153
|
+
const getDraftTime = useCallback((): ParsedTime => {
|
|
154
|
+
const hour24 = clockFormat === "12h" ? to24Hour(draftHour, draftPeriod) : draftHour;
|
|
155
|
+
return {
|
|
156
|
+
hour: hour24,
|
|
157
|
+
minute: draftMinute,
|
|
158
|
+
second: draftSecond,
|
|
159
|
+
};
|
|
160
|
+
}, [clockFormat, draftHour, draftMinute, draftSecond, draftPeriod]);
|
|
161
|
+
|
|
162
|
+
// Apply changes
|
|
163
|
+
const handleApply = useCallback(() => {
|
|
164
|
+
if (!draftDate) {
|
|
165
|
+
setOpen(false);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const draftTime = getDraftTime();
|
|
170
|
+
const plainDateTime = draftDate.toPlainDateTime({
|
|
171
|
+
hour: draftTime.hour,
|
|
172
|
+
minute: draftTime.minute,
|
|
173
|
+
second: showSeconds ? draftTime.second : 0,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Convert PlainDateTime to ZonedDateTime in user's timezone, then to Instant
|
|
177
|
+
const tz = Temporal.Now.timeZoneId();
|
|
178
|
+
const zoned = plainDateTime.toZonedDateTime(tz);
|
|
179
|
+
const instant = zoned.toInstant();
|
|
180
|
+
|
|
181
|
+
setCommittedValue(instant);
|
|
182
|
+
setOpen(false);
|
|
183
|
+
}, [draftDate, getDraftTime, showSeconds, setCommittedValue]);
|
|
184
|
+
|
|
185
|
+
// Cancel changes
|
|
186
|
+
const handleCancel = useCallback(() => {
|
|
187
|
+
// Revert to committed value
|
|
188
|
+
if (committedValue) {
|
|
189
|
+
const tz = Temporal.Now.timeZoneId();
|
|
190
|
+
const zoned = committedValue.toZonedDateTimeISO(tz);
|
|
191
|
+
const dateTime = zoned.toPlainDateTime();
|
|
192
|
+
|
|
193
|
+
setDraftDate(dateTime.toPlainDate());
|
|
194
|
+
const parsed = temporalToParsed(dateTime.toPlainTime());
|
|
195
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(parsed.hour).hour12 : parsed.hour);
|
|
196
|
+
setDraftMinute(parsed.minute);
|
|
197
|
+
setDraftSecond(parsed.second);
|
|
198
|
+
setDraftPeriod(to12Hour(parsed.hour).period);
|
|
199
|
+
}
|
|
200
|
+
setOpen(false);
|
|
201
|
+
}, [committedValue, clockFormat]);
|
|
202
|
+
|
|
203
|
+
// Clear value
|
|
204
|
+
const handleClear = useCallback(
|
|
205
|
+
(e: React.MouseEvent) => {
|
|
206
|
+
e.stopPropagation();
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
setCommittedValue(undefined);
|
|
209
|
+
},
|
|
210
|
+
[setCommittedValue]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Floating UI setup
|
|
214
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
215
|
+
open,
|
|
216
|
+
onOpenChange: handleOpenChange,
|
|
217
|
+
placement: align === "start" ? "bottom-start" : "bottom-end",
|
|
218
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
219
|
+
whileElementsMounted: autoUpdate,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const click = useClick(context);
|
|
223
|
+
const dismiss = useDismiss(context);
|
|
224
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
|
|
225
|
+
|
|
226
|
+
// IDs for accessibility
|
|
227
|
+
const id = useId();
|
|
228
|
+
const contentId = `datetimepicker-content-${id}`;
|
|
229
|
+
const inputId = `datetimepicker-input-${id}`;
|
|
230
|
+
|
|
231
|
+
// Display value (from committed value)
|
|
232
|
+
const displayValue = committedValue ? formatInstantDisplay(committedValue, clockFormat, showSeconds) : "";
|
|
233
|
+
const hasValue = Boolean(committedValue);
|
|
234
|
+
|
|
235
|
+
// Time selection handlers
|
|
236
|
+
const handleHourChange = (hour: number) => setDraftHour(hour);
|
|
237
|
+
const handleMinuteChange = (minute: number) => setDraftMinute(minute);
|
|
238
|
+
const handleSecondChange = (second: number) => setDraftSecond(second);
|
|
239
|
+
const handlePeriodChange = (period: TimePeriod) => setDraftPeriod(period);
|
|
240
|
+
|
|
241
|
+
// Generate time options
|
|
242
|
+
const hours = generateHours(clockFormat);
|
|
243
|
+
const minutes = generateMinutes(minuteStep);
|
|
244
|
+
const seconds = generateSeconds();
|
|
245
|
+
|
|
246
|
+
// Get current draft time for TimePicker display
|
|
247
|
+
const draftTime = getDraftTime();
|
|
248
|
+
|
|
249
|
+
// Check if a time is disabled
|
|
250
|
+
const checkTimeDisabled = useCallback(
|
|
251
|
+
(hour: number, minute: number, second: number): boolean => {
|
|
252
|
+
const temporal = Temporal.PlainTime.from({ hour, minute, second });
|
|
253
|
+
return isTimeDisabled(temporal, minTime, maxTime);
|
|
254
|
+
},
|
|
255
|
+
[minTime, maxTime]
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<>
|
|
260
|
+
{/* Trigger Input */}
|
|
261
|
+
<div className="relative flex items-center">
|
|
262
|
+
<Clock className="pointer-events-none absolute left-3 h-4 w-4 text-fg-subtle" />
|
|
263
|
+
<input
|
|
264
|
+
ref={refs.setReference as React.Ref<HTMLInputElement>}
|
|
265
|
+
id={inputId}
|
|
266
|
+
type="text"
|
|
267
|
+
readOnly
|
|
268
|
+
value={displayValue}
|
|
269
|
+
placeholder={placeholder}
|
|
270
|
+
disabled={isDisabled}
|
|
271
|
+
role="combobox"
|
|
272
|
+
aria-invalid={isInvalid}
|
|
273
|
+
aria-haspopup="dialog"
|
|
274
|
+
aria-expanded={open}
|
|
275
|
+
aria-controls={open ? contentId : undefined}
|
|
276
|
+
className={cn(
|
|
277
|
+
`flex h-9 w-full 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`,
|
|
278
|
+
clearable && hasValue ? "pr-9" : "pr-3",
|
|
279
|
+
isInvalid && "border-border-danger",
|
|
280
|
+
className
|
|
281
|
+
)}
|
|
282
|
+
{...getReferenceProps()}
|
|
283
|
+
/>
|
|
284
|
+
{clearable && hasValue && !isDisabled && (
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={handleClear}
|
|
288
|
+
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"
|
|
289
|
+
aria-label="Clear date and time"
|
|
290
|
+
>
|
|
291
|
+
<X className="h-3.5 w-3.5" />
|
|
292
|
+
</button>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{/* Hidden input for form submission */}
|
|
296
|
+
{name && committedValue && <input type="hidden" name={name} value={committedValue.toString()} />}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{/* Popover Content */}
|
|
300
|
+
{open && (
|
|
301
|
+
<FloatingPortal>
|
|
302
|
+
<div
|
|
303
|
+
ref={refs.setFloating}
|
|
304
|
+
id={contentId}
|
|
305
|
+
role="dialog"
|
|
306
|
+
aria-modal="true"
|
|
307
|
+
style={floatingStyles}
|
|
308
|
+
className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
|
|
309
|
+
{...getFloatingProps()}
|
|
310
|
+
>
|
|
311
|
+
<div className="flex gap-4">
|
|
312
|
+
{/* Date Calendar (left side) */}
|
|
313
|
+
<Calendar
|
|
314
|
+
mode="single"
|
|
315
|
+
value={draftDate}
|
|
316
|
+
onSelect={setDraftDate as (date: Temporal.PlainDate | undefined) => void}
|
|
317
|
+
month={month}
|
|
318
|
+
onMonthChange={setMonth}
|
|
319
|
+
minDate={minDate}
|
|
320
|
+
maxDate={maxDate}
|
|
321
|
+
disabled={disabledDates}
|
|
322
|
+
weekStartsOn={weekStartsOn}
|
|
323
|
+
showFooter={false}
|
|
324
|
+
/>
|
|
325
|
+
|
|
326
|
+
<Separator orientation="vertical" />
|
|
327
|
+
|
|
328
|
+
{/* Time Picker (right side) */}
|
|
329
|
+
<div className="flex flex-col gap-3">
|
|
330
|
+
<div className="text-center font-semibold text-sm">Time</div>
|
|
331
|
+
|
|
332
|
+
{/* Time columns */}
|
|
333
|
+
<div className="flex gap-2">
|
|
334
|
+
{/* Hours */}
|
|
335
|
+
<div className="flex flex-col gap-1">
|
|
336
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Hour</div>
|
|
337
|
+
<TimeColumn
|
|
338
|
+
values={hours}
|
|
339
|
+
selected={draftHour}
|
|
340
|
+
onSelect={handleHourChange}
|
|
341
|
+
disabled={(hour: number) => {
|
|
342
|
+
const hour24 = clockFormat === "12h" ? to24Hour(hour, draftPeriod) : hour;
|
|
343
|
+
return checkTimeDisabled(hour24, draftMinute, draftSecond);
|
|
344
|
+
}}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{/* Minutes */}
|
|
349
|
+
<div className="flex flex-col gap-1">
|
|
350
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Min</div>
|
|
351
|
+
<TimeColumn
|
|
352
|
+
values={minutes}
|
|
353
|
+
selected={draftMinute}
|
|
354
|
+
onSelect={handleMinuteChange}
|
|
355
|
+
disabled={(minute: number) => checkTimeDisabled(draftTime.hour, minute, draftSecond)}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{/* Seconds (optional) */}
|
|
360
|
+
{showSeconds && (
|
|
361
|
+
<div className="flex flex-col gap-1">
|
|
362
|
+
<div className="text-center font-medium text-fg-subtle text-xs">Sec</div>
|
|
363
|
+
<TimeColumn
|
|
364
|
+
values={seconds}
|
|
365
|
+
selected={draftSecond}
|
|
366
|
+
onSelect={handleSecondChange}
|
|
367
|
+
disabled={(second: number) => checkTimeDisabled(draftTime.hour, draftMinute, second)}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{/* AM/PM (12h format only) */}
|
|
373
|
+
{clockFormat === "12h" && (
|
|
374
|
+
<div className="flex flex-col gap-1">
|
|
375
|
+
<div className="text-center font-medium text-fg-subtle text-xs"> </div>
|
|
376
|
+
<PeriodSelector value={draftPeriod} onChange={handlePeriodChange} />
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<Separator className="my-3" />
|
|
384
|
+
|
|
385
|
+
{/* Footer with Cancel / Apply */}
|
|
386
|
+
<div className="flex items-center justify-end gap-2">
|
|
387
|
+
<Button type="button" variant="ghost" size="sm" onClick={handleCancel}>
|
|
388
|
+
Cancel
|
|
389
|
+
</Button>
|
|
390
|
+
<Button type="button" variant="solid" size="sm" onClick={handleApply}>
|
|
391
|
+
Apply
|
|
392
|
+
</Button>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</FloatingPortal>
|
|
396
|
+
)}
|
|
397
|
+
</>
|
|
398
|
+
);
|
|
399
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Temporal, WeekStartsOn } from "../../date-time";
|
|
2
|
+
|
|
3
|
+
export type DisabledDateMatcher =
|
|
4
|
+
| Temporal.PlainDate
|
|
5
|
+
| Temporal.PlainDate[]
|
|
6
|
+
| ((date: Temporal.PlainDate) => boolean)
|
|
7
|
+
| { before: Temporal.PlainDate }
|
|
8
|
+
| { after: Temporal.PlainDate }
|
|
9
|
+
| { from: Temporal.PlainDate; to: Temporal.PlainDate };
|
|
10
|
+
|
|
11
|
+
export type DateTimePickerProps = {
|
|
12
|
+
// Value (controlled/uncontrolled) - NOW USES INSTANT
|
|
13
|
+
value?: Temporal.Instant | undefined;
|
|
14
|
+
defaultValue?: Temporal.Instant | undefined;
|
|
15
|
+
onChange?: (instant: Temporal.Instant | undefined) => void;
|
|
16
|
+
|
|
17
|
+
// Date constraints
|
|
18
|
+
minDate?: Temporal.PlainDate;
|
|
19
|
+
maxDate?: Temporal.PlainDate;
|
|
20
|
+
disabledDates?: DisabledDateMatcher | DisabledDateMatcher[];
|
|
21
|
+
|
|
22
|
+
// Time constraints
|
|
23
|
+
minTime?: Temporal.PlainTime;
|
|
24
|
+
maxTime?: Temporal.PlainTime;
|
|
25
|
+
minuteStep?: 1 | 5 | 10 | 15 | 30;
|
|
26
|
+
|
|
27
|
+
// Time format
|
|
28
|
+
clockFormat?: "12h" | "24h"; // Default: "12h"
|
|
29
|
+
showSeconds?: boolean; // Default: false
|
|
30
|
+
|
|
31
|
+
// Display
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
weekStartsOn?: WeekStartsOn;
|
|
34
|
+
|
|
35
|
+
// Form
|
|
36
|
+
name?: string;
|
|
37
|
+
|
|
38
|
+
// State
|
|
39
|
+
isDisabled?: boolean;
|
|
40
|
+
isInvalid?: boolean;
|
|
41
|
+
|
|
42
|
+
// Style
|
|
43
|
+
className?: string;
|
|
44
|
+
align?: "start" | "end";
|
|
45
|
+
|
|
46
|
+
/** Whether the clear button is shown when a value is present (default: true) */
|
|
47
|
+
clearable?: boolean;
|
|
48
|
+
};
|