@rapidmx/web-client 0.28.0 → 0.29.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 (119) hide show
  1. package/README.md +3 -0
  2. package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +22 -10
  3. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +60 -0
  4. package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +108 -0
  5. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +25 -1
  6. package/apps/shared/components/calendar/CalendarSearchBox.tsx +83 -0
  7. package/apps/shared/components/calendar/EventMatchNav.tsx +72 -0
  8. package/apps/shared/components/calendar/EventModal.tsx +33 -12
  9. package/apps/shared/components/calendar/ListView.tsx +207 -0
  10. package/apps/shared/components/calendar/MonthView.tsx +14 -2
  11. package/apps/shared/components/calendar/SlideIn.tsx +47 -0
  12. package/apps/shared/components/calendar/SplitDayView.tsx +5 -0
  13. package/apps/shared/components/calendar/TimeGridView.tsx +23 -12
  14. package/apps/shared/components/calendar/activeOccurrence.ts +28 -0
  15. package/apps/shared/components/calendar/allDay.ts +16 -1
  16. package/apps/shared/components/calendar/calendarView.ts +41 -0
  17. package/apps/shared/components/layout/AppShell.tsx +13 -6
  18. package/apps/shared/components/mail/ConversationList.tsx +29 -3
  19. package/apps/shared/components/mail/ConversationThreadPane.tsx +15 -0
  20. package/apps/shared/components/mail/MailSelectionBar.tsx +117 -55
  21. package/apps/shared/components/mail/MessageDetailPane.tsx +3 -0
  22. package/apps/shared/components/mail/compose/ComposeContext.tsx +193 -35
  23. package/apps/shared/components/mail/compose/ComposeWindow.tsx +62 -11
  24. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +21 -10
  25. package/apps/shared/components/mail/compose/RichTextEditor.tsx +2 -1
  26. package/apps/shared/components/mail/compose/quotedBody.ts +45 -2
  27. package/apps/shared/components/mail/labelMenu.tsx +4 -0
  28. package/apps/shared/components/mail/layout/MailShell.tsx +9 -2
  29. package/apps/shared/components/mail/reading/MessageBody.tsx +1 -1
  30. package/apps/shared/components/mail/reading/bodyContent.ts +15 -13
  31. package/apps/shared/components/mail/reading/bodyHtml.ts +12 -7
  32. package/apps/shared/gestures/useEnterSlide.ts +1 -1
  33. package/apps/shared/keyboard/keymap.ts +2 -0
  34. package/apps/shared/mail/rangeSelection.ts +45 -0
  35. package/apps/shared/mail/rowExit.ts +39 -0
  36. package/apps/www/calendar/index.tsx +251 -53
  37. package/apps/www/index.tsx +522 -343
  38. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +3 -3
  39. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +15 -9
  40. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +15 -0
  41. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +16 -0
  42. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +20 -0
  43. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +31 -0
  44. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +20 -1
  45. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +7 -0
  46. package/dist/apps/shared/components/calendar/CalendarSearchBox.d.ts +22 -0
  47. package/dist/apps/shared/components/calendar/CalendarSearchBox.js +24 -0
  48. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +17 -0
  49. package/dist/apps/shared/components/calendar/EventMatchNav.js +37 -0
  50. package/dist/apps/shared/components/calendar/EventModal.d.ts +17 -1
  51. package/dist/apps/shared/components/calendar/EventModal.js +13 -11
  52. package/dist/apps/shared/components/calendar/ListView.d.ts +30 -0
  53. package/dist/apps/shared/components/calendar/ListView.js +111 -0
  54. package/dist/apps/shared/components/calendar/MonthView.js +12 -3
  55. package/dist/apps/shared/components/calendar/SlideIn.d.ts +16 -0
  56. package/dist/apps/shared/components/calendar/SlideIn.js +32 -0
  57. package/dist/apps/shared/components/calendar/SplitDayView.js +5 -1
  58. package/dist/apps/shared/components/calendar/TimeGridView.js +11 -3
  59. package/dist/apps/shared/components/calendar/activeOccurrence.d.ts +15 -0
  60. package/dist/apps/shared/components/calendar/activeOccurrence.js +21 -0
  61. package/dist/apps/shared/components/calendar/allDay.d.ts +4 -0
  62. package/dist/apps/shared/components/calendar/allDay.js +14 -1
  63. package/dist/apps/shared/components/calendar/calendarView.d.ts +16 -0
  64. package/dist/apps/shared/components/calendar/calendarView.js +36 -0
  65. package/dist/apps/shared/components/layout/AppShell.js +8 -4
  66. package/dist/apps/shared/components/mail/ConversationList.d.ts +9 -1
  67. package/dist/apps/shared/components/mail/ConversationList.js +10 -2
  68. package/dist/apps/shared/components/mail/ConversationThreadPane.js +9 -1
  69. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  70. package/dist/apps/shared/components/mail/MessageDetailPane.js +3 -0
  71. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +31 -0
  72. package/dist/apps/shared/components/mail/compose/ComposeContext.js +123 -11
  73. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +6 -1
  74. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +37 -11
  75. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +5 -1
  76. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +10 -6
  77. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +2 -1
  78. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +3 -0
  79. package/dist/apps/shared/components/mail/compose/quotedBody.js +43 -1
  80. package/dist/apps/shared/components/mail/labelMenu.d.ts +3 -1
  81. package/dist/apps/shared/components/mail/labelMenu.js +2 -2
  82. package/dist/apps/shared/components/mail/layout/MailShell.js +7 -1
  83. package/dist/apps/shared/components/mail/reading/MessageBody.js +1 -1
  84. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +5 -1
  85. package/dist/apps/shared/components/mail/reading/bodyContent.js +11 -11
  86. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +3 -2
  87. package/dist/apps/shared/components/mail/reading/bodyHtml.js +8 -4
  88. package/dist/apps/shared/gestures/useEnterSlide.d.ts +2 -0
  89. package/dist/apps/shared/gestures/useEnterSlide.js +1 -1
  90. package/dist/apps/shared/keyboard/keymap.d.ts +12 -0
  91. package/dist/apps/shared/keyboard/keymap.js +2 -0
  92. package/dist/apps/shared/mail/rangeSelection.d.ts +20 -0
  93. package/dist/apps/shared/mail/rangeSelection.js +34 -0
  94. package/dist/apps/shared/mail/rowExit.d.ts +12 -0
  95. package/dist/apps/shared/mail/rowExit.js +33 -0
  96. package/dist/apps/www/calendar/index.js +177 -18
  97. package/dist/apps/www/index.js +220 -65
  98. package/dist/lib/calendar/eventSearch.d.ts +42 -0
  99. package/dist/lib/calendar/eventSearch.js +152 -0
  100. package/dist/lib/components/navigation/BottomTabBar.d.ts +15 -3
  101. package/dist/lib/components/navigation/BottomTabBar.js +52 -15
  102. package/dist/lib/components/navigation/NavBadge.d.ts +13 -0
  103. package/dist/lib/components/navigation/NavBadge.js +18 -0
  104. package/dist/lib/mail/compose/quotedImages.d.ts +21 -0
  105. package/dist/lib/mail/compose/quotedImages.js +41 -0
  106. package/dist/lib/mail/inlineImages.d.ts +38 -0
  107. package/dist/lib/mail/inlineImages.js +90 -0
  108. package/dist/lib/mail/mailApi.d.ts +8 -1
  109. package/dist/lib/mail/mailApi.js +57 -5
  110. package/lib/calendar/eventSearch.ts +172 -0
  111. package/lib/components/navigation/BottomTabBar.tsx +107 -22
  112. package/lib/components/navigation/NavBadge.tsx +33 -0
  113. package/lib/mail/compose/quotedImages.ts +49 -0
  114. package/lib/mail/inlineImages.ts +104 -0
  115. package/lib/mail/mailApi.ts +64 -5
  116. package/package.json +1 -1
  117. package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +0 -36
  118. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +0 -10
  119. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +0 -11
@@ -17,19 +17,25 @@ import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calen
17
17
  import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
18
18
  import { createFolder } from "../../../lib/mail/mailApi.js";
19
19
  import { expandAllOccurrences } from "../../../lib/calendar/recurrence.js";
20
+ import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
20
21
  import Drawer from "../../../lib/components/overlays/Drawer.js";
21
22
  import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
22
23
  import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
24
+ import CalendarSearchBox from "../../shared/components/calendar/CalendarSearchBox.js";
25
+ import { ActiveOccurrenceContext } from "../../shared/components/calendar/activeOccurrence.js";
26
+ import { isUpcoming, occurrenceDay } from "../../shared/components/calendar/allDay.js";
23
27
  import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
24
28
  import EventModal from "../../shared/components/calendar/EventModal.js";
25
29
  import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
26
30
  import { anchorOf } from "../../shared/components/calendar/EventShell.js";
27
31
  import MiniDatePicker from "../../../lib/components/pickers/MiniDatePicker.js";
32
+ import ListView from "../../shared/components/calendar/ListView.js";
28
33
  import MonthView from "../../shared/components/calendar/MonthView.js";
29
34
  import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
30
35
  import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
31
36
  import Alert from "../../../lib/components/feedback/Alert.js";
32
37
  import Button from "../../../lib/components/buttons/Button.js";
38
+ import { CALENDAR_VIEWS, getStoredCalendarView, isCalendarView, storeCalendarView } from "../../shared/components/calendar/calendarView.js";
33
39
  import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
34
40
  import { nextHalfHour } from "../../shared/components/calendar/timePicker.js";
35
41
  import { useSwipeSlide } from "../../shared/gestures/useSwipeSlide.js";
@@ -41,13 +47,15 @@ import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
41
47
  import { notifyApiError } from "../../shared/notifications/apiErrors.js";
42
48
  import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
43
49
  import { redactedEventUidOf } from "../../shared/calendar/calendarLiveUpdates.js";
50
+ /** The furthest instants a `Date` can hold: the List's "range" reaches both. */
51
+ const MAX_DATE_MS = 8.64e15;
44
52
  function CalendarPage(props) {
45
53
  return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid, bookingHref: bookingSettingsHref(props.pluginNav) }) }));
46
54
  }
47
- const VIEW_TYPES = ["month", "week", "workWeek", "day", "split"];
48
- const VIEW_LABELS = { month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
49
- /** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4); the split view has none. */
55
+ const VIEW_LABELS = { list: "List", month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
56
+ /** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
50
57
  const VIEW_SHORTCUTS = {
58
+ list: SHORTCUTS.calendar.list,
51
59
  day: SHORTCUTS.calendar.day,
52
60
  workWeek: SHORTCUTS.calendar.workWeek,
53
61
  week: SHORTCUTS.calendar.week,
@@ -75,7 +83,20 @@ function CalendarContent({ userUid, bookingHref }) {
75
83
  // dnd-kit's own documented pattern for this exact conflict.
76
84
  const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }));
77
85
  const [drawerOpen, setDrawerOpen] = useState(false);
78
- const [view, setView] = useState("month");
86
+ // Month, until the effects below know better: a remembered view, or the List on a phone (never read while rendering, so the server's markup and the
87
+ // first client render agree). A desktop always opens on the Month unless the link or a remembered Week, Work Week, Day, Split or Month says otherwise.
88
+ const [view, setViewState] = useState("month");
89
+ // Whether the view was decided by the URL, a remembered choice or the reader (and so is not the device's default to follow).
90
+ const viewChosenRef = useRef(false);
91
+ /** Switches the view; a reader's own choice (`remember`) is also what the Calendar opens on next time - except the List on a desktop, which
92
+ * only a phone opens on, so it is never written there (and so can never turn up as the next desktop visit's view). */
93
+ function setView(next, remember = true) {
94
+ viewChosenRef.current = true;
95
+ setViewState(next);
96
+ if (remember && (next !== "list" || isMobile)) {
97
+ storeCalendarView(next);
98
+ }
99
+ }
79
100
  const [viewDate, setViewDate] = useState(() => startOfDay(new Date()));
80
101
  const [events, setEvents] = useState([]);
81
102
  const [loading, setLoading] = useState(true);
@@ -90,8 +111,14 @@ function CalendarContent({ userUid, bookingHref }) {
90
111
  useEffect(() => {
91
112
  const params = new URLSearchParams(window.location.search);
92
113
  const requestedView = params.get("view");
93
- if (VIEW_TYPES.includes(requestedView ?? "")) {
94
- setView(requestedView);
114
+ const storedView = getStoredCalendarView();
115
+ if (isCalendarView(requestedView)) {
116
+ // Asked for by the link: shown, but not remembered - it is not a choice of the reader's.
117
+ setView(requestedView, false);
118
+ }
119
+ else if (storedView && storedView !== "list") {
120
+ // A remembered List is not restored: it is the phone's default (below) and never the desktop's, whatever an older visit left in storage.
121
+ setView(storedView, false);
95
122
  }
96
123
  const requestedDate = params.get("date");
97
124
  // `parseISO()`, not `new Date()` - `?date=2026-06-16` is a local calendar day, but `new Date()` reads a
@@ -101,6 +128,13 @@ function CalendarContent({ userUid, bookingHref }) {
101
128
  setViewDate(startOfDay(parsedDate));
102
129
  }
103
130
  }, []);
131
+ // With nothing remembered the device decides: the List on a phone, the Month elsewhere (`useIsMobile()` only knows after mounting).
132
+ const isMobile = useIsMobile();
133
+ useEffect(() => {
134
+ if (!viewChosenRef.current) {
135
+ setViewState(isMobile ? "list" : "month");
136
+ }
137
+ }, [isMobile]);
104
138
  const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
105
139
  const folderColors = useMemo(() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])), [calendarFolders, colorFor]);
106
140
  function toggleCalendar(toggledUid) {
@@ -121,6 +155,10 @@ function CalendarContent({ userUid, bookingHref }) {
121
155
  reloadFolders();
122
156
  }
123
157
  const { rangeStart, rangeEnd, days } = useMemo(() => {
158
+ if (view === "list") {
159
+ // The list is not a range at all: every event, whenever it falls (a series bounded as the search bounds it - see `allOccurrences()`).
160
+ return { rangeStart: new Date(-MAX_DATE_MS), rangeEnd: new Date(MAX_DATE_MS), days: [] };
161
+ }
124
162
  if (view === "month") {
125
163
  const start = startOfWeek(startOfMonth(viewDate), { weekStartsOn: 1 });
126
164
  const end = endOfWeek(endOfMonth(viewDate), { weekStartsOn: 1 });
@@ -221,11 +259,103 @@ function CalendarContent({ userUid, bookingHref }) {
221
259
  }
222
260
  });
223
261
  }), [client]);
224
- const occurrences = useMemo(() => expandAllOccurrences(events, rangeStart, rangeEnd), [events, rangeStart, rangeEnd]);
262
+ // Search: `searchText` is the box, `query` what it held once typing paused (or Enter was pressed) and is what is searched for. A search covers every
263
+ // event of the checked calendars whatever the view shows (see `searchOccurrences()`); while one runs the views show only its matches, and
264
+ // `activeKey` is the match being pointed at (Previous/Next match step through `matches`).
265
+ const [searchText, setSearchText] = useState("");
266
+ const [query, setQuery] = useState("");
267
+ const [activeKey, setActiveKey] = useState(null);
268
+ const searchInputRef = useRef(null);
269
+ const searching = searchTerms(query).length > 0;
270
+ useEffect(() => {
271
+ const timer = setTimeout(() => setQuery(searchText), 300);
272
+ return () => clearTimeout(timer);
273
+ }, [searchText]);
274
+ // The List shows only what is from today on, so that is all a search steps through there; the other views reach the past ones.
275
+ const matches = useMemo(() => {
276
+ const found = searching ? searchOccurrences(events, query) : [];
277
+ const today = new Date();
278
+ return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
279
+ }, [events, query, searching, view]);
280
+ const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
281
+ // What an open event card steps through, in order: the matches while a search runs, else every event of the checked calendars (bounded as a
282
+ // search is). The List holds only what is from today on, so that is all its card steps through. Worked out only when something needs it.
283
+ const everything = useMemo(() => (view === "list" || modal?.occurrence ? allOccurrences(events) : []), [events, view, !!modal?.occurrence]);
284
+ const sequence = useMemo(() => {
285
+ if (searching) {
286
+ return matches;
287
+ }
288
+ if (view === "list") {
289
+ const today = new Date();
290
+ return everything.filter((occurrence) => isUpcoming(occurrence, today));
291
+ }
292
+ return everything;
293
+ }, [searching, matches, view, everything]);
294
+ const occurrences = useMemo(() => {
295
+ if (view === "list") {
296
+ return sequence;
297
+ }
298
+ return searching ? occurrencesInRange(matches, rangeStart, rangeEnd) : expandAllOccurrences(events, rangeStart, rangeEnd);
299
+ }, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
300
+ /** Points at `match`, moving the view to the day it is on unless the view already shows it. */
301
+ function showMatch(match) {
302
+ setActiveKey(match.occurrenceKey);
303
+ // An event's card that is open moves on to it, so the card and the view stay together.
304
+ setModal((current) => (current?.occurrence ? { occurrence: match } : current));
305
+ if (occurrencesInRange([match], rangeStart, rangeEnd).length === 0) {
306
+ setViewDate(occurrenceDay(match));
307
+ }
308
+ }
309
+ // The step that moved an open event card on, for the card to slide in from the side of the button pressed (also on a wrap-around).
310
+ const [slide, setSlide] = useState(undefined);
311
+ // The card open is where the sequence stands; with none, the match the search is on. A search wraps round at either end, the events of the
312
+ // calendar do not.
313
+ const stepFrom = modal?.occurrence;
314
+ const stepIndex = (direction) => stepSequenceIndex(sequence, stepFrom?.occurrenceKey ?? activeKey, stepFrom ? new Date(stepFrom.startDate) : undefined, direction, searching);
315
+ function stepMatch(direction) {
316
+ const next = sequence[stepIndex(direction)];
317
+ if (next) {
318
+ setSlide({ from: direction === 1 ? "right" : "left", occurrenceKey: next.occurrenceKey });
319
+ showMatch(next);
320
+ }
321
+ }
322
+ function clearSearch() {
323
+ setSearchText("");
324
+ setQuery("");
325
+ }
326
+ function submitSearch(backwards) {
327
+ if (searchText !== query) {
328
+ setQuery(searchText);
329
+ }
330
+ else {
331
+ stepMatch(backwards ? -1 : 1);
332
+ }
333
+ }
334
+ // A new search goes to its first match: the first one in the view if there is one, else the first from the day being viewed on (the nearest
335
+ // before it when there is none after). Matches that change under the same query (a saved edit, a live update) leave the view where it is.
336
+ const searchedQueryRef = useRef("");
337
+ useEffect(() => {
338
+ if (!searching || matches.length === 0) {
339
+ searchedQueryRef.current = "";
340
+ setActiveKey(null);
341
+ return;
342
+ }
343
+ if (searchedQueryRef.current !== query) {
344
+ searchedQueryRef.current = query;
345
+ // The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
346
+ const from = startOfDay(viewDate);
347
+ showMatch(matches[view === "list" ? initialMatchIndex(matches, from, rangeEnd, from) : initialMatchIndex(matches, rangeStart, rangeEnd, from)]);
348
+ }
349
+ }, [matches, query, searching]);
225
350
  const splitColumns = useMemo(() => calendarFolders
226
351
  .filter((f) => checkedFolderUids.has(f.uid))
227
352
  .map((f) => ({ folderUid: f.uid, name: f.name, color: colorFor(f) })), [calendarFolders, checkedFolderUidsKey, colorFor]);
228
353
  function shiftView(direction) {
354
+ if (view === "list") {
355
+ // Previous and Next move the list a month, from wherever it has been scrolled to (the list does the scrolling).
356
+ setListStep((step) => ({ direction, nonce: (step?.nonce ?? 0) + 1 }));
357
+ return;
358
+ }
229
359
  setViewDate((d) => view === "month"
230
360
  ? addMonths(d, direction)
231
361
  : view === "week" || view === "workWeek"
@@ -236,9 +366,8 @@ function CalendarContent({ userUid, bookingHref }) {
236
366
  // the Previous/Next buttons and shortcuts do, and it is off while an event is being dragged or the editor is open. The views
237
367
  // never scroll sideways (their columns share the width), so there is no horizontal scroller for the gesture to fight with.
238
368
  // The view follows the finger and, once the swipe commits, slides out while the next period slides in (see `useSwipeSlide()`).
239
- const isMobile = useIsMobile();
240
369
  const swipe = useSwipeSlide({
241
- enabled: isMobile && !dragging && !modal,
370
+ enabled: isMobile && !dragging && !modal && view !== "list",
242
371
  onShift: (direction) => shiftView(SWIPE_PERIOD_SHIFT[direction]),
243
372
  });
244
373
  // The desktop's mouse wheel steps through the months without end; in the views that scroll through the hours of a day it scrolls
@@ -249,7 +378,7 @@ function CalendarContent({ userUid, bookingHref }) {
249
378
  const arrivalEdge = useRef(null);
250
379
  const scrollerOf = () => swipe.ref.current?.querySelector("[data-calendar-scroller]") ?? null;
251
380
  useWheelPaging(swipe.ref, {
252
- enabled: !isMobile && !dragging && !modal,
381
+ enabled: !isMobile && !dragging && !modal && view !== "list",
253
382
  getScroller: () => (view === "month" ? null : scrollerOf()),
254
383
  onStep: (direction, edge) => {
255
384
  arrivalEdge.current = edge;
@@ -265,11 +394,20 @@ function CalendarContent({ userUid, bookingHref }) {
265
394
  scroller.scrollTop = edge === "top" ? 0 : scroller.scrollHeight;
266
395
  }
267
396
  }, [viewDate]);
397
+ // Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
398
+ useEffect(() => {
399
+ const marked = Array.from(swipe.ref.current.querySelectorAll("[data-occurrence-key]"));
400
+ marked.find((el) => el.dataset.occurrenceKey === activeKey)?.scrollIntoView({ block: "nearest", inline: "nearest" });
401
+ }, [activeKey, view === "list" ? null : viewDate, view, loading]);
402
+ // The List scrolls to `viewDate` when this changes (Today, a day picked in the mini calendar), even when it is the date it already has.
403
+ const [jumpNonce, setJumpNonce] = useState(0);
404
+ const [listStep, setListStep] = useState(null);
268
405
  function goToday() {
269
406
  setViewDate(startOfDay(new Date()));
407
+ setJumpNonce((n) => n + 1);
270
408
  }
271
409
  function handleSelectDay(date) {
272
- setView("day");
410
+ setView("day", false);
273
411
  setViewDate(startOfDay(date));
274
412
  }
275
413
  function openNewEvent(start, end, targetFolderUid, anchor, initialAllDay) {
@@ -288,6 +426,8 @@ function CalendarContent({ userUid, bookingHref }) {
288
426
  });
289
427
  }
290
428
  function openEvent(occurrence) {
429
+ setSlide(undefined);
430
+ setActiveKey(occurrence.occurrenceKey);
291
431
  setModal({ occurrence });
292
432
  }
293
433
  function closeModal() {
@@ -298,10 +438,15 @@ function CalendarContent({ userUid, bookingHref }) {
298
438
  useShortcut(SHORTCUTS.calendar.today, goToday);
299
439
  useShortcut(SHORTCUTS.calendar.previous, () => shiftView(-1));
300
440
  useShortcut(SHORTCUTS.calendar.next, () => shiftView(1));
441
+ useShortcut(SHORTCUTS.calendar.list, () => setView("list"));
301
442
  useShortcut(SHORTCUTS.calendar.day, () => setView("day"));
302
443
  useShortcut(SHORTCUTS.calendar.workWeek, () => setView("workWeek"));
303
444
  useShortcut(SHORTCUTS.calendar.week, () => setView("week"));
304
445
  useShortcut(SHORTCUTS.calendar.month, () => setView("month"));
446
+ useShortcut(SHORTCUTS.calendar.search, () => {
447
+ searchInputRef.current?.focus();
448
+ searchInputRef.current?.select();
449
+ });
305
450
  const newEventHint = useShortcutProps("New event", SHORTCUTS.calendar.create, !!mailboxUid && !!folderUid);
306
451
  const previousHint = useShortcutProps("Previous", SHORTCUTS.calendar.previous);
307
452
  const todayHint = useShortcutProps("Today", SHORTCUTS.calendar.today);
@@ -334,11 +479,13 @@ function CalendarContent({ userUid, bookingHref }) {
334
479
  notifyApiError(err, action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event");
335
480
  }
336
481
  }
337
- const title = view === "month"
338
- ? format(viewDate, "MMMM yyyy")
339
- : view === "week" || view === "workWeek"
340
- ? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
341
- : format(viewDate, "EEEE, MMMM d, yyyy");
482
+ const title = view === "list"
483
+ ? "Upcoming events"
484
+ : view === "month"
485
+ ? format(viewDate, "MMMM yyyy")
486
+ : view === "week" || view === "workWeek"
487
+ ? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
488
+ : format(viewDate, "EEEE, MMMM d, yyyy");
342
489
  const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
343
490
  const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
344
491
  const modalMailbox = mailboxes.find((mb) => mb.uid === modalMailboxUid);
@@ -348,8 +495,20 @@ function CalendarContent({ userUid, bookingHref }) {
348
495
  const mailboxOptions = useMemo(() => mailboxCalendars
349
496
  .filter((mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid))
350
497
  .map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })), [mailboxCalendars, writableMailboxes]);
351
- const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => setViewDate(startOfDay(date)) }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
352
- return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx("div", { className: "ml-auto flex flex-wrap gap-1", children: VIEW_TYPES.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, initialAllDay: modal.initialAllDay, anchor: modal.anchor, bookingHref: bookingHref, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && _jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() })] })] }));
498
+ const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => {
499
+ setViewDate(startOfDay(date));
500
+ setJumpNonce((n) => n + 1);
501
+ } }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
502
+ return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx(CalendarSearchBox, { value: searchText, onChange: setSearchText, onSubmit: submitSearch, onClear: clearSearch, onPrevious: () => stepMatch(-1), onNext: () => stepMatch(1), active: searching, count: matches.length, position: activeIndex, inputRef: searchInputRef }), _jsx("div", { className: "flex flex-wrap gap-1", children: CALENDAR_VIEWS.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(ActiveOccurrenceContext.Provider, { value: searching ? activeKey : null, children: _jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "list" ? (_jsx(ListView, { occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, focusDate: viewDate, jumpNonce: jumpNonce, step: listStep })) : view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) }) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, initialAllDay: modal.initialAllDay, anchor: modal.anchor, bookingHref: bookingHref, matchNav: modal.occurrence && sequence.length > 0
503
+ ? {
504
+ onPrevious: () => stepMatch(-1),
505
+ onNext: () => stepMatch(1),
506
+ canPrevious: stepIndex(-1) >= 0,
507
+ canNext: stepIndex(1) >= 0,
508
+ noun: searching ? "match" : "event",
509
+ slide,
510
+ }
511
+ : undefined, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && _jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() })] })] }));
353
512
  }
354
513
  export default CalendarPage;
355
514
  /** The tab's title: `Brand: Calendar` (see `pageTitle()`). */