@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.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/styles.css +2 -0
  4. package/dist/theme.generated.css +438 -0
  5. package/package.json +238 -0
  6. package/src/components/Avatar/Avatar.tsx +19 -0
  7. package/src/components/Avatar/index.ts +1 -0
  8. package/src/components/Badge/Badge.tsx +59 -0
  9. package/src/components/Badge/index.ts +1 -0
  10. package/src/components/BarChart/BarChart.tsx +51 -0
  11. package/src/components/BarChart/index.ts +1 -0
  12. package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
  13. package/src/components/Breadcrumb/index.ts +5 -0
  14. package/src/components/Button/Button.tsx +157 -0
  15. package/src/components/Button/index.ts +1 -0
  16. package/src/components/Calendar/Calendar.tsx +433 -0
  17. package/src/components/Calendar/calendar.hooks.ts +329 -0
  18. package/src/components/Calendar/calendar.types.ts +117 -0
  19. package/src/components/Calendar/index.ts +28 -0
  20. package/src/components/Card/Card.tsx +47 -0
  21. package/src/components/Card/index.ts +1 -0
  22. package/src/components/Checkbox/Checkbox.tsx +144 -0
  23. package/src/components/Checkbox/index.ts +1 -0
  24. package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
  25. package/src/components/CheckboxGroup/index.ts +5 -0
  26. package/src/components/Code/Editor/Editor.tsx +161 -0
  27. package/src/components/Code/Editor/index.ts +2 -0
  28. package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
  29. package/src/components/Code/Editor/themes.ts +112 -0
  30. package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
  31. package/src/components/Code/ValidationStatus/index.ts +1 -0
  32. package/src/components/Code/Workspace/StatusBar.tsx +47 -0
  33. package/src/components/Code/Workspace/Toolbar.tsx +47 -0
  34. package/src/components/Code/Workspace/Workspace.tsx +109 -0
  35. package/src/components/Code/Workspace/index.ts +3 -0
  36. package/src/components/ColorDot/ColorDot.tsx +50 -0
  37. package/src/components/ColorDot/index.ts +1 -0
  38. package/src/components/DatePicker/DatePicker.tsx +236 -0
  39. package/src/components/DatePicker/datepicker.hooks.ts +381 -0
  40. package/src/components/DatePicker/datepicker.types.ts +140 -0
  41. package/src/components/DatePicker/index.ts +12 -0
  42. package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
  43. package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
  44. package/src/components/DateRangePicker/index.ts +2 -0
  45. package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
  46. package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
  47. package/src/components/DateTimePicker/index.ts +2 -0
  48. package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
  49. package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
  50. package/src/components/DateTimeRangePicker/index.ts +2 -0
  51. package/src/components/EmptyState/EmptyState.tsx +29 -0
  52. package/src/components/EmptyState/index.ts +1 -0
  53. package/src/components/Field/Field.tsx +60 -0
  54. package/src/components/Field/index.ts +1 -0
  55. package/src/components/FormField/FormField.tsx +55 -0
  56. package/src/components/FormField/index.ts +2 -0
  57. package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
  58. package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
  59. package/src/components/HeatMap/heat-map.utils.ts +15 -0
  60. package/src/components/HeatMap/index.ts +3 -0
  61. package/src/components/HoverCard/HoverCard.tsx +83 -0
  62. package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
  63. package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
  64. package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
  65. package/src/components/HoverCard/index.ts +3 -0
  66. package/src/components/Input/Input.tsx +48 -0
  67. package/src/components/Input/index.ts +1 -0
  68. package/src/components/LineChart/LineChart.tsx +251 -0
  69. package/src/components/LineChart/index.ts +1 -0
  70. package/src/components/Markdown/Markdown.tsx +40 -0
  71. package/src/components/Markdown/index.ts +1 -0
  72. package/src/components/Message/Message.tsx +59 -0
  73. package/src/components/Message/components/MessageBody.tsx +9 -0
  74. package/src/components/Message/components/MessageDescription.tsx +9 -0
  75. package/src/components/Message/components/MessageIcon.tsx +13 -0
  76. package/src/components/Message/components/MessageTitle.tsx +9 -0
  77. package/src/components/Message/index.ts +10 -0
  78. package/src/components/MetricList/MetricList.tsx +13 -0
  79. package/src/components/MetricList/MetricRow.tsx +30 -0
  80. package/src/components/MetricList/index.ts +2 -0
  81. package/src/components/Modal/Modal.tsx +146 -0
  82. package/src/components/Modal/components/ModalBody.tsx +9 -0
  83. package/src/components/Modal/components/ModalContext.tsx +27 -0
  84. package/src/components/Modal/components/ModalFooter.tsx +9 -0
  85. package/src/components/Modal/components/ModalHeader.tsx +29 -0
  86. package/src/components/Modal/components/index.ts +9 -0
  87. package/src/components/Modal/index.ts +13 -0
  88. package/src/components/NavBottom/NavBottom.tsx +53 -0
  89. package/src/components/NavBottom/index.ts +3 -0
  90. package/src/components/NavSide/NavItem.tsx +39 -0
  91. package/src/components/NavSide/NavSide.tsx +40 -0
  92. package/src/components/NavSide/index.ts +2 -0
  93. package/src/components/NavSide/types.ts +36 -0
  94. package/src/components/Pagination/Pagination.tsx +86 -0
  95. package/src/components/Pagination/index.ts +2 -0
  96. package/src/components/Popover/Popover.tsx +68 -0
  97. package/src/components/Popover/components/PopoverBody.tsx +32 -0
  98. package/src/components/Popover/components/PopoverClose.tsx +26 -0
  99. package/src/components/Popover/components/PopoverContext.tsx +24 -0
  100. package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
  101. package/src/components/Popover/index.ts +10 -0
  102. package/src/components/ProgressBar/ProgressBar.tsx +42 -0
  103. package/src/components/ProgressBar/index.ts +1 -0
  104. package/src/components/RadioButton/RadioButton.tsx +89 -0
  105. package/src/components/RadioButton/index.ts +1 -0
  106. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
  107. package/src/components/RadioButtonGroup/index.ts +5 -0
  108. package/src/components/SearchInput/SearchInput.tsx +45 -0
  109. package/src/components/SearchInput/index.ts +2 -0
  110. package/src/components/Select/Select.tsx +289 -0
  111. package/src/components/Select/SelectDropdown.tsx +90 -0
  112. package/src/components/Select/SelectTrigger.tsx +130 -0
  113. package/src/components/Select/index.ts +3 -0
  114. package/src/components/Select/useSelectKeyboard.ts +111 -0
  115. package/src/components/Separator/Separator.tsx +20 -0
  116. package/src/components/Separator/index.ts +1 -0
  117. package/src/components/Sheet/Sheet.tsx +136 -0
  118. package/src/components/Sheet/components/SheetBody.tsx +9 -0
  119. package/src/components/Sheet/components/SheetContext.tsx +23 -0
  120. package/src/components/Sheet/components/SheetFooter.tsx +9 -0
  121. package/src/components/Sheet/components/SheetHeader.tsx +29 -0
  122. package/src/components/Sheet/components/index.ts +9 -0
  123. package/src/components/Sheet/index.ts +13 -0
  124. package/src/components/Skeleton/Skeleton.tsx +7 -0
  125. package/src/components/Skeleton/index.ts +1 -0
  126. package/src/components/Sparkline/Sparkline.tsx +93 -0
  127. package/src/components/Sparkline/index.ts +1 -0
  128. package/src/components/Spinner/Spinner.tsx +41 -0
  129. package/src/components/Spinner/index.ts +1 -0
  130. package/src/components/StatsCard/StatsCard.tsx +40 -0
  131. package/src/components/StatsCard/index.ts +1 -0
  132. package/src/components/Switch/Switch.tsx +128 -0
  133. package/src/components/Switch/index.ts +1 -0
  134. package/src/components/Tabs/Tabs.tsx +49 -0
  135. package/src/components/Tabs/components/TabsContext.tsx +35 -0
  136. package/src/components/Tabs/components/TabsList.tsx +98 -0
  137. package/src/components/Tabs/components/TabsPanels.tsx +26 -0
  138. package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
  139. package/src/components/Tabs/index.ts +3 -0
  140. package/src/components/TagChip/TagChip.tsx +46 -0
  141. package/src/components/TagChip/index.ts +1 -0
  142. package/src/components/TagInput/TagInput.tsx +60 -0
  143. package/src/components/TagInput/index.ts +1 -0
  144. package/src/components/Text/Text.tsx +35 -0
  145. package/src/components/Text/index.ts +1 -0
  146. package/src/components/TextInline/TextInline.tsx +35 -0
  147. package/src/components/TextInline/index.ts +1 -0
  148. package/src/components/Textarea/Textarea.tsx +20 -0
  149. package/src/components/Textarea/index.ts +1 -0
  150. package/src/components/TimePicker/PeriodSelector.tsx +28 -0
  151. package/src/components/TimePicker/TimeColumn.tsx +62 -0
  152. package/src/components/TimePicker/TimePicker.tsx +323 -0
  153. package/src/components/TimePicker/index.ts +7 -0
  154. package/src/components/TimePicker/timepicker.hooks.ts +173 -0
  155. package/src/components/TimePicker/timepicker.types.ts +101 -0
  156. package/src/components/Toast/Toast.tsx +111 -0
  157. package/src/components/Toast/index.ts +1 -0
  158. package/src/components/Toggle/Toggle.tsx +72 -0
  159. package/src/components/Toggle/index.ts +1 -0
  160. package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
  161. package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
  162. package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
  163. package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
  164. package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
  165. package/src/components/ToggleGroup/index.ts +2 -0
  166. package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
  167. package/src/components/Tooltip/Tooltip.tsx +66 -0
  168. package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
  169. package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
  170. package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
  171. package/src/components/Tooltip/index.ts +9 -0
  172. package/src/date-time/arithmetic.ts +101 -0
  173. package/src/date-time/boundaries.ts +91 -0
  174. package/src/date-time/compare.ts +85 -0
  175. package/src/date-time/duration.ts +63 -0
  176. package/src/date-time/elapsed.ts +18 -0
  177. package/src/date-time/format.ts +722 -0
  178. package/src/date-time/index.ts +279 -0
  179. package/src/date-time/now.ts +70 -0
  180. package/src/date-time/parse.ts +170 -0
  181. package/src/date-time/serialize.ts +90 -0
  182. package/src/date-time/timezone.ts +47 -0
  183. package/src/date-time/validate.ts +278 -0
  184. package/src/hooks/index.ts +10 -0
  185. package/src/hooks/useCollapsible.ts +16 -0
  186. package/src/hooks/useControllableState.ts +54 -0
  187. package/src/hooks/useDebouncedCallback.ts +21 -0
  188. package/src/hooks/useDebouncedState.ts +20 -0
  189. package/src/hooks/useDeviceFormFactor.ts +49 -0
  190. package/src/hooks/useLocalStorage.ts +67 -0
  191. package/src/hooks/useMergedFloatingRef.ts +29 -0
  192. package/src/hooks/usePresence.ts +127 -0
  193. package/src/hooks/useStack.ts +99 -0
  194. package/src/hooks/useTheme.ts +105 -0
  195. package/src/internal/Slot.tsx +79 -0
  196. package/src/internal/index.ts +2 -0
  197. package/src/internal/rovingFocus.ts +39 -0
  198. package/src/internal/types.ts +10 -0
  199. package/src/lib/EmptyBoundary.tsx +12 -0
  200. package/src/lib/ErrorBoundary.tsx +48 -0
  201. package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
  202. package/src/lib/contexts/ComponentsContext/index.ts +1 -0
  203. package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
  204. package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
  205. package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
  206. package/src/lib/contexts/OverlayContext/index.ts +6 -0
  207. package/src/lib/format.ts +51 -0
  208. package/src/lib/utils.ts +10 -0
  209. package/src/lib/withEmpty.tsx +14 -0
  210. package/src/lib/withErrorBoundary.tsx +16 -0
  211. package/src/lib/withSuspense.tsx +16 -0
  212. package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
  213. package/src/styles/base.css +41 -0
  214. package/src/theme/index.ts +37 -0
  215. package/src/theme/palette.json +100 -0
  216. package/src/theme/palette.schema.json +40 -0
  217. package/src/theme/schema.ts +122 -0
  218. package/src/theme/theme.json +349 -0
  219. package/src/theme/theme.schema.json +876 -0
  220. package/src/theme/typography.json +7 -0
  221. package/src/theme/typography.schema.json +36 -0
  222. package/src/typography/Body.tsx +8 -0
  223. package/src/typography/Caption.tsx +8 -0
  224. package/src/typography/Code.tsx +8 -0
  225. package/src/typography/Display.tsx +8 -0
  226. package/src/typography/Emphasis.tsx +8 -0
  227. package/src/typography/Heading.tsx +23 -0
  228. package/src/typography/Label.tsx +10 -0
  229. package/src/typography/Lead.tsx +8 -0
  230. package/src/typography/Mono.tsx +8 -0
  231. package/src/typography/Muted.tsx +8 -0
  232. package/src/typography/SectionTitle.tsx +8 -0
  233. package/src/typography/Subtitle.tsx +8 -0
  234. package/src/typography/Timestamp.tsx +8 -0
  235. package/src/typography/index.ts +10 -0
  236. 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,2 @@
1
+ export { DateRangePicker } from "./DateRangePicker";
2
+ export type { DateRangePickerProps, DateRangeValue, DisabledDateMatcher, PresetOption } from "./daterangepicker.types";
@@ -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">&nbsp;</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
+ };
@@ -0,0 +1,2 @@
1
+ export { DateTimePicker } from "./DateTimePicker";
2
+ export type { DateTimePickerProps, DisabledDateMatcher } from "./datetimepicker.types";