@rapidmx/web-client 0.14.1 → 0.15.1

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 (171) hide show
  1. package/apps/admin/domains/[uid].tsx +3 -2
  2. package/apps/shared/appearance/AppearanceForm.tsx +9 -1
  3. package/apps/shared/calendar/bookingPlugin.ts +100 -0
  4. package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
  5. package/apps/shared/components/admin/layout/AdminShell.tsx +37 -5
  6. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +3 -1
  7. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1491
  8. package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
  9. package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
  10. package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
  11. package/apps/shared/components/calendar/EventDetails.tsx +401 -0
  12. package/apps/shared/components/calendar/EventEditor.tsx +640 -0
  13. package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
  14. package/apps/shared/components/calendar/EventFormParts.tsx +622 -0
  15. package/apps/shared/components/calendar/EventModal.tsx +126 -866
  16. package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
  17. package/apps/shared/components/calendar/EventShell.tsx +202 -0
  18. package/apps/shared/components/calendar/FindATime.tsx +379 -0
  19. package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
  20. package/apps/shared/components/calendar/MonthView.tsx +15 -3
  21. package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
  22. package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
  23. package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
  24. package/apps/shared/components/calendar/RecurrenceEditor.tsx +10 -5
  25. package/apps/shared/components/calendar/RequestChangeForm.tsx +271 -0
  26. package/apps/shared/components/calendar/SplitDayView.tsx +6 -5
  27. package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
  28. package/apps/shared/components/calendar/TimeGridView.tsx +8 -6
  29. package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
  30. package/apps/shared/components/calendar/eventForm.ts +108 -0
  31. package/apps/shared/components/calendar/eventFormat.ts +183 -0
  32. package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
  33. package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
  34. package/apps/shared/components/layout/UserMenu.tsx +80 -28
  35. package/apps/shared/components/mail/ConversationList.tsx +100 -48
  36. package/apps/shared/components/mail/ConversationThreadPane.tsx +490 -486
  37. package/apps/shared/components/mail/InviteCard.tsx +387 -0
  38. package/apps/shared/components/mail/MessageDetailPane.tsx +24 -2
  39. package/apps/shared/components/mail/SwipeRow.tsx +120 -0
  40. package/apps/shared/components/mail/compose/ComposeWindow.tsx +7 -3
  41. package/apps/shared/components/mail/invite/InviteRowChip.tsx +104 -0
  42. package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
  43. package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
  44. package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
  45. package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
  46. package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
  47. package/apps/shared/components/mail/layout/MailShell.tsx +522 -490
  48. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  49. package/apps/shared/gestures/useEnterSlide.ts +54 -0
  50. package/apps/shared/gestures/useSwipe.ts +114 -0
  51. package/apps/shared/gestures/useSwipeSlide.ts +113 -0
  52. package/apps/shared/gestures/useWheelPaging.ts +107 -0
  53. package/apps/shared/mail/newMailNotifications.ts +171 -183
  54. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  55. package/apps/shared/notifications/preferences.ts +36 -0
  56. package/apps/shared/notifications/store.ts +560 -551
  57. package/apps/www/_routes.ts +1 -0
  58. package/apps/www/calendar/index.tsx +139 -37
  59. package/apps/www/index.tsx +273 -126
  60. package/apps/www/messages/[uid].tsx +31 -4
  61. package/apps/www/settings/profile/index.tsx +248 -0
  62. package/dist/apps/admin/domains/[uid].js +4 -2
  63. package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
  64. package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
  65. package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
  66. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
  67. package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
  68. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  69. package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -3
  70. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +3 -1
  71. package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
  72. package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
  73. package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
  74. package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
  75. package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
  76. package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
  77. package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
  78. package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
  79. package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
  80. package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
  81. package/dist/apps/shared/components/calendar/EventEditor.js +549 -0
  82. package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
  83. package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
  84. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +69 -0
  85. package/dist/apps/shared/components/calendar/EventFormParts.js +202 -0
  86. package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
  87. package/dist/apps/shared/components/calendar/EventModal.js +57 -459
  88. package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
  89. package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
  90. package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
  91. package/dist/apps/shared/components/calendar/EventShell.js +134 -0
  92. package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
  93. package/dist/apps/shared/components/calendar/FindATime.js +190 -0
  94. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.d.ts +8 -0
  95. package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
  96. package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
  97. package/dist/apps/shared/components/calendar/MonthView.js +11 -5
  98. package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
  99. package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
  100. package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
  101. package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
  102. package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
  103. package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
  104. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  105. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
  106. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
  107. package/dist/apps/shared/components/calendar/RequestChangeForm.js +138 -0
  108. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
  109. package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
  110. package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
  111. package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
  112. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
  113. package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
  114. package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
  115. package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
  116. package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
  117. package/dist/apps/shared/components/calendar/eventForm.js +1 -0
  118. package/dist/apps/shared/components/calendar/eventFormat.d.ts +67 -0
  119. package/dist/apps/shared/components/calendar/eventFormat.js +163 -0
  120. package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
  121. package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
  122. package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
  123. package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
  124. package/dist/apps/shared/components/layout/UserMenu.d.ts +10 -8
  125. package/dist/apps/shared/components/layout/UserMenu.js +70 -23
  126. package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
  127. package/dist/apps/shared/components/mail/ConversationList.js +21 -4
  128. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
  129. package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
  130. package/dist/apps/shared/components/mail/InviteCard.js +150 -0
  131. package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
  132. package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
  133. package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
  134. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -3
  135. package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +23 -0
  136. package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
  137. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
  138. package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
  139. package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
  140. package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
  141. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
  142. package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
  143. package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
  144. package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
  145. package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
  146. package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
  147. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +5 -0
  148. package/dist/apps/shared/components/mail/layout/MailShell.js +14 -3
  149. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  150. package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
  151. package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
  152. package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
  153. package/dist/apps/shared/gestures/useSwipe.js +90 -0
  154. package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
  155. package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
  156. package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
  157. package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
  158. package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
  159. package/dist/apps/shared/mail/newMailNotifications.js +3 -12
  160. package/dist/apps/shared/mail/useNewMailNotifications.js +3 -2
  161. package/dist/apps/shared/notifications/preferences.d.ts +13 -0
  162. package/dist/apps/shared/notifications/preferences.js +36 -0
  163. package/dist/apps/shared/notifications/store.d.ts +1 -0
  164. package/dist/apps/shared/notifications/store.js +7 -23
  165. package/dist/apps/www/_routes.js +1 -0
  166. package/dist/apps/www/calendar/index.js +76 -6
  167. package/dist/apps/www/index.js +140 -39
  168. package/dist/apps/www/messages/[uid].js +13 -5
  169. package/dist/apps/www/settings/profile/index.d.ts +4 -0
  170. package/dist/apps/www/settings/profile/index.js +124 -0
  171. package/package.json +2 -2
@@ -0,0 +1,32 @@
1
+ import { type RefObject } from "react";
2
+ /** Wheel movement, in pixels, that steps to the next or previous page when nothing inside scrolls: about one notch of a mouse wheel. */
3
+ export declare const WHEEL_STEP_PX = 100;
4
+ /** Wheel movement, in pixels, pushed against the top or bottom of what scrolls inside before it steps onto the next or previous page. */
5
+ export declare const WHEEL_EDGE_PX = 80;
6
+ /** Movement that isn't followed by more within this long starts from nothing again, so an old nudge doesn't add to a new one. */
7
+ export declare const WHEEL_IDLE_MS = 250;
8
+ /** After a step that starts at an edge, wheel events for this long are left to settle the new page, so one hard push isn't two steps. */
9
+ export declare const WHEEL_SETTLE_MS = 100;
10
+ /** Where the new page's scroll position goes: `"top"` when stepping forward past the bottom, `"bottom"` when stepping back past the top. */
11
+ export type WheelEdge = "top" | "bottom";
12
+ export interface UseWheelPagingOptions {
13
+ /** Nothing is handled while this is `false` (a phone, an event being dragged, a dialog open). */
14
+ enabled: boolean;
15
+ /** The element inside that scrolls vertically, if there is one now: the wheel scrolls it as usual and only steps past its ends.
16
+ * `null` when nothing inside scrolls (a month grid), and then every wheel movement counts towards a step. */
17
+ getScroller: () => HTMLElement | null;
18
+ /** Called for each step: `1` towards the next page (the wheel turned down), `-1` towards the previous. `edge` says where in the new
19
+ * page's scroller to start, when there is one. */
20
+ onStep: (direction: 1 | -1, edge: WheelEdge | null) => void;
21
+ }
22
+ /**
23
+ * Turns the mouse wheel over `ref`'s element into stepping through pages - the months, weeks or days of a calendar - without a limit:
24
+ * keep turning and it keeps stepping, and nothing waits for an animation to end.
25
+ *
26
+ * Where nothing inside scrolls, every `WHEEL_STEP_PX` of movement is a step (a bigger turn is several). Where something does, the
27
+ * wheel scrolls it as it always did and only what is pushed against its top or bottom counts (`WHEEL_EDGE_PX`), so scrolling on
28
+ * from the last hour of a day carries into the first hours of the next, and back the other way. The event is only cancelled when it
29
+ * counts towards a step, so the page and whatever scrolls inside keep their own wheel behaviour everywhere else. Zooming
30
+ * (Ctrl+wheel) and sideways movement are left alone.
31
+ */
32
+ export declare function useWheelPaging(ref: RefObject<HTMLElement | null>, { enabled, getScroller, onStep }: UseWheelPagingOptions): void;
@@ -0,0 +1,85 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { useEffect, useRef } from "react";
6
+ /** Wheel movement, in pixels, that steps to the next or previous page when nothing inside scrolls: about one notch of a mouse wheel. */
7
+ export const WHEEL_STEP_PX = 100;
8
+ /** Wheel movement, in pixels, pushed against the top or bottom of what scrolls inside before it steps onto the next or previous page. */
9
+ export const WHEEL_EDGE_PX = 80;
10
+ /** Movement that isn't followed by more within this long starts from nothing again, so an old nudge doesn't add to a new one. */
11
+ export const WHEEL_IDLE_MS = 250;
12
+ /** After a step that starts at an edge, wheel events for this long are left to settle the new page, so one hard push isn't two steps. */
13
+ export const WHEEL_SETTLE_MS = 100;
14
+ /** A wheel event's vertical movement in pixels, whichever unit the browser reports it in. */
15
+ function pixelsOf(event, page) {
16
+ return event.deltaMode === 1 ? event.deltaY * 16 : event.deltaMode === 2 ? event.deltaY * page : event.deltaY;
17
+ }
18
+ /**
19
+ * Turns the mouse wheel over `ref`'s element into stepping through pages - the months, weeks or days of a calendar - without a limit:
20
+ * keep turning and it keeps stepping, and nothing waits for an animation to end.
21
+ *
22
+ * Where nothing inside scrolls, every `WHEEL_STEP_PX` of movement is a step (a bigger turn is several). Where something does, the
23
+ * wheel scrolls it as it always did and only what is pushed against its top or bottom counts (`WHEEL_EDGE_PX`), so scrolling on
24
+ * from the last hour of a day carries into the first hours of the next, and back the other way. The event is only cancelled when it
25
+ * counts towards a step, so the page and whatever scrolls inside keep their own wheel behaviour everywhere else. Zooming
26
+ * (Ctrl+wheel) and sideways movement are left alone.
27
+ */
28
+ export function useWheelPaging(ref, { enabled, getScroller, onStep }) {
29
+ const latest = useRef({ getScroller, onStep });
30
+ latest.current = { getScroller, onStep };
31
+ /** Movement collected towards the next step, and when it last grew. */
32
+ const collected = useRef({ pixels: 0, at: 0 });
33
+ const settledUntil = useRef(0);
34
+ useEffect(() => {
35
+ const element = ref.current;
36
+ if (!enabled || !element) {
37
+ return;
38
+ }
39
+ function onWheel(event) {
40
+ if (event.ctrlKey || event.defaultPrevented || Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
41
+ return;
42
+ }
43
+ const pixels = pixelsOf(event, element.clientHeight || 600);
44
+ if (pixels === 0) {
45
+ return;
46
+ }
47
+ const scroller = latest.current.getScroller();
48
+ if (scroller) {
49
+ const room = scroller.scrollHeight - scroller.clientHeight;
50
+ const canScroll = pixels > 0 ? scroller.scrollTop < room - 1 : scroller.scrollTop > 0;
51
+ if (room > 0 && canScroll) {
52
+ // Still something to scroll that way: the browser does it, and what was pushed against an end is forgotten.
53
+ collected.current = { pixels: 0, at: 0 };
54
+ return;
55
+ }
56
+ }
57
+ event.preventDefault();
58
+ const now = Date.now();
59
+ if (scroller && now < settledUntil.current) {
60
+ return;
61
+ }
62
+ const state = collected.current;
63
+ if (now - state.at > WHEEL_IDLE_MS || Math.sign(state.pixels) !== Math.sign(pixels)) {
64
+ state.pixels = 0;
65
+ }
66
+ state.at = now;
67
+ state.pixels += pixels;
68
+ const needed = scroller ? WHEEL_EDGE_PX : WHEEL_STEP_PX;
69
+ while (Math.abs(state.pixels) >= needed) {
70
+ const direction = state.pixels > 0 ? 1 : -1;
71
+ state.pixels -= direction * needed;
72
+ latest.current.onStep(direction, scroller ? (direction > 0 ? "top" : "bottom") : null);
73
+ if (scroller) {
74
+ // One push against an end is one step; the next turn of the wheel scrolls the page it arrived at.
75
+ state.pixels = 0;
76
+ settledUntil.current = now + WHEEL_SETTLE_MS;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ // Not passive: the wheel's default (scrolling the page) has to be cancellable for the steps that replace it.
82
+ element.addEventListener("wheel", onWheel, { passive: false });
83
+ return () => element.removeEventListener("wheel", onWheel);
84
+ }, [enabled, ref]);
85
+ }
@@ -1,23 +1,18 @@
1
1
  import type { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
2
  /**
3
- * What decides whether a new message is announced, what the announcement says, and the two settings behind it (both per
4
- * browser, in `localStorage`): whether new-mail pop-ups are on at all, and what the user told the "turn on desktop
5
- * notifications" offer.
3
+ * What decides whether a new message is announced, what the announcement says, and the setting behind it (per browser, in
4
+ * `localStorage`): what the user told the "turn on desktop notifications" offer. Whether pop-ups are on at all is the
5
+ * notifications switch (`notifications/preferences.ts`).
6
6
  *
7
7
  * The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
8
8
  * ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
9
9
  */
10
- /** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
11
- export declare const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
12
10
  /** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
13
11
  export declare const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
14
12
  /** How much of the body an announcement quotes. */
15
13
  export declare const PREVIEW_MAX_LENGTH = 140;
16
14
  /** A message received longer ago than this is not "new": mail imported or migrated in bulk arrives as `create` events too. */
17
15
  export declare const MAX_NEW_AGE_MS: number;
18
- /** Whether new-mail pop-ups are on (the default). */
19
- export declare function getNewMailPopupsEnabled(): boolean;
20
- export declare function setNewMailPopupsEnabled(enabled: boolean): void;
21
16
  /** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
22
17
  export declare function getDesktopOfferDismissed(): boolean;
23
18
  export declare function setDesktopOfferDismissed(dismissed: boolean): void;
@@ -1,14 +1,12 @@
1
1
  import { formatMailAddress, splitMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
2
2
  /**
3
- * What decides whether a new message is announced, what the announcement says, and the two settings behind it (both per
4
- * browser, in `localStorage`): whether new-mail pop-ups are on at all, and what the user told the "turn on desktop
5
- * notifications" offer.
3
+ * What decides whether a new message is announced, what the announcement says, and the setting behind it (per browser, in
4
+ * `localStorage`): what the user told the "turn on desktop notifications" offer. Whether pop-ups are on at all is the
5
+ * notifications switch (`notifications/preferences.ts`).
6
6
  *
7
7
  * The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
8
8
  * ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
9
9
  */
10
- /** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
11
- export const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
12
10
  /** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
13
11
  export const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
14
12
  /** How much of the body an announcement quotes. */
@@ -38,13 +36,6 @@ function writeStorage(key, value) {
38
36
  // Storage blocked or full: the choice lasts until the page is reloaded, no longer.
39
37
  }
40
38
  }
41
- /** Whether new-mail pop-ups are on (the default). */
42
- export function getNewMailPopupsEnabled() {
43
- return readStorage(NEW_MAIL_POPUPS_KEY) !== "off";
44
- }
45
- export function setNewMailPopupsEnabled(enabled) {
46
- writeStorage(NEW_MAIL_POPUPS_KEY, enabled ? null : "off");
47
- }
48
39
  /** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
49
40
  export function getDesktopOfferDismissed() {
50
41
  return readStorage(DESKTOP_OFFER_KEY) !== null;
@@ -3,7 +3,8 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { desktopPermission, getDesktopOfferDismissed, getNewMailPopupsEnabled, noticeFor, noticeSender, ownAddressesOf, requestDesktopPermission, setDesktopOfferDismissed, shouldAnnounce, } from "./newMailNotifications.js";
6
+ import { desktopPermission, getDesktopOfferDismissed, noticeFor, noticeSender, ownAddressesOf, requestDesktopPermission, setDesktopOfferDismissed, shouldAnnounce, } from "./newMailNotifications.js";
7
+ import { getNotificationsEnabled } from "../notifications/preferences.js";
7
8
  import { getNotificationsSnapshot, notify } from "../notifications/store.js";
8
9
  /** How long a new-mail pop-up stays before it goes by itself (while it is neither hovered nor focused, and the tab is in view). */
9
10
  export const TOAST_DURATION_MS = 8000;
@@ -82,7 +83,7 @@ export function useNewMailNotifications({ mailboxes, mailboxFolders, open = defa
82
83
  const offerRef = useRef({ offered: offerDesktop, enable: enableDesktop, decline: declineDesktop });
83
84
  offerRef.current = { offered: offerDesktop, enable: enableDesktop, decline: declineDesktop };
84
85
  const announce = useCallback((message) => {
85
- if (!getNewMailPopupsEnabled() || announcedRef.current.has(message.uid)) {
86
+ if (!getNotificationsEnabled() || announcedRef.current.has(message.uid)) {
86
87
  return;
87
88
  }
88
89
  const { folders: knownFolders, ownAddresses: own } = latestRef.current;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The user's one switch for every pop-up (new mail, a failed send, an API error, a reminder), per browser in `localStorage`.
3
+ * Framework-free like the store, which reads it: `notify()` keeps a notification out of sight while it is off (it is still
4
+ * listed in "Recent notifications").
5
+ */
6
+ /**
7
+ * `localStorage` key of the switch. Absent means on; `"off"` means the user turned pop-ups off. The name is from when the switch
8
+ * only covered new mail, and is kept so a browser where that was turned off stays off.
9
+ */
10
+ export declare const NOTIFICATIONS_ENABLED_KEY = "rapidmx-new-mail-popups";
11
+ /** Whether pop-up notifications are on (the default). */
12
+ export declare function getNotificationsEnabled(): boolean;
13
+ export declare function setNotificationsEnabled(enabled: boolean): void;
@@ -0,0 +1,36 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The user's one switch for every pop-up (new mail, a failed send, an API error, a reminder), per browser in `localStorage`.
7
+ * Framework-free like the store, which reads it: `notify()` keeps a notification out of sight while it is off (it is still
8
+ * listed in "Recent notifications").
9
+ */
10
+ /**
11
+ * `localStorage` key of the switch. Absent means on; `"off"` means the user turned pop-ups off. The name is from when the switch
12
+ * only covered new mail, and is kept so a browser where that was turned off stays off.
13
+ */
14
+ export const NOTIFICATIONS_ENABLED_KEY = "rapidmx-new-mail-popups";
15
+ /** Whether pop-up notifications are on (the default). */
16
+ export function getNotificationsEnabled() {
17
+ try {
18
+ return localStorage.getItem(NOTIFICATIONS_ENABLED_KEY) !== "off";
19
+ }
20
+ catch {
21
+ return true;
22
+ }
23
+ }
24
+ export function setNotificationsEnabled(enabled) {
25
+ try {
26
+ if (enabled) {
27
+ localStorage.removeItem(NOTIFICATIONS_ENABLED_KEY);
28
+ }
29
+ else {
30
+ localStorage.setItem(NOTIFICATIONS_ENABLED_KEY, "off");
31
+ }
32
+ }
33
+ catch {
34
+ // Storage blocked or full: the choice lasts until the page is reloaded, no longer.
35
+ }
36
+ }
@@ -15,6 +15,7 @@
15
15
  * hidden (`setAllPaused()`), and continues from where it was.
16
16
  * - **Deduplicated by `dedupeKey`**: a second notification with a key that is still on screen (or waiting) does not stack - it replaces
17
17
  * the first one's content, restarts its clock and raises its `count`, so a flapping error is one pop-up saying "x3".
18
+ * - **The user can turn them all off** (`preferences.ts`): `notify()` then shows nothing new, and only records it in the history.
18
19
  * - **Nothing is lost when one goes**: the last `HISTORY_LIMIT` (30, except new-mail pop-ups, which the inbox already holds) are
19
20
  * kept, in memory and in `sessionStorage`, with which errors nobody has seen yet.
20
21
  *
@@ -2,29 +2,7 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- /**
6
- * The app's one notification store: every pop-up - new mail, a failed send, an API error, an expired session - goes through
7
- * `notify()`. It is deliberately framework-free (no React, no DOM beyond `sessionStorage`/timers), so it can be called from a hook,
8
- * a plain function or an `apiFetch` error path alike; `NotificationCenter` (the view, mounted once in the app frame) and
9
- * `useNotifications()` only *read* it.
10
- *
11
- * Rules, all enforced here rather than by the view so they hold whoever renders:
12
- *
13
- * - **At most `MAX_VISIBLE` (3) are visible**; the rest wait in a queue and appear as room is made. A sticky notification (an error, or
14
- * anything with actions) that finds the stack full pushes out the oldest one that would have gone by itself, rather than waiting behind
15
- * it. A queued notification that has waited `QUEUE_STALE_MS` is dropped (it would be news from the past) - it is still in the history.
16
- * - **Sticky until dismissed or resolved**: errors and notifications with actions never expire (`update()` or `dismiss()` resolve them).
17
- * Everything else goes by itself after its kind's default time, or `timeoutMs`. `sticky` overrides both ways.
18
- * - **A clock that stops**: a notification's remaining time stops while it is hovered or focused (`setPaused()`) and while the tab is
19
- * hidden (`setAllPaused()`), and continues from where it was.
20
- * - **Deduplicated by `dedupeKey`**: a second notification with a key that is still on screen (or waiting) does not stack - it replaces
21
- * the first one's content, restarts its clock and raises its `count`, so a flapping error is one pop-up saying "x3".
22
- * - **Nothing is lost when one goes**: the last `HISTORY_LIMIT` (30, except new-mail pop-ups, which the inbox already holds) are
23
- * kept, in memory and in `sessionStorage`, with which errors nobody has seen yet.
24
- *
25
- * Nothing is stored where there is no `window` (server-side rendering): the module state is shared between requests there, so
26
- * `notify()` only hands back an id.
27
- */
5
+ import { getNotificationsEnabled } from "./preferences.js";
28
6
  export const MAX_VISIBLE = 3;
29
7
  export const MAX_QUEUED = 20;
30
8
  export const QUEUE_STALE_MS = 30000;
@@ -254,6 +232,12 @@ export function notify(input) {
254
232
  paused: false,
255
233
  };
256
234
  applyInput(entry, input);
235
+ if (!getNotificationsEnabled()) {
236
+ // The user has pop-ups off: nothing is shown, but it is listed in the history like any other.
237
+ record(entry);
238
+ emit();
239
+ return id;
240
+ }
257
241
  const room = visibleCount() < MAX_VISIBLE;
258
242
  entries.push(entry);
259
243
  if (room) {
@@ -22,6 +22,7 @@ export const wwwRoutes = [
22
22
  { path: "/settings/filters/:uid", active: "settings", load: () => import("./settings/filters/[uid].js") },
23
23
  { path: "/settings/labels", active: "settings", load: () => import("./settings/labels/index.js") },
24
24
  { path: "/settings/privacy", active: "settings", load: () => import("./settings/privacy/index.js") },
25
+ { path: "/settings/profile", active: "settings", load: () => import("./settings/profile/index.js") },
25
26
  { path: "/settings/read-receipts", active: "settings", load: () => import("./settings/read-receipts/index.js") },
26
27
  { path: "/settings/sharing", active: "settings", load: () => import("./settings/sharing/index.js") },
27
28
  { path: "/settings/signatures", active: "settings", load: () => import("./settings/signatures/index.js") },
@@ -4,12 +4,14 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { routedPage } from "../_routedPage.js";
7
- import { useEffect, useMemo, useRef, useState } from "react";
7
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
8
8
  import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
9
9
  import { HiOutlineBars3 } from "react-icons/hi2";
10
10
  import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, parseISO, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
11
11
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
12
- import { listCalendarEvents } from "@rapidmx/react-shared/calendar/calendarApi.js";
12
+ import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
13
+ import { getCalendarEvent, listCalendarEvents } from "@rapidmx/react-shared/calendar/calendarApi.js";
14
+ import { getPushClient } from "@rapidmx/react-shared/mail/pushClient.js";
13
15
  import { moveOccurrence, resizeOccurrenceEnd } from "@rapidmx/react-shared/calendar/calendarMutations.js";
14
16
  import { resolveDragAction } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
15
17
  import { createFolder } from "@rapidmx/react-shared/mail/mailApi.js";
@@ -19,18 +21,25 @@ import CalendarShell, { useCalendarShell } from "../../shared/components/calenda
19
21
  import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
20
22
  import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
21
23
  import EventModal from "../../shared/components/calendar/EventModal.js";
24
+ import { anchorOf } from "../../shared/components/calendar/EventShell.js";
22
25
  import MiniDatePicker from "@rapidmx/react-shared/components/pickers/MiniDatePicker.js";
23
26
  import MonthView from "../../shared/components/calendar/MonthView.js";
24
27
  import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
25
28
  import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
26
29
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
27
30
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
31
+ import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
32
+ import { useSwipeSlide } from "../../shared/gestures/useSwipeSlide.js";
33
+ import { useEnterSlide } from "../../shared/gestures/useEnterSlide.js";
34
+ import { useWheelPaging } from "../../shared/gestures/useWheelPaging.js";
28
35
  import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
29
36
  import { useShortcut } from "../../shared/keyboard/useShortcut.js";
30
37
  import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
31
38
  import { notifyApiError } from "../../shared/notifications/apiErrors.js";
39
+ import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
40
+ import { redactedEventUidOf } from "../../shared/calendar/calendarLiveUpdates.js";
32
41
  function CalendarPage(props) {
33
- return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid }) }));
42
+ return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid, bookingHref: bookingSettingsHref(props.pluginNav) }) }));
34
43
  }
35
44
  const VIEW_TYPES = ["month", "week", "workWeek", "day", "split"];
36
45
  const VIEW_LABELS = { month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
@@ -53,7 +62,7 @@ function ViewButton({ view, current, onSelect }) {
53
62
  .filter(Boolean)
54
63
  .join(" "), children: VIEW_LABELS[view] }));
55
64
  }
56
- function CalendarContent({ userUid }) {
65
+ function CalendarContent({ userUid, bookingHref }) {
57
66
  const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } = useCalendarShell();
58
67
  // `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
59
68
  // scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
@@ -68,6 +77,8 @@ function CalendarContent({ userUid }) {
68
77
  const [loading, setLoading] = useState(true);
69
78
  const [error, setError] = useState(null);
70
79
  const [modal, setModal] = useState(null);
80
+ // True from the moment a touch drag of an event (or its resize handle) activates until it ends or is cancelled.
81
+ const [dragging, setDragging] = useState(false);
71
82
  // `null` means "not yet customized by the user" — every calendar is treated as checked without
72
83
  // needing to be seeded from `calendarFolders` the moment it loads (see `checkedFolderUids` below).
73
84
  // Once the user toggles anything, this becomes a real (possibly empty) set of exactly what's checked.
@@ -183,6 +194,29 @@ function CalendarContent({ userUid }) {
183
194
  const calendarFolderUidsKey = calendarFolders.map((f) => f.uid).sort().join(",");
184
195
  const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
185
196
  useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
197
+ // A private or confidential event that somebody creates or changes is announced on the push connection as its busy block, whatever the reader may
198
+ // see (see `redactedEventUidOf()`): that payload is never stored here. The event is fetched again by its uid, which comes back as much as this
199
+ // reader may see - all of it for the owner - and replaces (or joins) what the calendar holds; one that is gone (a 404) leaves it. A failed fetch
200
+ // changes nothing: the next load shows the event as it is.
201
+ const checkedFolderUidsRef = useRef(checkedFolderUids);
202
+ checkedFolderUidsRef.current = checkedFolderUids;
203
+ useEffect(() => getPushClient().onEvent((event) => {
204
+ const uid = redactedEventUidOf(event);
205
+ if (!uid) {
206
+ return;
207
+ }
208
+ void getCalendarEvent(uid).then((fresh) => {
209
+ if (checkedFolderUidsRef.current.has(fresh.folderUid)) {
210
+ setEvents((previous) => previous.some((existing) => existing.uid === fresh.uid)
211
+ ? previous.map((existing) => (existing.uid === fresh.uid ? fresh : existing))
212
+ : [...previous, fresh]);
213
+ }
214
+ }, (err) => {
215
+ if (err instanceof ApiRequestError && err.status === 404) {
216
+ setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
217
+ }
218
+ });
219
+ }), []);
186
220
  const occurrences = useMemo(() => expandAllOccurrences(events, rangeStart, rangeEnd), [events, rangeStart, rangeEnd]);
187
221
  const splitColumns = useMemo(() => calendarFolders
188
222
  .filter((f) => checkedFolderUids.has(f.uid))
@@ -194,6 +228,39 @@ function CalendarContent({ userUid }) {
194
228
  ? addWeeks(d, direction)
195
229
  : addDays(d, direction));
196
230
  }
231
+ // The phone layout's swipe between periods. Only the view area carries it (not the toolbar), it calls the same `shiftView()`
232
+ // the Previous/Next buttons and shortcuts do, and it is off while an event is being dragged or the editor is open. The views
233
+ // never scroll sideways (their columns share the width), so there is no horizontal scroller for the gesture to fight with.
234
+ // The view follows the finger and, once the swipe commits, slides out while the next period slides in (see `useSwipeSlide()`).
235
+ const isMobile = useIsMobile();
236
+ const swipe = useSwipeSlide({
237
+ enabled: isMobile && !dragging && !modal,
238
+ onShift: (direction) => shiftView(SWIPE_PERIOD_SHIFT[direction]),
239
+ });
240
+ // The desktop's mouse wheel steps through the months without end; in the views that scroll through the hours of a day it scrolls
241
+ // them as usual and, pushed past the last (or first) hour, carries on into the next (or previous) day, week or work week. Each
242
+ // step is the same `shiftView()` and enters with a short vertical slide (`useEnterSlide()`), which never waits for the last one.
243
+ const wheelSlide = useEnterSlide();
244
+ /** Where the scroller of the page a wheel step arrived at starts: its top after going on, its bottom after going back. */
245
+ const arrivalEdge = useRef(null);
246
+ const scrollerOf = () => swipe.ref.current?.querySelector("[data-calendar-scroller]") ?? null;
247
+ useWheelPaging(swipe.ref, {
248
+ enabled: !isMobile && !dragging && !modal,
249
+ getScroller: () => (view === "month" ? null : scrollerOf()),
250
+ onStep: (direction, edge) => {
251
+ arrivalEdge.current = edge;
252
+ shiftView(direction);
253
+ wheelSlide.enter(direction);
254
+ },
255
+ });
256
+ useLayoutEffect(() => {
257
+ const edge = arrivalEdge.current;
258
+ arrivalEdge.current = null;
259
+ const scroller = edge ? scrollerOf() : null;
260
+ if (scroller) {
261
+ scroller.scrollTop = edge === "top" ? 0 : scroller.scrollHeight;
262
+ }
263
+ }, [viewDate]);
197
264
  function goToday() {
198
265
  setViewDate(startOfDay(new Date()));
199
266
  }
@@ -201,7 +268,7 @@ function CalendarContent({ userUid }) {
201
268
  setView("day");
202
269
  setViewDate(startOfDay(date));
203
270
  }
204
- function openNewEvent(start, end, targetFolderUid) {
271
+ function openNewEvent(start, end, targetFolderUid, anchor, initialAllDay) {
205
272
  if (!mailboxUid || !folderUid) {
206
273
  return;
207
274
  }
@@ -210,6 +277,8 @@ function CalendarContent({ userUid }) {
210
277
  initialStart: start ?? new Date(),
211
278
  initialEnd: end ?? new Date(Date.now() + 30 * 60000),
212
279
  targetFolderUid,
280
+ anchor,
281
+ initialAllDay,
213
282
  });
214
283
  }
215
284
  function openEvent(occurrence) {
@@ -240,6 +309,7 @@ function CalendarContent({ userUid }) {
240
309
  reload();
241
310
  }
242
311
  async function handleDragEnd(event) {
312
+ setDragging(false);
243
313
  const overId = event.over ? String(event.over.id) : undefined;
244
314
  const action = resolveDragAction(String(event.active.id), overId, occurrences);
245
315
  if (!action) {
@@ -273,6 +343,6 @@ function CalendarContent({ userUid }) {
273
343
  .filter((mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid))
274
344
  .map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })), [mailboxCalendars, writableMailboxes]);
275
345
  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 })] }));
276
- 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", 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" }) }), _jsx(Button, { type: "button", onClick: () => openNewEvent(), 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 }) })), loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid) => openNewEvent(start, end, targetFolderUid) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: openNewEvent })) })), 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, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, onSaved: handleSaved, onDeleted: handleDeleted }))] })] }));
346
+ 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", 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" }) }), _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 }))] })] }));
277
347
  }
278
348
  export default routedPage("/calendar", CalendarPage);