@remit/web-client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/harness/build.mjs +84 -0
  2. package/harness/index.html +37 -0
  3. package/harness/vite-preset.ts +29 -0
  4. package/index.html +47 -0
  5. package/package.json +147 -0
  6. package/public/config.js +5 -0
  7. package/public/locales/de/common.json +1 -0
  8. package/public/locales/de/errors.json +1 -0
  9. package/public/locales/de/mail.json +13 -0
  10. package/public/locales/de/settings.json +1 -0
  11. package/public/locales/en/common.json +26 -0
  12. package/public/locales/en/errors.json +1 -0
  13. package/public/locales/en/mail.json +69 -0
  14. package/public/locales/en/settings.json +1 -0
  15. package/public/locales/fr/common.json +1 -0
  16. package/public/locales/fr/errors.json +1 -0
  17. package/public/locales/fr/mail.json +13 -0
  18. package/public/locales/fr/settings.json +1 -0
  19. package/public/locales/nl/common.json +1 -0
  20. package/public/locales/nl/errors.json +1 -0
  21. package/public/locales/nl/mail.json +13 -0
  22. package/public/locales/nl/settings.json +1 -0
  23. package/src/auth/AccountMenu.tsx +58 -0
  24. package/src/auth/AccountSession.tsx +19 -0
  25. package/src/auth/BetterAuthShell.tsx +198 -0
  26. package/src/auth/account-menu-mode.test.ts +64 -0
  27. package/src/auth/account-menu-mode.ts +28 -0
  28. package/src/auth/amplify-config.test.ts +158 -0
  29. package/src/auth/amplify-config.ts +60 -0
  30. package/src/auth/auth-error.test.ts +109 -0
  31. package/src/auth/auth-error.ts +107 -0
  32. package/src/auth/auth-interceptor.test.ts +86 -0
  33. package/src/auth/auth-interceptor.ts +23 -0
  34. package/src/auth/auth.css +334 -0
  35. package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
  36. package/src/auth/better-auth-config.ts +85 -0
  37. package/src/auth/better-auth-provider.tsx +20 -0
  38. package/src/auth/cognito/CognitoAccount.tsx +48 -0
  39. package/src/auth/cognito/CognitoShell.tsx +266 -0
  40. package/src/auth/cognito/cognito-token.test.ts +110 -0
  41. package/src/auth/cognito/cognito-token.ts +47 -0
  42. package/src/auth/cognito/cognito.css +21 -0
  43. package/src/auth/cognito-provider.tsx +22 -0
  44. package/src/auth/combined-provider.tsx +42 -0
  45. package/src/auth/composition.test.ts +136 -0
  46. package/src/auth/provider.tsx +63 -0
  47. package/src/auth/sign-out-visibility.test.ts +40 -0
  48. package/src/auth/sign-out-visibility.ts +10 -0
  49. package/src/auth/sign-up-disabled.test.ts +23 -0
  50. package/src/components/compose/AddressField.tsx +237 -0
  51. package/src/components/compose/ComposeBody.tsx +36 -0
  52. package/src/components/compose/ComposeForm.tsx +676 -0
  53. package/src/components/compose/ComposeProvider.tsx +133 -0
  54. package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
  55. package/src/components/compose/FromSelector.tsx +67 -0
  56. package/src/components/compose/FullCompose.tsx +82 -0
  57. package/src/components/compose/InlineCompose.tsx +29 -0
  58. package/src/components/compose/MobileComposeSheet.tsx +152 -0
  59. package/src/components/compose/PlateEditor.tsx +86 -0
  60. package/src/components/compose/PlateToolbar.tsx +110 -0
  61. package/src/components/compose/SubjectField.tsx +19 -0
  62. package/src/components/compose/draft-discard-banner.test.ts +76 -0
  63. package/src/components/compose/index.ts +6 -0
  64. package/src/components/compose/sanitize-quote-html.ts +23 -0
  65. package/src/components/layout/AppShellSkeleton.tsx +53 -0
  66. package/src/components/layout/ComposeFab.tsx +86 -0
  67. package/src/components/layout/Drawer.tsx +105 -0
  68. package/src/components/mail/AutoMovedIndicator.tsx +48 -0
  69. package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
  70. package/src/components/mail/BlockedImagesNotice.tsx +41 -0
  71. package/src/components/mail/BriefPane.tsx +289 -0
  72. package/src/components/mail/ConversationView.tsx +462 -0
  73. package/src/components/mail/DailyBrief.tsx +451 -0
  74. package/src/components/mail/DraftsView.tsx +279 -0
  75. package/src/components/mail/FlaggedList.tsx +170 -0
  76. package/src/components/mail/FlaggedPane.tsx +217 -0
  77. package/src/components/mail/IntelligencePane.test.ts +123 -0
  78. package/src/components/mail/IntelligencePane.tsx +441 -0
  79. package/src/components/mail/MailListHeader.tsx +187 -0
  80. package/src/components/mail/MailNav.tsx +70 -0
  81. package/src/components/mail/MailSidebarAdapter.tsx +351 -0
  82. package/src/components/mail/MailViewChrome.tsx +95 -0
  83. package/src/components/mail/MailboxPane.tsx +1219 -0
  84. package/src/components/mail/MessageActionMenu.tsx +368 -0
  85. package/src/components/mail/MessageBody.tsx +207 -0
  86. package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
  87. package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
  88. package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
  89. package/src/components/mail/MessageCard.tsx +401 -0
  90. package/src/components/mail/MessageDetail.tsx +153 -0
  91. package/src/components/mail/MessageList.tsx +804 -0
  92. package/src/components/mail/MessageListItem.tsx +267 -0
  93. package/src/components/mail/MessageToolbar.render.test.ts +57 -0
  94. package/src/components/mail/MessageToolbar.tsx +166 -0
  95. package/src/components/mail/MobileMessageBar.tsx +142 -0
  96. package/src/components/mail/MoveToTrigger.tsx +250 -0
  97. package/src/components/mail/NavMenuButton.tsx +25 -0
  98. package/src/components/mail/OutboxPane.tsx +531 -0
  99. package/src/components/mail/PullToRefresh.tsx +24 -0
  100. package/src/components/mail/RawMessageView.tsx +84 -0
  101. package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
  102. package/src/components/mail/SelectionToolbar.tsx +139 -0
  103. package/src/components/mail/SpamRescue.render.test.ts +35 -0
  104. package/src/components/mail/SpamRescue.tsx +138 -0
  105. package/src/components/mail/SwipeableMessageRow.tsx +148 -0
  106. package/src/components/mail/message-body-error-banner-content.ts +87 -0
  107. package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
  108. package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
  109. package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
  110. package/src/components/mail/organize/OrganizePanel.tsx +344 -0
  111. package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
  112. package/src/components/settings/AccountFormPanel.test.ts +72 -0
  113. package/src/components/settings/AccountFormPanel.tsx +942 -0
  114. package/src/components/settings/DangerZone.tsx +30 -0
  115. package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
  116. package/src/components/settings/DeleteAccountDialog.tsx +224 -0
  117. package/src/components/settings/FiltersList.render.test.ts +71 -0
  118. package/src/components/settings/FiltersList.tsx +93 -0
  119. package/src/components/settings/account-form-helpers.test.ts +172 -0
  120. package/src/components/settings/account-form-helpers.ts +105 -0
  121. package/src/components/ui/AppVersion.tsx +50 -0
  122. package/src/components/ui/BugReportButton.tsx +45 -0
  123. package/src/components/ui/ConfirmDialog.tsx +125 -0
  124. package/src/components/ui/DropdownMenu.tsx +105 -0
  125. package/src/components/ui/EmptyState.tsx +11 -0
  126. package/src/components/ui/ErrorBanner.tsx +88 -0
  127. package/src/components/ui/ErrorBannerProvider.tsx +77 -0
  128. package/src/components/ui/ErrorBannerStack.tsx +34 -0
  129. package/src/components/ui/ErrorState.tsx +80 -0
  130. package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
  131. package/src/components/ui/FatalErrorOverlay.tsx +151 -0
  132. package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
  133. package/src/components/ui/SlidePanel.tsx +72 -0
  134. package/src/components/ui/error-banners.test.ts +207 -0
  135. package/src/components/ui/error-banners.ts +111 -0
  136. package/src/hooks/queries/keys.ts +38 -0
  137. package/src/hooks/useArchiveMailbox.ts +104 -0
  138. package/src/hooks/useAutoMovedBadge.ts +77 -0
  139. package/src/hooks/useCurrentMailboxName.ts +99 -0
  140. package/src/hooks/useDebouncedValue.ts +12 -0
  141. package/src/hooks/useDeleteMessages.test.ts +103 -0
  142. package/src/hooks/useDeleteMessages.ts +226 -0
  143. package/src/hooks/useFilters.ts +112 -0
  144. package/src/hooks/useIntelligenceData.test.ts +269 -0
  145. package/src/hooks/useIntelligenceData.ts +284 -0
  146. package/src/hooks/useIsDark.ts +27 -0
  147. package/src/hooks/useKeyboardNavigation.ts +192 -0
  148. package/src/hooks/useLayoutTier.test.ts +47 -0
  149. package/src/hooks/useLayoutTier.ts +44 -0
  150. package/src/hooks/useLongPress.test.ts +291 -0
  151. package/src/hooks/useLongPress.ts +87 -0
  152. package/src/hooks/useMailboxAccount.ts +62 -0
  153. package/src/hooks/useMailboxNameIndex.ts +32 -0
  154. package/src/hooks/useMarkAsRead.test.ts +165 -0
  155. package/src/hooks/useMarkAsRead.ts +319 -0
  156. package/src/hooks/useMediaQuery.ts +39 -0
  157. package/src/hooks/useMessageBodyContent.test.ts +370 -0
  158. package/src/hooks/useMessageBodyContent.ts +380 -0
  159. package/src/hooks/useMoveMessages.test.ts +63 -0
  160. package/src/hooks/useMoveMessages.ts +221 -0
  161. package/src/hooks/useOrganizeJob.ts +90 -0
  162. package/src/hooks/useOrganizePreview.ts +40 -0
  163. package/src/hooks/useRescueCandidates.test.ts +217 -0
  164. package/src/hooks/useRescueCandidates.ts +48 -0
  165. package/src/hooks/useSaveDraft.ts +118 -0
  166. package/src/hooks/useSelection.test.ts +75 -0
  167. package/src/hooks/useSelection.ts +211 -0
  168. package/src/hooks/useSemanticSearch.ts +94 -0
  169. package/src/hooks/useSignature.ts +69 -0
  170. package/src/hooks/useStaleAccountSync.test.ts +211 -0
  171. package/src/hooks/useStaleAccountSync.ts +246 -0
  172. package/src/hooks/useSwipeNavigation.test.ts +35 -0
  173. package/src/hooks/useSwipeNavigation.ts +99 -0
  174. package/src/hooks/useToggleStar.ts +187 -0
  175. package/src/hooks/useToggleTrusted.test.ts +102 -0
  176. package/src/hooks/useToggleTrusted.ts +161 -0
  177. package/src/hooks/useTriageKeyboard.ts +103 -0
  178. package/src/hooks/useTriggerSync.test.ts +24 -0
  179. package/src/hooks/useTriggerSync.ts +58 -0
  180. package/src/hooks/useUpdateAddressFlags.ts +125 -0
  181. package/src/hooks/useVisualViewport.test.ts +127 -0
  182. package/src/hooks/useVisualViewport.ts +51 -0
  183. package/src/index.css +19 -0
  184. package/src/lib/account-order.test.ts +84 -0
  185. package/src/lib/account-order.ts +13 -0
  186. package/src/lib/adjacent-message.test.ts +43 -0
  187. package/src/lib/adjacent-message.ts +20 -0
  188. package/src/lib/api.ts +82 -0
  189. package/src/lib/app-info.ts +15 -0
  190. package/src/lib/auto-moved.test.ts +140 -0
  191. package/src/lib/auto-moved.ts +68 -0
  192. package/src/lib/autodiscovery.test.ts +94 -0
  193. package/src/lib/autodiscovery.ts +354 -0
  194. package/src/lib/brief.test.ts +366 -0
  195. package/src/lib/brief.ts +180 -0
  196. package/src/lib/bug-report.test.ts +213 -0
  197. package/src/lib/bug-report.ts +176 -0
  198. package/src/lib/client.ts +36 -0
  199. package/src/lib/console-errors.test.ts +64 -0
  200. package/src/lib/console-errors.ts +92 -0
  201. package/src/lib/conversation-target.test.ts +68 -0
  202. package/src/lib/conversation-target.ts +59 -0
  203. package/src/lib/display-category.ts +20 -0
  204. package/src/lib/drafts.test.ts +241 -0
  205. package/src/lib/drafts.ts +121 -0
  206. package/src/lib/error-classifier.test.ts +133 -0
  207. package/src/lib/error-classifier.ts +96 -0
  208. package/src/lib/fatal-error.test.ts +97 -0
  209. package/src/lib/fatal-error.ts +165 -0
  210. package/src/lib/folder-roles.test.ts +113 -0
  211. package/src/lib/folder-roles.ts +112 -0
  212. package/src/lib/format.test.ts +115 -0
  213. package/src/lib/format.ts +198 -0
  214. package/src/lib/i18n.ts +36 -0
  215. package/src/lib/intelligence-pref.test.ts +57 -0
  216. package/src/lib/intelligence-pref.ts +25 -0
  217. package/src/lib/keymap-dispatch.test.ts +178 -0
  218. package/src/lib/keymap-dispatch.ts +187 -0
  219. package/src/lib/keymap.test.ts +49 -0
  220. package/src/lib/keymap.ts +178 -0
  221. package/src/lib/mail-context.ts +64 -0
  222. package/src/lib/mail-route.test.ts +80 -0
  223. package/src/lib/mail-route.ts +44 -0
  224. package/src/lib/message-body-source.test.ts +98 -0
  225. package/src/lib/message-body-source.ts +48 -0
  226. package/src/lib/move-targets.test.ts +195 -0
  227. package/src/lib/move-targets.ts +74 -0
  228. package/src/lib/onboarding-completion.test.ts +64 -0
  229. package/src/lib/onboarding-completion.ts +23 -0
  230. package/src/lib/organize/filter-status.test.ts +99 -0
  231. package/src/lib/organize/filter-status.ts +86 -0
  232. package/src/lib/organize/organize-copy.test.ts +93 -0
  233. package/src/lib/organize/organize-copy.ts +61 -0
  234. package/src/lib/organize/organize-model.test.ts +104 -0
  235. package/src/lib/organize/organize-model.ts +105 -0
  236. package/src/lib/organize/organize-poll.test.ts +37 -0
  237. package/src/lib/organize/organize-poll.ts +28 -0
  238. package/src/lib/outbox-status.test.ts +64 -0
  239. package/src/lib/outbox-status.ts +75 -0
  240. package/src/lib/plate-serializer.ts +164 -0
  241. package/src/lib/provider-presets.test.ts +96 -0
  242. package/src/lib/provider-presets.ts +81 -0
  243. package/src/lib/query-error-handler.test.ts +144 -0
  244. package/src/lib/query-error-handler.ts +34 -0
  245. package/src/lib/query-escalation.integration.test.ts +138 -0
  246. package/src/lib/recent-searches.test.ts +53 -0
  247. package/src/lib/recent-searches.ts +34 -0
  248. package/src/lib/rescue-candidates.test.ts +94 -0
  249. package/src/lib/rescue-candidates.ts +35 -0
  250. package/src/lib/rescue-telemetry.test.ts +75 -0
  251. package/src/lib/rescue-telemetry.ts +55 -0
  252. package/src/lib/rum-adapter.test.ts +299 -0
  253. package/src/lib/rum-adapter.ts +107 -0
  254. package/src/lib/saved-searches.test.ts +80 -0
  255. package/src/lib/saved-searches.ts +52 -0
  256. package/src/lib/search-pending.test.ts +61 -0
  257. package/src/lib/search-pending.ts +24 -0
  258. package/src/lib/search-query.test.ts +35 -0
  259. package/src/lib/search-query.ts +11 -0
  260. package/src/lib/search-result.test.ts +160 -0
  261. package/src/lib/search-result.ts +102 -0
  262. package/src/lib/search-schemas.ts +34 -0
  263. package/src/lib/search-token-index.test.ts +112 -0
  264. package/src/lib/search-token-index.ts +56 -0
  265. package/src/lib/search-tokens.test.ts +218 -0
  266. package/src/lib/search-tokens.ts +133 -0
  267. package/src/lib/telemetry-context.tsx +9 -0
  268. package/src/lib/telemetry.ts +17 -0
  269. package/src/lib/theme-preference.ts +46 -0
  270. package/src/lib/theme.ts +33 -0
  271. package/src/lib/utils.ts +4 -0
  272. package/src/main.tsx +10 -0
  273. package/src/routeTree.gen.ts +392 -0
  274. package/src/router.test.ts +81 -0
  275. package/src/router.tsx +31 -0
  276. package/src/routes/__root.tsx +69 -0
  277. package/src/routes/index.tsx +7 -0
  278. package/src/routes/mail/$mailboxId.tsx +27 -0
  279. package/src/routes/mail/flagged.tsx +37 -0
  280. package/src/routes/mail/index.tsx +41 -0
  281. package/src/routes/mail/outbox.tsx +13 -0
  282. package/src/routes/mail.tsx +413 -0
  283. package/src/routes/onboarding.tsx +35 -0
  284. package/src/routes/settings/-accounts.test.ts +65 -0
  285. package/src/routes/settings/accounts.tsx +542 -0
  286. package/src/routes/settings/advanced.tsx +55 -0
  287. package/src/routes/settings/appearance.tsx +140 -0
  288. package/src/routes/settings/filters.tsx +141 -0
  289. package/src/routes/settings/folders.tsx +216 -0
  290. package/src/routes/settings/index.tsx +5 -0
  291. package/src/routes/settings/senders.tsx +466 -0
  292. package/src/routes/settings/suggested-vips.tsx +10 -0
  293. package/src/routes/settings.tsx +66 -0
  294. package/src/runtime-config.ts +75 -0
  295. package/src/shell/index.tsx +98 -0
  296. package/src/types/index.ts +73 -0
  297. package/src/vite-env.d.ts +4 -0
  298. package/vite.base.ts +43 -0
@@ -0,0 +1,250 @@
1
+ import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
2
+ import { type MoveMailboxOption, MoveMailboxPicker } from "@remit/ui";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { FolderInput } from "lucide-react";
5
+ import {
6
+ useCallback,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import { useTranslation } from "react-i18next";
14
+ import { Drawer } from "vaul";
15
+ import { ErrorState } from "@/components/ui/ErrorState";
16
+ import { useFolderAppointments } from "@/hooks/useArchiveMailbox";
17
+ import { useIsDesktop } from "@/hooks/useMediaQuery";
18
+ import { buildMailboxRoleMap, labelForMailbox } from "@/lib/folder-roles";
19
+ import { buildMoveTargets } from "@/lib/move-targets";
20
+ import { cn } from "@/lib/utils";
21
+
22
+ interface MoveToTriggerProps {
23
+ accountId: string;
24
+ currentMailboxId: string;
25
+ onMove: (destinationMailboxId: string) => void;
26
+ disabled?: boolean;
27
+ /**
28
+ * When set, replaces the default `Move to folder` label with a hint and
29
+ * disables the trigger. Used by the bulk-action toolbar to surface the
30
+ * cross-account selection guard inline.
31
+ */
32
+ disabledHint?: string;
33
+ /**
34
+ * Render style for the trigger. `icon-only` renders a 44px icon button
35
+ * (mobile selection top bar / per-message overflow), `compact` renders
36
+ * a small label+icon button (desktop bulk toolbar).
37
+ */
38
+ variant?: "icon-only" | "compact";
39
+ /**
40
+ * Optional accessible label override for the trigger button.
41
+ */
42
+ label?: string;
43
+ }
44
+
45
+ const TRIGGER_BASE =
46
+ "inline-flex items-center justify-center transition-colors";
47
+
48
+ const ICON_TRIGGER = cn(
49
+ TRIGGER_BASE,
50
+ "min-h-11 min-w-11 rounded hover:bg-surface-raised",
51
+ );
52
+
53
+ const COMPACT_TRIGGER = cn(
54
+ TRIGGER_BASE,
55
+ "min-h-11 gap-1.5 px-3 rounded text-sm font-medium hover:bg-surface-raised",
56
+ );
57
+
58
+ export const MoveToTrigger = ({
59
+ accountId,
60
+ currentMailboxId,
61
+ onMove,
62
+ disabled = false,
63
+ disabledHint,
64
+ variant = "icon-only",
65
+ label,
66
+ }: MoveToTriggerProps) => {
67
+ const [isOpen, setIsOpen] = useState(false);
68
+ const isDesktop = useIsDesktop();
69
+ const containerRef = useRef<HTMLDivElement>(null);
70
+ const triggerLabel = label ?? "Move to folder";
71
+ const popoverId = useId();
72
+ const { t } = useTranslation("mail", { useSuspense: false });
73
+ // `labelForMailbox` expects a translator with a positional `(key,
74
+ // fallback)` shape; i18next's `t` treats the second argument as an options
75
+ // object — passing it raw breaks fallback behavior. Wrap it the same way
76
+ // the sidebar adapter does, memoized so it's a stable `useMemo` dependency.
77
+ const translator = useCallback(
78
+ (key: string, fallback: string): string =>
79
+ t(key, { defaultValue: fallback }),
80
+ [t],
81
+ );
82
+
83
+ const {
84
+ data: mailboxesResponse,
85
+ isLoading,
86
+ isError,
87
+ error,
88
+ refetch,
89
+ } = useQuery({
90
+ ...mailboxOperationsListMailboxesOptions({ path: { accountId } }),
91
+ // Same staleTime as the sidebar query — mailboxes change rarely and
92
+ // invalidations are explicit, so we share the cache entry rather than
93
+ // triggering a fresh fetch every time the picker opens.
94
+ staleTime: Infinity,
95
+ enabled: isOpen,
96
+ });
97
+ const folderAppointments = useFolderAppointments(accountId);
98
+
99
+ const options = useMemo<MoveMailboxOption[]>(() => {
100
+ const targets = buildMoveTargets(
101
+ mailboxesResponse?.items ?? [],
102
+ folderAppointments,
103
+ );
104
+ const roleMap = buildMailboxRoleMap(folderAppointments);
105
+ return targets.map((mailbox) => ({
106
+ id: mailbox.mailboxId,
107
+ label: labelForMailbox(
108
+ mailbox,
109
+ roleMap.get(mailbox.mailboxId),
110
+ translator,
111
+ ),
112
+ searchValue: mailbox.fullPath,
113
+ isCurrent: mailbox.mailboxId === currentMailboxId,
114
+ }));
115
+ }, [
116
+ mailboxesResponse?.items,
117
+ folderAppointments,
118
+ currentMailboxId,
119
+ translator,
120
+ ]);
121
+
122
+ const handleSelect = useCallback(
123
+ (destinationMailboxId: string) => {
124
+ setIsOpen(false);
125
+ onMove(destinationMailboxId);
126
+ },
127
+ [onMove],
128
+ );
129
+
130
+ // Desktop popover: dismiss on outside click + Escape.
131
+ useEffect(() => {
132
+ if (!isOpen || !isDesktop) return;
133
+ const handlePointer = (event: MouseEvent) => {
134
+ if (
135
+ containerRef.current &&
136
+ !containerRef.current.contains(event.target as Node)
137
+ ) {
138
+ setIsOpen(false);
139
+ }
140
+ };
141
+ const handleKey = (event: KeyboardEvent) => {
142
+ if (event.key === "Escape") setIsOpen(false);
143
+ };
144
+ document.addEventListener("mousedown", handlePointer);
145
+ document.addEventListener("keydown", handleKey);
146
+ return () => {
147
+ document.removeEventListener("mousedown", handlePointer);
148
+ document.removeEventListener("keydown", handleKey);
149
+ };
150
+ }, [isOpen, isDesktop]);
151
+
152
+ const isTriggerDisabled = disabled || !!disabledHint;
153
+
154
+ const TriggerButton = (
155
+ <button
156
+ type="button"
157
+ onClick={(event) => {
158
+ event.stopPropagation();
159
+ if (isTriggerDisabled) return;
160
+ setIsOpen((prev) => !prev);
161
+ }}
162
+ aria-label={triggerLabel}
163
+ // Mobile opens a vaul Drawer (modal dialog), desktop opens a
164
+ // non-modal popover whose only content is the listbox of
165
+ // destinations. Reflect each surface accurately so screen readers
166
+ // announce the right structure.
167
+ aria-haspopup={isDesktop ? "listbox" : "dialog"}
168
+ aria-expanded={isOpen}
169
+ aria-controls={isOpen ? popoverId : undefined}
170
+ title={disabledHint}
171
+ className={variant === "icon-only" ? ICON_TRIGGER : COMPACT_TRIGGER}
172
+ >
173
+ <FolderInput className="size-4" />
174
+ {variant === "compact" && <span className="hidden sm:inline">Move</span>}
175
+ </button>
176
+ );
177
+
178
+ const pickerBody = isLoading ? (
179
+ <div className="px-3 py-5 text-sm text-fg-muted">Loading folders…</div>
180
+ ) : isError ? (
181
+ <div className="p-3">
182
+ <ErrorState
183
+ variant="inline"
184
+ title="Couldn't load folders"
185
+ error={error}
186
+ onRetry={() => refetch()}
187
+ />
188
+ </div>
189
+ ) : (
190
+ <MoveMailboxPicker
191
+ mailboxes={options}
192
+ onSelect={handleSelect}
193
+ onCancel={() => setIsOpen(false)}
194
+ autoFocus={!isDesktop}
195
+ labels={{
196
+ searchPlaceholder: t("move_picker_placeholder", {
197
+ defaultValue: "Move to…",
198
+ }),
199
+ searchAriaLabel: t("move_picker_filter_label", {
200
+ defaultValue: "Filter folders",
201
+ }),
202
+ optionLabel: (folderLabel) => `Move to ${folderLabel}`,
203
+ currentSuffix: "(current folder)",
204
+ currentTag: "current",
205
+ emptyMessage: (query) => `No folders match "${query}"`,
206
+ }}
207
+ />
208
+ );
209
+
210
+ if (!isDesktop) {
211
+ return (
212
+ <>
213
+ {TriggerButton}
214
+ <Drawer.Root open={isOpen} onOpenChange={setIsOpen}>
215
+ <Drawer.Portal>
216
+ <Drawer.Overlay className="fixed inset-0 z-40 bg-black/40" />
217
+ <Drawer.Content
218
+ className="fixed inset-x-0 bottom-0 z-50 flex flex-col bg-canvas rounded-t-lg"
219
+ style={{ maxHeight: "85dvh" }}
220
+ id={popoverId}
221
+ >
222
+ <Drawer.Handle className="mx-auto mt-2 mb-1 h-1.5 w-12 rounded-full bg-fg-subtle/30" />
223
+ <Drawer.Title className="px-4 py-2 text-base font-semibold border-b border-line">
224
+ Move to folder
225
+ </Drawer.Title>
226
+ <div className="flex-1 overflow-hidden">{pickerBody}</div>
227
+ </Drawer.Content>
228
+ </Drawer.Portal>
229
+ </Drawer.Root>
230
+ </>
231
+ );
232
+ }
233
+
234
+ return (
235
+ <div ref={containerRef} className="relative inline-block">
236
+ {TriggerButton}
237
+ {isOpen && (
238
+ <div
239
+ id={popoverId}
240
+ className={cn(
241
+ "absolute right-0 mt-1 z-50 w-72 max-h-96 flex flex-col",
242
+ "bg-surface border border-line rounded-md shadow-lg",
243
+ )}
244
+ >
245
+ {pickerBody}
246
+ </div>
247
+ )}
248
+ </div>
249
+ );
250
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * NavMenuButton — the hamburger that opens the nav slide-over.
3
+ *
4
+ * The brief and inboxes get this from the shared `MailHeader`; the outbox and
5
+ * drafts render their own pane headers, so they mount this to keep the mobile
6
+ * nav reachable. It only appears when the nav is a slide-over (narrow widths);
7
+ * where the nav is a persistent pane, there is nothing to open.
8
+ */
9
+ import { Button, useAppShellLayout } from "@remit/ui";
10
+ import { Menu } from "lucide-react";
11
+
12
+ export function NavMenuButton() {
13
+ const layout = useAppShellLayout();
14
+ if (!layout || layout.showNavPane) return null;
15
+ return (
16
+ <Button
17
+ variant="ghost"
18
+ size="sm"
19
+ icon={<Menu className="size-4" />}
20
+ onClick={layout.openNav}
21
+ aria-label="Open folders"
22
+ className="-ml-1 shrink-0"
23
+ />
24
+ );
25
+ }