@rapidmx/web-client 0.8.0 → 0.10.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 (247) hide show
  1. package/README.md +192 -88
  2. package/apps/shared/auth/accountUrl.ts +9 -0
  3. package/apps/shared/auth/adminAccess.ts +99 -0
  4. package/apps/shared/components/admin/elevation.ts +61 -0
  5. package/apps/shared/components/admin/layout/AdminShell.tsx +73 -7
  6. package/apps/shared/components/admin/settings/BrandingForm.tsx +2 -1
  7. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +87 -24
  8. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  9. package/apps/shared/components/contacts/ContactsToolbar.tsx +125 -115
  10. package/apps/shared/components/contacts/layout/ContactsShell.tsx +15 -3
  11. package/apps/shared/components/layout/AppShell.tsx +411 -288
  12. package/apps/shared/components/layout/BrandingChrome.tsx +3 -2
  13. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  14. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -126
  15. package/apps/shared/components/layout/UserMenu.tsx +303 -150
  16. package/apps/shared/components/mail/ConversationList.tsx +269 -253
  17. package/apps/shared/components/mail/ConversationThreadPane.tsx +475 -434
  18. package/apps/shared/components/mail/LazyReadingPane.tsx +115 -0
  19. package/apps/shared/components/mail/MailAddress.tsx +88 -0
  20. package/apps/shared/components/mail/MailSelectionBar.tsx +235 -222
  21. package/apps/shared/components/mail/MessageDetailPane.tsx +1458 -1383
  22. package/apps/shared/components/mail/NewMailToasts.tsx +141 -0
  23. package/apps/shared/components/mail/compose/ComposeContext.tsx +279 -160
  24. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  25. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1842 -1700
  26. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  27. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  28. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +48 -0
  29. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  30. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  31. package/apps/shared/components/mail/layout/MailShell.tsx +476 -445
  32. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  33. package/apps/shared/components/settings/layout/SettingsShell.tsx +252 -240
  34. package/apps/shared/components/tasks/layout/TasksShell.tsx +15 -3
  35. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  36. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  37. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  38. package/apps/shared/keyboard/dispatch.ts +124 -0
  39. package/apps/shared/keyboard/format.ts +89 -0
  40. package/apps/shared/keyboard/keymap.ts +114 -0
  41. package/apps/shared/keyboard/match.ts +44 -0
  42. package/apps/shared/keyboard/parse.ts +136 -0
  43. package/apps/shared/keyboard/platform.ts +34 -0
  44. package/apps/shared/keyboard/registry.ts +65 -0
  45. package/apps/shared/keyboard/targets.ts +79 -0
  46. package/apps/shared/keyboard/useShortcut.ts +50 -0
  47. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  48. package/apps/shared/mail/folderCounts.ts +302 -0
  49. package/apps/shared/mail/listSnapshots.ts +87 -0
  50. package/apps/shared/mail/mergeFirstPage.ts +47 -0
  51. package/apps/shared/mail/messageReadState.ts +85 -0
  52. package/apps/shared/mail/newMailNotifications.ts +183 -0
  53. package/apps/shared/mail/useMailConnection.ts +150 -0
  54. package/apps/shared/mail/useMailLiveUpdates.ts +232 -0
  55. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  56. package/apps/shared/mail/useNewMailNotifications.ts +163 -0
  57. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  58. package/apps/shared/navigation/AppRouter.tsx +300 -0
  59. package/apps/shared/navigation/appHrefs.ts +23 -0
  60. package/apps/shared/navigation/frameContext.tsx +35 -0
  61. package/apps/shared/navigation/idle.ts +45 -0
  62. package/apps/shared/navigation/routerContext.tsx +83 -0
  63. package/apps/shared/navigation/routes.ts +72 -0
  64. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  65. package/apps/shared/styles/app.css +28 -10
  66. package/apps/www/_routedPage.tsx +24 -0
  67. package/apps/www/_routes.ts +35 -0
  68. package/apps/www/calendar/index.tsx +55 -19
  69. package/apps/www/contacts/[uid].tsx +112 -107
  70. package/apps/www/contacts/index.tsx +584 -567
  71. package/apps/www/index.tsx +2280 -1854
  72. package/apps/www/messages/[uid].tsx +106 -101
  73. package/apps/www/settings/auto-reply/index.tsx +4 -1
  74. package/apps/www/settings/encryption/index.tsx +1252 -1249
  75. package/apps/www/settings/filters/[uid].tsx +4 -1
  76. package/apps/www/settings/filters/index.tsx +102 -99
  77. package/apps/www/settings/filters/new/index.tsx +138 -133
  78. package/apps/www/settings/labels/index.tsx +204 -201
  79. package/apps/www/settings/privacy/index.tsx +4 -1
  80. package/apps/www/settings/read-receipts/index.tsx +4 -1
  81. package/apps/www/settings/sharing/index.tsx +277 -274
  82. package/apps/www/settings/signatures/[uid].tsx +170 -167
  83. package/apps/www/settings/signatures/index.tsx +88 -85
  84. package/apps/www/settings/signatures/new/index.tsx +134 -129
  85. package/apps/www/tasks/index.tsx +18 -2
  86. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  87. package/dist/apps/shared/auth/accountUrl.js +8 -0
  88. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  89. package/dist/apps/shared/auth/adminAccess.js +89 -0
  90. package/dist/apps/shared/components/admin/elevation.d.ts +24 -0
  91. package/dist/apps/shared/components/admin/elevation.js +56 -0
  92. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +12 -4
  93. package/dist/apps/shared/components/admin/layout/AdminShell.js +49 -6
  94. package/dist/apps/shared/components/admin/settings/BrandingForm.js +1 -1
  95. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +31 -13
  96. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  97. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  98. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  99. package/dist/apps/shared/components/contacts/ContactsToolbar.js +7 -4
  100. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  101. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +15 -5
  102. package/dist/apps/shared/components/layout/AppShell.d.ts +29 -4
  103. package/dist/apps/shared/components/layout/AppShell.js +73 -14
  104. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +3 -2
  105. package/dist/apps/shared/components/layout/BrandingChrome.js +3 -2
  106. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  107. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +5 -2
  108. package/dist/apps/shared/components/layout/MailboxProvisioning.js +45 -8
  109. package/dist/apps/shared/components/layout/UserMenu.d.ts +28 -9
  110. package/dist/apps/shared/components/layout/UserMenu.js +87 -14
  111. package/dist/apps/shared/components/mail/ConversationList.js +9 -13
  112. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
  113. package/dist/apps/shared/components/mail/ConversationThreadPane.js +53 -28
  114. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  115. package/dist/apps/shared/components/mail/LazyReadingPane.js +82 -0
  116. package/dist/apps/shared/components/mail/MailAddress.d.ts +27 -0
  117. package/dist/apps/shared/components/mail/MailAddress.js +39 -0
  118. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +4 -1
  119. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -3
  120. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +7 -1
  121. package/dist/apps/shared/components/mail/MessageDetailPane.js +87 -36
  122. package/dist/apps/shared/components/mail/NewMailToasts.d.ts +19 -0
  123. package/dist/apps/shared/components/mail/NewMailToasts.js +55 -0
  124. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +30 -0
  125. package/dist/apps/shared/components/mail/compose/ComposeContext.js +71 -5
  126. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  127. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +1 -1
  128. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +122 -27
  129. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  130. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  131. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  132. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  133. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +14 -0
  134. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +11 -0
  135. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  136. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  137. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  138. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  139. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +19 -4
  140. package/dist/apps/shared/components/mail/layout/MailShell.js +85 -85
  141. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  142. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  143. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  144. package/dist/apps/shared/components/settings/layout/SettingsShell.js +15 -5
  145. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  146. package/dist/apps/shared/components/tasks/layout/TasksShell.js +15 -5
  147. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  148. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  149. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  150. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  151. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  152. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  153. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  154. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  155. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  156. package/dist/apps/shared/keyboard/format.js +74 -0
  157. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  158. package/dist/apps/shared/keyboard/keymap.js +84 -0
  159. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  160. package/dist/apps/shared/keyboard/match.js +31 -0
  161. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  162. package/dist/apps/shared/keyboard/parse.js +107 -0
  163. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  164. package/dist/apps/shared/keyboard/platform.js +22 -0
  165. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  166. package/dist/apps/shared/keyboard/registry.js +33 -0
  167. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  168. package/dist/apps/shared/keyboard/targets.js +67 -0
  169. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  170. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  171. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  172. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  173. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  174. package/dist/apps/shared/mail/folderCounts.js +212 -0
  175. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  176. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  177. package/dist/apps/shared/mail/mergeFirstPage.d.ts +23 -0
  178. package/dist/apps/shared/mail/mergeFirstPage.js +30 -0
  179. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  180. package/dist/apps/shared/mail/messageReadState.js +63 -0
  181. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  182. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  183. package/dist/apps/shared/mail/useMailConnection.d.ts +55 -0
  184. package/dist/apps/shared/mail/useMailConnection.js +96 -0
  185. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +55 -0
  186. package/dist/apps/shared/mail/useMailLiveUpdates.js +182 -0
  187. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  188. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  189. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +41 -0
  190. package/dist/apps/shared/mail/useNewMailNotifications.js +110 -0
  191. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  192. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  193. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  194. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  195. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  196. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  197. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  198. package/dist/apps/shared/navigation/frameContext.js +24 -0
  199. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  200. package/dist/apps/shared/navigation/idle.js +43 -0
  201. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  202. package/dist/apps/shared/navigation/routerContext.js +56 -0
  203. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  204. package/dist/apps/shared/navigation/routes.js +37 -0
  205. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  206. package/dist/apps/shared/styles/app.css +28 -10
  207. package/dist/apps/www/_routedPage.d.ts +12 -0
  208. package/dist/apps/www/_routedPage.js +19 -0
  209. package/dist/apps/www/_routes.d.ts +11 -0
  210. package/dist/apps/www/_routes.js +29 -0
  211. package/dist/apps/www/calendar/index.d.ts +2 -2
  212. package/dist/apps/www/calendar/index.js +39 -8
  213. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  214. package/dist/apps/www/contacts/[uid].js +6 -2
  215. package/dist/apps/www/contacts/index.d.ts +2 -2
  216. package/dist/apps/www/contacts/index.js +17 -4
  217. package/dist/apps/www/index.d.ts +2 -2
  218. package/dist/apps/www/index.js +361 -34
  219. package/dist/apps/www/messages/[uid].d.ts +3 -7
  220. package/dist/apps/www/messages/[uid].js +7 -4
  221. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  222. package/dist/apps/www/settings/auto-reply/index.js +3 -1
  223. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  224. package/dist/apps/www/settings/encryption/index.js +3 -1
  225. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  226. package/dist/apps/www/settings/filters/[uid].js +3 -1
  227. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  228. package/dist/apps/www/settings/filters/index.js +3 -1
  229. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  230. package/dist/apps/www/settings/filters/new/index.js +6 -2
  231. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  232. package/dist/apps/www/settings/labels/index.js +3 -1
  233. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  234. package/dist/apps/www/settings/privacy/index.js +3 -1
  235. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  236. package/dist/apps/www/settings/read-receipts/index.js +3 -1
  237. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  238. package/dist/apps/www/settings/sharing/index.js +3 -1
  239. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  240. package/dist/apps/www/settings/signatures/[uid].js +3 -1
  241. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  242. package/dist/apps/www/settings/signatures/index.js +3 -1
  243. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  244. package/dist/apps/www/settings/signatures/new/index.js +6 -2
  245. package/dist/apps/www/tasks/index.d.ts +2 -2
  246. package/dist/apps/www/tasks/index.js +15 -3
  247. package/package.json +2 -2
@@ -7,6 +7,7 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
7
  import { DnsRecordCheck, Domain, getDnsSetup, getDomain, verifyDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
8
8
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
9
9
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
10
+ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
10
11
 
11
12
  const RECORD_TYPE_LABELS: Record<DnsRecordCheck["type"], string> = {
12
13
  ownership: "Ownership (TXT)",
@@ -16,6 +17,31 @@ const RECORD_TYPE_LABELS: Record<DnsRecordCheck["type"], string> = {
16
17
  dmarc: "DMARC",
17
18
  };
18
19
 
20
+ /** How each record is named in a Copy button's accessible name ("Copy value for the SPF record"). */
21
+ const RECORD_TYPE_NAMES: Record<DnsRecordCheck["type"], string> = {
22
+ ownership: "ownership TXT",
23
+ mx: "MX",
24
+ spf: "SPF",
25
+ dkim: "DKIM",
26
+ dmarc: "DMARC",
27
+ };
28
+
29
+ /** An MX record's recommended value is `"<priority> <mail server>"` - two separate fields in a DNS provider's form. */
30
+ function splitMxValue(value: string): { priority: string; server: string } | null {
31
+ const match = /^(\d+)\s+(\S+)$/.exec(value.trim());
32
+ return match ? { priority: match[1], server: match[2] } : null;
33
+ }
34
+
35
+ /** A value to type into a DNS provider's form, shown in full (long DKIM keys wrap) with a Copy button beside it. */
36
+ function CopyableValue({ value, copyLabel }: { value: string; copyLabel: string }) {
37
+ return (
38
+ <div className="flex items-start gap-2">
39
+ <code className="flex-1 min-w-0 text-xs break-all">{value}</code>
40
+ <CopyButton value={value} label={copyLabel} />
41
+ </div>
42
+ );
43
+ }
44
+
19
45
  export interface DomainDnsSetupProps {
20
46
  uid: string;
21
47
  /** Called with the domain every time it's (re)loaded - e.g. after "Verify now". */
@@ -33,7 +59,6 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
33
59
  const [error, setError] = useState<string | null>(null);
34
60
  const [verifying, setVerifying] = useState(false);
35
61
  const [verifyError, setVerifyError] = useState<string | null>(null);
36
- const [copied, setCopied] = useState(false);
37
62
 
38
63
  function reload(id: string) {
39
64
  setLoading(true);
@@ -71,16 +96,6 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
71
96
  }
72
97
  }
73
98
 
74
- async function handleCopy(value: string) {
75
- try {
76
- await navigator.clipboard.writeText(value);
77
- setCopied(true);
78
- setTimeout(() => setCopied(false), 2000);
79
- } catch {
80
- // Clipboard access can be denied by the browser — the value is still selectable/copyable by hand.
81
- }
82
- }
83
-
84
99
  if (loading) {
85
100
  return <p className="text-sm text-text-muted">Loading&hellip;</p>;
86
101
  }
@@ -88,8 +103,9 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
88
103
  return <Alert>{error ?? "Domain not found."}</Alert>;
89
104
  }
90
105
 
91
- const ownershipValue =
92
- dnsSetup.find((c) => c.type === "ownership")?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`;
106
+ const ownership = dnsSetup.find((c) => c.type === "ownership");
107
+ const ownershipValue = ownership?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`;
108
+ const ownershipName = ownership?.recordName || domain.name;
93
109
 
94
110
  return (
95
111
  <div className="flex flex-col gap-5">
@@ -118,14 +134,18 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
118
134
  <p className="text-sm mb-2">
119
135
  Add the following TXT record to <strong>{domain.name}</strong> to prove ownership, then verify:
120
136
  </p>
121
- <div className="flex items-center gap-2">
122
- <code className="flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap">
123
- {ownershipValue}
124
- </code>
125
- <Button type="button" variant="secondary" className="!w-auto shrink-0" onClick={() => handleCopy(ownershipValue)}>
126
- {copied ? "Copied" : "Copy"}
127
- </Button>
128
- </div>
137
+ <dl className="grid grid-cols-[auto_1fr] items-start gap-x-4 gap-y-2 text-sm">
138
+ <dt className="text-text-muted py-2">Type</dt>
139
+ <dd className="py-2">TXT</dd>
140
+ <dt className="text-text-muted py-2">Name / host</dt>
141
+ <dd className="bg-surface-alt border border-border rounded-sm py-1.5 px-3">
142
+ <CopyableValue value={ownershipName} copyLabel="Copy name for the ownership TXT record" />
143
+ </dd>
144
+ <dt className="text-text-muted py-2">Value</dt>
145
+ <dd className="bg-surface-alt border border-border rounded-sm py-1.5 px-3">
146
+ <CopyableValue value={ownershipValue} copyLabel="Copy value for the ownership TXT record" />
147
+ </dd>
148
+ </dl>
129
149
  {verifyError && (
130
150
  <div className="mt-3">
131
151
  <Alert>{verifyError}</Alert>
@@ -145,7 +165,7 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
145
165
  <table className="w-full text-sm border-collapse">
146
166
  <thead>
147
167
  <tr>
148
- {["Record", "Status", "Recommended value"].map((h) => (
168
+ {["Record", "Status", "Name / host", "Recommended value"].map((h) => (
149
169
  <th
150
170
  key={h}
151
171
  className="text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border"
@@ -158,7 +178,10 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
158
178
  <tbody>
159
179
  {dnsSetup.map((check) => (
160
180
  <tr key={check.type}>
161
- <td className="py-2.5 px-2.5 border-b border-border">{RECORD_TYPE_LABELS[check.type]}</td>
181
+ <td className="py-2.5 px-2.5 border-b border-border">
182
+ {RECORD_TYPE_LABELS[check.type]}
183
+ <div className="text-xs text-text-muted">Type: {check.recordKind}</div>
184
+ </td>
162
185
  <td className="py-2.5 px-2.5 border-b border-border">
163
186
  {!check.configured ? (
164
187
  <span className="text-xs text-text-muted">Not configured</span>
@@ -172,16 +195,56 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
172
195
  </span>
173
196
  )}
174
197
  </td>
198
+ {/* A floor on the name column: beside a long DKIM key (which breaks anywhere) the table gave it a few characters, and a name wrapped one letter to a line. */}
199
+ <td className="py-2.5 px-2.5 border-b border-border text-text-muted min-w-[10rem]">
200
+ {check.recordName ? (
201
+ <CopyableValue
202
+ value={check.recordName}
203
+ copyLabel={`Copy name for the ${RECORD_TYPE_NAMES[check.type]} record`}
204
+ />
205
+ ) : (
206
+ <code className="text-xs">—</code>
207
+ )}
208
+ </td>
175
209
  <td className="py-2.5 px-2.5 border-b border-border text-text-muted">
176
- <code className="text-xs">{check.recommendedValue ?? "—"}</code>
210
+ {check.recommendedValue ? (
211
+ <DnsRecordValue check={check} value={check.recommendedValue} />
212
+ ) : (
213
+ <code className="text-xs">—</code>
214
+ )}
177
215
  </td>
178
216
  </tr>
179
217
  ))}
180
218
  </tbody>
181
219
  </table>
182
220
  </div>
221
+ <p className="text-xs text-text-muted mt-3">
222
+ Some DNS providers want the name relative to the domain (for example <code>@</code> for the domain itself) rather
223
+ than the full name.
224
+ </p>
183
225
  </div>
184
226
  )}
185
227
  </div>
186
228
  );
187
229
  }
230
+
231
+ /** The value cell of a checklist row: an MX value as its two form fields (priority and mail server), anything else whole. */
232
+ function DnsRecordValue({ check, value }: { check: DnsRecordCheck; value: string }) {
233
+ const recordName = RECORD_TYPE_NAMES[check.type];
234
+ const mx = check.type === "mx" ? splitMxValue(value) : null;
235
+ if (!mx) {
236
+ return <CopyableValue value={value} copyLabel={`Copy value for the ${recordName} record`} />;
237
+ }
238
+ return (
239
+ <div className="flex flex-col gap-1.5">
240
+ <div className="flex items-start gap-2">
241
+ <span className="text-xs w-20 shrink-0 pt-1">Priority</span>
242
+ <CopyableValue value={mx.priority} copyLabel={`Copy priority for the ${recordName} record`} />
243
+ </div>
244
+ <div className="flex items-start gap-2">
245
+ <span className="text-xs w-20 shrink-0 pt-1">Mail server</span>
246
+ <CopyableValue value={mx.server} copyLabel={`Copy mail server for the ${recordName} record`} />
247
+ </div>
248
+ </div>
249
+ );
250
+ }
@@ -9,6 +9,7 @@ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shar
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
11
  import AppShell, { AppShellProps } from "../../layout/AppShell.js";
12
+ import { useLocationSearch } from "../../../navigation/AppRouter.js";
12
13
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
13
14
 
14
15
  export type CalendarShellProps = Omit<AppShellProps, "active">;
@@ -69,6 +70,7 @@ export default function CalendarShell({
69
70
  impersonating,
70
71
  impersonationBaseUrl,
71
72
  trusted,
73
+ trustedRoles,
72
74
  pluginNav,
73
75
  children,
74
76
  }: PropsWithChildren<CalendarShellProps>) {
@@ -79,9 +81,12 @@ export default function CalendarShell({
79
81
  const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
80
82
  const [folderRefreshToken, setFolderRefreshToken] = useState(0);
81
83
 
84
+ // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
85
+ // effect, not during render, so the server render and the hydrating render agree.
86
+ const search = useLocationSearch();
82
87
  useEffect(() => {
83
- setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
84
- }, []);
88
+ setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
89
+ }, [search]);
85
90
 
86
91
  useEffect(() => {
87
92
  if (!userUid) {
@@ -186,6 +191,7 @@ export default function CalendarShell({
186
191
  impersonating={impersonating}
187
192
  impersonationBaseUrl={impersonationBaseUrl}
188
193
  trusted={trusted}
194
+ trustedRoles={trustedRoles}
189
195
  pluginNav={pluginNav}
190
196
  >
191
197
  {inner}
@@ -1,115 +1,125 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { ChangeEvent, useRef } from "react";
6
- import type { IconType } from "react-icons";
7
- import {
8
- HiOutlineArrowDownTray,
9
- HiOutlineArrowUpTray,
10
- HiOutlineEnvelope,
11
- HiOutlinePencil,
12
- HiOutlineStar,
13
- HiOutlineTag,
14
- HiOutlineTrash,
15
- HiOutlineUserPlus,
16
- HiStar,
17
- } from "react-icons/hi2";
18
-
19
- export interface ContactsToolbarProps {
20
- selectedCount: number;
21
- /** `true` only when every currently-selected contact is already favorited — flips the "favorite" button
22
- * to act (and read) as a removal instead of an addition. */
23
- allSelectedFavorited: boolean;
24
- onNewContact: () => void;
25
- onEdit: () => void;
26
- onDelete: () => void;
27
- onEmail: () => void;
28
- onToggleFavorite: () => void;
29
- onAddCategory: () => void;
30
- onExportVCard: () => void;
31
- /** A `.vcf` file the user picked via the toolbar's own hidden file input — parsing/import is the
32
- * page's responsibility (it has the mailbox/folder context and the actual `createContact` calls). */
33
- onImportFile: (file: File) => void;
34
- }
35
-
36
- function ToolbarButton({
37
- label,
38
- icon: Icon,
39
- disabled,
40
- onClick,
41
- }: {
42
- label: string;
43
- icon: IconType;
44
- disabled?: boolean;
45
- onClick: () => void;
46
- }) {
47
- return (
48
- <button
49
- type="button"
50
- onClick={onClick}
51
- disabled={disabled}
52
- className="flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt"
53
- >
54
- <Icon size={18} aria-hidden="true" />
55
- <span>{label}</span>
56
- </button>
57
- );
58
- }
59
-
60
- function Divider() {
61
- return <div className="w-px self-stretch my-1 bg-border" aria-hidden="true" />;
62
- }
63
-
64
- /**
65
- * Contacts' Outlook-style ribbon toolbar. Deliberately presentational — every action is a callback the
66
- * page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
67
- * selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
68
- * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
69
- * "+ New list" affordance next to the "Your contact lists" heading.
70
- */
71
- export default function ContactsToolbar({
72
- selectedCount,
73
- allSelectedFavorited,
74
- onNewContact,
75
- onEdit,
76
- onDelete,
77
- onEmail,
78
- onToggleFavorite,
79
- onAddCategory,
80
- onExportVCard,
81
- onImportFile,
82
- }: ContactsToolbarProps) {
83
- const importInputRef = useRef<HTMLInputElement | null>(null);
84
- const hasSelection = selectedCount > 0;
85
-
86
- function handleImportFileChosen(e: ChangeEvent<HTMLInputElement>) {
87
- const file = e.target.files?.[0];
88
- e.target.value = "";
89
- if (file) {
90
- onImportFile(file);
91
- }
92
- }
93
-
94
- return (
95
- <div role="toolbar" aria-label="Contacts actions" className="border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1">
96
- <ToolbarButton label="New contact" icon={HiOutlineUserPlus} onClick={onNewContact} />
97
- <Divider />
98
- <ToolbarButton label="Edit" icon={HiOutlinePencil} disabled={selectedCount !== 1} onClick={onEdit} />
99
- <ToolbarButton label="Delete" icon={HiOutlineTrash} disabled={!hasSelection} onClick={onDelete} />
100
- <Divider />
101
- <ToolbarButton label="Email" icon={HiOutlineEnvelope} disabled={!hasSelection} onClick={onEmail} />
102
- <ToolbarButton
103
- label={allSelectedFavorited ? "Unfavorite" : "Favorite"}
104
- icon={allSelectedFavorited ? HiStar : HiOutlineStar}
105
- disabled={!hasSelection}
106
- onClick={onToggleFavorite}
107
- />
108
- <ToolbarButton label="Add category" icon={HiOutlineTag} disabled={!hasSelection} onClick={onAddCategory} />
109
- <Divider />
110
- <ToolbarButton label="Export" icon={HiOutlineArrowUpTray} disabled={!hasSelection} onClick={onExportVCard} />
111
- <ToolbarButton label="Import" icon={HiOutlineArrowDownTray} onClick={() => importInputRef.current?.click()} />
112
- <input ref={importInputRef} type="file" accept=".vcf" className="sr-only" aria-label="Import contacts file" onChange={handleImportFileChosen} />
113
- </div>
114
- );
115
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { ChangeEvent, useRef } from "react";
6
+ import type { IconType } from "react-icons";
7
+ import {
8
+ HiOutlineArrowDownTray,
9
+ HiOutlineArrowUpTray,
10
+ HiOutlineEnvelope,
11
+ HiOutlinePencil,
12
+ HiOutlineStar,
13
+ HiOutlineTag,
14
+ HiOutlineTrash,
15
+ HiOutlineUserPlus,
16
+ HiStar,
17
+ } from "react-icons/hi2";
18
+ import { SHORTCUTS } from "../../keyboard/keymap.js";
19
+ import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
20
+
21
+ export interface ContactsToolbarProps {
22
+ selectedCount: number;
23
+ /** `true` only when every currently-selected contact is already favorited — flips the "favorite" button
24
+ * to act (and read) as a removal instead of an addition. */
25
+ allSelectedFavorited: boolean;
26
+ onNewContact: () => void;
27
+ onEdit: () => void;
28
+ onDelete: () => void;
29
+ onEmail: () => void;
30
+ onToggleFavorite: () => void;
31
+ onAddCategory: () => void;
32
+ onExportVCard: () => void;
33
+ /** A `.vcf` file the user picked via the toolbar's own hidden file input — parsing/import is the
34
+ * page's responsibility (it has the mailbox/folder context and the actual `createContact` calls). */
35
+ onImportFile: (file: File) => void;
36
+ /** The keyboard's "New contact" is registered by the page: the button names its shortcut in its tooltip and `aria-keyshortcuts`. */
37
+ shortcuts?: boolean;
38
+ }
39
+
40
+ function ToolbarButton({
41
+ label,
42
+ icon: Icon,
43
+ disabled,
44
+ onClick,
45
+ hint,
46
+ }: {
47
+ label: string;
48
+ icon: IconType;
49
+ disabled?: boolean;
50
+ onClick: () => void;
51
+ /** `title` and `aria-keyshortcuts` for a button with a shortcut. */
52
+ hint?: { title: string; "aria-keyshortcuts"?: string };
53
+ }) {
54
+ return (
55
+ <button
56
+ type="button"
57
+ onClick={onClick}
58
+ disabled={disabled}
59
+ {...hint}
60
+ className="flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt"
61
+ >
62
+ <Icon size={18} aria-hidden="true" />
63
+ <span>{label}</span>
64
+ </button>
65
+ );
66
+ }
67
+
68
+ function Divider() {
69
+ return <div className="w-px self-stretch my-1 bg-border" aria-hidden="true" />;
70
+ }
71
+
72
+ /**
73
+ * Contacts' Outlook-style ribbon toolbar. Deliberately presentational — every action is a callback the
74
+ * page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
75
+ * selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
76
+ * needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
77
+ * "+ New list" affordance next to the "Your contact lists" heading.
78
+ */
79
+ export default function ContactsToolbar({
80
+ selectedCount,
81
+ allSelectedFavorited,
82
+ onNewContact,
83
+ onEdit,
84
+ onDelete,
85
+ onEmail,
86
+ onToggleFavorite,
87
+ onAddCategory,
88
+ onExportVCard,
89
+ onImportFile,
90
+ shortcuts,
91
+ }: ContactsToolbarProps) {
92
+ const newContactHint = useShortcutProps("New contact", SHORTCUTS.contacts.create, !!shortcuts);
93
+ const importInputRef = useRef<HTMLInputElement | null>(null);
94
+ const hasSelection = selectedCount > 0;
95
+
96
+ function handleImportFileChosen(e: ChangeEvent<HTMLInputElement>) {
97
+ const file = e.target.files?.[0];
98
+ e.target.value = "";
99
+ if (file) {
100
+ onImportFile(file);
101
+ }
102
+ }
103
+
104
+ return (
105
+ <div role="toolbar" aria-label="Contacts actions" className="border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1">
106
+ <ToolbarButton label="New contact" icon={HiOutlineUserPlus} onClick={onNewContact} hint={shortcuts ? newContactHint : undefined} />
107
+ <Divider />
108
+ <ToolbarButton label="Edit" icon={HiOutlinePencil} disabled={selectedCount !== 1} onClick={onEdit} />
109
+ <ToolbarButton label="Delete" icon={HiOutlineTrash} disabled={!hasSelection} onClick={onDelete} />
110
+ <Divider />
111
+ <ToolbarButton label="Email" icon={HiOutlineEnvelope} disabled={!hasSelection} onClick={onEmail} />
112
+ <ToolbarButton
113
+ label={allSelectedFavorited ? "Unfavorite" : "Favorite"}
114
+ icon={allSelectedFavorited ? HiStar : HiOutlineStar}
115
+ disabled={!hasSelection}
116
+ onClick={onToggleFavorite}
117
+ />
118
+ <ToolbarButton label="Add category" icon={HiOutlineTag} disabled={!hasSelection} onClick={onAddCategory} />
119
+ <Divider />
120
+ <ToolbarButton label="Export" icon={HiOutlineArrowUpTray} disabled={!hasSelection} onClick={onExportVCard} />
121
+ <ToolbarButton label="Import" icon={HiOutlineArrowDownTray} onClick={() => importInputRef.current?.click()} />
122
+ <input ref={importInputRef} type="file" accept=".vcf" className="sr-only" aria-label="Import contacts file" onChange={handleImportFileChosen} />
123
+ </div>
124
+ );
125
+ }
@@ -10,6 +10,7 @@ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shar
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
12
  import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
+ import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
13
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
14
15
 
15
16
  export type ContactsShellProps = Omit<AppShellProps, "active">;
@@ -44,6 +45,7 @@ export default function ContactsShell({
44
45
  impersonating,
45
46
  impersonationBaseUrl,
46
47
  trusted,
48
+ trustedRoles,
47
49
  pluginNav,
48
50
  children,
49
51
  }: PropsWithChildren<ContactsShellProps>) {
@@ -55,9 +57,18 @@ export default function ContactsShell({
55
57
  const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
56
58
  const [drawerOpen, setDrawerOpen] = useState(false);
57
59
 
60
+ // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
61
+ // effect, not during render, so the server render and the hydrating render agree.
62
+ const search = useLocationSearch();
63
+ const navigate = useNavigate();
58
64
  useEffect(() => {
59
- setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
60
- }, []);
65
+ setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
66
+ }, [search]);
67
+ // Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
68
+ // with the page - is closed here.
69
+ useEffect(() => {
70
+ setDrawerOpen(false);
71
+ }, [search]);
61
72
 
62
73
  useEffect(() => {
63
74
  if (!userUid) {
@@ -142,7 +153,7 @@ export default function ContactsShell({
142
153
  className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
143
154
  value={mailboxUid}
144
155
  onChange={(e) => {
145
- window.location.href = `/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`;
156
+ navigate(`/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`);
146
157
  }}
147
158
  >
148
159
  {mailboxes.map((mb) => (
@@ -191,6 +202,7 @@ export default function ContactsShell({
191
202
  impersonating={impersonating}
192
203
  impersonationBaseUrl={impersonationBaseUrl}
193
204
  trusted={trusted}
205
+ trustedRoles={trustedRoles}
194
206
  pluginNav={pluginNav}
195
207
  >
196
208
  {inner}