@rightkit/workspace-ui 0.0.0-stage → 0.1.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,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@rightkit/react-shell/react";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import { DAY, WEEKDAY_LABELS, eventDay, hhmm, sameDay, startOfWeek } from "@rightkit/mail-ui/lib/calendarUtils";
5
+ export function MonthGrid({ cursor, events, today, onDay, onEvent }) {
6
+ const d = new Date(cursor);
7
+ const first = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
8
+ const gridStart = startOfWeek(first);
9
+ const month = d.getMonth();
10
+ const byDay = useMemo(() => {
11
+ const m = new Map();
12
+ for (const e of events) {
13
+ const k = eventDay(e);
14
+ (m.get(k) ?? m.set(k, []).get(k)).push(e);
15
+ }
16
+ return m;
17
+ }, [events]);
18
+ return (_jsxs("div", { className: "month-grid", children: [_jsx("div", { className: "month-dow", children: WEEKDAY_LABELS.map((w) => _jsx("div", { className: "dow-cell", children: w }, w)) }), _jsx("div", { className: "month-cells", children: Array.from({ length: 42 }, (_, i) => {
19
+ const ms = gridStart + i * DAY;
20
+ const day = new Date(ms);
21
+ const inMonth = day.getMonth() === month;
22
+ const isToday = ms === today;
23
+ const dayEvents = byDay.get(ms) ?? [];
24
+ return (_jsxs("div", { className: `month-cell ${inMonth ? "" : "out"} ${isToday ? "today" : ""}`, onClick: () => onDay(ms), children: [_jsx(Button, { variant: "ghost", className: "hit-overlay", "aria-label": day.toDateString() }), _jsx("div", { className: "month-daynum", children: _jsx("span", { className: isToday ? "today-pill" : "", children: day.getDate() }) }), _jsxs("div", { className: "month-events", children: [dayEvents.slice(0, 4).map((e) => (_jsxs(Button, { variant: "ghost", className: `cal-chip ${e.all_day ? "allday" : ""}`, title: e.summary, onClick: (ev) => { ev.stopPropagation(); onEvent(e); }, children: [!e.all_day && _jsx("span", { className: "chip-dot" }), !e.all_day && _jsx("span", { className: "chip-time", children: hhmm(e.start) }), _jsx("span", { className: "chip-name", children: e.summary || "(no title)" })] }, e.id))), dayEvents.length > 4 && _jsxs("span", { className: "chip-more", children: ["+", dayEvents.length - 4, " more"] })] })] }, i));
25
+ }) })] }));
26
+ }
27
+ const HOUR_PX = 48;
28
+ const HOURS = Array.from({ length: 24 }, (_, h) => h);
29
+ const formatHour = (h) => `${(h % 12) || 12} ${h < 12 ? "AM" : "PM"}`;
30
+ const minOf = (iso) => {
31
+ const d = new Date(iso);
32
+ return d.getHours() * 60 + d.getMinutes();
33
+ };
34
+ function packLanes(evs) {
35
+ const res = new Map();
36
+ const items = evs
37
+ .map((e) => {
38
+ const s = new Date(e.start).getTime();
39
+ const en = e.end ? new Date(e.end).getTime() : s + 3600000;
40
+ return { id: e.id, s, en: Math.max(en, s + 15 * 60000) };
41
+ })
42
+ .sort((a, b) => a.s - b.s);
43
+ let i = 0;
44
+ while (i < items.length) {
45
+ let clusterEnd = items[i].en;
46
+ const cluster = [items[i]];
47
+ let j = i;
48
+ while (j + 1 < items.length && items[j + 1].s < clusterEnd) {
49
+ j++;
50
+ cluster.push(items[j]);
51
+ clusterEnd = Math.max(clusterEnd, items[j].en);
52
+ }
53
+ const laneEnds = [];
54
+ for (const it of cluster) {
55
+ let lane = laneEnds.findIndex((end) => end <= it.s);
56
+ if (lane === -1) {
57
+ lane = laneEnds.length;
58
+ laneEnds.push(it.en);
59
+ }
60
+ else
61
+ laneEnds[lane] = it.en;
62
+ res.set(it.id, { lane, lanes: 0 });
63
+ }
64
+ for (const it of cluster)
65
+ res.get(it.id).lanes = laneEnds.length;
66
+ i = j + 1;
67
+ }
68
+ return res;
69
+ }
70
+ export function TimeGrid({ days, events, today, nowMin, onSlot, onEvent }) {
71
+ const bodyRef = useRef(null);
72
+ useEffect(() => {
73
+ if (bodyRef.current)
74
+ bodyRef.current.scrollTop = 7 * HOUR_PX;
75
+ }, []);
76
+ const cols = `64px repeat(${days.length}, 1fr)`;
77
+ return (_jsxs("div", { className: "time-grid", children: [_jsxs("div", { className: "tg-head", style: { gridTemplateColumns: cols }, children: [_jsx("div", { className: "tg-corner" }), days.map((ms) => {
78
+ const d = new Date(ms);
79
+ return (_jsxs("div", { className: `tg-day-head ${ms === today ? "today" : ""}`, children: [_jsx("span", { className: "tg-dow", children: WEEKDAY_LABELS[d.getDay()] }), _jsx("span", { className: `tg-daynum ${ms === today ? "today-pill" : ""}`, children: d.getDate() })] }, ms));
80
+ })] }), _jsxs("div", { className: "tg-allday", style: { gridTemplateColumns: cols }, children: [_jsx("div", { className: "tg-corner tg-allday-label", children: "all-day" }), days.map((ms) => {
81
+ const ad = events.filter((e) => e.all_day && sameDay(eventDay(e), ms));
82
+ return (_jsx("div", { className: "tg-allday-col", children: ad.map((e) => (_jsx(Button, { variant: "ghost", className: "cal-chip allday", title: e.summary, onClick: () => onEvent(e), children: _jsx("span", { className: "chip-name", children: e.summary || "(no title)" }) }, e.id))) }, ms));
83
+ })] }), _jsxs("div", { className: "tg-body", ref: bodyRef, style: { gridTemplateColumns: cols }, children: [_jsx("div", { className: "tg-gutter", children: HOURS.map((h) => (_jsx("div", { className: "tg-hour-label", style: { height: HOUR_PX }, children: h === 0 ? "" : formatHour(h) }, h))) }), days.map((ms) => {
84
+ const dayEvents = events
85
+ .filter((e) => !e.all_day && sameDay(eventDay(e), ms))
86
+ .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
87
+ const lanes = packLanes(dayEvents);
88
+ return (_jsxs("div", { className: `tg-col ${ms === today ? "today" : ""}`, style: { height: 24 * HOUR_PX }, onClick: (ev) => {
89
+ const rect = ev.currentTarget.getBoundingClientRect();
90
+ const hour = Math.max(0, Math.min(23, Math.floor((ev.clientY - rect.top) / HOUR_PX)));
91
+ onSlot(ms + hour * 3600000);
92
+ }, children: [HOURS.map((h) => (_jsx("div", { className: "tg-hline", style: { top: h * HOUR_PX } }, h))), ms === today && _jsx("div", { className: "tg-now", style: { top: (nowMin / 60) * HOUR_PX } }), dayEvents.map((e) => {
93
+ const startMin = minOf(e.start);
94
+ const rawEnd = e.end ? minOf(e.end) + (new Date(e.end).getDate() !== new Date(e.start).getDate() ? 1440 : 0) : startMin + 60;
95
+ const endMin = Math.max(startMin + 20, rawEnd);
96
+ const li = lanes.get(e.id) ?? { lane: 0, lanes: 1 };
97
+ const w = 100 / li.lanes;
98
+ return (_jsxs(Button, { variant: "ghost", className: "tg-event", style: {
99
+ top: (startMin / 60) * HOUR_PX,
100
+ height: ((endMin - startMin) / 60) * HOUR_PX,
101
+ left: `calc(${li.lane * w}% + 2px)`,
102
+ width: `calc(${w}% - 4px)`,
103
+ }, title: e.summary, onClick: (ev) => { ev.stopPropagation(); onEvent(e); }, children: [_jsx("span", { className: "tg-event-name", children: e.summary || "(no title)" }), _jsxs("span", { className: "tg-event-time", children: [hhmm(e.start), e.meet_link ? " · Meet" : ""] })] }, e.id));
104
+ })] }, ms));
105
+ })] })] }));
106
+ }
@@ -0,0 +1,3 @@
1
+ declare function CalendarViewInner(): import("react").JSX.Element;
2
+ export declare const CalendarView: import("react").MemoExoticComponent<typeof CalendarViewInner>;
3
+ export {};
@@ -0,0 +1,333 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, IconButton, Dialog, SegmentedControl } from "@rightkit/react-shell/react";
3
+ import { memo, useEffect, useRef, useState } from "react";
4
+ import Icon from "@rightkit/mail-ui/components/Icons";
5
+ import { AccountSwitcher } from "@rightkit/mail-ui/components/AccountSwitcher";
6
+ import { setPref } from "@rightkit/mail-ui/lib/preferences";
7
+ import { useAppStore } from "@rightkit/mail-ui/store/index";
8
+ import { showDialog } from "@rightkit/mail-ui/hooks/dialog";
9
+ import { listCalendars, listEvents, createEvent, updateEvent, deleteEvent, truncateEventSeries, rsvpEvent, freeBusy, } from "@rightkit/mail-ui/lib/ipc";
10
+ import { MonthGrid, TimeGrid } from "./CalendarGrids.js";
11
+ import { DAY, rangeFor, startOfDay, startOfWeek } from "@rightkit/mail-ui/lib/calendarUtils";
12
+ import { CalendarEventFormModal } from "./CalendarEventFormModal.js";
13
+ import { CalendarEventPopover } from "./CalendarEventPopover.js";
14
+ const CALENDAR_VIEWS = ["month", "week", "day"];
15
+ function CalendarViewInner() {
16
+ const [recurScope, setRecurScope] = useState(null);
17
+ const activeAccount = useAppStore((s) => s.activeAccount);
18
+ const setErr = useAppStore((s) => s.setErr);
19
+ const showUndo = useAppStore((s) => s.showUndo);
20
+ const setCalendars = useAppStore((s) => s.setCalendars);
21
+ const selectedCal = useAppStore((s) => s.selectedCal);
22
+ const setSelectedCal = useAppStore((s) => s.setSelectedCal);
23
+ const shownCals = useAppStore((s) => s.shownCals);
24
+ const setShownCals = useAppStore((s) => s.setShownCals);
25
+ const calView = useAppStore((s) => s.calView);
26
+ const setCalView = useAppStore((s) => s.setCalView);
27
+ const calCursor = useAppStore((s) => s.calCursor);
28
+ const setCalCursor = useAppStore((s) => s.setCalCursor);
29
+ const events = useAppStore((s) => s.events);
30
+ const setEvents = useAppStore((s) => s.setEvents);
31
+ const openEvent = useAppStore((s) => s.openEvent);
32
+ const setOpenEvent = useAppStore((s) => s.setOpenEvent);
33
+ const eventForm = useAppStore((s) => s.eventForm);
34
+ const setEventForm = useAppStore((s) => s.setEventForm);
35
+ const freeBusyMsg = useAppStore((s) => s.freeBusyMsg);
36
+ const setFreeBusyMsg = useAppStore((s) => s.setFreeBusyMsg);
37
+ const setCalendarBusy = useAppStore((s) => s.setCalendarBusy);
38
+ const eventSeed = useAppStore((s) => s.eventSeed);
39
+ const setEventSeed = useAppStore((s) => s.setEventSeed);
40
+ const [calendarNow] = useState(() => {
41
+ const now = new Date();
42
+ return {
43
+ today: startOfDay(now.getTime()),
44
+ minute: now.getHours() * 60 + now.getMinutes(),
45
+ };
46
+ });
47
+ async function reloadEvents() {
48
+ if (!activeAccount)
49
+ return;
50
+ const [minMs, maxMs] = rangeFor(calCursor, calView);
51
+ const min = new Date(minMs).toISOString();
52
+ const max = new Date(maxMs).toISOString();
53
+ const cals = Array.from(new Set(shownCals.length ? shownCals : [selectedCal]));
54
+ setCalendarBusy(true);
55
+ try {
56
+ const lists = await Promise.all(cals.map((c) => listEvents(activeAccount, c, min, max).catch(() => [])));
57
+ setEvents(lists.flat().sort((a, b) => {
58
+ const ax = a.start ? new Date(a.start).getTime() : 0;
59
+ const bx = b.start ? new Date(b.start).getTime() : 0;
60
+ return ax - bx;
61
+ }));
62
+ }
63
+ catch (e) {
64
+ setErr(String(e));
65
+ }
66
+ finally {
67
+ setCalendarBusy(false);
68
+ }
69
+ }
70
+ function toLocalInput(s) {
71
+ try {
72
+ const d = new Date(s);
73
+ return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
74
+ }
75
+ catch {
76
+ return "";
77
+ }
78
+ }
79
+ function buildRrule(f) {
80
+ if (!f.recurrence)
81
+ return null;
82
+ const parts = [`FREQ=${f.recurrence}`];
83
+ if (f.recurrence === "WEEKLY" && f.recurByday.length > 0)
84
+ parts.push(`BYDAY=${f.recurByday.join(",")}`);
85
+ if (f.recurEnd === "until" && f.recurUntil) {
86
+ const ymd = f.recurUntil.replace(/-/g, "");
87
+ parts.push(f.allDay ? `UNTIL=${ymd}` : `UNTIL=${ymd}T235959Z`);
88
+ }
89
+ else if (f.recurEnd === "count") {
90
+ const n = Math.max(1, parseInt(f.recurCount, 10) || 0);
91
+ if (n > 0)
92
+ parts.push(`COUNT=${n}`);
93
+ }
94
+ return `RRULE:${parts.join(";")}`;
95
+ }
96
+ function fillEditForm(ev, targetId) {
97
+ setRecurScope(null);
98
+ setEventForm({
99
+ id: targetId,
100
+ summary: ev.summary,
101
+ start: ev.all_day ? `${ev.start.slice(0, 10)}T09:00` : toLocalInput(ev.start),
102
+ end: ev.all_day ? `${ev.end.slice(0, 10)}T10:00` : toLocalInput(ev.end),
103
+ allDay: ev.all_day,
104
+ attendees: ev.attendees.map((a) => a.email).join(", "),
105
+ reminderMinutes: "",
106
+ addMeet: !!ev.meet_link,
107
+ recurrence: "",
108
+ recurEnd: "never",
109
+ recurUntil: "",
110
+ recurCount: "",
111
+ recurByday: [],
112
+ });
113
+ setFreeBusyMsg("");
114
+ }
115
+ /** Open the new/edit form. `seedDay` (ms) pre-dates a new event from a grid click. */
116
+ function openEventForm(ev, prefillSummary, withMeet = false, seedDay) {
117
+ setOpenEvent(null);
118
+ if (ev) {
119
+ if (ev.is_recurring && ev.recurring_event_id) {
120
+ setRecurScope({ ev, mode: "edit" });
121
+ return;
122
+ }
123
+ fillEditForm(ev, ev.id);
124
+ return;
125
+ }
126
+ // seedDay is the exact start time for a grid-click (month cells pass 9am,
127
+ // the time grid passes the clicked hour); otherwise round to the next hour.
128
+ const base = seedDay !== undefined ? new Date(seedDay) : new Date();
129
+ if (seedDay === undefined)
130
+ base.setMinutes(0, 0, 0);
131
+ const start = new Date(base.getTime() - base.getTimezoneOffset() * 60000);
132
+ const end = new Date(start.getTime() + 3600000);
133
+ setEventForm({
134
+ summary: prefillSummary ?? "",
135
+ start: start.toISOString().slice(0, 16),
136
+ end: end.toISOString().slice(0, 16),
137
+ allDay: false,
138
+ attendees: "",
139
+ reminderMinutes: "10",
140
+ addMeet: withMeet,
141
+ recurrence: "",
142
+ recurEnd: "never",
143
+ recurUntil: "",
144
+ recurCount: "",
145
+ recurByday: [],
146
+ });
147
+ setFreeBusyMsg("");
148
+ }
149
+ async function checkAvailability() {
150
+ if (!activeAccount || !eventForm)
151
+ return;
152
+ const guests = eventForm.attendees.split(",").map((s) => s.trim()).filter(Boolean);
153
+ if (guests.length === 0) {
154
+ setFreeBusyMsg("Add guests first.");
155
+ return;
156
+ }
157
+ const startIso = eventForm.allDay ? `${eventForm.start.slice(0, 10)}T00:00:00Z` : new Date(eventForm.start).toISOString();
158
+ const endIso = eventForm.allDay ? `${eventForm.end.slice(0, 10)}T23:59:59Z` : new Date(eventForm.end || eventForm.start).toISOString();
159
+ setFreeBusyMsg("Checking…");
160
+ try {
161
+ const res = await freeBusy(activeAccount, guests, startIso, endIso);
162
+ const busy = res.filter((r) => r.busy.length > 0).map((r) => r.calendar_id);
163
+ setFreeBusyMsg(busy.length === 0 ? "All guests are free." : `Busy: ${busy.join(", ")}`);
164
+ }
165
+ catch (e) {
166
+ setFreeBusyMsg(String(e));
167
+ }
168
+ }
169
+ async function submitEvent() {
170
+ if (!activeAccount || !eventForm)
171
+ return;
172
+ if (!eventForm.summary.trim() || !eventForm.start) {
173
+ setErr("Event needs a title and start time.");
174
+ return;
175
+ }
176
+ const f = eventForm;
177
+ const ev = {
178
+ summary: f.summary,
179
+ startRfc3339: f.allDay ? f.start.slice(0, 10) : new Date(f.start).toISOString(),
180
+ endRfc3339: f.allDay ? f.end.slice(0, 10) : new Date(f.end || f.start).toISOString(),
181
+ allDay: f.allDay,
182
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
183
+ attendees: f.attendees.split(/[,;\s]+/).map((s) => s.trim()).filter(Boolean),
184
+ reminderMinutes: f.reminderMinutes ? Number(f.reminderMinutes) : null,
185
+ addMeet: f.addMeet,
186
+ recurrence: buildRrule(f),
187
+ };
188
+ try {
189
+ if (f.id)
190
+ await updateEvent(activeAccount, selectedCal, f.id, ev);
191
+ else
192
+ await createEvent(activeAccount, selectedCal, ev);
193
+ setEventForm(null);
194
+ await reloadEvents();
195
+ showUndo(f.id ? "Event updated" : "Event created", [], "");
196
+ }
197
+ catch (e) {
198
+ setErr(String(e));
199
+ }
200
+ }
201
+ function performDelete(ev, targetId) {
202
+ if (!activeAccount)
203
+ return;
204
+ setRecurScope(null);
205
+ deleteEvent(activeAccount, ev.calendar_id || selectedCal, targetId)
206
+ .then(() => {
207
+ setOpenEvent(null);
208
+ return reloadEvents();
209
+ })
210
+ .then(() => showUndo("Event deleted", [], ""))
211
+ .catch((e) => setErr(String(e)));
212
+ }
213
+ function performTruncate(ev) {
214
+ if (!activeAccount)
215
+ return;
216
+ setRecurScope(null);
217
+ truncateEventSeries(activeAccount, ev.calendar_id || selectedCal, ev.recurring_event_id, ev.start)
218
+ .then(() => {
219
+ setOpenEvent(null);
220
+ return reloadEvents();
221
+ })
222
+ .then(() => showUndo("This and following events removed", [], ""))
223
+ .catch((e) => setErr(String(e)));
224
+ }
225
+ async function deleteEv(ev) {
226
+ if (!activeAccount)
227
+ return;
228
+ if (ev.is_recurring && ev.recurring_event_id) {
229
+ setRecurScope({ ev, mode: "delete" });
230
+ return;
231
+ }
232
+ if ((await showDialog({ kind: "confirm", message: `Delete "${ev.summary}"? Guests will be notified.` })) !== "ok")
233
+ return;
234
+ performDelete(ev, ev.id);
235
+ }
236
+ function rsvp(ev, response) {
237
+ if (!activeAccount)
238
+ return;
239
+ rsvpEvent(activeAccount, ev.calendar_id || selectedCal, ev.id, activeAccount, response)
240
+ .then(() => reloadEvents())
241
+ .then(() => {
242
+ setOpenEvent(null);
243
+ showUndo(`RSVP: ${response}`, [], "");
244
+ })
245
+ .catch((e) => setErr(String(e)));
246
+ }
247
+ // Load calendars once per account; pick the primary as the create target.
248
+ useEffect(() => {
249
+ setEventForm(null);
250
+ setOpenEvent(null);
251
+ if (!activeAccount)
252
+ return;
253
+ setCalendarBusy(true);
254
+ listCalendars(activeAccount)
255
+ .then((cals) => {
256
+ setCalendars(cals);
257
+ const cal = cals.find((c) => c.primary)?.id || cals[0]?.id || "primary";
258
+ setSelectedCal(cal);
259
+ // Like Google web, show ALL calendars by default the first time (so the
260
+ // grid isn't blank when events live on secondary calendars). Only seed
261
+ // when the user hasn't already chosen a subset.
262
+ if (shownCals.length === 0 && cals.length > 0) {
263
+ const all = cals.map((c) => c.id);
264
+ setShownCals(all);
265
+ setPref("shownCals", all);
266
+ }
267
+ })
268
+ .catch((e) => setErr(String(e)))
269
+ .finally(() => setCalendarBusy(false));
270
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- setEventForm/setOpenEvent/setErr/shownCals.length/setShownCals are store setters or a derived read; tracking them would refetch calendars on every render instead of only on account change.
271
+ }, [activeAccount]);
272
+ // Re-fetch whenever the focused period, view, account, or visible calendars change.
273
+ useEffect(() => {
274
+ if (!activeAccount)
275
+ return;
276
+ reloadEvents();
277
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- reloadEvents is a plain function redefined every render (closes over selectedCal/setErr); tracking it would refetch events on every render instead of only on the five real scope inputs.
278
+ }, [activeAccount, calCursor, calView, shownCals, selectedCal]);
279
+ // "Create event from email" / "Schedule meeting" → open the prefilled form.
280
+ useEffect(() => {
281
+ if (!eventSeed)
282
+ return;
283
+ const id = window.setTimeout(() => {
284
+ openEventForm(undefined, eventSeed.summary, eventSeed.withMeet);
285
+ setEventSeed(null);
286
+ }, 0);
287
+ return () => window.clearTimeout(id);
288
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- openEventForm/setEventSeed are recreated every render; tracking them would clear+rearm the timeout on every render instead of only when a new eventSeed arrives.
289
+ }, [eventSeed]);
290
+ function step(dir) {
291
+ const d = new Date(calCursor);
292
+ if (calView === "day")
293
+ setCalCursor(calCursor + dir * DAY);
294
+ else if (calView === "week")
295
+ setCalCursor(calCursor + dir * 7 * DAY);
296
+ else
297
+ setCalCursor(new Date(d.getFullYear(), d.getMonth() + dir, Math.min(d.getDate(), 28)).getTime());
298
+ }
299
+ // Wheel over the grid pages the period (Google-style), throttled so one
300
+ // gesture = one step rather than flying through months.
301
+ const wheelLock = useRef(0);
302
+ function onWheel(e) {
303
+ if (Math.abs(e.deltaY) < 4)
304
+ return;
305
+ const now = Date.now();
306
+ if (now - wheelLock.current < 320)
307
+ return;
308
+ wheelLock.current = now;
309
+ step(e.deltaY > 0 ? 1 : -1);
310
+ }
311
+ function pickView(v) {
312
+ setCalView(v);
313
+ setPref("calView", v);
314
+ }
315
+ const headerLabel = (() => {
316
+ const d = new Date(calCursor);
317
+ if (calView === "day")
318
+ return d.toLocaleDateString([], { weekday: "long", month: "long", day: "numeric", year: "numeric" });
319
+ if (calView === "week") {
320
+ const s = new Date(startOfWeek(calCursor));
321
+ const e = new Date(startOfWeek(calCursor) + 6 * DAY);
322
+ const sameMonth = s.getMonth() === e.getMonth();
323
+ return sameMonth
324
+ ? `${s.toLocaleDateString([], { month: "long" })} ${s.getDate()}–${e.getDate()}, ${e.getFullYear()}`
325
+ : `${s.toLocaleDateString([], { month: "short", day: "numeric" })} – ${e.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" })}`;
326
+ }
327
+ return d.toLocaleDateString([], { month: "long", year: "numeric" });
328
+ })();
329
+ return (_jsxs("div", { className: "calview", children: [_jsxs("div", { className: "cal-bar", children: [_jsx(Button, { variant: "ghost", className: "cal-today", onClick: () => setCalCursor(Date.now()), children: "Today" }), _jsx(IconButton, { label: "Previous", className: "cal-step", title: "Previous", onClick: () => step(-1), children: _jsx(Icon, { name: "chevron-left", size: 18 }) }), _jsx(IconButton, { label: "Next", className: "cal-step", title: "Next", onClick: () => step(1), children: _jsx(Icon, { name: "chevron-right", size: 18 }) }), _jsx("h2", { className: "cal-label", children: headerLabel }), _jsx("div", { className: "cal-bar-spacer" }), _jsx("div", { className: "cal-viewtoggle", children: _jsx(SegmentedControl, { label: "Calendar view", value: calView, options: CALENDAR_VIEWS.map((v) => ({ value: v, label: v[0].toUpperCase() + v.slice(1) })), onChange: (v) => pickView(v) }) }), _jsx(AccountSwitcher, {})] }), _jsx("div", { className: "cal-grid", onWheel: onWheel, children: calView === "month" ? (_jsx(MonthGrid, { cursor: calCursor, events: events, today: calendarNow.today, onDay: (ms) => openEventForm(undefined, undefined, false, ms + 9 * 3600000), onEvent: setOpenEvent })) : (_jsx(TimeGrid, { days: calView === "week"
330
+ ? Array.from({ length: 7 }, (_, i) => startOfWeek(calCursor) + i * DAY)
331
+ : [startOfDay(calCursor)], events: events, today: calendarNow.today, nowMin: calendarNow.minute, onSlot: (ms) => openEventForm(undefined, undefined, false, ms), onEvent: setOpenEvent })) }), openEvent && (_jsx(CalendarEventPopover, { openEvent: openEvent, setOpenEvent: setOpenEvent, openEventForm: openEventForm, deleteEv: deleteEv, rsvp: rsvp })), eventForm && (_jsx(CalendarEventFormModal, { eventForm: eventForm, setEventForm: setEventForm, freeBusyMsg: freeBusyMsg, checkAvailability: checkAvailability, submitEvent: submitEvent })), recurScope && (_jsxs(Dialog, { title: recurScope.mode === "edit" ? "Edit recurring event" : "Delete recurring event", width: 420, onClose: () => setRecurScope(null), children: [_jsxs("p", { className: "hint", children: ["\u201C", recurScope.ev.summary, "\u201D repeats. Apply to:"] }), _jsxs("div", { className: "recur-choices", children: [_jsx(Button, { variant: "primary", onClick: () => recurScope.mode === "edit" ? fillEditForm(recurScope.ev, recurScope.ev.id) : performDelete(recurScope.ev, recurScope.ev.id), children: "This event" }), recurScope.mode === "delete" && (_jsx(Button, { variant: "primary", onClick: () => performTruncate(recurScope.ev), children: "This and following events" })), _jsx(Button, { variant: "primary", onClick: () => recurScope.mode === "edit" ? fillEditForm(recurScope.ev, recurScope.ev.recurring_event_id) : performDelete(recurScope.ev, recurScope.ev.recurring_event_id), children: "All events" })] })] }))] }));
332
+ }
333
+ export const CalendarView = memo(CalendarViewInner);
@@ -0,0 +1,3 @@
1
+ declare function ContactsViewInner(): import("react").JSX.Element;
2
+ export declare const ContactsView: import("react").MemoExoticComponent<typeof ContactsViewInner>;
3
+ export {};