@remit/web-client 0.0.163 → 0.0.165

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/web-client",
3
- "version": "0.0.163",
3
+ "version": "0.0.165",
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": {
@@ -29,9 +29,7 @@ type AmplifyCall = {
29
29
  };
30
30
 
31
31
  declare global {
32
- // eslint-disable-next-line no-var
33
32
  var __AMPLIFY_MOCKS__: { configureCalls: AmplifyCall[] } | undefined;
34
- // eslint-disable-next-line no-var
35
33
  var __CACHE_BUST__: number | undefined;
36
34
  }
37
35
 
@@ -6,7 +6,6 @@ import { type AuthProvider, noneAuthProvider } from "./provider";
6
6
  type RequestFn = (req: Request) => Promise<Request>;
7
7
 
8
8
  declare global {
9
- // eslint-disable-next-line no-var
10
9
  var __REMIT_CLIENT_MOCKS__: { requestFns: RequestFn[] } | undefined;
11
10
  }
12
11
 
@@ -8,9 +8,7 @@ type AmplifyAuthMocks = {
8
8
  };
9
9
 
10
10
  declare global {
11
- // eslint-disable-next-line no-var
12
11
  var __AMPLIFY_AUTH_MOCKS__: AmplifyAuthMocks | undefined;
13
- // eslint-disable-next-line no-var
14
12
  var __AMPLIFY_CONFIG_MOCKS__:
15
13
  | { configured: boolean; configureCalls: number }
16
14
  | undefined;
@@ -1,6 +1,6 @@
1
+ import { cn } from "@remit/ui";
1
2
  import { X } from "lucide-react";
2
3
  import { type ReactNode, useEffect, useRef } from "react";
3
- import { cn } from "@/lib/utils";
4
4
 
5
5
  interface DrawerProps {
6
6
  isOpen: boolean;
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { ThreadRowData } from "@remit/ui";
4
+ import { wizardScopeFor } from "@remit/ui";
5
+ import { resolveBriefSelectionScope } from "./DailyBrief";
6
+
7
+ /**
8
+ * The brief spans accounts and folders, so it resolves both from the selection
9
+ * itself. Which of the two a selection spans decides what the wizard's folder
10
+ * and rule steps can say: told to pick a single account, a selection already
11
+ * inside one account has nothing to act on and the flow dead-ends (#525).
12
+ */
13
+
14
+ const row = (
15
+ id: string,
16
+ accountId: string,
17
+ mailboxId: string,
18
+ ): ThreadRowData => ({
19
+ id,
20
+ accountId,
21
+ mailboxId,
22
+ fromName: "Booking.com",
23
+ fromEmail: "noreply@booking.com",
24
+ subject: "Booking confirmation",
25
+ snippet: "Your stay is confirmed",
26
+ timeLabel: "Jul 2",
27
+ });
28
+
29
+ const rows: ThreadRowData[] = [
30
+ row("m1", "acc-personal", "mbx-inbox"),
31
+ row("m2", "acc-personal", "mbx-archive"),
32
+ row("m3", "acc-work", "mbx-inbox"),
33
+ ];
34
+
35
+ const scopeOf = (...ids: string[]) =>
36
+ resolveBriefSelectionScope(rows, new Set(ids));
37
+
38
+ describe("the scope a brief selection resolves to", () => {
39
+ it("carries the folder restriction when one account holds every row", () => {
40
+ const scope = scopeOf("m1", "m2");
41
+ assert.equal(scope.restriction, "spansFolders");
42
+ assert.equal(scope.accountId, "acc-personal");
43
+ assert.equal(scope.mailboxId, undefined);
44
+ assert.match(scope.moveDisabledHint ?? "", /spans 2 folders/);
45
+ });
46
+
47
+ it("reaches the folder step told about folders, not accounts", () => {
48
+ const scope = scopeOf("m1", "m2");
49
+ const wizard = wizardScopeFor(scope.accountId, scope.restriction);
50
+ assert.match(wizard.destination ?? "", /within one folder/);
51
+ assert.doesNotMatch(wizard.destination ?? "", /account/);
52
+ // The account is settled, so the preview behind a widened door has one to
53
+ // count against and the match step keeps it.
54
+ assert.equal(wizard.accountId, "acc-personal");
55
+ });
56
+
57
+ it("carries the account restriction when the rows span accounts", () => {
58
+ const scope = scopeOf("m1", "m3");
59
+ assert.equal(scope.restriction, "spansAccounts");
60
+ assert.equal(scope.accountId, undefined);
61
+ assert.match(
62
+ wizardScopeFor(scope.accountId, scope.restriction).destination ?? "",
63
+ /single account/,
64
+ );
65
+ });
66
+
67
+ it("carries no restriction from one account and one folder", () => {
68
+ const scope = scopeOf("m1");
69
+ assert.equal(scope.restriction, undefined);
70
+ assert.equal(scope.mailboxId, "mbx-inbox");
71
+ assert.equal(
72
+ wizardScopeFor(scope.accountId, scope.restriction).destination,
73
+ undefined,
74
+ );
75
+ });
76
+ });
@@ -54,6 +54,7 @@ import {
54
54
  partitionSpamResults,
55
55
  RefreshButton,
56
56
  type SearchResult,
57
+ type SelectionRestriction,
57
58
  SelectionTopBar,
58
59
  SpamResultsOffer,
59
60
  setBriefCategoryInQuery,
@@ -185,6 +186,8 @@ export interface BriefSelectionScope {
185
186
  accountId?: string;
186
187
  /** Source folder, when every selected row shares one. */
187
188
  mailboxId?: string;
189
+ /** Which scope the selection spans more of than those verbs can take. */
190
+ restriction?: SelectionRestriction;
188
191
  /** Why folder-scoped verbs are withheld, in the toolbar's own words. */
189
192
  moveDisabledHint?: string;
190
193
  }
@@ -214,6 +217,7 @@ export const resolveBriefSelectionScope = (
214
217
  }
215
218
  if (accountIds.size > 1) {
216
219
  return {
220
+ restriction: "spansAccounts",
217
221
  moveDisabledHint:
218
222
  "Move only works within one account — clear selection or pick messages from a single account",
219
223
  };
@@ -222,6 +226,7 @@ export const resolveBriefSelectionScope = (
222
226
  if (mailboxIds.size > 1) {
223
227
  return {
224
228
  accountId,
229
+ restriction: "spansFolders",
225
230
  moveDisabledHint: `Move only works within one folder — this selection spans ${mailboxIds.size} folders`,
226
231
  };
227
232
  }
@@ -362,7 +367,7 @@ function BriefSelectionChrome({
362
367
  accountId={scope.accountId}
363
368
  mailboxId={scope.mailboxId}
364
369
  selection={wizardSelection}
365
- crossAccount={scope.moveDisabledHint !== undefined}
370
+ selectionRestriction={scope.restriction}
366
371
  onFinished={exitSelection}
367
372
  />
368
373
  }
@@ -95,7 +95,7 @@ function StarredWizardHost({
95
95
  <SelectionWizardHost
96
96
  verb={verb}
97
97
  selection={selection}
98
- crossAccount
98
+ selectionRestriction="spansAccounts"
99
99
  onFinished={exitSelection}
100
100
  />
101
101
  );
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildCidResolver,
3
3
  type CidResolver,
4
+ cn,
4
5
  type EmailRenderCategory,
5
6
  MessageBodyView,
6
7
  } from "@remit/ui";
@@ -8,7 +9,6 @@ import { useMemo, useState } from "react";
8
9
  import { useIsDark } from "@/hooks/useIsDark";
9
10
  import { useMessageBodyContent } from "@/hooks/useMessageBodyContent";
10
11
  import { useToggleTrusted } from "@/hooks/useToggleTrusted";
11
- import { cn } from "@/lib/utils";
12
12
  import { BlockedImagesNotice } from "./BlockedImagesNotice";
13
13
  import { MessageBodyErrorBanner } from "./MessageBodyErrorBanner";
14
14
 
@@ -4,7 +4,7 @@ import type {
4
4
  RemitImapThreadMessageResponse,
5
5
  } from "@remit/api-http-client/types.gen.ts";
6
6
  import type { ThreadMessageData } from "@remit/ui";
7
- import { AddressList, CollapsedMessage, ExpandedMessage } from "@remit/ui";
7
+ import { AddressList, CollapsedMessage, cn, ExpandedMessage } from "@remit/ui";
8
8
  import { useQuery } from "@tanstack/react-query";
9
9
  import { BadgeCheck, Paperclip, Star } from "lucide-react";
10
10
  import { useState } from "react";
@@ -13,7 +13,6 @@ import { ErrorState } from "@/components/ui/ErrorState";
13
13
  import { isMessageNotFoundError } from "@/components/ui/error-banners";
14
14
  import { toDisplayCategory } from "@/lib/display-category";
15
15
  import { formatDatePreset } from "@/lib/format";
16
- import { cn } from "@/lib/utils";
17
16
  import { AutoMovedIndicator } from "./AutoMovedIndicator";
18
17
  import { MessageActionMenu } from "./MessageActionMenu";
19
18
  import { MessageAttachments } from "./MessageAttachments";
@@ -2,6 +2,7 @@ import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/type
2
2
  import {
3
3
  Banner,
4
4
  ConfirmDialog,
5
+ cn,
5
6
  type Density,
6
7
  deriveIsMultiSelectMode,
7
8
  type MessageListFilter,
@@ -51,7 +52,6 @@ import { tabStopId } from "@/lib/list-focus";
51
52
  import { useListHeaderChrome } from "@/lib/list-header-chrome";
52
53
  import { listVerbRequest } from "@/lib/list-verb-request";
53
54
  import { shouldExitSelectionOnNavigate } from "@/lib/selection-mode";
54
- import { cn } from "@/lib/utils";
55
55
  import { useSelectionWizard, useWizardStepValue } from "@/lib/wizard-history";
56
56
  import { retainOpenPanels } from "@/routing";
57
57
  import { LabelApplyTrigger } from "./LabelApplyTrigger";
@@ -1351,7 +1351,7 @@ export const MessageList = ({
1351
1351
  accountId={accountId}
1352
1352
  mailboxId={mailboxId}
1353
1353
  selection={wizardSelection}
1354
- crossAccount={moveDisabledHint !== undefined}
1354
+ selectionRestriction={moveDisabledHint ? "spansAccounts" : undefined}
1355
1355
  escalated={escalatedSelection}
1356
1356
  escalatedProgress={escalation.progress}
1357
1357
  onFinished={exitSelection}
@@ -14,6 +14,7 @@ import { messageOperationsDescribeMessageOptions } from "@remit/api-http-client/
14
14
  import {
15
15
  ComfortableRowBody,
16
16
  CompactRowBody,
17
+ cn,
17
18
  comfortableRowClass,
18
19
  compactRowClass,
19
20
  type Density,
@@ -26,7 +27,6 @@ import {
26
27
  } from "@remit/ui";
27
28
  import { useQueryClient } from "@tanstack/react-query";
28
29
  import { type MouseEvent, memo, type ReactNode, useCallback } from "react";
29
- import { cn } from "@/lib/utils";
30
30
  import { NavLink } from "@/routing";
31
31
  import { useThreadRowInteraction } from "./ThreadListInteraction";
32
32
  import { useModifierSelect } from "./useModifierSelect";
@@ -1,5 +1,5 @@
1
1
  import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
2
- import { Button, type FolderTreeNode, FolderTreePicker } from "@remit/ui";
2
+ import { Button, cn, type FolderTreeNode, FolderTreePicker } from "@remit/ui";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { FolderInput } from "lucide-react";
5
5
  import {
@@ -18,7 +18,6 @@ import { useCreateMailbox } from "@/hooks/useCreateMailbox";
18
18
  import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
19
19
  import { useIsDesktop } from "@/hooks/useMediaQuery";
20
20
  import { buildMoveOptions, folderDelimiter } from "@/lib/move-options";
21
- import { cn } from "@/lib/utils";
22
21
 
23
22
  interface MoveToTriggerProps {
24
23
  accountId: string;
@@ -23,6 +23,7 @@ import type {
23
23
  RemitImapOutboxMessageStatus,
24
24
  } from "@remit/api-http-client/types.gen.ts";
25
25
  import {
26
+ cn,
26
27
  LIST_ROW_SELECTOR,
27
28
  OutboxRow,
28
29
  ReadingPaneEmpty,
@@ -62,7 +63,6 @@ import {
62
63
  import { isOutboxListRow, isUnsendableStatus } from "@/lib/outbox-status";
63
64
  import { normalizeSearchQuery } from "@/lib/search-query";
64
65
  import { parseSearchTokens } from "@/lib/search-tokens";
65
- import { cn } from "@/lib/utils";
66
66
  import { retainOpenPanels } from "@/routing";
67
67
 
68
68
  /* ------------------------------------------------------------------ */
@@ -2,8 +2,6 @@ import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@t
2
2
  import {
3
3
  type ClauseDraft,
4
4
  type ClauseEditState,
5
- crossAccountDestinationReason,
6
- crossAccountRuleReason,
7
5
  derivePropertyClauses,
8
6
  deriveSenderClauses,
9
7
  dominantSender,
@@ -14,6 +12,7 @@ import {
14
12
  type RuleClause,
15
13
  type RunState,
16
14
  type SearchConversion,
15
+ type SelectionRestriction,
17
16
  SelectionWizard,
18
17
  type StepId,
19
18
  searchConversionNotice,
@@ -25,6 +24,7 @@ import {
25
24
  type Verb,
26
25
  type WizardDraft,
27
26
  type WizardMessage,
27
+ wizardScopeFor,
28
28
  } from "@remit/ui";
29
29
  import { useQuery } from "@tanstack/react-query";
30
30
  import { useBlocker } from "@tanstack/react-router";
@@ -125,8 +125,12 @@ export interface SelectionWizardHostProps {
125
125
  /** Where the selection was made, so a run invalidates the listing it changed. */
126
126
  mailboxId?: string;
127
127
  selection: readonly WizardSelectionMessage[];
128
- /** The ticked rows span more than one account, so no rule can be created. */
129
- crossAccount?: boolean;
128
+ /**
129
+ * Which scope the ticked rows span more of than the folder and rule steps can
130
+ * take. The surface that resolved the selection knows which of the two it is,
131
+ * and the wizard states that one (#525).
132
+ */
133
+ selectionRestriction?: SelectionRestriction;
130
134
  /** Present when the selection is a predicate rather than the ticked rows. */
131
135
  escalated?: EscalatedSelection;
132
136
  /**
@@ -257,7 +261,7 @@ function SelectionWizardSession({
257
261
  accountId,
258
262
  mailboxId,
259
263
  selection,
260
- crossAccount = false,
264
+ selectionRestriction,
261
265
  escalated: escalatedSelection,
262
266
  escalatedProgress,
263
267
  searchConversion,
@@ -451,18 +455,16 @@ function SelectionWizardSession({
451
455
 
452
456
  // A filter, a folder, and the preview that counts a widened door all belong to
453
457
  // one account, so a selection spanning accounts reaches none of them and is
454
- // told so on the step that asks (#477 5.5, #523). The one-off scope and the
455
- // ticked rows act on the messages themselves and are unaffected.
456
- const accountScoped = !crossAccount && !!accountId;
457
- const ruleRestriction = accountScoped ? undefined : crossAccountRuleReason;
458
- const folderRestriction = accountScoped
459
- ? undefined
460
- : crossAccountDestinationReason;
458
+ // told so on the step that asks (#477 5.5, #523). A selection spanning folders
459
+ // of one account is told about folders instead, and keeps its account: the
460
+ // preview behind a widened door has something to count against. The one-off
461
+ // scope and the ticked rows act on the messages themselves and are unaffected.
462
+ const wizardScope = wizardScopeFor(accountId, selectionRestriction);
461
463
  const restrictionFor = (step: StepId): string | undefined => {
462
- if (step === "folder") return folderRestriction;
464
+ if (step === "folder") return wizardScope.destination;
463
465
  if (step !== "rule") return undefined;
464
466
  return named.scope === "standing" || named.scope === "until"
465
- ? ruleRestriction
467
+ ? wizardScope.rule
466
468
  : undefined;
467
469
  };
468
470
  const blockedReason =
@@ -940,7 +942,7 @@ function SelectionWizardSession({
940
942
  match={{
941
943
  selectedCount: selection.length,
942
944
  mode,
943
- accountId: accountScoped ? accountId : undefined,
945
+ accountId: wizardScope.accountId,
944
946
  onModeChange: changeMode,
945
947
  semanticUnavailable,
946
948
  semanticErrorDetail:
@@ -977,13 +979,13 @@ function SelectionWizardSession({
977
979
  onSelect: (moveMailboxId) =>
978
980
  setDraft((held) => ({ ...held, moveMailboxId })),
979
981
  onCreateFolder: accountId ? createFolderIn : undefined,
980
- restriction: folderRestriction,
982
+ restriction: wizardScope.destination,
981
983
  }}
982
984
  rule={{
983
985
  draft: named,
984
986
  onScopeChange: (scope) => setDraft((held) => ({ ...held, scope })),
985
987
  onUntilChange: (until) => setDraft((held) => ({ ...held, until })),
986
- restriction: ruleRestriction,
988
+ restriction: wizardScope.rule,
987
989
  }}
988
990
  name={{
989
991
  name: named.name ?? "",
@@ -1056,7 +1058,7 @@ export function SelectionWizardHost(props: SelectionWizardHostProps) {
1056
1058
  accounts,
1057
1059
  ),
1058
1060
  selection: EMPTY_SELECTION,
1059
- crossAccount: false,
1061
+ selectionRestriction: undefined,
1060
1062
  escalated: undefined,
1061
1063
  searchConversion: conversion,
1062
1064
  }
@@ -1212,7 +1212,6 @@ function StepSync({
1212
1212
  smtpStartTls: effectiveSmtp?.smtpStartTls ?? false,
1213
1213
  },
1214
1214
  });
1215
- // eslint-disable-next-line react-hooks/exhaustive-deps
1216
1215
  }, []);
1217
1216
 
1218
1217
  // Poll sync status every 2s once account is created. Stop polling once the
@@ -9,6 +9,7 @@ import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.
9
9
  import {
10
10
  Button,
11
11
  ComposeLanguageSetting,
12
+ cn,
12
13
  Input,
13
14
  PasswordInput,
14
15
  Select,
@@ -27,7 +28,6 @@ import {
27
28
  PROVIDER_PRESETS,
28
29
  type ProviderPreset,
29
30
  } from "../../lib/provider-presets.js";
30
- import { cn } from "../../lib/utils";
31
31
  import {
32
32
  appendAppPasswordHint,
33
33
  computeSmtpAutoFill,
@@ -1,5 +1,5 @@
1
+ import { cn } from "@remit/ui";
1
2
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { cn } from "@/lib/utils";
3
3
 
4
4
  interface DropdownMenuProps {
5
5
  trigger: React.ReactNode;
@@ -1,5 +1,5 @@
1
+ import { cn } from "@remit/ui";
1
2
  import { AlertCircle, AlertTriangle, Info, X } from "lucide-react";
2
- import { cn } from "../../lib/utils";
3
3
  import type {
4
4
  ErrorBannerAction,
5
5
  ErrorBannerSeverity,
@@ -1,7 +1,6 @@
1
- import { DialogBackdrop, Kbd, KEY_HINT_GROUPS } from "@remit/ui";
1
+ import { cn, DialogBackdrop, Kbd, KEY_HINT_GROUPS } from "@remit/ui";
2
2
  import { X } from "lucide-react";
3
3
  import { Fragment, useCallback, useEffect } from "react";
4
- import { cn } from "@/lib/utils";
5
4
 
6
5
  interface KeyboardShortcutsModalProps {
7
6
  isOpen: boolean;
@@ -54,7 +54,6 @@ interface RawRuntimeConfig {
54
54
  }
55
55
 
56
56
  declare global {
57
- // eslint-disable-next-line no-var
58
57
  var __REMIT_CONFIG__: RawRuntimeConfig | undefined;
59
58
  }
60
59
 
@@ -1,6 +0,0 @@
1
- export { ComposeForm } from "./ComposeForm.js";
2
- export type { ComposeMode, ComposeState } from "./ComposeProvider.js";
3
- export { ComposeProvider, useCompose } from "./ComposeProvider.js";
4
- export { FullCompose } from "./FullCompose.js";
5
- export { InlineCompose } from "./InlineCompose.js";
6
- export { MobileComposeSheet } from "./MobileComposeSheet.js";
package/src/lib/utils.ts DELETED
@@ -1,4 +0,0 @@
1
- import { type ClassValue, clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
@@ -1,67 +0,0 @@
1
- export interface Thread {
2
- threadId: string;
3
- mailboxId: string;
4
- subject: string;
5
- snippet: string;
6
- participants: string[];
7
- participantCount: number;
8
- messageCount: number;
9
- hasUnread: boolean;
10
- unreadCount: number;
11
- hasAttachments: boolean;
12
- isStarred: boolean;
13
- lastMessageAt: string;
14
- createdAt: string;
15
- updatedAt: string;
16
- }
17
-
18
- export interface Message {
19
- messageId: string;
20
- threadId: string;
21
- mailboxId: string;
22
- subject: string;
23
- from: EmailAddress;
24
- to: EmailAddress[];
25
- cc: EmailAddress[];
26
- bcc: EmailAddress[];
27
- snippet: string;
28
- isRead: boolean;
29
- isStarred: boolean;
30
- hasAttachments: boolean;
31
- attachments: Attachment[];
32
- receivedAt: string;
33
- createdAt: string;
34
- updatedAt: string;
35
- }
36
-
37
- export interface EmailAddress {
38
- name?: string;
39
- address: string;
40
- }
41
-
42
- export interface Attachment {
43
- attachmentId: string;
44
- messageId: string;
45
- filename: string;
46
- mimeType: string;
47
- size: number;
48
- }
49
-
50
- export interface Mailbox {
51
- mailboxId: string;
52
- accountId: string;
53
- name: string;
54
- fullPath: string;
55
- messageCount: number;
56
- unreadCount: number;
57
- createdAt: string;
58
- updatedAt: string;
59
- }
60
-
61
- export interface Account {
62
- accountId: string;
63
- email: string;
64
- name?: string;
65
- createdAt: string;
66
- updatedAt: string;
67
- }