@super-calendar/native 2.4.0 → 2.6.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.
@@ -0,0 +1,276 @@
1
+ import { format, startOfDay, type Locale } from "date-fns";
2
+ import { memo, type ReactElement, useMemo, useState } from "react";
3
+ import {
4
+ type LayoutChangeEvent,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ } from "react-native";
11
+ import { useCalendarTheme } from "../theme";
12
+ import type { CalendarEvent, WeekStartsOn } from "../types";
13
+ import { createSlots, type SlotStyleProps } from "../utils/slots";
14
+ import {
15
+ buildMonthWeeks,
16
+ getIsToday,
17
+ getWeekDays,
18
+ getYearMonths,
19
+ groupEventsByDay,
20
+ isSameCalendarDay,
21
+ weekdayFormatToken,
22
+ } from "@super-calendar/core";
23
+
24
+ /**
25
+ * The styleable parts of {@link YearView}. `dayText` and `eventDot` are the
26
+ * text/marker inside a day cell (React Native text colour doesn't inherit).
27
+ */
28
+ export type YearViewSlot =
29
+ | "grid"
30
+ | "month"
31
+ | "monthTitle"
32
+ | "weekdays"
33
+ | "weekday"
34
+ | "week"
35
+ | "day"
36
+ | "dayText"
37
+ | "eventDot";
38
+
39
+ /** Props for {@link YearView}, the twelve mini-month year grid. */
40
+ export type YearViewProps<T = unknown> = SlotStyleProps<YearViewSlot> & {
41
+ /** Any date inside the year to render. */
42
+ date: Date;
43
+ /** Days holding at least one event get a dot. Omit for a plain year. */
44
+ events?: CalendarEvent<T>[];
45
+ weekStartsOn: WeekStartsOn;
46
+ locale?: Locale;
47
+ /** Highlight this date instead of the real "today". */
48
+ activeDate?: Date;
49
+ /**
50
+ * Smallest width a mini month may take before the grid drops a column
51
+ * (default 150). The grid fits 2–4 columns from the measured width.
52
+ */
53
+ minMonthWidth?: number;
54
+ /** Tap a day cell (e.g. to drill into the day or month view). */
55
+ onPressDay?: (date: Date) => void;
56
+ /** Tap a month's title (e.g. to jump to that month's view). */
57
+ onPressMonth?: (month: Date) => void;
58
+ };
59
+
60
+ // Seed before the first layout pass so the grid renders immediately (and in
61
+ // tests, where onLayout never fires).
62
+ const FALLBACK_COLUMNS = 3;
63
+
64
+ function YearViewInner<T>({
65
+ date,
66
+ events,
67
+ weekStartsOn,
68
+ locale,
69
+ activeDate,
70
+ minMonthWidth = 150,
71
+ onPressDay,
72
+ onPressMonth,
73
+ classNames,
74
+ styles: styleOverrides,
75
+ }: YearViewProps<T>): ReactElement {
76
+ const theme = useCalendarTheme();
77
+ const slot = createSlots<YearViewSlot>({ classNames, styles: styleOverrides });
78
+
79
+ const [columns, setColumns] = useState(FALLBACK_COLUMNS);
80
+ const handleLayout = (event: LayoutChangeEvent) => {
81
+ const width = event.nativeEvent.layout.width;
82
+ if (width <= 0) return;
83
+ setColumns(Math.max(2, Math.min(4, Math.floor(width / minMonthWidth))));
84
+ };
85
+
86
+ const months = useMemo(() => getYearMonths(date), [date]);
87
+ // Weekday initials for one shared header per mini month.
88
+ const weekdayLabels = useMemo(
89
+ () =>
90
+ getWeekDays(date, weekStartsOn).map((d) =>
91
+ format(d, weekdayFormatToken("narrow"), { locale }),
92
+ ),
93
+ [date, weekStartsOn, locale],
94
+ );
95
+ // Days that hold at least one event, keyed like `groupEventsByDay`.
96
+ const eventDays = useMemo(
97
+ () => new Set(events && events.length > 0 ? groupEventsByDay(events).keys() : []),
98
+ [events],
99
+ );
100
+
101
+ return (
102
+ <ScrollView onLayout={handleLayout} showsVerticalScrollIndicator>
103
+ <View {...slot("grid", { base: styles.grid })}>
104
+ {months.map((month) => {
105
+ const weeks = buildMonthWeeks(month, weekStartsOn);
106
+ const title = format(month, "MMMM", { locale });
107
+ const monthLabel = format(month, "MMMM yyyy", { locale });
108
+ const titleText = (
109
+ <Text
110
+ {...slot("monthTitle", {
111
+ base: styles.monthTitle,
112
+ themed: [styles.monthTitleText, { color: theme.colors.todayBackground }],
113
+ })}
114
+ allowFontScaling={false}
115
+ >
116
+ {title}
117
+ </Text>
118
+ );
119
+ return (
120
+ <View
121
+ key={month.toISOString()}
122
+ {...slot("month", { base: [styles.month, { width: `${100 / columns}%` }] })}
123
+ >
124
+ {onPressMonth ? (
125
+ <Pressable
126
+ onPress={() => onPressMonth(month)}
127
+ accessibilityRole="button"
128
+ accessibilityLabel={monthLabel}
129
+ >
130
+ {titleText}
131
+ </Pressable>
132
+ ) : (
133
+ <View accessible accessibilityRole="header" accessibilityLabel={monthLabel}>
134
+ {titleText}
135
+ </View>
136
+ )}
137
+ {/* Decorative initials: repeating 84 single letters across the twelve
138
+ mini months would drown a screen reader, so hide the rows (the
139
+ day cells carry full date labels). */}
140
+ <View
141
+ {...slot("weekdays", { base: styles.weekdays })}
142
+ accessibilityElementsHidden
143
+ importantForAccessibility="no-hide-descendants"
144
+ >
145
+ {weekdayLabels.map((label, i) => (
146
+ <Text
147
+ key={i}
148
+ {...slot("weekday", {
149
+ base: styles.weekday,
150
+ themed: [styles.weekdayText, { color: theme.colors.textMuted }],
151
+ })}
152
+ allowFontScaling={false}
153
+ >
154
+ {label}
155
+ </Text>
156
+ ))}
157
+ </View>
158
+ {weeks.map((week) => (
159
+ <View key={week[0].toISOString()} {...slot("week", { base: styles.week })}>
160
+ {week.map((day) => {
161
+ // Adjacent-month days keep the grid shape but stay blank,
162
+ // like the mini months of other year views.
163
+ if (day.getMonth() !== month.getMonth()) {
164
+ return <View key={day.toISOString()} style={styles.day} />;
165
+ }
166
+ const isHighlighted = activeDate
167
+ ? isSameCalendarDay(day, activeDate)
168
+ : getIsToday(day);
169
+ const hasEvents = eventDays.has(startOfDay(day).toISOString());
170
+ const label = `${format(day, "EEEE, d LLLL yyyy", { locale })}${
171
+ getIsToday(day) ? ", today" : ""
172
+ }${hasEvents ? ", has events" : ""}`;
173
+ const daySlot = slot("day", {
174
+ base: styles.day,
175
+ themed: isHighlighted
176
+ ? {
177
+ backgroundColor: theme.colors.todayBackground,
178
+ borderRadius: theme.todayBadgeRadius,
179
+ }
180
+ : undefined,
181
+ });
182
+ const content = (
183
+ <>
184
+ <Text
185
+ {...slot("dayText", {
186
+ themed: [
187
+ styles.dayText,
188
+ {
189
+ color: isHighlighted ? theme.colors.todayText : theme.colors.text,
190
+ },
191
+ ],
192
+ })}
193
+ allowFontScaling={false}
194
+ >
195
+ {day.getDate()}
196
+ </Text>
197
+ {hasEvents ? (
198
+ <View
199
+ {...slot("eventDot", {
200
+ base: styles.eventDot,
201
+ themed: {
202
+ backgroundColor: isHighlighted
203
+ ? theme.colors.todayText
204
+ : theme.colors.todayBackground,
205
+ },
206
+ })}
207
+ />
208
+ ) : null}
209
+ </>
210
+ );
211
+ return onPressDay ? (
212
+ <Pressable
213
+ key={day.toISOString()}
214
+ onPress={() => onPressDay(day)}
215
+ accessibilityRole="button"
216
+ accessibilityLabel={label}
217
+ {...daySlot}
218
+ >
219
+ {content}
220
+ </Pressable>
221
+ ) : (
222
+ <View
223
+ key={day.toISOString()}
224
+ accessible
225
+ accessibilityLabel={label}
226
+ {...daySlot}
227
+ >
228
+ {content}
229
+ </View>
230
+ );
231
+ })}
232
+ </View>
233
+ ))}
234
+ </View>
235
+ );
236
+ })}
237
+ </View>
238
+ </ScrollView>
239
+ );
240
+ }
241
+
242
+ /**
243
+ * A year at a glance: the twelve months as compact mini grids, with today
244
+ * highlighted and a dot under days that hold events. Tap a day or a month
245
+ * title to drill into a denser view. It's the view `Calendar` renders for
246
+ * `mode="year"`.
247
+ *
248
+ * @example
249
+ * ```tsx
250
+ * import { YearView } from "@super-calendar/native";
251
+ *
252
+ * <YearView
253
+ * date={new Date()}
254
+ * events={events}
255
+ * weekStartsOn={1}
256
+ * onPressDay={(day) => console.log(day)}
257
+ * />
258
+ * ```
259
+ */
260
+ export const YearView = memo(YearViewInner) as typeof YearViewInner;
261
+
262
+ const styles = StyleSheet.create({
263
+ grid: { flexDirection: "row", flexWrap: "wrap" },
264
+ month: { padding: 8 },
265
+ // Structural layout / themed typography split per slot, so a slot class can
266
+ // replace the look without breaking the layout.
267
+ monthTitle: { paddingBottom: 4 },
268
+ monthTitleText: { fontSize: 13, fontWeight: "700" },
269
+ weekdays: { flexDirection: "row" },
270
+ weekday: { flex: 1, textAlign: "center" },
271
+ weekdayText: { fontSize: 9, fontWeight: "600" },
272
+ week: { flexDirection: "row" },
273
+ day: { flex: 1, aspectRatio: 1, alignItems: "center", justifyContent: "center" },
274
+ dayText: { fontSize: 11 },
275
+ eventDot: { position: "absolute", bottom: 1, width: 3, height: 3, borderRadius: 2 },
276
+ });
package/src/index.tsx CHANGED
@@ -23,6 +23,7 @@
23
23
  export { Calendar, type CalendarProps, type CalendarSlot } from "./components/Calendar";
24
24
  export { Agenda, type AgendaProps, type AgendaSlot } from "./components/Agenda";
25
25
  export { MonthView, type MonthViewProps, type MonthViewSlot } from "./components/MonthView";
26
+ export { YearView, type YearViewProps, type YearViewSlot } from "./components/YearView";
26
27
  export { type ResolvedSlot, type SlotDefault, type SlotStyleProps } from "./utils/slots";
27
28
  export { MonthPager, type MonthPagerProps } from "./components/MonthPager";
28
29
  export { MonthList, type MonthListProps, type MonthListSlot } from "./components/MonthList";
@@ -1,9 +1,9 @@
1
1
  import { addMonths, differenceInCalendarMonths, format, isSameMonth, startOfDay, startOfMonth } from "date-fns";
2
2
  import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { CalendarSelectionProvider, buildMonthWeeks, compareDayEvents, darkColors, dayBadgeKind, daySelectionState, eventAccessibilityLabel, eventTimeLabel, getIsToday, getWeekDays, groupEventsByDay, isAllDayEvent, isDateSelectable, isSameCalendarDay, isWeekend, lightColors, monthEventCapacity, monthVisibleCount, rangeBandKind, titleEllipsizeMode, titleNumberOfLines, useCalendarSelection, weekdayFormatToken } from "@super-calendar/core";
4
- import { LegendList } from "@legendapp/list/react-native";
5
4
  import { Platform, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from "react-native";
6
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { LegendList } from "@legendapp/list/react-native";
7
7
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
8
8
  //#region src/theme.ts
9
9
  /** The default light theme. Every key the calendar reads falls back to this. */
@@ -1,9 +1,9 @@
1
1
  let date_fns = require("date-fns");
2
2
  let react = require("react");
3
3
  let _super_calendar_core = require("@super-calendar/core");
4
- let _legendapp_list_react_native = require("@legendapp/list/react-native");
5
4
  let react_native = require("react-native");
6
5
  let react_jsx_runtime = require("react/jsx-runtime");
6
+ let _legendapp_list_react_native = require("@legendapp/list/react-native");
7
7
  let react_native_gesture_handler = require("react-native-gesture-handler");
8
8
  //#region src/theme.ts
9
9
  /** The default light theme. Every key the calendar reads falls back to this. */