@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.
Files changed (48) hide show
  1. package/.eslintrc.js +29 -0
  2. package/.turbo/turbo-build.log +19 -0
  3. package/.turbo/turbo-lint.log +14 -0
  4. package/.turbo/turbo-test.log +261 -0
  5. package/.turbo/turbo-typecheck.log +1 -0
  6. package/CHANGELOG.md +127 -0
  7. package/dist/index.d.mts +679 -0
  8. package/dist/index.d.ts +679 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +47 -0
  14. package/src/components/Calendar.stories.tsx +226 -0
  15. package/src/components/Calendar.tsx +224 -0
  16. package/src/components/CalendarItemDay.tsx +385 -0
  17. package/src/components/CalendarItemEmpty.tsx +30 -0
  18. package/src/components/CalendarItemWeekName.tsx +67 -0
  19. package/src/components/CalendarList.stories.tsx +326 -0
  20. package/src/components/CalendarList.tsx +373 -0
  21. package/src/components/CalendarRowMonth.tsx +62 -0
  22. package/src/components/CalendarRowWeek.tsx +46 -0
  23. package/src/components/CalendarThemeProvider.tsx +43 -0
  24. package/src/components/HStack.tsx +67 -0
  25. package/src/components/VStack.tsx +67 -0
  26. package/src/components/index.ts +108 -0
  27. package/src/developer/decorators.tsx +54 -0
  28. package/src/developer/loggginHandler.tsx +6 -0
  29. package/src/developer/useRenderCount.ts +7 -0
  30. package/src/helpers/dates.test.ts +567 -0
  31. package/src/helpers/dates.ts +163 -0
  32. package/src/helpers/functions.ts +327 -0
  33. package/src/helpers/numbers.ts +11 -0
  34. package/src/helpers/strings.ts +2 -0
  35. package/src/helpers/tokens.ts +71 -0
  36. package/src/helpers/types.ts +3 -0
  37. package/src/hooks/useCalendar.test.ts +381 -0
  38. package/src/hooks/useCalendar.ts +351 -0
  39. package/src/hooks/useCalendarList.test.ts +382 -0
  40. package/src/hooks/useCalendarList.tsx +291 -0
  41. package/src/hooks/useDateRange.test.ts +128 -0
  42. package/src/hooks/useDateRange.ts +94 -0
  43. package/src/hooks/useOptimizedDayMetadata.test.ts +582 -0
  44. package/src/hooks/useOptimizedDayMetadata.ts +93 -0
  45. package/src/hooks/useTheme.ts +14 -0
  46. package/src/index.ts +24 -0
  47. package/tsconfig.json +13 -0
  48. 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]);