@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,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
|
+
};
|