@dbx-tools/ui-email 0.6.46 → 0.6.48
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/README.md +46 -12
- package/package.json +7 -6
- package/src/react/auth-gate.tsx +46 -17
- package/src/react/email-approval-card.tsx +14 -39
- package/src/react/email-body.tsx +8 -14
- package/src/react/email-compose.tsx +5 -7
package/README.md
CHANGED
|
@@ -13,9 +13,12 @@ Key features:
|
|
|
13
13
|
- Approval card for suspended `send_email` tool calls.
|
|
14
14
|
- Read-only draft preview for review queues, chat transcripts, and test pages.
|
|
15
15
|
- Standalone compose form that emits shared `EmailMessage` payloads.
|
|
16
|
-
-
|
|
16
|
+
- Shared React Email body renderer matching the delivered message.
|
|
17
|
+
- dbx-tools branding by default, with consumer brand overrides available.
|
|
17
18
|
- Recipient parsing, address display, and attachment-label helpers that mirror
|
|
18
19
|
server expectations.
|
|
20
|
+
- `AuthGate` sign-in screen for the email one-time-code auth plugin, branded from
|
|
21
|
+
the shared brand context and shaped for platform autofill.
|
|
19
22
|
- Styles wired to the AppKit UI/Tailwind foundation so host apps do not need a
|
|
20
23
|
separate email component theme.
|
|
21
24
|
|
|
@@ -46,8 +49,9 @@ const draft = email.emailMessageSchema.parse(toolCall.args);
|
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
`EmailApprovalCard` is the chat-facing component for the `send_email` tool. It
|
|
49
|
-
renders the
|
|
50
|
-
actions while leaving tool-call state and
|
|
52
|
+
renders the complete branded React Email card, including envelope metadata and
|
|
53
|
+
attachments, plus Approve/Deny actions while leaving tool-call state and
|
|
54
|
+
transport decisions to the host app.
|
|
51
55
|
|
|
52
56
|
Wire `onApprove` and `onDeny` to the chat framework's tool-result mechanism.
|
|
53
57
|
The component deliberately does not call the email API itself; the server-side
|
|
@@ -61,7 +65,7 @@ import { EmailPreview } from "@dbx-tools/ui-email/react";
|
|
|
61
65
|
<EmailPreview email={draft} />;
|
|
62
66
|
```
|
|
63
67
|
|
|
64
|
-
Use `EmailPreview` when a page needs
|
|
68
|
+
Use `EmailPreview` when a page needs the same full branded card without action
|
|
65
69
|
buttons, such as a review queue, audit log, or test harness.
|
|
66
70
|
|
|
67
71
|
## Provide A Compose View
|
|
@@ -81,7 +85,7 @@ attached files to base64 email attachments, and emits the assembled
|
|
|
81
85
|
`EmailMessage`. Fetch sender options and dispatch the final send through the
|
|
82
86
|
server package.
|
|
83
87
|
|
|
84
|
-
## Render
|
|
88
|
+
## Render An Email Body
|
|
85
89
|
|
|
86
90
|
```tsx
|
|
87
91
|
import { EmailBody } from "@dbx-tools/ui-email/react";
|
|
@@ -89,9 +93,10 @@ import { EmailBody } from "@dbx-tools/ui-email/react";
|
|
|
89
93
|
<EmailBody className="text-sm">{message.body}</EmailBody>;
|
|
90
94
|
```
|
|
91
95
|
|
|
92
|
-
`EmailBody`
|
|
93
|
-
|
|
94
|
-
and
|
|
96
|
+
`EmailBody` reuses `@dbx-tools/shared-email-template`, the same React Email body
|
|
97
|
+
component used by the server renderer. Drafts therefore keep the delivered
|
|
98
|
+
message's typography, rich-content styling, and default brand while they are
|
|
99
|
+
edited and approved.
|
|
95
100
|
|
|
96
101
|
## Reuse Field Helpers
|
|
97
102
|
|
|
@@ -106,13 +111,42 @@ const files = attachmentNames(message.attachments);
|
|
|
106
111
|
The helpers keep free-text recipient parsing and attachment labels consistent
|
|
107
112
|
across approval, compose, and custom UI surfaces.
|
|
108
113
|
|
|
114
|
+
## Gate An App Behind An Email Code
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { AuthGate } from "@dbx-tools/ui-email/react";
|
|
118
|
+
|
|
119
|
+
<AuthGate>
|
|
120
|
+
<App />
|
|
121
|
+
</AuthGate>;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`AuthGate` is the sign-in screen for an app fronted by the `@dbx-tools/email`
|
|
125
|
+
auth plugin - typically one published through
|
|
126
|
+
[`@dbx-tools/cli-tunnel`](../../cli/tunnel), where the hosting platform's own
|
|
127
|
+
identity-aware proxy is not in the request path. It calls the plugin's
|
|
128
|
+
`/api/email/auth/*` routes: on mount it checks `status`, renders `children`
|
|
129
|
+
straight through when the gate is off or a session already exists, and otherwise
|
|
130
|
+
runs the email -> code flow, revealing `children` once a verified code sets the
|
|
131
|
+
session cookie.
|
|
132
|
+
|
|
133
|
+
It holds no token: the session lives in an HttpOnly cookie the browser sends
|
|
134
|
+
automatically. `title` and `description` override the default copy, which
|
|
135
|
+
otherwise names the app from the brand context.
|
|
136
|
+
|
|
137
|
+
The code field carries `autocomplete="one-time-code"`, which is what lets iOS,
|
|
138
|
+
Android, and Safari offer the code straight from the notification. That only pays
|
|
139
|
+
off while the email keeps the conventional `Your verification code is: / <code>`
|
|
140
|
+
shape the gate sends, so change one and check the other.
|
|
141
|
+
|
|
109
142
|
## Modules
|
|
110
143
|
|
|
111
144
|
- `./react` - `EmailPreview`, `EmailApprovalCard`, `EmailComposeView`,
|
|
112
|
-
`EmailBody`, address/attachment helpers, shared email message
|
|
113
|
-
types.
|
|
145
|
+
`EmailBody`, `AuthGate`, address/attachment helpers, shared email message
|
|
146
|
+
types, and prop types.
|
|
114
147
|
- `./styles.css` - Tailwind/AppKit style entrypoint for the email components.
|
|
115
148
|
|
|
116
149
|
Pair this package with [`@dbx-tools/email`](../../node/email) for SMTP or
|
|
117
|
-
outbox delivery,
|
|
118
|
-
|
|
150
|
+
outbox delivery, [`@dbx-tools/shared-email-template`](../../shared/email-template)
|
|
151
|
+
for the universal presentation, and [`@dbx-tools/shared-email`](../../shared/email)
|
|
152
|
+
for schema validation in client/server boundaries.
|
package/package.json
CHANGED
|
@@ -24,19 +24,20 @@
|
|
|
24
24
|
"typescript": "^5.9.3"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@dbx-tools/shared-core": "0.6.
|
|
28
|
-
"@dbx-tools/shared-email": "0.6.
|
|
29
|
-
"@dbx-tools/
|
|
27
|
+
"@dbx-tools/shared-core": "0.6.48",
|
|
28
|
+
"@dbx-tools/shared-email": "0.6.48",
|
|
29
|
+
"@dbx-tools/shared-email-template": "0.6.48",
|
|
30
|
+
"@dbx-tools/ui-appkit": "0.6.48",
|
|
31
|
+
"@dbx-tools/ui-branding": "0.6.48",
|
|
30
32
|
"lucide-react": "^0.554.0",
|
|
31
33
|
"react": "^19.2.4",
|
|
32
|
-
"react-dom": "^19.2.4"
|
|
33
|
-
"streamdown": "^2.5.0"
|
|
34
|
+
"react-dom": "^19.2.4"
|
|
34
35
|
},
|
|
35
36
|
"license": "UNLICENSED",
|
|
36
37
|
"publishConfig": {
|
|
37
38
|
"access": "public"
|
|
38
39
|
},
|
|
39
|
-
"version": "0.6.
|
|
40
|
+
"version": "0.6.48",
|
|
40
41
|
"type": "module",
|
|
41
42
|
"exports": {
|
|
42
43
|
"./react": "./src/react/index.ts",
|
package/src/react/auth-gate.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { string } from "@dbx-tools/shared-core";
|
|
1
2
|
import type { AuthStatus } from "@dbx-tools/shared-email";
|
|
2
3
|
import { Button, Input } from "@dbx-tools/ui-appkit/react";
|
|
3
|
-
import {
|
|
4
|
+
import { BrandIcon, useBrand } from "@dbx-tools/ui-branding/react";
|
|
4
5
|
import { type FormEvent, type ReactNode, useCallback, useEffect, useState } from "react";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Email-
|
|
8
|
-
* plugin
|
|
9
|
-
*
|
|
8
|
+
* Email one-time-code sign-in gate for an app fronted by the `@dbx-tools/email`
|
|
9
|
+
* auth plugin - an app reachable on the public internet, where the hosting
|
|
10
|
+
* platform's own identity-aware proxy is not in the request path.
|
|
10
11
|
*
|
|
11
12
|
* Wrap the app in `<AuthGate>...</AuthGate>`. It calls the plugin's
|
|
12
13
|
* `/api/email/auth/*` routes: on mount it checks `status`; if the gate is
|
|
@@ -17,7 +18,12 @@ import { type FormEvent, type ReactNode, useCallback, useEffect, useState } from
|
|
|
17
18
|
* Presentational + fetch only: the session lives in an HttpOnly cookie the
|
|
18
19
|
* browser sends automatically, so this component holds no token. Anti-enumeration
|
|
19
20
|
* is server-side (every request-code call reports success), so the UI always
|
|
20
|
-
* advances to the code step after
|
|
21
|
+
* advances to the code step after the code is requested.
|
|
22
|
+
*
|
|
23
|
+
* Branding comes from the repo-wide `@dbx-tools/ui-branding` context, so the
|
|
24
|
+
* sign-in screen carries the host app's mark and name - the same brand the gate's
|
|
25
|
+
* code email is themed with - instead of a generic icon and a hardcoded product
|
|
26
|
+
* name. With no `BrandProvider` above it, the dbx-tools default context applies.
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
/** Base path the email auth routes are mounted under. */
|
|
@@ -51,6 +57,7 @@ export interface AuthGateProps {
|
|
|
51
57
|
* off) or the two-step login.
|
|
52
58
|
*/
|
|
53
59
|
export function AuthGate({ children, title, description }: AuthGateProps): ReactNode {
|
|
60
|
+
const { context: brand } = useBrand();
|
|
54
61
|
const [phase, setPhase] = useState<Phase>("loading");
|
|
55
62
|
const [email, setEmail] = useState("");
|
|
56
63
|
const [code, setCode] = useState("");
|
|
@@ -90,8 +97,8 @@ export function AuthGate({ children, title, description }: AuthGateProps): React
|
|
|
90
97
|
// rate-limit cooldown, which leaks no allow-list state.
|
|
91
98
|
setNotice(
|
|
92
99
|
result.retryAfter
|
|
93
|
-
? `
|
|
94
|
-
: "If that address
|
|
100
|
+
? `Too many requests. Try again in ${string.pluralize(result.retryAfter, "second")}.`
|
|
101
|
+
: "If an account exists for that email address, a verification code is on its way.",
|
|
95
102
|
);
|
|
96
103
|
setPhase("code");
|
|
97
104
|
} finally {
|
|
@@ -117,8 +124,8 @@ export function AuthGate({ children, title, description }: AuthGateProps): React
|
|
|
117
124
|
} else {
|
|
118
125
|
setNotice(
|
|
119
126
|
result.retryAfter
|
|
120
|
-
? `Too many attempts.
|
|
121
|
-
: "That code
|
|
127
|
+
? `Too many attempts. Try again in ${string.pluralize(result.retryAfter, "second")}.`
|
|
128
|
+
: "That verification code is incorrect or has expired.",
|
|
122
129
|
);
|
|
123
130
|
}
|
|
124
131
|
} finally {
|
|
@@ -135,11 +142,19 @@ export function AuthGate({ children, title, description }: AuthGateProps): React
|
|
|
135
142
|
<div className="flex min-h-screen items-center justify-center bg-background p-6">
|
|
136
143
|
<div className="w-full max-w-sm rounded-lg border border-border bg-card p-6 shadow-sm">
|
|
137
144
|
<div className="mb-4 flex items-center gap-2 text-foreground">
|
|
138
|
-
<
|
|
139
|
-
|
|
145
|
+
<BrandIcon className="size-5" alt="" aria-hidden />
|
|
146
|
+
{/*
|
|
147
|
+
Names the app, which is the convention for a sign-in screen and the
|
|
148
|
+
reassurance a recipient checks the code against. `brand.name` is the
|
|
149
|
+
same value that names the app in the code email.
|
|
150
|
+
*/}
|
|
151
|
+
<h1 className="text-lg font-semibold">{title ?? `Sign in to ${brand.name}`}</h1>
|
|
140
152
|
</div>
|
|
141
153
|
<p className="mb-4 text-sm text-muted-foreground">
|
|
142
|
-
{description ??
|
|
154
|
+
{description ??
|
|
155
|
+
(phase === "code"
|
|
156
|
+
? "Enter the 6-digit verification code sent to your email address."
|
|
157
|
+
: "Enter your email address and we will send you a verification code.")}
|
|
143
158
|
</p>
|
|
144
159
|
|
|
145
160
|
{phase === "email" ? (
|
|
@@ -147,27 +162,37 @@ export function AuthGate({ children, title, description }: AuthGateProps): React
|
|
|
147
162
|
<Input
|
|
148
163
|
type="email"
|
|
149
164
|
autoComplete="email"
|
|
165
|
+
aria-label="Email address"
|
|
150
166
|
placeholder="you@company.com"
|
|
151
167
|
value={email}
|
|
152
168
|
onChange={(e) => setEmail(e.target.value)}
|
|
153
169
|
required
|
|
154
170
|
/>
|
|
155
171
|
<Button type="submit" disabled={busy} className="w-full">
|
|
156
|
-
{busy ? "Sending…" : "Send code"}
|
|
172
|
+
{busy ? "Sending…" : "Send verification code"}
|
|
157
173
|
</Button>
|
|
158
174
|
</form>
|
|
159
175
|
) : (
|
|
160
176
|
<form onSubmit={verifyCode} className="space-y-3">
|
|
177
|
+
{/*
|
|
178
|
+
`autoComplete="one-time-code"` is what lets iOS/Android/Safari offer
|
|
179
|
+
the code straight from the notification, and it only pays off when
|
|
180
|
+
the email keeps the conventional "Your verification code is: /
|
|
181
|
+
<code>" shape the gate sends. `inputMode="numeric"` raises the
|
|
182
|
+
number pad without rejecting a paste.
|
|
183
|
+
*/}
|
|
161
184
|
<Input
|
|
162
185
|
inputMode="numeric"
|
|
163
186
|
autoComplete="one-time-code"
|
|
164
|
-
|
|
187
|
+
aria-label="Verification code"
|
|
188
|
+
placeholder="6-digit verification code"
|
|
189
|
+
maxLength={6}
|
|
165
190
|
value={code}
|
|
166
191
|
onChange={(e) => setCode(e.target.value)}
|
|
167
192
|
required
|
|
168
193
|
/>
|
|
169
194
|
<Button type="submit" disabled={busy} className="w-full">
|
|
170
|
-
{busy ? "Verifying…" : "
|
|
195
|
+
{busy ? "Verifying…" : "Continue"}
|
|
171
196
|
</Button>
|
|
172
197
|
<button
|
|
173
198
|
type="button"
|
|
@@ -178,12 +203,16 @@ export function AuthGate({ children, title, description }: AuthGateProps): React
|
|
|
178
203
|
setNotice(null);
|
|
179
204
|
}}
|
|
180
205
|
>
|
|
181
|
-
Use a different email
|
|
206
|
+
Use a different email address
|
|
182
207
|
</button>
|
|
183
208
|
</form>
|
|
184
209
|
)}
|
|
185
210
|
|
|
186
|
-
{notice ?
|
|
211
|
+
{notice ? (
|
|
212
|
+
<p role="status" aria-live="polite" className="mt-3 text-xs text-muted-foreground">
|
|
213
|
+
{notice}
|
|
214
|
+
</p>
|
|
215
|
+
) : null}
|
|
187
216
|
</div>
|
|
188
217
|
</div>
|
|
189
218
|
);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { EmailCard } from "@dbx-tools/shared-email-template";
|
|
1
2
|
import { Button } from "@dbx-tools/ui-appkit/react";
|
|
2
3
|
import { CheckIcon, MailIcon, XIcon } from "lucide-react";
|
|
3
|
-
import { EmailBody } from "./email-body.tsx";
|
|
4
4
|
import { attachmentNames, joinAddresses, type EmailDraft } from "./fields.ts";
|
|
5
5
|
|
|
6
6
|
// Presentational pieces for an outbound email awaiting a human Approve /
|
|
7
|
-
// Deny: the field preview (To / Cc / Subject / Body / Files, body
|
|
8
|
-
//
|
|
7
|
+
// Deny: the field preview (To / Cc / Subject / Body / Files, body rendered
|
|
8
|
+
// through the shared React Email presentation) and an approval card wrapping it.
|
|
9
9
|
// State and the resolve transport belong to the caller; these components
|
|
10
10
|
// only render and report intent. The editable counterpart is
|
|
11
11
|
// `EmailComposeView` in `./email-compose`; both share `./fields` and
|
|
@@ -21,48 +21,23 @@ export interface EmailPreviewProps {
|
|
|
21
21
|
/**
|
|
22
22
|
* Render an email draft as a labelled `To` / `Cc` / `Subject` / `Body` /
|
|
23
23
|
* `Files` list. `to` / `cc` may carry one or more addresses; the body is
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* rendered through the same React Email body used for delivery. Fields that
|
|
25
|
+
* are empty are omitted.
|
|
26
26
|
*/
|
|
27
27
|
export const EmailPreview = ({ email }: EmailPreviewProps) => {
|
|
28
28
|
const to = joinAddresses(email.to);
|
|
29
29
|
const cc = joinAddresses(email.cc);
|
|
30
|
+
const bcc = joinAddresses(email.bcc);
|
|
30
31
|
const attachments = attachmentNames(email.attachments);
|
|
32
|
+
const headers: Array<readonly [string, string]> = [];
|
|
33
|
+
if (to) headers.push(["To", to]);
|
|
34
|
+
if (cc) headers.push(["Cc", cc]);
|
|
35
|
+
if (bcc) headers.push(["Bcc", bcc]);
|
|
36
|
+
if (attachments) headers.push(["Files", attachments]);
|
|
31
37
|
return (
|
|
32
|
-
<
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
<dt className="w-16 shrink-0 text-muted-foreground">To</dt>
|
|
36
|
-
<dd className="truncate">{to}</dd>
|
|
37
|
-
</div>
|
|
38
|
-
)}
|
|
39
|
-
{cc && (
|
|
40
|
-
<div className="flex gap-2">
|
|
41
|
-
<dt className="w-16 shrink-0 text-muted-foreground">Cc</dt>
|
|
42
|
-
<dd className="truncate">{cc}</dd>
|
|
43
|
-
</div>
|
|
44
|
-
)}
|
|
45
|
-
{email.subject && (
|
|
46
|
-
<div className="flex gap-2">
|
|
47
|
-
<dt className="w-16 shrink-0 text-muted-foreground">Subject</dt>
|
|
48
|
-
<dd className="truncate font-medium">{email.subject}</dd>
|
|
49
|
-
</div>
|
|
50
|
-
)}
|
|
51
|
-
{email.body && (
|
|
52
|
-
<div className="flex gap-2">
|
|
53
|
-
<dt className="w-16 shrink-0 text-muted-foreground">Body</dt>
|
|
54
|
-
<dd className="min-w-0 flex-1 break-words text-foreground">
|
|
55
|
-
<EmailBody>{email.body}</EmailBody>
|
|
56
|
-
</dd>
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
{attachments && (
|
|
60
|
-
<div className="flex gap-2">
|
|
61
|
-
<dt className="w-16 shrink-0 text-muted-foreground">Files</dt>
|
|
62
|
-
<dd className="min-w-0 flex-1 truncate">{attachments}</dd>
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
</dl>
|
|
38
|
+
<div className="overflow-x-auto rounded-2xl bg-muted/20 p-2">
|
|
39
|
+
<EmailCard subject={email.subject || "Message"} body={email.body || ""} headers={headers} />
|
|
40
|
+
</div>
|
|
66
41
|
);
|
|
67
42
|
};
|
|
68
43
|
|
package/src/react/email-body.tsx
CHANGED
|
@@ -3,27 +3,21 @@
|
|
|
3
3
|
// render the drafted Markdown identically (links, lists, emphasis, and
|
|
4
4
|
// tables rather than raw syntax).
|
|
5
5
|
|
|
6
|
+
import { EmailBody as ReactEmailBody, type EmailBrand } from "@dbx-tools/shared-email-template";
|
|
6
7
|
import { cn } from "@dbx-tools/ui-appkit/react";
|
|
7
|
-
import { Streamdown } from "streamdown";
|
|
8
8
|
|
|
9
9
|
/** Props for {@link EmailBody}. */
|
|
10
10
|
export interface EmailBodyProps {
|
|
11
11
|
children: string;
|
|
12
12
|
/** Extra classes merged onto the prose container. */
|
|
13
13
|
className?: string;
|
|
14
|
+
/** Optional brand override; dbx-tools branding is the default. */
|
|
15
|
+
brand?: EmailBrand;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
/** Render an email body
|
|
17
|
-
export const EmailBody = ({ children, className }: EmailBodyProps) => (
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"prose prose-sm dark:prose-invert max-w-none break-words",
|
|
22
|
-
"text-[11px] leading-snug text-muted-foreground",
|
|
23
|
-
"[&_strong]:text-foreground [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_ul]:pl-4 [&_ol]:pl-4",
|
|
24
|
-
className,
|
|
25
|
-
)}
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</Streamdown>
|
|
18
|
+
/** Render an email body with the same React Email component used for delivery. */
|
|
19
|
+
export const EmailBody = ({ children, className, brand }: EmailBodyProps) => (
|
|
20
|
+
<div className={cn("max-w-none break-words", className)}>
|
|
21
|
+
<ReactEmailBody body={children} brand={brand} />
|
|
22
|
+
</div>
|
|
29
23
|
);
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
} from "@dbx-tools/ui-appkit/react";
|
|
14
14
|
import { EyeIcon, PaperclipIcon, PencilIcon, SendIcon, XIcon } from "lucide-react";
|
|
15
15
|
import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from "react";
|
|
16
|
-
import {
|
|
16
|
+
import { EmailPreview } from "./email-approval-card.tsx";
|
|
17
17
|
import { joinAddresses, parseAddresses, type EmailDraft } from "./fields.ts";
|
|
18
18
|
|
|
19
19
|
// A standard, editable email compose form usable outside a chat bubble
|
|
20
20
|
// (a settings page, a standalone "send" view, etc.). It shares the
|
|
21
|
-
// address / attachment helpers (`./fields`) and the
|
|
21
|
+
// address / attachment helpers (`./fields`) and the React Email body
|
|
22
22
|
// renderer (`./email-body`) with the read-only `EmailPreview`, so the
|
|
23
23
|
// two surfaces stay visually and semantically in sync.
|
|
24
24
|
//
|
|
@@ -236,7 +236,7 @@ export const EmailComposeView = ({
|
|
|
236
236
|
<div className="grid gap-1.5">
|
|
237
237
|
<div className="flex items-center justify-between">
|
|
238
238
|
<Label htmlFor={fieldId("body")} className="text-xs text-muted-foreground">
|
|
239
|
-
Body
|
|
239
|
+
Body
|
|
240
240
|
</Label>
|
|
241
241
|
<Button
|
|
242
242
|
type="button"
|
|
@@ -258,15 +258,13 @@ export const EmailComposeView = ({
|
|
|
258
258
|
</Button>
|
|
259
259
|
</div>
|
|
260
260
|
{showPreview ? (
|
|
261
|
-
<
|
|
262
|
-
<EmailBody>{body || "_Nothing to preview yet._"}</EmailBody>
|
|
263
|
-
</div>
|
|
261
|
+
<EmailPreview email={{ ...buildMessage(), body: body || "_Nothing to preview yet._" }} />
|
|
264
262
|
) : (
|
|
265
263
|
<Textarea
|
|
266
264
|
id={fieldId("body")}
|
|
267
265
|
value={body}
|
|
268
266
|
onChange={(e) => setBody(e.target.value)}
|
|
269
|
-
placeholder="Write your message
|
|
267
|
+
placeholder="Write your message..."
|
|
270
268
|
disabled={blocked}
|
|
271
269
|
className="min-h-32 font-mono text-xs"
|
|
272
270
|
/>
|