@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/ui",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -51,7 +51,25 @@ const Actions = () => (
51
51
 
52
52
  const SCOPE: SearchChip = { id: "in:spam", label: "in:spam", tone: "scope" };
53
53
 
54
- const Bar = ({ initialChips = [] }: { initialChips?: SearchChip[] }) => {
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="Search all mail"
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
- /** The daily brief's state: search unscoped, nothing narrowing it. */
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: () => (