@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
@@ -1,129 +1,134 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useState } from "react";
6
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import { createMailSignature, listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
8
- import { clearPreviousDefaults } from "../signatureDefaults.js";
9
- import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
10
- import RichTextEditor from "../../../../shared/components/mail/compose/RichTextEditor.js";
11
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
- import Button from "@rapidmx/react-shared/components/buttons/Button.js";
13
- import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
14
-
15
- const INPUT_CLASS =
16
- "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
17
-
18
- export type NewSignaturePageProps = Omit<SettingsShellProps, "active">;
19
-
20
- export default function NewSignaturePage(props: NewSignaturePageProps) {
21
- return (
22
- <SettingsShell {...props} active="signatures">
23
- <NewSignatureForm />
24
- </SettingsShell>
25
- );
26
- }
27
-
28
- /** Signatures aren't backed by a draft/attachment record the way Compose's own inline images are (see
29
- * `ComposeWindow.handleUploadImage()`) — inline images in a signature are out of scope for this phase
30
- * (`.claude/NOTES.md`'s Phase 12 entry), so this is a real, deliberate no-op, not a stub left unfinished. */
31
- async function handleUploadImage(): Promise<string | null> {
32
- return null;
33
- }
34
-
35
- function NewSignatureForm() {
36
- const { mailboxUid } = useSettingsShell();
37
- const [name, setName] = useState("");
38
- const [contentHtml, setContentHtml] = useState("");
39
- const [isDefaultForNewMessages, setIsDefaultForNewMessages] = useState(false);
40
- const [isDefaultForReplyForward, setIsDefaultForReplyForward] = useState(false);
41
- const [error, setError] = useState<string | null>(null);
42
- const [saving, setSaving] = useState(false);
43
-
44
- async function handleSubmit(e: FormEvent) {
45
- e.preventDefault();
46
- setError(null);
47
-
48
- if (!name.trim()) {
49
- setError("A name is required.");
50
- return;
51
- }
52
-
53
- setSaving(true);
54
- try {
55
- if (isDefaultForNewMessages || isDefaultForReplyForward) {
56
- const existing = await listMailSignatures(mailboxUid!, { limit: 100 });
57
- await clearPreviousDefaults(existing, undefined, isDefaultForNewMessages, isDefaultForReplyForward);
58
- }
59
- const created = await createMailSignature({
60
- mailboxUid: mailboxUid!,
61
- name: name.trim(),
62
- contentHtml,
63
- isDefaultForNewMessages,
64
- isDefaultForReplyForward,
65
- });
66
- window.location.href = `/settings/signatures/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`;
67
- } catch (err) {
68
- setError(err instanceof ApiRequestError ? err.message : "Could not create the signature.");
69
- } finally {
70
- setSaving(false);
71
- }
72
- }
73
-
74
- return (
75
- <div className="flex-1 min-w-0 overflow-y-auto p-6">
76
- <div className="max-w-3xl">
77
- <h1 className="text-lg font-bold tracking-tight mb-5">New signature</h1>
78
-
79
- {error && <Alert>{error}</Alert>}
80
-
81
- <form onSubmit={handleSubmit} className="flex flex-col gap-5">
82
- <div className="bg-surface border border-border rounded-md p-6 flex flex-col gap-4">
83
- <FormField label="Name" htmlFor="name">
84
- <input
85
- id="name"
86
- type="text"
87
- className={INPUT_CLASS}
88
- value={name}
89
- onChange={(e) => setName(e.target.value)}
90
- placeholder="Work signature"
91
- />
92
- </FormField>
93
- <label className="flex items-center gap-2 text-sm">
94
- <input
95
- type="checkbox"
96
- checked={isDefaultForNewMessages}
97
- onChange={(e) => setIsDefaultForNewMessages(e.target.checked)}
98
- />
99
- Use for new messages
100
- </label>
101
- <label className="flex items-center gap-2 text-sm">
102
- <input
103
- type="checkbox"
104
- checked={isDefaultForReplyForward}
105
- onChange={(e) => setIsDefaultForReplyForward(e.target.checked)}
106
- />
107
- Use for replies and forwards
108
- </label>
109
- </div>
110
-
111
- <div className="h-64">
112
- <RichTextEditor value={contentHtml} onChange={setContentHtml} fill onUploadImage={handleUploadImage} />
113
- </div>
114
-
115
- <div className="flex gap-3">
116
- <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
117
- Create signature
118
- </Button>
119
- <a href={`/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid!)}`}>
120
- <Button type="button" variant="secondary" className="!w-auto">
121
- Cancel
122
- </Button>
123
- </a>
124
- </div>
125
- </form>
126
- </div>
127
- </div>
128
- );
129
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { routedPage } from "../../../_routedPage.js";
6
+ import { useNavigate } from "../../../../shared/navigation/AppRouter.js";
7
+ import React, { FormEvent, useState } from "react";
8
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
+ import { createMailSignature, listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
10
+ import { clearPreviousDefaults } from "../signatureDefaults.js";
11
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
12
+ import RichTextEditor from "../../../../shared/components/mail/compose/RichTextEditor.js";
13
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
15
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
16
+
17
+ const INPUT_CLASS =
18
+ "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
19
+
20
+ export type NewSignaturePageProps = Omit<SettingsShellProps, "active">;
21
+
22
+ function NewSignaturePage(props: NewSignaturePageProps) {
23
+ return (
24
+ <SettingsShell {...props} active="signatures">
25
+ <NewSignatureForm />
26
+ </SettingsShell>
27
+ );
28
+ }
29
+
30
+ /** Signatures aren't backed by a draft/attachment record the way Compose's own inline images are (see
31
+ * `ComposeWindow.handleUploadImage()`) — inline images in a signature are out of scope for this phase
32
+ * (`.claude/NOTES.md`'s Phase 12 entry), so this is a real, deliberate no-op, not a stub left unfinished. */
33
+ async function handleUploadImage(): Promise<string | null> {
34
+ return null;
35
+ }
36
+
37
+ function NewSignatureForm() {
38
+ const navigate = useNavigate();
39
+ const { mailboxUid } = useSettingsShell();
40
+ const [name, setName] = useState("");
41
+ const [contentHtml, setContentHtml] = useState("");
42
+ const [isDefaultForNewMessages, setIsDefaultForNewMessages] = useState(false);
43
+ const [isDefaultForReplyForward, setIsDefaultForReplyForward] = useState(false);
44
+ const [error, setError] = useState<string | null>(null);
45
+ const [saving, setSaving] = useState(false);
46
+
47
+ async function handleSubmit(e: FormEvent) {
48
+ e.preventDefault();
49
+ setError(null);
50
+
51
+ if (!name.trim()) {
52
+ setError("A name is required.");
53
+ return;
54
+ }
55
+
56
+ setSaving(true);
57
+ try {
58
+ if (isDefaultForNewMessages || isDefaultForReplyForward) {
59
+ const existing = await listMailSignatures(mailboxUid!, { limit: 100 });
60
+ await clearPreviousDefaults(existing, undefined, isDefaultForNewMessages, isDefaultForReplyForward);
61
+ }
62
+ const created = await createMailSignature({
63
+ mailboxUid: mailboxUid!,
64
+ name: name.trim(),
65
+ contentHtml,
66
+ isDefaultForNewMessages,
67
+ isDefaultForReplyForward,
68
+ });
69
+ navigate(`/settings/signatures/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`);
70
+ } catch (err) {
71
+ setError(err instanceof ApiRequestError ? err.message : "Could not create the signature.");
72
+ } finally {
73
+ setSaving(false);
74
+ }
75
+ }
76
+
77
+ return (
78
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
79
+ <div className="max-w-3xl">
80
+ <h1 className="text-lg font-bold tracking-tight mb-5">New signature</h1>
81
+
82
+ {error && <Alert>{error}</Alert>}
83
+
84
+ <form onSubmit={handleSubmit} className="flex flex-col gap-5">
85
+ <div className="bg-surface border border-border rounded-md p-6 flex flex-col gap-4">
86
+ <FormField label="Name" htmlFor="name">
87
+ <input
88
+ id="name"
89
+ type="text"
90
+ className={INPUT_CLASS}
91
+ value={name}
92
+ onChange={(e) => setName(e.target.value)}
93
+ placeholder="Work signature"
94
+ />
95
+ </FormField>
96
+ <label className="flex items-center gap-2 text-sm">
97
+ <input
98
+ type="checkbox"
99
+ checked={isDefaultForNewMessages}
100
+ onChange={(e) => setIsDefaultForNewMessages(e.target.checked)}
101
+ />
102
+ Use for new messages
103
+ </label>
104
+ <label className="flex items-center gap-2 text-sm">
105
+ <input
106
+ type="checkbox"
107
+ checked={isDefaultForReplyForward}
108
+ onChange={(e) => setIsDefaultForReplyForward(e.target.checked)}
109
+ />
110
+ Use for replies and forwards
111
+ </label>
112
+ </div>
113
+
114
+ <div className="h-64">
115
+ <RichTextEditor value={contentHtml} onChange={setContentHtml} fill onUploadImage={handleUploadImage} />
116
+ </div>
117
+
118
+ <div className="flex gap-3">
119
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
120
+ Create signature
121
+ </Button>
122
+ <a href={`/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid!)}`}>
123
+ <Button type="button" variant="secondary" className="!w-auto">
124
+ Cancel
125
+ </Button>
126
+ </a>
127
+ </div>
128
+ </form>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export default routedPage("/settings/signatures/new", NewSignaturePage);
@@ -2,7 +2,8 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useEffect, useMemo, useState } from "react";
5
+ import { routedPage } from "../_routedPage.js";
6
+ import React, { FormEvent, useEffect, useMemo, useRef, useState } from "react";
6
7
  import { endOfWeek, isAfter, isBefore, isToday, parseISO, startOfDay } from "date-fns";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import {
@@ -26,11 +27,14 @@ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
26
27
  import { findWellKnownFolderUid } from "../../shared/mail/findWellKnownFolderUid.js";
27
28
  import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
28
29
  import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
30
+ import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
31
+ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
32
+ import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
29
33
 
30
34
  const INPUT_CLASS =
31
35
  "text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
32
36
 
33
- export default function TasksPage(props: TasksShellProps) {
37
+ function TasksPage(props: TasksShellProps) {
34
38
  return (
35
39
  <TasksShell {...props}>
36
40
  <TasksContent />
@@ -103,6 +107,14 @@ function TasksContent() {
103
107
  // checked task (the toolbar's Delete).
104
108
  const [pendingDelete, setPendingDelete] = useState<{ type: "single"; task: Task } | { type: "bulk" } | null>(null);
105
109
 
110
+ // "New task" from the keyboard: a task is made in the quick-add form, so this moves the caret to its title field (which the Flagged view,
111
+ // a list of flagged mail rather than tasks, doesn't have). A dialog open - the delete confirmation - silences it.
112
+ const addTaskRef = useRef<HTMLInputElement | null>(null);
113
+ const canAddTask = view.type !== "flagged";
114
+ // Registered only while the form is rendered (`canAddTask`), so its field is there.
115
+ useShortcut(SHORTCUTS.tasks.create, () => addTaskRef.current!.focus(), { enabled: canAddTask });
116
+ const addTaskHint = useShortcutProps("Add a task", SHORTCUTS.tasks.create, canAddTask);
117
+
106
118
  function reload(): Promise<void> {
107
119
  if (!folderUid) {
108
120
  setTasks([]);
@@ -344,8 +356,10 @@ function TasksContent() {
344
356
  <>
345
357
  <form onSubmit={handleCreate} className="flex items-center gap-2 bg-surface border border-border rounded-md p-2">
346
358
  <input
359
+ ref={addTaskRef}
347
360
  type="text"
348
361
  aria-label="Add a task"
362
+ {...addTaskHint}
349
363
  className={`${INPUT_CLASS} flex-1`}
350
364
  value={title}
351
365
  onChange={(e) => setTitle(e.target.value)}
@@ -636,3 +650,5 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }:
636
650
  </div>
637
651
  );
638
652
  }
653
+
654
+ export default routedPage("/tasks", TasksPage);
@@ -0,0 +1,2 @@
1
+ /** auth-server's account page, or `undefined` when no auth-server is configured. Tolerates a configured URL with a trailing slash. */
2
+ export declare function accountUrlOf(authServerUrl: string | undefined): string | undefined;
@@ -0,0 +1,8 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /** auth-server's account page, or `undefined` when no auth-server is configured. Tolerates a configured URL with a trailing slash. */
6
+ export function accountUrlOf(authServerUrl) {
7
+ return authServerUrl ? `${authServerUrl.replace(/\/+$/, "")}/account` : undefined;
8
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Whether the signed-in user holds an administrator role, learned without elevating.
3
+ *
4
+ * The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
5
+ * (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
6
+ * their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
7
+ * always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
8
+ * so the answer is read from there, with the same cross-origin credentials the profile lookups use.
9
+ *
10
+ * This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
11
+ * the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
12
+ * - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
13
+ */
14
+ /** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
15
+ export declare const DEFAULT_TRUSTED_ROLES: readonly string[];
16
+ /** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
17
+ * tab never inherits it. */
18
+ export declare const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
19
+ /** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
20
+ * that a role granted or removed shows up within the sitting. */
21
+ export declare const ADMIN_ACCESS_TTL_MS: number;
22
+ /** Forgets the lookups in flight (tests). */
23
+ export declare function resetAdminAccessLookups(): void;
24
+ /**
25
+ * Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
26
+ * doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
27
+ * body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
28
+ * Never rejects.
29
+ */
30
+ export declare function lookUpAdminAccess(authServerUrl: string, userUid: string, trustedRoles?: readonly string[], now?: number): Promise<boolean | undefined>;
@@ -0,0 +1,89 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
6
+ /**
7
+ * Whether the signed-in user holds an administrator role, learned without elevating.
8
+ *
9
+ * The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
10
+ * (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
11
+ * their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
12
+ * always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
13
+ * so the answer is read from there, with the same cross-origin credentials the profile lookups use.
14
+ *
15
+ * This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
16
+ * the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
17
+ * - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
18
+ */
19
+ /** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
20
+ export const DEFAULT_TRUSTED_ROLES = ["admin"];
21
+ /** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
22
+ * tab never inherits it. */
23
+ export const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
24
+ /** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
25
+ * that a role granted or removed shows up within the sitting. */
26
+ export const ADMIN_ACCESS_TTL_MS = 30 * 60 * 1000;
27
+ /** Lookups in flight, by key, so two components asking at once make one request. */
28
+ const lookups = new Map();
29
+ /** Forgets the lookups in flight (tests). */
30
+ export function resetAdminAccessLookups() {
31
+ lookups.clear();
32
+ }
33
+ function readRemembered(key, now) {
34
+ try {
35
+ const stored = JSON.parse(sessionStorage.getItem(key) ?? "null");
36
+ if (typeof stored?.admin === "boolean" && typeof stored.at === "number" && now - stored.at >= 0 && now - stored.at < ADMIN_ACCESS_TTL_MS) {
37
+ return stored.admin;
38
+ }
39
+ }
40
+ catch {
41
+ // Unreadable, or storage unavailable - ask again.
42
+ }
43
+ return undefined;
44
+ }
45
+ function remember(key, admin, now) {
46
+ try {
47
+ sessionStorage.setItem(key, JSON.stringify({ admin, at: now }));
48
+ }
49
+ catch {
50
+ // Storage unavailable - the next page load asks again.
51
+ }
52
+ }
53
+ /**
54
+ * Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
55
+ * doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
56
+ * body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
57
+ * Never rejects.
58
+ */
59
+ export function lookUpAdminAccess(authServerUrl, userUid, trustedRoles = DEFAULT_TRUSTED_ROLES, now = Date.now()) {
60
+ const key = `${ADMIN_ACCESS_KEY_PREFIX}${userUid}`;
61
+ const remembered = readRemembered(key, now);
62
+ if (remembered !== undefined) {
63
+ return Promise.resolve(remembered);
64
+ }
65
+ const inFlight = lookups.get(key);
66
+ if (inFlight) {
67
+ return inFlight;
68
+ }
69
+ const lookup = (async () => {
70
+ try {
71
+ const user = await authApiFetch(authServerUrl, "/users/me");
72
+ if (!user || user.uid !== userUid || !Array.isArray(user.roles)) {
73
+ return undefined;
74
+ }
75
+ const admin = user.roles.some((role) => typeof role === "string" && trustedRoles.includes(role));
76
+ remember(key, admin, now);
77
+ return admin;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ finally {
83
+ // Answered: what was learned is in storage; a failure may be asked again.
84
+ lookups.delete(key);
85
+ }
86
+ })();
87
+ lookups.set(key, lookup);
88
+ return lookup;
89
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The `ApiRequestError.code` a `@RequiresElevation()` endpoint answers a caller whose token isn't elevated with
3
+ * (a 403 - `ApiErrors.AUTH_REQUIRES_ELEVATION` in `@rapidrest/service-core`). Distinct from `api-103` (the caller is
4
+ * elevated but lacks the trusted role), which no amount of elevating fixes.
5
+ */
6
+ export declare const ELEVATION_REQUIRED_CODE = "api-104";
7
+ /** `sessionStorage` key holding the time (`Date.now()`) the browser was last sent to auth-server to elevate. */
8
+ export declare const ELEVATION_ATTEMPT_KEY = "rapidmx-admin-elevation-attempt";
9
+ /** How long after sending the browser to elevate a second `api-104` is taken to mean the elevation didn't take
10
+ * effect (e.g. auth-server's elevated cookie never reaching this origin), rather than a fresh need to elevate. */
11
+ export declare const ELEVATION_RETRY_WINDOW_MS: number;
12
+ /** Whether `err` is a 403 from an elevation-gated endpoint for a caller whose token isn't elevated. */
13
+ export declare function isElevationRequired(err: unknown): boolean;
14
+ /** auth-server's elevation page: it sends the browser back to `returnTo` once the user has confirmed their
15
+ * identity, or to its own account page if they cancel. */
16
+ export declare function elevationUrl(authServerUrl: string, returnTo: string): string;
17
+ /** Whether the browser was sent to elevate within `ELEVATION_RETRY_WINDOW_MS` of `now`. `false` when nothing was
18
+ * recorded, the record is unreadable or from the future, or storage is unavailable. */
19
+ export declare function elevationAttemptedRecently(now?: number): boolean;
20
+ /** Remembers, for this tab, that the browser is about to be sent to elevate. A no-op where storage is unavailable
21
+ * (then only the first-round protection is lost - elevating needs the user to confirm each time round). */
22
+ export declare function recordElevationAttempt(now?: number): void;
23
+ /** Forgets any recorded attempt - once elevation has worked, or when the user asks to try again. */
24
+ export declare function clearElevationAttempt(): void;
@@ -0,0 +1,56 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
6
+ /**
7
+ * The `ApiRequestError.code` a `@RequiresElevation()` endpoint answers a caller whose token isn't elevated with
8
+ * (a 403 - `ApiErrors.AUTH_REQUIRES_ELEVATION` in `@rapidrest/service-core`). Distinct from `api-103` (the caller is
9
+ * elevated but lacks the trusted role), which no amount of elevating fixes.
10
+ */
11
+ export const ELEVATION_REQUIRED_CODE = "api-104";
12
+ /** `sessionStorage` key holding the time (`Date.now()`) the browser was last sent to auth-server to elevate. */
13
+ export const ELEVATION_ATTEMPT_KEY = "rapidmx-admin-elevation-attempt";
14
+ /** How long after sending the browser to elevate a second `api-104` is taken to mean the elevation didn't take
15
+ * effect (e.g. auth-server's elevated cookie never reaching this origin), rather than a fresh need to elevate. */
16
+ export const ELEVATION_RETRY_WINDOW_MS = 2 * 60 * 1000;
17
+ /** Whether `err` is a 403 from an elevation-gated endpoint for a caller whose token isn't elevated. */
18
+ export function isElevationRequired(err) {
19
+ return err instanceof ApiRequestError && err.status === 403 && err.code === ELEVATION_REQUIRED_CODE;
20
+ }
21
+ /** auth-server's elevation page: it sends the browser back to `returnTo` once the user has confirmed their
22
+ * identity, or to its own account page if they cancel. */
23
+ export function elevationUrl(authServerUrl, returnTo) {
24
+ return `${authServerUrl}/auth/elevate?return_to=${encodeURIComponent(returnTo)}`;
25
+ }
26
+ /** Whether the browser was sent to elevate within `ELEVATION_RETRY_WINDOW_MS` of `now`. `false` when nothing was
27
+ * recorded, the record is unreadable or from the future, or storage is unavailable. */
28
+ export function elevationAttemptedRecently(now = Date.now()) {
29
+ try {
30
+ const recorded = Number(sessionStorage.getItem(ELEVATION_ATTEMPT_KEY));
31
+ const elapsed = now - recorded;
32
+ return recorded > 0 && elapsed >= 0 && elapsed < ELEVATION_RETRY_WINDOW_MS;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** Remembers, for this tab, that the browser is about to be sent to elevate. A no-op where storage is unavailable
39
+ * (then only the first-round protection is lost - elevating needs the user to confirm each time round). */
40
+ export function recordElevationAttempt(now = Date.now()) {
41
+ try {
42
+ sessionStorage.setItem(ELEVATION_ATTEMPT_KEY, String(now));
43
+ }
44
+ catch {
45
+ // See the doc comment.
46
+ }
47
+ }
48
+ /** Forgets any recorded attempt - once elevation has worked, or when the user asks to try again. */
49
+ export function clearElevationAttempt() {
50
+ try {
51
+ sessionStorage.removeItem(ELEVATION_ATTEMPT_KEY);
52
+ }
53
+ catch {
54
+ // Nothing was recorded.
55
+ }
56
+ }
@@ -24,9 +24,17 @@ export interface AdminShellProps extends PluginNavProps {
24
24
  * off-rail sections' - see `mergePluginNavItems`. */
25
25
  export declare function adminNavItems(pluginNav?: PluginNav): NavItem[];
26
26
  /**
27
- * Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
28
- * `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a canary: a 200 means the
29
- * caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
30
- * (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
27
+ * Gates every `apps/admin` page behind the `admin` trusted role, and behind an elevated session. Uses
28
+ * `GET /api/admin/release-notes` (any `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a
29
+ * canary. The endpoint is class-level `@RequiresElevation()`, checked *before* the trusted-role check, so a 200 means the
30
+ * caller's JWT is elevated and carries a trusted role: show the console. Otherwise:
31
+ *
32
+ * 403 `api-104` means the JWT isn't elevated (an administrator's normal sign-in). There is no local step-up form; the
33
+ * browser is sent to auth-server's `/auth/elevate?return_to=<this page>`, which returns it here once the user has
34
+ * confirmed their identity (or to its own account page if they cancel). Sent at most once per
35
+ * `ELEVATION_RETRY_WINDOW_MS`, so an elevated cookie that never reaches this origin can't bounce the browser back
36
+ * and forth - see `elevation.ts`, and the "didn't take effect" alert with its own "Try again" below.
37
+ *
38
+ * 403 `api-103` (elevated, but not an administrator), any other 403, and 401 mean "no administrator access".
31
39
  */
32
40
  export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }: PropsWithChildren<AdminShellProps>): React.JSX.Element;