@remit/web-client 0.0.149 → 0.0.150

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.149",
3
+ "version": "0.0.150",
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": {
@@ -244,7 +244,7 @@ export function MailSidebarAdapter({
244
244
  return (
245
245
  <NavLink
246
246
  to="/mail/outbox"
247
- search={{ q: undefined, selectedOutboxMessageId: undefined }}
247
+ search={{ q: undefined }}
248
248
  onClick={() => onClick?.()}
249
249
  className={className}
250
250
  aria-label={ariaLabel}
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * OutboxPane — compound component for the outbox view (/mail/outbox route).
3
3
  *
4
- * Usage in mail.tsx:
4
+ * Usage in the list layout route:
5
5
  *
6
- * <OutboxPane>
7
- * <AppShellSlotted
6
+ * <OutboxPane outboxMessageId={useOutboxDraftId()}>
7
+ * <MailShell
8
8
  * list={<OutboxPane.List />}
9
- * reading={<OutboxPane.Reading />}
9
+ * reading={<Outlet />}
10
10
  * />
11
11
  * </OutboxPane>
12
12
  *
@@ -29,7 +29,7 @@ import {
29
29
  useRovingFocus,
30
30
  } from "@remit/ui";
31
31
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
32
- import { useNavigate, useSearch } from "@tanstack/react-router";
32
+ import { useNavigate } from "@tanstack/react-router";
33
33
  import {
34
34
  AlertCircle,
35
35
  AlertTriangle,
@@ -143,9 +143,11 @@ interface OutboxPaneContextValue {
143
143
  /** True when that query asks for a filter the outbox cannot apply. */
144
144
  unsupportedQuery: boolean;
145
145
  isLoading: boolean;
146
+ /** The message the address names, which is the one the list highlights. */
146
147
  selectedMessageId: string | undefined;
147
148
  selectedMessage: RemitImapOutboxMessageResponse | undefined;
148
- onSelectMessage: (id: string | undefined) => void;
149
+ onOpenMessage: (outboxMessageId: string) => void;
150
+ onCloseMessage: () => void;
149
151
  }
150
152
 
151
153
  const OutboxPaneCtx = createContext<OutboxPaneContextValue | null>(null);
@@ -161,16 +163,16 @@ function useOutboxPane(): OutboxPaneContextValue {
161
163
  /* ------------------------------------------------------------------ */
162
164
 
163
165
  interface OutboxPaneProps {
166
+ /** The open message, as the address states it. */
167
+ outboxMessageId: string | undefined;
164
168
  children: ReactNode;
165
169
  }
166
170
 
167
- function OutboxPaneProvider({ children }: OutboxPaneProps) {
171
+ function OutboxPaneProvider({
172
+ outboxMessageId: selectedMessageId,
173
+ children,
174
+ }: OutboxPaneProps) {
168
175
  const navigate = useNavigate();
169
- // Read selectedOutboxMessageId from the URL directly — this avoids threading
170
- // a prop through the parent shell for a param that only outbox uses.
171
- const { selectedOutboxMessageId: selectedMessageId } = useSearch({
172
- strict: false,
173
- }) as { selectedOutboxMessageId?: string };
174
176
 
175
177
  const { data: outboxResponse, isLoading } = useQuery(
176
178
  outboxOperationsListOutboxMessagesOptions(),
@@ -210,19 +212,21 @@ function OutboxPaneProvider({ children }: OutboxPaneProps) {
210
212
  [messages, selectedMessageId],
211
213
  );
212
214
 
213
- const handleSelectMessage = useCallback(
214
- (id: string | undefined) => {
215
+ const handleOpenMessage = useCallback(
216
+ (outboxMessageId: string) => {
215
217
  navigate({
216
- to: "/mail/outbox",
217
- search: (prev) => ({
218
- ...prev,
219
- selectedOutboxMessageId: id,
220
- }),
218
+ to: "/mail/outbox/draft/$outboxMessageId",
219
+ params: { outboxMessageId },
220
+ search: (prev) => prev,
221
221
  });
222
222
  },
223
223
  [navigate],
224
224
  );
225
225
 
226
+ const handleCloseMessage = useCallback(() => {
227
+ navigate({ to: "/mail/outbox", search: (prev) => prev });
228
+ }, [navigate]);
229
+
226
230
  const ctx: OutboxPaneContextValue = {
227
231
  messages,
228
232
  hasQuery,
@@ -230,7 +234,8 @@ function OutboxPaneProvider({ children }: OutboxPaneProps) {
230
234
  isLoading,
231
235
  selectedMessageId,
232
236
  selectedMessage,
233
- onSelectMessage: handleSelectMessage,
237
+ onOpenMessage: handleOpenMessage,
238
+ onCloseMessage: handleCloseMessage,
234
239
  };
235
240
 
236
241
  return (
@@ -438,7 +443,7 @@ function OutboxList() {
438
443
  unsupportedQuery,
439
444
  isLoading,
440
445
  selectedMessageId,
441
- onSelectMessage,
446
+ onOpenMessage,
442
447
  } = useOutboxPane();
443
448
  const listRef = useRef<HTMLDivElement>(null);
444
449
  useRovingFocus({ containerRef: listRef, itemSelector: LIST_ROW_SELECTOR });
@@ -482,7 +487,7 @@ function OutboxList() {
482
487
  key={message.outboxMessageId}
483
488
  message={message}
484
489
  isSelected={selectedMessageId === message.outboxMessageId}
485
- onSelect={() => onSelectMessage(message.outboxMessageId)}
490
+ onSelect={() => onOpenMessage(message.outboxMessageId)}
486
491
  />
487
492
  ))}
488
493
  </div>
@@ -529,7 +534,8 @@ function OutboxPhone() {
529
534
  isLoading,
530
535
  selectedMessageId,
531
536
  selectedMessage,
532
- onSelectMessage,
537
+ onOpenMessage,
538
+ onCloseMessage,
533
539
  } = useOutboxPane();
534
540
  const listRef = useRef<HTMLDivElement>(null);
535
541
  useRovingFocus({ containerRef: listRef, itemSelector: LIST_ROW_SELECTOR });
@@ -544,10 +550,7 @@ function OutboxPhone() {
544
550
 
545
551
  if (selectedMessage) {
546
552
  return (
547
- <OutboxMessageDetail
548
- message={selectedMessage}
549
- onBack={() => onSelectMessage(undefined)}
550
- />
553
+ <OutboxMessageDetail message={selectedMessage} onBack={onCloseMessage} />
551
554
  );
552
555
  }
553
556
 
@@ -579,7 +582,7 @@ function OutboxPhone() {
579
582
  key={message.outboxMessageId}
580
583
  message={message}
581
584
  isSelected={selectedMessageId === message.outboxMessageId}
582
- onSelect={() => onSelectMessage(message.outboxMessageId)}
585
+ onSelect={() => onOpenMessage(message.outboxMessageId)}
583
586
  />
584
587
  ))}
585
588
  </div>
@@ -24,9 +24,12 @@ export const flaggedSearchSchema = listSearch.extend({
24
24
  selectedMessageId: z.string().optional(),
25
25
  });
26
26
 
27
- export const outboxSearchSchema = listSearch.extend({
28
- selectedOutboxMessageId: z.string().optional(),
29
- });
27
+ /**
28
+ * The outbox's open message is a path segment
29
+ * (`/mail/outbox/draft/<outboxMessageId>`), so the query carries nothing but
30
+ * the search. An old link's selection param is dropped here.
31
+ */
32
+ export const outboxSearchSchema = listSearch.extend({});
30
33
 
31
34
  export const mailboxSearchSchema = listSearch.extend({
32
35
  selectedMessageId: z.string().optional(),
@@ -34,6 +34,7 @@ import { Route as MailFlaggedIndexRouteImport } from './routes/mail/flagged/inde
34
34
  import { Route as MailOutboxIndexRouteImport } from './routes/mail/outbox/index'
35
35
  import { Route as MailBriefThreadIdIndexRouteImport } from './routes/mail/brief/$threadId/index'
36
36
  import { Route as MailBriefThreadIdMessageIdRouteImport } from './routes/mail/brief/$threadId/$messageId'
37
+ import { Route as MailOutboxDraftOutboxMessageIdRouteImport } from './routes/mail/outbox/draft/$outboxMessageId'
37
38
 
38
39
  const IndexRoute = IndexRouteImport.update({
39
40
  id: '/',
@@ -161,6 +162,12 @@ const MailBriefThreadIdMessageIdRoute =
161
162
  path: '/$messageId',
162
163
  getParentRoute: () => MailBriefThreadIdRoute,
163
164
  } as any)
165
+ const MailOutboxDraftOutboxMessageIdRoute =
166
+ MailOutboxDraftOutboxMessageIdRouteImport.update({
167
+ id: '/draft/$outboxMessageId',
168
+ path: '/draft/$outboxMessageId',
169
+ getParentRoute: () => MailOutboxRoute,
170
+ } as any)
164
171
 
165
172
  export interface FileRoutesByFullPath {
166
173
  '/': typeof IndexRoute
@@ -187,6 +194,7 @@ export interface FileRoutesByFullPath {
187
194
  '/mail/flagged/': typeof MailFlaggedIndexRoute
188
195
  '/mail/outbox/': typeof MailOutboxIndexRoute
189
196
  '/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
197
+ '/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
190
198
  '/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
191
199
  }
192
200
  export interface FileRoutesByTo {
@@ -207,6 +215,7 @@ export interface FileRoutesByTo {
207
215
  '/mail/flagged': typeof MailFlaggedIndexRoute
208
216
  '/mail/outbox': typeof MailOutboxIndexRoute
209
217
  '/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
218
+ '/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
210
219
  '/mail/brief/$threadId': typeof MailBriefThreadIdIndexRoute
211
220
  }
212
221
  export interface FileRoutesById {
@@ -235,6 +244,7 @@ export interface FileRoutesById {
235
244
  '/mail/flagged/': typeof MailFlaggedIndexRoute
236
245
  '/mail/outbox/': typeof MailOutboxIndexRoute
237
246
  '/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
247
+ '/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
238
248
  '/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
239
249
  }
240
250
  export interface FileRouteTypes {
@@ -264,6 +274,7 @@ export interface FileRouteTypes {
264
274
  | '/mail/flagged/'
265
275
  | '/mail/outbox/'
266
276
  | '/mail/brief/$threadId/$messageId'
277
+ | '/mail/outbox/draft/$outboxMessageId'
267
278
  | '/mail/brief/$threadId/'
268
279
  fileRoutesByTo: FileRoutesByTo
269
280
  to:
@@ -284,6 +295,7 @@ export interface FileRouteTypes {
284
295
  | '/mail/flagged'
285
296
  | '/mail/outbox'
286
297
  | '/mail/brief/$threadId/$messageId'
298
+ | '/mail/outbox/draft/$outboxMessageId'
287
299
  | '/mail/brief/$threadId'
288
300
  id:
289
301
  | '__root__'
@@ -311,6 +323,7 @@ export interface FileRouteTypes {
311
323
  | '/mail/flagged/'
312
324
  | '/mail/outbox/'
313
325
  | '/mail/brief/$threadId/$messageId'
326
+ | '/mail/outbox/draft/$outboxMessageId'
314
327
  | '/mail/brief/$threadId/'
315
328
  fileRoutesById: FileRoutesById
316
329
  }
@@ -498,6 +511,13 @@ declare module '@tanstack/react-router' {
498
511
  preLoaderRoute: typeof MailBriefThreadIdMessageIdRouteImport
499
512
  parentRoute: typeof MailBriefThreadIdRoute
500
513
  }
514
+ '/mail/outbox/draft/$outboxMessageId': {
515
+ id: '/mail/outbox/draft/$outboxMessageId'
516
+ path: '/draft/$outboxMessageId'
517
+ fullPath: '/mail/outbox/draft/$outboxMessageId'
518
+ preLoaderRoute: typeof MailOutboxDraftOutboxMessageIdRouteImport
519
+ parentRoute: typeof MailOutboxRoute
520
+ }
501
521
  }
502
522
  }
503
523
 
@@ -554,10 +574,12 @@ const MailFlaggedRouteWithChildren = MailFlaggedRoute._addFileChildren(
554
574
 
555
575
  interface MailOutboxRouteChildren {
556
576
  MailOutboxIndexRoute: typeof MailOutboxIndexRoute
577
+ MailOutboxDraftOutboxMessageIdRoute: typeof MailOutboxDraftOutboxMessageIdRoute
557
578
  }
558
579
 
559
580
  const MailOutboxRouteChildren: MailOutboxRouteChildren = {
560
581
  MailOutboxIndexRoute: MailOutboxIndexRoute,
582
+ MailOutboxDraftOutboxMessageIdRoute: MailOutboxDraftOutboxMessageIdRoute,
561
583
  }
562
584
 
563
585
  const MailOutboxRouteWithChildren = MailOutboxRoute._addFileChildren(
@@ -0,0 +1,19 @@
1
+ /**
2
+ * /mail/outbox/draft/$outboxMessageId — one outbox message open in the
3
+ * reading pane.
4
+ *
5
+ * An outbox message is addressable on its own: it belongs to no thread and no
6
+ * folder, so the id is the whole address. The `draft/` prefix keeps the id in a
7
+ * named segment, so the literal siblings the outbox will grow stay
8
+ * distinguishable from it.
9
+ */
10
+ import { createFileRoute } from "@tanstack/react-router";
11
+ import { OutboxPane } from "@/components/mail/OutboxPane";
12
+
13
+ function OutboxDraftPane() {
14
+ return <OutboxPane.Reading />;
15
+ }
16
+
17
+ export const Route = createFileRoute("/mail/outbox/draft/$outboxMessageId")({
18
+ component: OutboxDraftPane,
19
+ });
@@ -1,18 +1,23 @@
1
1
  /**
2
2
  * /mail/outbox — one of the four list layouts. Two panes, list and reading;
3
3
  * there is no intelligence rail for mail that has not been sent yet.
4
+ *
5
+ * The open message is the segment below this one, which is why nothing here
6
+ * reads a selection out of the query.
4
7
  */
5
8
  import { createFileRoute, Outlet } from "@tanstack/react-router";
6
9
  import { MailShell } from "@/components/layout/MailShell";
7
10
  import { OutboxPane } from "@/components/mail/OutboxPane";
8
11
  import { useSearchMirror } from "@/hooks/useSearchMirror";
9
12
  import { outboxSearchSchema } from "@/lib/mail-search";
13
+ import { useOutboxDraftId } from "@/routing";
10
14
 
11
15
  function OutboxLayout() {
16
+ const outboxMessageId = useOutboxDraftId();
12
17
  useSearchMirror({ to: "/mail/outbox" });
13
18
 
14
19
  return (
15
- <OutboxPane>
20
+ <OutboxPane outboxMessageId={outboxMessageId}>
16
21
  <MailShell
17
22
  phone={<OutboxPane.Phone />}
18
23
  list={<OutboxPane.List />}
@@ -10,3 +10,4 @@ export {
10
10
  useOpenPanel,
11
11
  } from "./fragment";
12
12
  export { NavLink, type NavLinkProps } from "./nav-link";
13
+ export { useOutboxDraftId } from "./outbox-draft";
@@ -0,0 +1,17 @@
1
+ import { useParams } from "@tanstack/react-router";
2
+
3
+ /**
4
+ * The outbox message the reader has open, read off the path.
5
+ *
6
+ * The message is a child route of the list, and the list layout mounts the
7
+ * panes above the `Outlet` — so it asks the router which of its children
8
+ * matched rather than reading a param it does not own. The `from` names a real
9
+ * route, so a segment that does not exist fails to compile.
10
+ */
11
+ export function useOutboxDraftId(): string | undefined {
12
+ const draft = useParams({
13
+ from: "/mail/outbox/draft/$outboxMessageId",
14
+ shouldThrow: false,
15
+ });
16
+ return draft?.outboxMessageId;
17
+ }