@remit/web-client 0.0.107 → 0.0.109
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.
- package/package.json +1 -1
- package/src/auth/AccountMenu.tsx +1 -8
- package/src/components/layout/MailTopBar.tsx +21 -10
- package/src/components/mail/DailyBrief.tsx +69 -11
- package/src/components/mail/FlaggedList.tsx +5 -0
- package/src/components/mail/MailListHeader.tsx +26 -20
- package/src/components/mail/MailViewChrome.tsx +27 -26
- package/src/lib/search-surface.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.109",
|
|
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": {
|
package/src/auth/AccountMenu.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Avatar } from "@remit/ui";
|
|
2
|
-
import {
|
|
3
|
-
import { LogOut, Settings } from "lucide-react";
|
|
2
|
+
import { LogOut } from "lucide-react";
|
|
4
3
|
import {
|
|
5
4
|
DropdownMenu,
|
|
6
5
|
DropdownMenuItem,
|
|
@@ -14,7 +13,6 @@ interface AccountMenuViewProps {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
const AccountMenuView = ({ email, onSignOut }: AccountMenuViewProps) => {
|
|
17
|
-
const navigate = useNavigate();
|
|
18
16
|
const displayName = email ?? "Account";
|
|
19
17
|
|
|
20
18
|
return (
|
|
@@ -36,11 +34,6 @@ const AccountMenuView = ({ email, onSignOut }: AccountMenuViewProps) => {
|
|
|
36
34
|
<DropdownMenuSeparator />
|
|
37
35
|
</>
|
|
38
36
|
)}
|
|
39
|
-
<DropdownMenuItem onClick={() => navigate({ to: "/settings/accounts" })}>
|
|
40
|
-
<Settings className="size-4" />
|
|
41
|
-
Settings
|
|
42
|
-
</DropdownMenuItem>
|
|
43
|
-
<DropdownMenuSeparator />
|
|
44
37
|
<DropdownMenuItem onClick={onSignOut}>
|
|
45
38
|
<LogOut className="size-4" />
|
|
46
39
|
Sign out
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MailTopBar — the app's one search surface and its global actions.
|
|
3
3
|
*
|
|
4
|
-
* Mounted by the `/mail` shell
|
|
5
|
-
*
|
|
6
|
-
* not the list's: the list header drops its own field wherever this bar
|
|
7
|
-
* mounted, so exactly one search input exists on the page and the "/"
|
|
8
|
-
* has one target.
|
|
4
|
+
* Mounted by the `/mail` shell across the top of the whole layout — the nav
|
|
5
|
+
* column, the list, the reading pane and the intelligence rail. It is the app's
|
|
6
|
+
* search, not the list's: the list header drops its own field wherever this bar
|
|
7
|
+
* is mounted, so exactly one search input exists on the page and the "/"
|
|
8
|
+
* shortcut has one target.
|
|
9
9
|
*
|
|
10
10
|
* The actions here are the ones that belong to the app rather than to whatever
|
|
11
|
-
* is currently listed or open — compose, bug report,
|
|
12
|
-
* move and the rest stay on the reading pane's own
|
|
11
|
+
* is currently listed or open — the nav toggle, compose, bug report, settings,
|
|
12
|
+
* account. Reply, delete, move and the rest stay on the reading pane's own
|
|
13
|
+
* toolbar, under this bar.
|
|
13
14
|
*
|
|
14
15
|
* The field carries one chip: the scope of the view the user navigated into
|
|
15
16
|
* (`in:spam` in Spam, nothing on the brief). Removing it goes to the brief and
|
|
@@ -19,8 +20,9 @@
|
|
|
19
20
|
* sections instead, where the text is not repeated.
|
|
20
21
|
*/
|
|
21
22
|
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
22
|
-
import { AppTopBar, Button, SearchBar } from "@remit/ui";
|
|
23
|
-
import {
|
|
23
|
+
import { AppTopBar, Button, NavToggleButton, SearchBar } from "@remit/ui";
|
|
24
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
25
|
+
import { Settings, SquarePen } from "lucide-react";
|
|
24
26
|
import { AccountMenu } from "@/auth/AccountMenu";
|
|
25
27
|
import { BugReportButton } from "@/components/ui/BugReportButton";
|
|
26
28
|
import { useGlobalCompose } from "@/hooks/useComposeTarget";
|
|
@@ -42,6 +44,7 @@ interface MailTopBarProps {
|
|
|
42
44
|
export function MailTopBar({ accounts }: MailTopBarProps) {
|
|
43
45
|
const { searchInput, onSearchChange, onSearchClear, onSearchClearQuery } =
|
|
44
46
|
useMailContext();
|
|
47
|
+
const navigate = useNavigate();
|
|
45
48
|
const compose = useGlobalCompose(accounts);
|
|
46
49
|
const { scope, clearScope } = useSearchScope(accounts);
|
|
47
50
|
const chips =
|
|
@@ -55,6 +58,7 @@ export function MailTopBar({ accounts }: MailTopBarProps) {
|
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
60
|
<AppTopBar
|
|
61
|
+
leading={<NavToggleButton />}
|
|
58
62
|
search={
|
|
59
63
|
<SearchBar
|
|
60
64
|
value={searchInput}
|
|
@@ -64,7 +68,6 @@ export function MailTopBar({ accounts }: MailTopBarProps) {
|
|
|
64
68
|
chips={chips}
|
|
65
69
|
onRemoveChip={clearScope}
|
|
66
70
|
placeholder={placeholder}
|
|
67
|
-
size="lg"
|
|
68
71
|
/>
|
|
69
72
|
}
|
|
70
73
|
actions={
|
|
@@ -78,6 +81,14 @@ export function MailTopBar({ accounts }: MailTopBarProps) {
|
|
|
78
81
|
onClick={compose}
|
|
79
82
|
/>
|
|
80
83
|
<BugReportButton />
|
|
84
|
+
<Button
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="sm"
|
|
87
|
+
icon={<Settings className="size-4" />}
|
|
88
|
+
title="Settings"
|
|
89
|
+
aria-label="Settings"
|
|
90
|
+
onClick={() => navigate({ to: "/settings/accounts" })}
|
|
91
|
+
/>
|
|
81
92
|
<AccountMenu />
|
|
82
93
|
</>
|
|
83
94
|
}
|
|
@@ -42,8 +42,10 @@ import {
|
|
|
42
42
|
type FilterSheetProps,
|
|
43
43
|
type FilterSheetSource,
|
|
44
44
|
KeyboardHintBar,
|
|
45
|
+
partitionSpamResults,
|
|
45
46
|
type SearchResult,
|
|
46
47
|
SelectionTopBar,
|
|
48
|
+
SpamResultsOffer,
|
|
47
49
|
type ThreadRowData,
|
|
48
50
|
type ThreadSection,
|
|
49
51
|
} from "@remit/ui";
|
|
@@ -81,6 +83,7 @@ import type { ListHeaderChrome } from "@/lib/list-header-chrome";
|
|
|
81
83
|
import { useMailContext } from "@/lib/mail-context";
|
|
82
84
|
import { relatedSearchResults, rowToSearchResult } from "@/lib/search-result";
|
|
83
85
|
import { parseSearchTokens } from "@/lib/search-tokens";
|
|
86
|
+
import { spamOfferForResults } from "@/lib/spam-offer";
|
|
84
87
|
import {
|
|
85
88
|
type SelectionWizardControl,
|
|
86
89
|
useSelectionWizard,
|
|
@@ -463,6 +466,7 @@ export function DailyBrief({
|
|
|
463
466
|
const tokenContext = useSearchTokenContext();
|
|
464
467
|
const isDesktop = useIsDesktop();
|
|
465
468
|
const wizard = useSelectionWizard();
|
|
469
|
+
const navigate = useNavigate();
|
|
466
470
|
|
|
467
471
|
const nonMuted = useMemo(
|
|
468
472
|
() => sortAccountsByCreatedAt(accounts.filter((a) => !a.muted?.value)),
|
|
@@ -592,9 +596,33 @@ export function DailyBrief({
|
|
|
592
596
|
);
|
|
593
597
|
}, [threadsData, searchData, selectedAccountId, sq, queryTokens]);
|
|
594
598
|
|
|
599
|
+
// The unscoped search reaches every folder, Spam included (see `filteredRows`
|
|
600
|
+
// above), and the brief is the one global-scope view whose own rows now stand
|
|
601
|
+
// in for the read-only results panel once the query commits. The panel held
|
|
602
|
+
// Spam out and offered a way to it instead (`MailListHeader`'s `spamOffer`);
|
|
603
|
+
// the brief's own body has to do the same, or a committed search surfaces
|
|
604
|
+
// junk mail inline and drops the way back to it. A bare token query (e.g.
|
|
605
|
+
// `is:unread`) never reaches the widened endpoint, so there is nothing to
|
|
606
|
+
// hold out.
|
|
607
|
+
const { bodyRows, briefSpamOffer } = useMemo(() => {
|
|
608
|
+
if (!sq) return { bodyRows: filteredRows, briefSpamOffer: undefined };
|
|
609
|
+
const asResults = filteredRows.map((row) =>
|
|
610
|
+
rowToSearchResult(row, resultFolderIndex),
|
|
611
|
+
);
|
|
612
|
+
const { spam } = partitionSpamResults(asResults);
|
|
613
|
+
if (spam.length === 0) {
|
|
614
|
+
return { bodyRows: filteredRows, briefSpamOffer: undefined };
|
|
615
|
+
}
|
|
616
|
+
const spamIds = new Set(spam.map((result) => result.id));
|
|
617
|
+
return {
|
|
618
|
+
bodyRows: filteredRows.filter((row) => !spamIds.has(row.id)),
|
|
619
|
+
briefSpamOffer: spamOfferForResults(asResults),
|
|
620
|
+
};
|
|
621
|
+
}, [filteredRows, sq, resultFolderIndex]);
|
|
622
|
+
|
|
595
623
|
const sections = useMemo<ThreadSection[]>(
|
|
596
|
-
() => groupBriefSections(
|
|
597
|
-
[
|
|
624
|
+
() => groupBriefSections(bodyRows),
|
|
625
|
+
[bodyRows],
|
|
598
626
|
);
|
|
599
627
|
|
|
600
628
|
const accountSources = useMemo<FilterSheetSource[]>(() => {
|
|
@@ -693,10 +721,14 @@ export function DailyBrief({
|
|
|
693
721
|
]);
|
|
694
722
|
|
|
695
723
|
// The brief is genuinely empty (caught up) only when nothing is narrowing the
|
|
696
|
-
// view: no account source and no search. When a
|
|
697
|
-
// the list says so instead, so the narrowing
|
|
724
|
+
// view: no account source and no search — free text or token. When a
|
|
725
|
+
// source/search yields nothing, the list says so instead, so the narrowing
|
|
726
|
+
// is still visible as the reason.
|
|
698
727
|
const caughtUp =
|
|
699
|
-
sections.length === 0 &&
|
|
728
|
+
sections.length === 0 &&
|
|
729
|
+
selectedAccountId === "all" &&
|
|
730
|
+
sq.length === 0 &&
|
|
731
|
+
queryTokens.length === 0;
|
|
700
732
|
|
|
701
733
|
// "Caught up" is a claim about the user's mail, so it may only be made once
|
|
702
734
|
// the server has confirmed no sync is running (#452). The config snapshot
|
|
@@ -752,12 +784,32 @@ export function DailyBrief({
|
|
|
752
784
|
briefSkeleton
|
|
753
785
|
)
|
|
754
786
|
) : (
|
|
755
|
-
<
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
787
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
788
|
+
{briefSpamOffer && (
|
|
789
|
+
<SpamResultsOffer
|
|
790
|
+
count={briefSpamOffer.count}
|
|
791
|
+
onScopeToSpam={() =>
|
|
792
|
+
navigate({
|
|
793
|
+
to: "/mail/$mailboxId",
|
|
794
|
+
params: { mailboxId: briefSpamOffer.mailboxId },
|
|
795
|
+
search: {
|
|
796
|
+
q: searchQuery || undefined,
|
|
797
|
+
selectedMessageId: undefined,
|
|
798
|
+
selectedThreadId: undefined,
|
|
799
|
+
},
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
804
|
+
<div className="min-h-0 flex-1">
|
|
805
|
+
<BriefListBody
|
|
806
|
+
sections={sections}
|
|
807
|
+
briefCategory={selectedCategory}
|
|
808
|
+
selectedThreadId={selectedMessageId}
|
|
809
|
+
onSelectThread={onSelectMessage}
|
|
810
|
+
/>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
761
813
|
);
|
|
762
814
|
|
|
763
815
|
// The cursor and selection wrap the whole pane, not just the list body: the
|
|
@@ -785,6 +837,12 @@ export function DailyBrief({
|
|
|
785
837
|
relatedResults,
|
|
786
838
|
relatedLoading,
|
|
787
839
|
onSelectSearchResult,
|
|
840
|
+
// The body already narrows to the committed query
|
|
841
|
+
// (`matchesBriefSearch` + `matchesSearchTokens` + the server `query`,
|
|
842
|
+
// above), so a committed search is a selectable list here exactly as
|
|
843
|
+
// it is on the mailbox route (#212) — the two-engine panel stays for
|
|
844
|
+
// the typing/uncommitted state only.
|
|
845
|
+
searchResultsInBody: true,
|
|
788
846
|
}}
|
|
789
847
|
rows={filteredRows}
|
|
790
848
|
>
|
|
@@ -237,6 +237,11 @@ export function FlaggedList({
|
|
|
237
237
|
searchResults={searchResults}
|
|
238
238
|
searchLoading={isLoading}
|
|
239
239
|
onSelectSearchResult={(result) => onSelectMessage?.(result.id)}
|
|
240
|
+
// A committed search renders in this view's own rows (`rows` already
|
|
241
|
+
// narrows to the query above), so the multi-select toolbar stays
|
|
242
|
+
// reachable exactly as it does on the mailbox route (#212). The
|
|
243
|
+
// two-engine panel stays for the typing/uncommitted state only.
|
|
244
|
+
searchResultsInBody
|
|
240
245
|
>
|
|
241
246
|
<ThreadListInteraction
|
|
242
247
|
selectedMessageId={selectedMessageId}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
import {
|
|
42
42
|
Button,
|
|
43
43
|
type FilterSheetProps,
|
|
44
|
+
FilterToggle,
|
|
44
45
|
isConvertible,
|
|
45
46
|
MakeFilterAction,
|
|
46
47
|
MobileSearchView,
|
|
@@ -129,13 +130,13 @@ export interface MailListHeaderProps {
|
|
|
129
130
|
relatedResultsLabel?: string;
|
|
130
131
|
/**
|
|
131
132
|
* The body renders the committed search itself as a selectable list — the
|
|
132
|
-
* mailbox route's `MessageList`
|
|
133
|
-
* multi-select toolbar and the
|
|
134
|
-
*
|
|
135
|
-
* swapping in the read-only
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* and
|
|
133
|
+
* mailbox route's `MessageList` and the brief and Starred's own rows filter
|
|
134
|
+
* to the search and host the multi-select toolbar and (on the mailbox route)
|
|
135
|
+
* the "Select all N matching" escalation (#212). When set, a committed query
|
|
136
|
+
* keeps the body on tablet/desktop instead of swapping in the read-only
|
|
137
|
+
* two-engine `SearchResults` panel; the panel still shows while the query is
|
|
138
|
+
* being typed (uncommitted). A view whose body is not a search-result list
|
|
139
|
+
* leaves this unset and keeps the panel for every query.
|
|
139
140
|
*/
|
|
140
141
|
searchResultsInBody?: boolean;
|
|
141
142
|
}
|
|
@@ -336,12 +337,12 @@ export function MailListHeader({
|
|
|
336
337
|
// It belongs to the search, not to any one way of showing it. The affordance
|
|
337
338
|
// therefore sits in the pane, above whichever body is up — the read-only
|
|
338
339
|
// results panel, or a list whose own rows narrow to the committed query — and
|
|
339
|
-
// is mounted on the same condition as the query itself. Rendering it inside
|
|
340
|
-
// results panel made it a mailbox's flash: the panel shows while the query
|
|
341
|
-
// being typed and hands back to `MessageList` the moment the query commits
|
|
342
|
-
// the URL, taking the affordance down with it a few hundred milliseconds
|
|
343
|
-
// it appeared
|
|
344
|
-
//
|
|
340
|
+
// is mounted on the same condition as the query itself. Rendering it inside
|
|
341
|
+
// the results panel made it a mailbox's flash: the panel shows while the query
|
|
342
|
+
// is being typed and hands back to `MessageList` the moment the query commits
|
|
343
|
+
// to the URL, taking the affordance down with it a few hundred milliseconds
|
|
344
|
+
// after it appeared. Mounting it here instead keeps it up through that
|
|
345
|
+
// hand-back on every view.
|
|
345
346
|
const searchHadSemanticReach = related.length > 0;
|
|
346
347
|
const conversion = useMemo(
|
|
347
348
|
() => convertSearchToRule(parsed, { searchHadSemanticReach }),
|
|
@@ -372,10 +373,11 @@ export function MailListHeader({
|
|
|
372
373
|
// Tablet + desktop keep the inline toolbar search; while a query is being
|
|
373
374
|
// typed the list-pane body swaps to the same sectioned results the phone
|
|
374
375
|
// takeover shows, under the same FilterSheet. A view whose own body renders
|
|
375
|
-
// the committed search as a selectable list (`searchResultsInBody
|
|
376
|
-
// mailbox route) keeps the panel only until the query
|
|
377
|
-
// then hands back to its
|
|
378
|
-
//
|
|
376
|
+
// the committed search as a selectable list (`searchResultsInBody` — the
|
|
377
|
+
// mailbox route, the brief, Starred) keeps the panel only until the query
|
|
378
|
+
// commits to the URL, then hands back to its own rows so multi-select (and,
|
|
379
|
+
// on the mailbox route, the "Select all N matching" escalation) is reachable
|
|
380
|
+
// (#212). Clearing the query restores the normal list.
|
|
379
381
|
const showInlineResults = showInlineSearchResults({
|
|
380
382
|
tier,
|
|
381
383
|
hasLiveInput: hasQuery,
|
|
@@ -434,9 +436,12 @@ export function MailListHeader({
|
|
|
434
436
|
/>
|
|
435
437
|
),
|
|
436
438
|
titleMeta: (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
<>
|
|
440
|
+
<span className="shrink-0 text-2xs text-fg-subtle">
|
|
441
|
+
{unreadCount.toLocaleString()} unread
|
|
442
|
+
</span>
|
|
443
|
+
{!hasQuery && <FilterToggle />}
|
|
444
|
+
</>
|
|
440
445
|
),
|
|
441
446
|
searchSlot: ownsSearch && !searchExpanded && (
|
|
442
447
|
<Button
|
|
@@ -487,6 +492,7 @@ export function MailListHeader({
|
|
|
487
492
|
chromeResults,
|
|
488
493
|
makeFilterAction,
|
|
489
494
|
searchConversion,
|
|
495
|
+
hasQuery,
|
|
490
496
|
],
|
|
491
497
|
);
|
|
492
498
|
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
* surface and the section headers flatten correctly when filtered).
|
|
19
19
|
*/
|
|
20
20
|
import {
|
|
21
|
+
FilterPanelProvider,
|
|
21
22
|
type FilterPreset,
|
|
22
23
|
FilterSheet,
|
|
23
24
|
type FilterSheetProps,
|
|
24
25
|
type SearchResult,
|
|
25
26
|
} from "@remit/ui";
|
|
26
|
-
import {
|
|
27
|
+
import type { ReactNode } from "react";
|
|
27
28
|
import { useMailContext } from "@/lib/mail-context";
|
|
28
29
|
import { MailListHeader } from "./MailListHeader";
|
|
29
30
|
|
|
@@ -54,7 +55,8 @@ interface MailViewChromeProps {
|
|
|
54
55
|
/**
|
|
55
56
|
* The body renders the committed search as a selectable list itself; see
|
|
56
57
|
* `MailListHeader`. The mailbox route sets this so a committed search yields
|
|
57
|
-
* its multi-select `MessageList` (#212);
|
|
58
|
+
* its multi-select `MessageList` (#212); Starred sets it for the same reason,
|
|
59
|
+
* over its own already query-narrowed rows.
|
|
58
60
|
*/
|
|
59
61
|
searchResultsInBody?: boolean;
|
|
60
62
|
}
|
|
@@ -80,7 +82,6 @@ export function MailViewChrome({
|
|
|
80
82
|
relatedResultsLabel,
|
|
81
83
|
searchResultsInBody,
|
|
82
84
|
}: MailViewChromeProps) {
|
|
83
|
-
const [expanded, setExpanded] = useState(false);
|
|
84
85
|
// A query owns the pane: the filter chrome and the search's own affordance
|
|
85
86
|
// narrow the same list from the same place, so the filter sheet stands down
|
|
86
87
|
// for as long as something is being searched. Its state survives — clearing
|
|
@@ -94,8 +95,6 @@ export function MailViewChrome({
|
|
|
94
95
|
sources: preset.sources,
|
|
95
96
|
selectedCategory,
|
|
96
97
|
activeFilters,
|
|
97
|
-
expanded,
|
|
98
|
-
onExpandedChange: setExpanded,
|
|
99
98
|
onSelectCategory,
|
|
100
99
|
onSelectSource,
|
|
101
100
|
onToggleFilter,
|
|
@@ -103,26 +102,28 @@ export function MailViewChrome({
|
|
|
103
102
|
};
|
|
104
103
|
|
|
105
104
|
return (
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
105
|
+
<FilterPanelProvider>
|
|
106
|
+
<MailListHeader
|
|
107
|
+
title={title}
|
|
108
|
+
unreadCount={unreadCount}
|
|
109
|
+
footer={footer}
|
|
110
|
+
searchFilter={filterConfig}
|
|
111
|
+
searchResults={searchResults}
|
|
112
|
+
searchLoading={searchLoading}
|
|
113
|
+
relatedResults={relatedResults}
|
|
114
|
+
relatedLoading={relatedLoading}
|
|
115
|
+
onSelectSearchResult={onSelectSearchResult}
|
|
116
|
+
searchResultsLabel={searchResultsLabel}
|
|
117
|
+
relatedResultsLabel={relatedResultsLabel}
|
|
118
|
+
searchResultsInBody={searchResultsInBody}
|
|
119
|
+
>
|
|
120
|
+
{/* One shell either way: the children's parent must not change when a
|
|
121
|
+
query starts, or everything under it — including the header's own
|
|
122
|
+
search field — remounts mid-keystroke. */}
|
|
123
|
+
<FilterSheet {...filterConfig} hideChrome={searching}>
|
|
124
|
+
{children}
|
|
125
|
+
</FilterSheet>
|
|
126
|
+
</MailListHeader>
|
|
127
|
+
</FilterPanelProvider>
|
|
127
128
|
);
|
|
128
129
|
}
|
|
@@ -13,12 +13,12 @@ import type { LayoutTier } from "@/hooks/useLayoutTier";
|
|
|
13
13
|
* Phone never swaps: it shows the body and keeps the panel in its full-screen
|
|
14
14
|
* takeover instead, so this only decides tablet/desktop.
|
|
15
15
|
*
|
|
16
|
-
* A view whose body renders committed results itself (`bodyRendersCommittedResults
|
|
17
|
-
* the mailbox route) keeps the panel only while the query is
|
|
18
|
-
* uncommitted, not yet mirrored to the URL — and hands back to
|
|
19
|
-
* the query commits, so the committed search is a selectable
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* A view whose body renders committed results itself (`bodyRendersCommittedResults`
|
|
17
|
+
* — the mailbox route, the brief, Starred) keeps the panel only while the query is
|
|
18
|
+
* still being typed — uncommitted, not yet mirrored to the URL — and hands back to
|
|
19
|
+
* the body the moment the query commits, so the committed search is a selectable
|
|
20
|
+
* list. A view that leaves the flag off keeps the panel for any query, committed
|
|
21
|
+
* or not.
|
|
22
22
|
*/
|
|
23
23
|
export const showInlineSearchResults = (input: {
|
|
24
24
|
tier: LayoutTier;
|