Data

EmailPreview

An email as a client shows it: subject, labels and star, sender with picture, to and cc, the date, the message and attachments to download.

#document#card#user#file#no-js

Demo

Preview

Q3 report, with the numbers

WorkFinance
Grace Hopper <grace@example.com>
to me, Alan Turing; cc Finance

Hi Ada,\n\nThe Q3 report is attached, with the spreadsheet behind it.\nPage 4 has the one question I still have.\n\nThanks,\nGrace

3 attachments

Welcome to Quillo

hello@quillolang.org

Thanks for signing up. Start with the tour, or read the guide.

Get it

It needs theme.qwe, as every component does, and avatar.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/avatar.qwe$ curl -O https://quillolang.org/components/ui/email_preview.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/email_preview.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/email_preview.qwe
view EmailPreviewDemo() =    <div.demo-stack>        <EmailPreview id="email-preview-demo-q3" subject="Q3 report, with the numbers" date="2026-10-27T14:30:00Z" labels={["Work", "Finance"]}                      starred={true} unread={true} me="ada@example.com"                      from={{ name: "Grace Hopper", email: "grace@example.com", src: "https://i.pravatar.cc/80?img=47" }}                      to={["ada@example.com", { name: "Alan Turing", email: "alan@example.com" }]} cc={[{ name: "Finance", email: "finance@example.com" }]}                      body="Hi Ada,\n\nThe Q3 report is attached, with the spreadsheet behind it.\nPage 4 has the one question I still have.\n\nThanks,\nGrace"                      attachments={[{ name: "Q3 report.pdf", size: 2483112, href: "#email_preview" }, { name: "Q3 numbers.xlsx", size: 84211, href: "#email_preview" },                                    { name: "chart.png", size: 312000 }]}/>        <EmailPreview id="email-preview-demo-html" subject="Welcome to Quillo" from={{ email: "hello@quillolang.org" }} date="2026-10-01">            <p>Thanks for signing up. <a href="#email_preview">Start with the tour</a>, or read <strong>the guide</strong>.</p>        </EmailPreview>    </div>

Source

ui/email_preview.qwe
// EmailPreview: an email as a mail client shows it: the subject with its labels,// who sent it (picture, name and address) and to whom, when, the message, and its// attachments to download.////   <EmailPreview subject="Q3 report" date="2026-10-27T14:30:00Z" labels={["Work"]}//                 from={{ name: "Grace Hopper", email: "grace@example.com" }} to={["ada@example.com"]}//                 body="Hi Ada,\n\nThe report is attached.\n\nGrace"//                 attachments={[{ name: "Q3 report.pdf", size: 2483112, href: "/files/q3.pdf" }]}/>//// subject: its subject. from: { name, email, src? } (src their picture; initials// without one). to, cc: lists of addresses, or of { name, email }; me: the reader's// address, written "me" among them. date: an ISO date-time, shown as "Tue, Oct 27,// 2026, 14:30" (its time as written). body: the message as plain text, paragraphs// split at blank lines and line breaks kept; or children for HTML you trust.// attachments: { name, size?, href? } (size in bytes). labels: words shown as// chips by the subject. starred: a star by the subject. unread: the subject bold// with a dot. id: its id. It is an <article>. Needs avatar.qwe.import ui/avatar.qwe use Avatarimport ui/theme.qwe use ui_clslet email_preview_css = css`.ui-email-preview { display: grid; gap: 16px; min-width: 0; padding: 20px 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.55 var(--ui-font); }.ui-email-preview-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }.ui-email-preview-subject { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }.ui-email-preview-unread .ui-email-preview-subject { font-weight: 750; }.ui-email-preview-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ui-accent); }.ui-email-preview-label { padding: 1px 8px; border-radius: 4px; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 12px; font-weight: 500; }.ui-email-preview-star { width: 18px; height: 18px; margin-left: auto; color: var(--ui-warning); }.ui-email-preview-head { display: grid; grid-template-columns: auto 1fr auto; gap: 2px 12px; align-items: center; }.ui-email-preview-head .ui-avatar { grid-row: span 2; }.ui-email-preview-from { min-width: 0; overflow-wrap: anywhere; }.ui-email-preview-from b { font-weight: 650; }.ui-email-preview-addr { color: var(--ui-muted); font-size: 13px; }.ui-email-preview-to { grid-column: 2 / -1; color: var(--ui-muted); font-size: 13px; overflow-wrap: anywhere; }.ui-email-preview-date { color: var(--ui-muted); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }.ui-email-preview-body { min-width: 0; overflow-wrap: anywhere; }.ui-email-preview-body p { margin: 0 0 12px; white-space: pre-line; }.ui-email-preview-body > :last-child { margin-bottom: 0; }.ui-email-preview-files { display: grid; gap: 8px; padding-top: 14px; border-top: 1px solid var(--ui-border); }.ui-email-preview-files h4 { margin: 0; color: var(--ui-muted); font-size: 12px; font-weight: 600; }.ui-email-preview-files ul { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-email-preview-file { display: flex; align-items: center; gap: 10px; max-width: 260px; padding: 8px 12px 8px 8px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); color: var(--ui-fg); text-decoration: none; }a.ui-email-preview-file:hover { border-color: var(--ui-accent); background: var(--ui-accent-soft); }a.ui-email-preview-file:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-email-preview-ext { --k: var(--ui-muted); flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 6px;  background: color-mix(in srgb, var(--k) 16%, transparent); color: var(--k); font: 700 9px/1 var(--ui-font); }.ui-email-preview-k-pdf { --k: var(--ui-danger); } .ui-email-preview-k-image { --k: var(--ui-success); } .ui-email-preview-k-doc { --k: var(--ui-info); }.ui-email-preview-k-sheet { --k: var(--ui-success); } .ui-email-preview-k-archive { --k: var(--ui-warning); }.ui-email-preview-fname { display: grid; min-width: 0; font-size: 13px; }.ui-email-preview-fname span:first-child { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }.ui-email-preview-fname span + span { color: var(--ui-muted); font-size: 12px; }@media (max-width: 520px) { .ui-email-preview { padding: 16px; } .ui-email-preview-head { grid-template-columns: auto 1fr; } .ui-email-preview-date { grid-column: 2; } }`let email_preview_days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]let email_preview_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date-time as "Tue, Oct 27, 2026, 14:30" (the time as written); other text as it is.fn email_preview_date(d) -> str {    let s = str(d)    let day = slice(s, 0, 10)    if !date.valid(day) { return s }    let p = date.parts(day)    let base = `${email_preview_days[p.weekday - 1]}, ${email_preview_months[p.month - 1]} ${p.day}, ${p.year}`    len(s) >= 16 ? `${base}, ${slice(s, 11, 16)}` : base}fn email_preview_size(n: num) -> str {    if n < 1024 { return `${n} B` }    let unit = n < 1024 * 1024 ? 1024 : (n < 1024 * 1024 * 1024 ? 1024 * 1024 : 1024 * 1024 * 1024)    let tenths = round(n * 10 / unit)    `${floor(tenths / 10)}${tenths % 10 == 0 ? "" : `.${tenths % 10}`} ${unit == 1024 ? "KB" : (unit == 1024 * 1024 ? "MB" : "GB")}`}fn email_preview_ext(name: str) -> str {    let parts = split(name, ".")    len(parts) > 1 ? lower(last(parts)) : ""}fn email_preview_kind(name: str) -> str =    match email_preview_ext(name) {        "pdf" => "pdf"        "png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" => "image"        "doc" | "docx" | "txt" | "md" | "odt" => "doc"        "xls" | "xlsx" | "csv" | "ods" => "sheet"        "zip" | "gz" | "tar" | "7z" => "archive"        _ => "file"    }// People as the header writes them: names where given, "me" for the reader.fn email_preview_people(people: list, me) -> str =    join(people | map((p) => {        let addr = type_of(p) == "map" ? p.email : str(p)        me != null && addr == me ? "me" : (type_of(p) == "map" ? (p.name ?? p.email) : str(p))    }), ", ")view EmailPreview(subject: str, from: map, to: list = [], cc: list = [], me: str? = null, date = null, body: str? = null,                  attachments: list = [], labels: list = [], starred: bool = false, unread: bool = false, id: str? = null,                  class: str? = null, children) {    style global email_preview_css    <article class={ui_cls(["ui-email-preview", unread && "ui-email-preview-unread", class])} id={id} aria-label={`Email: ${subject}`}>        <div.ui-email-preview-top>            {unread && <span.ui-email-preview-dot role="img" aria-label="Unread"></span>}            <h2.ui-email-preview-subject>{subject}</h2>            {for l in labels => <span.ui-email-preview-label key={l}>{l}</span>}            {starred && <svg.ui-email-preview-star viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="Starred">                <path d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/>            </svg>}        </div>        <div.ui-email-preview-head>            <Avatar name={from.name ?? from.email} src={from.src}/>            <div.ui-email-preview-from>                <b>{from.name ?? from.email}</b>                {from.name != null && from.email != null && <span.ui-email-preview-addr> &lt;{from.email}&gt;</span>}            </div>            {date != null && <time.ui-email-preview-date datetime={str(date)}>{email_preview_date(date)}</time>}            {(len(to) > 0 || len(cc) > 0) && <div.ui-email-preview-to>                {len(to) > 0 && `to ${email_preview_people(to, me)}`}{len(to) > 0 && len(cc) > 0 && "; "}{len(cc) > 0 && `cc ${email_preview_people(cc, me)}`}            </div>}        </div>        <div.ui-email-preview-body>            {body != null && for [i, para] in enumerate(re.split(trim(body), "\\n\\s*\\n")) => <p key={i}>{para}</p>}            {children}        </div>        {len(attachments) > 0 && <div.ui-email-preview-files>            <h4>{len(attachments)} {len(attachments) == 1 ? "attachment" : "attachments"}</h4>            <ul>                {for f in attachments => {                    let inner = [                        <span class={`ui-email-preview-ext ui-email-preview-k-${email_preview_kind(f.name)}`} aria-hidden="true">{upper(slice(email_preview_ext(f.name), 0, 4))}</span>,                        <span.ui-email-preview-fname><span>{f.name}</span>{f.size != null && <span>{email_preview_size(f.size)}</span>}</span>,                    ]                    <li key={f.name}>{f.href != null                        ? <a.ui-email-preview-file href={f.href} download={f.name} aria-label={`Download ${f.name}`}>{inner}</a>                        : <span.ui-email-preview-file>{inner}</span>}</li>                }}            </ul>        </div>}    </article>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On EmailPreview: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.