diff --git a/components/email/__tests__/attachment-chips.test.tsx b/components/email/__tests__/attachment-chips.test.tsx
index 1ed87c478..bdff69a81 100644
--- a/components/email/__tests__/attachment-chips.test.tsx
+++ b/components/email/__tests__/attachment-chips.test.tsx
@@ -54,6 +54,18 @@ describe("ListAttachmentChips", () => {
expect(load).not.toHaveBeenCalled();
});
+ it("draws Gmail's pill with a filled type badge in Mountain View", () => {
+ const attachments = [att({ name: "invoice.pdf" })];
+ const { rerender } = render();
+ const chip = screen.getByTitle("invoice.pdf");
+ expect(chip.className).toContain("rounded-full");
+ expect(chip.querySelector(".bg-red-600 svg")).toBeTruthy();
+
+ rerender();
+ expect(screen.getByTitle("invoice.pdf").className).not.toContain("rounded-full");
+ expect(screen.getByTitle("invoice.pdf").querySelector(".bg-red-600")).toBeNull();
+ });
+
it("does not load for a row without a paperclip", () => {
const load = vi.fn(() => () => {});
render();
diff --git a/components/email/attachment-chips.tsx b/components/email/attachment-chips.tsx
index 4ced28825..9a8118ea1 100644
--- a/components/email/attachment-chips.tsx
+++ b/components/email/attachment-chips.tsx
@@ -21,17 +21,18 @@ export function realAttachments(attachments?: Attachment[]): Attachment[] {
);
}
-const ICON_BY_TYPE: { match: RegExp; icon: typeof FileIcon; className: string }[] = [
- { match: /^image\//, icon: FileImage, className: "text-violet-600 dark:text-violet-400" },
- { match: /pdf/, icon: FileText, className: "text-red-600 dark:text-red-400" },
- { match: /sheet|excel|csv/, icon: FileSpreadsheet, className: "text-emerald-600 dark:text-emerald-400" },
- { match: /zip|compress|tar|rar|7z/, icon: FileArchive, className: "text-amber-600 dark:text-amber-400" },
- { match: /word|document|rtf|text\//, icon: FileText, className: "text-sky-600 dark:text-sky-400" },
+// `badge` fills the square Mountain View draws behind a white glyph, as Gmail does.
+const ICON_BY_TYPE: { match: RegExp; icon: typeof FileIcon; className: string; badge: string }[] = [
+ { match: /^image\//, icon: FileImage, className: "text-violet-600 dark:text-violet-400", badge: "bg-red-600" },
+ { match: /pdf/, icon: FileText, className: "text-red-600 dark:text-red-400", badge: "bg-red-600" },
+ { match: /sheet|excel|csv/, icon: FileSpreadsheet, className: "text-emerald-600 dark:text-emerald-400", badge: "bg-emerald-600" },
+ { match: /zip|compress|tar|rar|7z/, icon: FileArchive, className: "text-amber-600 dark:text-amber-400", badge: "bg-zinc-500" },
+ { match: /word|document|rtf|text\//, icon: FileText, className: "text-sky-600 dark:text-sky-400", badge: "bg-blue-600" },
];
function iconFor(type: string, name: string) {
const probe = `${type || ""} ${name || ""}`.toLowerCase();
- return ICON_BY_TYPE.find((e) => e.match.test(probe)) ?? { icon: FileIcon, className: "text-muted-foreground" };
+ return ICON_BY_TYPE.find((e) => e.match.test(probe)) ?? { icon: FileIcon, className: "text-muted-foreground", badge: "bg-zinc-500" };
}
/** Long names are unreadable truncated at the tail — the extension is the
@@ -49,6 +50,13 @@ function shortName(name: string, max = 18): string {
export const CHIP_BOX_CLASS = "inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-0.5 text-xs";
export const CHIP_ICON_CLASS = "h-3.5 w-3.5 flex-shrink-0";
+// Mountain View's chips are Gmail's: a pill with a filled square in front.
+export const MV_CHIP_BOX_CLASS = "inline-flex h-7 items-center gap-2 rounded-full border border-border ps-2 pe-3 text-sm";
+export const MV_CHIP_BADGE_CLASS = "inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[3px] text-white";
+export const MV_CHIP_BADGE_ICON_CLASS = "h-3 w-3";
+
+export type ChipVariant = "default" | "mountain-view";
+
interface AttachmentChipsProps {
attachments?: Attachment[];
onOpen: (attachment: Attachment) => void;
@@ -56,10 +64,12 @@ interface AttachmentChipsProps {
max?: number;
/** A chip drawn ahead of the attachments in the same row (the verification code). */
leading?: ReactNode;
+ variant?: ChipVariant;
className?: string;
}
-export function AttachmentChips({ attachments, onOpen, max = 2, leading, className }: AttachmentChipsProps) {
+export function AttachmentChips({ attachments, onOpen, max = 2, leading, variant = "default", className }: AttachmentChipsProps) {
+ const mv = variant === "mountain-view";
const real = realAttachments(attachments);
if (!real.length && !leading) return null;
@@ -68,10 +78,10 @@ export function AttachmentChips({ attachments, onOpen, max = 2, leading, classNa
const overflow = real.length - shown.length;
return (
-
+
{leading}
{shown.map((a) => {
- const { icon: Icon, className: iconClass } = iconFor(a.type, a.name ?? "");
+ const { icon: Icon, className: iconClass, badge } = iconFor(a.type, a.name ?? "");
return (
);
})}
{overflow > 0 && (
-
+
+{overflow}
)}
@@ -172,8 +193,8 @@ export function ListAttachmentChips({ email, load, ...chipProps }: ListAttachmen
}
return (
-
-
+
+
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx
index eb04344a8..c320b133f 100644
--- a/components/email/thread-list-item.tsx
+++ b/components/email/thread-list-item.tsx
@@ -100,19 +100,26 @@ function UnreadDot({ besideAvatar }: { besideAvatar: boolean }) {
}
/** The chip row under a row's preview: the verification code, then the attachments. */
-function RowChips({ email, loadAttachments, onOpenAttachment }: {
+function RowChips({ email, loadAttachments, onOpenAttachment, mountainView, mvRow }: {
email: Email;
loadAttachments?: LoadListAttachments;
onOpenAttachment?: (attachment: Attachment) => void;
+ /** Gmail's pill chips. */
+ mountainView?: boolean;
+ /** A one-line Mountain View row: the chips start under the subject, as in Gmail. */
+ mvRow?: boolean;
}) {
+ const variant = mountainView || mvRow ? "mountain-view" : "default";
const code = useListVerificationCode(email);
- const leading = code ? : undefined;
+ const leading = code ? : undefined;
+ // The sender column is w-32 (lg:w-40) and the row line has gap-3 after it.
+ const className = cn("mt-1.5", mvRow && "mt-0 pb-2 ps-[8.75rem] lg:ps-[10.75rem]");
// Most rows have no attachment: no loader for them, nor an empty chip row.
const mayHaveAttachments = email.hasAttachment || (email.attachments?.length ?? 0) > 0;
if (onOpenAttachment && mayHaveAttachments) {
- return ;
+ return ;
}
- return leading ? {leading}
: null;
+ return leading ? {leading}
: null;
}
/**
@@ -740,7 +747,7 @@ const SingleEmailItem = React.forwardRef(
{mvRow && (
-
+
)}
>
) : (
@@ -864,7 +871,7 @@ const SingleEmailItem = React.forwardRef(
)}
)}
-
+
>
)}
@@ -1394,6 +1401,7 @@ export const ThreadListItem = React.forwardRef onOpenAttachment(latestEmail, a) : undefined}
+ mvRow
/>
)}
>
@@ -1525,6 +1533,7 @@ export const ThreadListItem = React.forwardRef onOpenAttachment(latestEmail, a) : undefined}
+ mountainView={mvMobileRow}
/>
>
)}
diff --git a/components/email/verification-code-chip.tsx b/components/email/verification-code-chip.tsx
index d3aab8ceb..42fbe108b 100644
--- a/components/email/verification-code-chip.tsx
+++ b/components/email/verification-code-chip.tsx
@@ -8,7 +8,7 @@ import type { Email } from "@/lib/jmap/types";
import { listVerificationCode } from "@/lib/verification-code";
import { useSettingsStore } from "@/stores/settings-store";
import { toast } from "@/stores/toast-store";
-import { CHIP_BOX_CLASS, CHIP_ICON_CLASS } from "./attachment-chips";
+import { CHIP_BOX_CLASS, CHIP_ICON_CLASS, MV_CHIP_BADGE_CLASS, MV_CHIP_BADGE_ICON_CLASS, MV_CHIP_BOX_CLASS, type ChipVariant } from "./attachment-chips";
/** The code a list row offers: found in the subject and preview of a fresh mail. */
export function useListVerificationCode(email: Email): string | null {
@@ -22,11 +22,12 @@ export function useListVerificationCode(email: Email): string | null {
interface VerificationCodeChipProps {
code: string;
+ variant?: ChipVariant;
className?: string;
}
/** The one-time code of a sign-in mail (lib/verification-code.ts); a click copies it. */
-export function VerificationCodeChip({ code, className }: VerificationCodeChipProps) {
+export function VerificationCodeChip({ code, variant = "default", className }: VerificationCodeChipProps) {
const t = useTranslations("email_viewer.verification_code");
const [copied, setCopied] = useState(false);
@@ -56,9 +57,17 @@ export function VerificationCodeChip({ code, className }: VerificationCodeChipPr
title={t("copy", { code })}
aria-label={t("copy", { code })}
data-testid="verification-code-chip"
- className={cn(CHIP_BOX_CLASS, "flex-shrink-0 bg-background/60 font-medium text-foreground hover:bg-muted", className)}
+ className={variant === "mountain-view"
+ ? cn(MV_CHIP_BOX_CLASS, "flex-shrink-0 bg-transparent font-medium text-foreground/80 hover:bg-muted", className)
+ : cn(CHIP_BOX_CLASS, "flex-shrink-0 bg-background/60 font-medium text-foreground hover:bg-muted", className)}
>
-
+ {variant === "mountain-view" ? (
+
+
+
+ ) : (
+
+ )}
{code}
);