@lazerlen/legend-calendar 1.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/.eslintrc.js +29 -0
- package/.turbo/turbo-build.log +19 -0
- package/.turbo/turbo-lint.log +14 -0
- package/.turbo/turbo-test.log +261 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +127 -0
- package/dist/index.d.mts +679 -0
- package/dist/index.d.ts +679 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
- package/src/components/Calendar.stories.tsx +226 -0
- package/src/components/Calendar.tsx +224 -0
- package/src/components/CalendarItemDay.tsx +385 -0
- package/src/components/CalendarItemEmpty.tsx +30 -0
- package/src/components/CalendarItemWeekName.tsx +67 -0
- package/src/components/CalendarList.stories.tsx +326 -0
- package/src/components/CalendarList.tsx +373 -0
- package/src/components/CalendarRowMonth.tsx +62 -0
- package/src/components/CalendarRowWeek.tsx +46 -0
- package/src/components/CalendarThemeProvider.tsx +43 -0
- package/src/components/HStack.tsx +67 -0
- package/src/components/VStack.tsx +67 -0
- package/src/components/index.ts +108 -0
- package/src/developer/decorators.tsx +54 -0
- package/src/developer/loggginHandler.tsx +6 -0
- package/src/developer/useRenderCount.ts +7 -0
- package/src/helpers/dates.test.ts +567 -0
- package/src/helpers/dates.ts +163 -0
- package/src/helpers/functions.ts +327 -0
- package/src/helpers/numbers.ts +11 -0
- package/src/helpers/strings.ts +2 -0
- package/src/helpers/tokens.ts +71 -0
- package/src/helpers/types.ts +3 -0
- package/src/hooks/useCalendar.test.ts +381 -0
- package/src/hooks/useCalendar.ts +351 -0
- package/src/hooks/useCalendarList.test.ts +382 -0
- package/src/hooks/useCalendarList.tsx +291 -0
- package/src/hooks/useDateRange.test.ts +128 -0
- package/src/hooks/useDateRange.ts +94 -0
- package/src/hooks/useOptimizedDayMetadata.test.ts +582 -0
- package/src/hooks/useOptimizedDayMetadata.ts +93 -0
- package/src/hooks/useTheme.ts +14 -0
- package/src/index.ts +24 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import type { DayState } from "@/components/CalendarItemDay";
|
|
4
|
+
import {
|
|
5
|
+
addDays,
|
|
6
|
+
endOfMonth,
|
|
7
|
+
fromDateId,
|
|
8
|
+
isWeekend,
|
|
9
|
+
startOfMonth,
|
|
10
|
+
startOfWeek,
|
|
11
|
+
subDays,
|
|
12
|
+
toDateId,
|
|
13
|
+
} from "@/helpers/dates";
|
|
14
|
+
import { range } from "@/helpers/numbers";
|
|
15
|
+
|
|
16
|
+
const getNumberOfEmptyCellsAtStart = (
|
|
17
|
+
month: Date,
|
|
18
|
+
firstDayOfWeek: "sunday" | "monday"
|
|
19
|
+
) => {
|
|
20
|
+
const startOfMonthDay = month.getDay();
|
|
21
|
+
|
|
22
|
+
if (firstDayOfWeek === "sunday") {
|
|
23
|
+
return startOfMonthDay;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return startOfMonthDay === 0 ? 6 : startOfMonthDay - 1;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** All fields that affects the day's state. */
|
|
30
|
+
interface CalendarDayStateFields {
|
|
31
|
+
/** Is this day disabled? */
|
|
32
|
+
isDisabled: boolean;
|
|
33
|
+
/** Is this the current day? */
|
|
34
|
+
isToday: boolean;
|
|
35
|
+
/** Is this the start of a range? */
|
|
36
|
+
isStartOfRange: boolean;
|
|
37
|
+
/** Is this the end of a range? */
|
|
38
|
+
isEndOfRange: boolean;
|
|
39
|
+
/** The state of the day */
|
|
40
|
+
state: DayState;
|
|
41
|
+
/** Is the range valid (has both start and end dates set)? */
|
|
42
|
+
isRangeValid: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The type of each day in the calendar. Has a few pre-computed properties to
|
|
47
|
+
* help increase re-rendering performance.
|
|
48
|
+
*/
|
|
49
|
+
export type CalendarDayMetadata = {
|
|
50
|
+
date: Date;
|
|
51
|
+
/** The day displayed in the desired format from `calendarDayFormat` */
|
|
52
|
+
displayLabel: string;
|
|
53
|
+
/** Does this day belong to a different month? */
|
|
54
|
+
isDifferentMonth: boolean;
|
|
55
|
+
/** Is this the last day of the month? */
|
|
56
|
+
isEndOfMonth: boolean;
|
|
57
|
+
/** Is this the last day of the week? */
|
|
58
|
+
isEndOfWeek: boolean;
|
|
59
|
+
/** Is this the first day of the month? */
|
|
60
|
+
isStartOfMonth: boolean;
|
|
61
|
+
/** Is this the first day of the week? */
|
|
62
|
+
isStartOfWeek: boolean;
|
|
63
|
+
/** Is this day part of the weekend? */
|
|
64
|
+
isWeekend: boolean;
|
|
65
|
+
|
|
66
|
+
/** The ID of this date is the `YYYY-MM-DD` representation */
|
|
67
|
+
id: string;
|
|
68
|
+
} & CalendarDayStateFields;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* An active date range to highlight in the calendar.
|
|
72
|
+
*/
|
|
73
|
+
export interface CalendarActiveDateRange {
|
|
74
|
+
startId?: string;
|
|
75
|
+
endId?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UseCalendarParams {
|
|
79
|
+
/**
|
|
80
|
+
* The calendar's month. It can be any date within the month, since it gets
|
|
81
|
+
* normalized to the first day of the month.
|
|
82
|
+
*
|
|
83
|
+
* **Tip**: To convert to date ID, use `toDateId(date)`.
|
|
84
|
+
*/
|
|
85
|
+
calendarMonthId: string;
|
|
86
|
+
/**
|
|
87
|
+
* The minimum date allowed to be selected (inclusive). Dates earlier than
|
|
88
|
+
* this will be disabled.
|
|
89
|
+
*
|
|
90
|
+
* **Tip**: To convert to date ID, use `toDateId(date)`.
|
|
91
|
+
*/
|
|
92
|
+
calendarMinDateId?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The maximum date allowed to be selected (inclusive). Dates later than this
|
|
95
|
+
* will be disabled.
|
|
96
|
+
*
|
|
97
|
+
* **Tip**: To convert to date ID, use `toDateId(date)`.
|
|
98
|
+
*/
|
|
99
|
+
calendarMaxDateId?: string;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The locale to use for the date formatting. If you're using custom
|
|
103
|
+
* formatting functions, this value will be forwarded as the second argument.
|
|
104
|
+
* @defaultValue "en-US"
|
|
105
|
+
*/
|
|
106
|
+
calendarFormatLocale?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A custom function to override the default month format ("January 2022").
|
|
110
|
+
*/
|
|
111
|
+
getCalendarMonthFormat?: (date: Date, locale: string) => string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A custom function to override the default month format ("S", "M", "T").
|
|
115
|
+
*/
|
|
116
|
+
getCalendarWeekDayFormat?: (date: Date, locale: string) => string;
|
|
117
|
+
/**
|
|
118
|
+
* A custom function to override the default day format ("1", "9", "17").
|
|
119
|
+
*/
|
|
120
|
+
getCalendarDayFormat?: (date: Date, locale: string) => string;
|
|
121
|
+
/**
|
|
122
|
+
* The day of the week to start the calendar with.
|
|
123
|
+
* @defaultValue "sunday"
|
|
124
|
+
*/
|
|
125
|
+
calendarFirstDayOfWeek?: "sunday" | "monday";
|
|
126
|
+
/**
|
|
127
|
+
* The active date ranges to highlight in the calendar.
|
|
128
|
+
*/
|
|
129
|
+
calendarActiveDateRanges?: CalendarActiveDateRange[];
|
|
130
|
+
/**
|
|
131
|
+
* The disabled date IDs. Dates in this list will be in the `disabled` state
|
|
132
|
+
* unless they are part of an active range.
|
|
133
|
+
*/
|
|
134
|
+
calendarDisabledDateIds?: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type GetStateFields = Pick<
|
|
138
|
+
UseCalendarParams,
|
|
139
|
+
| "calendarActiveDateRanges"
|
|
140
|
+
| "calendarMinDateId"
|
|
141
|
+
| "calendarMaxDateId"
|
|
142
|
+
| "calendarDisabledDateIds"
|
|
143
|
+
> & {
|
|
144
|
+
todayId?: string;
|
|
145
|
+
id: string;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Computes the state fields for a given date.
|
|
150
|
+
*/
|
|
151
|
+
export const getStateFields = ({
|
|
152
|
+
todayId,
|
|
153
|
+
id,
|
|
154
|
+
calendarActiveDateRanges,
|
|
155
|
+
calendarMinDateId,
|
|
156
|
+
calendarMaxDateId,
|
|
157
|
+
calendarDisabledDateIds,
|
|
158
|
+
}: GetStateFields): CalendarDayStateFields => {
|
|
159
|
+
const activeRange = calendarActiveDateRanges?.find(({ startId, endId }) => {
|
|
160
|
+
// Regular range
|
|
161
|
+
if (startId && endId) {
|
|
162
|
+
return id >= startId && id <= endId;
|
|
163
|
+
} else if (startId) {
|
|
164
|
+
return id === startId;
|
|
165
|
+
} else if (endId) {
|
|
166
|
+
return id === endId;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const isRangeValid =
|
|
172
|
+
activeRange?.startId !== undefined && activeRange.endId !== undefined;
|
|
173
|
+
|
|
174
|
+
const isDisabled =
|
|
175
|
+
(calendarDisabledDateIds?.includes(id) ||
|
|
176
|
+
(calendarMinDateId && id < calendarMinDateId) ||
|
|
177
|
+
(calendarMaxDateId && id > calendarMaxDateId)) === true;
|
|
178
|
+
|
|
179
|
+
const isToday = todayId === id;
|
|
180
|
+
|
|
181
|
+
const state: DayState = activeRange
|
|
182
|
+
? ("active" as const)
|
|
183
|
+
: isDisabled
|
|
184
|
+
? "disabled"
|
|
185
|
+
: isToday
|
|
186
|
+
? "today"
|
|
187
|
+
: "idle";
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
isStartOfRange: id === activeRange?.startId,
|
|
191
|
+
isEndOfRange: id === activeRange?.endId,
|
|
192
|
+
isRangeValid,
|
|
193
|
+
state,
|
|
194
|
+
isDisabled,
|
|
195
|
+
isToday,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const getBaseCalendarMonthFormat = (date: Date, locale: string) => {
|
|
200
|
+
return new Intl.DateTimeFormat(locale, {
|
|
201
|
+
month: "long",
|
|
202
|
+
year: "numeric",
|
|
203
|
+
}).format(date);
|
|
204
|
+
};
|
|
205
|
+
const getBaseCalendarWeekDayFormat = (date: Date, locale: string) => {
|
|
206
|
+
return new Intl.DateTimeFormat(locale, {
|
|
207
|
+
weekday: "narrow",
|
|
208
|
+
}).format(date);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const getBaseCalendarDayFormat = (date: Date, locale: string) => {
|
|
212
|
+
return new Intl.DateTimeFormat(locale, {
|
|
213
|
+
day: "numeric",
|
|
214
|
+
}).format(date);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Builds a calendar based on the given parameters.
|
|
219
|
+
*/
|
|
220
|
+
export const buildCalendar = (params: UseCalendarParams) => {
|
|
221
|
+
const {
|
|
222
|
+
calendarMonthId: monthId,
|
|
223
|
+
calendarFirstDayOfWeek = "sunday",
|
|
224
|
+
calendarFormatLocale = "en-US",
|
|
225
|
+
getCalendarMonthFormat = getBaseCalendarMonthFormat,
|
|
226
|
+
getCalendarWeekDayFormat = getBaseCalendarWeekDayFormat,
|
|
227
|
+
getCalendarDayFormat = getBaseCalendarDayFormat,
|
|
228
|
+
} = params;
|
|
229
|
+
|
|
230
|
+
const month = fromDateId(monthId);
|
|
231
|
+
const monthStart = startOfMonth(month);
|
|
232
|
+
const monthStartId = toDateId(monthStart);
|
|
233
|
+
const monthEnd = endOfMonth(month);
|
|
234
|
+
const monthEndId = toDateId(monthEnd);
|
|
235
|
+
|
|
236
|
+
const emptyDaysAtStart = getNumberOfEmptyCellsAtStart(
|
|
237
|
+
monthStart,
|
|
238
|
+
calendarFirstDayOfWeek
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const startOfWeekIndex = calendarFirstDayOfWeek === "sunday" ? 0 : 1;
|
|
242
|
+
const endOfWeekIndex = calendarFirstDayOfWeek === "sunday" ? 6 : 0;
|
|
243
|
+
|
|
244
|
+
const todayId = toDateId(new Date());
|
|
245
|
+
|
|
246
|
+
// The first day to iterate is the first day of the month minus the empty days at the start
|
|
247
|
+
let dayToIterate = subDays(monthStart, emptyDaysAtStart);
|
|
248
|
+
|
|
249
|
+
const weeksList: CalendarDayMetadata[][] = [
|
|
250
|
+
[
|
|
251
|
+
...range(1, emptyDaysAtStart).map((): CalendarDayMetadata => {
|
|
252
|
+
const id = toDateId(dayToIterate);
|
|
253
|
+
|
|
254
|
+
const dayShape: CalendarDayMetadata = {
|
|
255
|
+
date: dayToIterate,
|
|
256
|
+
displayLabel: getCalendarDayFormat(
|
|
257
|
+
dayToIterate,
|
|
258
|
+
calendarFormatLocale
|
|
259
|
+
),
|
|
260
|
+
id,
|
|
261
|
+
isDifferentMonth: true,
|
|
262
|
+
isEndOfMonth: false,
|
|
263
|
+
isEndOfWeek: dayToIterate.getDay() === endOfWeekIndex,
|
|
264
|
+
isStartOfMonth: false,
|
|
265
|
+
isStartOfWeek: dayToIterate.getDay() === startOfWeekIndex,
|
|
266
|
+
isWeekend: isWeekend(dayToIterate),
|
|
267
|
+
...getStateFields({
|
|
268
|
+
...params,
|
|
269
|
+
todayId,
|
|
270
|
+
id,
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
dayToIterate = addDays(dayToIterate, 1);
|
|
274
|
+
return dayShape;
|
|
275
|
+
}),
|
|
276
|
+
],
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// By this point, we're back at the start of the month
|
|
280
|
+
while (dayToIterate.getMonth() === monthStart.getMonth()) {
|
|
281
|
+
const currentWeek = weeksList[weeksList.length - 1];
|
|
282
|
+
if (currentWeek.length === 7) {
|
|
283
|
+
weeksList.push([]);
|
|
284
|
+
}
|
|
285
|
+
const id = toDateId(dayToIterate);
|
|
286
|
+
weeksList[weeksList.length - 1].push({
|
|
287
|
+
date: dayToIterate,
|
|
288
|
+
displayLabel: getCalendarDayFormat(dayToIterate, calendarFormatLocale),
|
|
289
|
+
id,
|
|
290
|
+
isDifferentMonth: false,
|
|
291
|
+
isEndOfMonth: id === monthEndId,
|
|
292
|
+
isEndOfWeek: dayToIterate.getDay() === endOfWeekIndex,
|
|
293
|
+
isStartOfMonth: id === monthStartId,
|
|
294
|
+
isStartOfWeek: dayToIterate.getDay() === startOfWeekIndex,
|
|
295
|
+
isWeekend: isWeekend(dayToIterate),
|
|
296
|
+
...getStateFields({
|
|
297
|
+
...params,
|
|
298
|
+
todayId,
|
|
299
|
+
id,
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
dayToIterate = addDays(dayToIterate, 1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Once all the days of the month have been added, we need to add the empty days at the end
|
|
306
|
+
const lastWeek = weeksList[weeksList.length - 1];
|
|
307
|
+
const emptyDaysAtEnd = 7 - lastWeek.length;
|
|
308
|
+
lastWeek.push(
|
|
309
|
+
...range(1, emptyDaysAtEnd).map(() => {
|
|
310
|
+
const id = toDateId(dayToIterate);
|
|
311
|
+
const dayShape: CalendarDayMetadata = {
|
|
312
|
+
date: dayToIterate,
|
|
313
|
+
displayLabel: getCalendarDayFormat(dayToIterate, calendarFormatLocale),
|
|
314
|
+
id,
|
|
315
|
+
isDifferentMonth: true,
|
|
316
|
+
isEndOfMonth: false,
|
|
317
|
+
isEndOfWeek: dayToIterate.getDay() === endOfWeekIndex,
|
|
318
|
+
isStartOfMonth: false,
|
|
319
|
+
isStartOfWeek: dayToIterate.getDay() === startOfWeekIndex,
|
|
320
|
+
isWeekend: isWeekend(dayToIterate),
|
|
321
|
+
...getStateFields({
|
|
322
|
+
...params,
|
|
323
|
+
todayId,
|
|
324
|
+
id,
|
|
325
|
+
}),
|
|
326
|
+
};
|
|
327
|
+
dayToIterate = addDays(dayToIterate, 1);
|
|
328
|
+
return dayShape;
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const startOfWeekDate = startOfWeek(month, calendarFirstDayOfWeek);
|
|
333
|
+
const weekDaysList = range(1, 7).map((i) =>
|
|
334
|
+
getCalendarWeekDayFormat(
|
|
335
|
+
addDays(startOfWeekDate, i - 1),
|
|
336
|
+
calendarFormatLocale
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
weeksList,
|
|
342
|
+
calendarRowMonth: getCalendarMonthFormat(month, calendarFormatLocale),
|
|
343
|
+
weekDaysList,
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Returns a memoized calendar based on the given parameters.
|
|
349
|
+
*/
|
|
350
|
+
export const useCalendar = (params: UseCalendarParams) =>
|
|
351
|
+
useMemo(() => buildCalendar(params), [params]);
|