@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,329 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { addDays, formatDateMediumYear, isPlainDate, isSameDay, isSameMonth, startOfMonth, startOfWeek, Temporal, today } from "../../date-time";
3
+ import type { DateRangeValue, DateSelectionState, DisabledDateMatcher, SelectionMode } from "./calendar.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: SelectionMode,
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: SelectionMode, value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined) {
221
+ return useCallback((date: Temporal.PlainDate) => getDateSelectionState(date, mode, value), [mode, value]);
222
+ }
223
+
224
+ // =============================================================================
225
+ // Display Formatting
226
+ // =============================================================================
227
+
228
+ /**
229
+ * Default display format for a date (e.g., "Jan 15, 2025")
230
+ */
231
+ export function defaultFormatDisplay(date: Temporal.PlainDate): string {
232
+ return formatDateMediumYear(date);
233
+ }
234
+
235
+ /**
236
+ * Format a range for display
237
+ */
238
+ export function formatRangeDisplay(range: DateRangeValue | undefined, formatFn: (date: Temporal.PlainDate) => string = defaultFormatDisplay): string {
239
+ if (!range?.from && !range?.to) {
240
+ return "";
241
+ }
242
+
243
+ if (range.from && !range.to) {
244
+ return `${formatFn(range.from)} – ...`;
245
+ }
246
+
247
+ if (!range.from && range.to) {
248
+ return `... – ${formatFn(range.to)}`;
249
+ }
250
+
251
+ return `${formatFn(range.from!)} – ${formatFn(range.to!)}`;
252
+ }
253
+
254
+ /**
255
+ * Format multiple dates for display
256
+ */
257
+ export function formatMultipleDisplay(dates: Temporal.PlainDate[] | undefined, formatFn: (date: Temporal.PlainDate) => string = defaultFormatDisplay): string {
258
+ if (!dates || dates.length === 0) {
259
+ return "";
260
+ }
261
+
262
+ if (dates.length === 1) {
263
+ return formatFn(dates[0]!);
264
+ }
265
+
266
+ if (dates.length <= 3) {
267
+ return dates.map(formatFn).join(", ");
268
+ }
269
+
270
+ return `${dates.length} dates selected`;
271
+ }
272
+
273
+ /**
274
+ * Get display value based on mode
275
+ */
276
+ export function getDisplayValue(
277
+ mode: SelectionMode,
278
+ value: Temporal.PlainDate | DateRangeValue | Temporal.PlainDate[] | undefined,
279
+ formatFn: (date: Temporal.PlainDate) => string = defaultFormatDisplay
280
+ ): string {
281
+ if (mode === "single") {
282
+ const singleValue = value as Temporal.PlainDate | undefined;
283
+ return singleValue ? formatFn(singleValue) : "";
284
+ }
285
+
286
+ if (mode === "range") {
287
+ return formatRangeDisplay(value as DateRangeValue | undefined, formatFn);
288
+ }
289
+
290
+ return formatMultipleDisplay(value as Temporal.PlainDate[] | undefined, formatFn);
291
+ }
292
+
293
+ // =============================================================================
294
+ // Month Grid Generation
295
+ // =============================================================================
296
+
297
+ export interface CalendarDay {
298
+ /** The date this day represents */
299
+ date: Temporal.PlainDate;
300
+ /** Whether this date is in the displayed month */
301
+ isCurrentMonth: boolean;
302
+ /** Whether this is today's date */
303
+ isToday: boolean;
304
+ }
305
+
306
+ /**
307
+ * Generate all days to display for a given month.
308
+ * Returns 42 days (6 weeks) to ensure consistent grid layout.
309
+ */
310
+ export function useCalendarDays(month: Temporal.PlainDate, weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0): CalendarDay[] {
311
+ return useMemo(() => {
312
+ const todayDate = today();
313
+ const firstDayOfMonth = startOfMonth(month);
314
+ const startDate = startOfWeek(firstDayOfMonth, weekStartsOn);
315
+
316
+ const days: CalendarDay[] = [];
317
+
318
+ for (let i = 0; i < 42; i++) {
319
+ const date = addDays(startDate, i);
320
+ days.push({
321
+ date,
322
+ isCurrentMonth: isSameMonth(date, month),
323
+ isToday: isSameDay(date, todayDate),
324
+ });
325
+ }
326
+
327
+ return days;
328
+ }, [month, weekStartsOn]);
329
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Calendar types for the generic date picker calendar component
3
+ *
4
+ * This component is used by DatePicker, DateTimePicker, and DateTimeRangePicker
5
+ * for consistent date selection UI.
6
+ */
7
+
8
+ import type { Temporal, WeekStartsOn } from "../../date-time";
9
+
10
+ /**
11
+ * Date range value
12
+ */
13
+ export type DateRangeValue = {
14
+ from: Temporal.PlainDate | undefined;
15
+ to: Temporal.PlainDate | undefined;
16
+ };
17
+
18
+ /**
19
+ * Selection mode
20
+ */
21
+ export type SelectionMode = "single" | "range" | "multiple";
22
+
23
+ /**
24
+ * Disabled date matchers
25
+ */
26
+ export type DisabledDateMatcher =
27
+ | Temporal.PlainDate
28
+ | Temporal.PlainDate[]
29
+ | ((date: Temporal.PlainDate) => boolean)
30
+ | { before: Temporal.PlainDate }
31
+ | { after: Temporal.PlainDate }
32
+ | { from: Temporal.PlainDate; to: Temporal.PlainDate };
33
+
34
+ /**
35
+ * Preset option (generic over value type)
36
+ */
37
+ export type PresetOption<T> = {
38
+ label: string;
39
+ value: T;
40
+ };
41
+
42
+ // ============================================================================
43
+ // Discriminated Union Props
44
+ // ============================================================================
45
+
46
+ type CalendarBaseProps = {
47
+ /** Current month being displayed */
48
+ month: Temporal.PlainDate;
49
+ /** Called when month should change */
50
+ onMonthChange: (month: Temporal.PlainDate) => void;
51
+
52
+ /** Minimum selectable date */
53
+ minDate?: Temporal.PlainDate | undefined;
54
+ /** Maximum selectable date */
55
+ maxDate?: Temporal.PlainDate | undefined;
56
+ /** Disabled date matchers */
57
+ disabled?: DisabledDateMatcher | DisabledDateMatcher[] | undefined;
58
+
59
+ /** First day of week (0 = Sunday, 1 = Monday) */
60
+ weekStartsOn?: WeekStartsOn | undefined;
61
+
62
+ /** Show footer with Today/Cancel/Apply buttons */
63
+ showFooter?: boolean | undefined;
64
+ /** Show Today button (only if showFooter=true) */
65
+ showToday?: boolean | undefined;
66
+ /** Called when Apply is clicked */
67
+ onApply?: (() => void) | undefined;
68
+ /** Called when Cancel is clicked */
69
+ onCancel?: (() => void) | undefined;
70
+
71
+ /** Additional CSS class */
72
+ className?: string | undefined;
73
+ };
74
+
75
+ export type CalendarSingleProps = CalendarBaseProps & {
76
+ mode: "single";
77
+ value: Temporal.PlainDate | undefined;
78
+ onSelect: (date: Temporal.PlainDate | undefined) => void;
79
+ presets?: PresetOption<Temporal.PlainDate>[] | undefined;
80
+ };
81
+
82
+ export type CalendarRangeProps = CalendarBaseProps & {
83
+ mode: "range";
84
+ value: DateRangeValue | undefined;
85
+ onSelect: (range: DateRangeValue | undefined) => void;
86
+ presets?: PresetOption<DateRangeValue>[] | undefined;
87
+ };
88
+
89
+ export type CalendarMultipleProps = CalendarBaseProps & {
90
+ mode: "multiple";
91
+ value: Temporal.PlainDate[];
92
+ onSelect: (dates: Temporal.PlainDate[]) => void;
93
+ max?: number | undefined;
94
+ presets?: PresetOption<Temporal.PlainDate[]>[] | undefined;
95
+ };
96
+
97
+ /**
98
+ * Calendar props - discriminated union based on mode
99
+ */
100
+ export type CalendarProps = CalendarSingleProps | CalendarRangeProps | CalendarMultipleProps;
101
+
102
+ // ============================================================================
103
+ // Internal Types
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Position of a date within a range selection
108
+ */
109
+ export type RangePosition = "start" | "middle" | "end" | "single" | "none";
110
+
111
+ /**
112
+ * Selection state for a date cell
113
+ */
114
+ export type DateSelectionState = {
115
+ isSelected: boolean;
116
+ rangePosition: RangePosition;
117
+ };
@@ -0,0 +1,28 @@
1
+ export { Calendar } from "./Calendar";
2
+ export {
3
+ defaultFormatDisplay,
4
+ formatMultipleDisplay,
5
+ formatRangeDisplay,
6
+ getDateSelectionState,
7
+ getDisplayValue,
8
+ isAfterTemporal,
9
+ isBeforeTemporal,
10
+ isBetweenTemporal,
11
+ isDateDisabled,
12
+ isSameDateTemporal,
13
+ temporalToISO,
14
+ useIsDateDisabled,
15
+ useSelectionState,
16
+ } from "./calendar.hooks";
17
+ export type {
18
+ CalendarMultipleProps,
19
+ CalendarProps,
20
+ CalendarRangeProps,
21
+ CalendarSingleProps,
22
+ DateRangeValue,
23
+ DateSelectionState,
24
+ DisabledDateMatcher,
25
+ PresetOption,
26
+ RangePosition,
27
+ SelectionMode,
28
+ } from "./calendar.types";
@@ -0,0 +1,47 @@
1
+ import { cn } from "@sixthshift/design-system/utils";
2
+ import * as React from "react";
3
+
4
+ export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
5
+ /** Optional header title - string or ReactNode */
6
+ title?: React.ReactNode;
7
+ /** Optional action element displayed on the right side of the header */
8
+ headerAction?: React.ReactNode;
9
+ };
10
+
11
+ export const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, title, headerAction, children, onClick, onKeyDown, ...props }, ref) => {
12
+ const isInteractive = !!onClick;
13
+
14
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
15
+ if (isInteractive && (e.key === "Enter" || e.key === " ")) {
16
+ e.preventDefault();
17
+ onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);
18
+ }
19
+ onKeyDown?.(e);
20
+ };
21
+
22
+ return (
23
+ // biome-ignore lint/a11y/noStaticElementInteractions: role="button" + tabIndex are added dynamically when onClick is present
24
+ <div
25
+ ref={ref}
26
+ className={cn(
27
+ "rounded-xl border border-border-normal bg-bg-normal p-6 text-fg-normal shadow",
28
+ isInteractive &&
29
+ "cursor-pointer transition-colors hover:border-border-strong focus-visible:outline-2 focus-visible:outline-border-strong focus-visible:outline-offset-2",
30
+ className
31
+ )}
32
+ onClick={onClick}
33
+ onKeyDown={handleKeyDown}
34
+ {...(isInteractive && { role: "button", tabIndex: 0 })}
35
+ {...props}
36
+ >
37
+ {(title || headerAction) && (
38
+ <header className="mb-4 flex items-start justify-between gap-4">
39
+ {title && <div className="font-semibold">{title}</div>}
40
+ {headerAction && <div className="shrink-0">{headerAction}</div>}
41
+ </header>
42
+ )}
43
+ {children}
44
+ </div>
45
+ );
46
+ });
47
+ Card.displayName = "Card";
@@ -0,0 +1 @@
1
+ export { Card, type CardProps } from "./Card";
@@ -0,0 +1,144 @@
1
+ import { useControllableState } from "@sixthshift/design-system/hooks";
2
+ import { Label } from "@sixthshift/design-system/label";
3
+ import { cn } from "@sixthshift/design-system/utils";
4
+ import * as React from "react";
5
+
6
+ export type CheckboxProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "value"> & {
7
+ /** Controlled checked state */
8
+ checked?: boolean | "indeterminate";
9
+ /** Default checked state for uncontrolled mode */
10
+ defaultChecked?: boolean | "indeterminate";
11
+ /** Called when checked state changes */
12
+ onCheckedChange?: (checked: boolean) => void;
13
+ /** Input name for form submission */
14
+ name?: string;
15
+ /** Value submitted with form (defaults to "on") */
16
+ value?: string;
17
+ /** Label rendered next to the checkbox. Wraps in a <label> for a11y. */
18
+ label?: React.ReactNode;
19
+ /** Classes applied to the wrapping label element when `label` is provided */
20
+ labelClassName?: string;
21
+ };
22
+
23
+ const CheckIcon = () => (
24
+ <svg
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ strokeWidth="3"
30
+ strokeLinecap="round"
31
+ strokeLinejoin="round"
32
+ className="h-3 w-3"
33
+ aria-hidden="true"
34
+ >
35
+ <polyline points="20 6 9 17 4 12" />
36
+ </svg>
37
+ );
38
+
39
+ const IndeterminateIcon = () => (
40
+ <svg
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ strokeWidth="3"
46
+ strokeLinecap="round"
47
+ className="h-3 w-3"
48
+ aria-hidden="true"
49
+ >
50
+ <line x1="5" y1="12" x2="19" y2="12" />
51
+ </svg>
52
+ );
53
+
54
+ const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
55
+ (
56
+ {
57
+ className,
58
+ checked: controlledChecked,
59
+ defaultChecked = false,
60
+ onCheckedChange,
61
+ disabled,
62
+ name,
63
+ value = "on",
64
+ label,
65
+ labelClassName,
66
+ id: idProp,
67
+ ...props
68
+ },
69
+ ref
70
+ ) => {
71
+ const generatedId = React.useId();
72
+ const id = idProp ?? (label ? generatedId : undefined);
73
+
74
+ const [checked, setChecked] = useControllableState({
75
+ value: controlledChecked,
76
+ defaultValue: defaultChecked,
77
+ onChange: onCheckedChange as ((value: boolean | "indeterminate") => void) | undefined,
78
+ });
79
+
80
+ const isChecked = checked === true;
81
+ const isIndeterminate = checked === "indeterminate";
82
+
83
+ const button = (
84
+ <>
85
+ {/* biome-ignore lint/a11y/useSemanticElements: Custom checkbox implementation with button role="checkbox" is WAI-ARIA best practice for styled checkboxes */}
86
+ <button
87
+ type="button"
88
+ role="checkbox"
89
+ id={id}
90
+ aria-checked={isIndeterminate ? "mixed" : isChecked}
91
+ data-state={isIndeterminate ? "indeterminate" : isChecked ? "checked" : "unchecked"}
92
+ disabled={disabled}
93
+ ref={ref}
94
+ onClick={() => setChecked(!isChecked)}
95
+ className={cn(
96
+ "peer h-4 w-4 shrink-0 cursor-pointer rounded-xs border border-border-brand shadow focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
97
+ (isChecked || isIndeterminate) && "bg-bg-brand text-fg-on-brand",
98
+ className
99
+ )}
100
+ {...props}
101
+ >
102
+ <span className="flex items-center justify-center text-current">
103
+ {isChecked && <CheckIcon />}
104
+ {isIndeterminate && <IndeterminateIcon />}
105
+ </span>
106
+ </button>
107
+ {name && (
108
+ <input
109
+ type="checkbox"
110
+ aria-hidden
111
+ tabIndex={-1}
112
+ name={name}
113
+ value={value}
114
+ checked={isChecked}
115
+ disabled={disabled}
116
+ readOnly
117
+ style={{
118
+ position: "absolute",
119
+ pointerEvents: "none",
120
+ opacity: 0,
121
+ margin: 0,
122
+ width: 0,
123
+ height: 0,
124
+ }}
125
+ />
126
+ )}
127
+ </>
128
+ );
129
+
130
+ if (!label) return button;
131
+
132
+ return (
133
+ <div className={cn("flex items-center gap-2", labelClassName)}>
134
+ {button}
135
+ <Label htmlFor={id} className={cn(disabled && "cursor-not-allowed opacity-50")}>
136
+ {label}
137
+ </Label>
138
+ </div>
139
+ );
140
+ }
141
+ );
142
+ Checkbox.displayName = "Checkbox";
143
+
144
+ export { Checkbox };
@@ -0,0 +1 @@
1
+ export { Checkbox, type CheckboxProps } from "./Checkbox";