@remit/ui 0.0.129 → 0.0.131
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-action-toolbar.tsx +17 -10
- package/src/components/outbox-row.render.test.ts +15 -0
- package/src/components/outbox-row.stories.tsx +8 -0
- package/src/components/outbox-row.tsx +14 -9
- package/src/components/outbox-status-badge.tsx +12 -1
- package/src/components/reading-pane.tsx +10 -1
package/package.json
CHANGED
|
@@ -24,9 +24,10 @@ export interface MailActionToolbarProps {
|
|
|
24
24
|
/** Whether a thread is open. Drives the no-op-explain behaviour, never `disabled`. */
|
|
25
25
|
hasThread: boolean;
|
|
26
26
|
/**
|
|
27
|
-
* Fired when an action button is pressed
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Fired when an action button is pressed and there is nothing behind it —
|
|
28
|
+
* no thread open, or no handler wired for that verb. The host surfaces a
|
|
29
|
+
* one-line inline explanation — never a toast, never a disabled button
|
|
30
|
+
* (`doc/rules/ux.md`: never disable a control).
|
|
30
31
|
*/
|
|
31
32
|
onUnavailable?: (action: MailAction) => void;
|
|
32
33
|
/** A one-line inline notice rendered under the toolbar (e.g. "Open a message first"). */
|
|
@@ -59,10 +60,12 @@ export interface MailActionToolbarProps {
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* The reading-pane action toolbar, shared between the remit-ui AppShell
|
|
62
|
-
* reference and the live client. Buttons are always pressable:
|
|
63
|
-
* open
|
|
64
|
-
*
|
|
65
|
-
* the
|
|
63
|
+
* reference and the live client. Buttons are always pressable: where a press
|
|
64
|
+
* cannot act — no thread open, or no handler wired for that verb — it calls
|
|
65
|
+
* `onUnavailable(action)` so the host can explain why, rather than greying out
|
|
66
|
+
* (the never-disable tenet — a `disabled` button gives the user no path to
|
|
67
|
+
* learn what it does or what they must do first). Every press is answered; the
|
|
68
|
+
* bar has no branch that ends in silence.
|
|
66
69
|
*/
|
|
67
70
|
export function MailActionToolbar({
|
|
68
71
|
hasThread,
|
|
@@ -85,12 +88,16 @@ export function MailActionToolbar({
|
|
|
85
88
|
children,
|
|
86
89
|
className,
|
|
87
90
|
}: MailActionToolbarProps) {
|
|
91
|
+
// A verb with no handler behind it is unavailable in exactly the way one with
|
|
92
|
+
// no thread is, and says so the same way. Dropping the press instead left the
|
|
93
|
+
// three reply verbs pressable and mute for as long as a host took to wire
|
|
94
|
+
// them (#803).
|
|
88
95
|
const act = (action: MailAction, handler?: () => void) => () => {
|
|
89
|
-
if (!hasThread) {
|
|
96
|
+
if (!hasThread || !handler) {
|
|
90
97
|
onUnavailable?.(action);
|
|
91
98
|
return;
|
|
92
99
|
}
|
|
93
|
-
handler
|
|
100
|
+
handler();
|
|
94
101
|
};
|
|
95
102
|
|
|
96
103
|
return (
|
|
@@ -166,7 +173,7 @@ export function MailActionToolbar({
|
|
|
166
173
|
<div className="flex-1" />
|
|
167
174
|
{children}
|
|
168
175
|
</header>
|
|
169
|
-
{
|
|
176
|
+
{unavailableHint && (
|
|
170
177
|
// biome-ignore lint/a11y/useSemanticElements: <p> with role="status" preserves block layout; <output> is inline
|
|
171
178
|
<p
|
|
172
179
|
role="status"
|
|
@@ -46,6 +46,21 @@ describe("OutboxRow", () => {
|
|
|
46
46
|
assert.doesNotMatch(html, /disabled=""/);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it("offers only delete for an unfiled row and names the reason", () => {
|
|
50
|
+
const html = renderToString(
|
|
51
|
+
createElement(OutboxRow, {
|
|
52
|
+
...baseProps,
|
|
53
|
+
status: "unfiled",
|
|
54
|
+
error: "this account has no Sent folder",
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
assert.match(html, /Sent, not filed/);
|
|
58
|
+
assert.match(html, /this account has no Sent folder/);
|
|
59
|
+
assert.match(html, /aria-label="Delete message"/);
|
|
60
|
+
assert.doesNotMatch(html, /aria-label="Retry sending"/);
|
|
61
|
+
assert.doesNotMatch(html, /aria-label="Edit as draft"/);
|
|
62
|
+
});
|
|
63
|
+
|
|
49
64
|
it("omits retry for a blocked row but keeps edit and delete", () => {
|
|
50
65
|
const html = renderToString(
|
|
51
66
|
createElement(OutboxRow, {
|
|
@@ -24,6 +24,14 @@ export const Sending: Story = { args: { status: "sending" } };
|
|
|
24
24
|
|
|
25
25
|
export const Sent: Story = { args: { status: "sent" } };
|
|
26
26
|
|
|
27
|
+
export const Unfiled: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
status: "unfiled",
|
|
30
|
+
error:
|
|
31
|
+
"Sent, but not filed: this account has no Sent folder. Create one named Sent and later messages will be filed there.",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
27
35
|
export const Failed: Story = {
|
|
28
36
|
args: {
|
|
29
37
|
status: "failed",
|
|
@@ -11,7 +11,7 @@ export interface OutboxRowProps {
|
|
|
11
11
|
/** Pre-formatted timestamp shown at the row's trailing edge. */
|
|
12
12
|
time: string;
|
|
13
13
|
status: OutboxStatus;
|
|
14
|
-
/** Surfaced after the status label for failed
|
|
14
|
+
/** Surfaced after the status label for failed, blocked and unfiled rows. */
|
|
15
15
|
error?: string;
|
|
16
16
|
selected?: boolean;
|
|
17
17
|
onSelect: () => void;
|
|
@@ -26,7 +26,7 @@ export interface OutboxRowProps {
|
|
|
26
26
|
const tintForStatus = (status: OutboxStatus, selected?: boolean): string => {
|
|
27
27
|
if (selected) return "bg-accent-2-soft";
|
|
28
28
|
if (status === "failed") return "bg-danger-soft";
|
|
29
|
-
if (status === "blocked") return "bg-warning/10";
|
|
29
|
+
if (status === "blocked" || status === "unfiled") return "bg-warning/10";
|
|
30
30
|
return "";
|
|
31
31
|
};
|
|
32
32
|
|
|
@@ -49,7 +49,8 @@ export function OutboxRow({
|
|
|
49
49
|
onDelete,
|
|
50
50
|
deleting,
|
|
51
51
|
}: OutboxRowProps) {
|
|
52
|
-
const showActions =
|
|
52
|
+
const showActions =
|
|
53
|
+
status === "failed" || status === "blocked" || status === "unfiled";
|
|
53
54
|
|
|
54
55
|
return (
|
|
55
56
|
<div
|
|
@@ -96,12 +97,16 @@ export function OutboxRow({
|
|
|
96
97
|
} as const,
|
|
97
98
|
]
|
|
98
99
|
: []),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
...(status === "unfiled"
|
|
101
|
+
? []
|
|
102
|
+
: [
|
|
103
|
+
{
|
|
104
|
+
label: "Edit as draft",
|
|
105
|
+
iconOnly: true,
|
|
106
|
+
icon: <Send className="size-3.5" />,
|
|
107
|
+
onClick: onEdit,
|
|
108
|
+
} as const,
|
|
109
|
+
]),
|
|
105
110
|
]}
|
|
106
111
|
destructive={{
|
|
107
112
|
label: "Delete message",
|
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
import { cn } from "../lib/cn.js";
|
|
9
9
|
|
|
10
10
|
/** The non-draft outbox lifecycle states (drafts live in a separate view). */
|
|
11
|
-
export type OutboxStatus =
|
|
11
|
+
export type OutboxStatus =
|
|
12
|
+
| "queued"
|
|
13
|
+
| "sending"
|
|
14
|
+
| "sent"
|
|
15
|
+
| "unfiled"
|
|
16
|
+
| "failed"
|
|
17
|
+
| "blocked";
|
|
12
18
|
|
|
13
19
|
interface StatusConfig {
|
|
14
20
|
icon: typeof Clock;
|
|
@@ -26,6 +32,11 @@ export const outboxStatusConfig: Record<OutboxStatus, StatusConfig> = {
|
|
|
26
32
|
spin: true,
|
|
27
33
|
},
|
|
28
34
|
sent: { icon: CheckCircle, label: "Sent", className: "text-positive" },
|
|
35
|
+
unfiled: {
|
|
36
|
+
icon: AlertTriangle,
|
|
37
|
+
label: "Sent, not filed",
|
|
38
|
+
className: "text-warning",
|
|
39
|
+
},
|
|
29
40
|
failed: { icon: AlertCircle, label: "Failed", className: "text-danger" },
|
|
30
41
|
blocked: { icon: AlertTriangle, label: "Blocked", className: "text-warning" },
|
|
31
42
|
};
|
|
@@ -288,7 +288,16 @@ function MessageToolbar({
|
|
|
288
288
|
return (
|
|
289
289
|
<MailActionToolbar
|
|
290
290
|
hasThread={hasThread}
|
|
291
|
-
|
|
291
|
+
// This shell documents the layout and has no mail behind its verbs, so
|
|
292
|
+
// with a thread open a press says that rather than borrowing the line
|
|
293
|
+
// about opening one.
|
|
294
|
+
onUnavailable={() =>
|
|
295
|
+
setHint(
|
|
296
|
+
hasThread
|
|
297
|
+
? "Not wired in the layout reference"
|
|
298
|
+
: "Open a message first",
|
|
299
|
+
)
|
|
300
|
+
}
|
|
292
301
|
unavailableHint={hint}
|
|
293
302
|
>
|
|
294
303
|
<IntelligenceToggle
|