EmailComposerCard

Review one plain-text email and hand it off to a chosen mail client.

EmailComposerCard gives an AI-generated plain-text email a deliberate browser handoff. Users can edit a completed draft, copy one portable email package, then choose Gmail, Outlook, or their default mail handler.

Email draft
import { EmailComposerCard } from "@/components/fable-ui/email-composer-card"

export function WelcomeEmail() {
  return (
    <EmailComposerCard
      to={["reader@example.com"]}
      subject="Welcome to the team"
      body="Hi there,\n\nHere is your first-week checklist."
    />
  )

Installation

pnpm dlx shadcn@latest add shobky/fable-ui/email-composer-card

The registry item installs:

components/fable-ui/email-composer-card/index.ts
components/fable-ui/email-composer-card/email-composer-card.tsx
hooks/use-copy-to-clipboard.ts
lib/fable-ui/tools/show-email-composer-tool.ts
lib/fable-ui/manifests/show-email-composer.md

It depends on the text-editor registry item for shared plain-text draft and direction helpers, Fable core, and shadcn alert, button, card, dropdown-menu, empty, field, input, skeleton, textarea, and tooltip primitives.

Usage

import { EmailComposerCard } from "@/components/fable-ui/email-composer-card"
 
export function WelcomeEmail() {
  return (
    <EmailComposerCard
      to={["reader@example.com"]}
      subject="Welcome to the team"
      body={"Hi there,\n\nHere is your first-week checklist."}
      onDraftChange={(draft) => {
        // Persist only after a field loses focus.
        console.log(draft)
      }}
    />
  )
}

Recipients are comma-separated while editing and validated before sending. An empty recipient list is valid: the user can choose one in the mail client. The To field is always LTR; subject and body follow the supplied direction or auto detection.

The top inline-end Copy icon copies the current edited draft as deterministic plain text:

To: reader@example.com
Subject: Welcome to the team
 
Hi there,
 
Here is your first-week checklist.

To: is omitted when there are no recipients. Copy remains useful for complete, partial, error, and disabled content.

Email handoff

The Send icon opens a fixed menu rather than trying to discover installed applications:

ItemBrowser action
Open in GmailSynchronously opens a blank tab, clears opener, then navigates it to Gmail compose.
Open in OutlookSynchronously opens a blank tab, clears opener, then navigates it to Outlook compose.
Open default email appSynchronously assigns a RFC 6068 mailto: URL to window.location.href.

Gmail and Outlook links are best-effort compose URLs built with URL and URLSearchParams. They require the user to be signed in. The default option depends on the OS and browser mail handler. Browsers cannot enumerate installed mail apps, so the menu is intentionally fixed.

If a browser blocks Gmail or Outlook and window.open returns null, the card shows a visible status instead of claiming the compose window opened. The user can allow popups and try again, or copy the email package.

The mailto: body normalizes line breaks to CRLF and is disabled once the generated URL exceeds the component’s 1800-character reliability guard. Invalid recipients, partial streaming/error drafts, disabled drafts, and too-long links show a visible status while Copy remains available where the content is meaningful.

Tool definition

The AI SDK tool name is show_email_composer.

{
  subject: string
  body: string
  to?: string[]
  editable?: boolean
  direction?: "ltr" | "rtl" | "auto"
}

Use it for one user-reviewed plain-text email. Do not use it for automatic delivery, bulk email, attachments, HTML email, tracking, secrets, or host-side sending. The component opens a user-controlled compose handoff; it never sends mail itself.

lib/fable-ui/tools/show-email-composer-tool.ts
import { tool } from "ai"
import { z } from "zod"

import {
  EmailComposerCard,
  type EmailComposerCardProps,
} from "@/components/fable-ui/email-composer-card"
import { defineFableComponent } from "@/lib/fable-ui/core/definitions"

const directionSchema = z.enum(["ltr", "rtl", "auto"])

export const showEmailComposerInputSchema = z.object({
  subject: z.string(),
  body: z.string(),
  to: z.array(z.string()).optional(),
  editable: z.boolean().default(true),
  direction: directionSchema.default("auto"),
})

export type ShowEmailComposerInput = z.infer<
  typeof showEmailComposerInputSchema
>

function getPartialEmailComposerProps(input: unknown): EmailComposerCardProps {
  const partial =
    input && typeof input === "object" ? (input as Record<string, unknown>) : {}

  return {
    subject: typeof partial.subject === "string" ? partial.subject : "",
    body: typeof partial.body === "string" ? partial.body : "",
    to: Array.isArray(partial.to)
      ? partial.to.filter((value): value is string => typeof value === "string")
      : [],
    editable: typeof partial.editable === "boolean" ? partial.editable : true,
    direction:
      partial.direction === "ltr" ||
      partial.direction === "rtl" ||
      partial.direction === "auto"
        ? partial.direction
        : "auto",
    isStreaming: true,
  }
}

export function createShowEmailComposerTool() {
  return tool({
    description:
      "Show a plain-text email draft with an optional comma-separated recipient list. Use for a single email draft that a user may copy or open in their mail client; do not use for sending mail automatically, rich text, attachments, bulk mail, or host-side delivery.",
    inputSchema: showEmailComposerInputSchema,
    execute: async (input) => input,
  })
}

export const showEmailComposer = defineFableComponent({
  name: "show_email_composer",
  schema: showEmailComposerInputSchema,
  tool: createShowEmailComposerTool(),
  renderer: {
    Component: EmailComposerCard,
    loadingProps: { subject: "", body: "", isLoading: true },
    streamingProps: getPartialEmailComposerProps,
    emptyProps: { subject: "", body: "" },
    errorProps: (description, part) => ({
      ...getPartialEmailComposerProps(part?.input),
      isStreaming: false,
      error: { title: "Email draft unavailable", description },
    }),
    toProps: (data: ShowEmailComposerInput) => data,
  },
})

States

StateBehavior
ReadyShows editable recipient, subject, and body fields. The app menu is enabled only for a valid, short-enough draft.
LoadingShows a preparing status and skeleton.
StreamingShows partial email content read-only. Copy can package meaningful content; Send stays unavailable.
EmptyExplains that no recipient, subject, or body is available.
ErrorShows the error with any partial email read-only and copyable; Send stays unavailable.
DisabledKeeps the completed fields read-only. Copy remains available but the app menu explains that sending is unavailable.

Accessibility and RTL

The compact icons use accessible names and tooltips, and CardAction places them at the logical inline end for LTR and RTL layouts. The recipient control remains LTR for address readability. The menu items have visible provider marks and text labels, so color is never the only identifier.

Manifest and caveats

lib/fable-ui/manifests/show-email-composer.md
---
tool: show_email_composer
type: registry:component
---

# show_email_composer

Use `show_email_composer` to present one plain-text email draft with a subject, body, and optional recipients.

Provide the exact `subject` and `body`. Optional `to` values are individual email addresses. After generation completes, the user can edit the draft, copy one plain-text email package, or choose Gmail, Outlook, or their configured default mail client after recipient validation.

Gmail and Outlook are best-effort browser compose handoffs, not delivery. The fixed menu cannot discover installed apps. Keep subject and body short enough for the mailto handoff; if sending is unavailable, the user can still copy the package.

Avoid it for automatic delivery, bulk mail, attachments, HTML email, tracking, secret values, or host-side sending. An empty recipient list is valid when the user should choose recipients in their mail client.

Mailto length support and provider compose behavior vary by browser and handler. Treat the app choices as user-facing handoffs, not delivery confirmation or installed-app discovery.