cpk-ui 0.7.0 → 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +14 -14
  2. package/components/uis/Calendar/Calendar.d.ts +6 -0
  3. package/components/uis/Calendar/Calendar.d.ts.map +1 -0
  4. package/components/uis/Calendar/Calendar.js +482 -0
  5. package/components/uis/Calendar/CalendarDay.d.ts +21 -0
  6. package/components/uis/Calendar/CalendarDay.d.ts.map +1 -0
  7. package/components/uis/Calendar/CalendarDay.js +172 -0
  8. package/components/uis/Calendar/CalendarHeader.d.ts +22 -0
  9. package/components/uis/Calendar/CalendarHeader.d.ts.map +1 -0
  10. package/components/uis/Calendar/CalendarHeader.js +40 -0
  11. package/components/uis/Calendar/CalendarPage.d.ts +46 -0
  12. package/components/uis/Calendar/CalendarPage.d.ts.map +1 -0
  13. package/components/uis/Calendar/CalendarPage.js +34 -0
  14. package/components/uis/Calendar/CalendarPager.d.ts +11 -0
  15. package/components/uis/Calendar/CalendarPager.d.ts.map +1 -0
  16. package/components/uis/Calendar/CalendarPager.js +115 -0
  17. package/components/uis/Calendar/CalendarPager.web.d.ts +11 -0
  18. package/components/uis/Calendar/CalendarPager.web.d.ts.map +1 -0
  19. package/components/uis/Calendar/CalendarPager.web.js +72 -0
  20. package/components/uis/Calendar/CalendarWeekRow.d.ts +20 -0
  21. package/components/uis/Calendar/CalendarWeekRow.d.ts.map +1 -0
  22. package/components/uis/Calendar/CalendarWeekRow.js +64 -0
  23. package/components/uis/Calendar/CalendarWeekdayRow.d.ts +24 -0
  24. package/components/uis/Calendar/CalendarWeekdayRow.d.ts.map +1 -0
  25. package/components/uis/Calendar/CalendarWeekdayRow.js +45 -0
  26. package/components/uis/Calendar/intl.d.ts +18 -0
  27. package/components/uis/Calendar/intl.d.ts.map +1 -0
  28. package/components/uis/Calendar/intl.js +146 -0
  29. package/components/uis/Calendar/palette.d.ts +10 -0
  30. package/components/uis/Calendar/palette.d.ts.map +1 -0
  31. package/components/uis/Calendar/palette.js +93 -0
  32. package/components/uis/Calendar/types.d.ts +286 -0
  33. package/components/uis/Calendar/types.d.ts.map +1 -0
  34. package/components/uis/Calendar/types.js +1 -0
  35. package/components/uis/Calendar/useCalendarState.d.ts +66 -0
  36. package/components/uis/Calendar/useCalendarState.d.ts.map +1 -0
  37. package/components/uis/Calendar/useCalendarState.js +211 -0
  38. package/components/uis/Calendar/utils.d.ts +114 -0
  39. package/components/uis/Calendar/utils.d.ts.map +1 -0
  40. package/components/uis/Calendar/utils.js +429 -0
  41. package/components/uis/ParallaxHeader/ParallaxHeader.d.ts +44 -0
  42. package/components/uis/ParallaxHeader/ParallaxHeader.d.ts.map +1 -0
  43. package/components/uis/ParallaxHeader/ParallaxHeader.js +142 -0
  44. package/index.d.ts +2 -0
  45. package/index.d.ts.map +1 -1
  46. package/index.js +2 -0
  47. package/package.json +2 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@ maintained by Cross Platform Korea and uses
10
10
  [kstyled](https://crossplatformkorea.github.io/kstyled) for typed,
11
11
  compile-time-aware styling.
12
12
 
13
- `cpk-ui@0.7.0` pairs with `kstyled@0.4.1`. Keep the runtime and Babel plugin on
13
+ `cpk-ui@0.8.0-beta.0` pairs with `kstyled@0.4.1`. Keep the runtime and Babel plugin on
14
14
  the same kstyled version.
15
15
 
16
16
  ## Install
@@ -18,9 +18,9 @@ the same kstyled version.
18
18
  ### Expo
19
19
 
20
20
  ```bash
21
- npx expo install cpk-ui@0.7.0 kstyled@0.4.1 @expo/vector-icons \
21
+ npx expo install cpk-ui@0.8.0-beta.0 kstyled@0.4.1 @expo/vector-icons \
22
22
  react-native-gesture-handler react-native-svg expo-screen-orientation \
23
- @expo/match-media expo-haptics
23
+ react-native-reanimated @expo/match-media expo-haptics
24
24
  bun add --dev babel-plugin-kstyled@0.4.1
25
25
  ```
26
26
 
@@ -30,9 +30,9 @@ Install Expo modules first, then add the same runtime dependencies:
30
30
 
31
31
  ```bash
32
32
  npx install-expo-modules@latest
33
- bun add cpk-ui@0.7.0 kstyled@0.4.1 @expo/vector-icons \
33
+ bun add cpk-ui@0.8.0-beta.0 kstyled@0.4.1 @expo/vector-icons \
34
34
  react-native-gesture-handler react-native-svg expo-screen-orientation \
35
- @expo/match-media expo-haptics
35
+ react-native-reanimated @expo/match-media expo-haptics
36
36
  bun add --dev babel-plugin-kstyled@0.4.1
37
37
  ```
38
38
 
@@ -77,13 +77,13 @@ dark theme tokens. Applications that provide product-specific tokens can pass
77
77
 
78
78
  ## Component Surface
79
79
 
80
- | Area | Components |
81
- | ---------------- | ---------------------------------------------------------------------- |
82
- | Actions | Button, CustomPressable, Fab, IconButton |
83
- | Inputs | Checkbox, EditText, RadioGroup, Rating, SegmentedControl, SwitchToggle |
84
- | Feedback | AlertDialog, ErrorBoundary, LoadingIndicator, Snackbar |
85
- | Display | Accordion, Card, Hr, Icon, Typography |
86
- | Media and system | PinchZoom, StatusBarBrightness |
80
+ | Area | Components |
81
+ | ---------------- | -------------------------------------------------------------------------------- |
82
+ | Actions | Button, CustomPressable, Fab, IconButton |
83
+ | Inputs | Calendar, Checkbox, EditText, RadioGroup, Rating, SegmentedControl, SwitchToggle |
84
+ | Feedback | AlertDialog, ErrorBoundary, LoadingIndicator, Snackbar |
85
+ | Display | Accordion, Card, Hr, Icon, ParallaxHeader, Typography |
86
+ | Media and system | PinchZoom, StatusBarBrightness |
87
87
 
88
88
  Each public component has automated tests and Storybook coverage for its main
89
89
  states. Use the [live Storybook](https://ui.crossplatformkorea.com) to inspect
@@ -91,7 +91,7 @@ controls, responsive behavior, accessibility results, and source examples.
91
91
 
92
92
  ## Release Matrix
93
93
 
94
- The 0.7.0 release is verified with Expo 54, React Native 0.81, React 19,
94
+ The 0.8.0 beta is verified with Expo 54, React Native 0.81, React 19,
95
95
  kstyled 0.4, and React Native Web 0.21. This is the release validation matrix,
96
96
  not a claim that every consumer must use those exact React or React Native
97
97
  versions.
@@ -112,7 +112,7 @@ main-branch GitHub Actions publish workflow with provenance.
112
112
  ## Documentation
113
113
 
114
114
  - [Storybook](https://ui.crossplatformkorea.com)
115
- - [0.7.0 release notes](./docs/releases/0.7.0.md)
115
+ - [0.8.0 beta release notes](./docs/releases/0.8.0-beta.0.md)
116
116
  - [0.7 engineering notes](./docs/blog/2026-07-11-cpk-ui-0.7.0.md)
117
117
  - [Performance guide](./docs/PERFORMANCE.md)
118
118
  - [Changelog](./CHANGELOG.md)
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { CalendarProps, CalendarRef } from './types';
3
+ export declare const Calendar: React.NamedExoticComponent<CalendarProps & React.RefAttributes<CalendarRef>>;
4
+ export default Calendar;
5
+ export * from './types';
6
+ //# sourceMappingURL=Calendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Calendar.d.ts","sourceRoot":"","sources":["../../../../src/components/uis/Calendar/Calendar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAWN,MAAM,OAAO,CAAC;AAuBf,OAAO,KAAK,EASV,aAAa,EACb,WAAW,EAIZ,MAAM,SAAS,CAAC;AAw3BjB,eAAO,MAAM,QAAQ,8EAA4C,CAAC;AAElE,eAAe,QAAQ,CAAC;AAExB,cAAc,SAAS,CAAC"}
@@ -0,0 +1,482 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
+ import { AccessibilityInfo, I18nManager, useWindowDimensions, View, } from 'react-native';
4
+ import { css } from 'kstyled';
5
+ import { useAnimatedReaction, useSharedValue, withTiming, runOnJS, } from 'react-native-reanimated';
6
+ import { useTheme } from '../../../providers/ThemeProvider';
7
+ import { CalendarHeader } from './CalendarHeader';
8
+ import { CalendarPage } from './CalendarPage';
9
+ import { CalendarPager } from './CalendarPager';
10
+ import { CalendarWeekdayRow } from './CalendarWeekdayRow';
11
+ import { DEFAULT_FORMATTERS, getFormattersId, resolveLocale } from './intl';
12
+ import { isSamePalette, resolveCalendarPalette } from './palette';
13
+ import { resolveKeyboardAction, useControllable, useDisabledState, useReduceMotion, useShallowStable, useToday, } from './useCalendarState';
14
+ import { addDays, addPages, getCalendarGrid, localNoon, normalizePageKey, pageFirstDate, pageLastDate, resolveMetrics, resolveWeekStart, startOfWeek, toDateKeyInput, toDayKey, } from './utils';
15
+ const CONTAINER = css `
16
+ align-self: center;
17
+ width: 100%;
18
+ `;
19
+ const TWO_UP_ROW = css `
20
+ flex-direction: row;
21
+ align-items: flex-start;
22
+ gap: 24px;
23
+ `;
24
+ const COLUMN = css `
25
+ flex: 1;
26
+ `;
27
+ const EMPTY_MARKERS = Object.freeze({});
28
+ const EMPTY_STYLES = Object.freeze({});
29
+ const DEFAULT_LABELS = Object.freeze({
30
+ previousMonth: 'Previous month',
31
+ nextMonth: 'Next month',
32
+ previousWeek: 'Previous week',
33
+ nextWeek: 'Next week',
34
+ today: 'Today',
35
+ selected: 'Selected',
36
+ weekNumber: 'Week',
37
+ pageAnnouncement: (pageTitle) => pageTitle,
38
+ });
39
+ /** A Sunday, used only to derive weekday header labels. Never rendered. */
40
+ const WEEKDAY_LABEL_ANCHOR = localNoon(2024, 0, 7);
41
+ const COLLAPSE_DURATION = 220;
42
+ const BADGE_FONT_SCALE_LIMIT = 1.3;
43
+ const SINGLE_DOT_FONT_SCALE_LIMIT = 1.5;
44
+ /**
45
+ * `onKeyDown` is forwarded by react-native-web and simply never fires on native,
46
+ * so one handler covers both platforms without a `.web.tsx` split.
47
+ */
48
+ const KeyboardView = View;
49
+ function buildGeometry(metrics) {
50
+ return Object.freeze({
51
+ row: { height: metrics.rowHeight },
52
+ cell: { height: metrics.rowHeight, paddingTop: metrics.pillOffset },
53
+ pill: {
54
+ width: metrics.numberSize,
55
+ height: metrics.numberSize,
56
+ borderRadius: metrics.numberSize / 2,
57
+ },
58
+ dayText: { fontSize: metrics.dayFontSize },
59
+ badgeText: { fontSize: metrics.badgeFontSize },
60
+ dot: {
61
+ width: metrics.dotSize,
62
+ height: metrics.dotSize,
63
+ borderRadius: metrics.dotSize / 2,
64
+ },
65
+ dotRow: { height: metrics.dotRowHeight },
66
+ weekNumber: { width: metrics.weekNumberWidth },
67
+ weekNumberText: { fontSize: metrics.weekdayFontSize },
68
+ });
69
+ }
70
+ function CalendarContainer(props, ref) {
71
+ const { value, defaultValue, onChange, month, defaultMonth, onMonthChange, mode: modeProp, defaultMode, onModeChange, collapseProgress, locale: localeProp, weekStart: weekStartProp, today: todayProp, fixedWeeks = true, markers = EMPTY_MARKERS, showDots = true, maxDotsPerDay = 3, showWeekNumbers = false, showOutsideDays = true, minDate, maxDate, disabledDates, isDateDisabled, showHeader = true, showNavigationButtons = true, showWeekdayRow = true, paging = 'swipe', monthsToShow = 1, size, maxFontSizeMultiplier = 1.6, colors, styles: stylesProp = EMPTY_STYLES, style, formatters: formattersProp, renderHeader, renderWeekdayLabel, renderDay, onDayLongPress, onDisabledDayPress, labels: labelsPropInput, accessibilityLabel, testID = 'calendar', } = props;
72
+ const { theme, media } = useTheme();
73
+ const { fontScale } = useWindowDimensions();
74
+ const reduceMotion = useReduceMotion();
75
+ const locale = useMemo(() => localeProp ?? resolveLocale(), [localeProp]);
76
+ const weekStart = useMemo(() => weekStartProp ?? resolveWeekStart(locale), [locale, weekStartProp]);
77
+ const todayNoon = useToday(todayProp);
78
+ const todayKey = useMemo(() => toDayKey(todayNoon), [todayNoon]);
79
+ // Value comparison, not identity, for the three props callers almost always
80
+ // write as inline object literals. Without it `chrome` is rebuilt on every
81
+ // parent render and all 42 day cells re-render — the exact regression the
82
+ // memo test suite pins for `colors` but never checked for these.
83
+ const labelsProp = useShallowStable(labelsPropInput);
84
+ const styles = useShallowStable(stylesProp);
85
+ const formattersInput = useShallowStable(formattersProp);
86
+ const formatters = useMemo(() => formattersInput
87
+ ? Object.freeze({ ...DEFAULT_FORMATTERS, ...formattersInput })
88
+ : DEFAULT_FORMATTERS, [formattersInput]);
89
+ const formattersId = getFormattersId(formatters);
90
+ const [mode, setModeState] = useControllable(modeProp, defaultMode ?? 'month');
91
+ // ── selection ───────────────────────────────────────────────────────────────
92
+ const isValueControlled = value !== undefined;
93
+ const [selectedKeyState, setSelectedKeyState] = useState(() => defaultValue == null ? null : toDateKeyInput(defaultValue));
94
+ const selectedKey = isValueControlled
95
+ ? value === null
96
+ ? null
97
+ : toDateKeyInput(value)
98
+ : selectedKeyState;
99
+ // ── paging ──────────────────────────────────────────────────────────────────
100
+ const isMonthControlled = month !== undefined;
101
+ const [pageKeyState, setPageKeyState] = useState(() => normalizePageKey(defaultMonth ?? selectedKeyState ?? todayNoon, defaultMode ?? 'month', weekStartProp ?? resolveWeekStart(localeProp ?? resolveLocale()), monthsToShow));
102
+ const pageInput = month === undefined
103
+ ? pageKeyState
104
+ : typeof month === 'string'
105
+ ? month
106
+ : toDayKey(month);
107
+ const pageKey = useMemo(() => normalizePageKey(pageInput, mode, weekStart, monthsToShow), [mode, monthsToShow, pageInput, weekStart]);
108
+ const [focusedKey, setFocusedKey] = useState(null);
109
+ // ── grids ───────────────────────────────────────────────────────────────────
110
+ const gridFor = useCallback((key) => getCalendarGrid({
111
+ pageKey: key,
112
+ mode,
113
+ weekStart,
114
+ fixedWeeks,
115
+ showWeekNumbers,
116
+ locale,
117
+ formatters,
118
+ formattersId,
119
+ }), [
120
+ fixedWeeks,
121
+ formatters,
122
+ formattersId,
123
+ locale,
124
+ mode,
125
+ showWeekNumbers,
126
+ weekStart,
127
+ ]);
128
+ const previousPageKey = useMemo(() => addPages(pageKey, -1, mode, weekStart, monthsToShow), [mode, monthsToShow, pageKey, weekStart]);
129
+ const nextPageKey = useMemo(() => addPages(pageKey, 1, mode, weekStart, monthsToShow), [mode, monthsToShow, pageKey, weekStart]);
130
+ const currentGrid = useMemo(() => gridFor(pageKey), [gridFor, pageKey]);
131
+ // ── theming and geometry ────────────────────────────────────────────────────
132
+ // Value comparison, not identity: `theme` and an inline `colors` object are both
133
+ // rebuilt on every parent render, and a new palette would re-render every cell.
134
+ const paletteRef = useRef(null);
135
+ const palette = useMemo(() => {
136
+ const next = resolveCalendarPalette(theme, colors);
137
+ const previous = paletteRef.current;
138
+ if (previous && isSamePalette(previous, next)) {
139
+ return previous;
140
+ }
141
+ paletteRef.current = next;
142
+ return next;
143
+ }, [colors, theme]);
144
+ const resolvedSize = size ?? (media.isDesktop || media.isTablet ? 'expanded' : 'regular');
145
+ const metrics = useMemo(() => resolveMetrics(resolvedSize, fontScale, maxFontSizeMultiplier), [fontScale, maxFontSizeMultiplier, resolvedSize]);
146
+ const geometry = useMemo(() => buildGeometry(metrics), [metrics]);
147
+ const labels = useMemo(() => labelsProp
148
+ ? Object.freeze({ ...DEFAULT_LABELS, ...labelsProp })
149
+ : DEFAULT_LABELS, [labelsProp]);
150
+ // ── constraints ─────────────────────────────────────────────────────────────
151
+ const { resolver: isDisabled, version: disabledVersion, minKey, maxKey, } = useDisabledState({ minDate, maxDate, disabledDates, isDateDisabled });
152
+ // ── latest-value refs, so no handler identity depends on a changing callback ─
153
+ const callbacksRef = useRef({
154
+ onChange,
155
+ onMonthChange,
156
+ onModeChange,
157
+ onDayLongPress,
158
+ onDisabledDayPress,
159
+ });
160
+ const stateRef = useRef({
161
+ pageKey,
162
+ mode,
163
+ weekStart,
164
+ monthsToShow,
165
+ isMonthControlled,
166
+ isValueControlled,
167
+ isModeControlled: modeProp !== undefined,
168
+ });
169
+ useEffect(() => {
170
+ callbacksRef.current = {
171
+ onChange,
172
+ onMonthChange,
173
+ onModeChange,
174
+ onDayLongPress,
175
+ onDisabledDayPress,
176
+ };
177
+ }, [
178
+ onChange,
179
+ onDayLongPress,
180
+ onDisabledDayPress,
181
+ onModeChange,
182
+ onMonthChange,
183
+ ]);
184
+ useEffect(() => {
185
+ stateRef.current = {
186
+ pageKey,
187
+ mode,
188
+ weekStart,
189
+ monthsToShow,
190
+ isMonthControlled,
191
+ isValueControlled,
192
+ isModeControlled: modeProp !== undefined,
193
+ };
194
+ }, [
195
+ isMonthControlled,
196
+ isValueControlled,
197
+ mode,
198
+ modeProp,
199
+ monthsToShow,
200
+ pageKey,
201
+ weekStart,
202
+ ]);
203
+ const goToPage = useCallback((nextKey, source) => {
204
+ const current = stateRef.current;
205
+ if (!current.isMonthControlled) {
206
+ setPageKeyState(nextKey);
207
+ }
208
+ const first = pageFirstDate(nextKey, current.mode, current.weekStart);
209
+ callbacksRef.current.onMonthChange?.(new Date(first.getFullYear(), first.getMonth(), first.getDate()), nextKey, source);
210
+ }, []);
211
+ const stepPage = useCallback((delta, source) => {
212
+ const current = stateRef.current;
213
+ goToPage(addPages(current.pageKey, delta, current.mode, current.weekStart, current.monthsToShow), source);
214
+ }, [goToPage]);
215
+ const handleSwipeCommit = useCallback((delta) => stepPage(delta, 'swipe'), [stepPage]);
216
+ const focusKey = useCallback((key, source) => {
217
+ setFocusedKey(key);
218
+ const current = stateRef.current;
219
+ const targetPage = normalizePageKey(key, current.mode, current.weekStart, current.monthsToShow);
220
+ if (targetPage !== current.pageKey) {
221
+ goToPage(targetPage, source);
222
+ }
223
+ }, [goToPage]);
224
+ const selectKey = useCallback((key, source) => {
225
+ const current = stateRef.current;
226
+ if (!current.isValueControlled) {
227
+ setSelectedKeyState(key);
228
+ }
229
+ setFocusedKey(key);
230
+ const date = new Date(+key.slice(0, 4), +key.slice(5, 7) - 1, +key.slice(8, 10));
231
+ callbacksRef.current.onChange?.(date, key, source);
232
+ }, []);
233
+ // ── stable cell callbacks. No inline arrow ever reaches a day cell ───────────
234
+ const handleDayPress = useCallback((key) => selectKey(key, 'press'), [selectKey]);
235
+ const handleDayLongPress = useCallback((key) => {
236
+ callbacksRef.current.onDayLongPress?.(new Date(+key.slice(0, 4), +key.slice(5, 7) - 1, +key.slice(8, 10)), key);
237
+ }, []);
238
+ const handleDisabledDayPress = useCallback((key) => {
239
+ callbacksRef.current.onDisabledDayPress?.(new Date(+key.slice(0, 4), +key.slice(5, 7) - 1, +key.slice(8, 10)), key);
240
+ }, []);
241
+ const chrome = useMemo(() => Object.freeze({
242
+ palette,
243
+ metrics,
244
+ geometry,
245
+ showDots,
246
+ maxDots: fontScale > SINGLE_DOT_FONT_SCALE_LIMIT
247
+ ? 1
248
+ : Math.max(maxDotsPerDay, 0),
249
+ showBadges: fontScale <= BADGE_FONT_SCALE_LIMIT,
250
+ maxFontSizeMultiplier,
251
+ fontScale,
252
+ labels,
253
+ styles,
254
+ renderDay,
255
+ onPress: handleDayPress,
256
+ onLongPress: onDayLongPress ? handleDayLongPress : undefined,
257
+ onDisabledPress: onDisabledDayPress
258
+ ? handleDisabledDayPress
259
+ : undefined,
260
+ }), [
261
+ fontScale,
262
+ geometry,
263
+ handleDayLongPress,
264
+ handleDayPress,
265
+ handleDisabledDayPress,
266
+ labels,
267
+ maxDotsPerDay,
268
+ maxFontSizeMultiplier,
269
+ metrics,
270
+ onDayLongPress,
271
+ onDisabledDayPress,
272
+ palette,
273
+ renderDay,
274
+ showDots,
275
+ styles,
276
+ ]);
277
+ // ── collapse ────────────────────────────────────────────────────────────────
278
+ const internalCollapse = useSharedValue(mode === 'week' ? 1 : 0);
279
+ const collapse = collapseProgress ?? internalCollapse;
280
+ const activeRow = useSharedValue(0);
281
+ const anchorKey = selectedKey &&
282
+ selectedKey >= currentGrid.firstKey &&
283
+ selectedKey <= currentGrid.lastKey
284
+ ? selectedKey
285
+ : todayKey >= currentGrid.firstKey && todayKey <= currentGrid.lastKey
286
+ ? todayKey
287
+ : currentGrid.firstKey;
288
+ const activeRowIndex = useMemo(() => {
289
+ for (let i = 0; i < currentGrid.days.length; i++) {
290
+ if (currentGrid.days[i].key === anchorKey) {
291
+ return (i / 7) | 0;
292
+ }
293
+ }
294
+ return 0;
295
+ }, [anchorKey, currentGrid]);
296
+ useEffect(() => {
297
+ activeRow.value = mode === 'week' ? 0 : activeRowIndex;
298
+ }, [activeRow, activeRowIndex, mode]);
299
+ useEffect(() => {
300
+ if (collapseProgress) {
301
+ return;
302
+ }
303
+ internalCollapse.value = withTiming(mode === 'week' ? 1 : 0, {
304
+ duration: reduceMotion ? 0 : COLLAPSE_DURATION,
305
+ });
306
+ }, [collapseProgress, internalCollapse, mode, reduceMotion]);
307
+ const commitMode = useCallback((nextMode) => {
308
+ if (stateRef.current.mode === nextMode) {
309
+ return;
310
+ }
311
+ setModeState(nextMode);
312
+ callbacksRef.current.onModeChange?.(nextMode);
313
+ }, [setModeState]);
314
+ useAnimatedReaction(() => collapse.value, (progress, previous) => {
315
+ if (previous === null) {
316
+ return;
317
+ }
318
+ if (progress >= 0.999 && previous < 0.999) {
319
+ runOnJS(commitMode)('week');
320
+ }
321
+ else if (progress <= 0.001 && previous > 0.001) {
322
+ runOnJS(commitMode)('month');
323
+ }
324
+ }, [collapse, commitMode]);
325
+ // ── derived chrome values ───────────────────────────────────────────────────
326
+ const weekdayLabels = useMemo(() => {
327
+ const anchor = startOfWeek(WEEKDAY_LABEL_ANCHOR, weekStart);
328
+ return Object.freeze(Array.from({ length: 7 }, (_, index) => formatters.weekdayLabel(addDays(anchor, index), locale)));
329
+ }, [formatters, locale, weekStart]);
330
+ const headerDate = useMemo(() => pageFirstDate(pageKey, mode, weekStart), [mode, pageKey, weekStart]);
331
+ const title = useMemo(() => {
332
+ if (mode === 'week') {
333
+ return formatters.weekTitle(headerDate, addDays(headerDate, 6), locale);
334
+ }
335
+ if (monthsToShow > 1) {
336
+ const secondMonth = pageFirstDate(nextPageKey, mode, weekStart);
337
+ return `${formatters.monthTitle(headerDate, locale)} – ${formatters.monthTitle(addDays(secondMonth, -1), locale)}`;
338
+ }
339
+ return formatters.monthTitle(headerDate, locale);
340
+ }, [
341
+ formatters,
342
+ headerDate,
343
+ locale,
344
+ mode,
345
+ monthsToShow,
346
+ nextPageKey,
347
+ weekStart,
348
+ ]);
349
+ // Compared against each neighbouring page's own month/week range, not against the
350
+ // padded grid, so a min date on the 1st really does disable the previous button.
351
+ const canGoPrevious = minKey === null ||
352
+ toDayKey(pageLastDate(previousPageKey, mode, weekStart, monthsToShow)) >=
353
+ minKey;
354
+ const canGoNext = maxKey === null ||
355
+ toDayKey(pageFirstDate(nextPageKey, mode, weekStart)) <= maxKey;
356
+ const goPrevious = useCallback(() => stepPage(-1, 'button'), [stepPage]);
357
+ const goNext = useCallback(() => stepPage(1, 'button'), [stepPage]);
358
+ const headerContext = useMemo(() => ({
359
+ title,
360
+ date: headerDate,
361
+ mode,
362
+ canGoPrevious,
363
+ canGoNext,
364
+ goPrevious,
365
+ goNext,
366
+ }), [canGoNext, canGoPrevious, goNext, goPrevious, headerDate, mode, title]);
367
+ const announcedTitle = useRef(null);
368
+ useEffect(() => {
369
+ if (announcedTitle.current === null) {
370
+ announcedTitle.current = title;
371
+ return;
372
+ }
373
+ if (announcedTitle.current === title) {
374
+ return;
375
+ }
376
+ announcedTitle.current = title;
377
+ AccessibilityInfo.announceForAccessibility(labels.pageAnnouncement(title));
378
+ }, [labels, title]);
379
+ // ── roving focus target ─────────────────────────────────────────────────────
380
+ const tabbableKey = useMemo(() => {
381
+ const inPage = (key) => key >= currentGrid.firstKey && key <= currentGrid.lastKey;
382
+ if (focusedKey !== null && inPage(focusedKey)) {
383
+ return focusedKey;
384
+ }
385
+ if (selectedKey !== null && inPage(selectedKey)) {
386
+ return selectedKey;
387
+ }
388
+ if (inPage(todayKey)) {
389
+ return todayKey;
390
+ }
391
+ for (const day of currentGrid.days) {
392
+ if (!day.isOutside) {
393
+ return day.key;
394
+ }
395
+ }
396
+ return currentGrid.firstKey;
397
+ }, [currentGrid, focusedKey, selectedKey, todayKey]);
398
+ const handleKeyDown = useCallback((event) => {
399
+ const action = resolveKeyboardAction({
400
+ key: event.key,
401
+ shiftKey: event.shiftKey === true,
402
+ focusedKey: tabbableKey,
403
+ weekStart: stateRef.current.weekStart,
404
+ isRTL: I18nManager.isRTL,
405
+ mode: stateRef.current.mode,
406
+ });
407
+ if (!action) {
408
+ return;
409
+ }
410
+ event.preventDefault?.();
411
+ if (action.type === 'select') {
412
+ selectKey(tabbableKey, 'keyboard');
413
+ return;
414
+ }
415
+ focusKey(action.key, 'keyboard');
416
+ }, [focusKey, selectKey, tabbableKey]);
417
+ // ── imperative API ──────────────────────────────────────────────────────────
418
+ useImperativeHandle(ref, () => ({
419
+ goToMonth: (target) => {
420
+ const current = stateRef.current;
421
+ goToPage(normalizePageKey(target, current.mode, current.weekStart, current.monthsToShow), 'imperative');
422
+ },
423
+ goToToday: () => {
424
+ const current = stateRef.current;
425
+ goToPage(normalizePageKey(todayKey, current.mode, current.weekStart, current.monthsToShow), 'imperative');
426
+ },
427
+ next: () => stepPage(1, 'imperative'),
428
+ previous: () => stepPage(-1, 'imperative'),
429
+ setMode: (nextMode) => commitMode(nextMode),
430
+ focusDay: (key) => focusKey(key, 'imperative'),
431
+ }), [commitMode, focusKey, goToPage, stepPage, todayKey]);
432
+ // ── page rendering ──────────────────────────────────────────────────────────
433
+ const rowCount = mode === 'week' ? 1 : currentGrid.rowCount;
434
+ const collapsible = mode === 'week' || (monthsToShow === 1 && rowCount > 1);
435
+ const renderGrid = useCallback((gridPageKey, gridTestID) => {
436
+ const grid = gridFor(gridPageKey);
437
+ return (_jsx(CalendarPage, { activeRow: activeRow, chrome: chrome, collapse: collapse, collapsible: collapsible, disabledVersion: disabledVersion, grid: grid, isDisabled: isDisabled, markers: markers, selectedKey: selectedKey, showOutsideDays: showOutsideDays, showWeekNumbers: showWeekNumbers, tabbableKey: tabbableKey, testID: gridTestID, todayKey: todayKey }));
438
+ }, [
439
+ activeRow,
440
+ chrome,
441
+ collapse,
442
+ collapsible,
443
+ disabledVersion,
444
+ gridFor,
445
+ isDisabled,
446
+ markers,
447
+ selectedKey,
448
+ showOutsideDays,
449
+ showWeekNumbers,
450
+ tabbableKey,
451
+ todayKey,
452
+ ]);
453
+ const renderSlot = useCallback((slotPageKey) => {
454
+ if (monthsToShow === 1) {
455
+ return renderGrid(slotPageKey, `calendar-page-${slotPageKey}`);
456
+ }
457
+ const monthKeys = [slotPageKey];
458
+ for (let index = 1; index < monthsToShow; index++) {
459
+ monthKeys.push(addPages(slotPageKey, index, mode, weekStart, 1));
460
+ }
461
+ return (_jsx(View, { style: TWO_UP_ROW, children: monthKeys.map((monthKey) => (_jsx(View, { style: COLUMN, children: renderGrid(monthKey, `calendar-page-${monthKey}`) }, monthKey))) }));
462
+ }, [mode, monthsToShow, renderGrid, weekStart]);
463
+ const slots = useMemo(() => {
464
+ if (paging === 'none') {
465
+ return [{ key: pageKey, offset: 0, node: renderSlot(pageKey) }];
466
+ }
467
+ return [
468
+ { key: previousPageKey, offset: -1, node: renderSlot(previousPageKey) },
469
+ { key: pageKey, offset: 0, node: renderSlot(pageKey) },
470
+ { key: nextPageKey, offset: 1, node: renderSlot(nextPageKey) },
471
+ ];
472
+ }, [nextPageKey, pageKey, paging, previousPageKey, renderSlot]);
473
+ const containerStyle = useMemo(() => ({
474
+ backgroundColor: palette.background,
475
+ maxWidth: metrics.cellSize * 7 * 1.6 * monthsToShow,
476
+ }), [metrics.cellSize, monthsToShow, palette.background]);
477
+ const weekdayRow = showWeekdayRow ? (_jsx(View, { style: monthsToShow > 1 ? TWO_UP_ROW : undefined, children: Array.from({ length: monthsToShow }, (_, index) => (_jsx(View, { style: COLUMN, children: _jsx(CalendarWeekdayRow, { maxFontSizeMultiplier: maxFontSizeMultiplier, metrics: metrics, palette: palette, renderWeekdayLabel: renderWeekdayLabel, showWeekNumbers: showWeekNumbers, styles: styles, weekStart: weekStart, weekdayLabels: weekdayLabels }) }, `weekday-row-${index}`))) })) : null;
478
+ return (_jsxs(KeyboardView, { accessibilityLabel: accessibilityLabel ?? title, onKeyDown: handleKeyDown, style: [CONTAINER, containerStyle, styles.container, style], testID: testID, children: [showHeader ? (_jsx(CalendarHeader, { canGoNext: canGoNext, canGoPrevious: canGoPrevious, headerContext: headerContext, labels: labels, maxFontSizeMultiplier: maxFontSizeMultiplier, metrics: metrics, mode: mode, onNext: goNext, onPrevious: goPrevious, palette: palette, renderHeader: renderHeader, showNavigationButtons: showNavigationButtons, styles: styles, title: title })) : null, weekdayRow, _jsx(CalendarPager, { activeRow: activeRow, collapse: collapse, onCommit: handleSwipeCommit, reduceMotion: reduceMotion, rowCount: rowCount, rowHeight: metrics.rowHeight, slots: slots, swipeEnabled: paging === 'swipe', testID: "calendar-pager" })] }));
479
+ }
480
+ export const Calendar = React.memo(forwardRef(CalendarContainer));
481
+ export default Calendar;
482
+ export * from './types';
@@ -0,0 +1,21 @@
1
+ import React, { type ReactElement } from 'react';
2
+ import type { CalendarChrome, CalendarDayModel, CalendarMarker } from './types';
3
+ export type CalendarDayProps = {
4
+ day: CalendarDayModel;
5
+ /** Precomputed by the grid builder. A cell never formats a date itself. */
6
+ dayLabel: string;
7
+ accessibilityLabel: string;
8
+ isSelected: boolean;
9
+ isToday: boolean;
10
+ isDisabled: boolean;
11
+ /** Roving tab index: exactly one cell per page is tabbable. */
12
+ isTabbable: boolean;
13
+ /** Inert spacer for an out of month day when `showOutsideDays` is false. */
14
+ isHidden: boolean;
15
+ marker?: CalendarMarker;
16
+ chrome: CalendarChrome;
17
+ };
18
+ declare function CalendarDayContainer(props: CalendarDayProps): ReactElement;
19
+ export declare const CalendarDay: React.MemoExoticComponent<typeof CalendarDayContainer>;
20
+ export default CalendarDay;
21
+ //# sourceMappingURL=CalendarDay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CalendarDay.d.ts","sourceRoot":"","sources":["../../../../src/components/uis/Calendar/CalendarDay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuB,KAAK,YAAY,EAAC,MAAM,OAAO,CAAC;AAKrE,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EAChB,cAAc,EAEf,MAAM,SAAS,CAAC;AAyCjB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACxB,CAAC;AAoDF,iBAAS,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAsMnE;AAcD,eAAO,MAAM,WAAW,wDAAiD,CAAC;AAE1E,eAAe,WAAW,CAAC"}