@remit/ui 0.0.10 → 0.0.12
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
CHANGED
|
@@ -51,7 +51,25 @@ const Actions = () => (
|
|
|
51
51
|
|
|
52
52
|
const SCOPE: SearchChip = { id: "in:spam", label: "in:spam", tone: "scope" };
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The placeholders the app pairs with each scope state. Only the unscoped brief
|
|
56
|
+
* may claim to search all mail; a scoped view says so, and a mailbox route
|
|
57
|
+
* whose name has not loaded yet gets neutral wording rather than a placeholder
|
|
58
|
+
* asserting the wrong scope.
|
|
59
|
+
*/
|
|
60
|
+
const PLACEHOLDER = {
|
|
61
|
+
global: "Search all mail",
|
|
62
|
+
pending: "Search mail",
|
|
63
|
+
scoped: "Search this folder",
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
const Bar = ({
|
|
67
|
+
initialChips = [],
|
|
68
|
+
placeholder = PLACEHOLDER.global,
|
|
69
|
+
}: {
|
|
70
|
+
initialChips?: SearchChip[];
|
|
71
|
+
placeholder?: string;
|
|
72
|
+
}) => {
|
|
55
73
|
const [chips, setChips] = useState<SearchChip[]>(initialChips);
|
|
56
74
|
const [value, setValue] = useState("");
|
|
57
75
|
return (
|
|
@@ -71,7 +89,7 @@ const Bar = ({ initialChips = [] }: { initialChips?: SearchChip[] }) => {
|
|
|
71
89
|
}}
|
|
72
90
|
onClearQuery={() => setValue("")}
|
|
73
91
|
globalFocusKey={false}
|
|
74
|
-
placeholder=
|
|
92
|
+
placeholder={placeholder}
|
|
75
93
|
/>
|
|
76
94
|
}
|
|
77
95
|
/>
|
|
@@ -96,24 +114,67 @@ const WithPanes = ({ children }: { children: React.ReactNode }) => (
|
|
|
96
114
|
</div>
|
|
97
115
|
);
|
|
98
116
|
|
|
99
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* The daily brief's state: search unscoped, nothing narrowing it. No chip, and
|
|
119
|
+
* the only placeholder allowed to claim it searches all mail — which it now
|
|
120
|
+
* genuinely does, across every folder of every account.
|
|
121
|
+
*/
|
|
100
122
|
export const Unscoped: Story = {
|
|
101
123
|
render: () => <Bar />,
|
|
102
124
|
};
|
|
103
125
|
|
|
104
126
|
/**
|
|
105
127
|
* A narrowing scope in the bar, tinted to mark it as the view the user is in
|
|
106
|
-
* rather than a filter they typed. Removing it widens the search again
|
|
128
|
+
* rather than a filter they typed. Removing it widens the search again — a
|
|
129
|
+
* navigation back to the brief, not an edit of the text, because the chip
|
|
130
|
+
* mirrors the route.
|
|
131
|
+
*
|
|
132
|
+
* The placeholder narrows with the chip. A scoped bar reading "Search all mail"
|
|
133
|
+
* is a state the app never produces.
|
|
107
134
|
*/
|
|
108
135
|
export const Scoped: Story = {
|
|
109
|
-
render: () => <Bar initialChips={[SCOPE]} />,
|
|
136
|
+
render: () => <Bar initialChips={[SCOPE]} placeholder={PLACEHOLDER.scoped} />,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The third scope state: a mailbox route whose name has not resolved yet. The
|
|
141
|
+
* list underneath is already narrowed, so the bar must not claim to search
|
|
142
|
+
* everything — but a chip reading a raw uuid is worse than no chip, so it shows
|
|
143
|
+
* none and falls back to neutral wording until the name arrives.
|
|
144
|
+
*/
|
|
145
|
+
export const ScopePending: Story = {
|
|
146
|
+
render: () => <Bar placeholder={PLACEHOLDER.pending} />,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The virtual collections scope the bar too, and their chips read as whatever
|
|
151
|
+
* describes the collection. Flagged is a marker on the mail rather than a
|
|
152
|
+
* place, so it chips `is:starred`; the outbox is a place mail sits in and keeps
|
|
153
|
+
* the `in:` form.
|
|
154
|
+
*/
|
|
155
|
+
export const ScopedToFlagged: Story = {
|
|
156
|
+
render: () => (
|
|
157
|
+
<Bar
|
|
158
|
+
initialChips={[{ id: "is:starred", label: "is:starred", tone: "scope" }]}
|
|
159
|
+
placeholder={PLACEHOLDER.scoped}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const ScopedToOutbox: Story = {
|
|
165
|
+
render: () => (
|
|
166
|
+
<Bar
|
|
167
|
+
initialChips={[{ id: "in:outbox", label: "in:outbox", tone: "scope" }]}
|
|
168
|
+
placeholder={PLACEHOLDER.scoped}
|
|
169
|
+
/>
|
|
170
|
+
),
|
|
110
171
|
};
|
|
111
172
|
|
|
112
173
|
/** The arrangement: one bar over the nav, the list, and the message pane. */
|
|
113
174
|
export const OverTheLayout: Story = {
|
|
114
175
|
render: () => (
|
|
115
176
|
<WithPanes>
|
|
116
|
-
<Bar initialChips={[SCOPE]} />
|
|
177
|
+
<Bar initialChips={[SCOPE]} placeholder={PLACEHOLDER.scoped} />
|
|
117
178
|
</WithPanes>
|
|
118
179
|
),
|
|
119
180
|
};
|
|
@@ -59,6 +59,7 @@ function MailScreen({
|
|
|
59
59
|
initialExpanded = false,
|
|
60
60
|
initialSearchOpen = false,
|
|
61
61
|
initialSearchValue = "",
|
|
62
|
+
showSearch = true,
|
|
62
63
|
}: {
|
|
63
64
|
title: string;
|
|
64
65
|
unreadCount: number;
|
|
@@ -66,6 +67,7 @@ function MailScreen({
|
|
|
66
67
|
initialExpanded?: boolean;
|
|
67
68
|
initialSearchOpen?: boolean;
|
|
68
69
|
initialSearchValue?: string;
|
|
70
|
+
showSearch?: boolean;
|
|
69
71
|
}) {
|
|
70
72
|
const [searchValue, setSearchValue] = useState(initialSearchValue);
|
|
71
73
|
const [searchOpen, setSearchOpen] = useState(initialSearchOpen);
|
|
@@ -85,6 +87,7 @@ function MailScreen({
|
|
|
85
87
|
title={title}
|
|
86
88
|
unreadCount={unreadCount}
|
|
87
89
|
isDesktop={false}
|
|
90
|
+
showSearch={showSearch}
|
|
88
91
|
onMenuClick={() => undefined}
|
|
89
92
|
searchValue={searchValue}
|
|
90
93
|
onSearchChange={setSearchValue}
|
|
@@ -188,6 +191,43 @@ export const InboxFilterExpanded: Story = {
|
|
|
188
191
|
),
|
|
189
192
|
};
|
|
190
193
|
|
|
194
|
+
/**
|
|
195
|
+
* `showSearch={false}` — the header on desktop, where the app's top bar owns
|
|
196
|
+
* the search field for the whole shell. Title and unread count only: no
|
|
197
|
+
* magnifier, nothing to expand. Two search inputs on one page would compete
|
|
198
|
+
* for focus and for the "/" shortcut, so the header yields.
|
|
199
|
+
*
|
|
200
|
+
* This is what a desktop mail pane renders. Every story below it is the
|
|
201
|
+
* below-desktop case, where there is no top bar and the header keeps its own
|
|
202
|
+
* compact field.
|
|
203
|
+
*/
|
|
204
|
+
export const SearchOwnedByTopBar: Story = {
|
|
205
|
+
render: () => (
|
|
206
|
+
<MailScreen
|
|
207
|
+
title="Daily brief"
|
|
208
|
+
unreadCount={15338}
|
|
209
|
+
preset={briefFilterConfig(accounts)}
|
|
210
|
+
showSearch={false}
|
|
211
|
+
/>
|
|
212
|
+
),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The same header for a mailbox rather than the brief — still no search
|
|
217
|
+
* affordance, because whether the header owns one is a property of the layout,
|
|
218
|
+
* not of which list is under it.
|
|
219
|
+
*/
|
|
220
|
+
export const SearchOwnedByTopBarInbox: Story = {
|
|
221
|
+
render: () => (
|
|
222
|
+
<MailScreen
|
|
223
|
+
title="Inbox"
|
|
224
|
+
unreadCount={42}
|
|
225
|
+
preset={inboxFilterConfig()}
|
|
226
|
+
showSearch={false}
|
|
227
|
+
/>
|
|
228
|
+
),
|
|
229
|
+
};
|
|
230
|
+
|
|
191
231
|
/** Mobile search collapsed to a magnifier in the header top row. */
|
|
192
232
|
export const MobileSearchCollapsed: Story = {
|
|
193
233
|
render: () => (
|
|
@@ -72,6 +72,44 @@ const resultSections: SearchResultSection[] = [
|
|
|
72
72
|
{ id: "related", label: "Related", results: related },
|
|
73
73
|
];
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Matches from outside the inbox — Archive, Sent, Spam and a custom folder.
|
|
77
|
+
* These are the rows an unscoped search returns that an INBOX-only one could
|
|
78
|
+
* not, so they only ever appear under the brief's sections.
|
|
79
|
+
*/
|
|
80
|
+
const crossFolderMatches: SearchResult[] = [
|
|
81
|
+
{
|
|
82
|
+
id: "x1",
|
|
83
|
+
sender: "Mollie",
|
|
84
|
+
subject: "Invoice 2026-02 — archived",
|
|
85
|
+
snippet: "Filed to Archive last month; payment already settled.",
|
|
86
|
+
date: "Feb 24",
|
|
87
|
+
category: { label: "Receipt", tone: "positive" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "x2",
|
|
91
|
+
sender: "me",
|
|
92
|
+
subject: "Re: invoice query",
|
|
93
|
+
snippet: "Sent — attaching the invoice you asked for.",
|
|
94
|
+
date: "Feb 18",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "x3",
|
|
98
|
+
sender: "billing@unknown-vendor.test",
|
|
99
|
+
subject: "URGENT invoice attached",
|
|
100
|
+
snippet: "Marked as spam, but it is the invoice the user is looking for.",
|
|
101
|
+
date: "Feb 11",
|
|
102
|
+
category: { label: "Spam", tone: "warning" },
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "x4",
|
|
106
|
+
sender: "Accountant",
|
|
107
|
+
subject: "Invoices for the quarter",
|
|
108
|
+
snippet: "Filed under Projects/Bookkeeping.",
|
|
109
|
+
date: "Jan 30",
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
75
113
|
const emptySections: SearchResultSection[] = [
|
|
76
114
|
{ id: "top", label: "Top matches", results: [] },
|
|
77
115
|
];
|
|
@@ -111,6 +149,50 @@ export const Idle: Story = {
|
|
|
111
149
|
render: () => <Harness value="" recentSearches={recentSearches} />,
|
|
112
150
|
};
|
|
113
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The daily brief's unscoped search: no scope chip in the bar, and the literal
|
|
154
|
+
* section carries matches from every folder — Archive, Sent, Spam and custom
|
|
155
|
+
* folders alongside the inbox. Before the listing behind it took a search mode
|
|
156
|
+
* it could only ever return inbox mail, so this section was silently narrower
|
|
157
|
+
* than the bar promised.
|
|
158
|
+
*
|
|
159
|
+
* The rows themselves do not say which folder they came from; the sections are
|
|
160
|
+
* ordered newest first regardless of where each message is filed.
|
|
161
|
+
*/
|
|
162
|
+
export const UnscopedAcrossFolders: Story = {
|
|
163
|
+
render: () => (
|
|
164
|
+
<Harness
|
|
165
|
+
value="invoice"
|
|
166
|
+
sections={[
|
|
167
|
+
{
|
|
168
|
+
id: "top",
|
|
169
|
+
label: "Top matches",
|
|
170
|
+
results: [...topMatches, ...crossFolderMatches],
|
|
171
|
+
},
|
|
172
|
+
{ id: "related", label: "Related", results: related },
|
|
173
|
+
]}
|
|
174
|
+
/>
|
|
175
|
+
),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A scoped view (a mailbox route, its `in:` chip in the bar). Both sections are
|
|
180
|
+
* scoped to that folder and take the kit's default labels — the semantic
|
|
181
|
+
* section used to run unscoped here under an "Everywhere" heading, which
|
|
182
|
+
* contradicted the chip the same bar was showing.
|
|
183
|
+
*/
|
|
184
|
+
export const ScopedToOneFolder: Story = {
|
|
185
|
+
render: () => (
|
|
186
|
+
<Harness
|
|
187
|
+
value="invoice"
|
|
188
|
+
sections={[
|
|
189
|
+
{ id: "top", label: "Top matches", results: topMatches.slice(0, 2) },
|
|
190
|
+
{ id: "related", label: "Related", results: related.slice(0, 1) },
|
|
191
|
+
]}
|
|
192
|
+
/>
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
|
|
114
196
|
/** Typed filter tokens (`from:`, `has:attachment`, …) render as removable chips above the sections. */
|
|
115
197
|
export const WithFilterTokens: Story = {
|
|
116
198
|
render: () => (
|