@remit/web-client 0.0.141 → 0.0.143
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/components/mail/IntelligencePane.tsx +11 -11
- package/src/components/mail/MailNav.tsx +3 -7
- package/src/components/mail/MailSidebarAdapter.tsx +15 -16
- package/src/components/mail/MessageRow.tsx +5 -12
- package/src/components/ui/KeyboardShortcutsModal.tsx +10 -16
- package/src/routing/fragment.ts +35 -0
- package/src/routing/index.ts +7 -0
- package/src/routing/nav-link.tsx +58 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.143",
|
|
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": {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
2
|
import {
|
|
3
|
+
DialogBackdrop,
|
|
3
4
|
IntelligencePanel,
|
|
4
5
|
type IntelligenceQuickActions,
|
|
5
6
|
type SimilarMessageLinkComponent,
|
|
6
7
|
type SimilarState,
|
|
7
8
|
} from "@remit/ui";
|
|
8
|
-
import { Link } from "@tanstack/react-router";
|
|
9
9
|
import { Sparkles } from "lucide-react";
|
|
10
10
|
import { useCallback, useState } from "react";
|
|
11
11
|
import { useIntelligenceData } from "@/hooks/useIntelligenceData";
|
|
@@ -14,6 +14,7 @@ import { useUpdateAddressFlags } from "@/hooks/useUpdateAddressFlags";
|
|
|
14
14
|
import { isRescueCandidate } from "@/lib/rescue-candidates";
|
|
15
15
|
import { recordRescueSentToJunk } from "@/lib/rescue-telemetry";
|
|
16
16
|
import { useTelemetry } from "@/lib/telemetry-context";
|
|
17
|
+
import { NavLink } from "@/routing";
|
|
17
18
|
|
|
18
19
|
export interface IntelligencePaneProps {
|
|
19
20
|
onClose: () => void;
|
|
@@ -154,19 +155,17 @@ function ReclassifyDialog({
|
|
|
154
155
|
}) {
|
|
155
156
|
if (!isOpen) return null;
|
|
156
157
|
return (
|
|
157
|
-
<div
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
159
|
+
<DialogBackdrop
|
|
160
|
+
label="Dismiss reclassify sender"
|
|
161
|
+
onDismiss={onCancel}
|
|
162
|
+
className="backdrop-blur-sm"
|
|
163
|
+
/>
|
|
163
164
|
<div
|
|
164
165
|
role="dialog"
|
|
165
166
|
aria-modal="true"
|
|
166
167
|
aria-label="Reclassify sender"
|
|
167
168
|
className="relative z-10 w-full max-w-sm rounded-sm border border-line bg-surface p-6 shadow-lg"
|
|
168
|
-
onClick={(e) => e.stopPropagation()}
|
|
169
|
-
onKeyDown={(e) => e.stopPropagation()}
|
|
170
169
|
>
|
|
171
170
|
<h2 className="text-lg font-semibold">Reclassify this sender</h2>
|
|
172
171
|
<p className="mt-2 text-sm text-fg-muted">
|
|
@@ -316,15 +315,16 @@ function WiredPanel({
|
|
|
316
315
|
ariaLabel,
|
|
317
316
|
children,
|
|
318
317
|
}) => (
|
|
319
|
-
<
|
|
318
|
+
<NavLink
|
|
320
319
|
to="/mail/$mailboxId"
|
|
321
320
|
params={{ mailboxId: rowMailboxId }}
|
|
322
321
|
search={(prev) => ({ ...prev, selectedMessageId: messageId })}
|
|
322
|
+
variant="row"
|
|
323
323
|
className={className}
|
|
324
324
|
aria-label={ariaLabel}
|
|
325
325
|
>
|
|
326
326
|
{children}
|
|
327
|
-
</
|
|
327
|
+
</NavLink>
|
|
328
328
|
);
|
|
329
329
|
|
|
330
330
|
if (!data) {
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
12
12
|
import { useAppShellLayout } from "@remit/ui";
|
|
13
|
-
import { Link } from "@tanstack/react-router";
|
|
14
13
|
import { LogOut, Settings } from "lucide-react";
|
|
15
14
|
import { AccountSession } from "@/auth/AccountSession";
|
|
16
15
|
import { MailSidebarAdapter } from "@/components/mail/MailSidebarAdapter";
|
|
17
16
|
import { BugReportButton } from "@/components/ui/BugReportButton";
|
|
17
|
+
import { NavLink } from "@/routing";
|
|
18
18
|
|
|
19
19
|
interface MailNavProps {
|
|
20
20
|
accounts: RemitImapAccountResponse[];
|
|
@@ -39,14 +39,10 @@ export function MailNav({ accounts, onMailboxSelect }: MailNavProps) {
|
|
|
39
39
|
/>
|
|
40
40
|
</div>
|
|
41
41
|
<div className="border-t border-line px-2 py-2">
|
|
42
|
-
<
|
|
43
|
-
to="/settings/accounts"
|
|
44
|
-
onClick={onMailboxSelect}
|
|
45
|
-
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm text-fg-muted transition-colors hover:bg-surface hover:text-fg"
|
|
46
|
-
>
|
|
42
|
+
<NavLink to="/settings/accounts" onClick={onMailboxSelect}>
|
|
47
43
|
<Settings className="size-4 shrink-0" />
|
|
48
44
|
<span className="flex-1 truncate text-left">Settings</span>
|
|
49
|
-
</
|
|
45
|
+
</NavLink>
|
|
50
46
|
<BugReportButton />
|
|
51
47
|
<AccountSession>
|
|
52
48
|
{({ signOut }) => (
|
|
@@ -10,12 +10,7 @@ import type {
|
|
|
10
10
|
import type { NavAccount, NavLinkComponent, NavMailboxRole } from "@remit/ui";
|
|
11
11
|
import { NavSidebar } from "@remit/ui";
|
|
12
12
|
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
13
|
-
import {
|
|
14
|
-
Link,
|
|
15
|
-
useLocation,
|
|
16
|
-
useNavigate,
|
|
17
|
-
useParams,
|
|
18
|
-
} from "@tanstack/react-router";
|
|
13
|
+
import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
|
|
19
14
|
import { useCallback, useMemo, useState } from "react";
|
|
20
15
|
import { useTranslation } from "react-i18next";
|
|
21
16
|
import {
|
|
@@ -30,6 +25,7 @@ import {
|
|
|
30
25
|
removeSavedSearch,
|
|
31
26
|
saveSearch,
|
|
32
27
|
} from "@/lib/saved-searches";
|
|
28
|
+
import { NavLink } from "@/routing";
|
|
33
29
|
|
|
34
30
|
interface MailSidebarAdapterProps {
|
|
35
31
|
accounts: RemitImapAccountResponse[];
|
|
@@ -232,21 +228,24 @@ export function MailSidebarAdapter({
|
|
|
232
228
|
}) => {
|
|
233
229
|
if (navId === "brief") {
|
|
234
230
|
return (
|
|
235
|
-
<
|
|
231
|
+
<NavLink
|
|
236
232
|
to="/mail"
|
|
237
233
|
search={{ q: undefined, selectedMessageId: undefined }}
|
|
234
|
+
// Every other mail list lives under /mail, so the default prefix
|
|
235
|
+
// match would leave the brief marked current on all of them.
|
|
236
|
+
activeOptions={{ exact: true, includeSearch: false }}
|
|
238
237
|
onClick={() => onClick?.()}
|
|
239
238
|
className={className}
|
|
240
239
|
aria-label={ariaLabel}
|
|
241
240
|
title={title}
|
|
242
241
|
>
|
|
243
242
|
{children}
|
|
244
|
-
</
|
|
243
|
+
</NavLink>
|
|
245
244
|
);
|
|
246
245
|
}
|
|
247
246
|
if (navId === "outbox") {
|
|
248
247
|
return (
|
|
249
|
-
<
|
|
248
|
+
<NavLink
|
|
250
249
|
to="/mail/outbox"
|
|
251
250
|
search={{ q: undefined, selectedOutboxMessageId: undefined }}
|
|
252
251
|
onClick={() => onClick?.()}
|
|
@@ -255,12 +254,12 @@ export function MailSidebarAdapter({
|
|
|
255
254
|
title={title}
|
|
256
255
|
>
|
|
257
256
|
{children}
|
|
258
|
-
</
|
|
257
|
+
</NavLink>
|
|
259
258
|
);
|
|
260
259
|
}
|
|
261
260
|
if (navId === "flagged") {
|
|
262
261
|
return (
|
|
263
|
-
<
|
|
262
|
+
<NavLink
|
|
264
263
|
to="/mail/flagged"
|
|
265
264
|
search={{ q: undefined, selectedMessageId: undefined }}
|
|
266
265
|
onClick={() => onClick?.()}
|
|
@@ -269,12 +268,12 @@ export function MailSidebarAdapter({
|
|
|
269
268
|
title={title}
|
|
270
269
|
>
|
|
271
270
|
{children}
|
|
272
|
-
</
|
|
271
|
+
</NavLink>
|
|
273
272
|
);
|
|
274
273
|
}
|
|
275
274
|
if (navId === "settings") {
|
|
276
275
|
return (
|
|
277
|
-
<
|
|
276
|
+
<NavLink
|
|
278
277
|
to="/settings/accounts"
|
|
279
278
|
onClick={() => onClick?.()}
|
|
280
279
|
className={className}
|
|
@@ -282,11 +281,11 @@ export function MailSidebarAdapter({
|
|
|
282
281
|
title={title}
|
|
283
282
|
>
|
|
284
283
|
{children}
|
|
285
|
-
</
|
|
284
|
+
</NavLink>
|
|
286
285
|
);
|
|
287
286
|
}
|
|
288
287
|
return (
|
|
289
|
-
<
|
|
288
|
+
<NavLink
|
|
290
289
|
to="/mail/$mailboxId"
|
|
291
290
|
params={{ mailboxId: navId }}
|
|
292
291
|
// Drop any stale search query / selected message when switching mailbox.
|
|
@@ -300,7 +299,7 @@ export function MailSidebarAdapter({
|
|
|
300
299
|
title={title}
|
|
301
300
|
>
|
|
302
301
|
{children}
|
|
303
|
-
</
|
|
302
|
+
</NavLink>
|
|
304
303
|
);
|
|
305
304
|
};
|
|
306
305
|
|
|
@@ -25,17 +25,12 @@ import {
|
|
|
25
25
|
useLongPress,
|
|
26
26
|
} from "@remit/ui";
|
|
27
27
|
import { useQueryClient } from "@tanstack/react-query";
|
|
28
|
-
import { Link } from "@tanstack/react-router";
|
|
29
28
|
import { type MouseEvent, memo, type ReactNode, useCallback } from "react";
|
|
30
29
|
import { cn } from "@/lib/utils";
|
|
30
|
+
import { NavLink } from "@/routing";
|
|
31
31
|
import { useThreadRowInteraction } from "./ThreadListInteraction";
|
|
32
32
|
import { useModifierSelect } from "./useModifierSelect";
|
|
33
33
|
|
|
34
|
-
interface MailboxLinkSearch {
|
|
35
|
-
selectedMessageId?: string;
|
|
36
|
-
q?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
export interface MessageRowSelection {
|
|
40
35
|
isChecked: boolean;
|
|
41
36
|
onToggleCheck: (messageId: string) => void;
|
|
@@ -254,18 +249,16 @@ const MessageRowComponent = ({
|
|
|
254
249
|
}
|
|
255
250
|
|
|
256
251
|
return (
|
|
257
|
-
<
|
|
252
|
+
<NavLink
|
|
258
253
|
to="/mail/$mailboxId"
|
|
259
254
|
params={{ mailboxId: linkMailboxId }}
|
|
260
|
-
search={(prev
|
|
261
|
-
|
|
262
|
-
selectedMessageId: messageId,
|
|
263
|
-
})}
|
|
255
|
+
search={(prev) => ({ ...prev, selectedMessageId: messageId })}
|
|
256
|
+
variant="row"
|
|
264
257
|
{...interactionProps}
|
|
265
258
|
className={className}
|
|
266
259
|
>
|
|
267
260
|
{body}
|
|
268
|
-
</
|
|
261
|
+
</NavLink>
|
|
269
262
|
);
|
|
270
263
|
};
|
|
271
264
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Kbd, KEY_HINT_GROUPS } from "@remit/ui";
|
|
1
|
+
import { DialogBackdrop, Kbd, KEY_HINT_GROUPS } from "@remit/ui";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import { Fragment, useCallback, useEffect } from "react";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
@@ -37,29 +37,23 @@ export const KeyboardShortcutsModal = ({
|
|
|
37
37
|
if (!isOpen) return null;
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
|
-
<div
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
onKeyDown={(e) => e.key === "Escape" && onClose()}
|
|
47
|
-
>
|
|
48
|
-
{/* Backdrop */}
|
|
49
|
-
<div className="absolute inset-0 bg-canvas/80 backdrop-blur-sm" />
|
|
40
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
41
|
+
<DialogBackdrop
|
|
42
|
+
label="Dismiss keyboard shortcuts"
|
|
43
|
+
onDismiss={onClose}
|
|
44
|
+
className="backdrop-blur-sm"
|
|
45
|
+
/>
|
|
50
46
|
|
|
51
47
|
{/* Modal */}
|
|
52
|
-
{/* biome-ignore lint/a11y/noStaticElementInteractions: stops event propagation from the modal content; keyboard is handled by the outer wrapper */}
|
|
53
48
|
<div
|
|
49
|
+
role="dialog"
|
|
50
|
+
aria-modal="true"
|
|
51
|
+
aria-label="Keyboard shortcuts"
|
|
54
52
|
className={cn(
|
|
55
53
|
"relative z-10 max-h-[85vh] w-full max-w-2xl overflow-y-auto",
|
|
56
54
|
"rounded-lg border border-line bg-surface shadow-lg",
|
|
57
55
|
"p-6",
|
|
58
56
|
)}
|
|
59
|
-
onClick={(e) => e.stopPropagation()}
|
|
60
|
-
onKeyDown={(e) => {
|
|
61
|
-
if (e.key !== "Escape") e.stopPropagation();
|
|
62
|
-
}}
|
|
63
57
|
>
|
|
64
58
|
{/* Header */}
|
|
65
59
|
<div className="mb-6 flex items-center justify-between">
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useLocation } from "@tanstack/react-router";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The fragment tier carries panel visibility and nothing else — no identity, no
|
|
6
|
+
* payload, no selection. A panel qualifies only when its own copy is
|
|
7
|
+
* independent of in-memory data, so a cold load of the URL reproduces it
|
|
8
|
+
* exactly.
|
|
9
|
+
*/
|
|
10
|
+
export const panelFragments = [
|
|
11
|
+
"intelligence",
|
|
12
|
+
"nav",
|
|
13
|
+
"shortcuts",
|
|
14
|
+
"filters",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type PanelFragment = (typeof panelFragments)[number];
|
|
18
|
+
|
|
19
|
+
const panelFragmentSchema = z.enum(panelFragments);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A hand-edited or stale URL is a normal thing to receive, so an unrecognised
|
|
23
|
+
* fragment reads as "no panel open" instead of throwing.
|
|
24
|
+
*/
|
|
25
|
+
export function parsePanelFragment(hash: string): PanelFragment | undefined {
|
|
26
|
+
const parsed = panelFragmentSchema.safeParse(hash);
|
|
27
|
+
return parsed.success ? parsed.data : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The panel the URL currently asks for, if any. */
|
|
31
|
+
export function useOpenPanel(): PanelFragment | undefined {
|
|
32
|
+
return useLocation({
|
|
33
|
+
select: (location) => parsePanelFragment(location.hash),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NavLinkSurface } from "@remit/ui";
|
|
2
|
+
import type {
|
|
3
|
+
AnyRouter,
|
|
4
|
+
CreateLinkProps,
|
|
5
|
+
LinkComponentProps,
|
|
6
|
+
RegisteredRouter,
|
|
7
|
+
} from "@tanstack/react-router";
|
|
8
|
+
import { createLink } from "@tanstack/react-router";
|
|
9
|
+
import type { ReactElement } from "react";
|
|
10
|
+
import type { PanelFragment } from "./fragment";
|
|
11
|
+
|
|
12
|
+
const RouterNavLink = createLink(NavLinkSurface);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The router's own link props, minus the raw `hash`: the fragment tier is a
|
|
16
|
+
* closed union of panel names, so `fragment` is the only way to write it.
|
|
17
|
+
*/
|
|
18
|
+
export type NavLinkProps<
|
|
19
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
20
|
+
TFrom extends string = string,
|
|
21
|
+
TTo extends string | undefined = undefined,
|
|
22
|
+
TMaskFrom extends string = TFrom,
|
|
23
|
+
TMaskTo extends string = "",
|
|
24
|
+
> = LinkComponentProps<
|
|
25
|
+
typeof NavLinkSurface,
|
|
26
|
+
TRouter,
|
|
27
|
+
TFrom,
|
|
28
|
+
TTo,
|
|
29
|
+
TMaskFrom,
|
|
30
|
+
TMaskTo
|
|
31
|
+
> & {
|
|
32
|
+
fragment?: PanelFragment;
|
|
33
|
+
/**
|
|
34
|
+
* Closed off rather than omitted: `Omit` over these props is a mapped type,
|
|
35
|
+
* and the router's `to` / `params` / `search` inference cannot see through
|
|
36
|
+
* one.
|
|
37
|
+
*/
|
|
38
|
+
hash?: never;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The application's only navigation link. `createLink` keeps the router's `to` /
|
|
43
|
+
* `params` / `search` inference, so a route that does not exist or a param set
|
|
44
|
+
* that does not match it fails to compile instead of rendering a blank pane.
|
|
45
|
+
*/
|
|
46
|
+
export function NavLink<
|
|
47
|
+
TRouter extends AnyRouter = RegisteredRouter,
|
|
48
|
+
const TFrom extends string = string,
|
|
49
|
+
const TTo extends string | undefined = undefined,
|
|
50
|
+
const TMaskFrom extends string = TFrom,
|
|
51
|
+
const TMaskTo extends string = "",
|
|
52
|
+
>(props: NavLinkProps<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>): ReactElement;
|
|
53
|
+
export function NavLink({
|
|
54
|
+
fragment,
|
|
55
|
+
...props
|
|
56
|
+
}: CreateLinkProps & { fragment?: PanelFragment }): ReactElement {
|
|
57
|
+
return <RouterNavLink {...props} hash={fragment} />;
|
|
58
|
+
}
|