@remit/web-client 0.0.15 → 0.0.16
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/BriefPane.tsx +34 -5
- package/src/components/mail/FlaggedPane.tsx +30 -5
- package/src/components/mail/MailboxPane.tsx +9 -9
- package/src/components/mail/MessageToolbar.render.test.ts +29 -1
- package/src/components/mail/MessageToolbar.tsx +20 -28
- package/src/routes/mail.tsx +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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": {
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { unifiedThreadOperationsListAllThreadsOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
16
16
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ReadingPaneEmpty,
|
|
19
|
+
type SearchResult,
|
|
20
|
+
useAppShellLayout,
|
|
21
|
+
} from "@remit/ui";
|
|
18
22
|
import { useQuery } from "@tanstack/react-query";
|
|
19
23
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
|
20
24
|
import {
|
|
@@ -184,14 +188,20 @@ function BriefList() {
|
|
|
184
188
|
*/
|
|
185
189
|
function BriefReading() {
|
|
186
190
|
const { conversation } = useBriefPane();
|
|
187
|
-
const { onToggleIntelligence } = useMailContext();
|
|
191
|
+
const { intelligenceOpen, onToggleIntelligence } = useMailContext();
|
|
192
|
+
// The rail's own width gate, not the shell tier: between 1024 and 1280 the
|
|
193
|
+
// reading pane is mounted but the rail is not, so "enabled" would promise an
|
|
194
|
+
// open that cannot happen.
|
|
195
|
+
const railFits = useAppShellLayout()?.showIntelligencePane ?? false;
|
|
196
|
+
const hasThread = Boolean(conversation);
|
|
197
|
+
const canToggleIntelligence = railFits && hasThread;
|
|
188
198
|
|
|
189
199
|
return (
|
|
190
200
|
<section className="flex h-full w-full min-w-0 flex-col bg-canvas">
|
|
191
201
|
<MessageToolbar
|
|
192
|
-
hasThread={
|
|
193
|
-
intelligenceOpen={
|
|
194
|
-
|
|
202
|
+
hasThread={hasThread}
|
|
203
|
+
intelligenceOpen={canToggleIntelligence && intelligenceOpen}
|
|
204
|
+
canToggleIntelligence={canToggleIntelligence}
|
|
195
205
|
onToggleIntelligence={onToggleIntelligence}
|
|
196
206
|
/>
|
|
197
207
|
<div className="min-h-0 flex-1 overflow-hidden">
|
|
@@ -211,6 +221,24 @@ function BriefReading() {
|
|
|
211
221
|
);
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Intelligence pane: IntelligencePane for the thread open in the brief.
|
|
226
|
+
* Mount in the `intelligence` slot of `AppShellSlotted`. Only rendered ≥ 1280px.
|
|
227
|
+
*/
|
|
228
|
+
function BriefIntelligence() {
|
|
229
|
+
const { selectedThread } = useBriefPane();
|
|
230
|
+
const { onToggleIntelligence } = useMailContext();
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<IntelligencePane
|
|
234
|
+
onClose={onToggleIntelligence}
|
|
235
|
+
thread={selectedThread}
|
|
236
|
+
mailboxId={selectedThread?.mailboxId}
|
|
237
|
+
accountId={selectedThread?.accountId}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
214
242
|
/**
|
|
215
243
|
* Phone view: ConversationView when thread is open, or the DailyBrief list.
|
|
216
244
|
*/
|
|
@@ -272,6 +300,7 @@ function BriefPhone() {
|
|
|
272
300
|
const BriefPane = Object.assign(BriefPaneProvider, {
|
|
273
301
|
List: BriefList,
|
|
274
302
|
Reading: BriefReading,
|
|
303
|
+
Intelligence: BriefIntelligence,
|
|
275
304
|
Phone: BriefPhone,
|
|
276
305
|
});
|
|
277
306
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { unifiedThreadOperationsListAllThreadsOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
19
19
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
20
|
-
import { ReadingPaneEmpty } from "@remit/ui";
|
|
20
|
+
import { ReadingPaneEmpty, useAppShellLayout } from "@remit/ui";
|
|
21
21
|
import { useQuery } from "@tanstack/react-query";
|
|
22
22
|
import { useNavigate } from "@tanstack/react-router";
|
|
23
23
|
import {
|
|
@@ -128,14 +128,20 @@ function FlaggedListSlot() {
|
|
|
128
128
|
*/
|
|
129
129
|
function FlaggedReading() {
|
|
130
130
|
const { selectedThread } = useFlaggedPane();
|
|
131
|
-
const { onToggleIntelligence } = useMailContext();
|
|
131
|
+
const { intelligenceOpen, onToggleIntelligence } = useMailContext();
|
|
132
|
+
// The rail's own width gate, not the shell tier: between 1024 and 1280 the
|
|
133
|
+
// reading pane is mounted but the rail is not, so "enabled" would promise an
|
|
134
|
+
// open that cannot happen.
|
|
135
|
+
const railFits = useAppShellLayout()?.showIntelligencePane ?? false;
|
|
136
|
+
const hasThread = Boolean(selectedThread);
|
|
137
|
+
const canToggleIntelligence = railFits && hasThread;
|
|
132
138
|
|
|
133
139
|
return (
|
|
134
140
|
<section className="flex h-full w-full min-w-0 flex-col bg-canvas">
|
|
135
141
|
<MessageToolbar
|
|
136
|
-
hasThread={
|
|
137
|
-
intelligenceOpen={
|
|
138
|
-
|
|
142
|
+
hasThread={hasThread}
|
|
143
|
+
intelligenceOpen={canToggleIntelligence && intelligenceOpen}
|
|
144
|
+
canToggleIntelligence={canToggleIntelligence}
|
|
139
145
|
onToggleIntelligence={onToggleIntelligence}
|
|
140
146
|
/>
|
|
141
147
|
<div className="min-h-0 flex-1 overflow-hidden">
|
|
@@ -154,6 +160,24 @@ function FlaggedReading() {
|
|
|
154
160
|
);
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Intelligence pane: IntelligencePane for the open thread.
|
|
165
|
+
* Mount in the `intelligence` slot of `AppShellSlotted`. Only rendered ≥ 1280px.
|
|
166
|
+
*/
|
|
167
|
+
function FlaggedIntelligence() {
|
|
168
|
+
const { selectedThread } = useFlaggedPane();
|
|
169
|
+
const { onToggleIntelligence } = useMailContext();
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<IntelligencePane
|
|
173
|
+
onClose={onToggleIntelligence}
|
|
174
|
+
thread={selectedThread}
|
|
175
|
+
mailboxId={selectedThread?.mailboxId}
|
|
176
|
+
accountId={selectedThread?.accountId}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
/** Phone view: ConversationView when a thread is open, else the flat list. */
|
|
158
182
|
function FlaggedPhone() {
|
|
159
183
|
const { selectedThread, onCloseThread } = useFlaggedPane();
|
|
@@ -200,6 +224,7 @@ function FlaggedPhone() {
|
|
|
200
224
|
const FlaggedPane = Object.assign(FlaggedPaneProvider, {
|
|
201
225
|
List: FlaggedListSlot,
|
|
202
226
|
Reading: FlaggedReading,
|
|
227
|
+
Intelligence: FlaggedIntelligence,
|
|
203
228
|
Phone: FlaggedPhone,
|
|
204
229
|
});
|
|
205
230
|
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
ReadingPaneEmpty,
|
|
33
33
|
type RescueCandidate,
|
|
34
34
|
type SearchResult,
|
|
35
|
+
useAppShellLayout,
|
|
35
36
|
} from "@remit/ui";
|
|
36
37
|
import {
|
|
37
38
|
keepPreviousData,
|
|
@@ -179,7 +180,6 @@ interface MailboxPaneContextValue {
|
|
|
179
180
|
onSelectFilterCategory: (id: string) => void;
|
|
180
181
|
onToggleFilterAttribute: (id: string) => void;
|
|
181
182
|
onClearFilters: () => void;
|
|
182
|
-
showIntelligence: boolean;
|
|
183
183
|
intelligenceOpen: boolean;
|
|
184
184
|
onToggleIntelligence: () => void;
|
|
185
185
|
// List actions
|
|
@@ -782,13 +782,11 @@ function MailboxPaneProvider({
|
|
|
782
782
|
}
|
|
783
783
|
}, [normalizedSearchQuery, mailboxType, telemetry]);
|
|
784
784
|
|
|
785
|
-
const hasThread = Boolean(selectedThread);
|
|
786
785
|
const hasRemitDraftOpen =
|
|
787
786
|
isDraftsMailbox &&
|
|
788
787
|
composeState.isOpen &&
|
|
789
788
|
!!composeState.outboxMessageId &&
|
|
790
789
|
!selectedThread;
|
|
791
|
-
const showIntelligence = isDesktop && intelligenceOpen && hasThread;
|
|
792
790
|
|
|
793
791
|
useTriageKeyboard({
|
|
794
792
|
// A modal owns the keyboard outright. Suspending the layer is what keeps a
|
|
@@ -868,7 +866,6 @@ function MailboxPaneProvider({
|
|
|
868
866
|
onSelectFilterCategory,
|
|
869
867
|
onToggleFilterAttribute,
|
|
870
868
|
onClearFilters,
|
|
871
|
-
showIntelligence,
|
|
872
869
|
intelligenceOpen,
|
|
873
870
|
onToggleIntelligence,
|
|
874
871
|
onDeleteMessages: handleDeleteMessages,
|
|
@@ -1081,7 +1078,7 @@ function MailboxReading() {
|
|
|
1081
1078
|
selectedThread,
|
|
1082
1079
|
conversation,
|
|
1083
1080
|
hasRemitDraftOpen,
|
|
1084
|
-
|
|
1081
|
+
intelligenceOpen,
|
|
1085
1082
|
onToggleIntelligence,
|
|
1086
1083
|
toolbarComposeRequest,
|
|
1087
1084
|
onToolbarReply,
|
|
@@ -1094,9 +1091,12 @@ function MailboxReading() {
|
|
|
1094
1091
|
onToolbarMove,
|
|
1095
1092
|
composeState,
|
|
1096
1093
|
} = useMailboxPane();
|
|
1097
|
-
|
|
1098
|
-
|
|
1094
|
+
// The rail's own width gate, not the shell tier: between 1024 and 1280 the
|
|
1095
|
+
// reading pane is mounted but the rail is not, so "enabled" would promise an
|
|
1096
|
+
// open that cannot happen.
|
|
1097
|
+
const railFits = useAppShellLayout()?.showIntelligencePane ?? false;
|
|
1099
1098
|
const hasThread = Boolean(conversation);
|
|
1099
|
+
const canToggleIntelligence = railFits && hasThread;
|
|
1100
1100
|
|
|
1101
1101
|
const detailPane =
|
|
1102
1102
|
composeState.isOpen && !conversation ? (
|
|
@@ -1124,8 +1124,8 @@ function MailboxReading() {
|
|
|
1124
1124
|
<section className="flex h-full w-full min-w-0 flex-col bg-canvas">
|
|
1125
1125
|
<MessageToolbar
|
|
1126
1126
|
hasThread={hasThread}
|
|
1127
|
-
intelligenceOpen={
|
|
1128
|
-
|
|
1127
|
+
intelligenceOpen={canToggleIntelligence && intelligenceOpen}
|
|
1128
|
+
canToggleIntelligence={canToggleIntelligence}
|
|
1129
1129
|
onToggleIntelligence={onToggleIntelligence}
|
|
1130
1130
|
onReply={hasThread ? onToolbarReply : undefined}
|
|
1131
1131
|
onReplyAll={hasThread ? onToolbarReplyAll : undefined}
|
|
@@ -69,7 +69,7 @@ describe("MessageToolbar carries message context only (#49)", () => {
|
|
|
69
69
|
createElement(MessageToolbar, {
|
|
70
70
|
hasThread,
|
|
71
71
|
intelligenceOpen: false,
|
|
72
|
-
|
|
72
|
+
canToggleIntelligence: true,
|
|
73
73
|
onToggleIntelligence: () => undefined,
|
|
74
74
|
}) as never,
|
|
75
75
|
);
|
|
@@ -94,3 +94,31 @@ describe("MessageToolbar carries message context only (#49)", () => {
|
|
|
94
94
|
assert.match(html, /intelligence sidebar/);
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The toggle holds its slot whatever the view and the selection are (#52); it
|
|
100
|
+
* reports "cannot act" by being disabled, not by leaving the bar.
|
|
101
|
+
*/
|
|
102
|
+
describe("MessageToolbar keeps the intelligence toggle in place (#52)", () => {
|
|
103
|
+
const render = (canToggleIntelligence: boolean): string =>
|
|
104
|
+
renderToString(
|
|
105
|
+
createElement(MessageToolbar, {
|
|
106
|
+
hasThread: false,
|
|
107
|
+
intelligenceOpen: false,
|
|
108
|
+
canToggleIntelligence,
|
|
109
|
+
onToggleIntelligence: () => undefined,
|
|
110
|
+
}) as never,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
it("renders the toggle when it cannot open a rail", () => {
|
|
114
|
+
assert.match(render(false), /aria-label="Show intelligence sidebar"/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("disables it rather than dropping it from the bar", () => {
|
|
118
|
+
assert.match(render(false), /\sdisabled(=""|\s|>)/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("leaves it pressable once a rail can open", () => {
|
|
122
|
+
assert.equal(/\sdisabled(=""|\s|>)/.test(render(true)), false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
IntelligenceToggle,
|
|
3
|
+
type MailAction,
|
|
4
|
+
MailActionToolbar,
|
|
5
|
+
} from "@remit/ui";
|
|
3
6
|
import { useState } from "react";
|
|
4
7
|
import { tooltipForAction } from "@/lib/keymap";
|
|
5
8
|
import { MoveToTrigger } from "./MoveToTrigger";
|
|
@@ -13,19 +16,21 @@ import { MoveToTrigger } from "./MoveToTrigger";
|
|
|
13
16
|
* Search, compose, bug report and the account menu are not message context;
|
|
14
17
|
* they live in the app top bar above every pane (`MailTopBar`, #49).
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* The control set is fixed: every button occupies the same slot on every view
|
|
20
|
+
* and in every selection state (#52). The mail verbs stay pressable with no
|
|
21
|
+
* thread open — a press is a no-op that surfaces a one-line inline explanation
|
|
22
|
+
* (`doc/rules/ux.md`). The intelligence toggle greys out instead, because it
|
|
23
|
+
* has nothing to explain: it opens a rail, and there is no rail to open.
|
|
19
24
|
*/
|
|
20
25
|
export interface MessageToolbarProps {
|
|
21
26
|
hasThread: boolean;
|
|
22
27
|
intelligenceOpen: boolean;
|
|
23
28
|
/**
|
|
24
|
-
* Whether the intelligence toggle
|
|
25
|
-
*
|
|
26
|
-
*
|
|
29
|
+
* Whether pressing the intelligence toggle would open a rail: the view has
|
|
30
|
+
* one, the width allows it, and a thread is selected. The button renders
|
|
31
|
+
* either way — disabled when false, never absent (#52).
|
|
27
32
|
*/
|
|
28
|
-
|
|
33
|
+
canToggleIntelligence: boolean;
|
|
29
34
|
onToggleIntelligence: () => void;
|
|
30
35
|
|
|
31
36
|
/* ---- wired action callbacks (omit to keep the no-op-explain behaviour) ---- */
|
|
@@ -60,7 +65,7 @@ const OPEN_FIRST = "Open a message first";
|
|
|
60
65
|
export const MessageToolbar = ({
|
|
61
66
|
hasThread,
|
|
62
67
|
intelligenceOpen,
|
|
63
|
-
|
|
68
|
+
canToggleIntelligence,
|
|
64
69
|
onToggleIntelligence,
|
|
65
70
|
onReply,
|
|
66
71
|
onReplyAll,
|
|
@@ -103,24 +108,11 @@ export const MessageToolbar = ({
|
|
|
103
108
|
) : undefined
|
|
104
109
|
}
|
|
105
110
|
>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
title="Intelligence"
|
|
112
|
-
aria-label={
|
|
113
|
-
intelligenceOpen
|
|
114
|
-
? "Hide intelligence sidebar"
|
|
115
|
-
: "Show intelligence sidebar"
|
|
116
|
-
}
|
|
117
|
-
aria-pressed={intelligenceOpen}
|
|
118
|
-
onClick={onToggleIntelligence}
|
|
119
|
-
className={
|
|
120
|
-
intelligenceOpen ? "bg-accent-2-soft text-accent-2" : undefined
|
|
121
|
-
}
|
|
122
|
-
/>
|
|
123
|
-
)}
|
|
111
|
+
<IntelligenceToggle
|
|
112
|
+
open={intelligenceOpen}
|
|
113
|
+
enabled={canToggleIntelligence}
|
|
114
|
+
onToggle={onToggleIntelligence}
|
|
115
|
+
/>
|
|
124
116
|
</MailActionToolbar>
|
|
125
117
|
);
|
|
126
118
|
};
|
package/src/routes/mail.tsx
CHANGED
|
@@ -298,7 +298,8 @@ function MailLayout() {
|
|
|
298
298
|
/>
|
|
299
299
|
</div>
|
|
300
300
|
) : onBriefRoute ? (
|
|
301
|
-
// Daily brief (/mail/) — no mailboxId param;
|
|
301
|
+
// Daily brief (/mail/) — no mailboxId param; same 3-pane layout as a
|
|
302
|
+
// mailbox: an open message has an intelligence rail here too (#52).
|
|
302
303
|
<BriefPane selectedMessageId={mobileSelectedMessageId}>
|
|
303
304
|
{isSinglePane ? (
|
|
304
305
|
<AppShellSlotted
|
|
@@ -316,7 +317,9 @@ function MailLayout() {
|
|
|
316
317
|
topBar={topBar}
|
|
317
318
|
list={<BriefPane.List />}
|
|
318
319
|
reading={<BriefPane.Reading />}
|
|
320
|
+
intelligence={<BriefPane.Intelligence />}
|
|
319
321
|
intelligenceOpen={intelligenceOpen}
|
|
322
|
+
hasThread={Boolean(mobileSelectedMessageId)}
|
|
320
323
|
overlay={overlayContent}
|
|
321
324
|
skeleton={<AppShellSkeleton />}
|
|
322
325
|
isLoading={isLoading || hasNoAccounts}
|
|
@@ -326,7 +329,7 @@ function MailLayout() {
|
|
|
326
329
|
</BriefPane>
|
|
327
330
|
) : onFlaggedRoute ? (
|
|
328
331
|
// Flagged virtual mailbox (/mail/flagged) — flat starred list across
|
|
329
|
-
// accounts;
|
|
332
|
+
// accounts; same slots as the brief, intelligence rail included.
|
|
330
333
|
<FlaggedPane selectedMessageId={mobileSelectedMessageId}>
|
|
331
334
|
{isSinglePane ? (
|
|
332
335
|
<AppShellSlotted
|
|
@@ -344,7 +347,9 @@ function MailLayout() {
|
|
|
344
347
|
topBar={topBar}
|
|
345
348
|
list={<FlaggedPane.List />}
|
|
346
349
|
reading={<FlaggedPane.Reading />}
|
|
350
|
+
intelligence={<FlaggedPane.Intelligence />}
|
|
347
351
|
intelligenceOpen={intelligenceOpen}
|
|
352
|
+
hasThread={Boolean(mobileSelectedMessageId)}
|
|
348
353
|
overlay={overlayContent}
|
|
349
354
|
skeleton={<AppShellSkeleton />}
|
|
350
355
|
isLoading={isLoading || hasNoAccounts}
|
|
@@ -376,6 +381,7 @@ function MailLayout() {
|
|
|
376
381
|
reading={<MailboxPane.Reading />}
|
|
377
382
|
intelligence={<MailboxPane.Intelligence />}
|
|
378
383
|
intelligenceOpen={intelligenceOpen}
|
|
384
|
+
hasThread={Boolean(mobileSelectedMessageId)}
|
|
379
385
|
overlay={overlayContent}
|
|
380
386
|
skeleton={<AppShellSkeleton />}
|
|
381
387
|
isLoading={isLoading || hasNoAccounts}
|