@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.
Files changed (94) hide show
  1. package/coverage.svg +1 -1
  2. package/docs/components/bottom-sheet/content.md +2 -0
  3. package/docs/components/calendar/content.md +25 -1
  4. package/docs/components/carousel/content.md +1 -1
  5. package/docs/components/data-grid/content.md +50 -8
  6. package/docs/components/icon-button/content.md +23 -1
  7. package/docs/components/month-calendar/content.md +2 -0
  8. package/docs/components/range-calendar/content.md +3 -1
  9. package/docs/components/toast/content.md +23 -7
  10. package/docs/llms.txt +2 -0
  11. package/docs/resources/release-notes/0.2.5/content.md +25 -0
  12. package/docs/resources/release-notes/0.3.0/content.md +19 -0
  13. package/docs/resources/release-notes/content.md +2 -0
  14. package/lib/commonjs/components/BottomSheet/index.js +25 -1
  15. package/lib/commonjs/components/BottomSheet/index.js.map +1 -1
  16. package/lib/commonjs/components/BottomSheet/styles.js +8 -2
  17. package/lib/commonjs/components/BottomSheet/styles.js.map +1 -1
  18. package/lib/commonjs/components/Calendar/CalendarMonthHeader.js +15 -5
  19. package/lib/commonjs/components/Calendar/CalendarMonthHeader.js.map +1 -1
  20. package/lib/commonjs/components/Calendar/CalendarYearList.js +116 -0
  21. package/lib/commonjs/components/Calendar/CalendarYearList.js.map +1 -0
  22. package/lib/commonjs/components/Calendar/CalendarYearPickerGrid.js +58 -31
  23. package/lib/commonjs/components/Calendar/CalendarYearPickerGrid.js.map +1 -1
  24. package/lib/commonjs/components/Calendar/flashCalendarAdapter.js +22 -3
  25. package/lib/commonjs/components/Calendar/flashCalendarAdapter.js.map +1 -1
  26. package/lib/commonjs/components/Calendar/index.js +6 -2
  27. package/lib/commonjs/components/Calendar/index.js.map +1 -1
  28. package/lib/commonjs/components/Calendar/styles.js +16 -2
  29. package/lib/commonjs/components/Calendar/styles.js.map +1 -1
  30. package/lib/commonjs/components/IconButton/styles.js +107 -2
  31. package/lib/commonjs/components/IconButton/styles.js.map +1 -1
  32. package/lib/commonjs/components/MonthCalendar/index.js +39 -13
  33. package/lib/commonjs/components/MonthCalendar/index.js.map +1 -1
  34. package/lib/commonjs/components/RangeCalendar/index.js +6 -2
  35. package/lib/commonjs/components/RangeCalendar/index.js.map +1 -1
  36. package/lib/commonjs/components/Toast/index.js +5 -2
  37. package/lib/commonjs/components/Toast/index.js.map +1 -1
  38. package/lib/module/components/BottomSheet/index.js +27 -3
  39. package/lib/module/components/BottomSheet/index.js.map +1 -1
  40. package/lib/module/components/BottomSheet/styles.js +8 -2
  41. package/lib/module/components/BottomSheet/styles.js.map +1 -1
  42. package/lib/module/components/Calendar/CalendarMonthHeader.js +15 -5
  43. package/lib/module/components/Calendar/CalendarMonthHeader.js.map +1 -1
  44. package/lib/module/components/Calendar/CalendarYearList.js +112 -0
  45. package/lib/module/components/Calendar/CalendarYearList.js.map +1 -0
  46. package/lib/module/components/Calendar/CalendarYearPickerGrid.js +61 -34
  47. package/lib/module/components/Calendar/CalendarYearPickerGrid.js.map +1 -1
  48. package/lib/module/components/Calendar/flashCalendarAdapter.js +20 -2
  49. package/lib/module/components/Calendar/flashCalendarAdapter.js.map +1 -1
  50. package/lib/module/components/Calendar/index.js +7 -3
  51. package/lib/module/components/Calendar/index.js.map +1 -1
  52. package/lib/module/components/Calendar/styles.js +15 -1
  53. package/lib/module/components/Calendar/styles.js.map +1 -1
  54. package/lib/module/components/IconButton/styles.js +107 -2
  55. package/lib/module/components/IconButton/styles.js.map +1 -1
  56. package/lib/module/components/MonthCalendar/index.js +42 -15
  57. package/lib/module/components/MonthCalendar/index.js.map +1 -1
  58. package/lib/module/components/RangeCalendar/index.js +7 -3
  59. package/lib/module/components/RangeCalendar/index.js.map +1 -1
  60. package/lib/module/components/Toast/index.js +5 -2
  61. package/lib/module/components/Toast/index.js.map +1 -1
  62. package/lib/typescript/components/BottomSheet/index.d.ts +8 -0
  63. package/lib/typescript/components/BottomSheet/index.d.ts.map +1 -1
  64. package/lib/typescript/components/BottomSheet/styles.d.ts +5 -1
  65. package/lib/typescript/components/BottomSheet/styles.d.ts.map +1 -1
  66. package/lib/typescript/components/Calendar/CalendarMonthHeader.d.ts.map +1 -1
  67. package/lib/typescript/components/Calendar/CalendarYearList.d.ts +23 -0
  68. package/lib/typescript/components/Calendar/CalendarYearList.d.ts.map +1 -0
  69. package/lib/typescript/components/Calendar/CalendarYearPickerGrid.d.ts +5 -1
  70. package/lib/typescript/components/Calendar/CalendarYearPickerGrid.d.ts.map +1 -1
  71. package/lib/typescript/components/Calendar/flashCalendarAdapter.d.ts +8 -0
  72. package/lib/typescript/components/Calendar/flashCalendarAdapter.d.ts.map +1 -1
  73. package/lib/typescript/components/Calendar/index.d.ts.map +1 -1
  74. package/lib/typescript/components/Calendar/styles.d.ts +7 -1
  75. package/lib/typescript/components/Calendar/styles.d.ts.map +1 -1
  76. package/lib/typescript/components/IconButton/styles.d.ts +2 -2
  77. package/lib/typescript/components/IconButton/styles.d.ts.map +1 -1
  78. package/lib/typescript/components/MonthCalendar/index.d.ts.map +1 -1
  79. package/lib/typescript/components/RangeCalendar/index.d.ts.map +1 -1
  80. package/lib/typescript/components/Toast/index.d.ts.map +1 -1
  81. package/lib/typescript/components/ToggleButton/styles.d.ts +1 -1
  82. package/package.json +5 -5
  83. package/src/components/BottomSheet/index.tsx +174 -138
  84. package/src/components/BottomSheet/styles.ts +11 -3
  85. package/src/components/Calendar/CalendarMonthHeader.tsx +29 -22
  86. package/src/components/Calendar/CalendarYearList.tsx +136 -0
  87. package/src/components/Calendar/CalendarYearPickerGrid.tsx +60 -33
  88. package/src/components/Calendar/flashCalendarAdapter.ts +27 -2
  89. package/src/components/Calendar/index.tsx +8 -3
  90. package/src/components/Calendar/styles.ts +17 -1
  91. package/src/components/IconButton/styles.ts +111 -0
  92. package/src/components/MonthCalendar/index.tsx +55 -33
  93. package/src/components/RangeCalendar/index.tsx +8 -3
  94. 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
- {!isYearView ? (
98
- <View className={calendarMonthHeaderNavVariants()}>
99
- <IconButton
100
- as={ChevronLeft}
101
- variant="ghost"
102
- color="neutral"
103
- size="small"
104
- accessibilityLabel={labels.previousMonth}
105
- isDisabled={disabled}
106
- onPress={() => shiftMonth(-1)}
107
- />
108
- <IconButton
109
- as={ChevronRight}
110
- variant="ghost"
111
- color="neutral"
112
- size="small"
113
- accessibilityLabel={labels.nextMonth}
114
- isDisabled={disabled}
115
- onPress={() => shiftMonth(1)}
116
- />
117
- </View>
118
- ) : null}
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
- calendarYearPickerGridVariants,
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 ?? new Date().getFullYear();
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 years = useMemo(
53
- () =>
54
- Array.from(
55
- { length: YEAR_RANGE_BEFORE + YEAR_RANGE_AFTER + 1 },
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
- onMonthIdChange(formatMonthId({ year, month: currentMonth }));
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(calendarYearPickerGridVariants(), className)}
111
+ className={cn(calendarYearPickerListVariants(), className)}
76
112
  style={style}
77
113
  accessibilityRole="none"
78
114
  {...props}
79
115
  >
80
- {years.map((year) => {
81
- const selected = year === currentYear;
82
- return (
83
- <Pressable
84
- key={year}
85
- accessibilityRole="button"
86
- accessibilityState={{ selected, disabled }}
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: 4,
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: 4,
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={28}
147
- calendarRowVerticalSpacing={4}
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 } = useCalendarContext();
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',