feat(webmail): the new interface - logo, dark and light themes, command menu

A mark (a lambda on an ultramarine tile with the flap of an envelope), the
favicon, home-screen icons and an install manifest. Every colour is a variable
defined once per theme, and the Tailwind scales the screens already use read
them, so dark (the default) and light are a change of values and not of
classes. Geist is served by the app itself. Ctrl+K opens a command menu for
folders, actions, accounts and appearance, and typing something that matches no
command searches the mail; c writes a message and / goes to the search field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-22 22:33:15 -03:00
1 parent 6b97807856
commit e667843b59
38 files changed
+1161 -316

No files matched your search

@@ -105,21 +105,21 @@ function initialsOf(name: string, address: string): string {
/** A short type tag and colour for an attachment, from its extension. */
function attachmentKind(filename: string): { tag: string; className: string } {
const ext = (filename.split(".").pop() ?? "").toLowerCase();
if (ext === "pdf") return { tag: "PDF", className: "bg-rose-700" };
if (["png", "jpg", "jpeg", "gif", "webp", "bmp"].includes(ext)) return { tag: "IMG", className: "bg-sky-700" };
if (["doc", "docx", "odt", "rtf", "txt"].includes(ext)) return { tag: "DOC", className: "bg-blue-700" };
if (["xls", "xlsx", "ods", "csv"].includes(ext)) return { tag: "XLS", className: "bg-emerald-700" };
if (["zip", "rar", "7z", "gz", "tar"].includes(ext)) return { tag: "ZIP", className: "bg-amber-700" };
return { tag: (ext || "FILE").slice(0, 4).toUpperCase(), className: "bg-slate-600" };
if (ext === "pdf") return { tag: "PDF", className: "bg-[#c2352f]" };
if (["png", "jpg", "jpeg", "gif", "webp", "bmp"].includes(ext)) return { tag: "IMG", className: "bg-[#1f7aa8]" };
if (["doc", "docx", "odt", "rtf", "txt"].includes(ext)) return { tag: "DOC", className: "bg-[#2f5fb8]" };
if (["xls", "xlsx", "ods", "csv"].includes(ext)) return { tag: "XLS", className: "bg-[#1f8a5b]" };
if (["zip", "rar", "7z", "gz", "tar"].includes(ext)) return { tag: "ZIP", className: "bg-[#a8690f]" };
return { tag: (ext || "FILE").slice(0, 4).toUpperCase(), className: "bg-[#5b6272]" };
}
// One look for every control on the command bar, so the bar reads as a single
// strip. The label hides on narrow screens and the icon stays, which is what
// keeps six actions on one row at phone width.
const commandClass =
"inline-flex h-10 min-w-10 items-center justify-center gap-1.5 rounded-lg px-2.5 text-[13px] text-slate-200 transition-colors hover:bg-white/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 md:px-3";
"inline-flex h-10 min-w-10 items-center justify-center gap-1.5 rounded-lg px-2.5 text-[13px] text-slate-200 transition-colors hover:bg-ink/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 md:px-3";
const infoLinkClass =
"rounded-md px-1.5 py-1 text-[12.5px] font-medium text-indigo-300 transition-colors hover:bg-white/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
"rounded-md px-1.5 py-1 text-[12.5px] font-medium text-indigo-300 transition-colors hover:bg-ink/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
export function MessageReader(props: Props) {
const { message, row, folder, actions } = props;
@@ -181,7 +181,7 @@ export function MessageReader(props: Props) {
<div
role="toolbar"
aria-label={t("mail.messageActions")}
className="flex flex-none flex-wrap items-center gap-0.5 bg-dark-bg px-2 py-1.5 shadow-[0_1px_0_0_rgba(233,233,237,0.09)] sm:px-4"
className="flex flex-none flex-wrap items-center gap-0.5 bg-dark-bg px-2 py-1.5 shadow-[0_1px_0_0_rgb(var(--ink)/0.09)] sm:px-4"
>
<button
type="button"
@@ -218,7 +218,7 @@ export function MessageReader(props: Props) {
</Link>
)}
<span aria-hidden className="mx-1 h-5 w-px bg-white/[0.12]" />
<span aria-hidden className="mx-1 h-5 w-px bg-ink/[0.12]" />
<button type="button" onClick={props.onDelete} className={commandClass} title={deleteLabel}>
<Trash2 className="h-4 w-4" />
@@ -252,7 +252,7 @@ export function MessageReader(props: Props) {
setMoveOpen(false);
props.onMove(target.name);
}}
className="block w-full px-3.5 py-2.5 text-left text-[13px] text-slate-200 transition-colors hover:bg-white/[0.07] focus-visible:bg-white/[0.07] focus-visible:outline-none"
className="block w-full px-3.5 py-2.5 text-left text-[13px] text-slate-200 transition-colors hover:bg-ink/[0.07] focus-visible:bg-ink/[0.07] focus-visible:outline-none"
>
{target.label}
</button>
@@ -321,7 +321,7 @@ export function MessageReader(props: Props) {
address={row?.sender_address ?? address}
initials={initialsOf(displayName, address)}
size={44}
className="bg-indigo-900 text-indigo-300 shadow-[inset_0_0_0_1px_#5d5294]"
className="bg-indigo-900 text-indigo-300 shadow-[inset_0_0_0_1px_rgb(var(--a-700))]"
/>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
@@ -339,7 +339,7 @@ export function MessageReader(props: Props) {
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 ${
props.senderTrusted
? "bg-indigo-500/20 text-indigo-200 hover:bg-indigo-500/30"
: "text-slate-400 shadow-[inset_0_0_0_1px_rgba(233,233,237,0.18)] hover:bg-white/[0.07] hover:text-slate-200"
: "text-slate-400 shadow-[inset_0_0_0_1px_rgb(var(--ink)/0.18)] hover:bg-ink/[0.07] hover:text-slate-200"
}`}
>
<ShieldCheck className="h-3 w-3" aria-hidden />
@@ -364,7 +364,7 @@ export function MessageReader(props: Props) {
type="button"
onClick={() => setRecipientsOpen((open) => !open)}
aria-expanded={recipientsOpen}
className="ml-1 inline-flex items-center gap-0.5 rounded px-1 text-indigo-300 hover:bg-white/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
className="ml-1 inline-flex items-center gap-0.5 rounded px-1 text-indigo-300 hover:bg-ink/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
>
{recipientsOpen ? t("mail.showFewerRecipients") : `+${hiddenCount}`}
<ChevronDown className={`h-3 w-3 transition-transform ${recipientsOpen ? "rotate-180" : ""}`} />