@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,328 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { filterByRsvp, groupByDay, getOrganizerLabel, formatDayHeader, isEventCurrent, findNextUpcomingEvent, getCalendarEventKey, DEFAULT_RSVP_STATUSES, } from '../core/events.js';
4
+ import { CalendarSelector } from './CalendarSelector.js';
5
+ function useWindowWidth() {
6
+ const [width, setWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024);
7
+ useEffect(() => {
8
+ const handler = () => setWidth(window.innerWidth);
9
+ window.addEventListener('resize', handler);
10
+ return () => window.removeEventListener('resize', handler);
11
+ }, []);
12
+ return width;
13
+ }
14
+ const SOURCE_COLORS = ['#4285f4', '#ea4335', '#34a853', '#fbbc04', '#a142f4', '#00acc1', '#ff7043', '#8d6e63'];
15
+ const RSVP_CHIPS = [
16
+ {
17
+ status: 'accepted',
18
+ label: 'Accepted',
19
+ icon: (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M3 7.7L6 10.7L12 4.3", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })),
20
+ },
21
+ {
22
+ status: 'tentative',
23
+ label: 'Maybe',
24
+ icon: (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: [_jsx("path", { d: "M5.2 5.6C5.2 4.2 6.2 3.2 7.5 3.2C8.8 3.2 9.8 4.2 9.8 5.4C9.8 6.8 7.6 6.9 7.6 8.8", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("circle", { cx: "7.6", cy: "11.2", r: "0.9", fill: "currentColor" })] })),
25
+ },
26
+ {
27
+ status: 'needsAction',
28
+ label: 'Unaccepted',
29
+ icon: (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("circle", { cx: "7.5", cy: "7.5", r: "4.3", stroke: "currentColor", strokeWidth: "1.4" }) })),
30
+ },
31
+ {
32
+ status: 'declined',
33
+ label: 'Declined',
34
+ icon: (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M4 4L11 11M11 4L4 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) })),
35
+ },
36
+ ];
37
+ const DEFAULT_POPUP_NAME = 'notesdiary-cal-event';
38
+ const POPUP_W = 560;
39
+ const POPUP_H = 760;
40
+ function openEventPopup(url, popupRef, popupName) {
41
+ const existingPopup = popupRef.current;
42
+ if (existingPopup && !existingPopup.closed) {
43
+ try {
44
+ existingPopup.location = url;
45
+ existingPopup.focus();
46
+ return;
47
+ }
48
+ catch {
49
+ popupRef.current = null;
50
+ }
51
+ }
52
+ const left = Math.max(0, Math.round((window.screenX ?? 0) + (window.outerWidth - POPUP_W) / 2));
53
+ const top = Math.max(0, Math.round((window.screenY ?? 0) + (window.outerHeight - POPUP_H) / 2));
54
+ const popup = window.open(url, popupName, `popup,width=${POPUP_W},height=${POPUP_H},left=${left},top=${top}`);
55
+ if (!popup) {
56
+ window.open(url, '_blank');
57
+ return;
58
+ }
59
+ popupRef.current = popup;
60
+ try {
61
+ popup.opener = null;
62
+ }
63
+ catch {
64
+ // ignore
65
+ }
66
+ popup.focus();
67
+ }
68
+ function toIsoDate(d) {
69
+ return d.toISOString().slice(0, 10);
70
+ }
71
+ function formatTimeOfDay(dateTime) {
72
+ const d = new Date(dateTime);
73
+ return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
74
+ }
75
+ function formatTimeRange(event) {
76
+ const startDateTime = event.start.dateTime;
77
+ const endDateTime = event.end.dateTime;
78
+ if (!startDateTime)
79
+ return '';
80
+ const start = formatTimeOfDay(startDateTime);
81
+ if (!endDateTime)
82
+ return start;
83
+ return `${start} – ${formatTimeOfDay(endDateTime)}`;
84
+ }
85
+ function usableUrl(url) {
86
+ if (typeof url !== 'string')
87
+ return null;
88
+ const trimmed = url.trim();
89
+ return trimmed === '' ? null : trimmed;
90
+ }
91
+ export function CalendarPane(props) {
92
+ const isDesktop = useWindowWidth() >= 960;
93
+ const { sources, controller, selectedRsvpStatuses } = props;
94
+ const popupName = props.popupName ?? DEFAULT_POPUP_NAME;
95
+ const [version, setVersion] = useState(0);
96
+ useEffect(() => {
97
+ const unsubscribe = controller.subscribe(() => setVersion((v) => v + 1));
98
+ setVersion((v) => v + 1);
99
+ return unsubscribe;
100
+ }, [controller]);
101
+ useEffect(() => {
102
+ if (typeof controller.hydrate === 'function')
103
+ void Promise.resolve(controller.hydrate()).catch(() => { });
104
+ }, [controller]);
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ const events = useMemo(() => controller.getEvents(), [controller, version]);
107
+ const statuses = controller.getSourceStatus();
108
+ const connected = props.connected ?? sources.length > 0;
109
+ const ctrlState = controller.getState();
110
+ const multi = sources.length >= 2;
111
+ const needsScopeSources = sources.filter((s) => statuses[s.id] === 'needs-scope');
112
+ const needsReauth = connected && (props.needsReauth ?? (sources.length > 0 && needsScopeSources.length === sources.length));
113
+ const sourceColor = (id) => SOURCE_COLORS[Math.max(0, sources.findIndex((s) => s.id === id)) % SOURCE_COLORS.length];
114
+ const sourceEmail = (id) => sources.find((s) => s.id === id)?.email ?? '';
115
+ const todayIso = toIsoDate(new Date());
116
+ const dayHeaderRefs = useRef(new Map());
117
+ const timedEventRefs = useRef(new Map());
118
+ const hasScrolledForEventsRef = useRef(false);
119
+ const paneRef = useRef(null);
120
+ const chipsRef = useRef(null);
121
+ const popupRef = useRef(null);
122
+ const [now, setNow] = useState(() => new Date());
123
+ const filteredEvents = useMemo(() => filterByRsvp(events, selectedRsvpStatuses), [events, selectedRsvpStatuses]);
124
+ const calendarsByKey = useMemo(() => {
125
+ const map = new Map();
126
+ for (const s of sources)
127
+ for (const c of controller.getCalendars(s.id))
128
+ map.set(`${s.id}\u0000${c.id}`, c);
129
+ return map;
130
+ // eslint-disable-next-line react-hooks/exhaustive-deps
131
+ }, [sources, controller, version]);
132
+ const currentEventKeys = useMemo(() => {
133
+ const ids = new Set();
134
+ for (const event of filteredEvents) {
135
+ if (isEventCurrent(event, now)) {
136
+ ids.add(getCalendarEventKey(event));
137
+ }
138
+ }
139
+ return ids;
140
+ }, [filteredEvents, now]);
141
+ const nextUpcomingEvent = findNextUpcomingEvent(filteredEvents, now);
142
+ const selectedEventKey = currentEventKeys.values().next().value ??
143
+ (nextUpcomingEvent ? getCalendarEventKey(nextUpcomingEvent) : undefined);
144
+ function handleToggle(status) {
145
+ const next = new Set(selectedRsvpStatuses);
146
+ if (next.has(status)) {
147
+ next.delete(status);
148
+ if (next.size === 0) {
149
+ props.onRsvpStatusesChange(new Set(DEFAULT_RSVP_STATUSES));
150
+ return;
151
+ }
152
+ }
153
+ else {
154
+ next.add(status);
155
+ }
156
+ props.onRsvpStatusesChange(next);
157
+ }
158
+ const dayGroups = groupByDay(filteredEvents, todayIso);
159
+ function scrollToToday(behavior = 'auto') {
160
+ const todayEl = dayHeaderRefs.current.get(todayIso);
161
+ if (todayEl && typeof todayEl.scrollIntoView === 'function') {
162
+ todayEl.scrollIntoView({ behavior, block: 'start' });
163
+ }
164
+ }
165
+ function recenter(behavior) {
166
+ const el = selectedEventKey ? timedEventRefs.current.get(selectedEventKey) : undefined;
167
+ if (el && typeof el.scrollIntoView === 'function') {
168
+ el.scrollIntoView({ behavior, block: 'center' });
169
+ return;
170
+ }
171
+ scrollToToday(behavior);
172
+ }
173
+ const recenterRef = useRef(recenter);
174
+ recenterRef.current = recenter;
175
+ const idleRecenteredRef = useRef(false);
176
+ useEffect(() => {
177
+ let idleTimeoutId;
178
+ function fireIdle() {
179
+ recenterRef.current('smooth');
180
+ idleRecenteredRef.current = true;
181
+ }
182
+ function onActivity() {
183
+ if (idleRecenteredRef.current) {
184
+ idleRecenteredRef.current = false;
185
+ }
186
+ clearTimeout(idleTimeoutId);
187
+ idleTimeoutId = setTimeout(fireIdle, 30000);
188
+ }
189
+ window.addEventListener('mousemove', onActivity);
190
+ window.addEventListener('keydown', onActivity);
191
+ window.addEventListener('click', onActivity);
192
+ window.addEventListener('scroll', onActivity, { passive: true });
193
+ window.addEventListener('touchstart', onActivity, { passive: true });
194
+ onActivity();
195
+ return () => {
196
+ window.removeEventListener('mousemove', onActivity);
197
+ window.removeEventListener('keydown', onActivity);
198
+ window.removeEventListener('click', onActivity);
199
+ window.removeEventListener('scroll', onActivity);
200
+ window.removeEventListener('touchstart', onActivity);
201
+ clearTimeout(idleTimeoutId);
202
+ };
203
+ }, []);
204
+ useEffect(() => {
205
+ if (events.length === 0) {
206
+ hasScrolledForEventsRef.current = false;
207
+ return;
208
+ }
209
+ if (hasScrolledForEventsRef.current)
210
+ return;
211
+ recenterRef.current('auto');
212
+ hasScrolledForEventsRef.current = true;
213
+ }, [events, todayIso]);
214
+ useEffect(() => {
215
+ let timeoutId;
216
+ function scheduleNext() {
217
+ const nowMs = Date.now();
218
+ let nextBoundaryMs = Infinity;
219
+ for (const event of filteredEvents) {
220
+ if (event.isAllDay)
221
+ continue;
222
+ const startMs = event.start.dateTime ? new Date(event.start.dateTime).getTime() : NaN;
223
+ const endMs = event.end.dateTime ? new Date(event.end.dateTime).getTime() : NaN;
224
+ if (!Number.isNaN(startMs) && startMs > nowMs && startMs < nextBoundaryMs) {
225
+ nextBoundaryMs = startMs;
226
+ }
227
+ if (!Number.isNaN(endMs) && endMs > nowMs && endMs < nextBoundaryMs) {
228
+ nextBoundaryMs = endMs;
229
+ }
230
+ }
231
+ if (nextBoundaryMs !== Infinity) {
232
+ timeoutId = setTimeout(() => {
233
+ setNow(new Date());
234
+ setTimeout(() => {
235
+ if (idleRecenteredRef.current) {
236
+ recenterRef.current('smooth');
237
+ }
238
+ }, 0);
239
+ scheduleNext();
240
+ }, nextBoundaryMs - nowMs);
241
+ }
242
+ }
243
+ setNow(new Date());
244
+ scheduleNext();
245
+ return () => {
246
+ if (timeoutId !== undefined)
247
+ clearTimeout(timeoutId);
248
+ };
249
+ }, [filteredEvents]);
250
+ useEffect(() => {
251
+ const chipsEl = chipsRef.current;
252
+ const paneEl = paneRef.current;
253
+ if (!chipsEl || !paneEl)
254
+ return;
255
+ const observer = new ResizeObserver((entries) => {
256
+ const entry = entries[0];
257
+ if (!entry)
258
+ return;
259
+ // contentRect excludes padding; the sticky day-header offset needs the
260
+ // chip row's full border-box height (chips have vertical padding).
261
+ const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.target.getBoundingClientRect().height;
262
+ paneEl.style.setProperty('--chip-row-height', `${height}px`);
263
+ });
264
+ observer.observe(chipsEl);
265
+ return () => observer.disconnect();
266
+ }, [connected, needsReauth]);
267
+ if (!connected) {
268
+ return (_jsx("div", { className: "calendar-pane", children: _jsx("div", { className: "calendar-pane-prompt", children: props.emptyState ?? (_jsxs(_Fragment, { children: [_jsx("p", { children: "Not connected to Google Calendar." }), _jsx("button", { className: "calendar-connect-button", onClick: props.onConnect, children: "Connect Google Calendar" })] })) }) }));
269
+ }
270
+ if (needsReauth) {
271
+ return (_jsx("div", { className: "calendar-pane", children: _jsx("div", { className: "calendar-pane-prompt", children: props.sourceNotice && needsScopeSources.length > 0 ? (needsScopeSources.map((s) => _jsx("div", { children: props.sourceNotice?.(s) }, s.id))) : (_jsxs(_Fragment, { children: [_jsx("p", { children: "Calendar connection needs reauthorization." }), _jsx("button", { className: "calendar-connect-button", onClick: props.onReconnect, children: "Reconnect to restore sync" })] })) }) }));
272
+ }
273
+ const sourcesWithEvents = new Set(filteredEvents.map((e) => e.sourceId));
274
+ const hasErrorWithoutEvents = sources.some((s) => statuses[s.id] === 'error' && !sourcesWithEvents.has(s.id));
275
+ const calendarSources = sources.filter((s) => controller.getCalendars(s.id).length > 0);
276
+ const error = hasErrorWithoutEvents || (ctrlState.error && filteredEvents.length === 0);
277
+ const loading = ctrlState.loading && filteredEvents.length === 0;
278
+ const emptyStateText = 'No matching events in the next 30 days';
279
+ const showCalendarSelector = calendarSources.length > 0;
280
+ return (_jsxs("div", { className: "calendar-pane", ref: paneRef, children: [_jsxs("div", { className: "calendar-pane-scroll", children: [_jsxs("div", { className: "calendar-rsvp-chips", ref: chipsRef, children: [_jsx("button", { type: "button", className: "calendar-rsvp-chip calendar-scroll-today-btn", "aria-label": "Scroll to today", title: "Scroll to today", onClick: () => recenter('smooth'), children: _jsxs("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: [_jsx("rect", { x: "2", y: "3", width: "11", height: "10", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M2 6H13", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M5 2V4M10 2V4", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" }), _jsx("circle", { cx: "7.5", cy: "9.5", r: "1.3", fill: "currentColor" })] }) }), RSVP_CHIPS.map((chip) => (_jsx("button", { type: "button", className: `calendar-rsvp-chip${selectedRsvpStatuses.has(chip.status) ? ' selected' : ''}`, "aria-pressed": selectedRsvpStatuses.has(chip.status), "aria-label": chip.label, title: chip.label, onClick: () => handleToggle(chip.status), children: chip.icon }, chip.status)))] }), needsScopeSources.map((s) => (_jsx("div", { children: props.sourceNotice?.(s) }, s.id))), error && (_jsx("div", { className: "empty-state calendar-empty-state", role: "alert", children: _jsx("p", { children: "Couldn't load calendar events. Try again shortly." }) })), !(error && filteredEvents.length === 0) && (_jsxs("div", { className: "calendar-day-groups", children: [dayGroups.map((group) => (_jsxs("div", { className: "calendar-day-group", children: [_jsx("div", { className: "calendar-day-header", ref: (el) => {
281
+ if (el) {
282
+ dayHeaderRefs.current.set(group.dateKey, el);
283
+ }
284
+ else {
285
+ dayHeaderRefs.current.delete(group.dateKey);
286
+ }
287
+ }, children: formatDayHeader(group.dateKey, todayIso) }), group.events.map((event) => {
288
+ const eventKey = getCalendarEventKey(event);
289
+ const calendar = calendarsByKey.get(`${event.sourceId}\u0000${event.calendarId}`);
290
+ const showCalendarLabel = calendar && !calendar.primary;
291
+ const organizerLabel = getOrganizerLabel(event);
292
+ const showOrganizerLabel = multi
293
+ ? true
294
+ : organizerLabel && (!showCalendarLabel || organizerLabel !== calendar.summary);
295
+ const organizerText = multi
296
+ ? organizerLabel
297
+ ? `${organizerLabel} · ${sourceEmail(event.sourceId)}`
298
+ : sourceEmail(event.sourceId)
299
+ : organizerLabel;
300
+ const rsvpStatus = RSVP_CHIPS.some((chip) => chip.status === event.selfResponseStatus)
301
+ ? event.selfResponseStatus
302
+ : 'needsAction';
303
+ if (event.isAllDay) {
304
+ return (_jsxs("div", { className: `calendar-event-allday calendar-event-row--rsvp-${rsvpStatus}`, children: [_jsxs("span", { className: "calendar-event-title-wrap", children: [_jsx("span", { className: "calendar-event-title", children: event.summary }), props.renderEventAction?.(event)] }), showCalendarLabel && (_jsxs("span", { className: "calendar-event-calendar", children: [_jsx("span", { className: "calendar-event-calendar-dot", style: { backgroundColor: calendar.backgroundColor } }), calendar.summary] })), showOrganizerLabel && (_jsxs("span", { className: "calendar-event-organizer", children: [multi && (_jsx("span", { className: "calendar-event-source-dot", style: { backgroundColor: sourceColor(event.sourceId) } })), organizerText] }))] }, eventKey));
305
+ }
306
+ const hasLink = usableUrl(event.htmlLink) !== null;
307
+ return (_jsxs("div", { className: `calendar-event-row${currentEventKeys.has(eventKey) ? ' calendar-event-row--current' : ''}${selectedEventKey === eventKey ? ' calendar-event-row--selected' : ''} calendar-event-row--rsvp-${rsvpStatus}`, ref: (el) => {
308
+ if (el) {
309
+ timedEventRefs.current.set(eventKey, el);
310
+ }
311
+ else {
312
+ timedEventRefs.current.delete(eventKey);
313
+ }
314
+ }, children: [_jsxs("div", { className: "calendar-event-header", children: [_jsx("span", { className: "calendar-event-time", children: formatTimeRange(event) }), event.joinUrl && (_jsx("a", { className: "calendar-event-join", href: event.joinUrl, target: "_blank", rel: "noopener noreferrer", onClick: (e) => e.stopPropagation(), children: "Join" }))] }), _jsxs("div", { className: "calendar-event-title-wrap", children: [hasLink ? (_jsx("a", { className: "calendar-event-title-link", href: event.htmlLink, target: "_blank", rel: "noopener noreferrer", onClick: (e) => {
315
+ if (isDesktop &&
316
+ e.button === 0 &&
317
+ !e.ctrlKey &&
318
+ !e.metaKey &&
319
+ !e.shiftKey &&
320
+ !e.altKey) {
321
+ e.preventDefault();
322
+ openEventPopup(event.htmlLink, popupRef, popupName);
323
+ }
324
+ }, children: _jsx("span", { className: "calendar-event-title", children: event.summary }) })) : (_jsx("span", { className: "calendar-event-title", children: event.summary })), props.renderEventAction?.(event)] }), showCalendarLabel && (_jsxs("span", { className: "calendar-event-calendar", children: [_jsx("span", { className: "calendar-event-calendar-dot", style: { backgroundColor: calendar.backgroundColor } }), calendar.summary] })), showOrganizerLabel && (_jsxs("span", { className: "calendar-event-organizer", children: [multi && (_jsx("span", { className: "calendar-event-source-dot", style: { backgroundColor: sourceColor(event.sourceId) } })), organizerText] }))] }, eventKey));
325
+ })] }, group.dateKey))), loading && _jsx("p", { className: "calendar-empty-state-text", children: "Loading calendar\u2026" }), filteredEvents.length === 0 && !loading && !error && needsScopeSources.length === 0 && _jsx("p", { className: "calendar-empty-state-text", children: emptyStateText })] }))] }), showCalendarSelector && (_jsx("footer", { className: "calendar-pane-footer", children: calendarSources.map((s) => (_jsx(CalendarSelector, { calendars: controller.getCalendars(s.id), selectedIds: controller.getSelectedIds(s.id), onChange: (ids) => {
326
+ void controller.setSelectedIds(s.id, ids);
327
+ }, heading: multi ? s.email : undefined }, s.id))) }))] }));
328
+ }
@@ -0,0 +1,10 @@
1
+ import type { CalendarInfo } from '../core/types.js';
2
+ interface CalendarSelectorProps {
3
+ calendars: CalendarInfo[];
4
+ selectedIds: string[];
5
+ onChange: (selectedIds: string[]) => void;
6
+ /** Optional account heading (used when several sources are shown). */
7
+ heading?: string;
8
+ }
9
+ export declare function CalendarSelector({ calendars, selectedIds, onChange, heading }: CalendarSelectorProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from 'react';
3
+ import { filterCalendarsByName, summarizeCalendarSelection } from '../core/events.js';
4
+ export function CalendarSelector({ calendars, selectedIds, onChange, heading }) {
5
+ const [open, setOpen] = useState(false);
6
+ const [query, setQuery] = useState('');
7
+ const [activeIndex, setActiveIndex] = useState(0);
8
+ const buttonRef = useRef(null);
9
+ const searchRef = useRef(null);
10
+ const rootRef = useRef(null);
11
+ const listboxId = useId();
12
+ const visibleCalendars = filterCalendarsByName(calendars, query);
13
+ const primaryId = calendars.find((calendar) => calendar.primary)?.id ?? calendars[0]?.id;
14
+ useEffect(() => {
15
+ if (open)
16
+ searchRef.current?.focus();
17
+ }, [open]);
18
+ useEffect(() => {
19
+ if (!open)
20
+ return;
21
+ function onMouseDown(event) {
22
+ if (rootRef.current && !rootRef.current.contains(event.target))
23
+ setOpen(false);
24
+ }
25
+ function onKeyDown(event) {
26
+ if (event.key === 'Escape')
27
+ setOpen(false);
28
+ }
29
+ document.addEventListener('mousedown', onMouseDown);
30
+ document.addEventListener('keydown', onKeyDown);
31
+ return () => {
32
+ document.removeEventListener('mousedown', onMouseDown);
33
+ document.removeEventListener('keydown', onKeyDown);
34
+ };
35
+ }, [open]);
36
+ function close() {
37
+ setOpen(false);
38
+ buttonRef.current?.focus();
39
+ }
40
+ function toggle(id) {
41
+ if (selectedIds.includes(id)) {
42
+ if (selectedIds.length === 1)
43
+ return;
44
+ onChange(selectedIds.filter((selectedId) => selectedId !== id));
45
+ return;
46
+ }
47
+ onChange([...selectedIds, id]);
48
+ }
49
+ function selectAll() {
50
+ const visibleIds = visibleCalendars.map((calendar) => calendar.id);
51
+ onChange([...selectedIds, ...visibleIds.filter((id) => !selectedIds.includes(id))]);
52
+ }
53
+ function clear() {
54
+ onChange(primaryId ? [primaryId] : []);
55
+ }
56
+ function handleListboxKeyDown(event) {
57
+ if (event.key === 'Escape') {
58
+ event.preventDefault();
59
+ close();
60
+ return;
61
+ }
62
+ if (visibleCalendars.length === 0)
63
+ return;
64
+ if (event.key === 'ArrowDown') {
65
+ event.preventDefault();
66
+ setActiveIndex((index) => (index + 1) % visibleCalendars.length);
67
+ return;
68
+ }
69
+ if (event.key === 'ArrowUp') {
70
+ event.preventDefault();
71
+ setActiveIndex((index) => (index - 1 + visibleCalendars.length) % visibleCalendars.length);
72
+ return;
73
+ }
74
+ if (event.key === ' ' || event.key === 'Enter') {
75
+ event.preventDefault();
76
+ const activeCalendar = visibleCalendars[activeIndex];
77
+ if (activeCalendar)
78
+ toggle(activeCalendar.id);
79
+ }
80
+ }
81
+ const label = summarizeCalendarSelection(selectedIds, calendars);
82
+ return (_jsxs("div", { className: "calendar-selector", ref: rootRef, children: [_jsx("button", { ref: buttonRef, type: "button", role: "combobox", "aria-label": label, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", onClick: () => {
83
+ setOpen((isOpen) => !isOpen);
84
+ setQuery('');
85
+ setActiveIndex(0);
86
+ }, children: label }), open && (_jsxs("div", { className: "calendar-selector-dropdown calendar-selector-dropdown--upward", onKeyDown: handleListboxKeyDown, children: [heading && (_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: heading })), _jsx("input", { ref: searchRef, type: "search", role: "searchbox", "aria-label": "Search calendars", value: query, onChange: (event) => {
87
+ setQuery(event.target.value);
88
+ setActiveIndex(0);
89
+ } }), _jsxs("div", { className: "calendar-selector-actions", children: [_jsx("button", { type: "button", onClick: selectAll, children: "Select all" }), _jsx("button", { type: "button", onClick: clear, children: "Clear" }), _jsx("button", { type: "button", onClick: close, children: "Done" })] }), _jsx("div", { id: listboxId, role: "listbox", "aria-multiselectable": "true", "aria-activedescendant": visibleCalendars[activeIndex] ? `${listboxId}-${visibleCalendars[activeIndex].id}` : undefined, tabIndex: -1, children: calendars.length === 0 ? (_jsx("p", { children: "No calendars available" })) : visibleCalendars.length === 0 ? (_jsx("p", { children: "No calendars found" })) : (visibleCalendars.map((calendar, index) => (_jsxs("button", { type: "button", id: `${listboxId}-${calendar.id}`, role: "option", title: calendar.summary, "aria-selected": selectedIds.includes(calendar.id), "data-active": index === activeIndex || undefined, onClick: () => toggle(calendar.id), children: [_jsx("input", { type: "checkbox", className: "calendar-selector-checkbox", checked: selectedIds.includes(calendar.id), readOnly: true, tabIndex: -1, "aria-hidden": "true" }), _jsx("span", { "aria-hidden": "true", className: "calendar-selector-dot", "data-testid": `calendar-color-${calendar.id}`, style: { backgroundColor: calendar.backgroundColor } }), _jsx("span", { className: "calendar-selector-name", title: calendar.summary, children: calendar.summary })] }, calendar.id)))) })] }))] }));
90
+ }
@@ -0,0 +1,3 @@
1
+ export { CalendarPane } from './CalendarPane.js';
2
+ export type { CalendarPaneProps, RsvpSelection } from './CalendarPane.js';
3
+ export { CalendarSelector } from './CalendarSelector.js';
@@ -0,0 +1,2 @@
1
+ export { CalendarPane } from './CalendarPane.js';
2
+ export { CalendarSelector } from './CalendarSelector.js';