@remit/web-client 0.0.142 → 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 +4 -3
- 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/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": {
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type SimilarMessageLinkComponent,
|
|
7
7
|
type SimilarState,
|
|
8
8
|
} from "@remit/ui";
|
|
9
|
-
import { Link } from "@tanstack/react-router";
|
|
10
9
|
import { Sparkles } from "lucide-react";
|
|
11
10
|
import { useCallback, useState } from "react";
|
|
12
11
|
import { useIntelligenceData } from "@/hooks/useIntelligenceData";
|
|
@@ -15,6 +14,7 @@ import { useUpdateAddressFlags } from "@/hooks/useUpdateAddressFlags";
|
|
|
15
14
|
import { isRescueCandidate } from "@/lib/rescue-candidates";
|
|
16
15
|
import { recordRescueSentToJunk } from "@/lib/rescue-telemetry";
|
|
17
16
|
import { useTelemetry } from "@/lib/telemetry-context";
|
|
17
|
+
import { NavLink } from "@/routing";
|
|
18
18
|
|
|
19
19
|
export interface IntelligencePaneProps {
|
|
20
20
|
onClose: () => void;
|
|
@@ -315,15 +315,16 @@ function WiredPanel({
|
|
|
315
315
|
ariaLabel,
|
|
316
316
|
children,
|
|
317
317
|
}) => (
|
|
318
|
-
<
|
|
318
|
+
<NavLink
|
|
319
319
|
to="/mail/$mailboxId"
|
|
320
320
|
params={{ mailboxId: rowMailboxId }}
|
|
321
321
|
search={(prev) => ({ ...prev, selectedMessageId: messageId })}
|
|
322
|
+
variant="row"
|
|
322
323
|
className={className}
|
|
323
324
|
aria-label={ariaLabel}
|
|
324
325
|
>
|
|
325
326
|
{children}
|
|
326
|
-
</
|
|
327
|
+
</NavLink>
|
|
327
328
|
);
|
|
328
329
|
|
|
329
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
|
|
|
@@ -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
|
+
}
|