@super-calendar/core 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.mts +542 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +794 -0
- package/dist/index.mjs +747 -0
- package/package.json +54 -0
- package/src/index.ts +15 -0
- package/src/presentation.ts +39 -0
- package/src/tokens.ts +73 -0
- package/src/types.ts +70 -0
- package/src/utils/dateRange.ts +163 -0
- package/src/utils/dates.ts +118 -0
- package/src/utils/drag.ts +79 -0
- package/src/utils/eventDisplay.ts +180 -0
- package/src/utils/layout.ts +193 -0
- package/src/utils/monthGrid.ts +144 -0
- package/src/utils/recurrence.ts +98 -0
- package/src/utils/timezone.ts +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let date_fns = require("date-fns");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
//#region src/tokens.ts
|
|
5
|
+
const lightColors = {
|
|
6
|
+
gridLine: "#E2E4E9",
|
|
7
|
+
weekendBackground: "#F6F7F9",
|
|
8
|
+
outsideHoursBackground: "#F1F2F4",
|
|
9
|
+
todayBackground: "#1F6FEB",
|
|
10
|
+
todayText: "#FFFFFF",
|
|
11
|
+
selectedBackground: "#1F6FEB",
|
|
12
|
+
selectedText: "#FFFFFF",
|
|
13
|
+
rangeBackground: "#DCE7FF",
|
|
14
|
+
hoverBackground: "#E6ECF5",
|
|
15
|
+
nowIndicator: "#E5484D",
|
|
16
|
+
text: "#1A1B1E",
|
|
17
|
+
textMuted: "#6B7280",
|
|
18
|
+
textDisabled: "#B5B9C0",
|
|
19
|
+
eventBackground: "#DCE7FF",
|
|
20
|
+
eventText: "#1A1B1E"
|
|
21
|
+
};
|
|
22
|
+
const darkColors = {
|
|
23
|
+
gridLine: "#2A2E37",
|
|
24
|
+
weekendBackground: "#15171C",
|
|
25
|
+
outsideHoursBackground: "#101216",
|
|
26
|
+
todayBackground: "#1F6FEB",
|
|
27
|
+
todayText: "#FFFFFF",
|
|
28
|
+
selectedBackground: "#1F6FEB",
|
|
29
|
+
selectedText: "#FFFFFF",
|
|
30
|
+
rangeBackground: "#243B53",
|
|
31
|
+
hoverBackground: "#2E3138",
|
|
32
|
+
nowIndicator: "#E5484D",
|
|
33
|
+
text: "#ECEDEE",
|
|
34
|
+
textMuted: "#9BA1A6",
|
|
35
|
+
textDisabled: "#4B4F58",
|
|
36
|
+
eventBackground: "#243B53",
|
|
37
|
+
eventText: "#EAF2FF"
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/presentation.ts
|
|
41
|
+
/** The band shape for a day, given the fill-cell option. */
|
|
42
|
+
function rangeBandKind(day, fillCell) {
|
|
43
|
+
if (!day.isInRange || day.isRangeStart && day.isRangeEnd) return "none";
|
|
44
|
+
if (fillCell) return "fill";
|
|
45
|
+
if (day.isRangeStart) return "pill-start";
|
|
46
|
+
if (day.isRangeEnd) return "pill-end";
|
|
47
|
+
return "pill-mid";
|
|
48
|
+
}
|
|
49
|
+
/** Whether a band shape rounds its leading / trailing edge (pill ends). */
|
|
50
|
+
function bandRounding(kind) {
|
|
51
|
+
return {
|
|
52
|
+
start: kind === "pill-start",
|
|
53
|
+
end: kind === "pill-end"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The filled-badge kind for a day. `isSelected` is true for both range endpoints
|
|
58
|
+
* and discrete selected days; today always wins when it coincides.
|
|
59
|
+
*/
|
|
60
|
+
function dayBadgeKind(day, isToday) {
|
|
61
|
+
if (isToday) return "today";
|
|
62
|
+
if (day.isSelected) return "selected";
|
|
63
|
+
return "none";
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/utils/dates.ts
|
|
67
|
+
/** The seven dates of the week containing `date`, starting on `weekStartsOn`. */
|
|
68
|
+
const getWeekDays = (date, weekStartsOn) => {
|
|
69
|
+
const start = (0, date_fns.startOfWeek)(date, { weekStartsOn });
|
|
70
|
+
return Array.from({ length: 7 }, (_, index) => (0, date_fns.addDays)(start, index));
|
|
71
|
+
};
|
|
72
|
+
/** How many day columns a time-grid mode shows. `custom` uses `numberOfDays`. */
|
|
73
|
+
const viewDayCount = (mode, numberOfDays = 1) => {
|
|
74
|
+
switch (mode) {
|
|
75
|
+
case "week": return 7;
|
|
76
|
+
case "3days": return 3;
|
|
77
|
+
case "custom": return Math.max(1, Math.floor(numberOfDays));
|
|
78
|
+
default: return 1;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Days in the inclusive span from `weekStartsOn` to `weekEndsOn` (1–7),
|
|
83
|
+
* wrapping when the end precedes the start (e.g. Sat→Wed). Mirrors
|
|
84
|
+
* react-native-big-calendar's `weekDaysCount`.
|
|
85
|
+
*/
|
|
86
|
+
const weekDaysCount = (weekStartsOn, weekEndsOn) => {
|
|
87
|
+
if (weekEndsOn < weekStartsOn) {
|
|
88
|
+
let count = 1;
|
|
89
|
+
let i = weekStartsOn;
|
|
90
|
+
while (i !== weekEndsOn && count <= 7) {
|
|
91
|
+
i = (i + 1) % 7;
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
if (weekEndsOn > weekStartsOn) return weekEndsOn - weekStartsOn + 1;
|
|
97
|
+
return 1;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The day columns to render for a time-grid page. `week` spans the calendar week
|
|
101
|
+
* (honouring `weekStartsOn`). `custom` with a `weekEndsOn` spans the partial week
|
|
102
|
+
* from `weekStartsOn` to `weekEndsOn` (anchored to `date`'s week, paging by week);
|
|
103
|
+
* otherwise every mode shows `viewDayCount` consecutive days starting at `date`.
|
|
104
|
+
*/
|
|
105
|
+
const getViewDays = (mode, date, weekStartsOn, numberOfDays = 1, isRTL = false, weekEndsOn) => {
|
|
106
|
+
let days;
|
|
107
|
+
if (mode === "week") days = getWeekDays(date, weekStartsOn);
|
|
108
|
+
else if (mode === "custom" && weekEndsOn != null) {
|
|
109
|
+
const subject = (0, date_fns.startOfDay)(date);
|
|
110
|
+
const offset = weekStartsOn - subject.getDay();
|
|
111
|
+
days = Array.from({ length: weekDaysCount(weekStartsOn, weekEndsOn) }, (_, index) => (0, date_fns.addDays)(subject, index + offset));
|
|
112
|
+
} else days = Array.from({ length: viewDayCount(mode, numberOfDays) }, (_, index) => (0, date_fns.addDays)((0, date_fns.startOfDay)(date), index));
|
|
113
|
+
return isRTL ? days.reverse() : days;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The calendar weeks covering `month`, padded to whole weeks starting on
|
|
117
|
+
* `weekStartsOn`. `showSixWeeks` always returns six rows (42 days) for a
|
|
118
|
+
* fixed-height grid; `isRTL` reverses each week's day order.
|
|
119
|
+
*/
|
|
120
|
+
const buildMonthWeeks = (month, weekStartsOn, { showSixWeeks = false, isRTL = false } = {}) => {
|
|
121
|
+
const start = (0, date_fns.startOfWeek)((0, date_fns.startOfMonth)(month), { weekStartsOn });
|
|
122
|
+
const days = (0, date_fns.eachDayOfInterval)({
|
|
123
|
+
start,
|
|
124
|
+
end: showSixWeeks ? (0, date_fns.addDays)(start, 41) : (0, date_fns.endOfWeek)((0, date_fns.endOfMonth)(month), { weekStartsOn })
|
|
125
|
+
});
|
|
126
|
+
const weeks = [];
|
|
127
|
+
for (let index = 0; index < days.length; index += 7) {
|
|
128
|
+
const week = days.slice(index, index + 7);
|
|
129
|
+
weeks.push(isRTL ? week.reverse() : week);
|
|
130
|
+
}
|
|
131
|
+
return weeks;
|
|
132
|
+
};
|
|
133
|
+
const isWeekend = (date) => {
|
|
134
|
+
const day = date.getDay();
|
|
135
|
+
return day === 0 || day === 6;
|
|
136
|
+
};
|
|
137
|
+
const getIsToday = (date) => (0, date_fns.isToday)(date);
|
|
138
|
+
const isSameCalendarDay = (a, b) => (0, date_fns.isSameDay)(a, b);
|
|
139
|
+
/** Minutes elapsed since midnight (0–1439). */
|
|
140
|
+
const minutesIntoDay = (date) => (0, date_fns.getHours)(date) * 60 + (0, date_fns.getMinutes)(date);
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/utils/dateRange.ts
|
|
143
|
+
/** Whether `date` passes the min/max/disabled constraints (compared by calendar day). */
|
|
144
|
+
function isDateSelectable(date, constraints = {}) {
|
|
145
|
+
const day = (0, date_fns.startOfDay)(date);
|
|
146
|
+
if (constraints.minDate && (0, date_fns.isBefore)(day, (0, date_fns.startOfDay)(constraints.minDate))) return false;
|
|
147
|
+
if (constraints.maxDate && (0, date_fns.isAfter)(day, (0, date_fns.startOfDay)(constraints.maxDate))) return false;
|
|
148
|
+
if (constraints.isDateDisabled?.(day)) return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The range after pressing `pressed`, mirroring the familiar date-picker model:
|
|
153
|
+
* - no range yet, or a complete range exists → start fresh (`{ start: pressed, end: null }`),
|
|
154
|
+
* so a third press resets the selection.
|
|
155
|
+
* - an open range (a start but no end) → close it, auto-swapping when the press
|
|
156
|
+
* precedes the start so `start <= end` always holds.
|
|
157
|
+
*
|
|
158
|
+
* Returns `current` unchanged when `pressed` isn't selectable.
|
|
159
|
+
*/
|
|
160
|
+
function nextDateRange(current, pressed, constraints = {}) {
|
|
161
|
+
if (!isDateSelectable(pressed, constraints)) return current;
|
|
162
|
+
const day = (0, date_fns.startOfDay)(pressed);
|
|
163
|
+
if (!current || current.end) return {
|
|
164
|
+
start: day,
|
|
165
|
+
end: null
|
|
166
|
+
};
|
|
167
|
+
if ((0, date_fns.isBefore)(day, current.start)) return {
|
|
168
|
+
start: day,
|
|
169
|
+
end: current.start
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
start: current.start,
|
|
173
|
+
end: day
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** True when `date` is one of the range's two endpoints. */
|
|
177
|
+
function isRangeEndpoint(date, range) {
|
|
178
|
+
if (!range) return false;
|
|
179
|
+
if (isSameCalendarDay(date, range.start)) return true;
|
|
180
|
+
return range.end ? isSameCalendarDay(date, range.end) : false;
|
|
181
|
+
}
|
|
182
|
+
/** True when `date` falls within a complete range (endpoints included). */
|
|
183
|
+
function isWithinDateRange(date, range) {
|
|
184
|
+
if (!range || !range.end) return false;
|
|
185
|
+
const day = (0, date_fns.startOfDay)(date).getTime();
|
|
186
|
+
const a = (0, date_fns.startOfDay)(range.start).getTime();
|
|
187
|
+
const b = (0, date_fns.startOfDay)(range.end).getTime();
|
|
188
|
+
return day >= Math.min(a, b) && day <= Math.max(a, b);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The canonical per-day selection state, shared by `MonthView` (rendering) and
|
|
192
|
+
* `buildMonthGrid` (the headless grid) so the built-in views and a custom
|
|
193
|
+
* calendar can never disagree on what a day's state is.
|
|
194
|
+
*/
|
|
195
|
+
function daySelectionState(date, selection, constraints = {}) {
|
|
196
|
+
const isDisabled = !isDateSelectable(date, constraints);
|
|
197
|
+
const range = selection.selectedRange ?? null;
|
|
198
|
+
return {
|
|
199
|
+
isDisabled,
|
|
200
|
+
isSelected: !isDisabled && ((selection.selectedDates?.some((selected) => isSameCalendarDay(selected, date)) ?? false) || isRangeEndpoint(date, range)),
|
|
201
|
+
isInRange: !isDisabled && isWithinDateRange(date, range),
|
|
202
|
+
isRangeStart: !isDisabled && range != null && isSameCalendarDay(date, range.start),
|
|
203
|
+
isRangeEnd: !isDisabled && range?.end != null && isSameCalendarDay(date, range.end)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const CalendarSelectionContext = (0, react.createContext)({});
|
|
207
|
+
/**
|
|
208
|
+
* Provides the active selection to the month grid. Day cells read it via
|
|
209
|
+
* {@link useCalendarSelection} so they repaint on selection changes even when
|
|
210
|
+
* the virtualized list has cached (and so won't re-render) their page.
|
|
211
|
+
*/
|
|
212
|
+
const CalendarSelectionProvider = CalendarSelectionContext.Provider;
|
|
213
|
+
const useCalendarSelection = () => (0, react.useContext)(CalendarSelectionContext);
|
|
214
|
+
/**
|
|
215
|
+
* Controlled-ish range selection state for the month view. Returns the current
|
|
216
|
+
* `range` plus an `onPressDate` handler to wire to `Calendar`'s `onPressDay`, a
|
|
217
|
+
* `reset`, and the raw `setRange` for full control.
|
|
218
|
+
*
|
|
219
|
+
* ```tsx
|
|
220
|
+
* const { range, onPressDate } = useDateRange({ minDate: new Date() });
|
|
221
|
+
* <Calendar mode="month" selectedRange={range ?? undefined} onPressDay={onPressDate} … />
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
function useDateRange(options = {}) {
|
|
225
|
+
const { initialRange = null, minDate, maxDate, isDateDisabled } = options;
|
|
226
|
+
const [range, setRange] = (0, react.useState)(initialRange);
|
|
227
|
+
const constraints = (0, react.useMemo)(() => ({
|
|
228
|
+
minDate,
|
|
229
|
+
maxDate,
|
|
230
|
+
isDateDisabled
|
|
231
|
+
}), [
|
|
232
|
+
minDate,
|
|
233
|
+
maxDate,
|
|
234
|
+
isDateDisabled
|
|
235
|
+
]);
|
|
236
|
+
return {
|
|
237
|
+
range,
|
|
238
|
+
onPressDate: (0, react.useCallback)((date) => setRange((previous) => nextDateRange(previous, date, constraints)), [constraints]),
|
|
239
|
+
selectRange: (0, react.useCallback)((a, b) => {
|
|
240
|
+
if (!isDateSelectable(a, constraints) || !isDateSelectable(b, constraints)) return;
|
|
241
|
+
const [start, end] = a.getTime() <= b.getTime() ? [a, b] : [b, a];
|
|
242
|
+
setRange({
|
|
243
|
+
start: (0, date_fns.startOfDay)(start),
|
|
244
|
+
end: (0, date_fns.startOfDay)(end)
|
|
245
|
+
});
|
|
246
|
+
}, [constraints]),
|
|
247
|
+
reset: (0, react.useCallback)(() => setRange(null), []),
|
|
248
|
+
setRange
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/utils/drag.ts
|
|
253
|
+
/**
|
|
254
|
+
* Minutes to shift an event, snapping a vertical pixel drag to the nearest
|
|
255
|
+
* `stepMinutes`. Runs on the UI thread inside the drag gesture. Returns 0 for a
|
|
256
|
+
* degenerate grid (non-positive height/step).
|
|
257
|
+
*/
|
|
258
|
+
function snapDeltaMinutes(translationPx, cellHeightPx, stepMinutes) {
|
|
259
|
+
"worklet";
|
|
260
|
+
if (cellHeightPx <= 0 || stepMinutes <= 0) return 0;
|
|
261
|
+
const rawMinutes = translationPx / cellHeightPx * 60;
|
|
262
|
+
return Math.round(rawMinutes / stepMinutes) * stepMinutes;
|
|
263
|
+
}
|
|
264
|
+
/** A copy of `date` shifted by `minutes` (may be negative). */
|
|
265
|
+
function shiftMinutes(date, minutes) {
|
|
266
|
+
const next = new Date(date);
|
|
267
|
+
next.setMinutes(next.getMinutes() + minutes);
|
|
268
|
+
return next;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Resolve a committed drag into the event's new bounds: `start` shifts by
|
|
272
|
+
* `deltaStartMinutes`, `end` by `deltaEndMinutes` (a move passes the same delta
|
|
273
|
+
* to both; a resize passes 0 for the start). Returns `null` when the change
|
|
274
|
+
* would collapse the event below one `snapMinutes` step, so the gesture leaves
|
|
275
|
+
* the event untouched rather than committing a degenerate duration. Pure, so the
|
|
276
|
+
* commit path is unit-testable without a running gesture.
|
|
277
|
+
*/
|
|
278
|
+
function resolveDraggedBounds(start, end, deltaStartMinutes, deltaEndMinutes, snapMinutes) {
|
|
279
|
+
const nextStart = shiftMinutes(start, deltaStartMinutes);
|
|
280
|
+
const nextEnd = shiftMinutes(end, deltaEndMinutes);
|
|
281
|
+
if (nextEnd.getTime() - nextStart.getTime() < snapMinutes * 6e4) return null;
|
|
282
|
+
return {
|
|
283
|
+
start: nextStart,
|
|
284
|
+
end: nextEnd
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The start/end of a new event swept out on `day` by dragging from `startPx` to
|
|
289
|
+
* `endPx` (vertical pixels from the grid's top, i.e. the `minHour` line). Both
|
|
290
|
+
* ends snap to `snapMinutes`; the range is ordered (drag up or down) and widened
|
|
291
|
+
* to at least one step so a stationary press still yields a usable event.
|
|
292
|
+
* Returns `null` for a degenerate grid (non-positive height/step). Pure, so the
|
|
293
|
+
* commit path is unit-testable without a running gesture.
|
|
294
|
+
*/
|
|
295
|
+
function cellRangeFromDrag(day, startPx, endPx, cellHeightPx, minHour, snapMinutes) {
|
|
296
|
+
if (cellHeightPx <= 0 || snapMinutes <= 0) return null;
|
|
297
|
+
const snapAt = (px) => {
|
|
298
|
+
const rawMinutes = (minHour + px / cellHeightPx) * 60;
|
|
299
|
+
return Math.round(rawMinutes / snapMinutes) * snapMinutes;
|
|
300
|
+
};
|
|
301
|
+
const MAX = 1440;
|
|
302
|
+
let lower = Math.max(0, Math.min(MAX - snapMinutes, snapAt(startPx)));
|
|
303
|
+
let upper = Math.max(0, Math.min(MAX, snapAt(endPx)));
|
|
304
|
+
if (upper < lower) [lower, upper] = [upper, lower];
|
|
305
|
+
if (upper - lower < snapMinutes) upper = Math.min(MAX, lower + snapMinutes);
|
|
306
|
+
const start = new Date(day);
|
|
307
|
+
start.setHours(0, 0, 0, 0);
|
|
308
|
+
start.setMinutes(lower);
|
|
309
|
+
const end = new Date(day);
|
|
310
|
+
end.setHours(0, 0, 0, 0);
|
|
311
|
+
end.setMinutes(upper);
|
|
312
|
+
return {
|
|
313
|
+
start,
|
|
314
|
+
end
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/utils/eventDisplay.ts
|
|
319
|
+
/**
|
|
320
|
+
* Minimum event-box height (px) before the built-in renderer shows the time line
|
|
321
|
+
* on a narrow multi-column timed grid. Tied to the default theme's font sizes.
|
|
322
|
+
*/
|
|
323
|
+
const MIN_BOX_HEIGHT_FOR_TIME = 56;
|
|
324
|
+
/** Hard-clip an overflowing title by default; opt into a trailing ellipsis. */
|
|
325
|
+
function titleEllipsizeMode(ellipsizeTitle) {
|
|
326
|
+
return ellipsizeTitle ? "tail" : "clip";
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Screen-reader label for an event: its title followed by "all day" or its time
|
|
330
|
+
* range (which the grid otherwise only conveys visually). Empty title is dropped.
|
|
331
|
+
*/
|
|
332
|
+
function eventAccessibilityLabel(args) {
|
|
333
|
+
const timeFormat = args.ampm ? "h:mm a" : "HH:mm";
|
|
334
|
+
const time = args.isAllDay ? args.allDayLabel ?? "all day" : `${(0, date_fns.format)(args.start, timeFormat)} to ${(0, date_fns.format)(args.end, timeFormat)}`;
|
|
335
|
+
return [args.title, time].filter(Boolean).join(", ");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Month cells and the all-day lane show a single clipped line; timed-grid titles
|
|
339
|
+
* (`undefined`) wrap to fill the box.
|
|
340
|
+
*/
|
|
341
|
+
function titleNumberOfLines(mode, isAllDay) {
|
|
342
|
+
return mode === "month" || isAllDay ? 1 : void 0;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The secondary line under the title in the built-in renderer, or `null` when
|
|
346
|
+
* none should show. Timed events get their `start - end` range. An all-day event
|
|
347
|
+
* gets the literal "All day" in the schedule (which has no all-day lane to
|
|
348
|
+
* signal it positionally) and nothing on the day/week grid (the lane already
|
|
349
|
+
* does). Month cells and `showTime={false}` always return `null`.
|
|
350
|
+
*/
|
|
351
|
+
function eventTimeLabel(args) {
|
|
352
|
+
if (!args.showTime || args.mode === "month") return null;
|
|
353
|
+
if (args.isAllDay) return args.mode === "schedule" ? args.allDayLabel ?? "All day" : null;
|
|
354
|
+
const timeFormat = args.ampm ? "h:mm a" : "HH:mm";
|
|
355
|
+
return `${(0, date_fns.format)(args.start, timeFormat)} - ${(0, date_fns.format)(args.end, timeFormat)}`;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The default hour-axis label shared by both renderers' time grids, so the gutter
|
|
359
|
+
* reads the same on each: 24-hour "HH:00" (e.g. "08:00"), or a compact 12-hour
|
|
360
|
+
* "h AM/PM" (e.g. "8 AM") when `ampm` is set. Exported so a custom hour renderer
|
|
361
|
+
* can reuse the same formatting.
|
|
362
|
+
*/
|
|
363
|
+
function formatHour(hour, opts) {
|
|
364
|
+
if (opts?.ampm) {
|
|
365
|
+
const period = hour < 12 ? "AM" : "PM";
|
|
366
|
+
return `${hour % 12 === 0 ? 12 : hour % 12} ${period}`;
|
|
367
|
+
}
|
|
368
|
+
return `${String(hour).padStart(2, "0")}:00`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Whether the time line fits in the box. The wide `day` column and contexts with
|
|
372
|
+
* no live box height (e.g. schedule, where `boxHeightPx` is undefined) always
|
|
373
|
+
* show it; narrow multi-column modes only once the box is at least
|
|
374
|
+
* {@link MIN_BOX_HEIGHT_FOR_TIME} tall. Runs on the UI thread inside the event
|
|
375
|
+
* renderer's animated style.
|
|
376
|
+
*/
|
|
377
|
+
function isTimeVisibleAtHeight(boxHeightPx, mode) {
|
|
378
|
+
"worklet";
|
|
379
|
+
if (boxHeightPx == null || mode === "day") return true;
|
|
380
|
+
return boxHeightPx >= 56;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Lay out the built-in timed-grid event chip for a box of `boxHeightPx`: how
|
|
384
|
+
* many whole title lines fit, and whether the time line still has room below
|
|
385
|
+
* them. The title is primary, so the title fills the box in whole lines (never a
|
|
386
|
+
* half-cropped line) and the time only shows once a full line is left over. Pass
|
|
387
|
+
* `titleLineHeightPx`/`timeLineHeightPx` matching the rendered line heights so
|
|
388
|
+
* the clamp lands on a line boundary.
|
|
389
|
+
*
|
|
390
|
+
* Worklet-safe, so the native renderer can drive the title's max-height on the UI
|
|
391
|
+
* thread as the grid zooms; the dom renderer calls it with its static box height.
|
|
392
|
+
* A `boxHeightPx` of `undefined` (the schedule has no live box height) returns
|
|
393
|
+
* `titleMaxLines: 0` (no clamp) with the unconditional time visibility.
|
|
394
|
+
*/
|
|
395
|
+
function eventChipLayout(args) {
|
|
396
|
+
"worklet";
|
|
397
|
+
const wantTime = args.hasTime && isTimeVisibleAtHeight(args.boxHeightPx, args.mode);
|
|
398
|
+
if (args.boxHeightPx == null || args.titleLineHeightPx <= 0) return {
|
|
399
|
+
titleMaxLines: 0,
|
|
400
|
+
showTime: wantTime
|
|
401
|
+
};
|
|
402
|
+
const inner = args.boxHeightPx - args.paddingYPx * 2;
|
|
403
|
+
const titleMaxLines = Math.max(1, Math.floor((inner - (wantTime ? args.timeLineHeightPx : 0)) / args.titleLineHeightPx));
|
|
404
|
+
return {
|
|
405
|
+
titleMaxLines,
|
|
406
|
+
showTime: wantTime && inner - titleMaxLines * args.titleLineHeightPx >= args.timeLineHeightPx
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Derive how many event chips fit in a month cell from the measured space.
|
|
411
|
+
* `chipRowHeightPx` is one chip plus its gap; `moreRowHeightPx` is the overflow
|
|
412
|
+
* label plus its gap. Both counts are clamped to >= 0.
|
|
413
|
+
*/
|
|
414
|
+
function monthEventCapacity(availableHeightPx, chipRowHeightPx, moreRowHeightPx) {
|
|
415
|
+
if (chipRowHeightPx <= 0) return {
|
|
416
|
+
full: 0,
|
|
417
|
+
withMore: 0
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
full: Math.max(0, Math.floor(availableHeightPx / chipRowHeightPx)),
|
|
421
|
+
withMore: Math.max(0, Math.floor((availableHeightPx - moreRowHeightPx) / chipRowHeightPx))
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Chips to show for a day: all of them when they fit, otherwise `withMore` (at
|
|
426
|
+
* least one) so the rest collapse into a "+N more" label.
|
|
427
|
+
*/
|
|
428
|
+
function monthVisibleCount(total, capacity) {
|
|
429
|
+
if (total <= capacity.full) return total;
|
|
430
|
+
return Math.max(1, capacity.withMore);
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/utils/layout.ts
|
|
434
|
+
const MINUTES_PER_HOUR = 60;
|
|
435
|
+
const MIN_DURATION_HOURS = .25;
|
|
436
|
+
/**
|
|
437
|
+
* Lay out a single day's events: events that overlap in time are split into
|
|
438
|
+
* side-by-side columns. Multi-day events are clipped to the portion that falls
|
|
439
|
+
* on `day` (e.g. a 23:00→01:00 event renders 23:00–24:00 on the start day and
|
|
440
|
+
* 00:00–01:00 on the next). Pure — safe to call per render, never per frame.
|
|
441
|
+
*/
|
|
442
|
+
function layoutDayEvents(events, day) {
|
|
443
|
+
const dayStart = (0, date_fns.startOfDay)(day);
|
|
444
|
+
const nextDayStart = (0, date_fns.addDays)(dayStart, 1);
|
|
445
|
+
const segments = events.filter((event) => !isAllDayEvent(event)).filter((event) => event.start < nextDayStart && event.end > dayStart).map((event) => {
|
|
446
|
+
const segStart = (0, date_fns.max)([event.start, dayStart]);
|
|
447
|
+
const segEnd = (0, date_fns.min)([event.end, nextDayStart]);
|
|
448
|
+
return {
|
|
449
|
+
event,
|
|
450
|
+
start: (0, date_fns.differenceInMinutes)(segStart, dayStart) / MINUTES_PER_HOUR,
|
|
451
|
+
end: (0, date_fns.differenceInMinutes)(segEnd, dayStart) / MINUTES_PER_HOUR,
|
|
452
|
+
continuesBefore: event.start < dayStart,
|
|
453
|
+
continuesAfter: event.end > nextDayStart
|
|
454
|
+
};
|
|
455
|
+
}).sort((a, b) => a.start - b.start);
|
|
456
|
+
const positioned = [];
|
|
457
|
+
let cluster = [];
|
|
458
|
+
let clusterEnd = Number.NEGATIVE_INFINITY;
|
|
459
|
+
const flushCluster = () => {
|
|
460
|
+
const columnEnds = [];
|
|
461
|
+
const columnOf = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const seg of cluster) {
|
|
463
|
+
let column = columnEnds.findIndex((end) => end <= seg.start);
|
|
464
|
+
if (column === -1) {
|
|
465
|
+
column = columnEnds.length;
|
|
466
|
+
columnEnds.push(seg.end);
|
|
467
|
+
} else columnEnds[column] = seg.end;
|
|
468
|
+
columnOf.set(seg, column);
|
|
469
|
+
}
|
|
470
|
+
for (const seg of cluster) positioned.push({
|
|
471
|
+
event: seg.event,
|
|
472
|
+
startHours: seg.start,
|
|
473
|
+
durationHours: Math.max(seg.end - seg.start, MIN_DURATION_HOURS),
|
|
474
|
+
column: columnOf.get(seg) ?? 0,
|
|
475
|
+
columns: columnEnds.length,
|
|
476
|
+
continuesBefore: seg.continuesBefore,
|
|
477
|
+
continuesAfter: seg.continuesAfter
|
|
478
|
+
});
|
|
479
|
+
cluster = [];
|
|
480
|
+
};
|
|
481
|
+
for (const seg of segments) {
|
|
482
|
+
if (cluster.length > 0 && seg.start >= clusterEnd) flushCluster();
|
|
483
|
+
cluster.push(seg);
|
|
484
|
+
clusterEnd = Math.max(clusterEnd, seg.end);
|
|
485
|
+
}
|
|
486
|
+
if (cluster.length > 0) flushCluster();
|
|
487
|
+
return positioned;
|
|
488
|
+
}
|
|
489
|
+
const atMidnight = (date) => date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0;
|
|
490
|
+
/**
|
|
491
|
+
* Whether an event belongs in the all-day lane. An explicit `allDay` flag wins;
|
|
492
|
+
* otherwise it's inferred when the event spans whole days (both `start` and
|
|
493
|
+
* `end` land on midnight, e.g. an iCal-style all-day event). Pure.
|
|
494
|
+
*/
|
|
495
|
+
function isAllDayEvent(event) {
|
|
496
|
+
if (typeof event.allDay === "boolean") return event.allDay;
|
|
497
|
+
return event.end > event.start && atMidnight(event.start) && atMidnight(event.end);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The `startOfDay` ISO keys of every calendar day an event touches (inclusive).
|
|
501
|
+
* An event ending exactly at midnight does not count the following day. Used to
|
|
502
|
+
* index events by day for the month grid. Pure.
|
|
503
|
+
*/
|
|
504
|
+
function eventDayKeys(event) {
|
|
505
|
+
const first = (0, date_fns.startOfDay)(event.start);
|
|
506
|
+
const last = (0, date_fns.startOfDay)(event.end > event.start ? /* @__PURE__ */ new Date(event.end.getTime() - 1) : event.start);
|
|
507
|
+
const keys = [];
|
|
508
|
+
for (let cursor = first; cursor <= last; cursor = (0, date_fns.addDays)(cursor, 1)) keys.push(cursor.toISOString());
|
|
509
|
+
return keys;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Index events by the `startOfDay` ISO key of every day they touch (via
|
|
513
|
+
* {@link eventDayKeys}), so a month grid can look up a day's events with
|
|
514
|
+
* `startOfDay(date).toISOString()`. Built once and shared across month cells.
|
|
515
|
+
*/
|
|
516
|
+
function groupEventsByDay(events) {
|
|
517
|
+
const map = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const event of events) for (const key of eventDayKeys(event)) {
|
|
519
|
+
const list = map.get(key);
|
|
520
|
+
if (list) list.push(event);
|
|
521
|
+
else map.set(key, [event]);
|
|
522
|
+
}
|
|
523
|
+
return map;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Order a day's events for the month and list views: all-day events come first
|
|
527
|
+
* (they head the day regardless of their start time), then timed events by start.
|
|
528
|
+
* Shared by both renderers so the order is identical. Use as an `Array.sort`
|
|
529
|
+
* comparator.
|
|
530
|
+
*/
|
|
531
|
+
function compareDayEvents(a, b) {
|
|
532
|
+
const aAllDay = isAllDayEvent(a);
|
|
533
|
+
if (aAllDay !== isAllDayEvent(b)) return aAllDay ? -1 : 1;
|
|
534
|
+
return a.start.getTime() - b.start.getTime();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* The closed hour-spans of a day to shade on the time grid, given a
|
|
538
|
+
* `businessHours` callback and the visible `[minHour, maxHour]` window: the spans
|
|
539
|
+
* before open and after close (clamped to the window), the whole window when the
|
|
540
|
+
* day is closed (`null`) or the open hours are inverted/empty, or none when the
|
|
541
|
+
* callback returns `undefined`. Shared by both renderers so shading stays
|
|
542
|
+
* identical. Co-located with `groupEventsByDay`; both feed the grid layout.
|
|
543
|
+
*/
|
|
544
|
+
function closedHourBands(day, businessHours, minHour = 0, maxHour = 24) {
|
|
545
|
+
const open = businessHours?.(day);
|
|
546
|
+
if (open === void 0) return [];
|
|
547
|
+
if (open === null) return [{
|
|
548
|
+
start: minHour,
|
|
549
|
+
end: maxHour
|
|
550
|
+
}];
|
|
551
|
+
const start = Math.max(minHour, Math.min(maxHour, open.start));
|
|
552
|
+
const end = Math.max(minHour, Math.min(maxHour, open.end));
|
|
553
|
+
if (start >= end) return [{
|
|
554
|
+
start: minHour,
|
|
555
|
+
end: maxHour
|
|
556
|
+
}];
|
|
557
|
+
const bands = [];
|
|
558
|
+
if (start > minHour) bands.push({
|
|
559
|
+
start: minHour,
|
|
560
|
+
end: start
|
|
561
|
+
});
|
|
562
|
+
if (end < maxHour) bands.push({
|
|
563
|
+
start: end,
|
|
564
|
+
end: maxHour
|
|
565
|
+
});
|
|
566
|
+
return bands;
|
|
567
|
+
}
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/utils/monthGrid.ts
|
|
570
|
+
/**
|
|
571
|
+
* Pure month-grid builder: the weeks and weekday headers for `month`, each day
|
|
572
|
+
* annotated with selection/disabled/today state. Use this when you need the
|
|
573
|
+
* data outside React; inside a component prefer {@link useMonthGrid}.
|
|
574
|
+
*/
|
|
575
|
+
function buildMonthGrid(month, options = {}) {
|
|
576
|
+
const { weekStartsOn = 0, showSixWeeks = false, isRTL = false, selectedDates, selectedRange, minDate, maxDate, isDateDisabled, locale } = options;
|
|
577
|
+
const rows = buildMonthWeeks(month, weekStartsOn, {
|
|
578
|
+
showSixWeeks,
|
|
579
|
+
isRTL
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
weeks: rows.map((days) => ({
|
|
583
|
+
id: days[0].toISOString(),
|
|
584
|
+
days: days.map((date) => ({
|
|
585
|
+
date,
|
|
586
|
+
id: (0, date_fns.format)(date, "yyyy-MM-dd"),
|
|
587
|
+
label: (0, date_fns.format)(date, "d"),
|
|
588
|
+
isCurrentMonth: (0, date_fns.isSameMonth)(date, month),
|
|
589
|
+
isToday: getIsToday(date),
|
|
590
|
+
isWeekend: isWeekend(date),
|
|
591
|
+
...daySelectionState(date, {
|
|
592
|
+
selectedDates,
|
|
593
|
+
selectedRange
|
|
594
|
+
}, {
|
|
595
|
+
minDate,
|
|
596
|
+
maxDate,
|
|
597
|
+
isDateDisabled
|
|
598
|
+
})
|
|
599
|
+
}))
|
|
600
|
+
})),
|
|
601
|
+
weekdays: rows[0].map((date) => ({
|
|
602
|
+
date,
|
|
603
|
+
label: (0, date_fns.format)(date, "EEE", { locale })
|
|
604
|
+
}))
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Headless month-grid hook. Returns the weeks and weekday headers for `month`,
|
|
609
|
+
* each day annotated with selection/disabled/today state, so you can render a
|
|
610
|
+
* fully custom calendar without reimplementing the date maths.
|
|
611
|
+
*
|
|
612
|
+
* ```tsx
|
|
613
|
+
* const { weeks, weekdays } = useMonthGrid(month, { selectedRange: range });
|
|
614
|
+
* // map weekdays -> header cells, weeks -> rows, days -> your own <DayCell />
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
function useMonthGrid(month, options = {}) {
|
|
618
|
+
const { weekStartsOn, showSixWeeks, isRTL, selectedDates, selectedRange, minDate, maxDate, isDateDisabled, locale } = options;
|
|
619
|
+
return (0, react.useMemo)(() => buildMonthGrid(month, options), [
|
|
620
|
+
month,
|
|
621
|
+
weekStartsOn,
|
|
622
|
+
showSixWeeks,
|
|
623
|
+
isRTL,
|
|
624
|
+
selectedDates,
|
|
625
|
+
selectedRange,
|
|
626
|
+
minDate,
|
|
627
|
+
maxDate,
|
|
628
|
+
isDateDisabled,
|
|
629
|
+
locale
|
|
630
|
+
]);
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/utils/recurrence.ts
|
|
634
|
+
const STEP = {
|
|
635
|
+
daily: date_fns.addDays,
|
|
636
|
+
weekly: date_fns.addWeeks,
|
|
637
|
+
monthly: date_fns.addMonths,
|
|
638
|
+
yearly: date_fns.addYears
|
|
639
|
+
};
|
|
640
|
+
const MAX_OCCURRENCES = 5e3;
|
|
641
|
+
function withTimeOf(date, source) {
|
|
642
|
+
const next = new Date(date);
|
|
643
|
+
next.setHours(source.getHours(), source.getMinutes(), source.getSeconds(), source.getMilliseconds());
|
|
644
|
+
return next;
|
|
645
|
+
}
|
|
646
|
+
function* occurrenceStarts(start, rule, rangeEnd) {
|
|
647
|
+
const interval = Math.max(1, Math.trunc(rule.interval ?? 1));
|
|
648
|
+
let produced = 0;
|
|
649
|
+
const within = (date) => date.getTime() <= rangeEnd.getTime() && (rule.until == null || date.getTime() <= rule.until.getTime()) && (rule.count == null || produced < rule.count) && produced < MAX_OCCURRENCES;
|
|
650
|
+
if (rule.freq === "weekly" && rule.weekdays?.length) {
|
|
651
|
+
const weekdays = [...new Set(rule.weekdays)].sort((a, b) => a - b);
|
|
652
|
+
let weekStart = (0, date_fns.startOfWeek)(start, { weekStartsOn: 0 });
|
|
653
|
+
while (true) {
|
|
654
|
+
let advanced = false;
|
|
655
|
+
for (const weekday of weekdays) {
|
|
656
|
+
const date = withTimeOf((0, date_fns.addDays)(weekStart, weekday), start);
|
|
657
|
+
if (date.getTime() < start.getTime()) continue;
|
|
658
|
+
if (!within(date)) return;
|
|
659
|
+
produced += 1;
|
|
660
|
+
advanced = true;
|
|
661
|
+
yield date;
|
|
662
|
+
}
|
|
663
|
+
const nextWeek = (0, date_fns.addWeeks)(weekStart, interval);
|
|
664
|
+
if (!advanced && nextWeek.getTime() > rangeEnd.getTime()) return;
|
|
665
|
+
weekStart = nextWeek;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
let cursor = start;
|
|
669
|
+
while (within(cursor)) {
|
|
670
|
+
produced += 1;
|
|
671
|
+
yield cursor;
|
|
672
|
+
cursor = STEP[rule.freq](cursor, interval);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function instanceAt(event, start, durationMs) {
|
|
676
|
+
const instance = {
|
|
677
|
+
...event,
|
|
678
|
+
start,
|
|
679
|
+
end: new Date(start.getTime() + durationMs)
|
|
680
|
+
};
|
|
681
|
+
delete instance.recurrence;
|
|
682
|
+
return instance;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Materialise recurring events into concrete occurrences overlapping
|
|
686
|
+
* `[rangeStart, rangeEnd]`. Non-recurring events pass through untouched, so the
|
|
687
|
+
* result is ready to hand to `<Calendar events={...} />`. Each occurrence keeps
|
|
688
|
+
* the original event's duration and fields (minus `recurrence`).
|
|
689
|
+
*/
|
|
690
|
+
function expandRecurringEvents(events, rangeStart, rangeEnd) {
|
|
691
|
+
const out = [];
|
|
692
|
+
for (const event of events) {
|
|
693
|
+
if (!event.recurrence) {
|
|
694
|
+
out.push(event);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const durationMs = event.end.getTime() - event.start.getTime();
|
|
698
|
+
for (const start of occurrenceStarts(event.start, event.recurrence, rangeEnd)) {
|
|
699
|
+
if (start.getTime() + durationMs < rangeStart.getTime()) continue;
|
|
700
|
+
out.push(instanceAt(event, start, durationMs));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/utils/timezone.ts
|
|
707
|
+
/**
|
|
708
|
+
* Reinterpret an instant as its wall-clock time in `timeZone`, returned as a
|
|
709
|
+
* device-local `Date` whose fields (hours, minutes, …) read back as that zone's
|
|
710
|
+
* clock. The calendar lays events out from `getHours()`/`getMinutes()`, so
|
|
711
|
+
* passing zoned dates makes it render in `timeZone` regardless of the device.
|
|
712
|
+
*
|
|
713
|
+
* DST-correct via `Intl` (available on modern React Native Hermes/JSC and the
|
|
714
|
+
* web). The result is for display/layout only; it no longer points at the
|
|
715
|
+
* original UTC instant, so don't round-trip it back to a real time.
|
|
716
|
+
*/
|
|
717
|
+
function toZonedTime(date, timeZone) {
|
|
718
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
719
|
+
timeZone,
|
|
720
|
+
hourCycle: "h23",
|
|
721
|
+
year: "numeric",
|
|
722
|
+
month: "2-digit",
|
|
723
|
+
day: "2-digit",
|
|
724
|
+
hour: "2-digit",
|
|
725
|
+
minute: "2-digit",
|
|
726
|
+
second: "2-digit"
|
|
727
|
+
}).formatToParts(date);
|
|
728
|
+
const field = (type) => {
|
|
729
|
+
const part = parts.find((p) => p.type === type);
|
|
730
|
+
return part ? Number(part.value) : 0;
|
|
731
|
+
};
|
|
732
|
+
const hour = field("hour") % 24;
|
|
733
|
+
return new Date(field("year"), field("month") - 1, field("day"), hour, field("minute"), field("second"), date.getMilliseconds());
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Map every event's `start`/`end` through {@link toZonedTime} so the calendar
|
|
737
|
+
* displays them in `timeZone`. Other fields are preserved. Memoize the result
|
|
738
|
+
* (e.g. with `useMemo`) since it allocates new dates.
|
|
739
|
+
*/
|
|
740
|
+
function eventsInTimeZone(events, timeZone) {
|
|
741
|
+
return events.map((event) => ({
|
|
742
|
+
...event,
|
|
743
|
+
start: toZonedTime(event.start, timeZone),
|
|
744
|
+
end: toZonedTime(event.end, timeZone)
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
exports.CalendarSelectionProvider = CalendarSelectionProvider;
|
|
749
|
+
exports.MIN_BOX_HEIGHT_FOR_TIME = MIN_BOX_HEIGHT_FOR_TIME;
|
|
750
|
+
exports.bandRounding = bandRounding;
|
|
751
|
+
exports.buildMonthGrid = buildMonthGrid;
|
|
752
|
+
exports.buildMonthWeeks = buildMonthWeeks;
|
|
753
|
+
exports.cellRangeFromDrag = cellRangeFromDrag;
|
|
754
|
+
exports.closedHourBands = closedHourBands;
|
|
755
|
+
exports.compareDayEvents = compareDayEvents;
|
|
756
|
+
exports.darkColors = darkColors;
|
|
757
|
+
exports.dayBadgeKind = dayBadgeKind;
|
|
758
|
+
exports.daySelectionState = daySelectionState;
|
|
759
|
+
exports.eventAccessibilityLabel = eventAccessibilityLabel;
|
|
760
|
+
exports.eventChipLayout = eventChipLayout;
|
|
761
|
+
exports.eventDayKeys = eventDayKeys;
|
|
762
|
+
exports.eventTimeLabel = eventTimeLabel;
|
|
763
|
+
exports.eventsInTimeZone = eventsInTimeZone;
|
|
764
|
+
exports.expandRecurringEvents = expandRecurringEvents;
|
|
765
|
+
exports.formatHour = formatHour;
|
|
766
|
+
exports.getIsToday = getIsToday;
|
|
767
|
+
exports.getViewDays = getViewDays;
|
|
768
|
+
exports.getWeekDays = getWeekDays;
|
|
769
|
+
exports.groupEventsByDay = groupEventsByDay;
|
|
770
|
+
exports.isAllDayEvent = isAllDayEvent;
|
|
771
|
+
exports.isDateSelectable = isDateSelectable;
|
|
772
|
+
exports.isRangeEndpoint = isRangeEndpoint;
|
|
773
|
+
exports.isSameCalendarDay = isSameCalendarDay;
|
|
774
|
+
exports.isTimeVisibleAtHeight = isTimeVisibleAtHeight;
|
|
775
|
+
exports.isWeekend = isWeekend;
|
|
776
|
+
exports.isWithinDateRange = isWithinDateRange;
|
|
777
|
+
exports.layoutDayEvents = layoutDayEvents;
|
|
778
|
+
exports.lightColors = lightColors;
|
|
779
|
+
exports.minutesIntoDay = minutesIntoDay;
|
|
780
|
+
exports.monthEventCapacity = monthEventCapacity;
|
|
781
|
+
exports.monthVisibleCount = monthVisibleCount;
|
|
782
|
+
exports.nextDateRange = nextDateRange;
|
|
783
|
+
exports.rangeBandKind = rangeBandKind;
|
|
784
|
+
exports.resolveDraggedBounds = resolveDraggedBounds;
|
|
785
|
+
exports.shiftMinutes = shiftMinutes;
|
|
786
|
+
exports.snapDeltaMinutes = snapDeltaMinutes;
|
|
787
|
+
exports.titleEllipsizeMode = titleEllipsizeMode;
|
|
788
|
+
exports.titleNumberOfLines = titleNumberOfLines;
|
|
789
|
+
exports.toZonedTime = toZonedTime;
|
|
790
|
+
exports.useCalendarSelection = useCalendarSelection;
|
|
791
|
+
exports.useDateRange = useDateRange;
|
|
792
|
+
exports.useMonthGrid = useMonthGrid;
|
|
793
|
+
exports.viewDayCount = viewDayCount;
|
|
794
|
+
exports.weekDaysCount = weekDaysCount;
|