@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,381 @@
1
+ import { useCallback } from "react";
2
+ import { formatDateMediumYear, isPlainDate, Temporal } from "../../date-time";
3
+ import type { DatePickerMode, DateRangeValue, DateSelectionState, DisabledDateMatcher } from "./datepicker.types";
4
+
5
+ // =============================================================================
6
+ // Date Comparison Utilities (using Temporal)
7
+ // =============================================================================
8
+
9
+ /**
10
+ * Check if two PlainDates represent the same day
11
+ */
12
+ export function isSameDateTemporal(a: Temporal.PlainDate, b: Temporal.PlainDate): boolean {
13
+ return Temporal.PlainDate.compare(a, b) === 0;
14
+ }
15
+
16
+ /**
17
+ * Check if date a is before date b
18
+ */
19
+ export function isBeforeTemporal(a: Temporal.PlainDate, b: Temporal.PlainDate): boolean {
20
+ return Temporal.PlainDate.compare(a, b) < 0;
21
+ }
22
+
23
+ /**
24
+ * Check if date a is after date b
25
+ */
26
+ export function isAfterTemporal(a: Temporal.PlainDate, b: Temporal.PlainDate): boolean {
27
+ return Temporal.PlainDate.compare(a, b) > 0;
28
+ }
29
+
30
+ /**
31
+ * Check if a date is between two dates (inclusive)
32
+ */
33
+ export function isBetweenTemporal(date: Temporal.PlainDate, start: Temporal.PlainDate, end: Temporal.PlainDate): boolean {
34
+ return !isBeforeTemporal(date, start) && !isAfterTemporal(date, end);
35
+ }
36
+
37
+ /**
38
+ * Convert a Temporal.PlainDate to ISO string (for form submission)
39
+ */
40
+ export function temporalToISO(temporal: Temporal.PlainDate): string {
41
+ return temporal.toString();
42
+ }
43
+
44
+ // =============================================================================
45
+ // Disabled Date Checking
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Check if a date matches a single disabled matcher
50
+ */
51
+ function matchesDisabledMatcher(date: Temporal.PlainDate, matcher: DisabledDateMatcher): boolean {
52
+ // Single date (Temporal.PlainDate)
53
+ if (isPlainDate(matcher)) {
54
+ return isSameDateTemporal(date, matcher);
55
+ }
56
+
57
+ // Array of dates
58
+ if (Array.isArray(matcher)) {
59
+ return matcher.some((d) => isPlainDate(d) && isSameDateTemporal(date, d));
60
+ }
61
+
62
+ // Function
63
+ if (typeof matcher === "function") {
64
+ return matcher(date);
65
+ }
66
+
67
+ // Before constraint
68
+ if ("before" in matcher && !("to" in matcher)) {
69
+ return isBeforeTemporal(date, matcher.before);
70
+ }
71
+
72
+ // After constraint
73
+ if ("after" in matcher) {
74
+ return isAfterTemporal(date, matcher.after);
75
+ }
76
+
77
+ // Range constraint
78
+ if ("from" in matcher && "to" in matcher) {
79
+ return isBetweenTemporal(date, matcher.from, matcher.to);
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Check if a date is disabled based on matchers, minDate, and maxDate
87
+ */
88
+ export function isDateDisabled(
89
+ date: Temporal.PlainDate,
90
+ disabled?: DisabledDateMatcher | DisabledDateMatcher[],
91
+ minDate?: Temporal.PlainDate,
92
+ maxDate?: Temporal.PlainDate
93
+ ): boolean {
94
+ // Check min/max bounds
95
+ if (minDate && isBeforeTemporal(date, minDate)) {
96
+ return true;
97
+ }
98
+ if (maxDate && isAfterTemporal(date, maxDate)) {
99
+ return true;
100
+ }
101
+
102
+ // No disabled matchers
103
+ if (!disabled) {
104
+ return false;
105
+ }
106
+
107
+ // Single matcher
108
+ if (!Array.isArray(disabled)) {
109
+ return matchesDisabledMatcher(date, disabled);
110
+ }
111
+
112
+ // Array of matchers (any match = disabled)
113
+ return disabled.some((matcher) => matchesDisabledMatcher(date, matcher));
114
+ }
115
+
116
+ /**
117
+ * Hook for checking if dates are disabled
118
+ */
119
+ export function useIsDateDisabled(disabled?: DisabledDateMatcher | DisabledDateMatcher[], minDate?: Temporal.PlainDate, maxDate?: Temporal.PlainDate) {
120
+ return useCallback((date: Temporal.PlainDate) => isDateDisabled(date, disabled, minDate, maxDate), [disabled, minDate, maxDate]);
121
+ }
122
+
123
+ // =============================================================================
124
+ // Selection State
125
+ // =============================================================================
126
+
127
+ /**
128
+ * Get the selection state for a date based on mode and current value
129
+ */
130
+ export function getDateSelectionState(
131
+ date: Temporal.PlainDate,
132
+ mode: DatePickerMode,
133
+ value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined
134
+ ): DateSelectionState {
135
+ // Default: not selected
136
+ const notSelected: DateSelectionState = {
137
+ isSelected: false,
138
+ rangePosition: "none",
139
+ };
140
+
141
+ if (value === undefined) {
142
+ return notSelected;
143
+ }
144
+
145
+ // Single mode
146
+ if (mode === "single") {
147
+ const singleValue = value as Temporal.PlainDate | undefined;
148
+ if (!singleValue || !isPlainDate(singleValue)) return notSelected;
149
+ return {
150
+ isSelected: isSameDateTemporal(date, singleValue),
151
+ rangePosition: "single",
152
+ };
153
+ }
154
+
155
+ // Multiple mode
156
+ if (mode === "multiple") {
157
+ const multiValue = value as Temporal.PlainDate[];
158
+ return {
159
+ isSelected: multiValue.some((d) => isSameDateTemporal(date, d)),
160
+ rangePosition: "single",
161
+ };
162
+ }
163
+
164
+ // Range mode
165
+ const rangeValue = value as DateRangeValue;
166
+ const { from, to } = rangeValue;
167
+
168
+ // No selection
169
+ if (!from && !to) {
170
+ return notSelected;
171
+ }
172
+
173
+ // Only from selected
174
+ if (from && !to) {
175
+ return {
176
+ isSelected: isSameDateTemporal(date, from),
177
+ rangePosition: "single",
178
+ };
179
+ }
180
+
181
+ // Only to selected (shouldn't happen normally, but handle it)
182
+ if (!from && to) {
183
+ return {
184
+ isSelected: isSameDateTemporal(date, to),
185
+ rangePosition: "single",
186
+ };
187
+ }
188
+
189
+ // Both from and to selected
190
+ const isFrom = isSameDateTemporal(date, from!);
191
+ const isTo = isSameDateTemporal(date, to!);
192
+ const isBetween = isBetweenTemporal(date, from!, to!);
193
+
194
+ if (!isBetween && !isFrom && !isTo) {
195
+ return notSelected;
196
+ }
197
+
198
+ // Same day for from and to
199
+ if (isFrom && isTo) {
200
+ return { isSelected: true, rangePosition: "single" };
201
+ }
202
+
203
+ // Start of range
204
+ if (isFrom) {
205
+ return { isSelected: true, rangePosition: "start" };
206
+ }
207
+
208
+ // End of range
209
+ if (isTo) {
210
+ return { isSelected: true, rangePosition: "end" };
211
+ }
212
+
213
+ // Middle of range
214
+ return { isSelected: true, rangePosition: "middle" };
215
+ }
216
+
217
+ /**
218
+ * Hook for getting selection state
219
+ */
220
+ export function useSelectionState(mode: DatePickerMode, value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined) {
221
+ return useCallback((date: Temporal.PlainDate) => getDateSelectionState(date, mode, value), [mode, value]);
222
+ }
223
+
224
+ // =============================================================================
225
+ // Selection Handlers
226
+ // =============================================================================
227
+
228
+ /**
229
+ * Create a selection handler based on mode
230
+ */
231
+ export function createSelectionHandler(
232
+ mode: DatePickerMode,
233
+ value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined,
234
+ onChange:
235
+ | ((value: Temporal.PlainDate | undefined) => void)
236
+ | ((value: DateRangeValue | undefined) => void)
237
+ | ((value: Temporal.PlainDate[]) => void)
238
+ | undefined,
239
+ options?: {
240
+ closeOnSelect?: boolean | undefined;
241
+ onOpenChange?: ((open: boolean) => void) | undefined;
242
+ max?: number | undefined; // For multiple mode
243
+ }
244
+ ): (date: Temporal.PlainDate) => void {
245
+ return (date: Temporal.PlainDate) => {
246
+ if (!onChange) return;
247
+
248
+ if (mode === "single") {
249
+ const singleOnChange = onChange as (value: Temporal.PlainDate | undefined) => void;
250
+ // Toggle: if already selected, deselect
251
+ const currentValue = value as Temporal.PlainDate | undefined;
252
+ if (currentValue && isSameDateTemporal(date, currentValue)) {
253
+ singleOnChange(undefined);
254
+ } else {
255
+ singleOnChange(date);
256
+ // Close on select for single mode
257
+ if (options?.closeOnSelect !== false) {
258
+ options?.onOpenChange?.(false);
259
+ }
260
+ }
261
+ return;
262
+ }
263
+
264
+ if (mode === "multiple") {
265
+ const multiOnChange = onChange as (value: Temporal.PlainDate[]) => void;
266
+ const currentValue = (value as Temporal.PlainDate[]) || [];
267
+ const existingIndex = currentValue.findIndex((d) => isSameDateTemporal(date, d));
268
+
269
+ if (existingIndex >= 0) {
270
+ // Remove if already selected
271
+ multiOnChange(currentValue.filter((_, i) => i !== existingIndex));
272
+ } else {
273
+ // Add if not at max
274
+ if (!options?.max || currentValue.length < options.max) {
275
+ multiOnChange([...currentValue, date]);
276
+ }
277
+ }
278
+ return;
279
+ }
280
+
281
+ // Range mode
282
+ const rangeOnChange = onChange as (value: DateRangeValue | undefined) => void;
283
+ const currentRange = (value as DateRangeValue) || {
284
+ from: undefined,
285
+ to: undefined,
286
+ };
287
+
288
+ // If no from date, set it
289
+ if (!currentRange.from) {
290
+ rangeOnChange({ from: date, to: undefined });
291
+ return;
292
+ }
293
+
294
+ // If from but no to, set to (or swap if before from)
295
+ if (!currentRange.to) {
296
+ if (isBeforeTemporal(date, currentRange.from)) {
297
+ // Swap: new date becomes from, old from becomes to
298
+ rangeOnChange({ from: date, to: currentRange.from });
299
+ } else {
300
+ rangeOnChange({ from: currentRange.from, to: date });
301
+ }
302
+ // Close on select for range mode when complete
303
+ if (options?.closeOnSelect !== false) {
304
+ options?.onOpenChange?.(false);
305
+ }
306
+ return;
307
+ }
308
+
309
+ // If both are set, start a new selection
310
+ rangeOnChange({ from: date, to: undefined });
311
+ };
312
+ }
313
+
314
+ // =============================================================================
315
+ // Display Formatting
316
+ // =============================================================================
317
+
318
+ /**
319
+ * Default display format for a date (e.g., "Jan 15, 2025")
320
+ */
321
+ export function defaultFormatDisplay(date: Temporal.PlainDate): string {
322
+ return formatDateMediumYear(date);
323
+ }
324
+
325
+ /**
326
+ * Format a range for display
327
+ */
328
+ export function formatRangeDisplay(range: DateRangeValue | undefined, formatFn: (date: Temporal.PlainDate) => string = defaultFormatDisplay): string {
329
+ if (!range?.from && !range?.to) {
330
+ return "";
331
+ }
332
+
333
+ if (range.from && !range.to) {
334
+ return `${formatFn(range.from)} – ...`;
335
+ }
336
+
337
+ if (!range.from && range.to) {
338
+ return `... – ${formatFn(range.to)}`;
339
+ }
340
+
341
+ return `${formatFn(range.from!)} – ${formatFn(range.to!)}`;
342
+ }
343
+
344
+ /**
345
+ * Format multiple dates for display
346
+ */
347
+ export function formatMultipleDisplay(dates: Temporal.PlainDate[] | undefined, formatFn: (date: Temporal.PlainDate) => string = defaultFormatDisplay): string {
348
+ if (!dates || dates.length === 0) {
349
+ return "";
350
+ }
351
+
352
+ if (dates.length === 1) {
353
+ return formatFn(dates[0]!);
354
+ }
355
+
356
+ if (dates.length <= 3) {
357
+ return dates.map(formatFn).join(", ");
358
+ }
359
+
360
+ return `${dates.length} dates selected`;
361
+ }
362
+
363
+ /**
364
+ * Get display value based on mode
365
+ */
366
+ export function getDisplayValue(
367
+ mode: DatePickerMode,
368
+ value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined,
369
+ formatFn: (date: Temporal.PlainDate) => string = defaultFormatDisplay
370
+ ): string {
371
+ if (mode === "single") {
372
+ const singleValue = value as Temporal.PlainDate | undefined;
373
+ return singleValue ? formatFn(singleValue) : "";
374
+ }
375
+
376
+ if (mode === "range") {
377
+ return formatRangeDisplay(value as DateRangeValue | undefined, formatFn);
378
+ }
379
+
380
+ return formatMultipleDisplay(value as Temporal.PlainDate[] | undefined, formatFn);
381
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * DatePicker types
3
+ *
4
+ * All date values use Temporal.PlainDate at the component boundary.
5
+ * This provides type safety and semantic clarity about what kind of date is expected.
6
+ */
7
+
8
+ import type { Temporal, WeekStartsOn } from "../../date-time";
9
+ import type { PresetOption as CalendarPresetOption } from "../Calendar/calendar.types";
10
+
11
+ // =============================================================================
12
+ // Value Types
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Date range value for range mode
17
+ */
18
+ export type DateRangeValue = {
19
+ from: Temporal.PlainDate | undefined;
20
+ to: Temporal.PlainDate | undefined;
21
+ };
22
+
23
+ /**
24
+ * Selection mode for the DatePicker
25
+ */
26
+ export type DatePickerMode = "single" | "range" | "multiple";
27
+
28
+ // =============================================================================
29
+ // Disabled Date Matching
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Matchers for disabling dates
34
+ */
35
+ export type DisabledDateMatcher =
36
+ | Temporal.PlainDate // Single date
37
+ | Temporal.PlainDate[] // Array of dates
38
+ | ((date: Temporal.PlainDate) => boolean) // Custom function
39
+ | { before: Temporal.PlainDate } // All dates before
40
+ | { after: Temporal.PlainDate } // All dates after
41
+ | { from: Temporal.PlainDate; to: Temporal.PlainDate }; // Date range
42
+
43
+ // =============================================================================
44
+ // Props (discriminated union by mode)
45
+ // =============================================================================
46
+
47
+ type DatePickerBaseProps = {
48
+ /** First day of week (0 = Sunday, 1 = Monday) */
49
+ weekStartsOn?: WeekStartsOn;
50
+
51
+ /** Minimum selectable date */
52
+ minDate?: Temporal.PlainDate;
53
+
54
+ /** Maximum selectable date */
55
+ maxDate?: Temporal.PlainDate;
56
+
57
+ /** Disabled dates */
58
+ disabled?: DisabledDateMatcher | DisabledDateMatcher[];
59
+
60
+ /** Placeholder text */
61
+ placeholder?: string;
62
+
63
+ /** Input name for form submission */
64
+ name?: string;
65
+
66
+ /** Whether the picker is disabled */
67
+ isDisabled?: boolean;
68
+
69
+ /** Error state */
70
+ isInvalid?: boolean;
71
+
72
+ /** Additional class name for the trigger input */
73
+ className?: string;
74
+
75
+ /** Horizontal alignment of the popup calendar */
76
+ align?: "start" | "end";
77
+
78
+ /** Whether the clear button is shown when a value is present (default: true) */
79
+ clearable?: boolean;
80
+ };
81
+
82
+ export type DatePickerSingleProps = DatePickerBaseProps & {
83
+ mode?: "single";
84
+ /** Controlled value */
85
+ value?: Temporal.PlainDate | undefined;
86
+ /** Default value for uncontrolled mode */
87
+ defaultValue?: Temporal.PlainDate | undefined;
88
+ /** Called when date changes */
89
+ onChange?: (date: Temporal.PlainDate | undefined) => void;
90
+ /** Preset options (shown in left sidebar) */
91
+ presets?: CalendarPresetOption<Temporal.PlainDate>[];
92
+ };
93
+
94
+ export type DatePickerRangeProps = DatePickerBaseProps & {
95
+ mode: "range";
96
+ /** Controlled value */
97
+ value?: DateRangeValue | undefined;
98
+ /** Default value for uncontrolled mode */
99
+ defaultValue?: DateRangeValue | undefined;
100
+ /** Called when range changes */
101
+ onChange?: (range: DateRangeValue | undefined) => void;
102
+ /** Preset options (shown in left sidebar) */
103
+ presets?: CalendarPresetOption<DateRangeValue>[];
104
+ };
105
+
106
+ export type DatePickerMultipleProps = DatePickerBaseProps & {
107
+ mode: "multiple";
108
+ /** Controlled value (array of dates) */
109
+ value?: Temporal.PlainDate[] | undefined;
110
+ /** Default value for uncontrolled mode */
111
+ defaultValue?: Temporal.PlainDate[] | undefined;
112
+ /** Called when selection changes */
113
+ onChange?: (dates: Temporal.PlainDate[]) => void;
114
+ /** Maximum number of dates that can be selected */
115
+ max?: number | undefined;
116
+ /** Preset options (shown in left sidebar) */
117
+ presets?: CalendarPresetOption<Temporal.PlainDate[]>[];
118
+ };
119
+
120
+ /**
121
+ * DatePicker props - discriminated union based on mode
122
+ */
123
+ export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps | DatePickerMultipleProps;
124
+
125
+ // =============================================================================
126
+ // Internal Types
127
+ // =============================================================================
128
+
129
+ /**
130
+ * Position of a date within a range selection
131
+ */
132
+ export type RangePosition = "start" | "middle" | "end" | "single" | "none";
133
+
134
+ /**
135
+ * Selection state for a date cell
136
+ */
137
+ export type DateSelectionState = {
138
+ isSelected: boolean;
139
+ rangePosition: RangePosition;
140
+ };
@@ -0,0 +1,12 @@
1
+ // Note: PresetOption is now exported from Calendar types
2
+ export type { PresetOption } from "../Calendar/calendar.types";
3
+ export { DatePicker } from "./DatePicker";
4
+ export type {
5
+ DatePickerMode,
6
+ DatePickerMultipleProps,
7
+ DatePickerProps,
8
+ DatePickerRangeProps,
9
+ DatePickerSingleProps,
10
+ DateRangeValue,
11
+ DisabledDateMatcher,
12
+ } from "./datepicker.types";
@@ -0,0 +1,127 @@
1
+ import { useMemo } from "react";
2
+ import { today } from "../../date-time";
3
+
4
+ import { DatePicker } from "../DatePicker";
5
+ import type { DatePickerRangeProps } from "../DatePicker/datepicker.types";
6
+ import type { DateRangePickerProps, PresetOption } from "./daterangepicker.types";
7
+
8
+ /**
9
+ * Default preset ranges for DateRangePicker
10
+ * Computed fresh on each render to ensure "today" is always current
11
+ */
12
+ function getDefaultPresets(): PresetOption[] {
13
+ return [
14
+ {
15
+ label: "Today",
16
+ value: () => {
17
+ const t = today();
18
+ return { from: t, to: t };
19
+ },
20
+ },
21
+ {
22
+ label: "Yesterday",
23
+ value: () => {
24
+ const yesterday = today().subtract({ days: 1 });
25
+ return { from: yesterday, to: yesterday };
26
+ },
27
+ },
28
+ {
29
+ label: "Last 7 days",
30
+ value: () => {
31
+ const t = today();
32
+ const weekAgo = t.subtract({ days: 6 });
33
+ return { from: weekAgo, to: t };
34
+ },
35
+ },
36
+ {
37
+ label: "Last 30 days",
38
+ value: () => {
39
+ const t = today();
40
+ const monthAgo = t.subtract({ days: 29 });
41
+ return { from: monthAgo, to: t };
42
+ },
43
+ },
44
+ {
45
+ label: "This month",
46
+ value: () => {
47
+ const t = today();
48
+ const firstDay = t.with({ day: 1 });
49
+ return { from: firstDay, to: t };
50
+ },
51
+ },
52
+ {
53
+ label: "Last month",
54
+ value: () => {
55
+ const t = today();
56
+ const firstDayThisMonth = t.with({ day: 1 });
57
+ const lastDayLastMonth = firstDayThisMonth.subtract({ days: 1 });
58
+ const firstDayLastMonth = lastDayLastMonth.with({ day: 1 });
59
+ return { from: firstDayLastMonth, to: lastDayLastMonth };
60
+ },
61
+ },
62
+ ];
63
+ }
64
+
65
+ /**
66
+ * DateRangePicker - A specialized date range picker component
67
+ *
68
+ * Wraps DatePicker with mode="range" and adds common range presets.
69
+ * Uses Temporal.PlainDate for all date values.
70
+ */
71
+ export const DateRangePicker = (props: DateRangePickerProps) => {
72
+ const {
73
+ value,
74
+ defaultValue,
75
+ onChange,
76
+ minDate,
77
+ maxDate,
78
+ disabled,
79
+ presets: customPresets,
80
+ showPresets = true,
81
+ placeholder = "Select date range...",
82
+ weekStartsOn,
83
+ name,
84
+ isDisabled,
85
+ isInvalid,
86
+ className,
87
+ align,
88
+ clearable,
89
+ } = props;
90
+
91
+ // Compute presets on each render to ensure they're always current
92
+ // Convert PresetOption[] (with value functions) to DatePicker's PresetOption format
93
+ const datePickerPresets = useMemo(() => {
94
+ if (!showPresets) {
95
+ return undefined;
96
+ }
97
+
98
+ const presetsToUse = customPresets || getDefaultPresets();
99
+
100
+ return presetsToUse.map((preset) => ({
101
+ label: preset.label,
102
+ value: preset.value(), // Call the function to get the actual value
103
+ }));
104
+ }, [customPresets, showPresets]);
105
+
106
+ // Build props for DatePicker (only include defined values)
107
+ const datePickerProps: Record<string, unknown> = {
108
+ mode: "range",
109
+ placeholder,
110
+ ...(value !== undefined && { value }),
111
+ ...(defaultValue !== undefined && { defaultValue }),
112
+ ...(onChange !== undefined && { onChange }),
113
+ ...(minDate && { minDate }),
114
+ ...(maxDate && { maxDate }),
115
+ ...(disabled && { disabled }),
116
+ ...(datePickerPresets && { presets: datePickerPresets }),
117
+ ...(weekStartsOn !== undefined && { weekStartsOn }),
118
+ ...(name && { name }),
119
+ ...(isDisabled && { isDisabled }),
120
+ ...(isInvalid && { isInvalid }),
121
+ ...(className && { className }),
122
+ ...(align && { align }),
123
+ ...(clearable !== undefined && { clearable }),
124
+ };
125
+
126
+ return <DatePicker {...(datePickerProps as DatePickerRangeProps)} />;
127
+ };