Mail: a smartphone layout

Below 768px (or a touch screen under 500px tall) the mail view becomes one
column at a time: the list full screen, with a menu button beside the search
box that slides the sidebar in as a 70%-wide drawer (tap outside, pick a
folder or press Esc to close), and a floating compose button. Opening a
conversation or a draft shows the reading pane full screen with a back
arrow; composers, including reply/reply-all/forward, open full screen.

List rows stack sender/date, subject and snippet; tapping an avatar selects
a row and the bulk actions sit above the list. The app switcher, Settings
link, select-all box, resizers and right panel are hidden on a phone, and
the composer header drops the Attach label and pop-out at phone width.

Everything is gated on useIsMobile(); the desktop layout renders as before
(checked by screenshot comparison at 1440x900).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 08:09:09 +02:00
co-authored by Claude Opus 5.5
parent cfc89e78b3
commit fe3fd94165
7 changed files with 299 additions and 63 deletions
+54 -2
View File
@@ -98,6 +98,7 @@ export function MessageList({
checkedIds,
dimmedIds,
onToggleCheck,
compact = false,
}: {
threads: ThreadSummary[];
/** Which page of the list this is. Changing it scrolls back to the top. */
@@ -113,6 +114,8 @@ export function MessageList({
checkedIds: Set<string>;
dimmedIds?: Set<string>;
onToggleCheck: (id: string) => void;
/** Phone layout: sender, subject and snippet stacked; no hover-only actions. */
compact?: boolean;
}) {
const t = useT();
const parentRef = useRef<HTMLDivElement>(null);
@@ -214,7 +217,11 @@ export function MessageList({
className="absolute -left-[21px] top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-app-accent"
/>
)}
<div className={checkMode ? "hidden" : "contents group-hover/av:hidden"}>
<div
className={
checkMode ? "hidden" : compact ? "contents" : "contents group-hover/av:hidden"
}
>
<span
className="relative flex h-[33px] w-[33px] items-center justify-center overflow-hidden rounded-full text-[13px] font-semibold text-white"
style={{ backgroundColor: avatarBg }}
@@ -233,8 +240,10 @@ export function MessageList({
aria-label={t("mail.select")}
aria-pressed={checked}
onClick={stop(() => onToggleCheck(thread.id))}
// No hover on a touch screen: the avatar itself is the (invisible)
// selection target until selection mode shows the boxes.
className={`absolute inset-0 items-center justify-center ${
checkMode ? "flex" : "hidden group-hover/av:flex"
checkMode ? "flex" : compact ? "flex opacity-0" : "hidden group-hover/av:flex"
}`}
>
<span
@@ -253,6 +262,47 @@ export function MessageList({
</button>
</div>
{compact ? (
<div className="flex min-w-0 flex-1 flex-col justify-center">
<div className="flex min-w-0 items-center gap-1.5">
<span
className={`min-w-0 truncate text-[15px] ${
unread ? "font-black text-app-fg" : "text-app-fg"
}`}
>
{names}
</span>
{thread.message_count > 1 && (
<span className="shrink-0 rounded-full bg-app-hover px-1.5 text-[12px] text-app-muted">
{thread.message_count}
</span>
)}
<span className="ml-auto flex shrink-0 items-center gap-1.5 pl-2 text-app-faint">
{thread.has_attachments && <Paperclip />}
{thread.pinned && (
<Icon name="pin" size={15} className="text-[rgb(220_129_143)]" />
)}
<span
className={`whitespace-nowrap text-[13px] tabular-nums ${
unread ? "font-bold" : ""
}`}
>
{timeLabel(thread.last_message_at)}
</span>
</span>
</div>
<div
className={`flex min-w-0 items-center gap-1.5 text-[14px] ${
unread ? "font-black text-app-fg" : "text-app-fg"
}`}
>
<LabelChips labels={labelMap[thread.id] ?? []} />
<span className="truncate">{thread.subject}</span>
</div>
<div className="truncate text-xs text-app-faint">{thread.snippet}</div>
</div>
) : (
<>
{/* Sender column */}
<div className="flex w-[200px] min-w-0 shrink items-center gap-1.5">
<span
@@ -347,6 +397,8 @@ export function MessageList({
{timeLabel(thread.last_message_at)}
</span>
</div>
</>
)}
</div>
</div>
);