@rapidmx/web-client 0.5.0 → 0.7.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 (224) hide show
  1. package/README.md +88 -0
  2. package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
  3. package/apps/shared/components/admin/settings/BrandingForm.tsx +398 -385
  4. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
  5. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
  6. package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
  7. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
  8. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
  9. package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
  10. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  11. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  12. package/apps/shared/components/layout/AppShell.tsx +288 -270
  13. package/apps/shared/components/mail/ConversationList.tsx +225 -33
  14. package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
  15. package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
  16. package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
  17. package/apps/shared/components/mail/MenuButton.tsx +384 -0
  18. package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
  19. package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
  20. package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
  21. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
  22. package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
  23. package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
  24. package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
  25. package/apps/shared/components/mail/compose/recipients.ts +104 -0
  26. package/apps/shared/components/mail/labelMenu.tsx +278 -0
  27. package/apps/shared/components/mail/layout/MailShell.tsx +31 -4
  28. package/apps/shared/components/mail/listPreferences.ts +228 -0
  29. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -10
  30. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  31. package/apps/shared/plugins/pluginNav.ts +73 -0
  32. package/apps/shared/search/localIndexRpcClient.ts +3 -1
  33. package/apps/shared/styles/app.css +28 -5
  34. package/apps/www/index.tsx +1854 -1314
  35. package/apps/www/messages/[uid].tsx +5 -4
  36. package/dist/apps/admin/_404.d.ts +2 -0
  37. package/dist/apps/admin/_500.d.ts +2 -0
  38. package/dist/apps/admin/_layout.d.ts +8 -0
  39. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  40. package/dist/apps/admin/branding/index.d.ts +3 -0
  41. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  42. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  43. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  44. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  45. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  46. package/dist/apps/admin/domains/index.d.ts +3 -0
  47. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  48. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  49. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  50. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  51. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  52. package/dist/apps/admin/index.d.ts +3 -0
  53. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  54. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  55. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  56. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  57. package/dist/apps/admin/plugins/index.d.ts +3 -0
  58. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  59. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  60. package/dist/apps/admin/setup/index.d.ts +3 -0
  61. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  62. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  63. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  64. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  65. package/dist/apps/escrow/_layout.d.ts +10 -0
  66. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  67. package/dist/apps/escrow/index.d.ts +3 -0
  68. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  69. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  70. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  71. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  72. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  73. package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
  74. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  75. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  76. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  77. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  78. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +13 -0
  79. package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
  80. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  81. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +15 -0
  82. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
  83. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  84. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  85. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +13 -0
  86. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
  87. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +11 -0
  88. package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
  89. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +12 -0
  90. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
  91. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  92. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
  93. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  94. package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
  95. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  96. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  97. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  98. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  99. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  100. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  101. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  102. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  103. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  104. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  105. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  106. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  107. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  108. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  109. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  110. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  111. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  112. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  113. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  114. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  115. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  116. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  117. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  118. package/dist/apps/shared/components/layout/AppShell.js +15 -13
  119. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  120. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  121. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  122. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  123. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  124. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  125. package/dist/apps/shared/components/mail/ConversationList.d.ts +47 -0
  126. package/dist/apps/shared/components/mail/ConversationList.js +84 -10
  127. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +62 -0
  128. package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
  129. package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
  130. package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
  131. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
  132. package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
  133. package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
  134. package/dist/apps/shared/components/mail/MenuButton.js +211 -0
  135. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +101 -0
  136. package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
  137. package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
  138. package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
  139. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +73 -0
  140. package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
  141. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  142. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  143. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
  144. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  145. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  146. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
  147. package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
  148. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +43 -0
  149. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
  150. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  151. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  152. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
  153. package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
  154. package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
  155. package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
  156. package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
  157. package/dist/apps/shared/components/mail/labelMenu.js +134 -0
  158. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +65 -0
  159. package/dist/apps/shared/components/mail/layout/MailShell.js +17 -5
  160. package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
  161. package/dist/apps/shared/components/mail/listPreferences.js +186 -0
  162. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  163. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  164. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  165. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  166. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  167. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -10
  168. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  169. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  170. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  171. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  172. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  173. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  174. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  175. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  178. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  179. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  181. package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
  182. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  183. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  184. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  185. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  186. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  187. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  188. package/dist/apps/shared/styles/app.css +28 -5
  189. package/dist/apps/www/_404.d.ts +2 -0
  190. package/dist/apps/www/_500.d.ts +2 -0
  191. package/dist/apps/www/_layout.d.ts +14 -0
  192. package/dist/apps/www/calendar/index.d.ts +3 -0
  193. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  194. package/dist/apps/www/contacts/index.d.ts +3 -0
  195. package/dist/apps/www/index.d.ts +3 -0
  196. package/dist/apps/www/index.js +506 -112
  197. package/dist/apps/www/messages/[uid].d.ts +11 -0
  198. package/dist/apps/www/messages/[uid].js +2 -3
  199. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  200. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  201. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  202. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  203. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  204. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  205. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  206. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  207. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  208. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  209. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  210. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  211. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  212. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  213. package/dist/apps/www/tasks/index.d.ts +3 -0
  214. package/package.json +3 -3
  215. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  216. package/apps/www/settings/booking-types/[uid].tsx +0 -306
  217. package/apps/www/settings/booking-types/index.tsx +0 -98
  218. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  219. package/apps/www/settings/focused-inbox/index.tsx +0 -150
  220. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  221. package/dist/apps/www/settings/booking-types/[uid].js +0 -134
  222. package/dist/apps/www/settings/booking-types/index.js +0 -29
  223. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
  224. package/dist/apps/www/settings/focused-inbox/index.js +0 -71
@@ -1,199 +1,201 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
- import { HiOutlineBars3 } from "react-icons/hi2";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
- import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
- import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
- import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
- import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
14
-
15
- export type TasksShellProps = Omit<AppShellProps, "active">;
16
-
17
- export interface TasksShellContextValue {
18
- /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
19
- mailboxUid?: string;
20
- /** The selected mailbox's single `tasks`-type folder. */
21
- folderUid?: string;
22
- mailboxes: Mailbox[];
23
- /** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
24
- * can compute "assigned to me" without needing it threaded through separately. */
25
- userUid?: string;
26
- }
27
-
28
- const TasksShellContext = createContext<TasksShellContextValue>({ mailboxes: [] });
29
-
30
- /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
31
- export function useTasksShell(): TasksShellContextValue {
32
- return useContext(TasksShellContext);
33
- }
34
-
35
- type Status = "checking" | "error" | "ready";
36
-
37
- /**
38
- * The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
39
- * well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
40
- * in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
41
- * (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
42
- * grow its own app-specific controls later without threading extra props through a shared one.
43
- */
44
- export default function TasksShell({
45
- userUid,
46
- authServerUrl,
47
- impersonating,
48
- impersonationBaseUrl,
49
- trusted,
50
- children,
51
- }: PropsWithChildren<TasksShellProps>) {
52
- const [status, setStatus] = useState<Status>("checking");
53
- const [error, setError] = useState<string | null>(null);
54
- const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
55
- const [folders, setFolders] = useState<Folder[]>([]);
56
- const [folderError, setFolderError] = useState<string | null>(null);
57
- const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
58
- const [drawerOpen, setDrawerOpen] = useState(false);
59
-
60
- useEffect(() => {
61
- setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
62
- }, []);
63
-
64
- useEffect(() => {
65
- if (!userUid) {
66
- return;
67
- }
68
- listMailboxes({ limit: 100 })
69
- .then((result) => {
70
- setMailboxes(result);
71
- setStatus("ready");
72
- })
73
- .catch((err) => {
74
- setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
75
- setStatus("error");
76
- });
77
- }, [userUid]);
78
-
79
- const mailboxUid: string | undefined =
80
- (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
81
- mailboxes[0]?.uid;
82
-
83
- useEffect(() => {
84
- if (!mailboxUid) {
85
- setFolders([]);
86
- return;
87
- }
88
- setFolderError(null);
89
- listFolders(mailboxUid)
90
- .then(setFolders)
91
- .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's tasks folder."));
92
- }, [mailboxUid]);
93
-
94
- const folderUid: string | undefined = folders.find((f) => f.type === "tasks")?.uid;
95
-
96
- const contextValue = useMemo<TasksShellContextValue>(
97
- () => ({ mailboxUid, folderUid, mailboxes, userUid }),
98
- [mailboxUid, folderUid, mailboxes, userUid],
99
- );
100
-
101
- // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
102
- // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
103
- if (userUid && status === "ready" && !mailboxUid) {
104
- return <MailboxProvisioning />;
105
- }
106
-
107
- let inner: ReactNode = null;
108
- if (userUid && status === "checking") {
109
- // Renders immediately (no network round trip needed) so switching into Tasks never shows a
110
- // blank pane while `listMailboxes()` is in flight — the real sidebar (My Day/Important/Planned/
111
- // lists) lives in `apps/www/tasks/index.tsx`'s own content, so this mimics its rough shape rather
112
- // than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
113
- inner = (
114
- <aside className="w-56 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
115
- <Skeleton height="h-9" />
116
- <SkeletonList count={5} />
117
- </aside>
118
- );
119
- } else if (userUid && status === "error") {
120
- inner = (
121
- <div className="flex-1 flex items-center justify-center p-8">
122
- <div className="w-full max-w-md">
123
- <Alert>{error}</Alert>
124
- </div>
125
- </div>
126
- );
127
- } else if (userUid && status === "ready") {
128
- // A function, not a plain JSX constant — see MailShell/ContactsShell/CalendarShell's identical
129
- // comment: it's rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously
130
- // mounted, so the `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
131
- const sidebarContent = (idPrefix: string) => (
132
- <>
133
- {mailboxes.length > 1 && (
134
- <div>
135
- <label
136
- className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
137
- htmlFor={`${idPrefix}-tasks-mailbox-switcher`}
138
- >
139
- Mailbox
140
- </label>
141
- <select
142
- id={`${idPrefix}-tasks-mailbox-switcher`}
143
- className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
144
- value={mailboxUid}
145
- onChange={(e) => {
146
- window.location.href = `/tasks?mailboxUid=${encodeURIComponent(e.target.value)}`;
147
- }}
148
- >
149
- {mailboxes.map((mb) => (
150
- <option key={mb.uid} value={mb.uid}>
151
- {mb.displayName}
152
- {mb.ownerUserUid ? "" : " (shared)"}
153
- </option>
154
- ))}
155
- </select>
156
- </div>
157
- )}
158
- {folderError && <Alert>{folderError}</Alert>}
159
- </>
160
- );
161
-
162
- inner = (
163
- <>
164
- <aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
165
- {sidebarContent("desktop")}
166
- </aside>
167
- <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
168
- <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
169
- </Drawer>
170
- <main className="flex-1 min-w-0 flex flex-col">
171
- {(mailboxes.length > 1 || folderError) && (
172
- <button
173
- type="button"
174
- className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
175
- aria-label="Open mailbox switcher"
176
- onClick={() => setDrawerOpen(true)}
177
- >
178
- <HiOutlineBars3 size={20} aria-hidden="true" />
179
- </button>
180
- )}
181
- <TasksShellContext.Provider value={contextValue}>{children}</TasksShellContext.Provider>
182
- </main>
183
- </>
184
- );
185
- }
186
-
187
- return (
188
- <AppShell
189
- active="tasks"
190
- userUid={userUid}
191
- authServerUrl={authServerUrl}
192
- impersonating={impersonating}
193
- impersonationBaseUrl={impersonationBaseUrl}
194
- trusted={trusted}
195
- >
196
- {inner}
197
- </AppShell>
198
- );
199
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
+ import { HiOutlineBars3 } from "react-icons/hi2";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
+ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
+ import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
+ import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
14
+
15
+ export type TasksShellProps = Omit<AppShellProps, "active">;
16
+
17
+ export interface TasksShellContextValue {
18
+ /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
19
+ mailboxUid?: string;
20
+ /** The selected mailbox's single `tasks`-type folder. */
21
+ folderUid?: string;
22
+ mailboxes: Mailbox[];
23
+ /** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
24
+ * can compute "assigned to me" without needing it threaded through separately. */
25
+ userUid?: string;
26
+ }
27
+
28
+ const TasksShellContext = createContext<TasksShellContextValue>({ mailboxes: [] });
29
+
30
+ /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
31
+ export function useTasksShell(): TasksShellContextValue {
32
+ return useContext(TasksShellContext);
33
+ }
34
+
35
+ type Status = "checking" | "error" | "ready";
36
+
37
+ /**
38
+ * The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
39
+ * well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
40
+ * in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
41
+ * (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
42
+ * grow its own app-specific controls later without threading extra props through a shared one.
43
+ */
44
+ export default function TasksShell({
45
+ userUid,
46
+ authServerUrl,
47
+ impersonating,
48
+ impersonationBaseUrl,
49
+ trusted,
50
+ pluginNav,
51
+ children,
52
+ }: PropsWithChildren<TasksShellProps>) {
53
+ const [status, setStatus] = useState<Status>("checking");
54
+ const [error, setError] = useState<string | null>(null);
55
+ const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
56
+ const [folders, setFolders] = useState<Folder[]>([]);
57
+ const [folderError, setFolderError] = useState<string | null>(null);
58
+ const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
59
+ const [drawerOpen, setDrawerOpen] = useState(false);
60
+
61
+ useEffect(() => {
62
+ setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ if (!userUid) {
67
+ return;
68
+ }
69
+ listMailboxes({ limit: 100 })
70
+ .then((result) => {
71
+ setMailboxes(result);
72
+ setStatus("ready");
73
+ })
74
+ .catch((err) => {
75
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
76
+ setStatus("error");
77
+ });
78
+ }, [userUid]);
79
+
80
+ const mailboxUid: string | undefined =
81
+ (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
82
+ mailboxes[0]?.uid;
83
+
84
+ useEffect(() => {
85
+ if (!mailboxUid) {
86
+ setFolders([]);
87
+ return;
88
+ }
89
+ setFolderError(null);
90
+ listFolders(mailboxUid)
91
+ .then(setFolders)
92
+ .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's tasks folder."));
93
+ }, [mailboxUid]);
94
+
95
+ const folderUid: string | undefined = folders.find((f) => f.type === "tasks")?.uid;
96
+
97
+ const contextValue = useMemo<TasksShellContextValue>(
98
+ () => ({ mailboxUid, folderUid, mailboxes, userUid }),
99
+ [mailboxUid, folderUid, mailboxes, userUid],
100
+ );
101
+
102
+ // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
103
+ // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
104
+ if (userUid && status === "ready" && !mailboxUid) {
105
+ return <MailboxProvisioning />;
106
+ }
107
+
108
+ let inner: ReactNode = null;
109
+ if (userUid && status === "checking") {
110
+ // Renders immediately (no network round trip needed) so switching into Tasks never shows a
111
+ // blank pane while `listMailboxes()` is in flight — the real sidebar (My Day/Important/Planned/
112
+ // lists) lives in `apps/www/tasks/index.tsx`'s own content, so this mimics its rough shape rather
113
+ // than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
114
+ inner = (
115
+ <aside className="w-56 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
116
+ <Skeleton height="h-9" />
117
+ <SkeletonList count={5} />
118
+ </aside>
119
+ );
120
+ } else if (userUid && status === "error") {
121
+ inner = (
122
+ <div className="flex-1 flex items-center justify-center p-8">
123
+ <div className="w-full max-w-md">
124
+ <Alert>{error}</Alert>
125
+ </div>
126
+ </div>
127
+ );
128
+ } else if (userUid && status === "ready") {
129
+ // A function, not a plain JSX constant — see MailShell/ContactsShell/CalendarShell's identical
130
+ // comment: it's rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously
131
+ // mounted, so the `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
132
+ const sidebarContent = (idPrefix: string) => (
133
+ <>
134
+ {mailboxes.length > 1 && (
135
+ <div>
136
+ <label
137
+ className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
138
+ htmlFor={`${idPrefix}-tasks-mailbox-switcher`}
139
+ >
140
+ Mailbox
141
+ </label>
142
+ <select
143
+ id={`${idPrefix}-tasks-mailbox-switcher`}
144
+ className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
145
+ value={mailboxUid}
146
+ onChange={(e) => {
147
+ window.location.href = `/tasks?mailboxUid=${encodeURIComponent(e.target.value)}`;
148
+ }}
149
+ >
150
+ {mailboxes.map((mb) => (
151
+ <option key={mb.uid} value={mb.uid}>
152
+ {mb.displayName}
153
+ {mb.ownerUserUid ? "" : " (shared)"}
154
+ </option>
155
+ ))}
156
+ </select>
157
+ </div>
158
+ )}
159
+ {folderError && <Alert>{folderError}</Alert>}
160
+ </>
161
+ );
162
+
163
+ inner = (
164
+ <>
165
+ <aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
166
+ {sidebarContent("desktop")}
167
+ </aside>
168
+ <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
169
+ <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
170
+ </Drawer>
171
+ <main className="flex-1 min-w-0 flex flex-col">
172
+ {(mailboxes.length > 1 || folderError) && (
173
+ <button
174
+ type="button"
175
+ className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
176
+ aria-label="Open mailbox switcher"
177
+ onClick={() => setDrawerOpen(true)}
178
+ >
179
+ <HiOutlineBars3 size={20} aria-hidden="true" />
180
+ </button>
181
+ )}
182
+ <TasksShellContext.Provider value={contextValue}>{children}</TasksShellContext.Provider>
183
+ </main>
184
+ </>
185
+ );
186
+ }
187
+
188
+ return (
189
+ <AppShell
190
+ active="tasks"
191
+ userUid={userUid}
192
+ authServerUrl={authServerUrl}
193
+ impersonating={impersonating}
194
+ impersonationBaseUrl={impersonationBaseUrl}
195
+ trusted={trusted}
196
+ pluginNav={pluginNav}
197
+ >
198
+ {inner}
199
+ </AppShell>
200
+ );
201
+ }
@@ -0,0 +1,73 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+
6
+ /**
7
+ * One navigation entry a plugin contributes through its manifest's `ui` field. Structurally identical to
8
+ * `@rapidmx/restapi`'s `PluginUiNavItem`, defined locally so this package doesn't depend on restapi.
9
+ */
10
+ export interface PluginUiNavItem {
11
+ /** Unique within its list. A plugin page passes this as its shell's `active` to highlight the entry. */
12
+ id: string;
13
+ label: string;
14
+ /** A same-origin path under the host's prefix (`/settings/...`, `/admin/...`, `/...`). */
15
+ href: string;
16
+ }
17
+
18
+ /**
19
+ * The navigation every enabled plugin with a successfully built UI contributes, supplied by the server's
20
+ * `WwwRoute`/`AdminConsoleRoute` `fetchProps` as the `pluginNav` page prop.
21
+ */
22
+ export interface PluginNav {
23
+ /** Appended to `SettingsShell`'s section list. */
24
+ settingsSections?: PluginUiNavItem[];
25
+ /** Appended to `AdminShell`'s icon rail and mobile tab bar. */
26
+ adminNav?: PluginUiNavItem[];
27
+ /** Appended to `AppShell`'s app rail and mobile tab bar. */
28
+ appRail?: PluginUiNavItem[];
29
+ }
30
+
31
+ /** The page prop every www and admin page receives - extended by `AppShellProps` and `AdminShellProps`, so
32
+ * a page that spreads its props onto its shell passes `pluginNav` through with no code of its own. */
33
+ export interface PluginNavProps {
34
+ pluginNav?: PluginNav;
35
+ }
36
+
37
+ /** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
38
+ * protocol-relative URLs to another host). */
39
+ export function isSafePluginHref(href: string): boolean {
40
+ return /^\/(?![/\\])/.test(href);
41
+ }
42
+
43
+ /**
44
+ * Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
45
+ * isn't a same-origin path (see `isSafePluginHref`), or whose id is already taken - by a core item or
46
+ * `reservedIds` (core ids always win) or by an earlier plugin item.
47
+ */
48
+ export function mergePluginNavItems<T extends { id: string }>(
49
+ coreItems: T[],
50
+ pluginItems: PluginUiNavItem[] | undefined,
51
+ toItem: (item: PluginUiNavItem) => T,
52
+ reservedIds: string[] = [],
53
+ ): T[] {
54
+ if (!pluginItems?.length) {
55
+ return coreItems;
56
+ }
57
+ const taken = new Set([...coreItems.map((item) => item.id), ...reservedIds]);
58
+ const merged = [...coreItems];
59
+ for (const item of pluginItems) {
60
+ if (
61
+ typeof item?.id !== "string" ||
62
+ typeof item.label !== "string" ||
63
+ typeof item.href !== "string" ||
64
+ !isSafePluginHref(item.href) ||
65
+ taken.has(item.id)
66
+ ) {
67
+ continue;
68
+ }
69
+ taken.add(item.id);
70
+ merged.push(toItem(item));
71
+ }
72
+ return merged;
73
+ }
@@ -69,7 +69,9 @@ let channel: BroadcastChannel | undefined;
69
69
 
70
70
  function getWorker(): Worker {
71
71
  if (!worker) {
72
- worker = new Worker(new URL("./localIndexWorker.ts", import.meta.url), { type: "module" });
72
+ // Named by its compiled `.js` file, like every other relative import: tsc copies the literal into
73
+ // `dist` unchanged, where only the `.js` file exists, and Vite maps it back to the `.ts` source.
74
+ worker = new Worker(new URL("./localIndexWorker.js", import.meta.url), { type: "module" });
73
75
  worker.addEventListener("message", (event: MessageEvent<LocalIndexResponse>) => {
74
76
  const entry = pending.get(event.data.id);
75
77
  if (!entry) {
@@ -12,11 +12,24 @@
12
12
 
13
13
  /*
14
14
  * Tailwind v4's automatic content scanning excludes `node_modules` by default, which is exactly where
15
- * `@rapidmx/react-shared`'s components live from every consumer's perspective (a `link:`-ed sibling) -
16
- * without this, none of its Tailwind classnames (Modal/Drawer/Button/Alert/Skeleton/FormField/
17
- * PopoverPortal/ContactAvatar/MiniDatePicker/BottomTabBar) would ever be scanned.
15
+ * `@rapidmx/react-shared`'s components live - without these, none of its Tailwind classnames (Modal/
16
+ * Drawer/Button/Alert/Skeleton/FormField/PopoverPortal/ContactAvatar/MiniDatePicker/BottomTabBar) are
17
+ * generated, and e.g. a Modal renders with no overlay, position or width. The published package ships
18
+ * only its compiled `dist` (no `src`), and `@source` paths resolve from this file's directory
19
+ * (`apps/shared/styles/`), so both install layouts are listed; a path that doesn't exist is skipped:
20
+ * - `../../../node_modules/...`: react-shared under this package's own `node_modules` - this repo's
21
+ * checkout, or a consumer install that nests it under web-client.
22
+ * - `../../../../react-shared/dist`: react-shared hoisted beside this package, i.e. this file at
23
+ * `<consumer>/node_modules/@rapidmx/web-client/apps/shared/styles/` and react-shared at
24
+ * `<consumer>/node_modules/@rapidmx/react-shared` - how `server` installs both, for its pages and
25
+ * for the plugin pages' `plugin-ui.css`, which imports this file from there.
26
+ * `electron-client` links `node_modules/@rapidmx/web-client` to a checkout of this repo, so one of the
27
+ * two matches whether this file is reached through the link or its real path. From a checkout, the
28
+ * second path can also find a sibling `react-shared` checkout's build, which only adds classes
29
+ * react-shared itself uses.
18
30
  */
19
- @source "../../node_modules/@rapidmx/react-shared/src";
31
+ @source "../../../node_modules/@rapidmx/react-shared/dist";
32
+ @source "../../../../react-shared/dist";
20
33
 
21
34
  /*
22
35
  * `server`'s own build (unlike `electron-client`, which already explicitly sourced this same tree in
@@ -30,7 +43,7 @@
30
43
  * silently absent from the built stylesheet, even though `AdminShell.tsx` itself was very much in the
31
44
  * bundle - verified directly by diffing every classname `AdminShell.tsx` uses against the actual built
32
45
  * CSS. An explicit `@source` here makes the scan independent of Vite's per-entry chunking, the same
33
- * fix this file already applies to `react-shared`, just for this package's own source too.
46
+ * fix this file applies to `react-shared` above, just for this package's own source too.
34
47
  */
35
48
  @source "../../";
36
49
 
@@ -263,3 +276,13 @@ h5,
263
276
  h6 {
264
277
  font-family: var(--rr-font-display);
265
278
  }
279
+
280
+ /* The compose editor's quoted original (`composeQuoting.ts`'s blockquote). TipTap keeps the element but not the
281
+ * inline style it was seeded with, and Tailwind's preflight resets the browser's own blockquote indent - without
282
+ * this the quote under a reply reads as if it were part of the new message. */
283
+ .tiptap blockquote {
284
+ margin: 0;
285
+ padding-left: 1em;
286
+ border-left: 2px solid var(--rr-color-border);
287
+ color: var(--rr-color-text-muted);
288
+ }