Digest email
ProA Digest as an email: the same data and sections (needs you, health, what was done, notes), on EmailLayout's shell and colours, as a whole HTML document. Trends are in words, "Up 12% since last week" or "Not enough data yet: 2 points", since a sparkline's SVG doesn't survive mail clients; links are the app's URLs; agents and people are named as the app names them, and notes are text. Table layout and inline styles, so it survives email clients.
It is server-safe (no "use client", no hooks): render it to a string with renderDigestEmail in a route handler, a server action or a worker. For the same email inline on a page, use DigestEmailPreview.
Version 5.4.0· other versions
This page is for 5.4.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0, 5.0.0, 5.1.0 and 5.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/digest-email
Brings in: bidi, digest, email-heading, email-layout, heading-level, tokens
Usage
"use client"
import * as React from "react"
import { DigestEmailPreview, renderDigestEmail, type DigestEmailOptions } from "@/components/candor/digest-email"
import type { DigestData } from "@/components/candor/digest"
import { Button } from "@/components/candor/ui/button"
const planner = { kind: "agent" as const, agent: { id: "planner", name: "Planning agent", role: "planner" } }
const digest: DigestData = {
title: "Your week",
period: "6 to 12 November",
needsYou: [{ id: "n1", title: "Approve the supplier import", detail: "Waiting since Tuesday", href: "https://app.example.com/review/n1" }],
health: [
{ id: "h1", label: "Runs finished", value: 128, points: [96, 104, 99, 115, 121, 128], change: { value: 0.12, format: "percent", period: "since last week" } },
{ id: "h2", label: "Reruns", value: 5, points: [7, 5], change: { value: -2, period: "since last week" } },
{ id: "h3", label: "Hours saved", value: null, missing: "Not measured until the timer is on.", points: [] },
],
done: [
{ id: "d1", title: "Drafted the quarterly summary", by: planner, href: "https://app.example.com/docs/summary" },
{ id: "d2", title: "Signed off the plan", by: { kind: "person", name: "Dana Reyes" } },
],
notes: [{ id: "g1", name: "Northwind", notes: [{ id: "x1", text: "The supplier's file arrived a day late; nothing downstream moved.", by: planner }] }],
}
const options: DigestEmailOptions = {
brand: "Keel",
why: "You get this every Monday. Change it in Settings, Notifications.",
link: { label: "Open your week", href: "https://app.example.com/week" },
}
export default function Example() {
const [frame, setFrame] = React.useState(false)
// renderDigestEmail's string goes to your mailer, from a route handler or a worker. DigestEmailPreview shows the same
// email inline; the iframe shows the exact document.
const html = React.useMemo(() => renderDigestEmail(digest, options), [])
return (
<div className="flex max-w-xl flex-col gap-3">
<DigestEmailPreview digest={digest} {...options} className="border" />
<Button size="sm" variant="outline" className="self-start" aria-expanded={frame} onClick={() => setFrame((o) => !o)}>
{frame ? "Hide the HTML document" : "Show the HTML document"}
</Button>
{frame && <iframe title="Email document" srcDoc={html} sandbox="" className="block h-[30rem] w-full rounded-xl border bg-white" />}
</div>
)
}<DigestEmail>
| Prop | Type | Default | Description |
|---|---|---|---|
| brandrequired | string | — | The product name shown at the top. |
| whyrequired | string | — | Why the person got this, e.g. "You get this every Monday. Change it in Settings." |
| preheader | string | undefined | — | Shown in the inbox preview. Defaults to the digest's period, else its title. |
| link | { label: string; href: string } | undefined | — | One button at the end, to the digest in the app: its words and the app's URL. |
| minPoints | number | undefined | — | The fewest numbers a health row's points need before its change is written, at least 2. Defaults to 4, as Digest's. |
| colors | EmailColors | undefined | — | Fixed hex colours to match your brand. Defaults to EmailLayout's, the default theme's light values. |
| strings | Partial<Words> | undefined | — | The email's fixed words, e.g. `{ health: "Santé" }`. Passed directly: emails render outside any LocaleProvider. |
| lang | string | undefined | — | The email's language, for `<html lang>`, numbers and plural forms. Defaults to "en", as is a value that isn't a language tag. |
| digestrequired | DigestData | — | What the digest shows: Digest's data. |
<DigestEmailPreview>
The digest email inline, for a settings page or the docs: no <html> document, its own fixed colours so it looks as it will in a mail client. Its title is the one heading it adds to your page, at the level your page gives it; the sections' names are drawn as in the email but aren't headings here.
| Prop | Type | Default | Description |
|---|---|---|---|
| brandrequired | string | — | The product name shown at the top. |
| whyrequired | string | — | Why the person got this, e.g. "You get this every Monday. Change it in Settings." |
| preheader | string | undefined | — | Shown in the inbox preview. Defaults to the digest's period, else its title. |
| link | { label: string; href: string } | undefined | — | One button at the end, to the digest in the app: its words and the app's URL. |
| minPoints | number | undefined | — | The fewest numbers a health row's points need before its change is written, at least 2. Defaults to 4, as Digest's. |
| colors | EmailColors | undefined | — | Fixed hex colours to match your brand. Defaults to EmailLayout's, the default theme's light values. |
| strings | Partial<Words> | undefined | — | The email's fixed words, e.g. `{ health: "Santé" }`. Passed directly: emails render outside any LocaleProvider. |
| lang | string | undefined | — | The email's language, for `<html lang>`, numbers and plural forms. Defaults to "en", as is a value that isn't a language tag. |
| digestrequired | DigestData | — | What the digest shows: Digest's data. |
| headingLevel | HeadingLevel | undefined | — | The heading level of the email's title on your page. Default: one below the nearest `HeadingScope`, else 3. |
| label | string | undefined | — | The preview's accessible name. Default: "Email preview: " and the title. |
| className | string | undefined | — | Classes for the preview. |
Functions
renderDigestEmail(digest: DigestData, options: DigestEmailOptions)The digest as a complete HTML email, ready for the mailer. It doesn't use react-dom/server, so it runs anywhere: a route handler, a server action, a worker or the browser.
Strings
The fixed words, in English by default. Change any of them under digestEmail in <LocaleProvider strings={{ … }}>; the table is exported as digestEmailStrings.
| Key | English |
|---|---|
| needsYou | Needs you |
| health | Health |
| done | What was done |
| notes | Notes |
| nothing | Nothing to report for this period. |
| notKnown | Not known |
| notEnoughData | { one: "Not enough data yet: {count} point", other: "Not enough data yet: {count} points" } |
| up | Up {amount} {period} |
| down | Down {amount} {period} |
| same | No change {period} |
| things | { one: "{count} thing", other: "{count} things" } |
| wrote | Written by {name} |
| preview | Email preview |
Types
- DigestEmailOptions
- How DigestEmail writes a digest: who it's from, why it was sent, the one link out, and its words and colours.
{ /** The product name shown at the top. */ brand: string; /** Why the person got this, e.g. "You get this every Monday. Change it in Settings." */ why: string; /** Shown in the inbox preview. Defaults to the digest's period, else its title. */ preheader?: string | undefined; /** One button at the end, to the digest in the app: its words and the app's URL. */ link?: { label: string; href: string; } | undefined; /** The fewest numbers a health row's points need before its change is written, at least 2. Defaults to 4, as Digest's. */ minPoints?: number | undefined; /** Fixed hex colours to match your brand. Defaults to EmailLayout's, the default theme's light values. */ colors?: EmailColors | undefined; /** The email's fixed words, e.g. `{ health: "Santé" }`. Passed directly: emails render outside any LocaleProvider. */ strings?: Partial<Words> | undefined; /** The email's language, for `<html lang>`, numbers and plural forms. Defaults to "en", as is a value that isn't a language tag. */ lang?: string | undefined; }
Types from other items
The props above take these. Each is defined by another item; import it from the path shown.
- HeadingLevel from @/lib/candor/heading-level
- A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6