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

@@ -75,11 +75,11 @@ interface Props {
}
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";
const replyButtonClass =
"inline-flex h-10 items-center gap-2 rounded-xl px-4 text-[13.5px] text-slate-200 shadow-[inset_0_0_0_1px_rgba(233,233,237,0.18)] transition-colors hover:bg-white/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
"inline-flex h-10 items-center gap-2 rounded-xl px-4 text-[13.5px] text-slate-200 shadow-[inset_0_0_0_1px_rgb(var(--ink)/0.18)] transition-colors hover:bg-ink/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400";
/** The name and address in a header like `Jenny <jenny@x.test>`. */
function splitSender(from: string): { name: string; address: string } {
@@ -306,7 +306,7 @@ export function ThreadReader(props: Props) {
aria-label={thread.subject}
>
{/* The commands that apply to the whole conversation. */}
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 shadow-[inset_0_-1px_0_0_rgba(233,233,237,0.09)] sm:px-4">
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 shadow-[inset_0_-1px_0_0_rgb(var(--ink)/0.09)] sm:px-4">
<button type="button" onClick={props.onClose} className={`${commandClass} md:hidden`} aria-label={t("mail.backToList")}>
<ArrowLeft className="h-4 w-4" />
</button>
@@ -320,7 +320,7 @@ export function ThreadReader(props: Props) {
<span className="hidden sm:inline">{t("mail.replyAll")}</span>
</button>
)}
<span className="mx-1 hidden h-5 w-px bg-white/10 sm:block" />
<span className="mx-1 hidden h-5 w-px bg-ink/10 sm:block" />
<button type="button" onClick={() => void deleteThread()} disabled={busy || inThisFolder.length === 0} className={commandClass}>
<Trash2 className="h-4 w-4" />
<span className="hidden lg:inline">{props.deleteIsPermanent ? t("mail.deleteForever") : t("common.delete")}</span>
@@ -339,14 +339,14 @@ export function ThreadReader(props: Props) {
<ChevronDown className="h-3 w-3" />
</button>
{moveOpen && (
<div role="menu" className="absolute left-0 top-full z-30 mt-1 min-w-[180px] rounded-xl bg-dark-panel p-1.5 shadow-edge ring-1 ring-white/10">
<div role="menu" className="absolute left-0 top-full z-30 mt-1 min-w-[180px] rounded-xl bg-dark-panel p-1.5 shadow-edge ring-1 ring-ink/10">
{props.moveTargets.map((target) => (
<button
key={target.name}
role="menuitem"
type="button"
onClick={() => void moveThread(target.name)}
className="flex w-full items-center rounded-lg px-2.5 py-2 text-left text-[13px] text-slate-200 hover:bg-white/[0.07]"
className="flex w-full items-center rounded-lg px-2.5 py-2 text-left text-[13px] text-slate-200 hover:bg-ink/[0.07]"
>
{target.label}
</button>
@@ -422,7 +422,7 @@ export function ThreadReader(props: Props) {
address={info.sender_address}
initials={initialsOf(displayName, info.sender_address)}
size={isOpen ? 40 : 30}
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))]"
/>
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex items-baseline gap-2">
@@ -430,7 +430,7 @@ export function ThreadReader(props: Props) {
{displayName}
</span>
{isOpen && !info.own && sender.address && (
<span className="min-w-0 truncate text-[12.5px] text-slate-400">&lt;{sender.address}&gt;</span>
<span className="hidden min-w-0 truncate text-[12.5px] text-slate-400 sm:inline">&lt;{sender.address}&gt;</span>
)}
</span>
{!isOpen && (
@@ -477,7 +477,7 @@ export function ThreadReader(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 ${
trustedHere
? "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 />
@@ -486,7 +486,7 @@ export function ThreadReader(props: Props) {
)}
<Link
href={`/user/compose?mode=forward&subject=${encodeURIComponent(loaded.subject)}`}
className="ml-auto inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-indigo-300 hover:bg-white/[0.07]"
className="ml-auto inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-indigo-300 hover:bg-ink/[0.07]"
>
<Forward className="h-3.5 w-3.5" aria-hidden />
{t("mail.forward")}