@dbx-tools/ui-email 0.6.67 → 0.6.69

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 CHANGED
@@ -49,9 +49,9 @@ const draft = email.emailMessageSchema.parse(toolCall.args);
49
49
  ```
50
50
 
51
51
  `EmailApprovalCard` is the chat-facing component for the `send_email` tool. It
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.
52
+ renders mail-client chrome (sender, recipients, subject, attachments) around the
53
+ branded React Email card, plus Approve/Deny actions while leaving tool-call
54
+ state and transport decisions to the host app.
55
55
 
56
56
  Wire `onApprove` and `onDeny` to the chat framework's tool-result mechanism.
57
57
  The component deliberately does not call the email API itself; the server-side
@@ -65,11 +65,13 @@ import { EmailPreview } from "@dbx-tools/ui-email/react";
65
65
  <EmailPreview email={draft} />;
66
66
  ```
67
67
 
68
- Use `EmailPreview` when a page needs the same full branded card without action
69
- buttons, such as a review queue, audit log, or test harness. It projects the
70
- nearest `BrandProvider` context into email-safe colors and typography, so a live
71
- site brand picker updates the preview too. Pass the optional `brand` prop when a
72
- specific message needs an independent campaign or partner identity.
68
+ Use `EmailPreview` when a page needs the same received-mail presentation without
69
+ action buttons, such as a review queue, audit log, or test harness. Envelope
70
+ fields live in the client chrome; the card itself matches delivered HTML. It
71
+ projects the nearest `BrandProvider` context into email-safe colors and
72
+ typography, so a live site brand picker updates the preview too. Pass the
73
+ optional `brand` prop when a specific message needs an independent campaign or
74
+ partner identity.
73
75
 
74
76
  ## Provide A Compose View
75
77
 
package/package.json CHANGED
@@ -24,11 +24,11 @@
24
24
  "typescript": "^5.9.3"
25
25
  },
26
26
  "dependencies": {
27
- "@dbx-tools/shared-core": "0.6.67",
28
- "@dbx-tools/shared-email": "0.6.67",
29
- "@dbx-tools/shared-email-template": "0.6.67",
30
- "@dbx-tools/ui-appkit": "0.6.67",
31
- "@dbx-tools/ui-branding": "0.6.67",
27
+ "@dbx-tools/shared-core": "0.6.69",
28
+ "@dbx-tools/shared-email": "0.6.69",
29
+ "@dbx-tools/shared-email-template": "0.6.69",
30
+ "@dbx-tools/ui-appkit": "0.6.69",
31
+ "@dbx-tools/ui-branding": "0.6.69",
32
32
  "lucide-react": "^0.554.0",
33
33
  "react": "^19.2.4",
34
34
  "react-dom": "^19.2.4"
@@ -37,7 +37,7 @@
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
- "version": "0.6.67",
40
+ "version": "0.6.69",
41
41
  "type": "module",
42
42
  "exports": {
43
43
  "./react": "./src/react/index.ts",
@@ -1,16 +1,17 @@
1
1
  import {
2
2
  EmailCard,
3
3
  emailBrandFromContext,
4
+ resolveEmailBrand,
4
5
  type EmailBrand,
5
6
  } from "@dbx-tools/shared-email-template";
6
7
  import { Button } from "@dbx-tools/ui-appkit/react";
7
8
  import { useBrand } from "@dbx-tools/ui-branding/react";
8
- import { CheckIcon, MailIcon, XIcon } from "lucide-react";
9
+ import { CheckIcon, MailIcon, PaperclipIcon, XIcon } from "lucide-react";
9
10
  import { attachmentNames, joinAddresses, type EmailDraft } from "./fields.ts";
10
11
 
11
12
  // Presentational pieces for an outbound email awaiting a human Approve /
12
- // Deny: the field preview (To / Cc / Subject / Body / Files, body rendered
13
- // through the shared React Email presentation) and an approval card wrapping it.
13
+ // Deny: a mail-client chrome (sender, recipients, subject, attachments)
14
+ // around the shared React Email card, plus an approval card wrapping it.
14
15
  // State and the resolve transport belong to the caller; these components
15
16
  // only render and report intent. The editable counterpart is
16
17
  // `EmailComposeView` in `./email-compose`; both share `./fields` and
@@ -25,31 +26,81 @@ export interface EmailPreviewProps {
25
26
  brand?: EmailBrand;
26
27
  }
27
28
 
29
+ /** Initials for the sender avatar when no logo is available. */
30
+ function senderInitials(name: string): string {
31
+ const parts = name.trim().split(/\s+/).filter(Boolean);
32
+ if (parts.length === 0) return "?";
33
+ if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();
34
+ return `${parts[0]![0] ?? ""}${parts[1]![0] ?? ""}`.toUpperCase();
35
+ }
36
+
37
+ /** One muted "Label: value" row in the client chrome. */
38
+ const ChromeRow = ({ label, value }: { label: string; value: string }) => (
39
+ <p className="truncate text-xs text-muted-foreground">
40
+ <span className="font-medium text-foreground/70">{label}</span> {value}
41
+ </p>
42
+ );
43
+
28
44
  /**
29
- * Render an email draft as a labelled `To` / `Cc` / `Subject` / `Body` /
30
- * `Files` list. `to` / `cc` may carry one or more addresses; the body is
31
- * rendered through the same React Email body used for delivery. Fields that
32
- * are empty are omitted.
45
+ * Render an email draft the way a mail client shows a received message: sender
46
+ * identity, recipients, subject, and attachment chips in chrome around the
47
+ * same branded React Email card used for delivery. Envelope fields stay out of
48
+ * the delivered HTML — they are transport metadata, not body content.
33
49
  */
34
50
  export const EmailPreview = ({ email, brand }: EmailPreviewProps) => {
35
51
  const { context } = useBrand();
52
+ const theme = resolveEmailBrand(brand ?? emailBrandFromContext(context));
36
53
  const to = joinAddresses(email.to);
37
54
  const cc = joinAddresses(email.cc);
38
55
  const bcc = joinAddresses(email.bcc);
39
- const attachments = attachmentNames(email.attachments);
40
- const headers: Array<readonly [string, string]> = [];
41
- if (to) headers.push(["To", to]);
42
- if (cc) headers.push(["Cc", cc]);
43
- if (bcc) headers.push(["Bcc", bcc]);
44
- if (attachments) headers.push(["Files", attachments]);
56
+ const subject = email.subject?.trim() || "Message";
57
+ const files = email.attachments?.map((att) => att.filename).filter(Boolean) ?? [];
58
+ const attachmentSummary = attachmentNames(email.attachments);
59
+
45
60
  return (
46
- <div className="overflow-x-auto rounded-2xl bg-muted/20 p-2">
47
- <EmailCard
48
- subject={email.subject || "Message"}
49
- body={email.body || ""}
50
- headers={headers}
51
- brand={brand ?? emailBrandFromContext(context)}
52
- />
61
+ <div className="overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
62
+ <div className="space-y-3 border-b border-border bg-background px-4 py-3">
63
+ <div className="flex items-start gap-3">
64
+ <div
65
+ className="flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-semibold"
66
+ style={{ backgroundColor: theme.accent, color: theme.onAccent }}
67
+ aria-hidden
68
+ >
69
+ {senderInitials(theme.name)}
70
+ </div>
71
+ <div className="min-w-0 flex-1 space-y-1">
72
+ <div className="flex items-baseline justify-between gap-3">
73
+ <div className="min-w-0">
74
+ <p className="truncate text-sm font-semibold text-foreground">{theme.name}</p>
75
+ {theme.website ? (
76
+ <p className="truncate text-xs text-muted-foreground">{theme.website}</p>
77
+ ) : null}
78
+ </div>
79
+ <time className="shrink-0 text-xs text-muted-foreground">Just now</time>
80
+ </div>
81
+ {to ? <ChromeRow label="To" value={to} /> : null}
82
+ {cc ? <ChromeRow label="Cc" value={cc} /> : null}
83
+ {bcc ? <ChromeRow label="Bcc" value={bcc} /> : null}
84
+ <p className="pt-1 text-sm font-medium leading-snug text-foreground">{subject}</p>
85
+ </div>
86
+ </div>
87
+ {files.length > 0 ? (
88
+ <ul className="flex flex-wrap gap-1.5" aria-label={`Attachments: ${attachmentSummary}`}>
89
+ {files.map((filename) => (
90
+ <li
91
+ key={filename}
92
+ className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-border bg-muted/40 px-2 py-1 text-xs text-foreground"
93
+ >
94
+ <PaperclipIcon className="size-3 shrink-0 text-muted-foreground" />
95
+ <span className="truncate">{filename}</span>
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ ) : null}
100
+ </div>
101
+ <div className="bg-muted/30 p-3">
102
+ <EmailCard subject={subject} body={email.body || ""} brand={theme} />
103
+ </div>
53
104
  </div>
54
105
  );
55
106
  };