@open-webapp/calendar 0.0.0-stage → 0.2.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,257 @@
1
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
2
+ export const DEFAULT_RSVP_STATUSES = ['accepted', 'tentative', 'needsAction'];
3
+ /** Keeps stored calendar selections that still exist, defaulting to the primary calendar. */
4
+ export function reconcileSelectedCalendarIds(storedIds, calendars) {
5
+ const availableIds = new Set(calendars.map((calendar) => calendar.id));
6
+ const selectedIds = (storedIds ?? []).filter((id) => availableIds.has(id));
7
+ if (selectedIds.length > 0)
8
+ return selectedIds;
9
+ const fallback = calendars.find((calendar) => calendar.primary) ?? calendars[0];
10
+ return fallback ? [fallback.id] : [];
11
+ }
12
+ /** Filters calendars by a case-insensitive substring of their display name. */
13
+ export function filterCalendarsByName(calendars, query) {
14
+ const normalizedQuery = query.toLocaleLowerCase();
15
+ return normalizedQuery === ''
16
+ ? calendars
17
+ : calendars.filter((calendar) => calendar.summary.toLocaleLowerCase().includes(normalizedQuery));
18
+ }
19
+ /** Event IDs are unique only within their calendar and source. */
20
+ export function getCalendarEventKey(event) {
21
+ return `${event.sourceId}:${event.calendarId ?? ""}:${event.id}`;
22
+ }
23
+ /** Produces the compact calendar-selection label shown in the filter control. */
24
+ export function summarizeCalendarSelection(selectedIds, calendars) {
25
+ if (selectedIds.length !== 1)
26
+ return `${selectedIds.length} calendars`;
27
+ return calendars.find((calendar) => calendar.id === selectedIds[0])?.summary ?? '1 calendar';
28
+ }
29
+ /** True iff the event is an all-day event. Passthrough of the package-computed flag. */
30
+ export function isAllDay(event) {
31
+ return event.isAllDay;
32
+ }
33
+ /**
34
+ * Rolling fetch window: now-30d to now+30d, as full RFC3339 timestamps.
35
+ * Google Calendar's `timeMin`/`timeMax` reject bare `YYYY-MM-DD` dates.
36
+ */
37
+ export function getRollingWindow(now) {
38
+ const start = new Date(now.getTime() - 30 * MS_PER_DAY);
39
+ const end = new Date(now.getTime() + 30 * MS_PER_DAY);
40
+ return { start: start.toISOString(), end: end.toISOString() };
41
+ }
42
+ function toIsoDate(d) {
43
+ return d.toISOString().slice(0, 10);
44
+ }
45
+ /**
46
+ * Returns events whose `selfResponseStatus` is a member of `selected` —
47
+ * declined is a normal, independently-selectable status now, not hard-excluded.
48
+ */
49
+ export function filterByRsvp(events, selected) {
50
+ return events.filter((e) => selected.has(e.selfResponseStatus));
51
+ }
52
+ /** displayName -> email -> "You" (only when organizer.self is true and both above are absent). */
53
+ export function getOrganizerLabel(event) {
54
+ const { organizer } = event;
55
+ if (organizer.displayName)
56
+ return organizer.displayName;
57
+ if (organizer.email)
58
+ return organizer.email;
59
+ if (organizer.self === true)
60
+ return 'You';
61
+ return '';
62
+ }
63
+ /**
64
+ * True iff `event` is a timed (non-all-day) event currently in progress at `now`:
65
+ * both `start.dateTime` and `end.dateTime` exist and parse to valid instants,
66
+ * start <= now < end.
67
+ */
68
+ export function isEventCurrent(event, now) {
69
+ if (isAllDay(event))
70
+ return false;
71
+ const { start, end } = event;
72
+ if (!start.dateTime || !end.dateTime)
73
+ return false;
74
+ const startMs = new Date(start.dateTime).getTime();
75
+ const endMs = new Date(end.dateTime).getTime();
76
+ if (Number.isNaN(startMs) || Number.isNaN(endMs))
77
+ return false;
78
+ const nowMs = now.getTime();
79
+ return startMs <= nowMs && nowMs < endMs;
80
+ }
81
+ /**
82
+ * Finds the soonest strictly-future timed event: `!isAllDay(event)`,
83
+ * `start.dateTime` exists and parses valid, and `startMs > now.getTime()`.
84
+ * Returns the smallest `startMs`; ties keep first-encountered array order.
85
+ * Returns `null` if none qualify.
86
+ */
87
+ export function findNextUpcomingEvent(events, now) {
88
+ const nowMs = now.getTime();
89
+ let best = null;
90
+ let bestMs = Infinity;
91
+ for (const event of events) {
92
+ if (isAllDay(event))
93
+ continue;
94
+ const { dateTime } = event.start;
95
+ if (!dateTime)
96
+ continue;
97
+ const startMs = new Date(dateTime).getTime();
98
+ if (Number.isNaN(startMs))
99
+ continue;
100
+ if (startMs <= nowMs)
101
+ continue;
102
+ if (startMs < bestMs) {
103
+ bestMs = startMs;
104
+ best = event;
105
+ }
106
+ }
107
+ return best;
108
+ }
109
+ function getStartDateKey(event) {
110
+ const { start } = event;
111
+ if (start.date)
112
+ return start.date;
113
+ if (start.dateTime)
114
+ return start.dateTime.slice(0, 10);
115
+ return '';
116
+ }
117
+ function getStartTimeSortKey(event) {
118
+ const { start } = event;
119
+ if (start.dateTime)
120
+ return new Date(start.dateTime).getTime();
121
+ return -Infinity; // all-day events sort first
122
+ }
123
+ /**
124
+ * Group by local calendar date, sorted ascending by day. Within a day,
125
+ * all-day events come first (in list order), then timed events sorted by
126
+ * start time ascending.
127
+ *
128
+ * When `todayIso` is given and has no events, an empty group for it is
129
+ * still included (so a "Today" header can always be rendered / scrolled
130
+ * to), inserted in correct sorted position.
131
+ */
132
+ export function groupByDay(events, todayIso) {
133
+ const byDate = new Map();
134
+ for (const event of events) {
135
+ const key = getStartDateKey(event);
136
+ const list = byDate.get(key);
137
+ if (list) {
138
+ list.push(event);
139
+ }
140
+ else {
141
+ byDate.set(key, [event]);
142
+ }
143
+ }
144
+ if (todayIso !== undefined && !byDate.has(todayIso)) {
145
+ byDate.set(todayIso, []);
146
+ }
147
+ const dateKeys = Array.from(byDate.keys()).sort();
148
+ return dateKeys.map((dateKey) => {
149
+ const dayEvents = byDate.get(dateKey);
150
+ const allDayEvents = dayEvents.filter((e) => isAllDay(e));
151
+ const timedEvents = dayEvents
152
+ .filter((e) => !isAllDay(e))
153
+ .sort((a, b) => getStartTimeSortKey(a) - getStartTimeSortKey(b));
154
+ return { dateKey, events: [...allDayEvents, ...timedEvents] };
155
+ });
156
+ }
157
+ const MONTH_NAMES = [
158
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
159
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
160
+ ];
161
+ function parseIsoDateParts(dateKey) {
162
+ const [year, month, day] = dateKey.split('-').map(Number);
163
+ return { year, month, day };
164
+ }
165
+ function formatMonthDay(dateKey, includeYear) {
166
+ const { year, month, day } = parseIsoDateParts(dateKey);
167
+ const monthName = MONTH_NAMES[month - 1];
168
+ const base = `${monthName} ${day}`;
169
+ return includeYear ? `${base}, ${year}` : base;
170
+ }
171
+ function addDaysToIso(dateKey, days) {
172
+ const { year, month, day } = parseIsoDateParts(dateKey);
173
+ const d = new Date(Date.UTC(year, month - 1, day));
174
+ d.setUTCDate(d.getUTCDate() + days);
175
+ return toIsoDate(d);
176
+ }
177
+ /**
178
+ * "Today · Sept 26" / "Tomorrow · Sept 27" / else "Sept 28"-style.
179
+ * Includes year only when the event's year differs from today's year.
180
+ */
181
+ export function formatDayHeader(dateKey, today) {
182
+ const { year: todayYear } = parseIsoDateParts(today);
183
+ const { year: eventYear } = parseIsoDateParts(dateKey);
184
+ const includeYear = eventYear !== todayYear;
185
+ const monthDay = formatMonthDay(dateKey, includeYear);
186
+ if (dateKey === today)
187
+ return `Today · ${monthDay}`;
188
+ if (dateKey === addDaysToIso(today, 1))
189
+ return `Tomorrow · ${monthDay}`;
190
+ return monthDay;
191
+ }
192
+ const JOIN_URL_REGEX = /https?:\/\/[^\s<>"']*(?:zoom\.us|meet\.google\.com|teams\.microsoft\.com)[^\s<>"']*/i;
193
+ export function deriveSelfResponseStatus(raw) {
194
+ if (!Array.isArray(raw.attendees))
195
+ return 'accepted';
196
+ const status = raw.attendees.find((a) => a?.self === true)?.responseStatus;
197
+ if (status === 'accepted' || status === 'tentative' || status === 'needsAction' || status === 'declined') {
198
+ return status;
199
+ }
200
+ return 'needsAction';
201
+ }
202
+ export function extractJoinUrl(raw) {
203
+ if (raw.hangoutLink)
204
+ return raw.hangoutLink;
205
+ const video = raw.conferenceData?.entryPoints?.find((ep) => ep.entryPointType === 'video');
206
+ if (video?.uri)
207
+ return video.uri;
208
+ const loc = typeof raw.location === 'string' ? raw.location.match(JOIN_URL_REGEX) : null;
209
+ if (loc)
210
+ return loc[0];
211
+ const desc = typeof raw.description === 'string' ? raw.description.match(JOIN_URL_REGEX) : null;
212
+ return desc ? desc[0] : null;
213
+ }
214
+ /** Parses a raw Google Calendar event. Returns null for cancelled/malformed input; never throws. */
215
+ export function parseEvent(raw, ctx) {
216
+ try {
217
+ if (!raw || typeof raw !== 'object')
218
+ return null;
219
+ if (typeof raw.id !== 'string' || raw.id === '')
220
+ return null;
221
+ if (raw.status === 'cancelled')
222
+ return null;
223
+ const start = raw.start ?? {};
224
+ const end = raw.end ?? {};
225
+ return {
226
+ id: raw.id,
227
+ sourceId: ctx.sourceId,
228
+ calendarId: ctx.calendarId,
229
+ summary: raw.summary ?? '',
230
+ start: { dateTime: start.dateTime, date: start.date, timeZone: start.timeZone },
231
+ end: { dateTime: end.dateTime, date: end.date, timeZone: end.timeZone },
232
+ isAllDay: Boolean(start.date && !start.dateTime),
233
+ selfResponseStatus: deriveSelfResponseStatus(raw),
234
+ joinUrl: extractJoinUrl(raw),
235
+ organizer: {
236
+ displayName: raw.organizer?.displayName,
237
+ email: raw.organizer?.email,
238
+ self: Boolean(raw.organizer?.self),
239
+ },
240
+ htmlLink: raw.htmlLink ?? '',
241
+ };
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }
247
+ export function parseEvents(items, ctx) {
248
+ const out = [];
249
+ if (!Array.isArray(items))
250
+ return out;
251
+ for (const item of items) {
252
+ const e = parseEvent(item, ctx);
253
+ if (e)
254
+ out.push(e);
255
+ }
256
+ return out;
257
+ }
@@ -0,0 +1,23 @@
1
+ import type { CalendarInfo } from './types.js';
2
+ export declare class CalendarFetchError extends Error {
3
+ kind: 'network' | 'auth' | 'scope' | 'gone' | 'http';
4
+ status: number;
5
+ constructor(kind: 'network' | 'auth' | 'scope' | 'gone' | 'http', status?: number);
6
+ }
7
+ /** Thrown by a host's getToken when a token refresh was deferred (e.g. app inactive). Aborts a pass quietly. */
8
+ export declare class RefreshDeferredError extends Error {
9
+ readonly deferred = true;
10
+ constructor(message?: string);
11
+ }
12
+ export declare function isRefreshDeferredError(e: unknown): boolean;
13
+ export declare function fetchCalendarList(token: string): Promise<CalendarInfo[]>;
14
+ export interface EventsPage {
15
+ items: any[];
16
+ nextSyncToken?: string;
17
+ }
18
+ /** Fetches all pages of events. With syncToken, no time bounds are sent (the API forbids them). */
19
+ export declare function fetchEvents(token: string, calendarId: string, opts: {
20
+ syncToken?: string;
21
+ timeMin?: string;
22
+ timeMax?: string;
23
+ }): Promise<EventsPage>;
@@ -0,0 +1,108 @@
1
+ const BASE = 'https://www.googleapis.com/calendar/v3';
2
+ export class CalendarFetchError extends Error {
3
+ kind;
4
+ status;
5
+ constructor(kind, status = 0) {
6
+ super(`calendar fetch failed: ${kind} ${status}`);
7
+ this.kind = kind;
8
+ this.status = status;
9
+ }
10
+ }
11
+ /** Thrown by a host's getToken when a token refresh was deferred (e.g. app inactive). Aborts a pass quietly. */
12
+ export class RefreshDeferredError extends Error {
13
+ deferred = true;
14
+ constructor(message = 'token refresh deferred') {
15
+ super(message);
16
+ this.name = 'RefreshDeferredError';
17
+ }
18
+ }
19
+ export function isRefreshDeferredError(e) {
20
+ if (e instanceof RefreshDeferredError)
21
+ return true;
22
+ const x = e;
23
+ return !!x && (x.deferred === true || x.name === 'RefreshDeferredError');
24
+ }
25
+ async function request(url, token) {
26
+ let res;
27
+ try {
28
+ res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
29
+ }
30
+ catch {
31
+ throw new CalendarFetchError('network');
32
+ }
33
+ if (res.ok) {
34
+ try {
35
+ return await res.json();
36
+ }
37
+ catch {
38
+ throw new CalendarFetchError('http', res.status);
39
+ }
40
+ }
41
+ if (res.status === 410)
42
+ throw new CalendarFetchError('gone', 410);
43
+ if (res.status === 401)
44
+ throw new CalendarFetchError('auth', 401);
45
+ if (res.status === 403) {
46
+ let body = null;
47
+ try {
48
+ body = await res.json();
49
+ }
50
+ catch {
51
+ /* ignore */
52
+ }
53
+ const insufficient = JSON.stringify(body ?? {}).includes('insufficientPermissions');
54
+ throw new CalendarFetchError(insufficient ? 'scope' : 'http', 403);
55
+ }
56
+ throw new CalendarFetchError('http', res.status);
57
+ }
58
+ export async function fetchCalendarList(token) {
59
+ const out = [];
60
+ let pageToken;
61
+ do {
62
+ const u = new URL(`${BASE}/users/me/calendarList`);
63
+ if (pageToken)
64
+ u.searchParams.set('pageToken', pageToken);
65
+ const body = await request(u.toString(), token);
66
+ for (const c of body.items ?? []) {
67
+ out.push({
68
+ id: c.id,
69
+ summary: c.summaryOverride ?? c.summary ?? c.id,
70
+ backgroundColor: c.backgroundColor,
71
+ foregroundColor: c.foregroundColor,
72
+ primary: Boolean(c.primary),
73
+ accessRole: c.accessRole ?? '',
74
+ selected: c.selected,
75
+ });
76
+ }
77
+ pageToken = body.nextPageToken;
78
+ } while (pageToken);
79
+ return out;
80
+ }
81
+ const noMillis = (iso) => iso.replace(/\.\d{3}Z$/, 'Z');
82
+ /** Fetches all pages of events. With syncToken, no time bounds are sent (the API forbids them). */
83
+ export async function fetchEvents(token, calendarId, opts) {
84
+ const items = [];
85
+ let pageToken;
86
+ let nextSyncToken;
87
+ do {
88
+ const u = new URL(`${BASE}/calendars/${encodeURIComponent(calendarId)}/events`);
89
+ u.searchParams.set('maxResults', '250');
90
+ if (opts.syncToken) {
91
+ u.searchParams.set('syncToken', opts.syncToken);
92
+ }
93
+ else {
94
+ u.searchParams.set('singleEvents', 'true');
95
+ if (opts.timeMin)
96
+ u.searchParams.set('timeMin', noMillis(opts.timeMin));
97
+ if (opts.timeMax)
98
+ u.searchParams.set('timeMax', noMillis(opts.timeMax));
99
+ }
100
+ if (pageToken)
101
+ u.searchParams.set('pageToken', pageToken);
102
+ const body = await request(u.toString(), token);
103
+ items.push(...(body.items ?? []));
104
+ pageToken = body.nextPageToken;
105
+ nextSyncToken = body.nextSyncToken ?? nextSyncToken;
106
+ } while (pageToken);
107
+ return { items, nextSyncToken };
108
+ }
@@ -0,0 +1,8 @@
1
+ import type { CalendarEvent, CalendarInfo } from './types.js';
2
+ /** Stored selection filtered to existing calendars; falls back to primary (else first). Primary is not forced. */
3
+ export declare function reconcileSelection(stored: string[] | null, calendars: CalendarInfo[]): string[];
4
+ /** Applies an incremental page of raw items to one calendar's events (cancelled items remove). */
5
+ export declare function applyChanges(existing: CalendarEvent[], items: any[], ctx: {
6
+ sourceId: string;
7
+ calendarId: string;
8
+ }): CalendarEvent[];
@@ -0,0 +1,20 @@
1
+ import { reconcileSelectedCalendarIds } from './events.js';
2
+ import { parseEvent } from './events.js';
3
+ /** Stored selection filtered to existing calendars; falls back to primary (else first). Primary is not forced. */
4
+ export function reconcileSelection(stored, calendars) {
5
+ return reconcileSelectedCalendarIds(stored, calendars);
6
+ }
7
+ /** Applies an incremental page of raw items to one calendar's events (cancelled items remove). */
8
+ export function applyChanges(existing, items, ctx) {
9
+ const map = new Map(existing.map((e) => [e.id, e]));
10
+ for (const raw of items) {
11
+ if (!raw || typeof raw.id !== 'string')
12
+ continue;
13
+ const parsed = parseEvent(raw, ctx);
14
+ if (parsed)
15
+ map.set(parsed.id, parsed);
16
+ else
17
+ map.delete(raw.id);
18
+ }
19
+ return [...map.values()];
20
+ }
@@ -0,0 +1,25 @@
1
+ import type { CalendarEvent } from './types.js';
2
+ export interface SyncState {
3
+ /** Per-calendar incremental sync tokens. */
4
+ tokens: Record<string, string>;
5
+ /** Per-calendar ms epoch of last full sync (optional; absent on 0.1.0 rows). */
6
+ lastFullSyncAt?: Record<string, number>;
7
+ /** ms epoch of last successful sync pass (optional; absent on 0.1.0 rows). */
8
+ lastSyncAt?: number;
9
+ }
10
+ /** Persisted per-source row. `events` is an encrypted JSON string when written by the controller. */
11
+ export interface StorageRow {
12
+ events: CalendarEvent[] | string | unknown[];
13
+ syncState: SyncState | string | null | unknown;
14
+ selectedIds: string[];
15
+ }
16
+ export interface StorageAdapter {
17
+ get(sourceId: string): Promise<StorageRow | null>;
18
+ put(sourceId: string, row: StorageRow): Promise<void>;
19
+ delete(sourceId: string): Promise<void>;
20
+ clear(sourceId: string): Promise<void>;
21
+ }
22
+ export interface CryptoAdapter {
23
+ encrypt(plaintext: string): Promise<string>;
24
+ decrypt(ciphertext: string): Promise<string>;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { CalendarEvent } from './types.js';
2
+ export interface CalendarSyncResult {
3
+ events: CalendarEvent[];
4
+ syncToken?: string;
5
+ /** True when a full (windowed) sync was performed. */
6
+ full: boolean;
7
+ }
8
+ export declare const FULL_SYNC_MAX_AGE_MS: number;
9
+ /** Syncs one calendar: incremental when a token exists, full (windowed) otherwise or on 410. */
10
+ export declare function syncCalendar(args: {
11
+ token: string;
12
+ sourceId: string;
13
+ calendarId: string;
14
+ existing: CalendarEvent[];
15
+ syncToken?: string;
16
+ lastFullSyncAt?: number;
17
+ now: Date;
18
+ }): Promise<CalendarSyncResult>;
@@ -0,0 +1,30 @@
1
+ import { CalendarFetchError, fetchEvents } from './fetch.js';
2
+ import { getRollingWindow, parseEvents } from './events.js';
3
+ import { applyChanges } from './reconcile.js';
4
+ export const FULL_SYNC_MAX_AGE_MS = 24 * 3600_000;
5
+ /** Syncs one calendar: incremental when a token exists, full (windowed) otherwise or on 410. */
6
+ export async function syncCalendar(args) {
7
+ const { token, sourceId, calendarId, existing, syncToken, lastFullSyncAt, now } = args;
8
+ const ctx = { sourceId, calendarId };
9
+ const full = async () => {
10
+ const w = getRollingWindow(now);
11
+ const page = await fetchEvents(token, calendarId, { timeMin: w.start, timeMax: w.end });
12
+ return { events: parseEvents(page.items, ctx), syncToken: page.nextSyncToken, full: true };
13
+ };
14
+ const stale = lastFullSyncAt === undefined || now.getTime() - lastFullSyncAt > FULL_SYNC_MAX_AGE_MS;
15
+ if (!syncToken || stale)
16
+ return full();
17
+ try {
18
+ const page = await fetchEvents(token, calendarId, { syncToken });
19
+ return {
20
+ events: applyChanges(existing, page.items, ctx),
21
+ syncToken: page.nextSyncToken ?? syncToken,
22
+ full: false,
23
+ };
24
+ }
25
+ catch (e) {
26
+ if (e instanceof CalendarFetchError && e.kind === 'gone')
27
+ return full();
28
+ throw e;
29
+ }
30
+ }
@@ -0,0 +1,45 @@
1
+ /** A connected account that calendars are read from. */
2
+ export interface Source {
3
+ id: string;
4
+ email: string;
5
+ label?: string;
6
+ }
7
+ export type RsvpStatus = 'accepted' | 'tentative' | 'needsAction' | 'declined';
8
+ export interface CalendarEventDateTime {
9
+ dateTime?: string;
10
+ date?: string;
11
+ timeZone?: string;
12
+ }
13
+ export interface CalendarEvent {
14
+ id: string;
15
+ /** ID of the Source (account) this event came from. */
16
+ sourceId: string;
17
+ /** ID of the calendar this event belongs to. */
18
+ calendarId?: string;
19
+ summary: string;
20
+ start: CalendarEventDateTime;
21
+ end: CalendarEventDateTime;
22
+ /** True iff `start.date` is present with no `start.dateTime`. */
23
+ isAllDay: boolean;
24
+ selfResponseStatus: RsvpStatus;
25
+ joinUrl: string | null;
26
+ organizer: {
27
+ displayName?: string;
28
+ email?: string;
29
+ self: boolean;
30
+ };
31
+ htmlLink: string;
32
+ }
33
+ export interface CalendarInfo {
34
+ id: string;
35
+ summary: string;
36
+ backgroundColor?: string;
37
+ foregroundColor?: string;
38
+ primary: boolean;
39
+ accessRole: string;
40
+ selected?: boolean;
41
+ }
42
+ export interface ParseContext {
43
+ sourceId: string;
44
+ calendarId: string;
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export * from './core/types.js';
2
+ export * from './core/events.js';
3
+ export * from './core/storage.js';
4
+ export * from './core/fetch.js';
5
+ export * from './core/sync.js';
6
+ export * from './core/reconcile.js';
7
+ export * from './core/controller.js';
8
+ export * from './react/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './core/types.js';
2
+ export * from './core/events.js';
3
+ export * from './core/storage.js';
4
+ export * from './core/fetch.js';
5
+ export * from './core/sync.js';
6
+ export * from './core/reconcile.js';
7
+ export * from './core/controller.js';
8
+ export * from './react/index.js';
@@ -0,0 +1,23 @@
1
+ import { type JSX, type ReactNode } from 'react';
2
+ import type { CalendarEvent, RsvpStatus, Source } from '../core/types.js';
3
+ import type { CalendarController } from '../core/controller.js';
4
+ export type RsvpSelection = Set<RsvpStatus>;
5
+ export interface CalendarPaneProps {
6
+ sources: Source[];
7
+ controller: CalendarController;
8
+ selectedRsvpStatuses: RsvpSelection;
9
+ onRsvpStatusesChange: (next: RsvpSelection) => void;
10
+ renderEventAction?: (event: CalendarEvent) => ReactNode;
11
+ emptyState?: ReactNode;
12
+ sourceNotice?: (source: Source) => ReactNode;
13
+ onClose?: () => void;
14
+ /** Whether Google Calendar is connected. Defaults to `sources.length > 0`. */
15
+ connected?: boolean;
16
+ /** Whether the connection needs reauthorization. Defaults to all sources being 'needs-scope'. */
17
+ needsReauth?: boolean;
18
+ onConnect?: () => void;
19
+ onReconnect?: () => void;
20
+ /** Window name used for the event popup. */
21
+ popupName?: string;
22
+ }
23
+ export declare function CalendarPane(props: CalendarPaneProps): JSX.Element;