mysticals 0.3.1 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +856 -173
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { createElement } from "react";
5
5
  import { render } from "ink";
6
6
 
7
7
  // package.json
8
- var version = "0.3.1";
8
+ var version = "0.5.0";
9
9
 
10
10
  // src/client.ts
11
11
  import { spawn } from "child_process";
@@ -46,6 +46,7 @@ var methodSet = {
46
46
  "accounts.addCaldav": true,
47
47
  "accounts.update": true,
48
48
  "accounts.remove": true,
49
+ "accounts.reauth": true,
49
50
  "calendars.list": true,
50
51
  "calendars.setVisible": true,
51
52
  "events.list": true,
@@ -53,6 +54,7 @@ var methodSet = {
53
54
  "events.update": true,
54
55
  "events.delete": true,
55
56
  "events.respond": true,
57
+ "events.recurrence": true,
56
58
  "sync.now": true
57
59
  };
58
60
  var METHODS = Object.keys(methodSet);
@@ -238,6 +240,21 @@ function when(e, now) {
238
240
  return `${isSameDay(s, now) ? "Today" : format(s, "EEE, d MMM")} \xB7 ${format(s, "HH:mm")}`;
239
241
  }
240
242
 
243
+ // ../../packages/core/src/providers/http.ts
244
+ var http = { timeoutMs: 3e4 };
245
+ var timedFetch = async (input, init) => {
246
+ const timeout = AbortSignal.timeout(http.timeoutMs);
247
+ try {
248
+ return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
249
+ } catch (e) {
250
+ if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
251
+ throw e;
252
+ }
253
+ };
254
+ var AuthError = class extends Error {
255
+ name = "AuthError";
256
+ };
257
+
241
258
  // ../../packages/core/src/sync/engine.ts
242
259
  var MIN = 6e4;
243
260
  var SyncEngine = class {
@@ -260,6 +277,8 @@ var SyncEngine = class {
260
277
  queued = /* @__PURE__ */ new Map();
261
278
  /** Bumped by `edited`: a pass that started before a local edit must not overwrite it. */
262
279
  edits = /* @__PURE__ */ new Map();
280
+ /** Accounts whose last pass listed no calendars while the cache had some (see syncAccount). */
281
+ emptied = /* @__PURE__ */ new Set();
263
282
  running = false;
264
283
  unsubscribe;
265
284
  start() {
@@ -279,6 +298,7 @@ var SyncEngine = class {
279
298
  /**
280
299
  * Sync one account, or all accounts independently when id omitted.
281
300
  * A single-account call rejects with that account's error; the all-accounts call never rejects.
301
+ * The all-accounts call skips accounts whose credentials were rejected (`authError`): retrying can't help.
282
302
  */
283
303
  /**
284
304
  * `quiet`: the change came from this app (own edit/RSVP), so don't notify about it.
@@ -287,7 +307,7 @@ var SyncEngine = class {
287
307
  */
288
308
  async syncNow(accountId, opts = {}) {
289
309
  if (accountId === void 0) {
290
- await Promise.allSettled(this.store.list().map((a) => this.syncNow(a.id)));
310
+ await Promise.allSettled(this.store.list().filter((a) => !a.authError).map((a) => this.syncNow(a.id)));
291
311
  return;
292
312
  }
293
313
  const existing = this.inflight.get(accountId);
@@ -325,7 +345,8 @@ var SyncEngine = class {
325
345
  schedule(id2) {
326
346
  clearTimeout(this.timers.get(id2));
327
347
  this.timers.delete(id2);
328
- if (!this.running || !this.store.list().some((a) => a.id === id2)) {
348
+ const acc = this.store.list().find((a) => a.id === id2);
349
+ if (!this.running || !acc || acc.authError) {
329
350
  this.failures.delete(id2);
330
351
  return;
331
352
  }
@@ -339,7 +360,8 @@ var SyncEngine = class {
339
360
  }
340
361
  // ponytail: quiet silences the whole sync, so an external change landing in the same pass is not announced.
341
362
  async syncAccount(id2, quiet) {
342
- const hadError = !!this.store.list().find((a) => a.id === id2)?.error;
363
+ const acc = this.store.list().find((a) => a.id === id2);
364
+ const hadError = !!acc?.error;
343
365
  const edits = this.edits.get(id2) ?? 0;
344
366
  try {
345
367
  const provider = this.store.getProvider(id2);
@@ -362,10 +384,15 @@ var SyncEngine = class {
362
384
  } catch {
363
385
  prev = void 0;
364
386
  }
387
+ if (!calendars.length && prev?.calendars.length && !this.emptied.has(id2)) {
388
+ this.emptied.add(id2);
389
+ throw new Error("The server returned no calendars; showing the last synced data");
390
+ }
391
+ this.emptied.delete(id2);
365
392
  const changed = !prev || JSON.stringify([prev.calendars, prev.events]) !== JSON.stringify([next.calendars, next.events]);
366
393
  await this.store.writeCache(id2, next);
367
394
  this.failures.delete(id2);
368
- if (hadError) await this.store.update(id2, { error: void 0 });
395
+ if (hadError) await this.store.update(id2, { error: void 0, authError: false });
369
396
  if (changed || hadError) this.onChanged(id2);
370
397
  if (changed && !quiet && prev?.syncedAt && this.opts.onEvents) {
371
398
  const notes = diffEvents(prev.events, next.events, now);
@@ -374,9 +401,10 @@ var SyncEngine = class {
374
401
  } catch (e) {
375
402
  this.failures.set(id2, (this.failures.get(id2) ?? 0) + 1);
376
403
  const message = e instanceof Error ? e.message : String(e);
377
- await this.store.update(id2, { error: message }).catch(() => {
404
+ const auth = e instanceof AuthError;
405
+ await this.store.update(id2, { error: message, ...auth ? { authError: true } : {} }).catch(() => {
378
406
  });
379
- if (!hadError) this.onChanged(id2);
407
+ if (!hadError || auth && !acc?.authError) this.onChanged(id2);
380
408
  throw e;
381
409
  }
382
410
  }
@@ -474,6 +502,15 @@ var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
474
502
  var text = (max2) => z.string().max(max2);
475
503
  var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
476
504
  var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
505
+ var Recurrence = z.object({
506
+ freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
507
+ interval: z.number().int().min(1).max(999).optional(),
508
+ byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
509
+ until: z.iso.date().optional(),
510
+ count: z.number().int().min(1).max(999).optional(),
511
+ // An RRULE value kept from the provider: one line of plain RRULE parts only.
512
+ rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
513
+ }).strict();
477
514
  var NewEvent = z.object({
478
515
  accountId: id,
479
516
  calendarId: id,
@@ -483,7 +520,8 @@ var NewEvent = z.object({
483
520
  allDay: z.boolean(),
484
521
  location: text(1e3).optional(),
485
522
  description: text(2e4).optional(),
486
- attendees: z.array(z.email()).max(500).optional()
523
+ attendees: z.array(z.email()).max(500).optional(),
524
+ recurrence: Recurrence.optional()
487
525
  }).refine(ordered, "end before start");
488
526
  var Attendee = z.object({
489
527
  email: text(320).min(1),
@@ -500,7 +538,9 @@ var EventEdit = EventRef.extend({
500
538
  allDay: z.boolean(),
501
539
  location: text(1e3).optional(),
502
540
  description: text(2e4).optional(),
503
- attendees: z.array(Attendee).max(500)
541
+ attendees: z.array(Attendee).max(500),
542
+ // Absent keeps the series' rule; null stops repeating.
543
+ recurrence: Recurrence.nullish()
504
544
  }).refine(ordered, "end before start");
505
545
  var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
506
546
  var CaldavInput = z.object({
@@ -514,9 +554,10 @@ var CaldavInput = z.object({
514
554
  password: text(2e3).min(1),
515
555
  color: color.optional()
516
556
  });
557
+ var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
517
558
  var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
518
559
  var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
519
- var Scope = z.enum(["one", "following", "all"]).default("one");
560
+ var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
520
561
  function createApi(store, sync, deps) {
521
562
  const account = (accountId) => {
522
563
  const a = store.get(id.parse(accountId));
@@ -579,6 +620,26 @@ function createApi(store, sync, deps) {
579
620
  deps.onChanged?.(a.id);
580
621
  return a;
581
622
  },
623
+ reauth: async (accountId, raw) => {
624
+ const a = account(accountId);
625
+ let credentials;
626
+ if (a.kind === "google") {
627
+ const signed = await deps.googleSignIn();
628
+ if (signed.email.toLowerCase() !== a.email.toLowerCase())
629
+ throw new Error(`Signed in as ${signed.email}; sign in as ${a.email} to reconnect this account`);
630
+ credentials = signed.credentials;
631
+ } else {
632
+ const { password } = ReauthInput.parse(raw);
633
+ const old = store.readCreds(a.id);
634
+ if (old.kind !== "caldav") throw new Error("Credentials kind does not match account kind");
635
+ await deps.verifyCaldav({ label: a.label, serverUrl: old.serverUrl, username: old.username, password });
636
+ credentials = { ...old, password };
637
+ }
638
+ const updated = await store.update(a.id, { credentials, error: void 0, authError: false });
639
+ deps.onChanged?.(a.id);
640
+ syncOne(a.id);
641
+ return updated;
642
+ },
582
643
  remove: async (accountId) => {
583
644
  const a = account(accountId);
584
645
  await store.remove(a.id);
@@ -612,12 +673,19 @@ function createApi(store, sync, deps) {
612
673
  syncOne(a.id);
613
674
  return ev;
614
675
  },
615
- update: async (raw) => {
676
+ update: async (raw, scope) => {
616
677
  const edit = EventEdit.parse(raw);
617
678
  const cached2 = writable(cachedEvent(edit));
618
- const { title: title2, start, end, allDay, location, description, attendees } = edit;
619
- const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees });
620
- upsert(ev, cached2.accountId, cached2.calendarId);
679
+ const how = cached2.recurringEventId ? Scope.parse(scope) : "one";
680
+ const { title: title2, start, end, allDay, location, description, attendees, recurrence } = edit;
681
+ if (recurrence !== void 0 && cached2.recurringEventId && how === "one")
682
+ throw new Error("The repeat rule belongs to the series: choose \u201CThis and following\u201D or \u201CAll events\u201D");
683
+ const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees, ...recurrence !== void 0 ? { recurrence } : {} }, how);
684
+ if (how === "one") upsert(ev, cached2.accountId, cached2.calendarId);
685
+ else {
686
+ const own = { ...ev, accountId: cached2.accountId, calendarId: cached2.calendarId };
687
+ applyLocal(cached2.accountId, (events) => [...events.filter((e) => e.id !== own.id && !deletedBy(cached2, how, e)), own]);
688
+ }
621
689
  syncOne(cached2.accountId);
622
690
  return ev;
623
691
  },
@@ -628,6 +696,10 @@ function createApi(store, sync, deps) {
628
696
  applyLocal(cached2.accountId, (events) => events.filter((e) => !deletedBy(cached2, how, e)));
629
697
  syncOne(cached2.accountId);
630
698
  },
699
+ recurrence: async (raw) => {
700
+ const cached2 = cachedEvent(EventRef.parse(raw));
701
+ return store.getProvider(cached2.accountId).getRecurrence(cached2);
702
+ },
631
703
  respond: async (raw, status) => {
632
704
  const cached2 = cachedEvent(EventRef.parse(raw));
633
705
  const ev = await store.getProvider(cached2.accountId).respond(cached2, RsvpStatus.parse(status));
@@ -716,6 +788,7 @@ var AccountStore = class {
716
788
  await mkdir(dir, { recursive: true });
717
789
  await writeAtomic(join4(dir, "creds.bin"), data);
718
790
  }
791
+ /** Decrypted credentials; main/daemon only (accounts.reauth keeps the server and username), never to the renderer. */
719
792
  readCreds(id2) {
720
793
  return JSON.parse(this.crypto.decrypt(readFileSync2(join4(this.accountDir(id2), "creds.bin"))));
721
794
  }
@@ -751,6 +824,10 @@ var AccountStore = class {
751
824
  if (patch.error) acc.error = patch.error;
752
825
  else delete acc.error;
753
826
  }
827
+ if ("authError" in patch) {
828
+ if (patch.authError) acc.authError = true;
829
+ else delete acc.authError;
830
+ }
754
831
  await this.saveRegistry();
755
832
  return { ...acc };
756
833
  });
@@ -829,11 +906,171 @@ var AccountStore = class {
829
906
  };
830
907
 
831
908
  // ../../packages/core/src/mock/mockApi.ts
832
- import { addDays, format as format2 } from "date-fns";
909
+ import { addDays as addDays2, format as format4 } from "date-fns";
833
910
 
834
911
  // ../../packages/core/src/mock/MockProvider.ts
835
912
  import { randomUUID as randomUUID3 } from "crypto";
836
- import { parseISO as parseISO3 } from "date-fns";
913
+ import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format3, parseISO as parseISO4, startOfWeek } from "date-fns";
914
+
915
+ // ../../packages/core/src/logic/recurrence.ts
916
+ import { format as format2, isValid, parseISO as parseISO3 } from "date-fns";
917
+ var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
918
+ var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
919
+ var SHORT = { MO: "Mon", TU: "Tue", WE: "Wed", TH: "Thu", FR: "Fri", SA: "Sat", SU: "Sun" };
920
+ var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
921
+ var FREQS = ["daily", "weekly", "monthly", "yearly"];
922
+ var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
923
+ var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
924
+ var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
925
+ var shortDay = (d) => SHORT[d];
926
+ function normalize(r, start) {
927
+ if (r.rule) return { freq: r.freq, rule: r.rule };
928
+ const out = { freq: r.freq };
929
+ if (r.interval && r.interval > 1) out.interval = r.interval;
930
+ if (r.freq === "weekly" && r.byDay?.length) {
931
+ const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
932
+ if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
933
+ }
934
+ if (r.until) out.until = r.until;
935
+ else if (r.count) out.count = r.count;
936
+ return out;
937
+ }
938
+ function sameRecurrence(a, b, start) {
939
+ if (!a || !b) return a === b;
940
+ return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
941
+ }
942
+ function toRRule(r, allDay) {
943
+ if (r.rule) return r.rule;
944
+ const parts = [`FREQ=${r.freq.toUpperCase()}`];
945
+ if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
946
+ if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
947
+ if (r.until) {
948
+ const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
949
+ parts.push(`UNTIL=${until}`);
950
+ } else if (r.count) parts.push(`COUNT=${r.count}`);
951
+ return parts.join(";");
952
+ }
953
+ var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
954
+ function fromRRule(text2, start) {
955
+ const rule = text2.replace(/^RRULE:/i, "").trim();
956
+ const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
957
+ const i = p.indexOf("=");
958
+ return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
959
+ }));
960
+ const freq = (kv.get("FREQ") ?? "").toLowerCase();
961
+ const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
962
+ if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
963
+ const out = { freq };
964
+ const interval = Number(kv.get("INTERVAL") ?? 1);
965
+ if (!Number.isInteger(interval) || interval < 1) return custom();
966
+ if (interval > 1) out.interval = interval;
967
+ if (kv.has("BYDAY")) {
968
+ const days = kv.get("BYDAY").split(",");
969
+ if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
970
+ out.byDay = WEEKDAYS.filter((d) => days.includes(d));
971
+ }
972
+ const until = kv.get("UNTIL");
973
+ if (until) {
974
+ const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
975
+ if (!m) return custom();
976
+ if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
977
+ else {
978
+ const at3 = m[7] ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])) : new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
979
+ const s = start && start.length > 10 ? asDate(start) : void 0;
980
+ const minutes = (d) => d.getHours() * 60 + d.getMinutes();
981
+ if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
982
+ out.until = format2(at3, "yyyy-MM-dd");
983
+ }
984
+ } else if (kv.has("COUNT")) {
985
+ const count = Number(kv.get("COUNT"));
986
+ if (!Number.isInteger(count) || count < 1) return custom();
987
+ out.count = count;
988
+ }
989
+ return out;
990
+ }
991
+ function presets(start) {
992
+ const d = asDate(start);
993
+ return [
994
+ { id: "none", label: "Does not repeat", recurrence: null },
995
+ { id: "daily", label: "Every day", recurrence: { freq: "daily" } },
996
+ { id: "weekly", label: `Every week on ${format2(d, "EEEE")}`, recurrence: { freq: "weekly" } },
997
+ { id: "monthly", label: `Every month on day ${d.getDate()}`, recurrence: { freq: "monthly" } },
998
+ { id: "yearly", label: `Every year on ${format2(d, "d MMMM")}`, recurrence: { freq: "yearly" } },
999
+ { id: "weekdays", label: "Every weekday (Mon\u2013Fri)", recurrence: { freq: "weekly", byDay: WORKWEEK } }
1000
+ ];
1001
+ }
1002
+ function presetOf(r, start) {
1003
+ if (!r) return "none";
1004
+ if (r.rule) return "custom";
1005
+ const { until: _u, count: _c, ...base } = normalize(r, start);
1006
+ const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
1007
+ return hit?.id ?? "custom";
1008
+ }
1009
+ var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
1010
+ function describe(r, start) {
1011
+ if (!r) return "Does not repeat";
1012
+ if (r.rule) return `Custom rule (${r.rule})`;
1013
+ const n = normalize(r, start);
1014
+ const d = asDate(start);
1015
+ const every = n.interval ? `Every ${n.interval} ${UNIT[n.freq]}s` : `Every ${UNIT[n.freq]}`;
1016
+ const on = n.freq === "weekly" ? !n.interval && n.byDay?.join() === WORKWEEK.join() ? "" : ` on ${(n.byDay ?? [weekdayOf(start)]).map(shortDay).join(", ")}` : n.freq === "monthly" ? ` on day ${d.getDate()}` : n.freq === "yearly" ? ` on ${format2(d, "d MMMM")}` : "";
1017
+ const head = n.freq === "weekly" && !n.interval && n.byDay?.join() === WORKWEEK.join() ? "Every weekday (Mon\u2013Fri)" : `${every}${on}`;
1018
+ const end = n.until ? ` \xB7 until ${format2(parseISO3(n.until), "d MMM yyyy")}` : n.count ? ` \xB7 ${n.count} time${n.count > 1 ? "s" : ""}` : "";
1019
+ return head + end;
1020
+ }
1021
+ var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
1022
+ function parseEvery(text2) {
1023
+ const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
1024
+ const n = Number(m?.[1] ?? 1);
1025
+ if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
1026
+ const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
1027
+ return { freq, interval: n > 1 ? n : void 0 };
1028
+ }
1029
+ var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
1030
+ function parseDays(text2) {
1031
+ const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
1032
+ const bad = words.filter((w) => !WEEKDAYS.includes(w));
1033
+ if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
1034
+ return WEEKDAYS.filter((d) => words.includes(d));
1035
+ }
1036
+ var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
1037
+ function parseEnds(text2) {
1038
+ const t = text2.trim().toLowerCase();
1039
+ if (!t || t === "never") return {};
1040
+ if (/^\d{4}-\d{2}-\d{2}$/.test(t) && isValid(parseISO3(t)) && format2(parseISO3(t), "yyyy-MM-dd") === t) return { until: t };
1041
+ const m = /^(\d+)(\s*times?)?$/.exec(t);
1042
+ if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
1043
+ throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
1044
+ }
1045
+
1046
+ // ../../packages/core/src/mock/MockProvider.ts
1047
+ var MAX_INSTANCES = 60;
1048
+ function expand(first, r, series) {
1049
+ const start = parseISO4(first.start);
1050
+ const length = parseISO4(first.end).getTime() - start.getTime();
1051
+ const every = r.interval ?? 1;
1052
+ const days = r.byDay?.length ? r.byDay : [weekdayOf(first.start)];
1053
+ const until = r.until ? parseISO4(r.until) : void 0;
1054
+ const hits = (d) => {
1055
+ const n = differenceInCalendarDays(d, start);
1056
+ if (r.freq === "daily") return n % every === 0;
1057
+ if (r.freq === "weekly")
1058
+ return days.includes(weekdayOf(format3(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
1059
+ const months = differenceInCalendarMonths(d, start);
1060
+ if (d.getDate() !== start.getDate()) return false;
1061
+ return r.freq === "monthly" ? months % every === 0 : months % (12 * every) === 0;
1062
+ };
1063
+ const out = [];
1064
+ for (let n = 0; n < 3 * 366 && out.length < (r.count ?? MAX_INSTANCES) && out.length < MAX_INSTANCES; n++) {
1065
+ const d = addDays(start, n);
1066
+ if (until && differenceInCalendarDays(d, until) > 0) break;
1067
+ if (!hits(d)) continue;
1068
+ const s = first.allDay ? format3(d, "yyyy-MM-dd") : d.toISOString();
1069
+ const e = first.allDay ? format3(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
1070
+ out.push({ ...first, id: `${series}@${s}`, start: s, end: e, recurringEventId: series, recurrence: void 0 });
1071
+ }
1072
+ return out;
1073
+ }
837
1074
  var MockProvider = class {
838
1075
  constructor(accountId, email, calendars) {
839
1076
  this.accountId = accountId;
@@ -844,11 +1081,13 @@ var MockProvider = class {
844
1081
  email;
845
1082
  calendars;
846
1083
  events = [];
1084
+ /** Series id -> its rule. */
1085
+ rules = /* @__PURE__ */ new Map();
847
1086
  async listCalendars() {
848
1087
  return structuredClone(this.calendars);
849
1088
  }
850
1089
  async listEvents(calendarId, range) {
851
- const t = (iso2) => parseISO3(iso2).getTime();
1090
+ const t = (iso2) => parseISO4(iso2).getTime();
852
1091
  return structuredClone(
853
1092
  this.events.filter((e) => e.calendarId === calendarId && t(e.end) > t(range.start) && t(e.start) < t(range.end))
854
1093
  );
@@ -867,12 +1106,45 @@ var MockProvider = class {
867
1106
  organizer: { email: this.email },
868
1107
  attendees: (input.attendees ?? []).map((email) => ({ email, status: "needsAction" }))
869
1108
  };
1109
+ if (input.recurrence) return this.startSeries(ev, input.recurrence, ev.id);
870
1110
  this.events.push(ev);
871
1111
  return structuredClone(ev);
872
1112
  }
873
- async updateEvent(event) {
1113
+ /** Add the series `first` starts under `r` (or `first` alone for null); returns its first event. */
1114
+ startSeries(first, r, series) {
1115
+ const evs = r ? expand(first, r, series) : [{ ...first, recurringEventId: void 0, recurrence: void 0 }];
1116
+ if (r) this.rules.set(series, r);
1117
+ this.events.push(...evs);
1118
+ return structuredClone(evs[0]);
1119
+ }
1120
+ async getRecurrence(event) {
1121
+ return event.recurringEventId && this.rules.get(event.recurringEventId) || null;
1122
+ }
1123
+ async updateEvent(event, scope = "one") {
874
1124
  const i = this.events.findIndex((e) => e.id === event.id);
875
1125
  if (i < 0) throw new Error("not found");
1126
+ const old = this.events[i];
1127
+ const series = old.recurringEventId;
1128
+ if (event.recurrence !== void 0 && (!series || scope !== "one")) {
1129
+ const own = (e) => e.id === old.id || !!series && e.recurringEventId === series && (scope === "all" || e.start >= old.start);
1130
+ const firstOld = series && scope === "all" ? this.events.filter(own).sort((a, b) => a.start.localeCompare(b.start))[0] : old;
1131
+ this.events = this.events.filter((e) => !own(e));
1132
+ const shift = Date.parse(event.start) - Date.parse(old.start);
1133
+ const anchor = event.recurrence && firstOld !== old && !event.allDay ? { start: new Date(Date.parse(firstOld.start) + shift).toISOString(), end: new Date(Date.parse(firstOld.start) + shift + Date.parse(event.end) - Date.parse(event.start)).toISOString() } : {};
1134
+ const id2 = scope === "all" && series ? series : randomUUID3();
1135
+ return this.startSeries({ ...event, ...anchor, accountId: this.accountId }, event.recurrence, id2);
1136
+ }
1137
+ if (series && scope !== "one") {
1138
+ const shift = Date.parse(event.start) - Date.parse(old.start);
1139
+ const length = Date.parse(event.end) - Date.parse(event.start);
1140
+ const at3 = (ms) => new Date(ms).toISOString();
1141
+ const { title: title2, location, description, attendees } = event;
1142
+ this.events = this.events.map((e) => {
1143
+ if (e.id === event.id || e.recurringEventId !== series || scope === "following" && e.start < old.start) return e;
1144
+ const moved = old.allDay || event.allDay ? {} : { start: at3(Date.parse(e.start) + shift), end: at3(Date.parse(e.start) + shift + length) };
1145
+ return { ...e, title: title2, location, description, attendees, ...moved };
1146
+ });
1147
+ }
876
1148
  this.events[i] = { ...event, accountId: this.accountId };
877
1149
  return structuredClone(this.events[i]);
878
1150
  }
@@ -899,7 +1171,15 @@ var iso = (dayOffset, h, m = 0) => {
899
1171
  };
900
1172
  function createMockApi(onChanged) {
901
1173
  const accounts = [
902
- { id: "work", kind: "caldav", label: "Work", email: "me@work.example", color: "#8be9fd" },
1174
+ {
1175
+ id: "work",
1176
+ kind: "caldav",
1177
+ label: "Work",
1178
+ email: "me@work.example",
1179
+ color: "#8be9fd",
1180
+ // e2e: a revoked app password; accounts.reauth with any password fixes it.
1181
+ ...process.env.MYSTICALS_MOCK_AUTH_ERROR ? { error: "CalDAV login failed: the password was changed or revoked", authError: true } : {}
1182
+ },
903
1183
  { id: "personal", kind: "google", label: "Personal", email: "me@gmail.example", color: "#50fa7b" }
904
1184
  ];
905
1185
  const providers = /* @__PURE__ */ new Map([
@@ -938,13 +1218,22 @@ function createMockApi(onChanged) {
938
1218
  myStatus: "needsAction",
939
1219
  attendees: [{ email: "pm@work.example", status: "accepted", organizer: true }, { email: "me@work.example", status: "needsAction", self: true }]
940
1220
  });
1221
+ seed(work, "work-main", {
1222
+ title: "Quarterly roadmap sync with design",
1223
+ start: iso(0, 11, 30),
1224
+ end: iso(0, 12),
1225
+ location: "Zoom https://zoom.us/j/1234567890?pwd=abc"
1226
+ });
941
1227
  seed(personal, "p-main", { title: "Gym", start: iso(0, 19), end: iso(0, 20) });
942
1228
  for (let d = -2; d <= 4; d++) {
943
1229
  seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
944
1230
  }
1231
+ personal.rules.set("run-series", { freq: "daily", count: 7 });
945
1232
  seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
946
- const day = (offset) => format2(addDays(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
1233
+ const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
947
1234
  seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
1235
+ for (const title2 of ["Conference", "Team offsite", "Release freeze", "Birthday"])
1236
+ seed(personal, "p-main", { title: title2, start: day(3), end: day(4), allDay: true });
948
1237
  const prov = (id2) => {
949
1238
  const p = providers.get(id2);
950
1239
  if (!p) throw new Error(`unknown account ${id2}`);
@@ -971,6 +1260,14 @@ function createMockApi(onChanged) {
971
1260
  onChanged(id2);
972
1261
  return structuredClone(a);
973
1262
  },
1263
+ reauth: async (id2) => {
1264
+ const a = accounts.find((x) => x.id === id2);
1265
+ if (!a) throw new Error("unknown account");
1266
+ delete a.error;
1267
+ delete a.authError;
1268
+ onChanged(id2);
1269
+ return structuredClone(a);
1270
+ },
974
1271
  remove: async (id2) => {
975
1272
  const i = accounts.findIndex((a) => a.id === id2);
976
1273
  if (i >= 0) accounts.splice(i, 1);
@@ -999,8 +1296,8 @@ function createMockApi(onChanged) {
999
1296
  onChanged(input.accountId);
1000
1297
  return ev;
1001
1298
  },
1002
- update: async (ev) => {
1003
- const r = await own(ev.accountId, ev.calendarId).updateEvent(ev);
1299
+ update: async (ev, scope) => {
1300
+ const r = await own(ev.accountId, ev.calendarId).updateEvent(ev, scope);
1004
1301
  onChanged(ev.accountId);
1005
1302
  return r;
1006
1303
  },
@@ -1008,6 +1305,7 @@ function createMockApi(onChanged) {
1008
1305
  await own(ev.accountId, ev.calendarId).deleteEvent(ev, scope);
1009
1306
  onChanged(ev.accountId);
1010
1307
  },
1308
+ recurrence: async (ev) => own(ev.accountId, ev.calendarId).getRecurrence(ev),
1011
1309
  respond: async (ev, status) => {
1012
1310
  const r = await own(ev.accountId, ev.calendarId).respond(ev, status);
1013
1311
  onChanged(ev.accountId);
@@ -1034,7 +1332,7 @@ import {
1034
1332
 
1035
1333
  // ../../packages/core/src/providers/caldav/ics.ts
1036
1334
  import ICAL from "ical.js";
1037
- var MAX_INSTANCES = 1e5;
1335
+ var MAX_INSTANCES2 = 1e5;
1038
1336
  var PARTSTAT_IN = {
1039
1337
  ACCEPTED: "accepted",
1040
1338
  DECLINED: "declined",
@@ -1132,7 +1430,7 @@ function parseEvents(ics, href, etag, ctx, range) {
1132
1430
  -Infinity,
1133
1431
  ...vevents.filter((v) => v !== master).map((v) => v.getFirstPropertyValue("recurrence-id").toJSDate().getTime())
1134
1432
  );
1135
- for (let i = 0, next = it.next(); next && i < MAX_INSTANCES; i++, next = it.next()) {
1433
+ for (let i = 0, next = it.next(); next && i < MAX_INSTANCES2; i++, next = it.next()) {
1136
1434
  const ms = next.toJSDate().getTime();
1137
1435
  if (ms >= rangeEnd && ms > lastOverride) break;
1138
1436
  const d = event.getOccurrenceDetails(next);
@@ -1155,9 +1453,40 @@ function buildIcs(uid, input, email) {
1155
1453
  setText(v, "location", input.location);
1156
1454
  setText(v, "description", input.description);
1157
1455
  setAttendees(v, input.attendees ?? [], email);
1456
+ if (input.recurrence) setRule(v, input.recurrence, input.allDay);
1158
1457
  root.addSubcomponent(v);
1159
1458
  return root.toString();
1160
1459
  }
1460
+ function setRule(v, r, allDay) {
1461
+ v.removeAllProperties("rrule");
1462
+ v.addPropertyWithValue("rrule", ICAL.Recur.fromString(toRRule(r, allDay)));
1463
+ }
1464
+ function recurrenceOf(raw) {
1465
+ const master = findMaster(parse(raw.ics));
1466
+ const rule = master?.getFirstPropertyValue("rrule");
1467
+ return master && rule ? fromRRule(rule.toString(), toIso(dtstart(master))) : null;
1468
+ }
1469
+ function applyRule(root, master, event) {
1470
+ if (event.recurrence === void 0) return;
1471
+ const overrides = root.getAllSubcomponents("vevent").filter((v) => v !== master);
1472
+ if (event.recurrence === null) {
1473
+ for (const n of ["rrule", "rdate", "exdate"]) master.removeAllProperties(n);
1474
+ for (const o of overrides) root.removeSubcomponent(o);
1475
+ master.addProperty(replaceTime(master, "dtstart", fromIso(event.start, event.allDay)));
1476
+ master.addProperty(replaceTime(master, "dtend", fromIso(event.end, event.allDay)));
1477
+ return;
1478
+ }
1479
+ setRule(master, event.recurrence, event.allDay);
1480
+ const ms = (t) => t.toJSDate().getTime();
1481
+ const rid = (o) => o.getFirstPropertyValue("recurrence-id");
1482
+ const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t) => t instanceof ICAL.Time);
1483
+ const last = Math.max(-Infinity, ...overrides.map((o) => ms(rid(o))), ...exdates.map(ms));
1484
+ const slots2 = /* @__PURE__ */ new Set();
1485
+ const it = master.getFirstPropertyValue("rrule").iterator(dtstart(master));
1486
+ for (let t = it.next(), n = 0; t && ms(t) <= last && n < MAX_INSTANCES2; t = it.next(), n++) slots2.add(ms(t));
1487
+ for (const o of overrides) if (!slots2.has(ms(rid(o)))) root.removeSubcomponent(o);
1488
+ mapDates(master, "exdate", (t) => slots2.has(ms(t)) ? t : null);
1489
+ }
1161
1490
  function setAttendees(v, emails, email) {
1162
1491
  const wanted = [...new Set(emails.map(cleanEmail).filter(Boolean))];
1163
1492
  const existing = new Map(v.getAllProperties("attendee").map((p) => [cleanEmail(p.getFirstValue()), p]));
@@ -1217,9 +1546,14 @@ function applyUpdate(event, email) {
1217
1546
  const raw = event.raw;
1218
1547
  const root = parse(raw.ics);
1219
1548
  const v = targetVevent(root, raw);
1220
- setTime(v, "dtstart", fromIso(event.start, event.allDay));
1549
+ writeEdit(v, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
1550
+ if (event.recurrence && !raw.recurrenceId && !v.hasProperty("rrule")) setRule(v, event.recurrence, event.allDay);
1551
+ return root.toString();
1552
+ }
1553
+ function writeEdit(v, event, email, start, end) {
1554
+ v.addProperty(replaceTime(v, "dtstart", start));
1221
1555
  v.removeAllProperties("duration");
1222
- setTime(v, "dtend", fromIso(event.end, event.allDay));
1556
+ v.addProperty(replaceTime(v, "dtend", end));
1223
1557
  setText(v, "summary", event.title);
1224
1558
  setText(v, "location", event.location);
1225
1559
  setText(v, "description", event.description);
@@ -1228,10 +1562,156 @@ function applyUpdate(event, email) {
1228
1562
  event.attendees.map((a) => a.email),
1229
1563
  email
1230
1564
  );
1565
+ bump(v);
1566
+ }
1567
+ function bump(v) {
1231
1568
  v.updatePropertyWithValue("sequence", Number(v.getFirstPropertyValue("sequence") ?? 0) + 1);
1232
1569
  touch(v);
1570
+ }
1571
+ function timeProp(name, t) {
1572
+ const p = new ICAL.Property(name);
1573
+ p.setValue(t);
1574
+ const tzid = t.zone?.tzid;
1575
+ if (!t.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
1576
+ return p;
1577
+ }
1578
+ function replaceTime(v, name, t) {
1579
+ v.removeAllProperties(name);
1580
+ return timeProp(name, t);
1581
+ }
1582
+ var dtstart = (v) => v.getFirstPropertyValue("dtstart");
1583
+ var dateOf = (t) => ICAL.Time.fromData({ year: t.year, month: t.month, day: t.day, isDate: true });
1584
+ var daysBetween = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
1585
+ function addDays3(t, days) {
1586
+ const x = t.clone();
1587
+ x.adjust(days, 0, 0, 0);
1588
+ return x;
1589
+ }
1590
+ function spanFrom(start, event) {
1591
+ if (event.allDay) return [start, addDays3(start, daysBetween(fromIso(event.start, true), fromIso(event.end, true)))];
1592
+ const end = start.clone();
1593
+ end.adjust(0, 0, 0, (Date.parse(event.end) - Date.parse(event.start)) / 1e3);
1594
+ return [start, end];
1595
+ }
1596
+ function slotShift(master, before, event) {
1597
+ const anchor = dtstart(master);
1598
+ const zone = anchor.isDate ? void 0 : anchor.zone;
1599
+ const local = (t) => zone && !t.isDate ? t.convertToZone(zone) : t.clone();
1600
+ const after = fromIso(event.start, event.allDay);
1601
+ if (event.allDay) {
1602
+ const days2 = daysBetween(local(before), after);
1603
+ return (t) => addDays3(dateOf(local(t)), days2);
1604
+ }
1605
+ if (!anchor.isDate) {
1606
+ const seconds = (zone ? after.convertToZone(zone) : after).subtractDate(local(before)).toSeconds();
1607
+ return (t) => {
1608
+ const x = local(t);
1609
+ x.adjust(0, 0, 0, seconds);
1610
+ return x;
1611
+ };
1612
+ }
1613
+ const days = daysBetween(before, after);
1614
+ return (t) => {
1615
+ const d = addDays3(dateOf(t), days);
1616
+ return ICAL.Time.fromData({ year: d.year, month: d.month, day: d.day, hour: after.hour, minute: after.minute, second: after.second }, ICAL.Timezone.utcTimezone);
1617
+ };
1618
+ }
1619
+ function mapDates(v, name, map) {
1620
+ const props = v.getAllProperties(name);
1621
+ if (!props.length) return;
1622
+ const out = [];
1623
+ for (const p of props) {
1624
+ for (const value of p.getValues()) {
1625
+ if (!(value instanceof ICAL.Time)) {
1626
+ const keep = new ICAL.Property(name);
1627
+ keep.setValue(value);
1628
+ out.push(keep);
1629
+ continue;
1630
+ }
1631
+ const t = map(value);
1632
+ if (t) out.push(timeProp(name, t));
1633
+ }
1634
+ }
1635
+ v.removeAllProperties(name);
1636
+ for (const p of out) v.addProperty(p);
1637
+ }
1638
+ var attendeeKey = (v) => v.getAllProperties("attendee").map((p) => cleanEmail(p.getFirstValue())).sort().join(",");
1639
+ function rewriteSeries(root, master, edited, event, email, shift, anchor) {
1640
+ const shared = ["summary", "location", "description"].map((n) => [n, master.getFirstPropertyValue(n)]);
1641
+ const people = attendeeKey(master);
1642
+ const retyped = dtstart(master).isDate !== event.allDay;
1643
+ const [start, end] = spanFrom(shift(anchor), event);
1644
+ writeEdit(master, event, email, start, end);
1645
+ mapDates(master, "exdate", shift);
1646
+ mapDates(master, "rdate", shift);
1647
+ for (const o of root.getAllSubcomponents("vevent")) {
1648
+ const rid = o.getFirstPropertyValue("recurrence-id");
1649
+ if (!rid) continue;
1650
+ o.addProperty(replaceTime(o, "recurrence-id", shift(rid)));
1651
+ if (o === edited) {
1652
+ writeEdit(o, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
1653
+ continue;
1654
+ }
1655
+ const [s, e] = retyped ? spanFrom(shift(dtstart(o)), event) : [shift(dtstart(o)), shift(new ICAL.Event(o).endDate)];
1656
+ o.addProperty(replaceTime(o, "dtstart", s));
1657
+ o.removeAllProperties("duration");
1658
+ o.addProperty(replaceTime(o, "dtend", e));
1659
+ for (const [name, old] of shared) {
1660
+ if (String(o.getFirstPropertyValue(name) ?? "") === String(old ?? "")) setText(o, name, event[name === "summary" ? "title" : name]);
1661
+ }
1662
+ if (attendeeKey(o) === people) setAttendees(o, event.attendees.map((a) => a.email), email);
1663
+ bump(o);
1664
+ }
1665
+ }
1666
+ var instanceStart = (master, override, rid) => override ? dtstart(override) : ridTime(master, rid).time;
1667
+ function applyUpdateSeries(event, email) {
1668
+ const raw = event.raw;
1669
+ const root = parse(raw.ics);
1670
+ const master = findMaster(root);
1671
+ if (!raw.recurrenceId || !master) return applyUpdate(event, email);
1672
+ const edited = findOverride(root, raw.recurrenceId);
1673
+ const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
1674
+ rewriteSeries(root, master, edited, event, email, shift, dtstart(master));
1675
+ applyRule(root, master, event);
1233
1676
  return root.toString();
1234
1677
  }
1678
+ function applySplitFollowing(event, email, uid) {
1679
+ const raw = event.raw;
1680
+ if (!raw.recurrenceId) throw new Error("Not a recurring instance");
1681
+ const rest = applyDeleteFollowing(raw);
1682
+ if (!rest) return null;
1683
+ const root = parse(raw.ics);
1684
+ const master = masterOf(root);
1685
+ const slot = ridTime(master, raw.recurrenceId).time;
1686
+ const cut = slot.toJSDate().getTime();
1687
+ const edited = findOverride(root, raw.recurrenceId);
1688
+ const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
1689
+ for (const v of root.getAllSubcomponents("vevent")) {
1690
+ const t = v.getFirstPropertyValue("recurrence-id");
1691
+ if (t && t.toJSDate().getTime() < cut) root.removeSubcomponent(v);
1692
+ }
1693
+ const later = (t) => t.toJSDate().getTime() >= cut ? t : null;
1694
+ mapDates(master, "exdate", later);
1695
+ mapDates(master, "rdate", later);
1696
+ const first = dtstart(master);
1697
+ for (const p of event.recurrence === void 0 ? master.getAllProperties("rrule") : []) {
1698
+ const r = p.getFirstValue().clone();
1699
+ if (!r.count) continue;
1700
+ const it = r.iterator(first);
1701
+ let passed = 0;
1702
+ for (let t = it.next(); t && t.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t = it.next()) passed++;
1703
+ r.count = Math.max(1, r.count - passed);
1704
+ p.setValue(r);
1705
+ }
1706
+ for (const v of root.getAllSubcomponents("vevent")) {
1707
+ v.updatePropertyWithValue("uid", uid);
1708
+ v.updatePropertyWithValue("sequence", -1);
1709
+ v.updatePropertyWithValue("created", ICAL.Time.now());
1710
+ }
1711
+ rewriteSeries(root, master, edited, event, email, shift, slot);
1712
+ applyRule(root, master, event);
1713
+ return { rest, next: root.toString() };
1714
+ }
1235
1715
  function applyDeleteInstance(raw) {
1236
1716
  if (!raw.recurrenceId) throw new Error("Not a recurring instance");
1237
1717
  const root = parse(raw.ics);
@@ -1289,18 +1769,6 @@ function applyRespond(raw, email, status) {
1289
1769
  return root.toString();
1290
1770
  }
1291
1771
 
1292
- // ../../packages/core/src/providers/http.ts
1293
- var http = { timeoutMs: 3e4 };
1294
- var timedFetch = async (input, init) => {
1295
- const timeout = AbortSignal.timeout(http.timeoutMs);
1296
- try {
1297
- return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
1298
- } catch (e) {
1299
- if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
1300
- throw e;
1301
- }
1302
- };
1303
-
1304
1772
  // ../../packages/core/src/providers/caldav/index.ts
1305
1773
  var FALLBACK_COLOR = "#8e8e93";
1306
1774
  var DAY = 864e5;
@@ -1325,7 +1793,7 @@ async function connect2(serverUrl, username, password) {
1325
1793
  });
1326
1794
  return { account, headers };
1327
1795
  } catch (e) {
1328
- if (unauthorized) throw new Error("CalDAV login failed: wrong username or password");
1796
+ if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
1329
1797
  throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
1330
1798
  }
1331
1799
  }
@@ -1351,10 +1819,15 @@ async function listVeventCalendars(conn) {
1351
1819
  headers: conn.headers,
1352
1820
  props: CALENDAR_PROPS,
1353
1821
  projectedProps: { currentUserPrivilegeSet: true },
1354
- fetch: timedFetch
1822
+ fetch: davFetch
1355
1823
  });
1356
1824
  return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
1357
1825
  }
1826
+ var davFetch = async (input, init) => {
1827
+ const res = await timedFetch(input, init);
1828
+ if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
1829
+ return res;
1830
+ };
1358
1831
  function check(res, what) {
1359
1832
  if (res.ok) return;
1360
1833
  if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
@@ -1374,18 +1847,19 @@ var createCaldavProvider = (ctx) => {
1374
1847
  const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
1375
1848
  async function reload(calendarId, href, id2, around) {
1376
1849
  const { headers } = await getConn();
1377
- const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: timedFetch });
1850
+ const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
1378
1851
  if (!obj?.data) throw new Error("Saved event could not be read back from the server");
1379
1852
  const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
1380
1853
  const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
1381
- const ev = id2 === void 0 ? events[0] : events.find((e) => e.id === id2);
1854
+ const at3 = Date.parse(around.start);
1855
+ const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
1382
1856
  if (!ev) throw new Error("Saved event not found on the server");
1383
1857
  return ev;
1384
1858
  }
1385
1859
  const put = async (event, ics, what) => {
1386
1860
  const { headers } = await getConn();
1387
1861
  const raw = event.raw;
1388
- check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: timedFetch }), what);
1862
+ check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
1389
1863
  };
1390
1864
  const provider = {
1391
1865
  async listCalendars() {
@@ -1394,7 +1868,7 @@ var createCaldavProvider = (ctx) => {
1394
1868
  },
1395
1869
  async listEvents(calendarId, range) {
1396
1870
  const { headers } = await getConn();
1397
- const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: timedFetch });
1871
+ const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
1398
1872
  return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
1399
1873
  },
1400
1874
  async createEvent(calendarId, input) {
@@ -1402,20 +1876,38 @@ var createCaldavProvider = (ctx) => {
1402
1876
  const uid = randomUUID4();
1403
1877
  const filename = `${uid}.ics`;
1404
1878
  const ics = buildIcs(uid, input, ctx.email);
1405
- check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: timedFetch }), "Create event");
1879
+ check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
1406
1880
  const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
1407
1881
  return reload(calendarId, href, void 0, input);
1408
1882
  },
1409
- async updateEvent(event) {
1410
- await put(event, applyUpdate(event, ctx.email), "Update event");
1411
- return reload(event.calendarId, event.raw.href, event.id, event);
1883
+ async updateEvent(event, scope = "one") {
1884
+ const raw = event.raw;
1885
+ if (!raw.recurrenceId || scope === "one") {
1886
+ await put(event, applyUpdate(event, ctx.email), "Update event");
1887
+ return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
1888
+ }
1889
+ const uid = randomUUID4();
1890
+ const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
1891
+ if (!split) {
1892
+ await put(event, applyUpdateSeries(event, ctx.email), "Update event");
1893
+ return reload(event.calendarId, raw.href, void 0, event);
1894
+ }
1895
+ await put(event, split.rest, "Update event");
1896
+ const { headers } = await getConn();
1897
+ const filename = `${uid}.ics`;
1898
+ check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
1899
+ const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
1900
+ return reload(event.calendarId, href, void 0, event);
1412
1901
  },
1413
1902
  async deleteEvent(event, scope = "one") {
1414
1903
  const raw = event.raw;
1415
1904
  const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
1416
1905
  if (rest) return put(event, rest, "Delete event");
1417
1906
  const { headers } = await getConn();
1418
- check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: timedFetch }), "Delete event");
1907
+ check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
1908
+ },
1909
+ async getRecurrence(event) {
1910
+ return recurrenceOf(event.raw);
1419
1911
  },
1420
1912
  async respond(event, status) {
1421
1913
  await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
@@ -1427,7 +1919,7 @@ var createCaldavProvider = (ctx) => {
1427
1919
  async function verifyCaldav(input) {
1428
1920
  const c = await connect2(input.serverUrl.trim(), input.username, input.password);
1429
1921
  await listVeventCalendars(c);
1430
- const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: timedFetch }).catch(() => []);
1922
+ const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
1431
1923
  const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
1432
1924
  const login = cleanEmail(input.username);
1433
1925
  const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
@@ -1480,7 +1972,10 @@ function emailFromIdToken(idToken) {
1480
1972
  }
1481
1973
  function parseTokenResponse(json, now = Date.now()) {
1482
1974
  const j = json ?? {};
1483
- if (typeof j.error === "string") throw new Error(`Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`);
1975
+ if (typeof j.error === "string") {
1976
+ const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
1977
+ throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
1978
+ }
1484
1979
  if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
1485
1980
  const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
1486
1981
  return {
@@ -1607,7 +2102,29 @@ function truncateRecurrence(recurrence, cutoff) {
1607
2102
  return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
1608
2103
  });
1609
2104
  }
2105
+ var DAY2 = 864e5;
2106
+ var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY2).toISOString().slice(0, 10);
2107
+ var daysBetween2 = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY2);
2108
+ var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
2109
+ function shiftAnchor(anchor, before, start, allDay, timeZone) {
2110
+ if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween2(before.date ?? before.dateTime, start)) };
2111
+ if (anchor.dateTime && before.dateTime) {
2112
+ return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
2113
+ }
2114
+ const at3 = new Date(start);
2115
+ const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween2(before.date, localDate(at3)))}T00:00:00`);
2116
+ day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
2117
+ return { dateTime: day.toISOString(), timeZone };
2118
+ }
2119
+ function endFrom(from, event, timeZone) {
2120
+ if (event.allDay) return { date: plusDays(from.date, daysBetween2(event.start, event.end)) };
2121
+ return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
2122
+ }
1610
2123
  var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
2124
+ var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
2125
+ var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
2126
+ var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
2127
+ var nulled = (t) => ({ date: null, dateTime: null, timeZone: null, ...t });
1611
2128
  function createGoogleProviderImpl(ctx) {
1612
2129
  if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
1613
2130
  let creds = ctx.credentials;
@@ -1630,6 +2147,7 @@ function createGoogleProviderImpl(ctx) {
1630
2147
  });
1631
2148
  let res = await send(await token());
1632
2149
  if (res.status === 401) res = await send(await refresh());
2150
+ if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
1633
2151
  if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
1634
2152
  if (!res.ok) {
1635
2153
  const err = await res.json().catch(() => ({}));
@@ -1645,6 +2163,35 @@ function createGoogleProviderImpl(ctx) {
1645
2163
  return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
1646
2164
  };
1647
2165
  const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
2166
+ const fields = (event, prevAttendees, organizer) => {
2167
+ const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
2168
+ if (organizer) {
2169
+ const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
2170
+ body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
2171
+ }
2172
+ return body;
2173
+ };
2174
+ async function remainingRecurrence(calendarId, series, recurrence, slot) {
2175
+ const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
2176
+ if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
2177
+ const cut = Date.parse(slot.dateTime ?? slot.date);
2178
+ let passed = 0;
2179
+ let pageToken;
2180
+ do {
2181
+ const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
2182
+ showDeleted: "true",
2183
+ timeMax: new Date(cut).toISOString(),
2184
+ maxResults: "2500",
2185
+ ...pageToken ? { pageToken } : {}
2186
+ });
2187
+ for (const g of page.items ?? []) {
2188
+ const t = g.originalStartTime ?? g.start;
2189
+ if (Date.parse(t.dateTime ?? t.date) < cut) passed++;
2190
+ }
2191
+ pageToken = page.nextPageToken;
2192
+ } while (pageToken);
2193
+ return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
2194
+ }
1648
2195
  return {
1649
2196
  async listCalendars() {
1650
2197
  const out = [];
@@ -1685,36 +2232,67 @@ function createGoogleProviderImpl(ctx) {
1685
2232
  },
1686
2233
  async createEvent(calendarId, input) {
1687
2234
  const attendees = (input.attendees ?? []).map((email) => ({ email }));
2235
+ const rule = input.recurrence;
1688
2236
  const g = await api(
1689
2237
  "POST",
1690
2238
  eventPath(calendarId),
1691
2239
  { sendUpdates: attendees.length ? "all" : "none" },
1692
2240
  {
1693
2241
  summary: input.title,
1694
- start: toGTime(input.start, input.allDay),
1695
- end: toGTime(input.end, input.allDay),
2242
+ start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
2243
+ end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
1696
2244
  location: input.location,
1697
2245
  description: input.description,
1698
- attendees
2246
+ attendees,
2247
+ ...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
1699
2248
  }
1700
2249
  );
1701
2250
  return map(g, calendarId);
1702
2251
  },
1703
- async updateEvent(event) {
2252
+ async updateEvent(event, scope = "one") {
1704
2253
  const organizer = isOrganizer(event);
1705
- const body = {
1706
- summary: event.title,
1707
- start: toGTime(event.start, event.allDay),
1708
- end: toGTime(event.end, event.allDay),
1709
- location: event.location ?? null,
1710
- description: event.description ?? null
1711
- };
1712
- if (organizer) {
1713
- const prev = new Map((rawOf(event)?.attendees ?? []).map((a) => [a.email.toLowerCase(), a]));
1714
- body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
2254
+ const q = { sendUpdates: organizer ? "all" : "none" };
2255
+ const series = event.recurringEventId;
2256
+ const before = rawOf(event)?.start;
2257
+ if (!series || scope === "one" || !before) {
2258
+ const rule2 = !series ? event.recurrence : void 0;
2259
+ const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
2260
+ const body = {
2261
+ ...fields(event, rawOf(event)?.attendees, organizer),
2262
+ start: time(event.start),
2263
+ end: time(event.end),
2264
+ ...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
2265
+ };
2266
+ return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
1715
2267
  }
1716
- const g = await api("PATCH", eventPath(event.calendarId, event.id), { sendUpdates: organizer ? "all" : "none" }, body);
1717
- return map(g, event.calendarId);
2268
+ const master = await api("GET", eventPath(event.calendarId, series));
2269
+ const timeZone = master.start.timeZone ?? localZone();
2270
+ const rule = event.recurrence;
2271
+ const slot = rawOf(event)?.originalStartTime ?? before;
2272
+ const first = master.start.dateTime ?? master.start.date;
2273
+ const at3 = slot.dateTime ?? slot.date;
2274
+ if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
2275
+ const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
2276
+ const body = {
2277
+ ...fields(event, master.attendees, organizer),
2278
+ start: nulled(start2),
2279
+ end: nulled(endFrom(start2, event, timeZone)),
2280
+ // A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
2281
+ ...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
2282
+ };
2283
+ await api("PATCH", eventPath(event.calendarId, series), q, body);
2284
+ return event;
2285
+ }
2286
+ const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
2287
+ await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
2288
+ const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
2289
+ const created = await api("POST", eventPath(event.calendarId), q, {
2290
+ ...fields(event, master.attendees, organizer),
2291
+ start,
2292
+ end: endFrom(start, event, timeZone),
2293
+ ...recurrence ? { recurrence } : {}
2294
+ });
2295
+ return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
1718
2296
  },
1719
2297
  async deleteEvent(event, scope = "one") {
1720
2298
  const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
@@ -1732,6 +2310,12 @@ function createGoogleProviderImpl(ctx) {
1732
2310
  }
1733
2311
  await api("DELETE", eventPath(event.calendarId, series), q);
1734
2312
  },
2313
+ async getRecurrence(event) {
2314
+ if (!event.recurringEventId) return null;
2315
+ const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
2316
+ const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
2317
+ return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
2318
+ },
1735
2319
  async respond(event, status) {
1736
2320
  const attendees = rawOf(event)?.attendees ?? event.attendees.map((a) => ({ email: a.email, displayName: a.name, responseStatus: a.status, self: a.self, organizer: a.organizer }));
1737
2321
  if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
@@ -2070,32 +2654,32 @@ async function runDaemon() {
2070
2654
  // src/ui/hooks.ts
2071
2655
  import { createContext, useCallback, useContext, useEffect, useState } from "react";
2072
2656
  import { useStdout } from "ink";
2073
- import { addDays as addDays3, addWeeks as addWeeks2, startOfDay as startOfDay2 } from "date-fns";
2657
+ import { addDays as addDays5, addWeeks as addWeeks2, startOfDay as startOfDay2 } from "date-fns";
2074
2658
 
2075
2659
  // ../../packages/core/src/logic/layout.ts
2076
2660
  import {
2077
- addDays as addDays2,
2661
+ addDays as addDays4,
2078
2662
  addMonths as addMonths2,
2079
2663
  addWeeks,
2080
- format as format3,
2664
+ format as format5,
2081
2665
  isSameDay as isSameDay2,
2082
- parseISO as parseISO4,
2666
+ parseISO as parseISO5,
2083
2667
  startOfDay,
2084
2668
  startOfMonth,
2085
- startOfWeek
2669
+ startOfWeek as startOfWeek2
2086
2670
  } from "date-fns";
2087
2671
  var WEEK_STARTS_ON = 1;
2088
2672
  function eventBounds(e) {
2089
- const start = parseISO4(e.start);
2090
- let end = parseISO4(e.end);
2091
- if (end <= start) end = e.allDay ? addDays2(start, 1) : new Date(start.getTime() + 6e4);
2673
+ const start = parseISO5(e.start);
2674
+ let end = parseISO5(e.end);
2675
+ if (end <= start) end = e.allDay ? addDays4(start, 1) : new Date(start.getTime() + 6e4);
2092
2676
  return { start, end };
2093
2677
  }
2094
2678
  var isPast = (e, now) => eventBounds(e).end.getTime() <= +now;
2095
2679
  function overlapsDay(e, day) {
2096
2680
  const d0 = startOfDay(day);
2097
2681
  const { start, end } = eventBounds(e);
2098
- return start < addDays2(d0, 1) && end > d0;
2682
+ return start < addDays4(d0, 1) && end > d0;
2099
2683
  }
2100
2684
  function eventsOnDay(events, day) {
2101
2685
  return events.filter((e) => overlapsDay(e, day)).sort((a, b) => {
@@ -2142,31 +2726,31 @@ function layoutDay(events, day, minDur = 0) {
2142
2726
  return packColumns(items, minDur);
2143
2727
  }
2144
2728
  function monthGrid(date) {
2145
- const first = startOfWeek(startOfMonth(date), { weekStartsOn: WEEK_STARTS_ON });
2146
- return Array.from({ length: 42 }, (_, i) => addDays2(first, i));
2729
+ const first = startOfWeek2(startOfMonth(date), { weekStartsOn: WEEK_STARTS_ON });
2730
+ return Array.from({ length: 42 }, (_, i) => addDays4(first, i));
2147
2731
  }
2148
2732
  function viewDays(view, date) {
2149
2733
  if (view === "month") return monthGrid(date);
2150
2734
  if (view === "day") return [startOfDay(date)];
2151
- if (view === "3day") return [0, 1, 2].map((i) => addDays2(startOfDay(date), i));
2152
- const first = startOfWeek(date, { weekStartsOn: WEEK_STARTS_ON });
2153
- return Array.from({ length: 7 }, (_, i) => addDays2(first, i));
2735
+ if (view === "3day") return [0, 1, 2].map((i) => addDays4(startOfDay(date), i));
2736
+ const first = startOfWeek2(date, { weekStartsOn: WEEK_STARTS_ON });
2737
+ return Array.from({ length: 7 }, (_, i) => addDays4(first, i));
2154
2738
  }
2155
2739
  function viewRange(view, date) {
2156
2740
  const days = viewDays(view, date);
2157
- return { start: days[0].toISOString(), end: addDays2(days[days.length - 1], 1).toISOString() };
2741
+ return { start: days[0].toISOString(), end: addDays4(days[days.length - 1], 1).toISOString() };
2158
2742
  }
2159
2743
  function shiftDate(view, date, dir) {
2160
- if (view === "day") return addDays2(date, dir);
2161
- if (view === "3day") return addDays2(date, 3 * dir);
2744
+ if (view === "day") return addDays4(date, dir);
2745
+ if (view === "3day") return addDays4(date, 3 * dir);
2162
2746
  if (view === "week") return addWeeks(date, dir);
2163
2747
  return addMonths2(date, dir);
2164
2748
  }
2165
2749
  function rangeLabel(a, b) {
2166
2750
  const head = a.getFullYear() !== b.getFullYear() ? "d MMM yyyy" : a.getMonth() !== b.getMonth() ? "d MMM" : "d";
2167
- return `${format3(a, head)} \u2013 ${format3(b, "d MMM yyyy")}`;
2751
+ return `${format5(a, head)} \u2013 ${format5(b, "d MMM yyyy")}`;
2168
2752
  }
2169
- var ymd = (d) => format3(d, "yyyy-MM-dd");
2753
+ var ymd = (d) => format5(d, "yyyy-MM-dd");
2170
2754
 
2171
2755
  // ../../packages/core/src/logic/visible.ts
2172
2756
  var key2 = (accountId, calendarId) => `${accountId}/${calendarId}`;
@@ -2186,7 +2770,7 @@ var eventKey = (e) => `${e.accountId}/${e.calendarId}/${e.id}`;
2186
2770
  function useLive(load, deps) {
2187
2771
  const api = useApi();
2188
2772
  const [state, setState] = useState({});
2189
- const [bump, setBump] = useState(0);
2773
+ const [bump2, setBump] = useState(0);
2190
2774
  useEffect(() => {
2191
2775
  let seq = 0;
2192
2776
  let alive2 = true;
@@ -2203,7 +2787,7 @@ function useLive(load, deps) {
2203
2787
  alive2 = false;
2204
2788
  off();
2205
2789
  };
2206
- }, [api, bump, ...deps]);
2790
+ }, [api, bump2, ...deps]);
2207
2791
  return { ...state, reload: useCallback(() => setBump((b) => b + 1), []) };
2208
2792
  }
2209
2793
  function useDirectory() {
@@ -2224,13 +2808,13 @@ var AGENDA_DAYS = 14;
2224
2808
  function navRange({ view, date }) {
2225
2809
  if (view === "agenda" || view === "2day") {
2226
2810
  const start = startOfDay2(date);
2227
- return { start: start.toISOString(), end: addDays3(start, view === "agenda" ? AGENDA_DAYS : 2).toISOString() };
2811
+ return { start: start.toISOString(), end: addDays5(start, view === "agenda" ? AGENDA_DAYS : 2).toISOString() };
2228
2812
  }
2229
2813
  return viewRange(view, date);
2230
2814
  }
2231
2815
  var shiftNav = ({ view, date }, dir) => ({
2232
2816
  view,
2233
- date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ? addDays3(date, 2 * dir) : shiftDate(view, date, dir)
2817
+ date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ? addDays5(date, 2 * dir) : shiftDate(view, date, dir)
2234
2818
  });
2235
2819
  function useNav(initial = { view: "agenda", date: /* @__PURE__ */ new Date() }) {
2236
2820
  const [nav, setNav] = useState(initial);
@@ -2265,12 +2849,12 @@ function useTermSize() {
2265
2849
  // src/ui/App.tsx
2266
2850
  import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
2267
2851
  import { Box as Box11, Text as Text11, useApp, useStdin as useStdin2 } from "ink";
2268
- import { addDays as addDays9, differenceInMinutes as differenceInMinutes3, format as format11, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
2852
+ import { addDays as addDays11, differenceInMinutes as differenceInMinutes3, format as format13, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
2269
2853
 
2270
2854
  // ../../packages/core/src/logic/editor.ts
2271
- import { addDays as addDays4, addHours, format as format4, parseISO as parseISO5, startOfHour } from "date-fns";
2855
+ import { addDays as addDays6, addHours, format as format6, parseISO as parseISO6, startOfHour } from "date-fns";
2272
2856
  var LOCAL = "yyyy-MM-dd'T'HH:mm";
2273
- var toLocalInput = (iso2) => format4(iso2.length === 10 ? parseISO5(iso2) : new Date(iso2), LOCAL);
2857
+ var toLocalInput = (iso2) => format6(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
2274
2858
  var fromLocalInput = (local) => new Date(local).toISOString();
2275
2859
  var writableCalendars = (calendars, accountId) => calendars.filter((c) => c.accountId === accountId && !c.readOnly);
2276
2860
  var writableAccounts = (accounts, calendars) => accounts.filter((a) => writableCalendars(calendars, a.id).length > 0);
@@ -2280,8 +2864,8 @@ var splitEmails = (text2) => text2.split(/[\s,;]+/).map((s) => s.trim()).filter(
2280
2864
  var isEmail = (s) => EMAIL.test(s);
2281
2865
  function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new Date()) {
2282
2866
  const accountId = soleId(writableAccounts(accounts, calendars));
2283
- const start = prefill.start ? toLocalInput(prefill.start) : format4(startOfHour(addHours(now, 1)), LOCAL);
2284
- let end = prefill.end ? toLocalInput(prefill.end) : format4(addHours(new Date(start), 1), LOCAL);
2867
+ const start = prefill.start ? toLocalInput(prefill.start) : format6(startOfHour(addHours(now, 1)), LOCAL);
2868
+ let end = prefill.end ? toLocalInput(prefill.end) : format6(addHours(new Date(start), 1), LOCAL);
2285
2869
  if (prefill.allDay && prefill.end) end = inclusiveEnd(start, end);
2286
2870
  return {
2287
2871
  accountId,
@@ -2292,11 +2876,13 @@ function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new
2292
2876
  allDay: !!prefill.allDay,
2293
2877
  location: "",
2294
2878
  description: "",
2295
- attendees: []
2879
+ attendees: [],
2880
+ repeat: null,
2881
+ repeatWas: null
2296
2882
  };
2297
2883
  }
2298
2884
  var inclusiveEnd = (startLocal, endLocal) => {
2299
- const d = format4(addDays4(new Date(endLocal), -1), LOCAL);
2885
+ const d = format6(addDays6(new Date(endLocal), -1), LOCAL);
2300
2886
  return d.slice(0, 10) < startLocal.slice(0, 10) ? startLocal : d;
2301
2887
  };
2302
2888
  function formFromEvent(e) {
@@ -2311,23 +2897,28 @@ function formFromEvent(e) {
2311
2897
  allDay: e.allDay,
2312
2898
  location: e.location ?? "",
2313
2899
  description: e.description ?? "",
2314
- attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email)
2900
+ attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email),
2901
+ repeat: null,
2902
+ // A series' rule isn't cached: the editor loads it (events.recurrence) and sets both fields.
2903
+ repeatWas: e.recurringEventId ? void 0 : null
2315
2904
  };
2316
2905
  }
2906
+ var withLoadedRepeat = (f, rule) => ({ ...f, repeat: rule, repeatWas: rule });
2907
+ var repeatChanged = (f) => f.repeatWas !== void 0 && !sameRecurrence(f.repeat, f.repeatWas, f.start);
2317
2908
  function setAllDay(f, allDay) {
2318
2909
  if (allDay || f.end > f.start) return { ...f, allDay };
2319
- return { ...f, allDay, end: format4(addHours(new Date(f.start), 1), LOCAL) };
2910
+ return { ...f, allDay, end: format6(addHours(new Date(f.start), 1), LOCAL) };
2320
2911
  }
2321
2912
  function moveStart(f, start) {
2322
2913
  const end = new Date(new Date(f.end).getTime() + new Date(start).getTime() - new Date(f.start).getTime());
2323
- return { ...f, start, end: format4(end, LOCAL) };
2914
+ return { ...f, start, end: format6(end, LOCAL) };
2324
2915
  }
2325
2916
  function times(f) {
2326
2917
  if (f.allDay) {
2327
2918
  const start2 = f.start.slice(0, 10);
2328
2919
  const endIncl = f.end.slice(0, 10);
2329
2920
  if (endIncl < start2) throw new Error("End date is before start date");
2330
- return { allDay: true, start: start2, end: format4(addDays4(parseISO5(endIncl), 1), "yyyy-MM-dd") };
2921
+ return { allDay: true, start: start2, end: format6(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
2331
2922
  }
2332
2923
  const start = fromLocalInput(f.start);
2333
2924
  const end = fromLocalInput(f.end);
@@ -2350,7 +2941,8 @@ function formToInput(f) {
2350
2941
  ...times(f),
2351
2942
  location: f.location.trim() || void 0,
2352
2943
  description: f.description.trim() || void 0,
2353
- ...attendees.length ? { attendees } : {}
2944
+ ...attendees.length ? { attendees } : {},
2945
+ ...f.repeat ? { recurrence: f.repeat } : {}
2354
2946
  };
2355
2947
  }
2356
2948
  function applyForm(e, f) {
@@ -2364,7 +2956,9 @@ function applyForm(e, f) {
2364
2956
  ...times(f),
2365
2957
  location: f.location.trim() || void 0,
2366
2958
  description: f.description.trim() || void 0,
2367
- attendees: [...keep, ...invited]
2959
+ attendees: [...keep, ...invited],
2960
+ // Only a changed rule is sent; absent keeps the series' rule.
2961
+ ...repeatChanged(f) ? { recurrence: f.repeat } : {}
2368
2962
  };
2369
2963
  }
2370
2964
  var errorText = (e) => String(e instanceof Error ? e.message : e).replace(/^Error invoking remote method '[^']+': (\w*Error: )?/, "");
@@ -2483,10 +3077,10 @@ function Button({ k, label, onPress, color: color2 = C.accent }) {
2483
3077
  // src/ui/views/Agenda.tsx
2484
3078
  import { useMemo, useReducer, useRef as useRef2 } from "react";
2485
3079
  import { Box as Box3, Text as Text3 } from "ink";
2486
- import { addDays as addDays6, differenceInCalendarDays, differenceInMinutes as differenceInMinutes2, format as format7, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
3080
+ import { addDays as addDays8, differenceInCalendarDays as differenceInCalendarDays2, differenceInMinutes as differenceInMinutes2, format as format9, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
2487
3081
 
2488
3082
  // ../../packages/core/src/logic/details.ts
2489
- import { addDays as addDays5, format as format5, isSameDay as isSameDay3, parseISO as parseISO6 } from "date-fns";
3083
+ import { addDays as addDays7, format as format7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
2490
3084
  var same = (a, b) => !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
2491
3085
  function ownerLine(calendar, label, email) {
2492
3086
  return { calendar: same(calendar, label) || same(calendar, email) ? void 0 : calendar, label, email: same(email, label) ? void 0 : email };
@@ -2496,17 +3090,17 @@ function canEdit(e, account, calendar) {
2496
3090
  if (e.attendees.length === 0) return true;
2497
3091
  return same(e.organizer?.email, account.email) || e.attendees.some((a) => a.self && a.organizer);
2498
3092
  }
2499
- var at2 = (iso2, allDay) => (allDay ? parseISO6(iso2) : new Date(iso2)).getTime();
3093
+ var at2 = (iso2, allDay) => (allDay ? parseISO7(iso2) : new Date(iso2)).getTime();
2500
3094
  var pendingInvites = (events, now = /* @__PURE__ */ new Date()) => events.filter((e) => e.myStatus === "needsAction" && at2(e.end, e.allDay) > now.getTime()).sort((a, b) => at2(a.start, a.allDay) - at2(b.start, b.allDay));
2501
3095
  function formatWhen(e) {
2502
3096
  if (e.allDay) {
2503
- const s2 = parseISO6(e.start);
2504
- const last = addDays5(parseISO6(e.end), -1);
2505
- return last > s2 ? `${format5(s2, "EEE, d MMM")} \u2013 ${format5(last, "EEE, d MMM")} \xB7 all day` : `${format5(s2, "EEE, d MMM")} \xB7 all day`;
3097
+ const s2 = parseISO7(e.start);
3098
+ const last = addDays7(parseISO7(e.end), -1);
3099
+ return last > s2 ? `${format7(s2, "EEE, d MMM")} \u2013 ${format7(last, "EEE, d MMM")} \xB7 all day` : `${format7(s2, "EEE, d MMM")} \xB7 all day`;
2506
3100
  }
2507
3101
  const s = new Date(e.start);
2508
3102
  const t = new Date(e.end);
2509
- return isSameDay3(s, t) ? `${format5(s, "EEE, d MMM")} \xB7 ${format5(s, "HH:mm")} \u2013 ${format5(t, "HH:mm")}` : `${format5(s, "EEE, d MMM HH:mm")} \u2013 ${format5(t, "EEE, d MMM HH:mm")}`;
3103
+ return isSameDay3(s, t) ? `${format7(s, "EEE, d MMM")} \xB7 ${format7(s, "HH:mm")} \u2013 ${format7(t, "HH:mm")}` : `${format7(s, "EEE, d MMM HH:mm")} \u2013 ${format7(t, "EEE, d MMM HH:mm")}`;
2510
3104
  }
2511
3105
  var STATUS_ICON = {
2512
3106
  accepted: "\u2713",
@@ -2521,13 +3115,12 @@ function meetingUrl(location) {
2521
3115
  return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
2522
3116
  }
2523
3117
  function place(location = "") {
2524
- const url = meetingUrl(location);
2525
- const rest = (url ? location.replace(url, "") : location).replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
2526
- return rest || (url ? "video call" : "");
3118
+ const rest = location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3119
+ return rest || (meetingUrl(location) ? "video call" : "");
2527
3120
  }
2528
3121
 
2529
3122
  // ../../packages/core/src/logic/status.ts
2530
- import { differenceInMinutes, format as format6 } from "date-fns";
3123
+ import { differenceInMinutes, format as format8 } from "date-fns";
2531
3124
  function pickNowNext(events, now) {
2532
3125
  const t = now.getTime();
2533
3126
  const timed = events.filter((e) => !e.allDay && e.myStatus !== "declined").sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
@@ -2539,7 +3132,7 @@ function pickNowNext(events, now) {
2539
3132
  function startsLabel(start, now) {
2540
3133
  const s = new Date(start);
2541
3134
  const m = differenceInMinutes(s, now, { roundingMethod: "ceil" });
2542
- return m <= 60 ? `in ${m}m` : `at ${format6(s, "HH:mm")}`;
3135
+ return m <= 60 ? `in ${m}m` : `at ${format8(s, "HH:mm")}`;
2543
3136
  }
2544
3137
 
2545
3138
  // src/ui/screens/EventDetails.tsx
@@ -2753,7 +3346,7 @@ var span = (m) => {
2753
3346
  };
2754
3347
  function busyMinutes(list, day) {
2755
3348
  const from = +startOfDay3(day);
2756
- const to = +addDays6(day, 1);
3349
+ const to = +addDays8(day, 1);
2757
3350
  const iv = list.filter((e) => !e.allDay && e.myStatus !== "declined").map((e) => [Math.max(+eventBounds(e).start, from), Math.min(+eventBounds(e).end, to)]).sort((a, b) => a[0] - b[0]);
2758
3351
  let total = 0;
2759
3352
  let end = -Infinity;
@@ -2768,7 +3361,7 @@ var overlaps = (e, list) => !e.allDay && e.myStatus !== "declined" && list.some(
2768
3361
  (o) => o !== e && !o.allDay && o.myStatus !== "declined" && eventBounds(o).start < eventBounds(e).end && eventBounds(e).start < eventBounds(o).end
2769
3362
  );
2770
3363
  function relativeDay(day, now) {
2771
- const d = differenceInCalendarDays(day, now);
3364
+ const d = differenceInCalendarDays2(day, now);
2772
3365
  return d === 0 ? "today" : d === 1 ? "tomorrow" : d === -1 ? "yesterday" : d > 0 ? `in ${d} days` : `${-d} days ago`;
2773
3366
  }
2774
3367
  function fitParts(parts, w) {
@@ -2797,7 +3390,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2797
3390
  const rows = useMemo(() => {
2798
3391
  const out = [];
2799
3392
  for (let i = 0; i < AGENDA_DAYS; i++) {
2800
- const day = addDays6(startOfDay3(date), i);
3393
+ const day = addDays8(startOfDay3(date), i);
2801
3394
  const list = eventsOnDay(events, day);
2802
3395
  if (!list.length) {
2803
3396
  const last = out.at(-1);
@@ -2838,7 +3431,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2838
3431
  const key3 = "free" in r ? `free${ymd(r.free[0])}` : "e" in r ? `${ymd(r.day)}/${eventKey(r.e)}` : "nowLine" in r ? `${ymd(r.day)}/now` : "divider" in r ? `${ymd(r.day)}/d${i}` : ymd(r.day);
2839
3432
  if ("free" in r) {
2840
3433
  const [a, b] = r.free;
2841
- const label = isSameDay4(a, b) ? format7(a, "EEE d MMM") : `${format7(a, "EEE d MMM")} \u2013 ${format7(b, "EEE d MMM")}`;
3434
+ const label = isSameDay4(a, b) ? format9(a, "EEE d MMM") : `${format9(a, "EEE d MMM")} \u2013 ${format9(b, "EEE d MMM")}`;
2842
3435
  lines.push(
2843
3436
  /* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.muted, wrap: "truncate", children: ` ${label} free` }) }, key3)
2844
3437
  );
@@ -2852,7 +3445,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2852
3445
  }
2853
3446
  if ("nowLine" in r) {
2854
3447
  lines.push(
2855
- /* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${format7(now, "HH:mm")} ${"\u2500".repeat(width)}` }) }, key3)
3448
+ /* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${format9(now, "HH:mm")} ${"\u2500".repeat(width)}` }) }, key3)
2856
3449
  );
2857
3450
  return;
2858
3451
  }
@@ -2860,7 +3453,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2860
3453
  const today = isSameDay4(r.day, now);
2861
3454
  const list = eventsOnDay(events, r.day);
2862
3455
  const busy = busyMinutes(list, r.day);
2863
- const left = ` ${format7(r.day, "EEE d MMM")} `;
3456
+ const left = ` ${format9(r.day, "EEE d MMM")} `;
2864
3457
  const rel = relativeDay(r.day, now);
2865
3458
  const right = ` ${list.length} ${list.length === 1 ? "event" : "events"}${busy ? ` \xB7 ${span(busy)} busy` : ""} `;
2866
3459
  const rule = "\u2500".repeat(Math.max(width - left.length - rel.length - right.length - 1, 1));
@@ -2878,7 +3471,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2878
3471
  const { start, end } = eventBounds(e);
2879
3472
  const running = !e.allDay && start <= now && now < end;
2880
3473
  const past = isPast(e, now) || e.myStatus === "declined";
2881
- const clockOf = (d, edge) => isSameDay4(d, r.day) ? format7(d, "HH:mm") : +d === +addDays6(r.day, 1) ? "24:00" : edge;
3474
+ const clockOf = (d, edge) => isSameDay4(d, r.day) ? format9(d, "HH:mm") : +d === +addDays8(r.day, 1) ? "24:00" : edge;
2882
3475
  const badges = [];
2883
3476
  if (running) badges.push({ text: ` \u25CF now \xB7 ends in ${span(Math.max(differenceInMinutes2(end, now, { roundingMethod: "ceil" }), 1))}`, color: C.green, bold: true });
2884
3477
  else if (e === upNext) badges.push({ text: ` ${startsLabel(e.start, now)}`, color: C.yellow });
@@ -2916,16 +3509,16 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2916
3509
 
2917
3510
  // src/ui/views/Day.tsx
2918
3511
  import { useMemo as useMemo3 } from "react";
2919
- import { addDays as addDays7, startOfDay as startOfDay5 } from "date-fns";
3512
+ import { addDays as addDays9, startOfDay as startOfDay5 } from "date-fns";
2920
3513
 
2921
3514
  // src/ui/views/TimeGrid.tsx
2922
3515
  import { useMemo as useMemo2 } from "react";
2923
3516
  import { Box as Box5, Text as Text5 } from "ink";
2924
- import { format as format9, isSameDay as isSameDay6 } from "date-fns";
3517
+ import { format as format11, isSameDay as isSameDay6 } from "date-fns";
2925
3518
 
2926
3519
  // src/ui/views/Month.tsx
2927
3520
  import { Box as Box4, Text as Text4 } from "ink";
2928
- import { format as format8, isSameDay as isSameDay5, isSameMonth, startOfDay as startOfDay4 } from "date-fns";
3521
+ import { format as format10, isSameDay as isSameDay5, isSameMonth, startOfDay as startOfDay4 } from "date-fns";
2929
3522
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2930
3523
  function fit(items, rows, selected) {
2931
3524
  if (items.length <= rows) return { shown: items, more: 0 };
@@ -2936,7 +3529,7 @@ function fit(items, rows, selected) {
2936
3529
  }
2937
3530
  function EventLine({ e, day, now, selected, time, onSelect, onOpen }) {
2938
3531
  const { start, end } = eventBounds(e);
2939
- const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${format8(start, "HH:mm")} `;
3532
+ const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${format10(start, "HH:mm")} `;
2940
3533
  const live = !e.allDay && start <= now && now < end;
2941
3534
  return /* @__PURE__ */ jsx4(Clickable, { height: 1, onClick: clickEvent(e, selected, { onSelect, onOpen }), children: /* @__PURE__ */ jsxs4(
2942
3535
  Text4,
@@ -2960,7 +3553,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
2960
3553
  const cellHeight = Math.max(Math.floor((height - 1) / 6), 1);
2961
3554
  const weeks = [0, 1, 2, 3, 4, 5].map((w) => days.slice(w * 7, w * 7 + 7));
2962
3555
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2963
- /* @__PURE__ */ jsx4(Box4, { children: weeks[0].map((d) => /* @__PURE__ */ jsx4(Box4, { width: colWidth, children: /* @__PURE__ */ jsx4(Text4, { color: C.accent, bold: true, children: format8(d, "EEE") }) }, d.getDay())) }),
3556
+ /* @__PURE__ */ jsx4(Box4, { children: weeks[0].map((d) => /* @__PURE__ */ jsx4(Box4, { width: colWidth, children: /* @__PURE__ */ jsx4(Text4, { color: C.accent, bold: true, children: format10(d, "EEE") }) }, d.getDay())) }),
2964
3557
  weeks.map((week) => /* @__PURE__ */ jsx4(Box4, { height: cellHeight, children: week.map((day) => {
2965
3558
  const items = eventsOnDay(events, day);
2966
3559
  const { shown, more } = fit(items, cellHeight - 1, items.findIndex((e) => eventKey(e) === selectedKey));
@@ -2974,7 +3567,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
2974
3567
  inverse: here,
2975
3568
  color: today ? C.now : here ? C.accent : isSameMonth(day, date) ? void 0 : C.muted,
2976
3569
  children: [
2977
- here ? ` ${format8(day, "d")} ` : format8(day, "d"),
3570
+ here ? ` ${format10(day, "d")} ` : format10(day, "d"),
2978
3571
  cellHeight === 1 && items.length > 0 && /* @__PURE__ */ jsxs4(Text4, { color: C.muted, children: [
2979
3572
  " \xB7",
2980
3573
  items.length
@@ -3064,10 +3657,10 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3064
3657
  const selected = k === selectedKey;
3065
3658
  const first = p.start / 60;
3066
3659
  const { start, end } = eventBounds(e);
3067
- const from = isSameDay6(start, day) ? format9(start, "HH:mm") : "\u2026";
3660
+ const from = isSameDay6(start, day) ? format11(start, "HH:mm") : "\u2026";
3068
3661
  const title2 = `${rsvpMark(e)}${e.title || "(no title)"}`;
3069
3662
  const where = place(e.location);
3070
- const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${format9(start, "mm")} ${title2}`, `${from}\u2013${isSameDay6(end, day) ? format9(end, "HH:mm") : "\u2026"}`, where] : [`${from} ${title2}`, [where, duration(e)].filter(Boolean).join(" \xB7 ")];
3663
+ const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${format11(start, "mm")} ${title2}`, `${from}\u2013${isSameDay6(end, day) ? format11(end, "HH:mm") : "\u2026"}`, where] : [`${from} ${title2}`, [where, duration(e)].filter(Boolean).join(" \xB7 ")];
3071
3664
  const text2 = lines[h - first] ?? "";
3072
3665
  const inner = p.col < p.cols - 1 && w > 1 ? w - 1 : w;
3073
3666
  const past = isPast(e, now) || e.myStatus === "declined";
@@ -3095,7 +3688,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3095
3688
  const isToday = isSameDay6(c.day, now);
3096
3689
  const here = !!cursor && isSameDay6(c.day, cursor) && days.length > 1;
3097
3690
  return /* @__PURE__ */ jsx5(Clickable, { height: 1, width: colW, onClick: onPickDay && (() => onPickDay(c.day)), children: /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
3098
- /* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${format9(c.day, "EEE d")} ` }),
3691
+ /* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${format11(c.day, "EEE d")} ` }),
3099
3692
  c.count > 0 && /* @__PURE__ */ jsxs5(Text5, { color: C.muted, children: [
3100
3693
  "\xB7",
3101
3694
  c.count
@@ -3125,7 +3718,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3125
3718
  /* @__PURE__ */ jsx5(Clickable, { flexDirection: "column", width, onWheel: (dir) => scrollBy(dir), children: Array.from({ length: Math.min(gridH, 24) }, (_, i) => {
3126
3719
  const h = top + i;
3127
3720
  const current = today && h === now.getHours();
3128
- const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${format9(now, "HH:mm")}\u2500` : `${String(h).padStart(2, "0")}:00` }) });
3721
+ const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${format11(now, "HH:mm")}\u2500` : `${String(h).padStart(2, "0")}:00` }) });
3129
3722
  return row(
3130
3723
  `h${h}`,
3131
3724
  gutter,
@@ -3140,7 +3733,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3140
3733
  import { jsx as jsx6 } from "react/jsx-runtime";
3141
3734
  function useDays(date, n) {
3142
3735
  const t = startOfDay5(date).getTime();
3143
- return useMemo3(() => Array.from({ length: n }, (_, i) => addDays7(t, i)), [t, n]);
3736
+ return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t, i)), [t, n]);
3144
3737
  }
3145
3738
  var Day = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 1) });
3146
3739
  var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 2) });
@@ -3157,7 +3750,7 @@ function Week(p) {
3157
3750
  // src/ui/screens/EventEditor.tsx
3158
3751
  import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
3159
3752
  import { Box as Box6, Text as Text6 } from "ink";
3160
- import { format as format10, isValid, parse as parse2 } from "date-fns";
3753
+ import { format as format12, isValid as isValid2, parse as parse2 } from "date-fns";
3161
3754
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
3162
3755
  var LABELS = {
3163
3756
  title: "Title",
@@ -3170,20 +3763,29 @@ var LABELS = {
3170
3763
  endTime: "End time",
3171
3764
  location: "Location",
3172
3765
  description: "Notes",
3173
- attendees: "Invitees"
3766
+ attendees: "Invitees",
3767
+ repeat: "Repeat",
3768
+ repeatEvery: "Every",
3769
+ repeatDays: "On days",
3770
+ repeatEnds: "Stops"
3174
3771
  };
3772
+ var SCOPE_KEYS2 = { "1": "one", "2": "following", "3": "all" };
3175
3773
  var HINTS = {
3176
3774
  startDate: "YYYY-MM-DD",
3177
3775
  startTime: "HH:mm",
3178
3776
  endDate: "YYYY-MM-DD",
3179
3777
  endTime: "HH:mm",
3180
3778
  attendees: "a@x.com, b@y.com",
3181
- title: "New Event"
3779
+ title: "New Event",
3780
+ repeatEvery: "2 weeks",
3781
+ repeatDays: "mo th",
3782
+ repeatEnds: "never \xB7 2026-12-31 \xB7 10 times"
3182
3783
  };
3784
+ var PICKERS = /* @__PURE__ */ new Set(["account", "calendar", "allDay", "repeat"]);
3183
3785
  function toLocal(date, time) {
3184
3786
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time)) return;
3185
3787
  const d = parse2(`${date}T${time}`, "yyyy-MM-dd'T'HH:mm", /* @__PURE__ */ new Date());
3186
- return isValid(d) && format10(d, "yyyy-MM-dd'T'HH:mm") === `${date}T${time}` ? `${date}T${time}` : void 0;
3788
+ return isValid2(d) && format12(d, "yyyy-MM-dd'T'HH:mm") === `${date}T${time}` ? `${date}T${time}` : void 0;
3187
3789
  }
3188
3790
  var timeTexts = (f) => ({
3189
3791
  startDate: f.start.slice(0, 10),
@@ -3191,25 +3793,48 @@ var timeTexts = (f) => ({
3191
3793
  endDate: f.end.slice(0, 10),
3192
3794
  endTime: f.end.slice(11)
3193
3795
  });
3796
+ var repeatTexts = (r, start) => ({
3797
+ repeatEvery: r ? everyText(r) : "",
3798
+ repeatDays: daysText(r?.byDay?.length ? r.byDay : [weekdayOf(start)]),
3799
+ repeatEnds: r ? endsText(r) : "never"
3800
+ });
3194
3801
  var textsFrom = (f) => ({
3195
3802
  title: f.title,
3196
3803
  location: f.location,
3197
3804
  description: f.description,
3198
3805
  attendees: f.attendees.join(", "),
3199
- ...timeTexts(f)
3806
+ ...timeTexts(f),
3807
+ ...repeatTexts(f.repeat, f.start)
3200
3808
  });
3201
- var fieldsFor = (form, editing) => [
3202
- "title",
3203
- ...editing ? [] : ["account", "calendar"],
3204
- "allDay",
3205
- "startDate",
3206
- ...form.allDay ? [] : ["startTime"],
3207
- "endDate",
3208
- ...form.allDay ? [] : ["endTime"],
3209
- "location",
3210
- "description",
3211
- "attendees"
3212
- ];
3809
+ var isCustom = (s) => !!s.form.repeat && !s.form.repeat.rule && (s.custom || presetOf(s.form.repeat, s.form.start) === "custom");
3810
+ var fieldsFor = (s, editing) => {
3811
+ const { form } = s;
3812
+ const custom = isCustom(s);
3813
+ return [
3814
+ "title",
3815
+ ...editing ? [] : ["account", "calendar"],
3816
+ "allDay",
3817
+ "startDate",
3818
+ ...form.allDay ? [] : ["startTime"],
3819
+ "endDate",
3820
+ ...form.allDay ? [] : ["endTime"],
3821
+ "repeat",
3822
+ ...custom ? ["repeatEvery"] : [],
3823
+ ...custom && form.repeat?.freq === "weekly" ? ["repeatDays"] : [],
3824
+ ...form.repeat && !form.repeat.rule ? ["repeatEnds"] : [],
3825
+ "location",
3826
+ "description",
3827
+ "attendees"
3828
+ ];
3829
+ };
3830
+ function readRepeat(s) {
3831
+ const r = s.form.repeat;
3832
+ if (!r || r.rule) return r;
3833
+ const { until: _u, count: _c, ...base } = r;
3834
+ const rule = isCustom(s) ? { ...parseEvery(s.texts.repeatEvery) } : base;
3835
+ if (isCustom(s) && rule.freq === "weekly") rule.byDay = parseDays(s.texts.repeatDays);
3836
+ return { ...rule, ...parseEnds(s.texts.repeatEnds) };
3837
+ }
3213
3838
  function EventEditor({ event, initialStart, onClose }) {
3214
3839
  const api = useApi();
3215
3840
  const { accounts, calendars, loaded } = useDirectory();
@@ -3223,18 +3848,41 @@ function EventEditor({ event, initialStart, onClose }) {
3223
3848
  const [error, setError] = useState3("");
3224
3849
  const [saving, setSaving] = useState3(false);
3225
3850
  const busy = useRef3(false);
3851
+ const [askScope, setAskScopeState] = useState3(false);
3852
+ const asking = useRef3(false);
3853
+ const setAskScope = (on) => {
3854
+ asking.current = on;
3855
+ setAskScopeState(on);
3856
+ };
3857
+ const [repeatFailed, setRepeatFailed] = useState3(false);
3226
3858
  const alive2 = useRef3(true);
3227
3859
  useEffect3(() => () => void (alive2.current = false), []);
3228
3860
  useEffect3(() => {
3229
3861
  if (latest.current || !loaded) return;
3230
3862
  const form2 = event ? formFromEvent(event) : emptyForm(accounts, calendars, {}, initialStart);
3231
- latest.current = { form: form2, texts: textsFrom(form2), focus: 0 };
3863
+ latest.current = { form: form2, texts: textsFrom(form2), focus: 0, custom: false };
3232
3864
  setState(latest.current);
3865
+ if (event?.recurringEventId) {
3866
+ api.events.recurrence(event).then(
3867
+ (rule) => {
3868
+ if (!alive2.current || !latest.current) return;
3869
+ const f = withLoadedRepeat(latest.current.form, rule);
3870
+ update({ form: f, texts: { ...latest.current.texts, ...repeatTexts(rule, f.start) } });
3871
+ },
3872
+ () => alive2.current && setRepeatFailed(true)
3873
+ );
3874
+ }
3233
3875
  }, [loaded]);
3234
3876
  const choices = writableAccounts(accounts, calendars);
3235
- const save = async () => {
3877
+ const save = async (scope) => {
3236
3878
  if (!latest.current || busy.current) return;
3237
3879
  const { form: form2, texts: texts2 } = latest.current;
3880
+ let repeat;
3881
+ try {
3882
+ repeat = readRepeat(latest.current);
3883
+ } catch (e) {
3884
+ return setError(errorText(e));
3885
+ }
3238
3886
  setError("");
3239
3887
  const start = toLocal(texts2.startDate, form2.allDay ? form2.start.slice(11) : texts2.startTime);
3240
3888
  const end = toLocal(texts2.endDate, form2.allDay ? form2.end.slice(11) : texts2.endTime);
@@ -3247,14 +3895,18 @@ function EventEditor({ event, initialStart, onClose }) {
3247
3895
  description: texts2.description,
3248
3896
  attendees: splitEmails(texts2.attendees),
3249
3897
  start,
3250
- end
3898
+ end,
3899
+ repeat
3251
3900
  };
3252
3901
  let request;
3253
3902
  try {
3254
- request = event ? api.events.update(applyForm(event, f)) : api.events.create(formToInput(f));
3903
+ const edited = event && applyForm(event, f);
3904
+ if (edited && event.recurringEventId && !scope) return setAskScope(repeatChanged(f) ? "rule" : "any");
3905
+ request = edited ? api.events.update(edited, scope) : api.events.create(formToInput(f));
3255
3906
  } catch (e) {
3256
3907
  return setError(errorText(e));
3257
3908
  }
3909
+ setAskScope(false);
3258
3910
  busy.current = true;
3259
3911
  setSaving(true);
3260
3912
  try {
@@ -3273,7 +3925,8 @@ function EventEditor({ event, initialStart, onClose }) {
3273
3925
  const i = items.findIndex((x) => x.id === id2);
3274
3926
  return items[i < 0 ? dir > 0 ? 0 : items.length - 1 : (i + dir + items.length) % items.length].id;
3275
3927
  };
3276
- const pick = ({ form: form2, texts: texts2 }, field, dir) => {
3928
+ const pick = (st, field, dir) => {
3929
+ const { form: form2, texts: texts2 } = st;
3277
3930
  if (field === "account") {
3278
3931
  const accountId = cycle2(choices, form2.accountId, dir);
3279
3932
  update({ form: { ...form2, accountId, calendarId: soleId(writableCalendars(calendars, accountId)) } });
@@ -3282,6 +3935,19 @@ function EventEditor({ event, initialStart, onClose }) {
3282
3935
  } else if (field === "allDay") {
3283
3936
  const f = setAllDay(form2, !form2.allDay);
3284
3937
  update({ form: f, texts: { ...texts2, ...timeTexts(f) } });
3938
+ } else if (field === "repeat") {
3939
+ if (form2.repeatWas === void 0) return;
3940
+ const ids = [...presets(form2.start).map((p) => p.id), "custom"];
3941
+ const at3 = ids.indexOf(isCustom(st) ? "custom" : presetOf(form2.repeat, form2.start));
3942
+ const next = ids[(at3 + dir + ids.length) % ids.length];
3943
+ let prev = form2.repeat;
3944
+ try {
3945
+ prev = readRepeat(st);
3946
+ } catch {
3947
+ }
3948
+ const preset = presets(form2.start).find((p) => p.id === next)?.recurrence;
3949
+ const repeat = next === "custom" ? prev && !prev.rule ? prev : withEnd({ freq: "weekly" }, prev) : preset ? withEnd(preset, prev) : null;
3950
+ update({ form: { ...form2, repeat }, custom: next === "custom", texts: { ...texts2, ...repeatTexts(repeat, form2.start) } });
3285
3951
  }
3286
3952
  };
3287
3953
  const edit = ({ form: form2, texts: texts2 }, field, value2) => {
@@ -3301,17 +3967,22 @@ function EventEditor({ event, initialStart, onClose }) {
3301
3967
  const clickField = (f) => {
3302
3968
  const s = latest.current;
3303
3969
  if (!s || busy.current) return;
3304
- const i = fieldsFor(s.form, !!event).indexOf(f);
3970
+ const i = fieldsFor(s, !!event).indexOf(f);
3305
3971
  if (i < 0) return;
3306
- if (i === s.focus && (f === "account" || f === "calendar" || f === "allDay")) return pick(s, f, 1);
3972
+ if (i === s.focus && PICKERS.has(f)) return pick(s, f, 1);
3307
3973
  update({ focus: i });
3308
3974
  };
3309
3975
  useKeys((input, key3) => {
3976
+ if (key3.escape && asking.current && !busy.current) return setAskScope(false);
3310
3977
  if (key3.escape) return onClose();
3311
3978
  const s = latest.current;
3312
3979
  if (!s || busy.current) return;
3980
+ if (asking.current) {
3981
+ if (!Object.hasOwn(SCOPE_KEYS2, input) || asking.current === "rule" && SCOPE_KEYS2[input] === "one") return;
3982
+ return void save(SCOPE_KEYS2[input]);
3983
+ }
3313
3984
  if (key3.ctrl && input === "s") return void save();
3314
- const fields2 = fieldsFor(s.form, !!event);
3985
+ const fields2 = fieldsFor(s, !!event);
3315
3986
  const i = Math.min(s.focus, fields2.length - 1);
3316
3987
  const field = fields2[i];
3317
3988
  const move = (dir) => update({ focus: (i + dir + fields2.length) % fields2.length });
@@ -3319,19 +3990,20 @@ function EventEditor({ event, initialStart, onClose }) {
3319
3990
  if (key3.downArrow) return move(1);
3320
3991
  if (key3.upArrow) return move(-1);
3321
3992
  if (key3.return) return i === fields2.length - 1 ? void save() : move(1);
3322
- if (field === "account" || field === "calendar" || field === "allDay") {
3993
+ if (PICKERS.has(field)) {
3323
3994
  if (key3.leftArrow) pick(s, field, -1);
3324
3995
  else if (key3.rightArrow || input === " ") pick(s, field, 1);
3325
3996
  return;
3326
3997
  }
3327
- if (key3.backspace || key3.delete) return edit(s, field, s.texts[field].slice(0, -1));
3998
+ const text2 = field;
3999
+ if (key3.backspace || key3.delete) return edit(s, text2, s.texts[text2].slice(0, -1));
3328
4000
  if (key3.ctrl || key3.meta || !input) return;
3329
4001
  const printable = input.replace(/[\u0000-\u001f\u007f]/g, "");
3330
- if (printable) edit(s, field, s.texts[field] + printable);
4002
+ if (printable) edit(s, text2, s.texts[text2] + printable);
3331
4003
  });
3332
4004
  if (!state) return /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "Loading\u2026" });
3333
4005
  const { form, texts } = state;
3334
- const fields = fieldsFor(form, !!event);
4006
+ const fields = fieldsFor(state, !!event);
3335
4007
  const current = fields[Math.min(state.focus, fields.length - 1)];
3336
4008
  const account = accounts.find((a) => a.id === form.accountId);
3337
4009
  const calendar = calendars.find((c) => c.accountId === form.accountId && c.id === form.calendarId);
@@ -3346,6 +4018,11 @@ function EventEditor({ event, initialStart, onClose }) {
3346
4018
  return calendar ? { text: calendar.name } : { text: "\u2039 choose calendar \u203A", dim: true };
3347
4019
  case "allDay":
3348
4020
  return { text: form.allDay ? "[x]" : "[ ]" };
4021
+ case "repeat":
4022
+ if (form.repeatWas === void 0) return { text: repeatFailed ? "couldn\u2019t load; stays as it is" : "loading\u2026", dim: true };
4023
+ if (form.repeat?.rule) return { text: describe(form.repeat, form.start) };
4024
+ if (isCustom(state)) return { text: "Custom" };
4025
+ return { text: presets(form.start).find((p) => p.id === presetOf(form.repeat, form.start)).label };
3349
4026
  default:
3350
4027
  return texts[f] ? { text: texts[f] } : { text: HINTS[f] ?? "", dim: true };
3351
4028
  }
@@ -3356,7 +4033,7 @@ function EventEditor({ event, initialStart, onClose }) {
3356
4033
  rows.map((f) => {
3357
4034
  const on = f === current;
3358
4035
  const v = value(f);
3359
- const picker = !event && (f === "account" || f === "calendar");
4036
+ const picker = !event && (f === "account" || f === "calendar") || f === "repeat";
3360
4037
  return /* @__PURE__ */ jsxs6(Clickable, { onClick: () => clickField(f), children: [
3361
4038
  /* @__PURE__ */ jsxs6(Text6, { color: on ? C.cyan : void 0, children: [
3362
4039
  on ? "\u203A " : " ",
@@ -3368,7 +4045,13 @@ function EventEditor({ event, initialStart, onClose }) {
3368
4045
  }),
3369
4046
  /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: account ? `Organizer: ${account.email}` : "Choose which account this event belongs to." }) }),
3370
4047
  error && /* @__PURE__ */ jsx8(Text6, { color: C.red, children: error }),
3371
- saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
4048
+ saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : askScope ? /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
4049
+ /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: askScope === "rule" ? "Change the repeat rule for: " : "Save recurring event: " }),
4050
+ askScope !== "rule" && /* @__PURE__ */ jsx8(Button, { k: "1", label: "this event", color: C.green, onPress: () => void save("one") }),
4051
+ /* @__PURE__ */ jsx8(Button, { k: "2", label: "this and following", color: C.green, onPress: () => void save("following") }),
4052
+ /* @__PURE__ */ jsx8(Button, { k: "3", label: "all events", color: C.green, onPress: () => void save("all") }),
4053
+ /* @__PURE__ */ jsx8(Button, { k: "esc", label: "back", onPress: () => setAskScope(false) })
4054
+ ] }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
3372
4055
  /* @__PURE__ */ jsx8(Box6, { marginRight: 2, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "tab/\u2191\u2193 move \xB7 \u2190/\u2192/space pick" }) }),
3373
4056
  /* @__PURE__ */ jsx8(Button, { k: "ctrl+s", label: "save", color: C.green, onPress: () => void save() }),
3374
4057
  /* @__PURE__ */ jsx8(Button, { k: "esc", label: "cancel", onPress: onClose })
@@ -3391,12 +4074,12 @@ import { Box as Box8, Text as Text8 } from "ink";
3391
4074
  // src/ui/screens/Invites.tsx
3392
4075
  import { useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
3393
4076
  import { Box as Box7, Text as Text7 } from "ink";
3394
- import { addDays as addDays8, startOfDay as startOfDay6 } from "date-fns";
4077
+ import { addDays as addDays10, startOfDay as startOfDay6 } from "date-fns";
3395
4078
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3396
4079
  var INVITE_DAYS = 60;
3397
4080
  function useUpcoming(now) {
3398
4081
  const day = startOfDay6(now).getTime();
3399
- const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays8(day, INVITE_DAYS).toISOString() }), [day]);
4082
+ const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays10(day, INVITE_DAYS).toISOString() }), [day]);
3400
4083
  return useEvents(range);
3401
4084
  }
3402
4085
  var REPLY = { y: "accepted", n: "declined", m: "tentative" };
@@ -3574,12 +4257,12 @@ var NAMES = {
3574
4257
  };
3575
4258
  function useKeyState(init) {
3576
4259
  const ref = useRef5(init);
3577
- const [, bump] = useState6(0);
4260
+ const [, bump2] = useState6(0);
3578
4261
  return [
3579
4262
  () => ref.current,
3580
4263
  (next) => {
3581
4264
  ref.current = next;
3582
- bump((n) => n + 1);
4265
+ bump2((n) => n + 1);
3583
4266
  }
3584
4267
  ];
3585
4268
  }
@@ -3996,9 +4679,9 @@ function FirstSyncCheck({ width, height, children }) {
3996
4679
  ] }) });
3997
4680
  }
3998
4681
  function title({ view, date }) {
3999
- if (view === "month") return format11(date, "MMMM yyyy");
4000
- if (view === "day") return format11(date, "EEE d MMM yyyy");
4001
- const days = view === "agenda" ? [date, addDays9(date, AGENDA_DAYS - 1)] : view === "2day" ? [date, addDays9(date, 1)] : viewDays(view, date);
4682
+ if (view === "month") return format13(date, "MMMM yyyy");
4683
+ if (view === "day") return format13(date, "EEE d MMM yyyy");
4684
+ const days = view === "agenda" ? [date, addDays11(date, AGENDA_DAYS - 1)] : view === "2day" ? [date, addDays11(date, 1)] : viewDays(view, date);
4002
4685
  return rangeLabel(days[0], days[days.length - 1]);
4003
4686
  }
4004
4687
  var chronological = (a, b) => eventBounds(a).start.getTime() - eventBounds(b).start.getTime() || Number(b.allDay) - Number(a.allDay);
@@ -4076,8 +4759,8 @@ function Shell({ initialNav, updateCheck, notice }) {
4076
4759
  const { cursor: from, view, date, key: key3 } = live.current;
4077
4760
  const sel = ordered2.find((e) => eventKey(e) === key3);
4078
4761
  const minute = sel ? sel.allDay ? void 0 : Math.max(differenceInMinutes3(eventBounds(sel).start, from), 0) : live.current.minute;
4079
- const day = addDays9(from, n);
4080
- const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ? addDays9(day, -1) : day;
4762
+ const day = addDays11(from, n);
4763
+ const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ? addDays11(day, -1) : day;
4081
4764
  Object.assign(live.current, { cursor: day, date: nextDate, minute });
4082
4765
  setCursor(day);
4083
4766
  if (+nextDate !== +date) setDate(nextDate);