@sixthshift/design-system 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/styles.css +2 -0
- package/dist/theme.generated.css +438 -0
- package/package.json +238 -0
- package/src/components/Avatar/Avatar.tsx +19 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BarChart/BarChart.tsx +51 -0
- package/src/components/BarChart/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
- package/src/components/Breadcrumb/index.ts +5 -0
- package/src/components/Button/Button.tsx +157 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/Calendar.tsx +433 -0
- package/src/components/Calendar/calendar.hooks.ts +329 -0
- package/src/components/Calendar/calendar.types.ts +117 -0
- package/src/components/Calendar/index.ts +28 -0
- package/src/components/Card/Card.tsx +47 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +144 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
- package/src/components/CheckboxGroup/index.ts +5 -0
- package/src/components/Code/Editor/Editor.tsx +161 -0
- package/src/components/Code/Editor/index.ts +2 -0
- package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
- package/src/components/Code/Editor/themes.ts +112 -0
- package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
- package/src/components/Code/ValidationStatus/index.ts +1 -0
- package/src/components/Code/Workspace/StatusBar.tsx +47 -0
- package/src/components/Code/Workspace/Toolbar.tsx +47 -0
- package/src/components/Code/Workspace/Workspace.tsx +109 -0
- package/src/components/Code/Workspace/index.ts +3 -0
- package/src/components/ColorDot/ColorDot.tsx +50 -0
- package/src/components/ColorDot/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.tsx +236 -0
- package/src/components/DatePicker/datepicker.hooks.ts +381 -0
- package/src/components/DatePicker/datepicker.types.ts +140 -0
- package/src/components/DatePicker/index.ts +12 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
- package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
- package/src/components/DateRangePicker/index.ts +2 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
- package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
- package/src/components/DateTimePicker/index.ts +2 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
- package/src/components/DateTimeRangePicker/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +29 -0
- package/src/components/EmptyState/index.ts +1 -0
- package/src/components/Field/Field.tsx +60 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +55 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
- package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
- package/src/components/HeatMap/heat-map.utils.ts +15 -0
- package/src/components/HeatMap/index.ts +3 -0
- package/src/components/HoverCard/HoverCard.tsx +83 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
- package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
- package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
- package/src/components/HoverCard/index.ts +3 -0
- package/src/components/Input/Input.tsx +48 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/LineChart/LineChart.tsx +251 -0
- package/src/components/LineChart/index.ts +1 -0
- package/src/components/Markdown/Markdown.tsx +40 -0
- package/src/components/Markdown/index.ts +1 -0
- package/src/components/Message/Message.tsx +59 -0
- package/src/components/Message/components/MessageBody.tsx +9 -0
- package/src/components/Message/components/MessageDescription.tsx +9 -0
- package/src/components/Message/components/MessageIcon.tsx +13 -0
- package/src/components/Message/components/MessageTitle.tsx +9 -0
- package/src/components/Message/index.ts +10 -0
- package/src/components/MetricList/MetricList.tsx +13 -0
- package/src/components/MetricList/MetricRow.tsx +30 -0
- package/src/components/MetricList/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +146 -0
- package/src/components/Modal/components/ModalBody.tsx +9 -0
- package/src/components/Modal/components/ModalContext.tsx +27 -0
- package/src/components/Modal/components/ModalFooter.tsx +9 -0
- package/src/components/Modal/components/ModalHeader.tsx +29 -0
- package/src/components/Modal/components/index.ts +9 -0
- package/src/components/Modal/index.ts +13 -0
- package/src/components/NavBottom/NavBottom.tsx +53 -0
- package/src/components/NavBottom/index.ts +3 -0
- package/src/components/NavSide/NavItem.tsx +39 -0
- package/src/components/NavSide/NavSide.tsx +40 -0
- package/src/components/NavSide/index.ts +2 -0
- package/src/components/NavSide/types.ts +36 -0
- package/src/components/Pagination/Pagination.tsx +86 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Popover/Popover.tsx +68 -0
- package/src/components/Popover/components/PopoverBody.tsx +32 -0
- package/src/components/Popover/components/PopoverClose.tsx +26 -0
- package/src/components/Popover/components/PopoverContext.tsx +24 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
- package/src/components/Popover/index.ts +10 -0
- package/src/components/ProgressBar/ProgressBar.tsx +42 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.tsx +89 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
- package/src/components/RadioButtonGroup/index.ts +5 -0
- package/src/components/SearchInput/SearchInput.tsx +45 -0
- package/src/components/SearchInput/index.ts +2 -0
- package/src/components/Select/Select.tsx +289 -0
- package/src/components/Select/SelectDropdown.tsx +90 -0
- package/src/components/Select/SelectTrigger.tsx +130 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/useSelectKeyboard.ts +111 -0
- package/src/components/Separator/Separator.tsx +20 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +136 -0
- package/src/components/Sheet/components/SheetBody.tsx +9 -0
- package/src/components/Sheet/components/SheetContext.tsx +23 -0
- package/src/components/Sheet/components/SheetFooter.tsx +9 -0
- package/src/components/Sheet/components/SheetHeader.tsx +29 -0
- package/src/components/Sheet/components/index.ts +9 -0
- package/src/components/Sheet/index.ts +13 -0
- package/src/components/Skeleton/Skeleton.tsx +7 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Sparkline/Sparkline.tsx +93 -0
- package/src/components/Sparkline/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +41 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/StatsCard/StatsCard.tsx +40 -0
- package/src/components/StatsCard/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +128 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +49 -0
- package/src/components/Tabs/components/TabsContext.tsx +35 -0
- package/src/components/Tabs/components/TabsList.tsx +98 -0
- package/src/components/Tabs/components/TabsPanels.tsx +26 -0
- package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/TagChip/TagChip.tsx +46 -0
- package/src/components/TagChip/index.ts +1 -0
- package/src/components/TagInput/TagInput.tsx +60 -0
- package/src/components/TagInput/index.ts +1 -0
- package/src/components/Text/Text.tsx +35 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextInline/TextInline.tsx +35 -0
- package/src/components/TextInline/index.ts +1 -0
- package/src/components/Textarea/Textarea.tsx +20 -0
- package/src/components/Textarea/index.ts +1 -0
- package/src/components/TimePicker/PeriodSelector.tsx +28 -0
- package/src/components/TimePicker/TimeColumn.tsx +62 -0
- package/src/components/TimePicker/TimePicker.tsx +323 -0
- package/src/components/TimePicker/index.ts +7 -0
- package/src/components/TimePicker/timepicker.hooks.ts +173 -0
- package/src/components/TimePicker/timepicker.types.ts +101 -0
- package/src/components/Toast/Toast.tsx +111 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Toggle/Toggle.tsx +72 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
- package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
- package/src/components/Tooltip/Tooltip.tsx +66 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
- package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
- package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/date-time/arithmetic.ts +101 -0
- package/src/date-time/boundaries.ts +91 -0
- package/src/date-time/compare.ts +85 -0
- package/src/date-time/duration.ts +63 -0
- package/src/date-time/elapsed.ts +18 -0
- package/src/date-time/format.ts +722 -0
- package/src/date-time/index.ts +279 -0
- package/src/date-time/now.ts +70 -0
- package/src/date-time/parse.ts +170 -0
- package/src/date-time/serialize.ts +90 -0
- package/src/date-time/timezone.ts +47 -0
- package/src/date-time/validate.ts +278 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCollapsible.ts +16 -0
- package/src/hooks/useControllableState.ts +54 -0
- package/src/hooks/useDebouncedCallback.ts +21 -0
- package/src/hooks/useDebouncedState.ts +20 -0
- package/src/hooks/useDeviceFormFactor.ts +49 -0
- package/src/hooks/useLocalStorage.ts +67 -0
- package/src/hooks/useMergedFloatingRef.ts +29 -0
- package/src/hooks/usePresence.ts +127 -0
- package/src/hooks/useStack.ts +99 -0
- package/src/hooks/useTheme.ts +105 -0
- package/src/internal/Slot.tsx +79 -0
- package/src/internal/index.ts +2 -0
- package/src/internal/rovingFocus.ts +39 -0
- package/src/internal/types.ts +10 -0
- package/src/lib/EmptyBoundary.tsx +12 -0
- package/src/lib/ErrorBoundary.tsx +48 -0
- package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
- package/src/lib/contexts/ComponentsContext/index.ts +1 -0
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
- package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
- package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
- package/src/lib/contexts/OverlayContext/index.ts +6 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/utils.ts +10 -0
- package/src/lib/withEmpty.tsx +14 -0
- package/src/lib/withErrorBoundary.tsx +16 -0
- package/src/lib/withSuspense.tsx +16 -0
- package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
- package/src/styles/base.css +41 -0
- package/src/theme/index.ts +37 -0
- package/src/theme/palette.json +100 -0
- package/src/theme/palette.schema.json +40 -0
- package/src/theme/schema.ts +122 -0
- package/src/theme/theme.json +349 -0
- package/src/theme/theme.schema.json +876 -0
- package/src/theme/typography.json +7 -0
- package/src/theme/typography.schema.json +36 -0
- package/src/typography/Body.tsx +8 -0
- package/src/typography/Caption.tsx +8 -0
- package/src/typography/Code.tsx +8 -0
- package/src/typography/Display.tsx +8 -0
- package/src/typography/Emphasis.tsx +8 -0
- package/src/typography/Heading.tsx +23 -0
- package/src/typography/Label.tsx +10 -0
- package/src/typography/Lead.tsx +8 -0
- package/src/typography/Mono.tsx +8 -0
- package/src/typography/Muted.tsx +8 -0
- package/src/typography/SectionTitle.tsx +8 -0
- package/src/typography/Subtitle.tsx +8 -0
- package/src/typography/Timestamp.tsx +8 -0
- package/src/typography/index.ts +10 -0
- package/tailwind.config.ts +261 -0
|
@@ -0,0 +1,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";
|