@super-calendar/dom 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 +463 -0
- package/dist/index.d.ts +463 -0
- package/dist/index.js +1393 -0
- package/dist/index.mjs +1319 -0
- package/package.json +62 -0
- package/src/Agenda.tsx +188 -0
- package/src/Calendar.tsx +230 -0
- package/src/MonthList.tsx +176 -0
- package/src/MonthView.tsx +546 -0
- package/src/TimeGrid.tsx +833 -0
- package/src/index.ts +49 -0
- package/src/theme.ts +38 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@super-calendar/dom",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Virtualized month / week / day calendar and date picker for react-dom. No React Native, no react-native-web.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"calendar",
|
|
7
|
+
"date-picker",
|
|
8
|
+
"month-view",
|
|
9
|
+
"react",
|
|
10
|
+
"react-dom",
|
|
11
|
+
"web",
|
|
12
|
+
"week-view"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/afonsojramos/react-native-super-calendar#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/afonsojramos/react-native-super-calendar/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/afonsojramos/react-native-super-calendar.git",
|
|
22
|
+
"directory": "packages/dom"
|
|
23
|
+
},
|
|
24
|
+
"source": "./src/index.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"!**/__tests__"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"default": "./dist/index.mjs"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"default": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@super-calendar/core": "2.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@legendapp/list": ">=3",
|
|
55
|
+
"date-fns": ">=3",
|
|
56
|
+
"react": ">=18",
|
|
57
|
+
"react-dom": ">=18"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/Agenda.tsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { LegendList } from "@legendapp/list/react";
|
|
2
|
+
import { format, isSameDay, type Locale, startOfDay } from "date-fns";
|
|
3
|
+
import { type ComponentType, type CSSProperties, useMemo } from "react";
|
|
4
|
+
import type { CalendarEvent } from "@super-calendar/core";
|
|
5
|
+
import { eventTimeLabel, getIsToday, isAllDayEvent } from "@super-calendar/core";
|
|
6
|
+
import { type DomCalendarTheme, mergeDomTheme } from "./theme";
|
|
7
|
+
|
|
8
|
+
/** Props passed to a custom agenda (schedule) event renderer. */
|
|
9
|
+
export interface DomAgendaEventArgs<T = unknown> {
|
|
10
|
+
event: CalendarEvent<T>;
|
|
11
|
+
/** Always "schedule"; lets a renderer shared with other views branch on it. */
|
|
12
|
+
mode: "schedule";
|
|
13
|
+
isAllDay: boolean;
|
|
14
|
+
/** Render the time range in 12-hour AM/PM. */
|
|
15
|
+
ampm?: boolean;
|
|
16
|
+
onPress: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DomAgendaEvent<T = unknown> = ComponentType<DomAgendaEventArgs<T>>;
|
|
20
|
+
|
|
21
|
+
export interface AgendaProps<T = unknown> {
|
|
22
|
+
/** Events to list. The consumer controls which (and therefore the date range). */
|
|
23
|
+
events: CalendarEvent<T>[];
|
|
24
|
+
locale?: Locale;
|
|
25
|
+
/** Render times in 12-hour AM/PM (default false, 24h). */
|
|
26
|
+
ampm?: boolean;
|
|
27
|
+
/** Highlight this date's header instead of the real "today". */
|
|
28
|
+
activeDate?: Date;
|
|
29
|
+
theme?: Partial<DomCalendarTheme>;
|
|
30
|
+
/** Height of the scroll viewport, in px (default 480). */
|
|
31
|
+
height?: number | string;
|
|
32
|
+
/** Replace the built-in event row. */
|
|
33
|
+
renderEvent?: DomAgendaEvent<T>;
|
|
34
|
+
onPressEvent?: (event: CalendarEvent<T>) => void;
|
|
35
|
+
/** Tap a day's header. */
|
|
36
|
+
onPressDay?: (date: Date) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
style?: CSSProperties;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Row<T> =
|
|
42
|
+
| { kind: "header"; date: Date; key: string }
|
|
43
|
+
| { kind: "event"; event: CalendarEvent<T>; key: string };
|
|
44
|
+
|
|
45
|
+
function DefaultAgendaRow<T>({
|
|
46
|
+
event,
|
|
47
|
+
isAllDay,
|
|
48
|
+
ampm = false,
|
|
49
|
+
theme,
|
|
50
|
+
}: DomAgendaEventArgs<T> & { theme: DomCalendarTheme }) {
|
|
51
|
+
const time =
|
|
52
|
+
eventTimeLabel({
|
|
53
|
+
mode: "schedule",
|
|
54
|
+
isAllDay,
|
|
55
|
+
start: event.start,
|
|
56
|
+
end: event.end,
|
|
57
|
+
ampm,
|
|
58
|
+
showTime: true,
|
|
59
|
+
}) ?? "";
|
|
60
|
+
// A filled card with the title on top and the time below, matching the native
|
|
61
|
+
// schedule row.
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
padding: "6px 10px",
|
|
66
|
+
borderRadius: 8,
|
|
67
|
+
background: theme.eventBackground,
|
|
68
|
+
color: theme.eventText,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<div style={{ fontWeight: 600, fontSize: 14 }}>{event.title}</div>
|
|
72
|
+
{time ? <div style={{ fontSize: 13, opacity: 0.75 }}>{time}</div> : null}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A vertical, day-grouped list of events: the schedule view, with plain DOM
|
|
79
|
+
* elements. Events are sorted by start and grouped under a date header per day;
|
|
80
|
+
* the consumer controls which events (and therefore which dates) are shown. The
|
|
81
|
+
* react-dom counterpart of the React Native `Agenda`.
|
|
82
|
+
*/
|
|
83
|
+
export function Agenda<T = unknown>({
|
|
84
|
+
events,
|
|
85
|
+
locale,
|
|
86
|
+
ampm = false,
|
|
87
|
+
activeDate,
|
|
88
|
+
theme: themeOverrides,
|
|
89
|
+
height = 480,
|
|
90
|
+
renderEvent,
|
|
91
|
+
onPressEvent,
|
|
92
|
+
onPressDay,
|
|
93
|
+
className,
|
|
94
|
+
style,
|
|
95
|
+
}: AgendaProps<T>) {
|
|
96
|
+
const theme = useMemo(() => mergeDomTheme(themeOverrides), [themeOverrides]);
|
|
97
|
+
const Renderer = renderEvent;
|
|
98
|
+
|
|
99
|
+
const rows = useMemo<Row<T>[]>(() => {
|
|
100
|
+
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
101
|
+
const out: Row<T>[] = [];
|
|
102
|
+
let currentDay: Date | null = null;
|
|
103
|
+
sorted.forEach((event, index) => {
|
|
104
|
+
if (!currentDay || !isSameDay(event.start, currentDay)) {
|
|
105
|
+
currentDay = startOfDay(event.start);
|
|
106
|
+
out.push({ kind: "header", date: currentDay, key: `h-${currentDay.toISOString()}` });
|
|
107
|
+
}
|
|
108
|
+
out.push({ kind: "event", event, key: `e-${event.start.toISOString()}-${index}` });
|
|
109
|
+
});
|
|
110
|
+
return out;
|
|
111
|
+
}, [events]);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={className}
|
|
116
|
+
style={{ fontFamily: theme.fontFamily, color: theme.text, ...style }}
|
|
117
|
+
>
|
|
118
|
+
<LegendList
|
|
119
|
+
data={rows}
|
|
120
|
+
keyExtractor={(row: Row<T>) => row.key}
|
|
121
|
+
recycleItems={false}
|
|
122
|
+
estimatedItemSize={44}
|
|
123
|
+
style={{ height, overflowY: "auto" }}
|
|
124
|
+
renderItem={({ item }: { item: Row<T> }) => {
|
|
125
|
+
if (item.kind === "header") {
|
|
126
|
+
const highlighted = activeDate
|
|
127
|
+
? isSameDay(item.date, activeDate)
|
|
128
|
+
: getIsToday(item.date);
|
|
129
|
+
const label = format(item.date, "EEEE, d LLLL", locale ? { locale } : undefined);
|
|
130
|
+
const color = highlighted ? theme.todayBackground : theme.textMuted;
|
|
131
|
+
return onPressDay ? (
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={() => onPressDay(item.date)}
|
|
135
|
+
style={{
|
|
136
|
+
...headerStyle,
|
|
137
|
+
color,
|
|
138
|
+
border: "none",
|
|
139
|
+
background: "transparent",
|
|
140
|
+
cursor: "pointer",
|
|
141
|
+
font: "inherit",
|
|
142
|
+
textAlign: "left",
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{label}
|
|
146
|
+
</button>
|
|
147
|
+
) : (
|
|
148
|
+
<div style={{ ...headerStyle, color }}>{label}</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const event = item.event;
|
|
152
|
+
const isAllDay = isAllDayEvent(event);
|
|
153
|
+
const onPress = () => onPressEvent?.(event);
|
|
154
|
+
const args: DomAgendaEventArgs<T> = { event, mode: "schedule", isAllDay, ampm, onPress };
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={onPress}
|
|
159
|
+
style={{
|
|
160
|
+
display: "block",
|
|
161
|
+
width: "100%",
|
|
162
|
+
textAlign: "left",
|
|
163
|
+
border: "none",
|
|
164
|
+
background: "transparent",
|
|
165
|
+
cursor: "pointer",
|
|
166
|
+
font: "inherit",
|
|
167
|
+
padding: "2px 12px",
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{Renderer ? <Renderer {...args} /> : <DefaultAgendaRow {...args} theme={theme} />}
|
|
171
|
+
</button>
|
|
172
|
+
);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
{rows.length === 0 ? (
|
|
176
|
+
<div style={{ padding: "16px 12px", color: theme.textMuted, fontSize: 14 }}>No events</div>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const headerStyle: CSSProperties = {
|
|
183
|
+
display: "block",
|
|
184
|
+
width: "100%",
|
|
185
|
+
padding: "12px 12px 4px",
|
|
186
|
+
fontSize: 13,
|
|
187
|
+
fontWeight: 600,
|
|
188
|
+
};
|
package/src/Calendar.tsx
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Locale } from "date-fns";
|
|
2
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
3
|
+
import type {
|
|
4
|
+
BusinessHours,
|
|
5
|
+
CalendarEvent,
|
|
6
|
+
DateRange,
|
|
7
|
+
DateSelectionConstraints,
|
|
8
|
+
TimeGridMode,
|
|
9
|
+
WeekStartsOn,
|
|
10
|
+
} from "@super-calendar/core";
|
|
11
|
+
import { Agenda, type DomAgendaEvent } from "./Agenda";
|
|
12
|
+
import { type DomMonthEvent, MonthView } from "./MonthView";
|
|
13
|
+
import { type DomRenderEvent, TimeGrid } from "./TimeGrid";
|
|
14
|
+
import type { DomCalendarTheme } from "./theme";
|
|
15
|
+
|
|
16
|
+
export interface CalendarProps<T = unknown> extends DateSelectionConstraints {
|
|
17
|
+
/**
|
|
18
|
+
* The view to render (default "week"). `month` renders a month grid, `schedule`
|
|
19
|
+
* a day-grouped agenda list, and the others a time grid.
|
|
20
|
+
*/
|
|
21
|
+
mode?: "month" | "schedule" | TimeGridMode;
|
|
22
|
+
/** Controlled anchor date. Change it (e.g. from your own header) to navigate. */
|
|
23
|
+
date: Date;
|
|
24
|
+
/** Your events. */
|
|
25
|
+
events?: CalendarEvent<T>[];
|
|
26
|
+
/** First day of the week. Sunday = 0 (default) … Saturday = 6. */
|
|
27
|
+
weekStartsOn?: WeekStartsOn;
|
|
28
|
+
/** Column count for `mode="custom"`. */
|
|
29
|
+
numberOfDays?: number;
|
|
30
|
+
locale?: Locale;
|
|
31
|
+
theme?: Partial<DomCalendarTheme>;
|
|
32
|
+
/** Height of the scroll viewport, in px. */
|
|
33
|
+
height?: number | string;
|
|
34
|
+
className?: string;
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
|
|
37
|
+
/** Tap an event (both layouts). */
|
|
38
|
+
onPressEvent?: (event: CalendarEvent<T>) => void;
|
|
39
|
+
|
|
40
|
+
// --- Time-grid modes (week / day / 3days / custom) ---
|
|
41
|
+
/** 12-hour AM/PM time labels (default false). */
|
|
42
|
+
ampm?: boolean;
|
|
43
|
+
/** Initial pixels per hour. */
|
|
44
|
+
hourHeight?: number;
|
|
45
|
+
/** Initial scroll position, in minutes from midnight. */
|
|
46
|
+
scrollOffsetMinutes?: number;
|
|
47
|
+
/** Sub-divisions per hour for the grid lines. */
|
|
48
|
+
timeslots?: number;
|
|
49
|
+
/** Shade the hours outside business hours. */
|
|
50
|
+
businessHours?: BusinessHours;
|
|
51
|
+
/** Show the current-time indicator (default true). */
|
|
52
|
+
showNowIndicator?: boolean;
|
|
53
|
+
/** Show the all-day lane (default true). */
|
|
54
|
+
showAllDayEventCell?: boolean;
|
|
55
|
+
/** Snap dragged/created events to this many minutes. */
|
|
56
|
+
dragStepMinutes?: number;
|
|
57
|
+
/** Tap empty grid space. */
|
|
58
|
+
onPressCell?: (date: Date) => void;
|
|
59
|
+
/** Drag empty grid space to create. */
|
|
60
|
+
onCreateEvent?: (start: Date, end: Date) => void;
|
|
61
|
+
/** Fires when an event drag begins. */
|
|
62
|
+
onDragStart?: (event: CalendarEvent<T>) => void;
|
|
63
|
+
/** Enables drag-to-move/resize; return `false` to reject the drop. */
|
|
64
|
+
onDragEvent?: (event: CalendarEvent<T>, start: Date, end: Date) => void | boolean;
|
|
65
|
+
/** Tap a day's column header. */
|
|
66
|
+
onPressDateHeader?: (day: Date) => void;
|
|
67
|
+
/** Custom time-grid event renderer. */
|
|
68
|
+
renderTimeEvent?: DomRenderEvent<T>;
|
|
69
|
+
/** Replace the hour-axis label. Receives the hour (0–23) and the `ampm` flag. */
|
|
70
|
+
hourComponent?: (hour: number, ampm: boolean) => ReactNode;
|
|
71
|
+
|
|
72
|
+
// --- Month mode ---
|
|
73
|
+
/** Max chips per day before a "+N more" row. */
|
|
74
|
+
maxVisibleEventCount?: number;
|
|
75
|
+
/** Overflow row template; `{moreCount}` is replaced. */
|
|
76
|
+
moreLabel?: string;
|
|
77
|
+
/** Render neighbouring months' days in the leading/trailing cells (default true). */
|
|
78
|
+
showAdjacentMonths?: boolean;
|
|
79
|
+
/** Fill the cell with the range background instead of the pill band. */
|
|
80
|
+
fillCellOnSelection?: boolean;
|
|
81
|
+
/** Selected span. */
|
|
82
|
+
selectedRange?: DateRange;
|
|
83
|
+
/** Discrete selected days. */
|
|
84
|
+
selectedDates?: Date[];
|
|
85
|
+
/**
|
|
86
|
+
* In month mode, make day cells keyboard-navigable (one roving tab stop, arrow
|
|
87
|
+
* keys, Enter to open the day). Default false: keyboard focus moves through
|
|
88
|
+
* events only. Has no effect on the time-grid modes.
|
|
89
|
+
*/
|
|
90
|
+
keyboardDayNavigation?: boolean;
|
|
91
|
+
/** Tap a day cell. */
|
|
92
|
+
onPressDay?: (date: Date) => void;
|
|
93
|
+
/** Tap the "+N more" overflow row. */
|
|
94
|
+
onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
|
|
95
|
+
/** Custom month chip renderer. */
|
|
96
|
+
renderMonthEvent?: DomMonthEvent<T>;
|
|
97
|
+
|
|
98
|
+
// --- Schedule mode ---
|
|
99
|
+
/** Custom agenda row renderer (`mode="schedule"`). */
|
|
100
|
+
renderScheduleEvent?: DomAgendaEvent<T>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Batteries-included entry point for the react-dom renderer: it picks the right
|
|
105
|
+
* view for `mode`. `month` renders a single {@link MonthView}; `schedule` renders
|
|
106
|
+
* an {@link Agenda}; the time-grid modes render a {@link TimeGrid}. For a
|
|
107
|
+
* scrolling month picker, use {@link MonthList} directly.
|
|
108
|
+
*/
|
|
109
|
+
export function Calendar<T = unknown>({
|
|
110
|
+
mode = "week",
|
|
111
|
+
date,
|
|
112
|
+
events,
|
|
113
|
+
weekStartsOn = 0,
|
|
114
|
+
numberOfDays,
|
|
115
|
+
locale,
|
|
116
|
+
theme,
|
|
117
|
+
height,
|
|
118
|
+
className,
|
|
119
|
+
style,
|
|
120
|
+
onPressEvent,
|
|
121
|
+
// time grid
|
|
122
|
+
ampm,
|
|
123
|
+
hourHeight,
|
|
124
|
+
scrollOffsetMinutes,
|
|
125
|
+
timeslots,
|
|
126
|
+
businessHours,
|
|
127
|
+
showNowIndicator,
|
|
128
|
+
showAllDayEventCell,
|
|
129
|
+
dragStepMinutes,
|
|
130
|
+
onPressCell,
|
|
131
|
+
onCreateEvent,
|
|
132
|
+
onDragStart,
|
|
133
|
+
onDragEvent,
|
|
134
|
+
onPressDateHeader,
|
|
135
|
+
renderTimeEvent,
|
|
136
|
+
hourComponent,
|
|
137
|
+
// month
|
|
138
|
+
maxVisibleEventCount,
|
|
139
|
+
moreLabel,
|
|
140
|
+
showAdjacentMonths,
|
|
141
|
+
fillCellOnSelection,
|
|
142
|
+
selectedRange,
|
|
143
|
+
selectedDates,
|
|
144
|
+
minDate,
|
|
145
|
+
maxDate,
|
|
146
|
+
isDateDisabled,
|
|
147
|
+
keyboardDayNavigation,
|
|
148
|
+
onPressDay,
|
|
149
|
+
onPressMore,
|
|
150
|
+
renderMonthEvent,
|
|
151
|
+
// schedule
|
|
152
|
+
renderScheduleEvent,
|
|
153
|
+
}: CalendarProps<T>) {
|
|
154
|
+
if (mode === "schedule") {
|
|
155
|
+
return (
|
|
156
|
+
<Agenda<T>
|
|
157
|
+
events={events ?? []}
|
|
158
|
+
locale={locale}
|
|
159
|
+
ampm={ampm}
|
|
160
|
+
theme={theme}
|
|
161
|
+
height={height}
|
|
162
|
+
activeDate={date}
|
|
163
|
+
className={className}
|
|
164
|
+
style={style}
|
|
165
|
+
renderEvent={renderScheduleEvent}
|
|
166
|
+
onPressEvent={onPressEvent}
|
|
167
|
+
onPressDay={onPressDay}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (mode === "month") {
|
|
173
|
+
return (
|
|
174
|
+
<MonthView<T>
|
|
175
|
+
date={date}
|
|
176
|
+
events={events ?? []}
|
|
177
|
+
weekStartsOn={weekStartsOn}
|
|
178
|
+
locale={locale}
|
|
179
|
+
theme={theme}
|
|
180
|
+
className={className}
|
|
181
|
+
style={style}
|
|
182
|
+
maxVisibleEventCount={maxVisibleEventCount}
|
|
183
|
+
moreLabel={moreLabel}
|
|
184
|
+
showAdjacentMonths={showAdjacentMonths}
|
|
185
|
+
fillCellOnSelection={fillCellOnSelection}
|
|
186
|
+
selectedRange={selectedRange}
|
|
187
|
+
selectedDates={selectedDates}
|
|
188
|
+
minDate={minDate}
|
|
189
|
+
maxDate={maxDate}
|
|
190
|
+
isDateDisabled={isDateDisabled}
|
|
191
|
+
keyboardDayNavigation={keyboardDayNavigation}
|
|
192
|
+
onPressDay={onPressDay}
|
|
193
|
+
onPressEvent={onPressEvent}
|
|
194
|
+
onPressMore={onPressMore}
|
|
195
|
+
renderEvent={renderMonthEvent}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<TimeGrid<T>
|
|
202
|
+
date={date}
|
|
203
|
+
mode={mode}
|
|
204
|
+
events={events}
|
|
205
|
+
weekStartsOn={weekStartsOn}
|
|
206
|
+
numberOfDays={numberOfDays}
|
|
207
|
+
locale={locale}
|
|
208
|
+
theme={theme}
|
|
209
|
+
height={height}
|
|
210
|
+
className={className}
|
|
211
|
+
style={style}
|
|
212
|
+
ampm={ampm}
|
|
213
|
+
hourHeight={hourHeight}
|
|
214
|
+
scrollOffsetMinutes={scrollOffsetMinutes}
|
|
215
|
+
timeslots={timeslots}
|
|
216
|
+
businessHours={businessHours}
|
|
217
|
+
showNowIndicator={showNowIndicator}
|
|
218
|
+
showAllDayEventCell={showAllDayEventCell}
|
|
219
|
+
dragStepMinutes={dragStepMinutes}
|
|
220
|
+
onPressEvent={onPressEvent}
|
|
221
|
+
onPressCell={onPressCell}
|
|
222
|
+
onCreateEvent={onCreateEvent}
|
|
223
|
+
onDragStart={onDragStart}
|
|
224
|
+
onDragEvent={onDragEvent}
|
|
225
|
+
onPressDateHeader={onPressDateHeader}
|
|
226
|
+
renderEvent={renderTimeEvent}
|
|
227
|
+
hourComponent={hourComponent}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { LegendList } from "@legendapp/list/react";
|
|
2
|
+
import { addMonths, type Locale, startOfMonth } from "date-fns";
|
|
3
|
+
import { type CSSProperties, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
buildMonthGrid,
|
|
6
|
+
type CalendarEvent,
|
|
7
|
+
compareDayEvents,
|
|
8
|
+
type DateRange,
|
|
9
|
+
type DateSelectionConstraints,
|
|
10
|
+
groupEventsByDay,
|
|
11
|
+
type WeekStartsOn,
|
|
12
|
+
} from "@super-calendar/core";
|
|
13
|
+
import { type DomMonthEvent, MonthView } from "./MonthView";
|
|
14
|
+
import { type DomCalendarTheme, mergeDomTheme } from "./theme";
|
|
15
|
+
|
|
16
|
+
export interface MonthListProps<T = unknown> extends DateSelectionConstraints {
|
|
17
|
+
/** Anchor month; the list spans `pastMonths` before to `futureMonths` after. */
|
|
18
|
+
date: Date;
|
|
19
|
+
/** Months to render before the anchor (default 1). */
|
|
20
|
+
pastMonths?: number;
|
|
21
|
+
/** Months to render after the anchor (default 12). */
|
|
22
|
+
futureMonths?: number;
|
|
23
|
+
weekStartsOn?: WeekStartsOn;
|
|
24
|
+
/** Events to render as chips in each day cell (calendar layout when provided). */
|
|
25
|
+
events?: CalendarEvent<T>[];
|
|
26
|
+
/** Custom chip renderer; falls back to the built-in titled chip. */
|
|
27
|
+
renderEvent?: DomMonthEvent<T>;
|
|
28
|
+
/** Max chips shown per day before a "+N more" row (default 3). */
|
|
29
|
+
maxVisibleEventCount?: number;
|
|
30
|
+
/** Template for the overflow row; `{moreCount}` is replaced. */
|
|
31
|
+
moreLabel?: string;
|
|
32
|
+
/** Tap an event chip. */
|
|
33
|
+
onPressEvent?: (event: CalendarEvent<T>) => void;
|
|
34
|
+
/** Tap the "+N more" overflow row. */
|
|
35
|
+
onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
|
|
36
|
+
selectedRange?: DateRange;
|
|
37
|
+
selectedDates?: Date[];
|
|
38
|
+
/** Fill the whole cell on selection instead of the default rounded pill band. */
|
|
39
|
+
fillCellOnSelection?: boolean;
|
|
40
|
+
locale?: Locale;
|
|
41
|
+
theme?: Partial<DomCalendarTheme>;
|
|
42
|
+
/** Height of the scroll viewport, in px (default 480). */
|
|
43
|
+
height?: number | string;
|
|
44
|
+
/**
|
|
45
|
+
* When events are shown, make each month's day cells keyboard-navigable (roving
|
|
46
|
+
* tab stop, arrow keys, Enter to open the day). Default false. The picker
|
|
47
|
+
* layout (no `events`) is always navigable regardless.
|
|
48
|
+
*/
|
|
49
|
+
keyboardDayNavigation?: boolean;
|
|
50
|
+
onPressDay?: (date: Date) => void;
|
|
51
|
+
className?: string;
|
|
52
|
+
style?: CSSProperties;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A vertically scrolling, virtualized list of months: the date picker. Built on
|
|
57
|
+
* Legend List's DOM renderer and the library's headless grid logic. Selection is
|
|
58
|
+
* controlled, pass `selectedRange` (or `selectedDates`) and handle `onPressDay`.
|
|
59
|
+
*/
|
|
60
|
+
export function MonthList<T = unknown>({
|
|
61
|
+
date,
|
|
62
|
+
pastMonths = 1,
|
|
63
|
+
futureMonths = 12,
|
|
64
|
+
weekStartsOn = 0,
|
|
65
|
+
events,
|
|
66
|
+
renderEvent,
|
|
67
|
+
maxVisibleEventCount,
|
|
68
|
+
moreLabel,
|
|
69
|
+
onPressEvent,
|
|
70
|
+
onPressMore,
|
|
71
|
+
selectedRange,
|
|
72
|
+
selectedDates,
|
|
73
|
+
fillCellOnSelection = false,
|
|
74
|
+
locale,
|
|
75
|
+
theme: themeOverrides,
|
|
76
|
+
height = 480,
|
|
77
|
+
minDate,
|
|
78
|
+
maxDate,
|
|
79
|
+
isDateDisabled,
|
|
80
|
+
keyboardDayNavigation,
|
|
81
|
+
onPressDay,
|
|
82
|
+
className,
|
|
83
|
+
style,
|
|
84
|
+
}: MonthListProps<T>) {
|
|
85
|
+
const theme = useMemo(() => mergeDomTheme(themeOverrides), [themeOverrides]);
|
|
86
|
+
|
|
87
|
+
const months = useMemo(() => {
|
|
88
|
+
const first = startOfMonth(addMonths(date, -pastMonths));
|
|
89
|
+
const count = pastMonths + futureMonths + 1;
|
|
90
|
+
return Array.from({ length: count }, (_, i) => addMonths(first, i));
|
|
91
|
+
}, [date, pastMonths, futureMonths]);
|
|
92
|
+
|
|
93
|
+
// The weekday header only depends on the week start and locale, not the
|
|
94
|
+
// rendered month window.
|
|
95
|
+
const weekdays = useMemo(
|
|
96
|
+
() => buildMonthGrid(date, { weekStartsOn, locale }).weekdays,
|
|
97
|
+
[date, weekStartsOn, locale],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Build the day→events index once for the whole list rather than per month.
|
|
101
|
+
const eventsByDay = useMemo(() => {
|
|
102
|
+
if (!events) return undefined;
|
|
103
|
+
const map = groupEventsByDay(events);
|
|
104
|
+
// Each day reads all-day events first, then timed events by start.
|
|
105
|
+
for (const list of map.values()) list.sort(compareDayEvents);
|
|
106
|
+
return map;
|
|
107
|
+
}, [events]);
|
|
108
|
+
|
|
109
|
+
// Stable reference so LegendList only re-renders rows when selection or events
|
|
110
|
+
// actually change, not on every parent render.
|
|
111
|
+
const extraData = useMemo(
|
|
112
|
+
() => [selectedRange, selectedDates, events] as const,
|
|
113
|
+
[selectedRange, selectedDates, events],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
className={className}
|
|
119
|
+
style={{ fontFamily: theme.fontFamily, color: theme.text, ...style }}
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
display: "grid",
|
|
124
|
+
gridTemplateColumns: "repeat(7, minmax(0, 1fr))",
|
|
125
|
+
borderBottom: `1px solid ${theme.gridLine}`,
|
|
126
|
+
padding: "8px 0",
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{weekdays.map((wd) => (
|
|
130
|
+
<span
|
|
131
|
+
key={wd.label}
|
|
132
|
+
style={{ textAlign: "center", fontSize: 12, fontWeight: 600, color: theme.textMuted }}
|
|
133
|
+
>
|
|
134
|
+
{wd.label}
|
|
135
|
+
</span>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
<LegendList
|
|
139
|
+
data={months}
|
|
140
|
+
extraData={extraData}
|
|
141
|
+
keyExtractor={(m: Date) => m.toISOString()}
|
|
142
|
+
recycleItems={false}
|
|
143
|
+
estimatedItemSize={theme.cellHeight * 7 + 40}
|
|
144
|
+
// Open on the anchor month (today by default), not the first past month.
|
|
145
|
+
// `months` starts `pastMonths` before the anchor, so it sits at that index.
|
|
146
|
+
initialScrollIndex={pastMonths}
|
|
147
|
+
style={{ height, overflowY: "auto" }}
|
|
148
|
+
renderItem={({ item }: { item: Date }) => (
|
|
149
|
+
<MonthView<T>
|
|
150
|
+
date={item}
|
|
151
|
+
weekStartsOn={weekStartsOn}
|
|
152
|
+
events={events}
|
|
153
|
+
eventsByDay={eventsByDay}
|
|
154
|
+
renderEvent={renderEvent}
|
|
155
|
+
maxVisibleEventCount={maxVisibleEventCount}
|
|
156
|
+
moreLabel={moreLabel}
|
|
157
|
+
onPressEvent={onPressEvent}
|
|
158
|
+
onPressMore={onPressMore}
|
|
159
|
+
selectedRange={selectedRange}
|
|
160
|
+
selectedDates={selectedDates}
|
|
161
|
+
fillCellOnSelection={fillCellOnSelection}
|
|
162
|
+
showAdjacentMonths={false}
|
|
163
|
+
showWeekdays={false}
|
|
164
|
+
locale={locale}
|
|
165
|
+
theme={themeOverrides}
|
|
166
|
+
minDate={minDate}
|
|
167
|
+
maxDate={maxDate}
|
|
168
|
+
isDateDisabled={isDateDisabled}
|
|
169
|
+
keyboardDayNavigation={keyboardDayNavigation}
|
|
170
|
+
onPressDay={onPressDay}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|