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:
1 parent
7b38f99dcf
commit
7d20567921
17 files changed
+1244
-153
No files matched your search
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user