@remit/web-client 0.0.202 → 0.0.203

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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/components/compose/ComposeSmtpMissingBanner.tsx +3 -6
  3. package/src/components/layout/ComposeFab.tsx +3 -5
  4. package/src/components/layout/MailTopBar.tsx +3 -4
  5. package/src/components/mail/BriefPane.tsx +14 -31
  6. package/src/components/mail/DailyBrief.tsx +9 -12
  7. package/src/components/mail/DraftsView.tsx +3 -17
  8. package/src/components/mail/FlaggedList.tsx +1 -1
  9. package/src/components/mail/FlaggedPane.tsx +14 -31
  10. package/src/components/mail/MailListHeader.tsx +10 -11
  11. package/src/components/mail/MailSidebarAdapter.tsx +4 -25
  12. package/src/components/mail/MailboxPane.tsx +21 -59
  13. package/src/components/mail/MessageActionMenu.tsx +4 -11
  14. package/src/components/mail/MessageList.tsx +9 -13
  15. package/src/components/mail/OutboxPane.tsx +9 -26
  16. package/src/components/mail/SelectionWizardHost.tsx +1 -1
  17. package/src/components/mail/SwipeableMessageRow.tsx +4 -15
  18. package/src/hooks/useCurrentMailboxName.ts +5 -9
  19. package/src/hooks/useSearchScope.ts +6 -12
  20. package/src/hooks/useSearchTokenContext.ts +3 -2
  21. package/src/hooks/useSemanticSearch.ts +3 -3
  22. package/src/lib/mail-route.ts +10 -0
  23. package/src/lib/search-scope.test.ts +45 -32
  24. package/src/lib/search-scope.ts +14 -19
  25. package/src/lib/search-view.test.ts +1 -1
  26. package/src/lib/search-view.ts +2 -2
  27. package/src/routes/mail/$mailboxId.tsx +1 -2
  28. package/src/routes/mail/brief.tsx +1 -2
  29. package/src/routes/mail/flagged.tsx +1 -2
  30. package/src/routes/mail/outbox.tsx +1 -2
  31. package/src/routes/mail.tsx +7 -3
  32. package/src/routing/browsed-list.ts +60 -9
  33. package/src/routing/compose.ts +2 -27
  34. package/src/routing/index.ts +40 -1
  35. package/src/routing/nav-selection.ts +24 -0
  36. package/src/routing/open-thread.ts +89 -2
  37. package/src/routing/outbox-draft.ts +27 -1
  38. package/src/{hooks/useSearchField.render.test.ts → routing/search-field.render.test.ts} +1 -1
  39. package/src/{hooks/useSearchField.ts → routing/search-field.ts} +1 -1
  40. package/src/{hooks → routing}/search-mirror-convergence.render.test.ts +1 -1
  41. package/src/{hooks → routing}/search-mirror-detail.render.test.ts +1 -1
  42. package/src/{hooks/useSearchMirror.ts → routing/search-mirror.ts} +2 -1
  43. package/src/routing/search-navigation.ts +44 -0
  44. package/src/routing/sections.ts +53 -0
  45. /package/src/{lib → routing}/wizard-history.test.ts +0 -0
  46. /package/src/{lib → routing}/wizard-history.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.202",
3
+ "version": "0.0.203",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  import { ComposeSmtpMissingBanner as Banner } from "@remit/ui";
2
- import { useNavigate } from "@tanstack/react-router";
3
2
  import type { Ref } from "react";
3
+ import { useConfigureAccountSmtp } from "@/routing";
4
4
 
5
5
  interface ComposeSmtpMissingBannerProps {
6
6
  accountId: string;
@@ -12,16 +12,13 @@ export const ComposeSmtpMissingBanner = ({
12
12
  accountId,
13
13
  configureRef,
14
14
  }: ComposeSmtpMissingBannerProps) => {
15
- const navigate = useNavigate();
15
+ const configureSmtp = useConfigureAccountSmtp();
16
16
 
17
17
  return (
18
18
  <Banner
19
19
  configureRef={configureRef}
20
20
  onConfigure={() => {
21
- navigate({
22
- to: "/settings/accounts",
23
- search: { editAccountId: accountId, focusSmtp: true },
24
- });
21
+ configureSmtp(accountId);
25
22
  }}
26
23
  />
27
24
  );
@@ -1,7 +1,5 @@
1
- import { useLocation } from "@tanstack/react-router";
2
1
  import { Pencil } from "lucide-react";
3
- import { locationOpensDetail } from "@/lib/mail-route";
4
- import { useOpenCompose } from "@/routing";
2
+ import { useOpenCompose, useOpensDetail } from "@/routing";
5
3
 
6
4
  /**
7
5
  * Floating Action Button for composing a new message. Mobile-only.
@@ -16,9 +14,9 @@ import { useOpenCompose } from "@/routing";
16
14
  */
17
15
  export const ComposeFab = () => {
18
16
  const compose = useOpenCompose();
19
- const location = useLocation();
17
+ const opensDetail = useOpensDetail();
20
18
 
21
- if (locationOpensDetail(location.pathname)) return null;
19
+ if (opensDetail) return null;
22
20
 
23
21
  return (
24
22
  <button
@@ -8,7 +8,6 @@
8
8
  */
9
9
  import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
10
10
  import { RefreshButton, ShellTopBar, shortcutHintForAction } from "@remit/ui";
11
- import { useNavigate } from "@tanstack/react-router";
12
11
  import { useMemo } from "react";
13
12
  import { AccountMenu } from "@/auth/AccountMenu";
14
13
  import { useRefreshControl } from "@/hooks/useRefreshControl";
@@ -16,7 +15,7 @@ import { useSearchScope } from "@/hooks/useSearchScope";
16
15
  import { openBugReport } from "@/lib/bug-report";
17
16
  import { useMailContext } from "@/lib/mail-context";
18
17
  import { useMailFreshness } from "@/lib/mail-freshness";
19
- import { useOpenCompose } from "@/routing";
18
+ import { useGoToSection, useOpenCompose } from "@/routing";
20
19
 
21
20
  interface MailTopBarProps {
22
21
  accounts: RemitImapAccountResponse[];
@@ -25,7 +24,7 @@ interface MailTopBarProps {
25
24
  export function MailTopBar({ accounts }: MailTopBarProps) {
26
25
  const { searchInput, onSearchChange, onSearchClear, onSearchClearQuery } =
27
26
  useMailContext();
28
- const navigate = useNavigate();
27
+ const goToSection = useGoToSection();
29
28
  const compose = useOpenCompose();
30
29
  const { scope, clearScope } = useSearchScope(accounts);
31
30
  const chips =
@@ -59,7 +58,7 @@ export function MailTopBar({ accounts }: MailTopBarProps) {
59
58
  }}
60
59
  onCompose={compose}
61
60
  onReportBug={openBugReport}
62
- onOpenSettings={() => navigate({ to: "/settings/accounts" })}
61
+ onOpenSettings={() => goToSection("accounts")}
63
62
  composeShortcut={shortcutHintForAction("compose")}
64
63
  refreshControl={
65
64
  <RefreshButton
@@ -16,7 +16,6 @@ import { unifiedThreadOperationsListAllThreadsOptions } from "@remit/api-http-cl
16
16
  import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
17
17
  import { ReadingPaneEmpty } from "@remit/ui";
18
18
  import { useQuery } from "@tanstack/react-query";
19
- import { useNavigate } from "@tanstack/react-router";
20
19
  import {
21
20
  createContext,
22
21
  type ReactNode,
@@ -48,10 +47,12 @@ import {
48
47
  type OpenThreadTarget,
49
48
  type ReplyMode,
50
49
  replyToThread,
50
+ useCloseThread,
51
+ useGoToSection,
51
52
  useIsComposing,
52
53
  useIsReplying,
53
54
  useOpenReply,
54
- useRetainOpenPanels,
55
+ useOpenThread,
55
56
  } from "@/routing";
56
57
 
57
58
  /* ------------------------------------------------------------------ */
@@ -113,8 +114,9 @@ interface BriefPaneProps {
113
114
  }
114
115
 
115
116
  function BriefPaneProvider({ thread, children }: BriefPaneProps) {
116
- const navigate = useNavigate();
117
- const retainPanels = useRetainOpenPanels();
117
+ const openThread = useOpenThread();
118
+ const closeThread = useCloseThread();
119
+ const goToSection = useGoToSection();
118
120
  const { searchInput } = useMailContext();
119
121
  const threadId = thread?.threadId;
120
122
  const pointedAtMessageId = thread?.messageId;
@@ -155,37 +157,18 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
155
157
 
156
158
  const handleOpenThread = useCallback(
157
159
  (target: OpenThreadTarget, options?: OpenMessageOptions) => {
158
- navigate({
159
- to: "/mail/brief/$threadId/$messageId",
160
- params: target,
161
- replace: options?.replace,
162
- // Commit the active query with the open so the debounced q-mirror —
163
- // which walks back up to the list when the query goes active — is
164
- // already satisfied and leaves the conversation alone. The *live*
165
- // `searchInput`: a row can be tapped before the debounce settles, when
166
- // the committed query is still empty.
167
- search: (prev) => ({ ...prev, q: searchInput || undefined }),
168
- hash: retainPanels,
169
- });
160
+ openThread(target, { replace: options?.replace, query: searchInput });
170
161
  },
171
- [navigate, retainPanels, searchInput],
162
+ [openThread, searchInput],
172
163
  );
173
164
 
174
- const handleCloseThread = useCallback(() => {
175
- navigate({
176
- to: "/mail/brief",
177
- search: (prev) => prev,
178
- hash: retainPanels,
179
- });
180
- }, [navigate, retainPanels]);
181
-
182
165
  const handleDeselectIfRemoved = useCallback(
183
166
  (removedIds: string[]) => {
184
167
  if (!selectedMessageId) return;
185
168
  if (!removedIds.includes(selectedMessageId)) return;
186
- handleCloseThread();
169
+ closeThread();
187
170
  },
188
- [selectedMessageId, handleCloseThread],
171
+ [selectedMessageId, closeThread],
189
172
  );
190
173
 
191
174
  const actions = useThreadActions({
@@ -247,7 +230,7 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
247
230
  // would otherwise fire at the message behind whatever is being typed — or
248
231
  // answer a row the cursor moved to while a reply was open.
249
232
  enabled: !isComposing && !isReplying,
250
- onClose: handleCloseThread,
233
+ onClose: closeThread,
251
234
  handlers: {
252
235
  reply: () => replyToFocusedThread?.("reply"),
253
236
  replyAll: () => replyToFocusedThread?.("reply-all"),
@@ -265,8 +248,8 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
265
248
  if (!triageTarget) return;
266
249
  toggleReadFor([triageTarget.messageId], !triageTarget.isRead);
267
250
  },
268
- goFlagged: () => navigate({ to: "/mail/flagged" }),
269
- goSettings: () => navigate({ to: "/settings" }),
251
+ goFlagged: () => goToSection("flagged"),
252
+ goSettings: () => goToSection("settings"),
270
253
  },
271
254
  });
272
255
 
@@ -287,7 +270,7 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
287
270
  selectedThread,
288
271
  conversation,
289
272
  onOpenThread: handleOpenThread,
290
- onCloseThread: handleCloseThread,
273
+ onCloseThread: closeThread,
291
274
  nextThread: adjacentThread(nextMessageId),
292
275
  previousThread: adjacentThread(previousMessageId),
293
276
  actions,
@@ -63,7 +63,6 @@ import {
63
63
  toggleBriefFilterInQuery,
64
64
  } from "@remit/ui";
65
65
  import { useQueries, useQuery } from "@tanstack/react-query";
66
- import { useNavigate } from "@tanstack/react-router";
67
66
  import { AlertCircle, RefreshCw } from "lucide-react";
68
67
  import {
69
68
  type ReactNode,
@@ -103,12 +102,14 @@ import { showInlineSearchResults } from "@/lib/search-surface";
103
102
  import { parseSearchTokens } from "@/lib/search-tokens";
104
103
  import { resolveSelectionAccountScope } from "@/lib/selection-account-scope";
105
104
  import { spamOfferForResults } from "@/lib/spam-offer";
105
+ import { wizardSelectionFrom } from "@/lib/wizard-selection";
106
106
  import {
107
+ type OpenThreadTarget,
107
108
  type SelectionWizardControl,
109
+ useGoToSection,
110
+ useScopeSearchToMailbox,
108
111
  useSelectionWizard,
109
- } from "@/lib/wizard-history";
110
- import { wizardSelectionFrom } from "@/lib/wizard-selection";
111
- import type { OpenThreadTarget } from "@/routing";
112
+ } from "@/routing";
112
113
  import { LabelApplyTrigger } from "./LabelApplyTrigger";
113
114
  import { MailListHeader, type MailListHeaderProps } from "./MailListHeader";
114
115
  import type { MessageListCommands } from "./MessageList";
@@ -160,14 +161,14 @@ interface ErrorBannerProps {
160
161
  }
161
162
 
162
163
  const ErrorBanner = ({ accountEmail }: ErrorBannerProps) => {
163
- const navigate = useNavigate();
164
+ const goToSection = useGoToSection();
164
165
  return (
165
166
  <div className="flex items-center gap-2 px-row-inset py-2 border-b border-line bg-danger-soft/40 text-xs text-danger">
166
167
  <AlertCircle className="size-3.5 shrink-0" />
167
168
  <span className="flex-1 truncate">{accountEmail} can't connect</span>
168
169
  <button
169
170
  type="button"
170
- onClick={() => navigate({ to: "/settings/accounts" })}
171
+ onClick={() => goToSection("accounts")}
171
172
  className="shrink-0 underline text-danger hover:opacity-80"
172
173
  >
173
174
  Reconnect
@@ -398,7 +399,7 @@ export function DailyBrief({
398
399
  const isDesktop = useIsDesktop();
399
400
  const tier = useLayoutTier();
400
401
  const wizard = useSelectionWizard();
401
- const navigate = useNavigate();
402
+ const scopeSearchToMailbox = useScopeSearchToMailbox();
402
403
 
403
404
  const nonMuted = useMemo(
404
405
  () => sortAccountsByCreatedAt(accounts.filter((a) => !a.muted?.value)),
@@ -811,11 +812,7 @@ export function DailyBrief({
811
812
  <SpamResultsOffer
812
813
  count={briefSpamOffer.count}
813
814
  onScopeToSpam={() =>
814
- navigate({
815
- to: "/mail/$mailboxId",
816
- params: { mailboxId: briefSpamOffer.mailboxId },
817
- search: { q: searchQuery || undefined },
818
- })
815
+ scopeSearchToMailbox(briefSpamOffer.mailboxId, searchQuery)
819
816
  }
820
817
  />
821
818
  )}
@@ -36,16 +36,11 @@ import {
36
36
  type ThreadSection,
37
37
  } from "@remit/ui";
38
38
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
39
- import { useNavigate } from "@tanstack/react-router";
40
39
  import { FileText, Inbox, Trash2 } from "lucide-react";
41
40
  import { useMemo } from "react";
42
41
 
43
42
  import { groupDraftSections } from "@/lib/drafts";
44
- import {
45
- useComposeDraftId,
46
- useEditDraft,
47
- useRetainOpenPanels,
48
- } from "@/routing";
43
+ import { useComposeDraftId, useEditDraft, useOpenThread } from "@/routing";
49
44
  import { NavMenuButton } from "./NavMenuButton";
50
45
 
51
46
  // ---------------------------------------------------------------------------
@@ -136,8 +131,6 @@ const ImapDraftRow = ({ row, isSelected, onOpen }: ImapDraftRowProps) => {
136
131
  // ---------------------------------------------------------------------------
137
132
 
138
133
  interface DraftsViewProps {
139
- /** The mailbox id for the \Drafts mailbox being viewed. */
140
- mailboxId: string;
141
134
  /** The accountId that owns this \Drafts mailbox. */
142
135
  accountId: string;
143
136
  /** The currently selected message id (reading pane). */
@@ -151,7 +144,6 @@ interface DraftsViewProps {
151
144
  }
152
145
 
153
146
  export function DraftsView({
154
- mailboxId,
155
147
  accountId,
156
148
  selectedMessageId,
157
149
  imapThreads,
@@ -160,8 +152,7 @@ export function DraftsView({
160
152
  }: DraftsViewProps) {
161
153
  const openDraftId = useComposeDraftId();
162
154
  const editDraft = useEditDraft();
163
- const navigate = useNavigate();
164
- const retainPanels = useRetainOpenPanels();
155
+ const openThread = useOpenThread();
165
156
  const queryClient = useQueryClient();
166
157
 
167
158
  // Fetch the full outbox list — both sources are already fetched by the
@@ -196,12 +187,7 @@ export function DraftsView({
196
187
  (thread) => thread.messageId === messageId,
197
188
  )?.threadId;
198
189
  if (!threadId) return;
199
- navigate({
200
- to: "/mail/$mailboxId/$threadId/$messageId",
201
- params: { mailboxId, threadId, messageId },
202
- search: (prev) => prev,
203
- hash: retainPanels,
204
- });
190
+ openThread({ threadId, messageId });
205
191
  };
206
192
 
207
193
  const isEmpty = sections.length === 0;
@@ -37,9 +37,9 @@ import { useMailContext } from "@/lib/mail-context";
37
37
  import { rowToSearchResult } from "@/lib/search-result";
38
38
  import { parseSearchTokens } from "@/lib/search-tokens";
39
39
  import { dedupeByThread } from "@/lib/starred-rows";
40
- import { useSelectionWizard } from "@/lib/wizard-history";
41
40
  import { wizardSelectionFrom } from "@/lib/wizard-selection";
42
41
  import type { OpenThreadTarget } from "@/routing";
42
+ import { useSelectionWizard } from "@/routing";
43
43
  import { MailViewChrome } from "./MailViewChrome";
44
44
  import type { MessageListCommands } from "./MessageList";
45
45
  import { MessageRow } from "./MessageRow";
@@ -26,7 +26,6 @@
26
26
  */
27
27
  import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
28
28
  import { ReadingPaneEmpty } from "@remit/ui";
29
- import { useNavigate } from "@tanstack/react-router";
30
29
  import {
31
30
  createContext,
32
31
  type ReactNode,
@@ -59,10 +58,12 @@ import {
59
58
  type OpenThreadTarget,
60
59
  type ReplyMode,
61
60
  replyToThread,
61
+ useCloseThread,
62
+ useGoToSection,
62
63
  useIsComposing,
63
64
  useIsReplying,
64
65
  useOpenReply,
65
- useRetainOpenPanels,
66
+ useOpenThread,
66
67
  } from "@/routing";
67
68
 
68
69
  /* ------------------------------------------------------------------ */
@@ -124,8 +125,9 @@ interface FlaggedPaneProps {
124
125
  }
125
126
 
126
127
  function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
127
- const navigate = useNavigate();
128
- const retainPanels = useRetainOpenPanels();
128
+ const openThread = useOpenThread();
129
+ const closeThread = useCloseThread();
130
+ const goToSection = useGoToSection();
129
131
  const { searchInput } = useMailContext();
130
132
  const threadId = thread?.threadId;
131
133
  const pointedAtMessageId = thread?.messageId;
@@ -161,37 +163,18 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
161
163
 
162
164
  const handleOpenThread = useCallback(
163
165
  (target: OpenThreadTarget, options?: OpenMessageOptions) => {
164
- navigate({
165
- to: "/mail/flagged/$threadId/$messageId",
166
- params: target,
167
- replace: options?.replace,
168
- // Commit the active query with the open so the debounced q-mirror —
169
- // which walks back up to the list when the query goes active — is
170
- // already satisfied and leaves the conversation alone. The *live*
171
- // `searchInput`: a row can be tapped before the debounce settles, when
172
- // the committed query is still empty.
173
- search: (prev) => ({ ...prev, q: searchInput || undefined }),
174
- hash: retainPanels,
175
- });
166
+ openThread(target, { replace: options?.replace, query: searchInput });
176
167
  },
177
- [navigate, retainPanels, searchInput],
168
+ [openThread, searchInput],
178
169
  );
179
170
 
180
- const handleCloseThread = useCallback(() => {
181
- navigate({
182
- to: "/mail/flagged",
183
- search: (prev) => prev,
184
- hash: retainPanels,
185
- });
186
- }, [navigate, retainPanels]);
187
-
188
171
  const handleDeselectIfRemoved = useCallback(
189
172
  (removedIds: string[]) => {
190
173
  if (!selectedMessageId) return;
191
174
  if (!removedIds.includes(selectedMessageId)) return;
192
- handleCloseThread();
175
+ closeThread();
193
176
  },
194
- [selectedMessageId, handleCloseThread],
177
+ [selectedMessageId, closeThread],
195
178
  );
196
179
 
197
180
  const actions = useThreadActions({
@@ -253,7 +236,7 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
253
236
  // would otherwise fire at the message behind whatever is being typed — or
254
237
  // answer a row the cursor moved to while a reply was open.
255
238
  enabled: !isComposing && !isReplying,
256
- onClose: handleCloseThread,
239
+ onClose: closeThread,
257
240
  handlers: {
258
241
  reply: () => replyToFocusedThread?.("reply"),
259
242
  replyAll: () => replyToFocusedThread?.("reply-all"),
@@ -271,8 +254,8 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
271
254
  if (!triageTarget) return;
272
255
  toggleReadFor([triageTarget.messageId], !triageTarget.isRead);
273
256
  },
274
- goBrief: () => navigate({ to: "/mail/brief" }),
275
- goSettings: () => navigate({ to: "/settings" }),
257
+ goBrief: () => goToSection("brief"),
258
+ goSettings: () => goToSection("settings"),
276
259
  },
277
260
  });
278
261
 
@@ -293,7 +276,7 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
293
276
  selectedThread,
294
277
  conversation,
295
278
  onOpenThread: handleOpenThread,
296
- onCloseThread: handleCloseThread,
279
+ onCloseThread: closeThread,
297
280
  nextThread: adjacentThread(nextMessageId),
298
281
  previousThread: adjacentThread(previousMessageId),
299
282
  actions,
@@ -58,7 +58,6 @@ import {
58
58
  useAppShellLayout,
59
59
  useSuggestList,
60
60
  } from "@remit/ui";
61
- import { useNavigate, useRouterState } from "@tanstack/react-router";
62
61
  import { Menu, Search, X } from "lucide-react";
63
62
  import {
64
63
  type ReactNode,
@@ -88,7 +87,11 @@ import {
88
87
  searchTokenLabel,
89
88
  } from "@/lib/search-tokens";
90
89
  import { spamOfferForResults } from "@/lib/spam-offer";
91
- import { useOpenWizard } from "@/lib/wizard-history";
90
+ import {
91
+ useBrowsedList,
92
+ useOpenWizard,
93
+ useScopeSearchToMailbox,
94
+ } from "@/routing";
92
95
 
93
96
  export interface MailListHeaderProps {
94
97
  title: string;
@@ -176,7 +179,7 @@ export function MailListHeader({
176
179
  searchViewKey,
177
180
  } = useMailContext();
178
181
  const tokenContext = useSearchTokenContext();
179
- const navigate = useNavigate();
182
+ const scopeSearchToMailbox = useScopeSearchToMailbox();
180
183
  const layout = useAppShellLayout();
181
184
  const tier = useLayoutTier();
182
185
  const [searchOpen, setSearchOpen] = useState(false);
@@ -301,10 +304,10 @@ export function MailListHeader({
301
304
  // The mailbox's appointed role is what lets a search scoped to Spam show its
302
305
  // rows rather than drop them.
303
306
  const { scope } = useSearchScope(accounts);
304
- const matches = useRouterState({ select: (s) => s.matches });
305
- const scopedMailboxId = routeMailboxId(matches);
307
+ const browsed = useBrowsedList();
308
+ const scopedMailboxId = routeMailboxId(browsed);
306
309
  const routeScope = resultsScopeForRoute(
307
- matches,
310
+ browsed,
308
311
  scope,
309
312
  scopedMailboxId ? resultFolderIndex.get(scopedMailboxId)?.role : undefined,
310
313
  );
@@ -320,11 +323,7 @@ export function MailListHeader({
320
323
  ? {
321
324
  kind: "global",
322
325
  onScopeToSpam: () =>
323
- navigate({
324
- to: "/mail/$mailboxId",
325
- params: { mailboxId: spamOffer.mailboxId },
326
- search: { q: searchQuery || undefined },
327
- }),
326
+ scopeSearchToMailbox(spamOffer.mailboxId, searchQuery),
328
327
  }
329
328
  : routeScope;
330
329
 
@@ -10,7 +10,6 @@ import type {
10
10
  import type { NavAccount, NavLinkComponent, NavMailboxRole } from "@remit/ui";
11
11
  import { NavSidebar } from "@remit/ui";
12
12
  import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
13
- import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
14
13
  import { useCallback, useMemo, useState } from "react";
15
14
  import { useTranslation } from "react-i18next";
16
15
  import {
@@ -25,7 +24,7 @@ import {
25
24
  removeSavedSearch,
26
25
  saveSearch,
27
26
  } from "@/lib/saved-searches";
28
- import { NavLink } from "@/routing";
27
+ import { NavLink, useSearchEverything, useSelectedNavId } from "@/routing";
29
28
 
30
29
  interface MailSidebarAdapterProps {
31
30
  accounts: RemitImapAccountResponse[];
@@ -112,26 +111,6 @@ function toNavMailbox(
112
111
  };
113
112
  }
114
113
 
115
- /**
116
- * Resolves the currently selected nav ID from the active route so the kit
117
- * component can highlight the right item.
118
- * - /mail/outbox → "outbox"
119
- * - /mail/flagged → "flagged"
120
- * - /mail/brief → "brief"
121
- * - /mail/$mailboxId → mailboxId
122
- */
123
- function useSelectedNavId(): string {
124
- const location = useLocation();
125
- const params = useParams({ strict: false }) as { mailboxId?: string };
126
-
127
- if (location.pathname.startsWith("/settings")) return "settings";
128
- if (location.pathname.startsWith("/mail/outbox")) return "outbox";
129
- if (location.pathname.startsWith("/mail/flagged")) return "flagged";
130
- if (location.pathname.startsWith("/mail/brief")) return "brief";
131
- if (params.mailboxId) return params.mailboxId;
132
- return "";
133
- }
134
-
135
114
  /**
136
115
  * Pane 1 of the 4-pane shell. Data bridge that fetches mailboxes per account,
137
116
  * maps them onto the kit's NavAccount shape, and renders each entry as a real
@@ -144,7 +123,7 @@ export function MailSidebarAdapter({
144
123
  variant = "desktop",
145
124
  }: MailSidebarAdapterProps) {
146
125
  const queryClient = useQueryClient();
147
- const navigate = useNavigate();
126
+ const searchEverything = useSearchEverything();
148
127
  const selectedNavId = useSelectedNavId();
149
128
  const { searchInput, onSearchChange } = useMailContext();
150
129
  const [savedSearches, setSavedSearches] = useState(loadSavedSearches);
@@ -341,10 +320,10 @@ export function MailSidebarAdapter({
341
320
  const handleSelectSavedSearch = useCallback(
342
321
  (query: string) => {
343
322
  onSearchChange(query);
344
- navigate({ to: "/mail/brief", search: { q: query } });
323
+ searchEverything(query);
345
324
  onMailboxSelect?.();
346
325
  },
347
- [onSearchChange, navigate, onMailboxSelect],
326
+ [onSearchChange, searchEverything, onMailboxSelect],
348
327
  );
349
328
 
350
329
  return (