Mail · Boxes and lists: mailboxes, folders and labels, the mail list, inbox zero

MailboxList sets mailboxes, folders and labels in sections: a press, an
arrow or Enter opens one, the open one lit; a box with mail unread
stands out with its count, and a label wears one of the theme's chart
hues. MailList holds messages in a multiple-select list: the sender
strong beside a dot while unread, the subject and first words under
it, the time today, the weekday this week, the date this year or with
its year, a paperclip for files, and a star whose press stays off the
row. InboxZeroState points to feedback::EmptyState.

Sections of selectable lists now have one home, lists::Sections, which
ChannelList and MemberList use too; ListItem gains a third line.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 23:35:04 -04:00
1 parent 7b38f99dcf
commit 7d20567921
17 files changed
+1244 -153

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
use ely_gpui_component::{
buttons::Button,
feedback::EmptyState,
mail::{Mail, MailList, Mailbox, MailboxList},
primitives::IconName,
theme::{ActiveTheme, Radius},
};
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{SignedDuration, Timestamp};
use super::Page;
use crate::{
probe::probe,
script::Step,
ui::{change, keep, section},
};
pub const PAGE: Page = Page {
number: 29,
slug: "mail",
title: "Mail",
summary: "Mailboxes and the messages in them: reading, writing, sorting and putting off.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::DownAt("mail-boxes", 100.0, 139.0),
Step::UpAt("mail-boxes", 100.0, 139.0),
Step::Wait(200),
Step::Shot("box-opened"),
Step::DownAt("mail-list", 417.0, 112.0),
Step::UpAt("mail-list", 417.0, 112.0),
Step::Wait(200),
Step::Shot("starred"),
];
fn boxes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let open = keep("mail-open-box", || SharedString::from("inbox"), window, cx);
let shown = open.read(cx).clone();
section(
"MailboxList / FolderList",
"Mailboxes, folders and labels in sections: a press, an arrow or Enter opens one, and the one open is lit. A box with mail unread stands out with its count; a label wears its hue.",
cx,
)
.child(probe(
"mail-boxes",
div().w(px(260.)).child(
MailboxList::new("mail-boxes")
.section(
"Mailboxes",
[
Mailbox::new("inbox", "Inbox", IconName::Inbox).unread(12),
Mailbox::new("starred", "Starred", IconName::Star),
Mailbox::new("snoozed", "Snoozed", IconName::Clock),
Mailbox::new("sent", "Sent", IconName::Send),
Mailbox::new("drafts", "Drafts", IconName::FileText).unread(2),
Mailbox::new("archive", "Archive", IconName::Archive),
Mailbox::new("spam", "Spam", IconName::OctagonAlert).unread(4),
Mailbox::new("trash", "Trash", IconName::Trash2),
],
)
.section(
"Folders",
[
Mailbox::new("atrium", "Atrium", IconName::Folder).unread(3),
Mailbox::new("invoices", "Invoices", IconName::Folder),
],
)
.section(
"Labels",
[
Mailbox::label("clients", "Clients", 0),
Mailbox::label("studio", "Studio", 5),
Mailbox::label("press", "Press", 3),
],
)
.open(shown)
.on_open(move |key, _, cx| change(&open, cx, |open| *open = key.clone())),
),
))
}
/// The demo's inbox: dated back from now, so the list reads the time, the weekday, the date and the year.
fn inbox() -> Vec<Mail> {
let now = Timestamp::now();
let ago = |minutes: i64| now - SignedDuration::from_mins(minutes);
vec![
Mail::new(
"review",
"Ana Lima",
"Friday's review moves to 10:00",
ago(12),
)
.snippet("We'll meet in the model room. Bring the plaster samples if they've dried.")
.unread(true),
Mail::new("samples", "Ben Ito", "Plaster samples", ago(95))
.snippet("Three finishes are in the stair hall: lime, tadelakt and a sanded gypsum.")
.attachments(3)
.unread(true),
Mail::new("brief", "Chloé Martin", "The brief, final", ago(60 * 26))
.snippet("Light from above, one stair, olive trees by the windows.")
.starred(true),
Mail::new(
"invoice",
"Studio Office",
"Invoice 2026-114",
ago(60 * 24 * 3),
)
.snippet("Please find the invoice for the September model work attached.")
.attachments(1),
Mail::new(
"press",
"Dev Rao",
"A visit from the press",
ago(60 * 24 * 9),
)
.snippet("They'd like to see the atrium before the opening. Would the 14th work?"),
Mail::new(
"welcome",
"Eli Park",
"Welcome to the studio",
ago(60 * 24 * 400),
)
.snippet("A few notes to start: where the models live, and who to ask.")
.starred(true),
]
}
fn list(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let selected = keep(
"mail-selected",
|| vec![SharedString::from("review")],
window,
cx,
);
let stars = keep(
"mail-stars",
|| vec![SharedString::from("brief"), SharedString::from("welcome")],
window,
cx,
);
let starred = stars.read(cx).clone();
let mails = inbox()
.into_iter()
.map(|mail| {
let on = starred.contains(&mail.key);
mail.starred(on)
})
.collect::<Vec<_>>();
let chosen = selected.read(cx).clone();
section(
"MailList / MailItem",
"Messages with the sender strong while unread, the subject and first words under it, when each came, a paperclip for files, and a star to press. A press or an arrow selects; Command or Shift takes several.",
cx,
)
.child(probe(
"mail-list",
div().w(px(440.)).child(
MailList::new("mail-list", mails)
.selected(chosen)
.on_select(move |keys, _, cx| change(&selected, cx, |selected| *selected = keys.to_vec()))
.on_star(move |key, on, _, cx| {
change(&stars, cx, |stars| {
stars.retain(|each| each != key);
if on {
stars.push(key.clone());
}
})
}),
),
))
}
fn zero(cx: &mut App) -> impl IntoElement + use<> {
let theme = cx.theme();
section(
"InboxZeroState → EmptyState",
"An inbox with nothing left in it says so, calm and brief.",
cx,
)
.child(probe(
"mail-zero",
div()
.w(px(440.))
.py_6()
.border_1()
.border_color(theme.colors.border)
.rounded(theme.radius(Radius::Lg))
.child(
EmptyState::new("mail-zero", IconName::Inbox, "All caught up")
.body("Nothing waits in your inbox. New mail lands here, newest first.")
.action(Button::new("mail-zero-archive", "Open the archive")),
),
))
}
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(boxes(window, cx))
.child(list(window, cx))
.child(zero(cx))
.into_any_element()
}
+2
View File
@@ -15,6 +15,7 @@ mod generative;
mod git;
mod layout;
mod lists;
mod mail;
mod media;
mod menus;
mod messaging;
@@ -71,6 +72,7 @@ pub const ALL: &[Page] = &[
media::PAGE,
files::PAGE,
messaging::PAGE,
mail::PAGE,
theme::PAGE,
];