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:
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" : ""}`} />
|
||||
|
||||
Reference in new issue
Block a user