@lotics/ui 1.12.0 → 1.13.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,679 @@
1
+ import React, { useCallback, useImperativeHandle, useMemo, useState } from "react";
2
+ import { Pressable, StyleSheet, View } from "react-native";
3
+ import { Text } from "./text";
4
+ import { colors } from "./colors";
5
+ import { useScreenSize } from "./use_screen_size";
6
+ import { Picker, PickerOption } from "./picker";
7
+ import { IconButton } from "./icon_button";
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export type CalendarSingleValue = Date | null;
14
+ export type CalendarRangeValue = { start: Date | null; end: Date | null };
15
+
16
+ /** Day of week: 0 = Sunday, 1 = Monday, ..., 6 = Saturday */
17
+ export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;
18
+
19
+ export type CalendarValue<TMode extends "single" | "range"> = TMode extends "single"
20
+ ? CalendarSingleValue
21
+ : CalendarRangeValue;
22
+
23
+ export interface CalendarRef {
24
+ /** Navigate calendar view to a specific month */
25
+ navigateToMonth: (year: number, month: number) => void;
26
+ }
27
+
28
+ interface CalendarBaseProps {
29
+ /** First day of the week (0 = Sunday, 1 = Monday, ..., 6 = Saturday). Default: 1 (Monday) */
30
+ firstDayOfWeek?: DayOfWeek;
31
+ /** BCP-47 locale for weekday/month names. Default: "en-US". */
32
+ locale?: string;
33
+ /** Ref to access calendar methods like navigateToMonth */
34
+ ref?: React.Ref<CalendarRef>;
35
+ }
36
+
37
+ export type CalendarProps<TMode extends "single" | "range" = "single" | "range"> =
38
+ CalendarBaseProps &
39
+ (TMode extends "single"
40
+ ? {
41
+ mode: "single";
42
+ value: CalendarSingleValue;
43
+ onValueChange: (value: CalendarSingleValue) => void;
44
+ }
45
+ : TMode extends "range"
46
+ ? {
47
+ mode: "range";
48
+ value: CalendarRangeValue;
49
+ onValueChange: (value: CalendarRangeValue) => void;
50
+ }
51
+ : {
52
+ mode: "single" | "range";
53
+ value: CalendarSingleValue | CalendarRangeValue;
54
+ onValueChange: (value: CalendarSingleValue | CalendarRangeValue) => void;
55
+ });
56
+
57
+ interface CalendarMonthProps {
58
+ year: number;
59
+ month: number;
60
+ selectedDate: Date | null;
61
+ rangeStart: Date | null;
62
+ rangeEnd: Date | null;
63
+ isRange: boolean;
64
+ onDateSelect: (date: Date) => void;
65
+ onMonthChange: (year: number, month: number) => void;
66
+ showNavigation?: boolean;
67
+ /** Show only left arrow (for range mode left calendar) */
68
+ showLeftArrow?: boolean;
69
+ /** Show only right arrow (for range mode right calendar) */
70
+ showRightArrow?: boolean;
71
+ /** Callback for left arrow click (for range mode) */
72
+ onPrevMonth?: () => void;
73
+ /** Callback for right arrow click (for range mode) */
74
+ onNextMonth?: () => void;
75
+ /** First day of the week (0 = Sunday, 1 = Monday, ..., 6 = Saturday). Default: 1 (Monday) */
76
+ firstDayOfWeek?: DayOfWeek;
77
+ /** BCP-47 locale for weekday/month names. */
78
+ locale: string;
79
+ }
80
+
81
+ // =============================================================================
82
+ // Helper Functions
83
+ // =============================================================================
84
+
85
+ /** BCP-47 locale used when a caller does not supply one. */
86
+ const DEFAULT_LOCALE = "en-US";
87
+
88
+ interface LocalizedNames {
89
+ /** Weekday short names, index 0 = Sunday. */
90
+ weekdays: string[];
91
+ /** Month long names, index 0 = January. */
92
+ months: string[];
93
+ /** Month names as Picker options keyed by month index. */
94
+ monthOptions: PickerOption<string>[];
95
+ /** Full date string for a day cell's accessible name. */
96
+ formatDayLabel: (date: Date) => string;
97
+ }
98
+
99
+ // Jan 1 2017 was a Sunday — formatting Jan 1..7 yields Sun..Sat. Sample dates are
100
+ // built in local time so the runtime timezone can't shift the rendered name.
101
+ function buildLocalizedNames(locale: string): LocalizedNames {
102
+ const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
103
+ const weekdays = [1, 2, 3, 4, 5, 6, 7].map((day) =>
104
+ weekdayFormatter.format(new Date(2017, 0, day)),
105
+ );
106
+ const monthFormatter = new Intl.DateTimeFormat(locale, { month: "long" });
107
+ const months = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((month) =>
108
+ monthFormatter.format(new Date(2017, month, 1)),
109
+ );
110
+ const dayLabelFormatter = new Intl.DateTimeFormat(locale, { dateStyle: "full" });
111
+ return {
112
+ weekdays,
113
+ months,
114
+ monthOptions: months.map((month, index) => ({ label: month, value: String(index) })),
115
+ formatDayLabel: (date) => dayLabelFormatter.format(date),
116
+ };
117
+ }
118
+
119
+ // Intl formatters are costly to construct; derive each locale's names once.
120
+ const localizedNamesCache = new Map<string, LocalizedNames>();
121
+
122
+ function getLocalizedNames(locale: string): LocalizedNames {
123
+ let names = localizedNamesCache.get(locale);
124
+ if (!names) {
125
+ names = buildLocalizedNames(locale);
126
+ localizedNamesCache.set(locale, names);
127
+ }
128
+ return names;
129
+ }
130
+
131
+ function getDaysOfWeekLabels(weekdays: string[], firstDayOfWeek: DayOfWeek): string[] {
132
+ const result: string[] = [];
133
+ for (let i = 0; i < 7; i++) {
134
+ result.push(weekdays[(firstDayOfWeek + i) % 7]);
135
+ }
136
+ return result;
137
+ }
138
+
139
+ function getYearOptions(currentYear: number): PickerOption<string>[] {
140
+ const options: PickerOption<string>[] = [];
141
+ for (let year = currentYear - 10; year <= currentYear + 10; year++) {
142
+ options.push({ label: String(year), value: String(year) });
143
+ }
144
+ return options;
145
+ }
146
+
147
+ function getDaysInMonth(year: number, month: number): number {
148
+ return new Date(year, month + 1, 0).getDate();
149
+ }
150
+
151
+ function getFirstDayOfMonth(year: number, month: number, firstDayOfWeek: DayOfWeek): number {
152
+ const day = new Date(year, month, 1).getDay();
153
+ // Calculate offset based on first day of week
154
+ return (day - firstDayOfWeek + 7) % 7;
155
+ }
156
+
157
+ function isSameDay(date1: Date | null, date2: Date | null): boolean {
158
+ if (!date1 || !date2) return false;
159
+ return (
160
+ date1.getFullYear() === date2.getFullYear() &&
161
+ date1.getMonth() === date2.getMonth() &&
162
+ date1.getDate() === date2.getDate()
163
+ );
164
+ }
165
+
166
+ function isInRange(date: Date, start: Date | null, end: Date | null): boolean {
167
+ if (!start || !end) return false;
168
+ const time = date.getTime();
169
+ const startTime = start.getTime();
170
+ const endTime = end.getTime();
171
+ return time > Math.min(startTime, endTime) && time < Math.max(startTime, endTime);
172
+ }
173
+
174
+ function isToday(date: Date): boolean {
175
+ const today = new Date();
176
+ return isSameDay(date, today);
177
+ }
178
+
179
+ // =============================================================================
180
+ // Calendar Month Component
181
+ // =============================================================================
182
+
183
+ function CalendarMonth(props: CalendarMonthProps) {
184
+ const {
185
+ year,
186
+ month,
187
+ selectedDate,
188
+ rangeStart,
189
+ rangeEnd,
190
+ isRange,
191
+ onDateSelect,
192
+ onMonthChange,
193
+ showNavigation = true,
194
+ showLeftArrow = false,
195
+ showRightArrow = false,
196
+ onPrevMonth,
197
+ onNextMonth,
198
+ firstDayOfWeek = 1, // Default to Monday
199
+ locale,
200
+ } = props;
201
+
202
+ const names = getLocalizedNames(locale);
203
+ const daysInMonth = getDaysInMonth(year, month);
204
+ const firstDayOffset = getFirstDayOfMonth(year, month, firstDayOfWeek);
205
+ const daysOfWeekLabels = useMemo(
206
+ () => getDaysOfWeekLabels(names.weekdays, firstDayOfWeek),
207
+ [names, firstDayOfWeek],
208
+ );
209
+
210
+ const handlePrevMonth = useCallback(() => {
211
+ if (month === 0) {
212
+ onMonthChange(year - 1, 11);
213
+ } else {
214
+ onMonthChange(year, month - 1);
215
+ }
216
+ }, [year, month, onMonthChange]);
217
+
218
+ const handleNextMonth = useCallback(() => {
219
+ if (month === 11) {
220
+ onMonthChange(year + 1, 0);
221
+ } else {
222
+ onMonthChange(year, month + 1);
223
+ }
224
+ }, [year, month, onMonthChange]);
225
+
226
+ const handleMonthPickerChange = useCallback(
227
+ (newMonth: string) => {
228
+ onMonthChange(year, parseInt(newMonth, 10));
229
+ },
230
+ [year, onMonthChange],
231
+ );
232
+
233
+ const handleYearPickerChange = useCallback(
234
+ (newYear: string) => {
235
+ onMonthChange(parseInt(newYear, 10), month);
236
+ },
237
+ [month, onMonthChange],
238
+ );
239
+
240
+ const yearOptions = useMemo(() => getYearOptions(year), [year]);
241
+
242
+ const weeks = useMemo(() => {
243
+ const result: (number | null)[][] = [];
244
+ let currentWeek: (number | null)[] = [];
245
+
246
+ // Add empty cells for days before the first day of month
247
+ for (let i = 0; i < firstDayOffset; i++) {
248
+ currentWeek.push(null);
249
+ }
250
+
251
+ // Add days of the month
252
+ for (let day = 1; day <= daysInMonth; day++) {
253
+ currentWeek.push(day);
254
+ if (currentWeek.length === 7) {
255
+ result.push(currentWeek);
256
+ currentWeek = [];
257
+ }
258
+ }
259
+
260
+ // Add empty cells for remaining days
261
+ if (currentWeek.length > 0) {
262
+ while (currentWeek.length < 7) {
263
+ currentWeek.push(null);
264
+ }
265
+ result.push(currentWeek);
266
+ }
267
+
268
+ return result;
269
+ }, [daysInMonth, firstDayOffset]);
270
+
271
+ return (
272
+ <View style={styles.monthContainer}>
273
+ {/* Header with month/year and navigation */}
274
+ <View style={styles.monthHeader}>
275
+ {showNavigation && (
276
+ <IconButton
277
+ icon="chevron-left"
278
+ accessibilityLabel="Previous month"
279
+ onPress={handlePrevMonth}
280
+ />
281
+ )}
282
+ {showLeftArrow && onPrevMonth && (
283
+ <IconButton
284
+ icon="chevron-left"
285
+ accessibilityLabel="Previous month"
286
+ onPress={onPrevMonth}
287
+ />
288
+ )}
289
+ {showNavigation ? (
290
+ <View style={styles.pickerContainer}>
291
+ <Picker
292
+ value={String(month)}
293
+ options={names.monthOptions}
294
+ onValueChange={handleMonthPickerChange}
295
+ />
296
+ <Picker
297
+ value={String(year)}
298
+ options={yearOptions}
299
+ onValueChange={handleYearPickerChange}
300
+ />
301
+ </View>
302
+ ) : (
303
+ <View style={styles.monthYearText}>
304
+ <Text weight="medium" size="sm">
305
+ {names.months[month]} {year}
306
+ </Text>
307
+ </View>
308
+ )}
309
+ {showNavigation && (
310
+ <IconButton
311
+ icon="chevron-right"
312
+ accessibilityLabel="Next month"
313
+ onPress={handleNextMonth}
314
+ />
315
+ )}
316
+ {showRightArrow && onNextMonth && (
317
+ <IconButton
318
+ icon="chevron-right"
319
+ accessibilityLabel="Next month"
320
+ onPress={onNextMonth}
321
+ />
322
+ )}
323
+ </View>
324
+
325
+ {/* Days of week header */}
326
+ <View style={styles.weekHeader}>
327
+ {daysOfWeekLabels.map((day) => (
328
+ <View key={day} style={styles.dayHeaderCell}>
329
+ <Text size="xs" color="zinc-500">
330
+ {day}
331
+ </Text>
332
+ </View>
333
+ ))}
334
+ </View>
335
+
336
+ {/* Calendar grid */}
337
+ {weeks.map((week, weekIndex) => (
338
+ <View key={weekIndex} style={styles.weekRow}>
339
+ {week.map((day, dayIndex) => {
340
+ if (day === null) {
341
+ return <View key={dayIndex} style={styles.dayCell} />;
342
+ }
343
+
344
+ const date = new Date(year, month, day);
345
+ const isSelected = isSameDay(date, selectedDate);
346
+ const isRangeStart = isRange && isSameDay(date, rangeStart);
347
+ const isRangeEnd = isRange && isSameDay(date, rangeEnd);
348
+ const inRange = isRange && isInRange(date, rangeStart, rangeEnd);
349
+ const isTodayDate = isToday(date);
350
+ const isActive = isSelected || isRangeStart || isRangeEnd;
351
+
352
+ return (
353
+ <Pressable
354
+ key={dayIndex}
355
+ accessibilityRole="button"
356
+ accessibilityLabel={names.formatDayLabel(date)}
357
+ accessibilityState={{ selected: isActive }}
358
+ style={({ hovered }) => [
359
+ styles.dayCell,
360
+ inRange && styles.dayCellInRange,
361
+ isActive && styles.dayCellSelected,
362
+ hovered && !isActive && styles.dayCellHovered,
363
+ ]}
364
+ onPress={() => onDateSelect(date)}
365
+ >
366
+ <Text
367
+ size="sm"
368
+ color={isActive ? "inverted" : "default"}
369
+ userSelect="none"
370
+ weight={isTodayDate ? "semibold" : "regular"}
371
+ >
372
+ {day}
373
+ </Text>
374
+ {isTodayDate && (
375
+ <View style={[styles.todayDot, isActive && styles.todayDotInverted]} />
376
+ )}
377
+ </Pressable>
378
+ );
379
+ })}
380
+ </View>
381
+ ))}
382
+ </View>
383
+ );
384
+ }
385
+
386
+ // =============================================================================
387
+ // Main Calendar Component
388
+ // =============================================================================
389
+
390
+ export function Calendar<TMode extends "single" | "range">(
391
+ props: CalendarProps<TMode>,
392
+ ): React.ReactElement {
393
+ const screenSize = useScreenSize();
394
+ const firstDayOfWeek = props.firstDayOfWeek ?? 1; // Default to Monday
395
+ const locale = props.locale ?? DEFAULT_LOCALE;
396
+
397
+ if (props.mode === "single") {
398
+ return (
399
+ <SingleCalendar
400
+ value={props.value as CalendarSingleValue}
401
+ onValueChange={props.onValueChange as (value: CalendarSingleValue) => void}
402
+ firstDayOfWeek={firstDayOfWeek}
403
+ locale={locale}
404
+ />
405
+ );
406
+ }
407
+
408
+ return (
409
+ <RangeCalendar
410
+ ref={props.ref}
411
+ value={props.value as CalendarRangeValue}
412
+ onValueChange={props.onValueChange as (value: CalendarRangeValue) => void}
413
+ isSmall={screenSize.small}
414
+ firstDayOfWeek={firstDayOfWeek}
415
+ locale={locale}
416
+ />
417
+ );
418
+ }
419
+
420
+ // =============================================================================
421
+ // Single Calendar
422
+ // =============================================================================
423
+
424
+ interface SingleCalendarInternalProps {
425
+ value: Date | null;
426
+ onValueChange: (value: Date | null) => void;
427
+ firstDayOfWeek: DayOfWeek;
428
+ locale: string;
429
+ }
430
+
431
+ function SingleCalendar(props: SingleCalendarInternalProps) {
432
+ const { value, onValueChange, firstDayOfWeek, locale } = props;
433
+
434
+ const initialDate = useMemo(() => value || new Date(), [value]);
435
+ const [viewYear, setViewYear] = useState(initialDate.getFullYear());
436
+ const [viewMonth, setViewMonth] = useState(initialDate.getMonth());
437
+
438
+ const handleDateSelect = useCallback(
439
+ (date: Date) => {
440
+ onValueChange(date);
441
+ },
442
+ [onValueChange],
443
+ );
444
+
445
+ const handleMonthChange = useCallback((year: number, month: number) => {
446
+ setViewYear(year);
447
+ setViewMonth(month);
448
+ }, []);
449
+
450
+ return (
451
+ <View style={styles.calendarContainer}>
452
+ <CalendarMonth
453
+ year={viewYear}
454
+ month={viewMonth}
455
+ selectedDate={value}
456
+ rangeStart={null}
457
+ rangeEnd={null}
458
+ isRange={false}
459
+ onDateSelect={handleDateSelect}
460
+ onMonthChange={handleMonthChange}
461
+ firstDayOfWeek={firstDayOfWeek}
462
+ locale={locale}
463
+ />
464
+ </View>
465
+ );
466
+ }
467
+
468
+ // =============================================================================
469
+ // Range Calendar
470
+ // =============================================================================
471
+
472
+ interface RangeCalendarInternalProps {
473
+ value: { start: Date | null; end: Date | null };
474
+ onValueChange: (value: { start: Date | null; end: Date | null }) => void;
475
+ isSmall: boolean;
476
+ firstDayOfWeek: DayOfWeek;
477
+ locale: string;
478
+ ref?: React.Ref<CalendarRef>;
479
+ }
480
+
481
+ function RangeCalendar(props: RangeCalendarInternalProps) {
482
+ const { value, onValueChange, isSmall, firstDayOfWeek, locale, ref } = props;
483
+
484
+ // Initialize view to show start date's month, or current month
485
+ const initialDate = useMemo(() => value.start || new Date(), [value.start]);
486
+ const [leftYear, setLeftYear] = useState(initialDate.getFullYear());
487
+ const [leftMonth, setLeftMonth] = useState(initialDate.getMonth());
488
+
489
+ // Expose navigation method via ref
490
+ useImperativeHandle(ref, () => ({
491
+ navigateToMonth: (year: number, month: number) => {
492
+ setLeftYear(year);
493
+ setLeftMonth(month);
494
+ },
495
+ }));
496
+
497
+ // Right calendar shows next month
498
+ const rightYear = leftMonth === 11 ? leftYear + 1 : leftYear;
499
+ const rightMonth = leftMonth === 11 ? 0 : leftMonth + 1;
500
+
501
+ // Track which part of range we're selecting
502
+ const [selectingEnd, setSelectingEnd] = useState(false);
503
+
504
+ const handleDateSelect = useCallback(
505
+ (date: Date) => {
506
+ if (!value.start || (value.start && value.end) || !selectingEnd) {
507
+ // Start new selection
508
+ onValueChange({ start: date, end: null });
509
+ setSelectingEnd(true);
510
+ } else {
511
+ // Complete selection
512
+ const start = value.start;
513
+ // Ensure start is before end
514
+ if (date < start) {
515
+ onValueChange({ start: date, end: start });
516
+ } else {
517
+ onValueChange({ start, end: date });
518
+ }
519
+ setSelectingEnd(false);
520
+ }
521
+ },
522
+ [value, onValueChange, selectingEnd],
523
+ );
524
+
525
+ const handleMonthChange = useCallback((year: number, month: number) => {
526
+ setLeftYear(year);
527
+ setLeftMonth(month);
528
+ }, []);
529
+
530
+ // Shared navigation - navigates both calendars together
531
+ const handlePrevMonth = useCallback(() => {
532
+ if (leftMonth === 0) {
533
+ setLeftYear(leftYear - 1);
534
+ setLeftMonth(11);
535
+ } else {
536
+ setLeftMonth(leftMonth - 1);
537
+ }
538
+ }, [leftYear, leftMonth]);
539
+
540
+ const handleNextMonth = useCallback(() => {
541
+ if (leftMonth === 11) {
542
+ setLeftYear(leftYear + 1);
543
+ setLeftMonth(0);
544
+ } else {
545
+ setLeftMonth(leftMonth + 1);
546
+ }
547
+ }, [leftYear, leftMonth]);
548
+
549
+ // On small screens: single calendar with navigation in header
550
+ // On desktop: horizontal layout with arrows inline
551
+ if (isSmall) {
552
+ return (
553
+ <View style={styles.calendarContainer}>
554
+ <CalendarMonth
555
+ year={leftYear}
556
+ month={leftMonth}
557
+ selectedDate={null}
558
+ rangeStart={value.start}
559
+ rangeEnd={value.end}
560
+ isRange={true}
561
+ onDateSelect={handleDateSelect}
562
+ onMonthChange={handleMonthChange}
563
+ showNavigation={true}
564
+ firstDayOfWeek={firstDayOfWeek}
565
+ locale={locale}
566
+ />
567
+ </View>
568
+ );
569
+ }
570
+
571
+ // Desktop: horizontal layout with arrows inline with month/year
572
+ return (
573
+ <View style={[styles.calendarContainer, { flexDirection: "row", gap: 24 }]}>
574
+ <CalendarMonth
575
+ year={leftYear}
576
+ month={leftMonth}
577
+ selectedDate={null}
578
+ rangeStart={value.start}
579
+ rangeEnd={value.end}
580
+ isRange={true}
581
+ onDateSelect={handleDateSelect}
582
+ onMonthChange={handleMonthChange}
583
+ showNavigation={false}
584
+ showLeftArrow={true}
585
+ onPrevMonth={handlePrevMonth}
586
+ firstDayOfWeek={firstDayOfWeek}
587
+ locale={locale}
588
+ />
589
+ <CalendarMonth
590
+ year={rightYear}
591
+ month={rightMonth}
592
+ selectedDate={null}
593
+ rangeStart={value.start}
594
+ rangeEnd={value.end}
595
+ isRange={true}
596
+ onDateSelect={handleDateSelect}
597
+ onMonthChange={handleMonthChange}
598
+ showNavigation={false}
599
+ showRightArrow={true}
600
+ onNextMonth={handleNextMonth}
601
+ firstDayOfWeek={firstDayOfWeek}
602
+ locale={locale}
603
+ />
604
+ </View>
605
+ );
606
+ }
607
+
608
+ // =============================================================================
609
+ // Styles
610
+ // =============================================================================
611
+
612
+ const styles = StyleSheet.create({
613
+ calendarContainer: {},
614
+ monthContainer: {
615
+ minWidth: 280,
616
+ },
617
+ monthHeader: {
618
+ flexDirection: "row",
619
+ alignItems: "center",
620
+ justifyContent: "space-between",
621
+ gap: 8,
622
+ },
623
+ pickerContainer: {
624
+ flex: 1,
625
+ flexDirection: "row",
626
+ gap: 8,
627
+ justifyContent: "center",
628
+ },
629
+ monthYearText: {
630
+ flex: 1,
631
+ alignItems: "center",
632
+ justifyContent: "center",
633
+ },
634
+ weekHeader: {
635
+ flexDirection: "row",
636
+ justifyContent: "space-around",
637
+ borderBottomWidth: 1,
638
+ borderBottomColor: colors.zinc["200"],
639
+ paddingBottom: 8,
640
+ marginBottom: 4,
641
+ },
642
+ dayHeaderCell: {
643
+ width: 36,
644
+ alignItems: "center",
645
+ justifyContent: "center",
646
+ height: 32,
647
+ },
648
+ weekRow: {
649
+ flexDirection: "row",
650
+ justifyContent: "space-around",
651
+ },
652
+ dayCell: {
653
+ width: 36,
654
+ height: 36,
655
+ alignItems: "center",
656
+ justifyContent: "center",
657
+ borderRadius: 999,
658
+ },
659
+ dayCellSelected: {
660
+ backgroundColor: colors.zinc["800"],
661
+ },
662
+ dayCellInRange: {
663
+ backgroundColor: colors.zinc["100"],
664
+ },
665
+ dayCellHovered: {
666
+ backgroundColor: colors.zinc["50"],
667
+ },
668
+ todayDot: {
669
+ position: "absolute",
670
+ bottom: 4,
671
+ width: 4,
672
+ height: 4,
673
+ borderRadius: 2,
674
+ backgroundColor: colors.zinc["800"],
675
+ },
676
+ todayDotInverted: {
677
+ backgroundColor: colors.white,
678
+ },
679
+ });