@remit/web-client 0.0.64 → 0.0.66

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.
@@ -0,0 +1,361 @@
1
+ import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
2
+ import {
3
+ Button,
4
+ type ClauseEditState,
5
+ type ClauseField,
6
+ type FilterRule,
7
+ FilterRuleEditor,
8
+ type FolderOption,
9
+ type MatchOperator,
10
+ type RuleScope,
11
+ } from "@remit/ui";
12
+ import { useQuery } from "@tanstack/react-query";
13
+ import { CheckCircle2, Loader2 } from "lucide-react";
14
+ import { useMemo, useRef, useState } from "react";
15
+ import { useCreateFilter } from "@/hooks/useFilters";
16
+ import { useOrganizeJob } from "@/hooks/useOrganizeJob";
17
+ import { useRulePreview } from "@/hooks/useRulePreview";
18
+ import { getMailboxDisplayName } from "@/lib/folder-roles";
19
+ import { buildMoveTargets } from "@/lib/move-targets";
20
+ import {
21
+ buildInitialRule,
22
+ normalizeClauseValue,
23
+ rulePredicate,
24
+ ruleToDraft,
25
+ SUPPORTED_CLAUSE_FIELDS,
26
+ } from "@/lib/organize/rule-model";
27
+
28
+ interface OrganizeRuleEditorProps {
29
+ accountId: string;
30
+ selectedMessageIds: string[];
31
+ /** The widen probe's matched total — seeds the live count without a re-fetch. */
32
+ seedCount: number;
33
+ /**
34
+ * The deployment ships no vector pipeline, so the widen cannot run. The rule
35
+ * opens on the sender-fallback `From` chips instead of the anchor.
36
+ */
37
+ semanticUnavailable?: boolean;
38
+ /** Distinct sender addresses, for the fallback clauses and the progress copy. */
39
+ senders?: string[];
40
+ /** A folder a "Something else" shortcut pre-picked. */
41
+ seedMailboxId?: string;
42
+ /** A scope a "Something else" shortcut pre-picked. */
43
+ seedScope?: RuleScope;
44
+ onClose: () => void;
45
+ }
46
+
47
+ /**
48
+ * The Organize surface as the chip editor (RFC 038 D1). The rule is rendered and
49
+ * edited over the existing preview/apply endpoints: clause chips, a
50
+ * match-operator toggle, a move action, and a scope that maps one-time apply to
51
+ * a back-apply job and standing/until to a `Filter`. The count is live and the
52
+ * commit gate holds apply until it settles, so the set the editor shows is the
53
+ * set a commit acts on. Rendered inside the desktop dialog and the mobile sheet
54
+ * alike, so the two cannot drift.
55
+ */
56
+ export function OrganizeRuleEditor({
57
+ accountId,
58
+ selectedMessageIds,
59
+ seedCount,
60
+ semanticUnavailable = false,
61
+ senders = [],
62
+ seedMailboxId,
63
+ seedScope,
64
+ onClose,
65
+ }: OrganizeRuleEditorProps) {
66
+ const anchorMessageId = selectedMessageIds[0];
67
+ const senderFallback = semanticUnavailable && senders.length > 0;
68
+
69
+ const [rule, setRule] = useState<FilterRule>(() =>
70
+ buildInitialRule({
71
+ anchorMessageId,
72
+ semanticUnavailable,
73
+ senders,
74
+ selectionCount: selectedMessageIds.length,
75
+ seedMailboxId,
76
+ seedScope,
77
+ }),
78
+ );
79
+ const [clauseEdit, setClauseEdit] = useState<ClauseEditState | undefined>();
80
+ const nextClauseId = useRef(0);
81
+
82
+ const { data: mailboxesData } = useQuery({
83
+ ...mailboxOperationsListMailboxesOptions({ path: { accountId } }),
84
+ staleTime: Number.POSITIVE_INFINITY,
85
+ });
86
+
87
+ const folders: FolderOption[] = useMemo(
88
+ () =>
89
+ buildMoveTargets(mailboxesData?.items ?? []).map((mailbox) => ({
90
+ id: mailbox.mailboxId,
91
+ label: getMailboxDisplayName(mailbox.fullPath),
92
+ })),
93
+ [mailboxesData?.items],
94
+ );
95
+
96
+ const preview = useRulePreview(
97
+ accountId,
98
+ rulePredicate(rule, anchorMessageId),
99
+ seedCount,
100
+ );
101
+
102
+ const organizeJob = useOrganizeJob(accountId);
103
+ const createFilter = useCreateFilter(accountId);
104
+
105
+ const startAddClause = () =>
106
+ setClauseEdit({
107
+ mode: "add",
108
+ draft: { field: SUPPORTED_CLAUSE_FIELDS[0], value: "" },
109
+ });
110
+
111
+ const startEditClause = (clauseId: string) => {
112
+ const clause = rule.clauses.find((entry) => entry.id === clauseId);
113
+ if (!clause) return;
114
+ setClauseEdit({
115
+ mode: "edit",
116
+ clauseId,
117
+ draft: { field: clause.field, value: clause.value },
118
+ });
119
+ };
120
+
121
+ const changeDraftField = (field: ClauseField) =>
122
+ setClauseEdit((edit) =>
123
+ edit ? { ...edit, draft: { ...edit.draft, field } } : edit,
124
+ );
125
+
126
+ const changeDraftValue = (value: string) =>
127
+ setClauseEdit((edit) =>
128
+ edit ? { ...edit, draft: { ...edit.draft, value } } : edit,
129
+ );
130
+
131
+ const submitClause = () => {
132
+ if (!clauseEdit) return;
133
+ const field = clauseEdit.draft.field;
134
+ const value = normalizeClauseValue(field, clauseEdit.draft.value);
135
+ if (value === "") return;
136
+ setRule((current) => {
137
+ if (clauseEdit.mode === "edit" && clauseEdit.clauseId) {
138
+ return {
139
+ ...current,
140
+ clauses: current.clauses.map((clause) =>
141
+ clause.id === clauseEdit.clauseId
142
+ ? { id: clause.id, field, value }
143
+ : clause,
144
+ ),
145
+ };
146
+ }
147
+ nextClauseId.current += 1;
148
+ return {
149
+ ...current,
150
+ clauses: [
151
+ ...current.clauses,
152
+ { id: `clause-${nextClauseId.current}`, field, value },
153
+ ],
154
+ };
155
+ });
156
+ setClauseEdit(undefined);
157
+ };
158
+
159
+ const removeClause = (clauseId: string) =>
160
+ setRule((current) => ({
161
+ ...current,
162
+ clauses: current.clauses.filter((clause) => clause.id !== clauseId),
163
+ }));
164
+
165
+ const addWiden = () =>
166
+ setRule((current) => ({
167
+ ...current,
168
+ widen: { anchorCount: Math.max(selectedMessageIds.length, 1) },
169
+ }));
170
+
171
+ const removeWiden = () =>
172
+ setRule((current) => ({ ...current, widen: undefined }));
173
+
174
+ const changeMatchOperator = (matchOperator: MatchOperator) =>
175
+ setRule((current) => ({ ...current, matchOperator }));
176
+
177
+ const changeMove = (mailboxId: string) =>
178
+ setRule((current) => ({
179
+ ...current,
180
+ moveMailboxId: mailboxId || undefined,
181
+ }));
182
+
183
+ const changeScope = (scope: RuleScope) =>
184
+ setRule((current) => ({ ...current, scope }));
185
+
186
+ const changeName = (name: string) =>
187
+ setRule((current) => ({ ...current, name }));
188
+
189
+ const changeUntil = (until: string) =>
190
+ setRule((current) => ({ ...current, until }));
191
+
192
+ const commit = () => {
193
+ const draft = ruleToDraft(rule, anchorMessageId);
194
+ if (rule.scope === "once") {
195
+ organizeJob.start(draft);
196
+ return;
197
+ }
198
+ createFilter.createFilter(
199
+ draft,
200
+ rule.scope === "standing" ? "standing" : "temporary",
201
+ (rule.name ?? "").trim(),
202
+ );
203
+ };
204
+
205
+ if (organizeJob.isStarting || organizeJob.isRunning || organizeJob.isDone) {
206
+ return (
207
+ <JobProgress
208
+ progress={organizeJob.progress}
209
+ isDone={organizeJob.isDone}
210
+ senderFallback={senderFallback}
211
+ onClose={onClose}
212
+ />
213
+ );
214
+ }
215
+
216
+ if (createFilter.isPending) {
217
+ return <SavingState />;
218
+ }
219
+
220
+ if (createFilter.isSuccess) {
221
+ return <FilterSaved onClose={onClose} />;
222
+ }
223
+
224
+ if (createFilter.isError) {
225
+ return <CommitError onRetry={createFilter.reset} onClose={onClose} />;
226
+ }
227
+
228
+ return (
229
+ <FilterRuleEditor
230
+ rule={rule}
231
+ folders={folders}
232
+ preview={preview}
233
+ semanticAvailable={!semanticUnavailable}
234
+ clauseFields={SUPPORTED_CLAUSE_FIELDS}
235
+ clauseEdit={clauseEdit}
236
+ onStartAddClause={startAddClause}
237
+ onStartEditClause={startEditClause}
238
+ onRemoveClause={removeClause}
239
+ onChangeDraftField={changeDraftField}
240
+ onChangeDraftValue={changeDraftValue}
241
+ onSubmitClause={submitClause}
242
+ onCancelClause={() => setClauseEdit(undefined)}
243
+ onAddWiden={addWiden}
244
+ onRemoveWiden={removeWiden}
245
+ onChangeMatchOperator={changeMatchOperator}
246
+ onChangeMove={changeMove}
247
+ onChangeScope={changeScope}
248
+ onChangeName={changeName}
249
+ onChangeUntil={changeUntil}
250
+ onCommit={commit}
251
+ onCancel={onClose}
252
+ />
253
+ );
254
+ }
255
+
256
+ function JobProgress({
257
+ progress,
258
+ isDone,
259
+ senderFallback,
260
+ onClose,
261
+ }: {
262
+ progress: ReturnType<typeof useOrganizeJob>["progress"];
263
+ isDone: boolean;
264
+ senderFallback: boolean;
265
+ onClose: () => void;
266
+ }) {
267
+ const failed = progress.state === "Failed";
268
+ return (
269
+ <div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
270
+ {!isDone ? (
271
+ <Loader2 className="size-8 animate-spin text-accent-2" />
272
+ ) : failed ? (
273
+ <span className="text-sm font-semibold text-danger">
274
+ Organize failed
275
+ </span>
276
+ ) : (
277
+ <CheckCircle2 className="size-8 text-positive" />
278
+ )}
279
+
280
+ {!isDone && (
281
+ <p className="text-sm font-medium text-fg">
282
+ {senderFallback
283
+ ? "Organizing mail from these senders…"
284
+ : "Organizing similar mail…"}
285
+ </p>
286
+ )}
287
+
288
+ {isDone && !failed && (
289
+ <div className="text-sm text-fg">
290
+ <p className="font-medium">Done</p>
291
+ <p className="mt-1 text-xs text-fg-muted">
292
+ {progress.appliedCount} of {progress.matchedCount} moved
293
+ {progress.failedCount > 0
294
+ ? ` · ${progress.failedCount} failed`
295
+ : ""}
296
+ .
297
+ </p>
298
+ </div>
299
+ )}
300
+
301
+ {isDone && failed && (
302
+ <p className="max-w-xs text-xs text-fg-muted">
303
+ {progress.errorMessage || "Something went wrong. Please try again."}
304
+ </p>
305
+ )}
306
+
307
+ {isDone && (
308
+ <Button variant="primary" onClick={onClose} className="mt-2">
309
+ Done
310
+ </Button>
311
+ )}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ function SavingState() {
317
+ return (
318
+ <div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
319
+ <Loader2 className="size-8 animate-spin text-accent-2" />
320
+ <p className="text-sm font-medium text-fg">Saving rule…</p>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function FilterSaved({ onClose }: { onClose: () => void }) {
326
+ return (
327
+ <div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
328
+ <CheckCircle2 className="size-8 text-positive" />
329
+ <p className="text-sm font-medium text-fg">Filter saved</p>
330
+ <p className="max-w-xs text-xs text-fg-muted">
331
+ You can see it, and when it expires, under Settings › Filters.
332
+ </p>
333
+ <Button variant="primary" onClick={onClose} className="mt-2">
334
+ Done
335
+ </Button>
336
+ </div>
337
+ );
338
+ }
339
+
340
+ function CommitError({
341
+ onRetry,
342
+ onClose,
343
+ }: {
344
+ onRetry: () => void;
345
+ onClose: () => void;
346
+ }) {
347
+ return (
348
+ <div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
349
+ <p className="text-sm font-medium text-danger">Couldn't save the rule</p>
350
+ <p className="max-w-xs text-xs text-fg-muted">Please try again.</p>
351
+ <div className="mt-2 flex gap-2">
352
+ <Button variant="primary" onClick={onRetry}>
353
+ Try again
354
+ </Button>
355
+ <Button variant="ghost" onClick={onClose}>
356
+ Not now
357
+ </Button>
358
+ </div>
359
+ </div>
360
+ );
361
+ }
@@ -5,18 +5,17 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
5
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
6
  import type { ReactNode } from "react";
7
7
  import { ErrorBannerProvider } from "@/components/ui/ErrorBannerProvider";
8
- import { OrganizePanel } from "./OrganizePanel";
8
+ import { OrganizeRuleEditor } from "./OrganizeRuleEditor";
9
9
  import type { FolderOption } from "./SomethingElsePanel";
10
10
  import { SomethingElsePanel } from "./SomethingElsePanel";
11
11
 
12
12
  /**
13
- * `Flows/Smart Organize` — the guided select-similar organize flow (issue
14
- * #211), rendered from the real web-client components, not a prototype copy:
15
- * the same {@link OrganizePanel} the desktop dialog and the mobile sheet use,
16
- * and the same {@link SomethingElsePanel} the flow seeds from. The prototype's
17
- * interactive `Inbox` / `Walkthrough` stories live alongside these in the
18
- * workbench; these show each in-sheet panel in isolation so they can't drift
19
- * from what ships.
13
+ * `Flows/Smart Organize` — the Organize surface as the chip editor (RFC 038 D1),
14
+ * rendered from the real web-client component the desktop dialog and the mobile
15
+ * sheet both mount, not a prototype copy. Each story opens the editor on a
16
+ * seeded state a widen-anchored rule, the sender-fallback clauses (#251), a
17
+ * pre-seeded standing rule, and the capability-absent cases so the surface
18
+ * can't drift from what ships.
20
19
  */
21
20
 
22
21
  const ACCOUNT_ID = "acc-1";
@@ -57,8 +56,8 @@ const FOLDER_OPTIONS: FolderOption[] = [
57
56
  ];
58
57
 
59
58
  /**
60
- * A QueryClient pre-seeded with the account's mailboxes, so the sentence's
61
- * folder picker renders its real options without a network round-trip.
59
+ * A QueryClient pre-seeded with the account's mailboxes, so the editor's folder
60
+ * picker renders its real options without a network round-trip.
62
61
  */
63
62
  function seededClient(): QueryClient {
64
63
  const client = new QueryClient({
@@ -71,7 +70,7 @@ function seededClient(): QueryClient {
71
70
  return client;
72
71
  }
73
72
 
74
- /** Phone frame + a bottom sheet holding the panel, mirroring the mobile home. */
73
+ /** Phone frame + a bottom sheet holding the editor, mirroring the mobile home. */
75
74
  function SheetStage({ children }: { children: ReactNode }) {
76
75
  return (
77
76
  <QueryClientProvider client={seededClient()}>
@@ -109,44 +108,29 @@ export default meta;
109
108
 
110
109
  type Story = StoryObj;
111
110
 
112
- /** The organize sentence on a widened selection — folder picker + four scopes. */
111
+ /** The rule editor on a widened selection — the widen chip and a live count. */
113
112
  export const Organize: Story = {
114
113
  render: () => (
115
114
  <SheetStage>
116
- <OrganizePanel
115
+ <OrganizeRuleEditor
117
116
  accountId={ACCOUNT_ID}
118
- mailboxId="mbx-inbox"
119
117
  selectedMessageIds={["msg-1", "msg-2", "msg-3"]}
120
- matchPredicate={{
121
- anchorMessageId: "msg-1",
122
- matchOperator: "And",
123
- literalClauses: [],
124
- }}
125
- matchedCount={47}
118
+ seedCount={47}
126
119
  onClose={() => undefined}
127
120
  />
128
121
  </SheetStage>
129
122
  ),
130
123
  };
131
124
 
132
- /**
133
- * A larger widen — the same sentence over a broad match set, as when the anchor
134
- * is a common sender.
135
- */
125
+ /** A larger widen — the same editor over a broad match set. */
136
126
  export const FromSearch: Story = {
137
127
  name: "From Search",
138
128
  render: () => (
139
129
  <SheetStage>
140
- <OrganizePanel
130
+ <OrganizeRuleEditor
141
131
  accountId={ACCOUNT_ID}
142
- mailboxId="mbx-inbox"
143
132
  selectedMessageIds={["msg-1"]}
144
- matchPredicate={{
145
- anchorMessageId: "msg-1",
146
- matchOperator: "And",
147
- literalClauses: [],
148
- }}
149
- matchedCount={412}
133
+ seedCount={412}
150
134
  seedMailboxId="mbx-archive"
151
135
  onClose={() => undefined}
152
136
  />
@@ -154,22 +138,16 @@ export const FromSearch: Story = {
154
138
  ),
155
139
  };
156
140
 
157
- /** The standing scope pre-selected — the "Always keep…" sentence + filter name. */
141
+ /** The standing scope pre-selected — the rule that keeps working on new mail. */
158
142
  export const AlwaysRule: Story = {
159
143
  name: "Always Rule",
160
144
  render: () => (
161
145
  <SheetStage>
162
- <OrganizePanel
146
+ <OrganizeRuleEditor
163
147
  accountId={ACCOUNT_ID}
164
- mailboxId="mbx-inbox"
165
148
  selectedMessageIds={["msg-1", "msg-2"]}
166
- matchPredicate={{
167
- anchorMessageId: "msg-1",
168
- matchOperator: "And",
169
- literalClauses: [],
170
- }}
171
- matchedCount={47}
172
- initialScope="standing"
149
+ seedCount={47}
150
+ seedScope="standing"
173
151
  seedMailboxId="mbx-travel"
174
152
  onClose={() => undefined}
175
153
  />
@@ -177,22 +155,20 @@ export const AlwaysRule: Story = {
177
155
  ),
178
156
  };
179
157
 
180
- /** The widen matched nothing — the sentence organizes just the selection. */
181
- export const NoSimilarFound: Story = {
182
- name: "No Similar Found",
158
+ /**
159
+ * The deployment ships no vector pipeline and the selection has no senders to
160
+ * match on — the widen is not offered and the rule opens empty, awaiting a
161
+ * clause (#226/#201).
162
+ */
163
+ export const SemanticUnavailable: Story = {
164
+ name: "Semantic Unavailable",
183
165
  render: () => (
184
166
  <SheetStage>
185
- <OrganizePanel
167
+ <OrganizeRuleEditor
186
168
  accountId={ACCOUNT_ID}
187
- mailboxId="mbx-inbox"
188
169
  selectedMessageIds={["msg-1", "msg-2"]}
189
- matchPredicate={{
190
- anchorMessageId: "msg-1",
191
- matchOperator: "And",
192
- literalClauses: [],
193
- }}
194
- matchedCount={0}
195
- fallback
170
+ seedCount={0}
171
+ semanticUnavailable
196
172
  onClose={() => undefined}
197
173
  />
198
174
  </SheetStage>
@@ -200,25 +176,21 @@ export const NoSimilarFound: Story = {
200
176
  };
201
177
 
202
178
  /**
203
- * The deployment ships no vector pipeline, so the server ran no semantic widen
204
- * the sentence organizes just the selection and the heading names the real
205
- * reason (#226/#201).
179
+ * No vector pipeline, but the selection has senders to match on — the widen fell
180
+ * back to the sender `From` clauses (#251). The senders span different domains,
181
+ * so the fallback keeps one editable `From` chip per address, combined with
182
+ * "or". Every commit carries exactly these clauses.
206
183
  */
207
- export const SemanticUnavailable: Story = {
208
- name: "Semantic Unavailable",
184
+ export const SenderFallback: Story = {
185
+ name: "Sender Fallback",
209
186
  render: () => (
210
187
  <SheetStage>
211
- <OrganizePanel
188
+ <OrganizeRuleEditor
212
189
  accountId={ACCOUNT_ID}
213
- mailboxId="mbx-inbox"
214
190
  selectedMessageIds={["msg-1", "msg-2"]}
215
- matchPredicate={{
216
- anchorMessageId: "msg-1",
217
- matchOperator: "And",
218
- literalClauses: [],
219
- }}
220
- matchedCount={0}
191
+ seedCount={128}
221
192
  semanticUnavailable
193
+ senders={["npm@github.com", "noreply@medium.com"]}
222
194
  onClose={() => undefined}
223
195
  />
224
196
  </SheetStage>
@@ -226,59 +198,40 @@ export const SemanticUnavailable: Story = {
226
198
  };
227
199
 
228
200
  /**
229
- * No vector pipeline, but the selection has senders to match on — the widen
230
- * fell back to matching all mail from those senders. The heading names the
231
- * senders and states the real semantics, and every commit scope (including the
232
- * standing filter) carries the sender `From` clauses, so it reaches the widened
233
- * set and keeps working on future mail.
201
+ * The sender fallback where every selected sender shares one registrable domain
202
+ * (RFC 038 D2): the per-address `From` chips collapse to a single `FromDomain`
203
+ * chip "anyone at github.com" rather than listing addresses. Matches
204
+ * `sub.github.com` too, never a look-alike like `github.com.evil.example`.
234
205
  */
235
- export const SenderFallback: Story = {
236
- name: "Sender Fallback",
206
+ export const SenderFallbackDomain: Story = {
207
+ name: "Sender Fallback (domain)",
237
208
  render: () => (
238
209
  <SheetStage>
239
- <OrganizePanel
210
+ <OrganizeRuleEditor
240
211
  accountId={ACCOUNT_ID}
241
- mailboxId="mbx-inbox"
242
- selectedMessageIds={["msg-1", "msg-2"]}
243
- matchPredicate={{
244
- matchOperator: "Or",
245
- literalClauses: [
246
- { field: "From", value: "npm@github.com" },
247
- { field: "From", value: "notifications@github.com" },
248
- ],
249
- }}
250
- matchedCount={128}
212
+ selectedMessageIds={["msg-1", "msg-2", "msg-3"]}
213
+ seedCount={342}
251
214
  semanticUnavailable
252
- senders={["npm@github.com", "notifications@github.com"]}
215
+ senders={[
216
+ "npm@github.com",
217
+ "notifications@github.com",
218
+ "ci@sub.github.com",
219
+ ]}
253
220
  onClose={() => undefined}
254
221
  />
255
222
  </SheetStage>
256
223
  ),
257
224
  };
258
225
 
259
- /**
260
- * The sender fallback with a standing rule pre-selected — the "Always keep mail
261
- * from these senders" sentence, the filter that will match future mail at index
262
- * time.
263
- */
226
+ /** The sender fallback with a standing rule pre-selected. */
264
227
  export const SenderFallbackStanding: Story = {
265
228
  name: "Sender Fallback (standing)",
266
229
  render: () => (
267
230
  <SheetStage>
268
- <OrganizePanel
231
+ <OrganizeRuleEditor
269
232
  accountId={ACCOUNT_ID}
270
- mailboxId="mbx-inbox"
271
233
  selectedMessageIds={["msg-1", "msg-2", "msg-3", "msg-4"]}
272
- matchPredicate={{
273
- matchOperator: "Or",
274
- literalClauses: [
275
- { field: "From", value: "npm@github.com" },
276
- { field: "From", value: "notifications@github.com" },
277
- { field: "From", value: "noreply@medium.com" },
278
- { field: "From", value: "digest@substack.com" },
279
- ],
280
- }}
281
- matchedCount={412}
234
+ seedCount={412}
282
235
  semanticUnavailable
283
236
  senders={[
284
237
  "npm@github.com",
@@ -286,7 +239,7 @@ export const SenderFallbackStanding: Story = {
286
239
  "noreply@medium.com",
287
240
  "digest@substack.com",
288
241
  ]}
289
- initialScope="standing"
242
+ seedScope="standing"
290
243
  seedMailboxId="mbx-archive"
291
244
  onClose={() => undefined}
292
245
  />