@cdx-ui/components 0.2.5 → 0.3.1
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/coverage.svg +1 -1
- package/docs/components/bottom-sheet/content.md +2 -0
- package/docs/components/calendar/content.md +25 -1
- package/docs/components/carousel/content.md +1 -1
- package/docs/components/data-grid/content.md +50 -8
- package/docs/components/icon-button/content.md +23 -1
- package/docs/components/month-calendar/content.md +2 -0
- package/docs/components/range-calendar/content.md +3 -1
- package/docs/components/toast/content.md +23 -7
- package/docs/llms.txt +2 -0
- package/docs/resources/release-notes/0.2.5/content.md +25 -0
- package/docs/resources/release-notes/0.3.0/content.md +19 -0
- package/docs/resources/release-notes/content.md +2 -0
- package/lib/commonjs/components/BottomSheet/index.js +25 -1
- package/lib/commonjs/components/BottomSheet/index.js.map +1 -1
- package/lib/commonjs/components/BottomSheet/styles.js +8 -2
- package/lib/commonjs/components/BottomSheet/styles.js.map +1 -1
- package/lib/commonjs/components/Calendar/CalendarMonthHeader.js +15 -5
- package/lib/commonjs/components/Calendar/CalendarMonthHeader.js.map +1 -1
- package/lib/commonjs/components/Calendar/CalendarYearList.js +116 -0
- package/lib/commonjs/components/Calendar/CalendarYearList.js.map +1 -0
- package/lib/commonjs/components/Calendar/CalendarYearPickerGrid.js +58 -31
- package/lib/commonjs/components/Calendar/CalendarYearPickerGrid.js.map +1 -1
- package/lib/commonjs/components/Calendar/flashCalendarAdapter.js +22 -3
- package/lib/commonjs/components/Calendar/flashCalendarAdapter.js.map +1 -1
- package/lib/commonjs/components/Calendar/index.js +6 -2
- package/lib/commonjs/components/Calendar/index.js.map +1 -1
- package/lib/commonjs/components/Calendar/styles.js +16 -2
- package/lib/commonjs/components/Calendar/styles.js.map +1 -1
- package/lib/commonjs/components/IconButton/styles.js +107 -2
- package/lib/commonjs/components/IconButton/styles.js.map +1 -1
- package/lib/commonjs/components/MonthCalendar/index.js +39 -13
- package/lib/commonjs/components/MonthCalendar/index.js.map +1 -1
- package/lib/commonjs/components/RangeCalendar/index.js +6 -2
- package/lib/commonjs/components/RangeCalendar/index.js.map +1 -1
- package/lib/commonjs/components/Toast/index.js +5 -2
- package/lib/commonjs/components/Toast/index.js.map +1 -1
- package/lib/module/components/BottomSheet/index.js +27 -3
- package/lib/module/components/BottomSheet/index.js.map +1 -1
- package/lib/module/components/BottomSheet/styles.js +8 -2
- package/lib/module/components/BottomSheet/styles.js.map +1 -1
- package/lib/module/components/Calendar/CalendarMonthHeader.js +15 -5
- package/lib/module/components/Calendar/CalendarMonthHeader.js.map +1 -1
- package/lib/module/components/Calendar/CalendarYearList.js +112 -0
- package/lib/module/components/Calendar/CalendarYearList.js.map +1 -0
- package/lib/module/components/Calendar/CalendarYearPickerGrid.js +61 -34
- package/lib/module/components/Calendar/CalendarYearPickerGrid.js.map +1 -1
- package/lib/module/components/Calendar/flashCalendarAdapter.js +20 -2
- package/lib/module/components/Calendar/flashCalendarAdapter.js.map +1 -1
- package/lib/module/components/Calendar/index.js +7 -3
- package/lib/module/components/Calendar/index.js.map +1 -1
- package/lib/module/components/Calendar/styles.js +15 -1
- package/lib/module/components/Calendar/styles.js.map +1 -1
- package/lib/module/components/IconButton/styles.js +107 -2
- package/lib/module/components/IconButton/styles.js.map +1 -1
- package/lib/module/components/MonthCalendar/index.js +42 -15
- package/lib/module/components/MonthCalendar/index.js.map +1 -1
- package/lib/module/components/RangeCalendar/index.js +7 -3
- package/lib/module/components/RangeCalendar/index.js.map +1 -1
- package/lib/module/components/Toast/index.js +5 -2
- package/lib/module/components/Toast/index.js.map +1 -1
- package/lib/typescript/components/BottomSheet/index.d.ts +8 -0
- package/lib/typescript/components/BottomSheet/index.d.ts.map +1 -1
- package/lib/typescript/components/BottomSheet/styles.d.ts +5 -1
- package/lib/typescript/components/BottomSheet/styles.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/CalendarMonthHeader.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/CalendarYearList.d.ts +23 -0
- package/lib/typescript/components/Calendar/CalendarYearList.d.ts.map +1 -0
- package/lib/typescript/components/Calendar/CalendarYearPickerGrid.d.ts +5 -1
- package/lib/typescript/components/Calendar/CalendarYearPickerGrid.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/flashCalendarAdapter.d.ts +8 -0
- package/lib/typescript/components/Calendar/flashCalendarAdapter.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/index.d.ts.map +1 -1
- package/lib/typescript/components/Calendar/styles.d.ts +7 -1
- package/lib/typescript/components/Calendar/styles.d.ts.map +1 -1
- package/lib/typescript/components/IconButton/styles.d.ts +2 -2
- package/lib/typescript/components/IconButton/styles.d.ts.map +1 -1
- package/lib/typescript/components/MonthCalendar/index.d.ts.map +1 -1
- package/lib/typescript/components/RangeCalendar/index.d.ts.map +1 -1
- package/lib/typescript/components/Toast/index.d.ts.map +1 -1
- package/lib/typescript/components/ToggleButton/styles.d.ts +1 -1
- package/package.json +5 -5
- package/src/components/BottomSheet/index.tsx +174 -138
- package/src/components/BottomSheet/styles.ts +11 -3
- package/src/components/Calendar/CalendarMonthHeader.tsx +29 -22
- package/src/components/Calendar/CalendarYearList.tsx +136 -0
- package/src/components/Calendar/CalendarYearPickerGrid.tsx +60 -33
- package/src/components/Calendar/flashCalendarAdapter.ts +27 -2
- package/src/components/Calendar/index.tsx +8 -3
- package/src/components/Calendar/styles.ts +17 -1
- package/src/components/IconButton/styles.ts +111 -0
- package/src/components/MonthCalendar/index.tsx +55 -33
- package/src/components/RangeCalendar/index.tsx +8 -3
- package/src/components/Toast/index.tsx +13 -3
|
@@ -94,28 +94,35 @@ export const CalendarMonthHeader = forwardRef<ComponentRef<typeof View>, Calenda
|
|
|
94
94
|
/>
|
|
95
95
|
</Pressable>
|
|
96
96
|
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
97
|
+
{/* Stays mounted in the year view so the header keeps its height and the label does not move. */}
|
|
98
|
+
<View
|
|
99
|
+
className={calendarMonthHeaderNavVariants({ hidden: isYearView })}
|
|
100
|
+
pointerEvents={isYearView ? 'none' : 'auto'}
|
|
101
|
+
accessibilityElementsHidden={isYearView}
|
|
102
|
+
importantForAccessibility={isYearView ? 'no-hide-descendants' : 'auto'}
|
|
103
|
+
{...(isYearView ? { 'aria-hidden': true } : {})}
|
|
104
|
+
>
|
|
105
|
+
<IconButton
|
|
106
|
+
as={ChevronLeft}
|
|
107
|
+
variant="ghost"
|
|
108
|
+
color="neutral"
|
|
109
|
+
size="small"
|
|
110
|
+
accessibilityLabel={labels.previousMonth}
|
|
111
|
+
isDisabled={disabled || isYearView}
|
|
112
|
+
focusable={!isYearView}
|
|
113
|
+
onPress={() => shiftMonth(-1)}
|
|
114
|
+
/>
|
|
115
|
+
<IconButton
|
|
116
|
+
as={ChevronRight}
|
|
117
|
+
variant="ghost"
|
|
118
|
+
color="neutral"
|
|
119
|
+
size="small"
|
|
120
|
+
accessibilityLabel={labels.nextMonth}
|
|
121
|
+
isDisabled={disabled || isYearView}
|
|
122
|
+
focusable={!isYearView}
|
|
123
|
+
onPress={() => shiftMonth(1)}
|
|
124
|
+
/>
|
|
125
|
+
</View>
|
|
119
126
|
</View>
|
|
120
127
|
);
|
|
121
128
|
},
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Fragment, useCallback, useMemo, useRef, type ReactElement } from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
import { ScrollShadow } from '../ScrollShadow';
|
|
4
|
+
import {
|
|
5
|
+
calendarYearPickerListContentVariants,
|
|
6
|
+
calendarYearPickerPlaceholderVariants,
|
|
7
|
+
calendarYearPickerRowVariants,
|
|
8
|
+
} from './styles';
|
|
9
|
+
|
|
10
|
+
/** Years listed before / after today's year on a side with no `minValue` / `maxValue`. */
|
|
11
|
+
const DEFAULT_YEAR_WINDOW = 100;
|
|
12
|
+
const YEAR_COLUMNS = 3;
|
|
13
|
+
/** Pill cell (40px) plus its vertical margins and row gap; fixed so the initial offset is exact. */
|
|
14
|
+
export const YEAR_ROW_HEIGHT = 52;
|
|
15
|
+
|
|
16
|
+
const ROW_STYLE = { height: YEAR_ROW_HEIGHT };
|
|
17
|
+
|
|
18
|
+
export interface YearRange {
|
|
19
|
+
first: number;
|
|
20
|
+
last: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getYearRange(
|
|
24
|
+
minYear: number | undefined,
|
|
25
|
+
maxYear: number | undefined,
|
|
26
|
+
visibleYear: number,
|
|
27
|
+
todayYear: number,
|
|
28
|
+
): YearRange {
|
|
29
|
+
const first =
|
|
30
|
+
minYear ?? Math.min(todayYear - DEFAULT_YEAR_WINDOW, visibleYear, maxYear ?? Infinity);
|
|
31
|
+
const last = maxYear ?? Math.max(todayYear + DEFAULT_YEAR_WINDOW, visibleYear, first);
|
|
32
|
+
return { first, last: Math.max(first, last) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clamp(value: number, min: number, max: number): number {
|
|
36
|
+
return Math.min(Math.max(value, min), max);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CalendarYearListProps extends YearRange {
|
|
40
|
+
selectedYear: number;
|
|
41
|
+
/** Fixed list height, matched to the sibling view so switching views does not resize. */
|
|
42
|
+
height: number;
|
|
43
|
+
renderYear: (year: number) => ReactElement;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Three-up year list shared by the Calendar, RangeCalendar, and MonthCalendar year views.
|
|
48
|
+
*
|
|
49
|
+
* A plain `ScrollView` rather than a `FlatList`: the pickers render this inside their popover
|
|
50
|
+
* `ScrollView`, and a nested `VirtualizedList` breaks windowing there. The list is a few hundred
|
|
51
|
+
* light cells at most, so virtualization buys little.
|
|
52
|
+
*/
|
|
53
|
+
export function CalendarYearList({
|
|
54
|
+
first,
|
|
55
|
+
last,
|
|
56
|
+
selectedYear,
|
|
57
|
+
height,
|
|
58
|
+
renderYear,
|
|
59
|
+
}: CalendarYearListProps) {
|
|
60
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
61
|
+
const hasAppliedInitialOffset = useRef(false);
|
|
62
|
+
|
|
63
|
+
const rows = useMemo<(number | null)[][]>(() => {
|
|
64
|
+
const count = last - first + 1;
|
|
65
|
+
return Array.from({ length: Math.ceil(count / YEAR_COLUMNS) }, (_, row) =>
|
|
66
|
+
Array.from({ length: YEAR_COLUMNS }, (_, column) => {
|
|
67
|
+
const index = row * YEAR_COLUMNS + column;
|
|
68
|
+
return index < count ? first + index : null;
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}, [first, last]);
|
|
72
|
+
|
|
73
|
+
const visibleRows = height / YEAR_ROW_HEIGHT;
|
|
74
|
+
const selectedRow = Math.floor((clamp(selectedYear, first, last) - first) / YEAR_COLUMNS);
|
|
75
|
+
const firstVisibleRow = Math.max(0, selectedRow - Math.floor((visibleRows - 1) / 2));
|
|
76
|
+
const maxOffset = Math.max(0, rows.length * YEAR_ROW_HEIGHT - height);
|
|
77
|
+
const initialOffsetY = Math.min(firstVisibleRow * YEAR_ROW_HEIGHT, maxOffset);
|
|
78
|
+
|
|
79
|
+
// `contentOffset` is computed once so later re-renders (a new selection) never yank the
|
|
80
|
+
// scroll position back.
|
|
81
|
+
const initialOffset = useRef({ x: 0, y: initialOffsetY }).current;
|
|
82
|
+
const listStyle = useMemo(() => ({ height }), [height]);
|
|
83
|
+
|
|
84
|
+
// iOS and Android apply `contentOffset` before first paint; React Native Web ignores it, so
|
|
85
|
+
// the same offset is applied once content has a size.
|
|
86
|
+
const handleContentSizeChange = useCallback(() => {
|
|
87
|
+
if (hasAppliedInitialOffset.current) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
hasAppliedInitialOffset.current = true;
|
|
91
|
+
if (initialOffset.y > 0) {
|
|
92
|
+
scrollRef.current?.scrollTo({ x: 0, y: initialOffset.y, animated: false });
|
|
93
|
+
}
|
|
94
|
+
}, [initialOffset]);
|
|
95
|
+
|
|
96
|
+
const content = useMemo(
|
|
97
|
+
() =>
|
|
98
|
+
rows.map((cells, row) => (
|
|
99
|
+
<View
|
|
100
|
+
key={first + row * YEAR_COLUMNS}
|
|
101
|
+
className={calendarYearPickerRowVariants()}
|
|
102
|
+
style={ROW_STYLE}
|
|
103
|
+
>
|
|
104
|
+
{cells.map((year, column) =>
|
|
105
|
+
year === null ? (
|
|
106
|
+
<View
|
|
107
|
+
key={`placeholder-${String(column)}`}
|
|
108
|
+
className={calendarYearPickerPlaceholderVariants()}
|
|
109
|
+
accessibilityElementsHidden
|
|
110
|
+
importantForAccessibility="no-hide-descendants"
|
|
111
|
+
aria-hidden
|
|
112
|
+
/>
|
|
113
|
+
) : (
|
|
114
|
+
<Fragment key={year}>{renderYear(year)}</Fragment>
|
|
115
|
+
),
|
|
116
|
+
)}
|
|
117
|
+
</View>
|
|
118
|
+
)),
|
|
119
|
+
[first, rows, renderYear],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ScrollShadow surface="primary" size="small">
|
|
124
|
+
<ScrollView
|
|
125
|
+
ref={scrollRef}
|
|
126
|
+
style={listStyle}
|
|
127
|
+
contentContainerClassName={calendarYearPickerListContentVariants()}
|
|
128
|
+
contentOffset={initialOffset}
|
|
129
|
+
onContentSizeChange={handleContentSizeChange}
|
|
130
|
+
nestedScrollEnabled
|
|
131
|
+
>
|
|
132
|
+
{content}
|
|
133
|
+
</ScrollView>
|
|
134
|
+
</ScrollShadow>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
import { forwardRef, useCallback, useMemo, type ComponentRef } from 'react';
|
|
1
|
+
import { forwardRef, useCallback, useMemo, useState, type ComponentRef } from 'react';
|
|
2
2
|
import { Pressable, Text, View, type ViewProps } from 'react-native';
|
|
3
3
|
import {
|
|
4
4
|
formatMonthId,
|
|
5
5
|
formatSelectYear,
|
|
6
|
+
parseDateId,
|
|
6
7
|
parseMonthId,
|
|
7
8
|
resolveDateComponentLabels,
|
|
8
9
|
type DateComponentLabels,
|
|
10
|
+
type DateId,
|
|
9
11
|
type MonthId,
|
|
10
12
|
} from '@cdx-ui/formatters';
|
|
11
13
|
import { cn } from '@cdx-ui/utils';
|
|
14
|
+
import { CalendarYearList, getYearRange } from './CalendarYearList';
|
|
12
15
|
import type { CalendarNavigationView } from './CalendarMonthHeader';
|
|
16
|
+
import { getFlashCalendarGridHeight } from './flashCalendarAdapter';
|
|
13
17
|
import {
|
|
14
18
|
calendarPillCellTextVariants,
|
|
15
19
|
calendarPillCellVariants,
|
|
16
|
-
|
|
20
|
+
calendarYearPickerListVariants,
|
|
17
21
|
} from './styles';
|
|
18
22
|
|
|
19
|
-
const YEAR_RANGE_BEFORE = 9;
|
|
20
|
-
const YEAR_RANGE_AFTER = 11;
|
|
21
|
-
|
|
22
23
|
export interface CalendarYearPickerGridProps extends ViewProps {
|
|
23
24
|
monthId: MonthId;
|
|
24
25
|
onMonthIdChange: (monthId: MonthId) => void;
|
|
26
|
+
/** Earliest selectable date; its year is the first year listed. */
|
|
27
|
+
minValue?: DateId;
|
|
28
|
+
/** Latest selectable date; its year is the last year listed. */
|
|
29
|
+
maxValue?: DateId;
|
|
25
30
|
disabled?: boolean;
|
|
26
31
|
onViewChange: (view: CalendarNavigationView) => void;
|
|
27
32
|
labels?: Partial<DateComponentLabels>;
|
|
@@ -35,6 +40,8 @@ export const CalendarYearPickerGrid = forwardRef<
|
|
|
35
40
|
{
|
|
36
41
|
monthId,
|
|
37
42
|
onMonthIdChange,
|
|
43
|
+
minValue,
|
|
44
|
+
maxValue,
|
|
38
45
|
disabled = false,
|
|
39
46
|
onViewChange,
|
|
40
47
|
labels: labelsProp,
|
|
@@ -45,54 +52,74 @@ export const CalendarYearPickerGrid = forwardRef<
|
|
|
45
52
|
ref,
|
|
46
53
|
) => {
|
|
47
54
|
const labels = useMemo(() => resolveDateComponentLabels(labelsProp), [labelsProp]);
|
|
55
|
+
const [todayYear] = useState(() => new Date().getFullYear());
|
|
48
56
|
const parts = parseMonthId(monthId);
|
|
49
|
-
const currentYear = parts?.year ??
|
|
57
|
+
const currentYear = parts?.year ?? todayYear;
|
|
50
58
|
const currentMonth = parts?.month ?? 1;
|
|
59
|
+
const minParts = minValue ? parseDateId(minValue) : null;
|
|
60
|
+
const maxParts = maxValue ? parseDateId(maxValue) : null;
|
|
61
|
+
const minYear = minParts?.year;
|
|
62
|
+
const minMonth = minParts?.month;
|
|
63
|
+
const maxYear = maxParts?.year;
|
|
64
|
+
const maxMonth = maxParts?.month;
|
|
51
65
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
(_, i) => currentYear - YEAR_RANGE_BEFORE + i,
|
|
57
|
-
),
|
|
58
|
-
[currentYear],
|
|
59
|
-
);
|
|
66
|
+
const { first, last } = getYearRange(minYear, maxYear, currentYear, todayYear);
|
|
67
|
+
|
|
68
|
+
// Matches the day grid so switching views does not change the Calendar's height.
|
|
69
|
+
const height = getFlashCalendarGridHeight(monthId);
|
|
60
70
|
|
|
61
71
|
const selectYear = useCallback(
|
|
62
72
|
(year: number) => {
|
|
63
73
|
if (disabled) {
|
|
64
74
|
return;
|
|
65
75
|
}
|
|
66
|
-
|
|
76
|
+
let month = currentMonth;
|
|
77
|
+
if (minYear === year && minMonth !== undefined) {
|
|
78
|
+
month = Math.max(month, minMonth);
|
|
79
|
+
}
|
|
80
|
+
if (maxYear === year && maxMonth !== undefined) {
|
|
81
|
+
month = Math.min(month, maxMonth);
|
|
82
|
+
}
|
|
83
|
+
onMonthIdChange(formatMonthId({ year, month }));
|
|
67
84
|
onViewChange('month');
|
|
68
85
|
},
|
|
69
|
-
[currentMonth, disabled, onMonthIdChange, onViewChange],
|
|
86
|
+
[currentMonth, disabled, maxMonth, maxYear, minMonth, minYear, onMonthIdChange, onViewChange],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const renderYear = useCallback(
|
|
90
|
+
(year: number) => {
|
|
91
|
+
const selected = year === currentYear;
|
|
92
|
+
return (
|
|
93
|
+
<Pressable
|
|
94
|
+
accessibilityRole="button"
|
|
95
|
+
accessibilityState={{ selected, disabled }}
|
|
96
|
+
accessibilityLabel={formatSelectYear(labels, year)}
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
onPress={() => selectYear(year)}
|
|
99
|
+
className={calendarPillCellVariants({ selected, disabled })}
|
|
100
|
+
>
|
|
101
|
+
<Text className={calendarPillCellTextVariants({ selected })}>{String(year)}</Text>
|
|
102
|
+
</Pressable>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
[currentYear, disabled, labels, selectYear],
|
|
70
106
|
);
|
|
71
107
|
|
|
72
108
|
return (
|
|
73
109
|
<View
|
|
74
110
|
ref={ref}
|
|
75
|
-
className={cn(
|
|
111
|
+
className={cn(calendarYearPickerListVariants(), className)}
|
|
76
112
|
style={style}
|
|
77
113
|
accessibilityRole="none"
|
|
78
114
|
{...props}
|
|
79
115
|
>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
accessibilityLabel={formatSelectYear(labels, year)}
|
|
88
|
-
disabled={disabled}
|
|
89
|
-
onPress={() => selectYear(year)}
|
|
90
|
-
className={calendarPillCellVariants({ selected, disabled })}
|
|
91
|
-
>
|
|
92
|
-
<Text className={calendarPillCellTextVariants({ selected })}>{String(year)}</Text>
|
|
93
|
-
</Pressable>
|
|
94
|
-
);
|
|
95
|
-
})}
|
|
116
|
+
<CalendarYearList
|
|
117
|
+
first={first}
|
|
118
|
+
last={last}
|
|
119
|
+
selectedYear={currentYear}
|
|
120
|
+
height={height}
|
|
121
|
+
renderYear={renderYear}
|
|
122
|
+
/>
|
|
96
123
|
</View>
|
|
97
124
|
);
|
|
98
125
|
},
|
|
@@ -6,6 +6,9 @@ import { formatDateId, parseMonthId, type DateId, type MonthId } from '@cdx-ui/f
|
|
|
6
6
|
const DAY_SIZE = 36;
|
|
7
7
|
const DAY_RADIUS = DAY_SIZE / 2;
|
|
8
8
|
|
|
9
|
+
/** Bottom padding the theme adds under the weekday header row (`rowWeek.container`). */
|
|
10
|
+
const WEEK_HEADER_PADDING_BOTTOM = 4;
|
|
11
|
+
|
|
9
12
|
const CALENDAR_COLOR_VARS = [
|
|
10
13
|
'--color-surface-action-strong',
|
|
11
14
|
'--color-content-action-on-strong',
|
|
@@ -159,7 +162,7 @@ export function useFlashCalendarTheme(selectedDateId?: DateId): CalendarTheme {
|
|
|
159
162
|
},
|
|
160
163
|
rowWeek: {
|
|
161
164
|
container: {
|
|
162
|
-
paddingBottom:
|
|
165
|
+
paddingBottom: WEEK_HEADER_PADDING_BOTTOM,
|
|
163
166
|
},
|
|
164
167
|
},
|
|
165
168
|
itemWeekName: {
|
|
@@ -249,7 +252,7 @@ export function useFlashCalendarRangeTheme(range?: {
|
|
|
249
252
|
},
|
|
250
253
|
rowWeek: {
|
|
251
254
|
container: {
|
|
252
|
-
paddingBottom:
|
|
255
|
+
paddingBottom: WEEK_HEADER_PADDING_BOTTOM,
|
|
253
256
|
},
|
|
254
257
|
},
|
|
255
258
|
itemWeekName: {
|
|
@@ -348,6 +351,28 @@ export const FLASH_CALENDAR_RANGE_DAY_SPACING = 0;
|
|
|
348
351
|
|
|
349
352
|
export const FLASH_CALENDAR_DAY_HEIGHT = DAY_SIZE;
|
|
350
353
|
|
|
354
|
+
export const FLASH_CALENDAR_WEEK_HEADER_HEIGHT = 28;
|
|
355
|
+
|
|
356
|
+
export const FLASH_CALENDAR_ROW_VERTICAL_SPACING = 4;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Rendered height of the Flash Calendar month grid for `monthId`, derived from the same layout
|
|
360
|
+
* values passed to Flash Calendar (zero-height month row, weekday header, one row per week with
|
|
361
|
+
* Sunday as the first day). Lets the year view match the day view without measuring.
|
|
362
|
+
*/
|
|
363
|
+
export function getFlashCalendarGridHeight(monthId: MonthId): number {
|
|
364
|
+
const parts = parseMonthId(monthId);
|
|
365
|
+
const firstWeekday = parts ? new Date(parts.year, parts.month - 1, 1).getDay() : 0;
|
|
366
|
+
const daysInMonth = parts ? new Date(parts.year, parts.month, 0).getDate() : 31;
|
|
367
|
+
const weekCount = Math.ceil((firstWeekday + daysInMonth) / 7);
|
|
368
|
+
return (
|
|
369
|
+
FLASH_CALENDAR_WEEK_HEADER_HEIGHT +
|
|
370
|
+
WEEK_HEADER_PADDING_BOTTOM +
|
|
371
|
+
weekCount * FLASH_CALENDAR_DAY_HEIGHT +
|
|
372
|
+
(weekCount + 1) * FLASH_CALENDAR_ROW_VERTICAL_SPACING
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
351
376
|
export function formatFlashCalendarWeekdayShort(date: Date, locale: string): string {
|
|
352
377
|
return new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(date);
|
|
353
378
|
}
|
|
@@ -15,6 +15,8 @@ import { CalendarMonthHeader } from './CalendarMonthHeader';
|
|
|
15
15
|
import { CalendarYearPickerGrid } from './CalendarYearPickerGrid';
|
|
16
16
|
import {
|
|
17
17
|
FLASH_CALENDAR_DAY_HEIGHT,
|
|
18
|
+
FLASH_CALENDAR_ROW_VERTICAL_SPACING,
|
|
19
|
+
FLASH_CALENDAR_WEEK_HEADER_HEIGHT,
|
|
18
20
|
formatFlashCalendarWeekdayShort,
|
|
19
21
|
getMonthDateIds,
|
|
20
22
|
toCalendarDateId,
|
|
@@ -143,8 +145,8 @@ const CalendarGrid = forwardRef<View, ViewProps>(({ className, style, ...props }
|
|
|
143
145
|
calendarDisabledDateIds={unavailableDateIds}
|
|
144
146
|
calendarMonthHeaderHeight={0}
|
|
145
147
|
calendarDayHeight={FLASH_CALENDAR_DAY_HEIGHT}
|
|
146
|
-
calendarWeekHeaderHeight={
|
|
147
|
-
calendarRowVerticalSpacing={
|
|
148
|
+
calendarWeekHeaderHeight={FLASH_CALENDAR_WEEK_HEADER_HEIGHT}
|
|
149
|
+
calendarRowVerticalSpacing={FLASH_CALENDAR_ROW_VERTICAL_SPACING}
|
|
148
150
|
calendarRowHorizontalSpacing={4}
|
|
149
151
|
getCalendarMonthFormat={() => ''}
|
|
150
152
|
getCalendarWeekDayFormat={formatWeekday}
|
|
@@ -158,12 +160,15 @@ const CalendarGrid = forwardRef<View, ViewProps>(({ className, style, ...props }
|
|
|
158
160
|
CalendarGrid.displayName = 'Calendar.Grid';
|
|
159
161
|
|
|
160
162
|
const CalendarYearPicker = forwardRef<View, ViewProps>(({ className, style, ...props }, ref) => {
|
|
161
|
-
const { monthId, onMonthIdChange, disabled, setView, labels } =
|
|
163
|
+
const { monthId, onMonthIdChange, minValue, maxValue, disabled, setView, labels } =
|
|
164
|
+
useCalendarContext();
|
|
162
165
|
return (
|
|
163
166
|
<CalendarYearPickerGrid
|
|
164
167
|
ref={ref}
|
|
165
168
|
monthId={monthId}
|
|
166
169
|
onMonthIdChange={onMonthIdChange}
|
|
170
|
+
minValue={minValue}
|
|
171
|
+
maxValue={maxValue}
|
|
167
172
|
disabled={disabled}
|
|
168
173
|
onViewChange={setView}
|
|
169
174
|
labels={labels}
|
|
@@ -31,12 +31,28 @@ export const calendarMonthHeaderIconVariants = cva('size-5 shrink-0', {
|
|
|
31
31
|
defaultVariants: { isYearView: false },
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
export const calendarMonthHeaderNavVariants = cva('flex-row items-center gap-1'
|
|
34
|
+
export const calendarMonthHeaderNavVariants = cva('flex-row items-center gap-1', {
|
|
35
|
+
variants: {
|
|
36
|
+
hidden: {
|
|
37
|
+
true: 'opacity-0',
|
|
38
|
+
false: '',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { hidden: false },
|
|
42
|
+
});
|
|
35
43
|
|
|
36
44
|
export const calendarYearPickerGridVariants = cva(
|
|
37
45
|
'flex-row flex-wrap justify-between gap-y-2 px-1 py-1',
|
|
38
46
|
);
|
|
39
47
|
|
|
48
|
+
export const calendarYearPickerListVariants = cva('w-full');
|
|
49
|
+
|
|
50
|
+
export const calendarYearPickerListContentVariants = cva('px-1');
|
|
51
|
+
|
|
52
|
+
export const calendarYearPickerRowVariants = cva('flex-row items-center justify-between');
|
|
53
|
+
|
|
54
|
+
export const calendarYearPickerPlaceholderVariants = cva('w-[31%]');
|
|
55
|
+
|
|
40
56
|
export const calendarPillCellVariants = cva(
|
|
41
57
|
'my-0.5 w-[31%] items-center justify-center rounded-[var(--border-radius-round)] py-2.5',
|
|
42
58
|
{
|
|
@@ -29,6 +29,9 @@ export const iconButtonRootVariants = cva(
|
|
|
29
29
|
neutral: [],
|
|
30
30
|
action: [],
|
|
31
31
|
danger: [],
|
|
32
|
+
warning: [],
|
|
33
|
+
success: [],
|
|
34
|
+
info: [],
|
|
32
35
|
},
|
|
33
36
|
size: iconButtonSizeClassNames,
|
|
34
37
|
},
|
|
@@ -67,6 +70,39 @@ export const iconButtonRootVariants = cva(
|
|
|
67
70
|
}),
|
|
68
71
|
],
|
|
69
72
|
},
|
|
73
|
+
{
|
|
74
|
+
variant: 'solid',
|
|
75
|
+
color: 'warning',
|
|
76
|
+
className: [
|
|
77
|
+
'bg-surface-warning-strong',
|
|
78
|
+
Platform.select({
|
|
79
|
+
default: 'data-[active=true]:bg-surface-warning-strong-active',
|
|
80
|
+
web: 'data-[hover=true]:bg-surface-warning-strong-hover data-[active=true]:data-[hover=true]:bg-surface-warning-strong-active',
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
variant: 'solid',
|
|
86
|
+
color: 'success',
|
|
87
|
+
className: [
|
|
88
|
+
'bg-surface-success-strong',
|
|
89
|
+
Platform.select({
|
|
90
|
+
default: 'data-[active=true]:bg-surface-success-strong-active',
|
|
91
|
+
web: 'data-[hover=true]:bg-surface-success-strong-hover data-[active=true]:data-[hover=true]:bg-surface-success-strong-active',
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
variant: 'solid',
|
|
97
|
+
color: 'info',
|
|
98
|
+
className: [
|
|
99
|
+
'bg-surface-info-strong',
|
|
100
|
+
Platform.select({
|
|
101
|
+
default: 'data-[active=true]:bg-surface-info-strong-active',
|
|
102
|
+
web: 'data-[hover=true]:bg-surface-info-strong-hover data-[active=true]:data-[hover=true]:bg-surface-info-strong-active',
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
},
|
|
70
106
|
|
|
71
107
|
// ── subtle × color ─────────────────────────────────────────────────
|
|
72
108
|
{
|
|
@@ -102,6 +138,39 @@ export const iconButtonRootVariants = cva(
|
|
|
102
138
|
}),
|
|
103
139
|
],
|
|
104
140
|
},
|
|
141
|
+
{
|
|
142
|
+
variant: 'subtle',
|
|
143
|
+
color: 'warning',
|
|
144
|
+
className: [
|
|
145
|
+
'bg-surface-warning-subtle',
|
|
146
|
+
Platform.select({
|
|
147
|
+
default: 'data-[active=true]:bg-surface-warning-subtle-active',
|
|
148
|
+
web: 'data-[hover=true]:bg-surface-warning-subtle-hover data-[active=true]:data-[hover=true]:bg-surface-warning-subtle-active',
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
variant: 'subtle',
|
|
154
|
+
color: 'success',
|
|
155
|
+
className: [
|
|
156
|
+
'bg-surface-success-subtle',
|
|
157
|
+
Platform.select({
|
|
158
|
+
default: 'data-[active=true]:bg-surface-success-subtle-active',
|
|
159
|
+
web: 'data-[hover=true]:bg-surface-success-subtle-hover data-[active=true]:data-[hover=true]:bg-surface-success-subtle-active',
|
|
160
|
+
}),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
variant: 'subtle',
|
|
165
|
+
color: 'info',
|
|
166
|
+
className: [
|
|
167
|
+
'bg-surface-info-subtle',
|
|
168
|
+
Platform.select({
|
|
169
|
+
default: 'data-[active=true]:bg-surface-info-subtle-active',
|
|
170
|
+
web: 'data-[hover=true]:bg-surface-info-subtle-hover data-[active=true]:data-[hover=true]:bg-surface-info-subtle-active',
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
},
|
|
105
174
|
|
|
106
175
|
// ── ghost × color ──────────────────────────────────────────────────
|
|
107
176
|
{
|
|
@@ -134,6 +203,36 @@ export const iconButtonRootVariants = cva(
|
|
|
134
203
|
}),
|
|
135
204
|
],
|
|
136
205
|
},
|
|
206
|
+
{
|
|
207
|
+
variant: 'ghost',
|
|
208
|
+
color: 'warning',
|
|
209
|
+
className: [
|
|
210
|
+
Platform.select({
|
|
211
|
+
default: 'data-[active=true]:bg-surface-warning-subtle-active',
|
|
212
|
+
web: 'data-[hover=true]:bg-surface-warning-subtle-hover data-[active=true]:data-[hover=true]:bg-surface-warning-subtle-active',
|
|
213
|
+
}),
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
variant: 'ghost',
|
|
218
|
+
color: 'success',
|
|
219
|
+
className: [
|
|
220
|
+
Platform.select({
|
|
221
|
+
default: 'data-[active=true]:bg-surface-success-subtle-active',
|
|
222
|
+
web: 'data-[hover=true]:bg-surface-success-subtle-hover data-[active=true]:data-[hover=true]:bg-surface-success-subtle-active',
|
|
223
|
+
}),
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
variant: 'ghost',
|
|
228
|
+
color: 'info',
|
|
229
|
+
className: [
|
|
230
|
+
Platform.select({
|
|
231
|
+
default: 'data-[active=true]:bg-surface-info-subtle-active',
|
|
232
|
+
web: 'data-[hover=true]:bg-surface-info-subtle-hover data-[active=true]:data-[hover=true]:bg-surface-info-subtle-active',
|
|
233
|
+
}),
|
|
234
|
+
],
|
|
235
|
+
},
|
|
137
236
|
],
|
|
138
237
|
defaultVariants: {
|
|
139
238
|
variant: 'solid',
|
|
@@ -166,18 +265,30 @@ export const iconButtonIconColorVariants = cva([], {
|
|
|
166
265
|
neutral: [],
|
|
167
266
|
action: [],
|
|
168
267
|
danger: [],
|
|
268
|
+
warning: [],
|
|
269
|
+
success: [],
|
|
270
|
+
info: [],
|
|
169
271
|
},
|
|
170
272
|
},
|
|
171
273
|
compoundVariants: [
|
|
172
274
|
{ variant: 'solid', color: 'neutral', className: 'text-content-primary' },
|
|
173
275
|
{ variant: 'solid', color: 'action', className: 'text-content-action-on-strong' },
|
|
174
276
|
{ variant: 'solid', color: 'danger', className: 'text-content-danger-on-strong' },
|
|
277
|
+
{ variant: 'solid', color: 'warning', className: 'text-content-warning-on-strong' },
|
|
278
|
+
{ variant: 'solid', color: 'success', className: 'text-content-success-on-strong' },
|
|
279
|
+
{ variant: 'solid', color: 'info', className: 'text-content-info-on-strong' },
|
|
175
280
|
{ variant: 'subtle', color: 'neutral', className: 'text-content-primary' },
|
|
176
281
|
{ variant: 'subtle', color: 'action', className: 'text-content-action-on-subtle' },
|
|
177
282
|
{ variant: 'subtle', color: 'danger', className: 'text-content-danger-on-subtle' },
|
|
283
|
+
{ variant: 'subtle', color: 'warning', className: 'text-content-warning-on-subtle' },
|
|
284
|
+
{ variant: 'subtle', color: 'success', className: 'text-content-success-on-subtle' },
|
|
285
|
+
{ variant: 'subtle', color: 'info', className: 'text-content-info-on-subtle' },
|
|
178
286
|
{ variant: 'ghost', color: 'neutral', className: 'text-content-primary' },
|
|
179
287
|
{ variant: 'ghost', color: 'action', className: 'text-content-action' },
|
|
180
288
|
{ variant: 'ghost', color: 'danger', className: 'text-content-danger' },
|
|
289
|
+
{ variant: 'ghost', color: 'warning', className: 'text-content-warning' },
|
|
290
|
+
{ variant: 'ghost', color: 'success', className: 'text-content-success' },
|
|
291
|
+
{ variant: 'ghost', color: 'info', className: 'text-content-info' },
|
|
181
292
|
],
|
|
182
293
|
defaultVariants: {
|
|
183
294
|
variant: 'solid',
|