fix: tighten mail layout responsiveness

This commit is contained in:
zxyszx
2026-08-02 10:03:34 +08:00
parent 83ca0dd7d7
commit 68bcce8091
2 changed files with 97 additions and 78 deletions
+5 -1
View File
@@ -44,7 +44,11 @@
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
font-size: 13px; font-size: 13px;
} }
html, body, #root { min-height: 100%; } html, body, #root {
min-height: 100%;
min-width: 0;
width: 100%;
}
html.dark { color-scheme: dark; } html.dark { color-scheme: dark; }
.dark { .dark {
+92 -77
View File
@@ -89,6 +89,19 @@ const filterLabels: Record<MailFilter, string> = {
const emptyAdvancedSearch: AdvancedMailSearch = { from: "", to: "", subject: "", startDate: "", endDate: "", hasAttachments: false, unread: false, starred: false } const emptyAdvancedSearch: AdvancedMailSearch = { from: "", to: "", subject: "", startDate: "", endDate: "", hasAttachments: false, unread: false, starred: false }
const emptyAdvancedSearchDraft: AdvancedMailSearchDraft = { ...emptyAdvancedSearch } const emptyAdvancedSearchDraft: AdvancedMailSearchDraft = { ...emptyAdvancedSearch }
const mailboxSelectionStorageVersion = "2" const mailboxSelectionStorageVersion = "2"
const mailCompactBreakpoint = 1024
function useMaxViewportWidth(maxWidth: number) {
const [matches, setMatches] = React.useState(false)
React.useEffect(() => {
const media = window.matchMedia(`(max-width: ${maxWidth - 1}px)`)
const sync = () => setMatches(media.matches)
media.addEventListener("change", sync)
sync()
return () => media.removeEventListener("change", sync)
}, [maxWidth])
return matches
}
export function MailPage() { export function MailPage() {
const qc = useQueryClient() const qc = useQueryClient()
@@ -119,6 +132,8 @@ export function MailPage() {
const [language, setLanguage] = useLanguage() const [language, setLanguage] = useLanguage()
const [displayMode] = useDisplayMode() const [displayMode] = useDisplayMode()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const isNarrowMailViewport = useMaxViewportWidth(mailCompactBreakpoint)
const compactMailLayout = isMobile || isNarrowMailViewport || displayMode === "compact"
const [refreshing, setRefreshing] = React.useState(false) const [refreshing, setRefreshing] = React.useState(false)
const [autoRefreshing, setAutoRefreshing] = React.useState(false) const [autoRefreshing, setAutoRefreshing] = React.useState(false)
const [lastAutoRefreshAt, setLastAutoRefreshAt] = React.useState<Date | null>(null) const [lastAutoRefreshAt, setLastAutoRefreshAt] = React.useState<Date | null>(null)
@@ -1052,7 +1067,7 @@ export function MailPage() {
} }
const sidebarContent = ( const sidebarContent = (
<Sidebar collapsible="none" className="h-full w-full border-r border-border bg-sidebar text-sidebar-foreground"> <Sidebar collapsible="none" className="h-full w-full border-r border-border bg-sidebar text-sidebar-foreground">
<SidebarHeader className={cn("pb-3 pt-4", sidebarCollapsed ? "px-2" : "px-3")}> <SidebarHeader className={cn("pb-2 pt-3", sidebarCollapsed ? "px-2" : "px-3")}>
<AccountHeader <AccountHeader
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
name={me.data?.user.displayName || selectedMailbox?.address || "LanQin"} name={me.data?.user.displayName || selectedMailbox?.address || "LanQin"}
@@ -1063,7 +1078,7 @@ export function MailPage() {
onLanguageChange={setLanguage} onLanguageChange={setLanguage}
onSettings={openSettings} onSettings={openSettings}
/> />
<div className={cn("mt-2 flex gap-2", sidebarCollapsed && "justify-center")}> <div className={cn("mt-2 flex gap-1.5", sidebarCollapsed && "justify-center")}>
<MailboxSwitcher <MailboxSwitcher
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
mailboxes={mailboxList.data?.items || []} mailboxes={mailboxList.data?.items || []}
@@ -1073,13 +1088,13 @@ export function MailPage() {
onSelect={switchMailbox} onSelect={switchMailbox}
/> />
{!sidebarCollapsed && !isAllMailboxSelected && ( {!sidebarCollapsed && !isAllMailboxSelected && (
<Button type="button" variant="outline" size="icon" className="h-9 w-9 shrink-0 rounded-md bg-background shadow-none hover:bg-background" onClick={copyCurrentMailbox} disabled={!selectedMailbox} aria-label="复制邮箱地址"> <Button type="button" variant="outline" size="icon" className="h-8 w-8 shrink-0 rounded-md bg-background shadow-none hover:bg-background" onClick={copyCurrentMailbox} disabled={!selectedMailbox} aria-label="复制邮箱地址">
<Copy className="h-4 w-4" /> <Copy className="h-3.5 w-3.5" />
</Button> </Button>
)} )}
</div> </div>
{canSendMail && ( {canSendMail && (
<Button className={cn("mt-5 h-10 w-full rounded-md text-sm font-semibold shadow-none", sidebarCollapsed && "px-0")} size={sidebarCollapsed ? "icon" : "default"} onClick={() => openCompose()} disabled={!selectedComposeMailbox}> <Button className={cn("mt-4 h-9 w-full rounded-md text-[13px] font-semibold shadow-none", sidebarCollapsed && "px-0")} size={sidebarCollapsed ? "icon" : "default"} onClick={() => openCompose()} disabled={!selectedComposeMailbox}>
<PencilLine className="h-4 w-4" /> <PencilLine className="h-4 w-4" />
{!sidebarCollapsed && <span></span>} {!sidebarCollapsed && <span></span>}
</Button> </Button>
@@ -1108,7 +1123,7 @@ export function MailPage() {
<SidebarMenuButton <SidebarMenuButton
isActive={item.type === "starred" ? mailView === "starred" : item.type === "scheduled" ? mailView === "scheduled" : item.type === "sendQueue" ? mailView === "sendQueue" : mailView === "folder" && folder === item.folderName} isActive={item.type === "starred" ? mailView === "starred" : item.type === "scheduled" ? mailView === "scheduled" : item.type === "sendQueue" ? mailView === "sendQueue" : mailView === "folder" && folder === item.folderName}
className={cn( className={cn(
"h-9 rounded-md px-2 text-sm font-normal", "h-8 rounded-md px-2 text-[13px] font-normal",
sidebarCollapsed && "justify-center px-0", sidebarCollapsed && "justify-center px-0",
item.type === "folder" && item.custom && canOrganizeCurrentMailbox && !sidebarCollapsed && "cursor-grab active:cursor-grabbing", item.type === "folder" && item.custom && canOrganizeCurrentMailbox && !sidebarCollapsed && "cursor-grab active:cursor-grabbing",
item.type === "folder" && item.custom && draggingFolderId === item.folderId && "opacity-50", item.type === "folder" && item.custom && draggingFolderId === item.folderId && "opacity-50",
@@ -1172,7 +1187,7 @@ export function MailPage() {
<SidebarMenuItem key={`${account.id}-${item.name}`}> <SidebarMenuItem key={`${account.id}-${item.name}`}>
<SidebarMenuButton <SidebarMenuButton
isActive={externalFolder === item.name} isActive={externalFolder === item.name}
className="pl-8" className="h-8 pl-8 text-[13px]"
onClick={() => openExternalFolder(account, item.name)} onClick={() => openExternalFolder(account, item.name)}
> >
{folderIcons[item.role.toLowerCase()] || <Inbox className="h-4 w-4" />} {folderIcons[item.role.toLowerCase()] || <Inbox className="h-4 w-4" />}
@@ -1214,7 +1229,7 @@ export function MailPage() {
<SidebarMenuButton <SidebarMenuButton
isActive={mailView === "folder" && folder === item.folderName} isActive={mailView === "folder" && folder === item.folderName}
className={cn( className={cn(
"h-9 rounded-md px-2 text-sm font-normal", "h-8 rounded-md px-2 text-[13px] font-normal",
sidebarCollapsed && "justify-center px-0", sidebarCollapsed && "justify-center px-0",
item.type === "folder" && item.custom && canOrganizeCurrentMailbox && !sidebarCollapsed && "cursor-grab active:cursor-grabbing", item.type === "folder" && item.custom && canOrganizeCurrentMailbox && !sidebarCollapsed && "cursor-grab active:cursor-grabbing",
draggingFolderId === item.folderId && "opacity-50", draggingFolderId === item.folderId && "opacity-50",
@@ -1272,7 +1287,7 @@ export function MailPage() {
<SidebarMenuItem key={label.id}> <SidebarMenuItem key={label.id}>
<SidebarMenuButton <SidebarMenuButton
isActive={!labelEditMode && mailView === "label" && selectedLabelId === label.id} isActive={!labelEditMode && mailView === "label" && selectedLabelId === label.id}
className={cn("h-9 rounded-md px-2 text-sm font-normal", sidebarCollapsed && "justify-center px-0")} className={cn("h-8 rounded-md px-2 text-[13px] font-normal", sidebarCollapsed && "justify-center px-0")}
onClick={() => { if (!labelEditMode) openLabel(label.id) }} onClick={() => { if (!labelEditMode) openLabel(label.id) }}
> >
{sidebarCollapsed ? ( {sidebarCollapsed ? (
@@ -1323,7 +1338,7 @@ export function MailPage() {
<NoMailboxState onOpenSettings={openSettings} /> <NoMailboxState onOpenSettings={openSettings} />
) : mailView === "scheduled" && canScheduleMail ? ( ) : mailView === "scheduled" && canScheduleMail ? (
<ScheduledSendView <ScheduledSendView
compact={isMobile || displayMode === "compact"} compact={compactMailLayout}
items={visibleScheduledItems} items={visibleScheduledItems}
total={scheduledItems.length} total={scheduledItems.length}
loading={scheduledSends.isLoading} loading={scheduledSends.isLoading}
@@ -1335,7 +1350,7 @@ export function MailPage() {
<PermissionEmptyState title="无定时发送权限" description="当前账号不能查看或管理定时发送任务。" onOpenSettings={openSettings} /> <PermissionEmptyState title="无定时发送权限" description="当前账号不能查看或管理定时发送任务。" onOpenSettings={openSettings} />
) : mailView === "sendQueue" && canViewSendQueue ? ( ) : mailView === "sendQueue" && canViewSendQueue ? (
<SendQueueView <SendQueueView
compact={isMobile || displayMode === "compact"} compact={compactMailLayout}
items={visibleSendQueueItems} items={visibleSendQueueItems}
total={sendQueueItems.length} total={sendQueueItems.length}
loading={sendQueue.isLoading} loading={sendQueue.isLoading}
@@ -1358,7 +1373,7 @@ export function MailPage() {
/> />
) : mailView === "sendQueue" ? ( ) : mailView === "sendQueue" ? (
<PermissionEmptyState title="无发送队列权限" description="当前账号不能查看发送队列。" onOpenSettings={openSettings} /> <PermissionEmptyState title="无发送队列权限" description="当前账号不能查看发送队列。" onOpenSettings={openSettings} />
) : isMobile || displayMode === "compact" ? ( ) : compactMailLayout ? (
<CompactMailView <CompactMailView
title={viewTitle} title={viewTitle}
icon={mailView === "label" && selectedLabel ? <Badge variant="outline" className="gap-1.5 rounded-md font-normal"><span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: generateLabelColor(selectedLabel.name).backgroundColor }} />{selectedLabel.name}</Badge> : undefined} icon={mailView === "label" && selectedLabel ? <Badge variant="outline" className="gap-1.5 rounded-md font-normal"><span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: generateLabelColor(selectedLabel.name).backgroundColor }} />{selectedLabel.name}</Badge> : undefined}
@@ -1402,12 +1417,12 @@ export function MailPage() {
/> />
) : ( ) : (
<ResizablePanelGroup direction="horizontal" className="min-h-0 flex-1 bg-background"> <ResizablePanelGroup direction="horizontal" className="min-h-0 flex-1 bg-background">
<ResizablePanel defaultSize={23} minSize={18} maxSize={30}> <ResizablePanel defaultSize={24} minSize={20} maxSize={34} className="min-w-0">
<div className="flex h-full min-h-0 flex-col bg-background"> <div className="flex h-full min-h-0 flex-col bg-background">
<div className="shrink-0 border-b px-4 pb-3 pt-3"> <div className="shrink-0 border-b px-3 pb-2.5 pt-2.5">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
<Input value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={handleSearchKeyDown} placeholder="搜索发件人、主题、内容..." className="h-10 rounded-lg bg-background pl-9 text-sm shadow-none" /> <Input value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={handleSearchKeyDown} placeholder="搜索发件人、主题、内容..." className="h-9 rounded-md bg-background pl-8 text-[13px] shadow-none" />
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Button type="button" variant={advancedSearchActive || advancedSearchOpen ? "secondary" : "outline"} size="sm" className="h-7 rounded-md px-2 text-xs font-normal shadow-none" onClick={toggleAdvancedSearch}> <Button type="button" variant={advancedSearchActive || advancedSearchOpen ? "secondary" : "outline"} size="sm" className="h-7 rounded-md px-2 text-xs font-normal shadow-none" onClick={toggleAdvancedSearch}>
@@ -1431,7 +1446,7 @@ export function MailPage() {
<Button type="button" variant="ghost" className="h-6 rounded-full px-2 text-xs font-normal text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={clearAdvancedSearch}></Button> <Button type="button" variant="ghost" className="h-6 rounded-full px-2 text-xs font-normal text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={clearAdvancedSearch}></Button>
</div> </div>
)} )}
<div className="mt-3 flex min-w-0 items-center gap-2 overflow-hidden text-xs text-muted-foreground"> <div className="mt-2.5 flex min-w-0 items-center gap-1.5 overflow-hidden text-xs text-muted-foreground">
<span className="shrink-0"></span> <span className="shrink-0"></span>
{(["attachments", "starred", "recent7"] as MailFilter[]).map((value) => ( {(["attachments", "starred", "recent7"] as MailFilter[]).map((value) => (
<Button <Button
@@ -1439,7 +1454,7 @@ export function MailPage() {
type="button" type="button"
variant={mailFilter === value ? "secondary" : "outline"} variant={mailFilter === value ? "secondary" : "outline"}
size="sm" size="sm"
className={cn("h-7 shrink-0 rounded-full px-3 text-xs font-normal shadow-none", mailFilter === value && "bg-accent text-accent-foreground")} className={cn("h-7 shrink-0 rounded-full px-2.5 text-xs font-normal shadow-none", mailFilter === value && "bg-accent text-accent-foreground")}
onClick={() => setMailFilter(mailFilter === value ? "all" : value)} onClick={() => setMailFilter(mailFilter === value ? "all" : value)}
> >
{filterLabels[value]} {filterLabels[value]}
@@ -1447,27 +1462,27 @@ export function MailPage() {
))} ))}
</div> </div>
</div> </div>
<div className="flex h-16 shrink-0 items-center justify-between gap-3 border-b px-4"> <div className="flex min-h-14 shrink-0 items-center justify-between gap-2 border-b px-3 py-2">
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-2">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Checkbox aria-label="选择当前页邮件" checked={compactAllSelected ? true : compactSomeSelected ? "indeterminate" : false} onCheckedChange={(value) => toggleCompactSelectAll(value === true)} /> <Checkbox aria-label="选择当前页邮件" checked={compactAllSelected ? true : compactSomeSelected ? "indeterminate" : false} onCheckedChange={(value) => toggleCompactSelectAll(value === true)} />
<ChevronDown className="h-4 w-4 text-muted-foreground" /> <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
</div> </div>
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-2">
<h1 className="shrink-0 text-2xl font-bold leading-none text-foreground">{mailView === "label" && selectedLabel ? selectedLabel.name : viewTitle}</h1> <h1 className="min-w-0 truncate text-xl font-bold leading-none text-foreground">{mailView === "label" && selectedLabel ? selectedLabel.name : viewTitle}</h1>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Button size="icon" variant="ghost" onClick={refreshMail} disabled={refreshing || autoRefreshing} className={cn("h-8 w-8 text-muted-foreground hover:bg-transparent hover:text-foreground", (refreshing || autoRefreshing) && "text-primary")} title={autoRefreshing ? "自动刷新中" : "刷新邮件"}> <Button size="icon" variant="ghost" onClick={refreshMail} disabled={refreshing || autoRefreshing} className={cn("h-7 w-7 text-muted-foreground hover:bg-transparent hover:text-foreground", (refreshing || autoRefreshing) && "text-primary")} title={autoRefreshing ? "自动刷新中" : "刷新邮件"}>
<RefreshCcw className={cn("h-4 w-4", (refreshing || autoRefreshing) && "animate-spin")} /> <RefreshCcw className={cn("h-4 w-4", (refreshing || autoRefreshing) && "animate-spin")} />
</Button> </Button>
{canOrganizeMail && <Button size="icon" variant="ghost" disabled={!activeMailboxId || markAllRead.isPending || unreadCount === 0} onClick={() => markAllRead.mutate(allMessages)} className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:text-foreground" title="全部已读"><Download className="h-4 w-4" /></Button>} {canOrganizeMail && <Button size="icon" variant="ghost" disabled={!activeMailboxId || markAllRead.isPending || unreadCount === 0} onClick={() => markAllRead.mutate(allMessages)} className="h-7 w-7 text-muted-foreground hover:bg-transparent hover:text-foreground" title="全部已读"><Download className="h-4 w-4" /></Button>}
<Button size="icon" variant="ghost" disabled className="h-8 w-8 text-muted-foreground hover:bg-transparent" title="导入"><Upload className="h-4 w-4" /></Button> <Button size="icon" variant="ghost" disabled className="hidden h-7 w-7 text-muted-foreground hover:bg-transparent min-[1180px]:inline-flex" title="导入"><Upload className="h-4 w-4" /></Button>
</div> </div>
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <div className="flex min-w-0 shrink-0 items-center gap-1.5">
{selectedCountOnPage > 0 && canOrganizeMail && <BulkActionMenu pending={bulkPending} onAction={runBulkAction} />} {selectedCountOnPage > 0 && canOrganizeMail && <BulkActionMenu pending={bulkPending} onAction={runBulkAction} />}
<Button type="button" variant={mailFilter === "all" ? "secondary" : "outline"} size="sm" className="h-9 rounded-md px-4 text-sm font-normal shadow-none" onClick={() => setMailFilter("all")}></Button> <Button type="button" variant={mailFilter === "all" ? "secondary" : "outline"} size="sm" className="h-8 rounded-md px-3 text-[13px] font-normal shadow-none" onClick={() => setMailFilter("all")}></Button>
<Button type="button" variant={mailFilter === "unread" ? "secondary" : "outline"} size="sm" className="h-9 rounded-md px-4 text-sm font-normal shadow-none" onClick={() => setMailFilter("unread")}></Button> <Button type="button" variant={mailFilter === "unread" ? "secondary" : "outline"} size="sm" className="h-8 rounded-md px-3 text-[13px] font-normal shadow-none" onClick={() => setMailFilter("unread")}></Button>
</div> </div>
</div> </div>
<ScrollArea className="min-h-0 flex-1"> <ScrollArea className="min-h-0 flex-1">
@@ -1486,7 +1501,7 @@ export function MailPage() {
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel defaultSize={77} minSize={60}> <ResizablePanel defaultSize={76} minSize={50} className="min-w-0">
<section className="h-full min-h-0 bg-background"> <section className="h-full min-h-0 bg-background">
{!selectedId && ( {!selectedId && (
<div className="grid h-full place-items-center text-muted-foreground"> <div className="grid h-full place-items-center text-muted-foreground">
@@ -1532,7 +1547,7 @@ export function MailPage() {
return ( return (
<div className="h-svh overflow-hidden bg-background"> <div className="h-svh overflow-hidden bg-background">
<SidebarProvider className="h-full min-h-0 w-full flex-col"> <SidebarProvider className="h-full min-h-0 w-full min-w-0 flex-col">
{isMobile ? ( {isMobile ? (
<div className="flex h-full min-h-0 flex-col"> <div className="flex h-full min-h-0 flex-col">
{!selectedId && ( {!selectedId && (
@@ -1560,13 +1575,13 @@ export function MailPage() {
<section className="flex min-h-0 flex-1 flex-col">{contentView}</section> <section className="flex min-h-0 flex-1 flex-col">{contentView}</section>
</div> </div>
) : ( ) : (
<ResizablePanelGroup direction="horizontal" className="h-full min-h-0 w-full"> <ResizablePanelGroup direction="horizontal" className="h-full min-h-0 w-full min-w-0">
<ResizablePanel defaultSize={13} minSize={10} maxSize={18}> <ResizablePanel defaultSize={13} minSize={8} maxSize={18} className="min-w-0">
{sidebarContent} {sidebarContent}
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel defaultSize={87} minSize={60}> <ResizablePanel defaultSize={87} minSize={45} className="min-w-0">
<section className="flex h-full min-h-0 flex-col"> <section className="flex h-full min-h-0 min-w-0 flex-col">
{contentView} {contentView}
</section> </section>
</ResizablePanel> </ResizablePanel>
@@ -2236,11 +2251,11 @@ function BulkActionMenu({ pending, onAction }: { pending: boolean; onAction: (ac
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={pending}> <Button variant="outline" size="icon" className="h-8 w-8 rounded-md shadow-none" disabled={pending} title="批量操作" aria-label="批量操作">
<Ellipsis className="h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onSelect={() => onAction("read")}></DropdownMenuItem> <DropdownMenuItem onSelect={() => onAction("read")}></DropdownMenuItem>
<DropdownMenuItem onSelect={() => onAction("unread")}></DropdownMenuItem> <DropdownMenuItem onSelect={() => onAction("unread")}></DropdownMenuItem>
<DropdownMenuItem onSelect={() => onAction("star")}></DropdownMenuItem> <DropdownMenuItem onSelect={() => onAction("star")}></DropdownMenuItem>
@@ -2592,23 +2607,23 @@ function CompactMailView({
} }
return ( return (
<div className="flex min-h-0 flex-1 flex-col bg-background"> <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background">
<div className="flex min-h-12 shrink-0 items-center justify-between gap-2 border-b px-3 sm:px-4"> <div className="flex min-h-11 shrink-0 items-center justify-between gap-2 border-b px-3 sm:px-4">
<div className="flex min-w-0 flex-1 items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-2.5">
<Checkbox aria-label="选择当前页邮件" checked={allSelected ? true : someSelected ? "indeterminate" : false} onCheckedChange={(value) => onSelectAll(value === true)} /> <Checkbox aria-label="选择当前页邮件" checked={allSelected ? true : someSelected ? "indeterminate" : false} onCheckedChange={(value) => onSelectAll(value === true)} />
<div className="flex min-w-0 items-center gap-2 text-base font-semibold"> <div className="flex min-w-0 items-center gap-2 text-[15px] font-semibold">
{icon} {icon}
<span className="truncate">{title}</span> <span className="truncate">{title}</span>
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-1.5">
{selectedIds.length > 0 ? ( {selectedIds.length > 0 ? (
<> <>
<span className="hidden text-sm text-muted-foreground min-[380px]:inline"> {selectedIds.length} </span> <span className="hidden text-xs text-muted-foreground min-[380px]:inline"> {selectedIds.length} </span>
{canOrganize && <BulkActionMenu pending={bulkPending} onAction={onBulkAction} />} {canOrganize && <BulkActionMenu pending={bulkPending} onAction={onBulkAction} />}
</> </>
) : ( ) : (
<div className="text-sm text-muted-foreground">{messages.length} / {total} </div> <div className="text-xs text-muted-foreground">{messages.length} / {total} </div>
)} )}
</div> </div>
</div> </div>
@@ -2900,7 +2915,7 @@ function CompactMessageRow({ message, active, checked, scheduled, onCheckedChang
const hiddenLabelCount = Math.max((message.labels?.length || 0) - visibleLabels.length, 0) const hiddenLabelCount = Math.max((message.labels?.length || 0) - visibleLabels.length, 0)
const senderName = senderDisplayName(message) const senderName = senderDisplayName(message)
return ( return (
<div onClick={onClick} onContextMenu={onContextMenu} className={cn("cursor-pointer border-b px-3 py-3 text-sm transition-colors hover:bg-accent/50 sm:grid sm:grid-cols-[32px_28px_minmax(140px,220px)_minmax(0,1fr)_104px_36px] sm:items-center sm:gap-2 sm:px-4 sm:py-2", active && "bg-accent", !message.isRead && "font-semibold")}> <div onClick={onClick} onContextMenu={onContextMenu} className={cn("cursor-pointer border-b px-3 py-2.5 text-[13px] transition-colors hover:bg-accent/50 sm:grid sm:grid-cols-[28px_24px_minmax(112px,180px)_minmax(0,1fr)_86px_30px] sm:items-center sm:gap-2 sm:px-3 sm:py-2", active && "bg-accent", !message.isRead && "font-semibold")}>
<div className="flex gap-3 sm:contents"> <div className="flex gap-3 sm:contents">
<Checkbox aria-label="选择邮件" checked={checked} onCheckedChange={(value) => onCheckedChange(value === true)} onClick={(event) => event.stopPropagation()} className="mt-0.5 shrink-0 sm:mt-0" /> <Checkbox aria-label="选择邮件" checked={checked} onCheckedChange={(value) => onCheckedChange(value === true)} onClick={(event) => event.stopPropagation()} className="mt-0.5 shrink-0 sm:mt-0" />
{message.isRead ? ( {message.isRead ? (
@@ -2913,8 +2928,8 @@ function CompactMessageRow({ message, active, checked, scheduled, onCheckedChang
<div className="min-w-0 truncate" title={senderTitle(message)}>{senderName}</div> <div className="min-w-0 truncate" title={senderTitle(message)}>{senderName}</div>
<div className="flex shrink-0 items-center gap-1 sm:hidden"> <div className="flex shrink-0 items-center gap-1 sm:hidden">
<span className="text-xs text-muted-foreground">{formatDate(message.receivedAt)}</span> <span className="text-xs text-muted-foreground">{formatDate(message.receivedAt)}</span>
{canOrganize && <Button type="button" variant="ghost" size="icon" aria-label={message.isStarred ? "取消星标" : "添加星标"} className="h-7 w-7 text-muted-foreground hover:text-yellow-500" onClick={(event) => { event.stopPropagation(); onStar() }}> {canOrganize && <Button type="button" variant="ghost" size="icon" aria-label={message.isStarred ? "取消星标" : "添加星标"} className="h-6 w-6 text-muted-foreground hover:text-yellow-500" onClick={(event) => { event.stopPropagation(); onStar() }}>
<Star className={cn("h-4 w-4", message.isStarred && "fill-yellow-400 text-yellow-500")} /> <Star className={cn("h-3.5 w-3.5", message.isStarred && "fill-yellow-400 text-yellow-500")} />
</Button>} </Button>}
</div> </div>
</div> </div>
@@ -2930,8 +2945,8 @@ function CompactMessageRow({ message, active, checked, scheduled, onCheckedChang
</div> </div>
</div> </div>
<div className="hidden shrink-0 text-right text-xs text-muted-foreground sm:block">{formatDate(message.receivedAt)}</div> <div className="hidden shrink-0 text-right text-xs text-muted-foreground sm:block">{formatDate(message.receivedAt)}</div>
{canOrganize && <Button type="button" variant="ghost" size="icon" aria-label={message.isStarred ? "取消星标" : "添加星标"} className="hidden h-7 w-7 text-muted-foreground hover:text-yellow-500 sm:inline-flex" onClick={(event) => { event.stopPropagation(); onStar() }}> {canOrganize && <Button type="button" variant="ghost" size="icon" aria-label={message.isStarred ? "取消星标" : "添加星标"} className="hidden h-6 w-6 text-muted-foreground hover:text-yellow-500 sm:inline-flex" onClick={(event) => { event.stopPropagation(); onStar() }}>
<Star className={cn("h-4 w-4", message.isStarred && "fill-yellow-400 text-yellow-500")} /> <Star className={cn("h-3.5 w-3.5", message.isStarred && "fill-yellow-400 text-yellow-500")} />
</Button>} </Button>}
</div> </div>
) )
@@ -2980,8 +2995,8 @@ function AccountHeader({ collapsed, name, email, darkMode, language, onToggleThe
if (collapsed) { if (collapsed) {
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<Avatar className="size-[32px] rounded-full"> <Avatar className="size-[30px] rounded-full">
<AvatarFallback className="bg-primary text-[13px] font-semibold text-primary-foreground">{accountInitial(displayName, email)}</AvatarFallback> <AvatarFallback className="bg-primary text-xs font-semibold text-primary-foreground">{accountInitial(displayName, email)}</AvatarFallback>
</Avatar> </Avatar>
</div> </div>
) )
@@ -2989,20 +3004,20 @@ function AccountHeader({ collapsed, name, email, darkMode, language, onToggleThe
return ( return (
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<Avatar className="size-[32px] rounded-full"> <Avatar className="size-[30px] rounded-full">
<AvatarFallback className="bg-primary text-[13px] font-semibold text-primary-foreground">{accountInitial(displayName, email)}</AvatarFallback> <AvatarFallback className="bg-primary text-xs font-semibold text-primary-foreground">{accountInitial(displayName, email)}</AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0 text-sm"> <div className="min-w-0 text-[13px]">
<div className="truncate text-sm font-semibold leading-5 text-foreground">{displayName}</div> <div className="truncate text-[13px] font-semibold leading-5 text-foreground">{displayName}</div>
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<Button type="button" variant="ghost" size="icon" className="size-8 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={onToggleTheme}> <Button type="button" variant="ghost" size="icon" className="size-7 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={onToggleTheme}>
{darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} {darkMode ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="ghost" size="icon" className="hidden size-8 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" aria-label="切换语言" title="切换语言"> <Button type="button" variant="ghost" size="icon" className="hidden size-7 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" aria-label="切换语言" title="切换语言">
<span className="text-sm font-medium leading-none">{currentLanguage.shortLabel}</span> <span className="text-sm font-medium leading-none">{currentLanguage.shortLabel}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -3015,8 +3030,8 @@ function AccountHeader({ collapsed, name, email, darkMode, language, onToggleThe
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button type="button" variant="ghost" size="icon" className="size-8 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={onSettings}> <Button type="button" variant="ghost" size="icon" className="size-7 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={onSettings}>
<Settings className="h-4 w-4" /> <Settings className="h-3.5 w-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -3039,17 +3054,17 @@ function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMail
return ( return (
<DropdownMenu onOpenChange={(open) => { if (!open) setMailboxQuery("") }}> <DropdownMenu onOpenChange={(open) => { if (!open) setMailboxQuery("") }}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" className={cn("h-9 min-w-0 flex-1 justify-start gap-2 overflow-hidden rounded-md border-input bg-background px-2 text-left font-normal shadow-none hover:bg-background", collapsed && "w-8 flex-none justify-center px-0")} title={displayAddress}> <Button variant="outline" className={cn("h-8 min-w-0 flex-1 justify-start gap-1.5 overflow-hidden rounded-md border-input bg-background px-2 text-left font-normal shadow-none hover:bg-background", collapsed && "w-8 flex-none justify-center px-0")} title={displayAddress}>
<Mail className="h-4 w-4 shrink-0 text-muted-foreground" /> <Mail className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
{!collapsed && ( {!collapsed && (
<> <>
<span className="min-w-0 flex-1 truncate text-sm font-medium">{displayAddress}</span> <span className="min-w-0 flex-1 truncate text-[13px] font-medium">{displayAddress}</span>
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" /> <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</> </>
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[225px] p-1"> <DropdownMenuContent align="start" className="w-[220px] p-1">
{mailboxes.length > 0 && ( {mailboxes.length > 0 && (
<div className="px-1 pb-1"> <div className="px-1 pb-1">
<Input <Input
@@ -3058,20 +3073,20 @@ function MailboxSwitcher({ collapsed, mailboxes, selectedMailboxId, selectedMail
onChange={(event) => setMailboxQuery(event.target.value)} onChange={(event) => setMailboxQuery(event.target.value)}
onKeyDown={(event) => event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()}
placeholder="搜索邮箱..." placeholder="搜索邮箱..."
className="h-9 rounded-md bg-background px-2 text-sm shadow-none" className="h-8 rounded-md bg-background px-2 text-[13px] shadow-none"
/> />
</div> </div>
)} )}
{mailboxes.length === 0 && <DropdownMenuItem disabled></DropdownMenuItem>} {mailboxes.length === 0 && <DropdownMenuItem disabled></DropdownMenuItem>}
{mailboxes.length > 0 && showAllMailboxOption && ( {mailboxes.length > 0 && showAllMailboxOption && (
<DropdownMenuItem onSelect={() => onSelect("all")} className={cn("h-8 gap-2 rounded-sm px-2 text-sm font-normal", isAllSelected && "bg-accent text-accent-foreground")}> <DropdownMenuItem onSelect={() => onSelect("all")} className={cn("h-8 gap-2 rounded-sm px-2 text-[13px] font-normal", isAllSelected && "bg-accent text-accent-foreground")}>
<Mail className="h-4 w-4 shrink-0 text-muted-foreground" /> <Mail className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate"></span> <span className="min-w-0 flex-1 truncate"></span>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{filteredMailboxes.map((mailbox) => ( {filteredMailboxes.map((mailbox) => (
<DropdownMenuItem key={mailbox.id} onSelect={() => onSelect(mailbox.id)} className={cn("h-8 min-w-0 gap-2 rounded-sm px-2 text-sm font-normal", !isAllSelected && selectedMailbox?.id === mailbox.id && "bg-accent text-accent-foreground")}> <DropdownMenuItem key={mailbox.id} onSelect={() => onSelect(mailbox.id)} className={cn("h-8 min-w-0 gap-2 rounded-sm px-2 text-[13px] font-normal", !isAllSelected && selectedMailbox?.id === mailbox.id && "bg-accent text-accent-foreground")}>
<Mail className="h-4 w-4 shrink-0 text-muted-foreground" /> <Mail className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate" title={mailbox.address}>{mailbox.address}</span> <span className="min-w-0 flex-1 truncate" title={mailbox.address}>{mailbox.address}</span>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
@@ -3301,8 +3316,8 @@ function MessageRow({
const visibleLabels = (message.labels || []).slice(0, 2) const visibleLabels = (message.labels || []).slice(0, 2)
const hiddenLabelCount = Math.max((message.labels?.length || 0) - visibleLabels.length, 0) const hiddenLabelCount = Math.max((message.labels?.length || 0) - visibleLabels.length, 0)
const senderName = senderDisplayName(message) const senderName = senderDisplayName(message)
return <div onClick={onClick} onContextMenu={onContextMenu} className={cn("cursor-pointer border-b px-4 py-3 transition-colors hover:bg-accent/60", active && "bg-accent", !message.isRead && "font-semibold")}> return <div onClick={onClick} onContextMenu={onContextMenu} className={cn("cursor-pointer border-b px-3 py-2.5 transition-colors hover:bg-accent/60", active && "bg-accent", !message.isRead && "font-semibold")}>
<div className="flex gap-3"> <div className="flex gap-2.5">
<Checkbox <Checkbox
aria-label="选择邮件" aria-label="选择邮件"
checked={checked} checked={checked}
@@ -3312,23 +3327,23 @@ function MessageRow({
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="mb-1 flex items-center justify-between gap-2"> <div className="mb-1 flex items-center justify-between gap-2">
<div className="min-w-0 truncate text-sm font-medium" title={senderTitle(message)}>{senderName}</div> <div className="min-w-0 truncate text-[13px] font-medium" title={senderTitle(message)}>{senderName}</div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
{canOrganize && <Button {canOrganize && <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
aria-label={message.isStarred ? "取消星标" : "添加星标"} aria-label={message.isStarred ? "取消星标" : "添加星标"}
className="h-7 w-7 text-muted-foreground hover:text-yellow-500" className="h-6 w-6 text-muted-foreground hover:text-yellow-500"
onClick={(e) => { e.stopPropagation(); onStar() }} onClick={(e) => { e.stopPropagation(); onStar() }}
> >
<Star className={cn("h-4 w-4", message.isStarred && "fill-yellow-400 text-yellow-500")} /> <Star className={cn("h-3.5 w-3.5", message.isStarred && "fill-yellow-400 text-yellow-500")} />
</Button>} </Button>}
<div className="text-xs text-muted-foreground">{formatDate(message.receivedAt)}</div> <div className="text-xs text-muted-foreground">{formatDate(message.receivedAt)}</div>
</div> </div>
</div> </div>
<div className="mb-1 flex min-w-0 items-center gap-2"> <div className="mb-1 flex min-w-0 items-center gap-2">
<span className="min-w-0 truncate text-sm text-foreground">{message.subject || "无主题"}</span> <span className="min-w-0 truncate text-[13px] text-foreground">{message.subject || "无主题"}</span>
{scheduled && <Badge variant="secondary" className="h-5 shrink-0 rounded-md px-1.5 text-[11px] font-normal"></Badge>} {scheduled && <Badge variant="secondary" className="h-5 shrink-0 rounded-md px-1.5 text-[11px] font-normal"></Badge>}
{visibleLabels.map((label) => <MailLabelBadge key={label.id} label={label} />)} {visibleLabels.map((label) => <MailLabelBadge key={label.id} label={label} />)}
{hiddenLabelCount > 0 && <Badge variant="outline" className="h-5 shrink-0 rounded-md px-1.5 text-[11px] font-normal text-muted-foreground">+{hiddenLabelCount}</Badge>} {hiddenLabelCount > 0 && <Badge variant="outline" className="h-5 shrink-0 rounded-md px-1.5 text-[11px] font-normal text-muted-foreground">+{hiddenLabelCount}</Badge>}