draft: save post-v1.2.41 UI experiments

This commit is contained in:
zxyszx
2026-08-13 18:12:29 +08:00
parent 7fc91f2ed9
commit 6021e04f0d
9 changed files with 324 additions and 104 deletions
+10
View File
@@ -0,0 +1,10 @@
# Third-party notices
## PurCarte theme background
`apps/web/public/purcarte-moonlit.webp` is distributed with
[komari-theme-purcarte](https://github.com/Montia37/komari-theme-purcarte),
copyright (c) 2025 Montia37, under the MIT License.
The project uses the asset as a replaceable interface background and does not
include the PurCarte application code.
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+7 -9
View File
@@ -21,7 +21,6 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar"
@@ -64,8 +63,8 @@ function ProtectedContent() {
}
return (
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarProvider className="admin-app-shell">
<Sidebar collapsible="none" className="admin-sidebar-panel">
<SidebarHeader className="border-b">
<div className="space-y-1 group-data-[collapsible=icon]:space-y-0">
<SidebarMenu>
@@ -99,7 +98,7 @@ function ProtectedContent() {
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" className="group-data-[collapsible=icon]:!p-0" asChild>
<SidebarMenuButton size="lg" asChild>
<Link to="/profile">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarFallback className="rounded-lg bg-muted text-foreground">
@@ -115,15 +114,14 @@ function ProtectedContent() {
</SidebarMenuItem>
</SidebarMenu>
<div className="p-2">
<Button variant="outline" size="sm" className="w-full gap-2 text-xs" onClick={logout}>
<LogOut className="h-3.5 w-3.5" />退
<Button variant="destructive" size="sm" className="w-full gap-2 text-xs" onClick={logout} aria-label="退出登录" title="退出登录">
<LogOut className="h-3.5 w-3.5" /><span>退</span>
</Button>
</div>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<div className="flex min-h-svh flex-col bg-muted/20">
<SidebarInset className="admin-content-panel">
<div className="flex min-h-0 flex-1 flex-col bg-transparent">
<div className="flex h-12 items-center gap-3 border-b bg-background px-3 md:hidden">
<SidebarTrigger aria-label="打开导航" />
<div className="min-w-0 flex-1 truncate text-sm font-semibold">
+1 -1
View File
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card text-card-foreground p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
+18 -17
View File
@@ -180,21 +180,6 @@ const Sidebar = React.forwardRef<
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
@@ -219,6 +204,22 @@ const Sidebar = React.forwardRef<
)
}
if (collapsible === "none") {
return (
<div
data-sidebar="sidebar"
className={cn(
"hidden min-h-svh w-[--sidebar-width] shrink-0 self-stretch flex-col bg-sidebar text-sidebar-foreground md:flex",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
return (
<div
ref={ref}
@@ -519,7 +520,7 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-[hsl(var(--sidebar-active))] data-[active=true]:font-medium data-[active=true]:text-[hsl(var(--sidebar-active-foreground))] data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -730,7 +731,7 @@ const SidebarMenuSubButton = React.forwardRef<
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
"data-[active=true]:bg-[hsl(var(--sidebar-active))] data-[active=true]:text-[hsl(var(--sidebar-active-foreground))]",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
+250 -31
View File
@@ -35,8 +35,11 @@
--sidebar-accent-foreground: 0 0% 12%;
--sidebar-border: 0 0% 89.8%;
--sidebar-ring: 0 0% 42%;
--sidebar-active: 207 100% 92%;
--sidebar-active-foreground: 202 100% 30%;
--compose-send: 217 89% 43%;
--compose-send-hover: 216 94% 32%;
--mail-selected: 210 100% 96%;
}
* { @apply border-border; }
@@ -57,38 +60,42 @@
html.dark { color-scheme: dark; }
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--action-primary: 240 4% 24%;
--action-primary-foreground: 0 0% 98%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 72%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--background: 220 4% 14%;
--foreground: 0 0% 94%;
--card: 220 4% 16%;
--card-foreground: 0 0% 94%;
--popover: 220 4% 18%;
--popover-foreground: 0 0% 94%;
--primary: 204 100% 55%;
--primary-foreground: 0 0% 100%;
--action-primary: 220 4% 25%;
--action-primary-foreground: 0 0% 94%;
--secondary: 220 4% 21%;
--secondary-foreground: 0 0% 94%;
--muted: 220 4% 21%;
--muted-foreground: 220 3% 72%;
--accent: 220 4% 25%;
--accent-foreground: 0 0% 96%;
--destructive: 359 74% 47%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 0 0% 98%;
--sidebar-primary-foreground: 240 5.9% 10%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--compose-send: 217 89% 52%;
--compose-send-hover: 214 94% 60%;
--border: 0 0% 100% / 0.12;
--input: 0 0% 100% / 0.16;
--ring: 204 100% 55%;
--sidebar-background: 220 4% 19%;
--sidebar-foreground: 0 0% 86%;
--sidebar-primary: 204 100% 55%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 4% 25%;
--sidebar-accent-foreground: 0 0% 96%;
--sidebar-border: 0 0% 100% / 0.1;
--sidebar-ring: 204 100% 55%;
--sidebar-active: 204 100% 29%;
--sidebar-active-foreground: 204 100% 76%;
--compose-send: 204 100% 40%;
--compose-send-hover: 204 100% 47%;
--mail-selected: 220 5% 24%;
}
}
@layer components {
@@ -124,6 +131,167 @@
max-width: 384px;
}
.dark .mail-list-pane {
border-color: hsl(0 0% 100% / 0.16);
}
.dark .mail-sidebar-pane {
border-color: hsl(0 0% 100% / 0.14);
}
@media (min-width: 768px) {
.mail-app-shell,
.settings-app-shell,
.admin-app-shell {
position: relative;
isolation: isolate;
background-color: hsl(var(--background));
background-image: none;
}
.mail-sidebar-wrap,
.mail-list-pane,
.mail-detail-pane,
.settings-sidebar-panel,
.settings-content-panel,
.admin-content-panel {
overflow: hidden;
border: 0;
box-shadow: none;
background-color: rgb(255 255 255) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.mail-sidebar-wrap,
.settings-sidebar-panel {
border-right: 1px solid hsl(var(--border));
}
.admin-sidebar-panel,
.admin-sidebar-panel [data-sidebar="sidebar"] {
border-right: 1px solid hsl(var(--border));
background-color: rgb(255 255 255) !important;
box-shadow: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.mail-sidebar-pane {
background-color: transparent !important;
border: 0;
}
.mail-list-surface,
.mail-detail-surface,
.mail-content-grid,
.compact-mail-surface {
background: transparent !important;
}
.settings-content-panel > main,
.admin-content-panel > div {
background: transparent !important;
}
.settings-glass-content [data-slot="card"],
.settings-glass-content main > div > section.bg-card,
.settings-glass-content main > div > div > section.bg-card {
background-color: rgb(255 255 255) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.dark .mail-app-shell,
.dark .settings-app-shell,
.dark .admin-app-shell {
background-color: hsl(220 4% 13%);
background-image:
linear-gradient(hsl(220 20% 4% / 0.34), hsl(220 20% 4% / 0.34)),
url("/purcarte-moonlit.webp");
}
.dark .mail-sidebar-wrap,
.dark .mail-list-pane,
.dark .mail-detail-pane,
.dark .settings-sidebar-panel,
.dark .settings-content-panel,
.dark .admin-content-panel {
border-color: hsl(0 0% 100% / 0.14);
box-shadow: none;
}
.dark .mail-sidebar-wrap {
background-color: rgb(0 0 0 / 0.5) !important;
}
.dark .mail-sidebar-pane {
background-color: transparent !important;
border: 0;
}
.dark .mail-list-pane {
background-color: rgb(0 0 0 / 0.5) !important;
}
.dark .mail-list-surface {
background: transparent !important;
}
.dark .mail-detail-pane,
.dark .mail-detail-surface {
background-color: rgb(0 0 0 / 0.5) !important;
}
.dark .mail-content-grid {
background: transparent !important;
}
.dark .compact-mail-surface {
background-color: rgb(0 0 0 / 0.5) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.dark .settings-sidebar-panel,
.dark .settings-content-panel,
.dark .admin-content-panel {
background-color: rgb(0 0 0 / 0.5) !important;
}
.dark .admin-sidebar-panel,
.dark .admin-sidebar-panel [data-sidebar="sidebar"] {
background-color: rgb(0 0 0 / 0.5) !important;
}
.dark .settings-glass-content [data-slot="card"],
.dark .settings-glass-content main > div > section.bg-card,
.dark .settings-glass-content main > div > div > section.bg-card {
background-color: rgb(0 0 0 / 0.5) !important;
}
.dark .admin-page [data-slot="card"],
.dark .admin-page .overview-setup-panel {
background-color: rgb(0 0 0 / 0.5) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
}
@media (min-width: 1024px) {
.mail-shell-grid,
.settings-shell-grid {
position: relative;
z-index: 0;
gap: 0;
padding: 0;
}
.mail-content-grid {
gap: 0;
}
}
@media (max-width: 767px) {
.mail-content-grid {
grid-template-columns: minmax(0, 1fr);
@@ -169,7 +337,58 @@
.admin-page [data-slot="card"] {
border-radius: 0.5rem;
box-shadow: none;
border-color: hsl(var(--border));
background-color: rgb(255 255 255) !important;
box-shadow: 0 1px 2px hsl(0 0% 0% / 0.04);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.admin-page .overview-setup-panel {
background-color: rgb(255 255 255) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.overview-stat {
border-right: 1px solid hsl(var(--border));
border-bottom: 1px solid hsl(var(--border));
}
.overview-stat:nth-child(2n) {
border-right: 0;
}
.overview-stat:nth-last-child(-n + 2) {
border-bottom: 0;
}
@media (min-width: 768px) {
.overview-stat:nth-child(2n) {
border-right: 1px solid hsl(var(--border));
}
.overview-stat:nth-child(4n) {
border-right: 0;
}
.overview-stat:nth-last-child(-n + 4) {
border-bottom: 0;
}
}
@media (min-width: 1280px) {
.overview-stat {
border-bottom: 0;
}
.overview-stat:nth-child(4n) {
border-right: 1px solid hsl(var(--border));
}
.overview-stat:last-child {
border-right: 0;
}
}
.admin-page [data-slot="card-header"] {
+22 -30
View File
@@ -2,7 +2,7 @@ import * as React from "react"
import DOMPurify from "dompurify"
import { useSearchParams } from "react-router-dom"
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { ArrowRight, BookOpen, CheckCircle2, ChevronDown, Circle, ClipboardList, Cloud, Copy, Download, ExternalLink, Eye, EyeOff, Github, Globe2, HardDrive, KeyRound, Loader2, Mail, Mailbox, MoreHorizontal, RefreshCcw, Scale, Search, Send, ShieldCheck, Star, Trash2, Users } from "lucide-react"
import { BookOpen, CheckCircle2, ChevronDown, Circle, ClipboardList, Cloud, Copy, Download, ExternalLink, Eye, EyeOff, Github, Globe2, HardDrive, KeyRound, Loader2, Mail, Mailbox, MoreHorizontal, RefreshCcw, Scale, Search, Send, ShieldCheck, Star, Trash2, Users } from "lucide-react"
import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, MaildirSyncHealth, PermissionGroup, PermissionInfo, PermissionLimits, SystemSettings } from "@/lib/api"
import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -134,19 +134,23 @@ export function AdminPage() {
}
return (
<ScrollArea className="h-[calc(100svh-3rem)] md:h-svh">
<main className="admin-page mx-auto w-full max-w-[1180px] px-3 pb-10 pt-3 sm:px-4 sm:pt-4">
<ScrollArea className="h-[calc(100svh-3rem)] md:h-full">
<main className="admin-page mx-auto w-full max-w-[1440px] px-3 pb-8 pt-3 sm:px-4 sm:pt-4">
<AdminPageHeader section={section} refreshing={refreshing} onRefresh={refreshAdminPage} />
{sectionQuery?.isError && <QueryFailure error={sectionQuery.error} onRetry={() => { void sectionQuery.refetch() }} />}
{section === "overview" && canOverview && (
<div className="mb-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<section className="overview-stats mb-3 grid grid-cols-2 overflow-hidden rounded-lg border bg-card md:grid-cols-4 xl:grid-cols-8">
<Stat icon={<Users />} label="账号" value={overview.data?.users || 0} />
<Stat icon={<Globe2 />} label="邮件域名" value={overview.data?.domains || 0} />
<Stat icon={<Mailbox />} label="邮箱" value={overview.data?.mailboxes || 0} />
<Stat icon={<ShieldCheck />} label="存储用量" value={formatBytes(overview.data?.storageBytes || 0)} />
</div>
<Stat label="活跃账号" value={overview.data?.activeUsers || 0} />
<Stat label="活跃邮箱" value={overview.data?.activeMailboxes || 0} />
<Stat label="邮件转发" value={overview.data?.aliases || 0} />
<Stat label="未读邮件" value={overview.data?.unreadMessages || 0} />
</section>
)}
{section === "overview" && <OverviewSection overview={overview.data} domains={domainItems} settings={settings.data} visibleSections={visibleSections} onSectionChange={(next) => setParams(next === "overview" ? {} : { section: next })} />}
@@ -191,44 +195,32 @@ function AdminPageHeader({ section, refreshing, onRefresh }: { section: Section;
function OverviewSection({ overview, domains, settings, visibleSections, onSectionChange }: { overview?: { activeUsers: number; activeMailboxes: number; aliases: number; messages: number; unreadMessages: number }; domains: Domain[]; settings?: SystemSettings; visibleSections: Section[]; onSectionChange: (section: Section) => void }) {
const checklist = setupChecklist(overview, domains, settings).filter((item) => visibleSections.includes(item.section))
return (
<div className="space-y-6">
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_420px]">
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<InfoBox label="活跃账号" value={overview?.activeUsers || 0} />
<InfoBox label="活跃邮箱" value={overview?.activeMailboxes || 0} />
<InfoBox label="邮件转发" value={overview?.aliases || 0} />
<InfoBox label="未读邮件" value={overview?.unreadMessages || 0} />
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent className="space-y-2">
<div className="space-y-3">
<section className="overview-setup-panel rounded-lg border bg-card p-4 text-card-foreground">
<h2 className="mb-3 text-sm font-semibold"></h2>
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-5">
{checklist.map((item) => (
<Button key={item.key} type="button" variant="outline" className="h-auto w-full justify-start gap-3 px-3 py-2 text-left font-normal" onClick={() => onSectionChange(item.section)}>
<Button key={item.key} type="button" variant="outline" className="h-auto min-h-[62px] w-full justify-start gap-2.5 px-3 py-2 text-left font-normal" onClick={() => onSectionChange(item.section)}>
{item.done ? <CheckCircle2 className="h-4 w-4 shrink-0 text-green-600" /> : <Circle className="h-4 w-4 shrink-0 text-muted-foreground" />}
<span className="min-w-0 flex-1">
<span className="block font-medium">{item.title}</span>
<span className="block truncate text-xs text-muted-foreground">{item.detail}</span>
</span>
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button>
))}
</CardContent>
</Card>
</div>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_420px]">
</section>
<div className="grid items-start gap-3 lg:grid-cols-2">
<Card>
<CardHeader><CardTitle>DNS </CardTitle></CardHeader>
<CardContent className="space-y-2">
<CardHeader className="pb-3"><CardTitle>DNS </CardTitle></CardHeader>
<CardContent className="grid gap-2 sm:grid-cols-2">
{domains.map((domain) => <DomainBadgeRow key={domain.id} domain={domain} />)}
{domains.length === 0 && <Empty text="暂无域名" />}
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle></CardTitle></CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<CardHeader className="pb-3"><CardTitle></CardTitle></CardHeader>
<CardContent className="grid gap-2 text-sm text-muted-foreground sm:grid-cols-2">
<InfoLine label="公网地址" value={settings?.publicBaseUrl || "-"} />
<InfoLine label="SMTP" value={settings?.smtpHost ? `${settings.smtpHost}:${settings.smtpPort}` : "-"} />
<InfoLine label="注册" value={settings?.openRegistration ? "已开放" : "关闭"} />
@@ -2223,8 +2215,8 @@ function sendAuditBadgeVariant(event?: string) {
return "secondary"
}
function Stat({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
return <Card><CardContent className="flex items-center gap-3 p-4 sm:gap-4 sm:p-5"><div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-muted text-foreground sm:h-10 sm:w-10">{icon}</div><div className="min-w-0"><div className="truncate text-xl font-semibold tracking-tight sm:text-2xl">{value}</div><div className="text-xs text-muted-foreground">{label}</div></div></CardContent></Card>
function Stat({ icon, label, value }: { icon?: React.ReactNode; label: string; value: React.ReactNode }) {
return <div className="overview-stat flex min-h-[64px] items-center gap-2.5 p-3">{icon && <div className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-muted text-foreground [&>svg]:h-4 [&>svg]:w-4">{icon}</div>}<div className="min-w-0"><div className="truncate text-lg font-semibold leading-6">{value}</div><div className="truncate text-xs text-muted-foreground">{label}</div></div></div>
}
function InfoBox({ label, value }: { label: string; value: React.ReactNode }) { return <div className="rounded-lg border p-4"><div className="text-xl font-semibold tracking-tight sm:text-2xl">{value}</div><div className="text-xs text-muted-foreground">{label}</div></div> }
function Empty({ text }: { text: string }) { return <div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">{text}</div> }
+10 -10
View File
@@ -1307,7 +1307,7 @@ export function MailPage() {
setAdvancedSearchOpen(false)
}
const sidebarContent = (
<Sidebar collapsible="none" className="h-full min-h-0 w-full overflow-hidden border-r border-border bg-sidebar text-sidebar-foreground">
<Sidebar collapsible="none" className="mail-sidebar-pane h-full min-h-0 w-full overflow-hidden border-r border-border bg-sidebar text-sidebar-foreground">
<SidebarHeader className={cn("shrink-0 pb-2 pt-3", sidebarCollapsed ? "px-2" : "px-3")}>
<AccountHeader
collapsed={sidebarCollapsed}
@@ -1739,7 +1739,7 @@ export function MailPage() {
) : (
<div className={cn("mail-content-grid min-h-0 flex-1 bg-background", selectedId && "is-reading")}>
<div className={cn("mail-list-pane min-w-0", selectedId && "max-[767px]:hidden")}>
<div className="flex h-full min-h-0 flex-col bg-background">
<div className="mail-list-surface flex h-full min-h-0 flex-col bg-card">
<div className="shrink-0 border-b px-3 pb-2.5 pt-2.5">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
@@ -1821,7 +1821,7 @@ export function MailPage() {
</div>
<section className={cn("mail-detail-pane min-w-0", !selectedId && "max-[767px]:hidden")}>
<div className="h-full min-h-0 bg-background">
<div className="mail-detail-surface h-full min-h-0 bg-background">
{!selectedId && (
<div className="grid h-full place-items-center text-muted-foreground">
<div className="flex flex-col items-center gap-4 text-center">
@@ -1872,7 +1872,7 @@ export function MailPage() {
)
return (
<div className="h-svh overflow-hidden bg-background">
<div className="mail-app-shell h-svh overflow-hidden bg-background">
<SidebarProvider className="h-full min-h-0 w-full min-w-0 flex-col">
{isMobile ? (
<div className="flex h-full min-h-0 flex-col">
@@ -1900,7 +1900,7 @@ export function MailPage() {
</div>
) : (
<div className="mail-shell-grid h-full min-h-0 w-full min-w-0 overflow-hidden">
<div className="h-full min-h-0 min-w-0 overflow-hidden">
<div className="mail-sidebar-wrap h-full min-h-0 min-w-0 overflow-hidden">
{sidebarContent}
</div>
<section className="flex h-full min-h-0 min-w-0 flex-col">
@@ -3030,7 +3030,7 @@ function CompactMailView({
}
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background">
<div className="compact-mail-surface flex min-h-0 min-w-0 flex-1 flex-col bg-background">
<div className="flex min-h-11 shrink-0 items-center gap-3 border-b px-3 sm:px-4">
<div className={cn("flex min-w-0 items-center gap-2.5", selectedIds.length === 0 && "flex-1")}>
<Checkbox aria-label="选择当前页邮件" checked={allSelected ? true : someSelected ? "indeterminate" : false} onCheckedChange={(value) => onSelectAll(value === true)} />
@@ -3121,7 +3121,7 @@ function CompactMessageDetail({
language: Language
}) {
return (
<div className="flex min-h-0 flex-1 flex-col bg-background">
<div className="compact-mail-surface flex min-h-0 flex-1 flex-col bg-background">
<div className="shrink-0 border-b px-3 py-2 sm:px-4">
<div className="flex min-h-10 items-center gap-2 sm:hidden">
<Button variant="ghost" size="icon" onClick={onBack} aria-label="返回">
@@ -3393,7 +3393,7 @@ function CompactMessageRow({ message, active, checked, scheduled, onCheckedChang
<div onClick={onClick} onContextMenu={onContextMenu} className={cn(
"cursor-pointer border-b border-l-2 px-3 py-2.5 text-[13px] transition-colors 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",
message.isRead ? "border-l-transparent hover:bg-accent/50" : "border-l-primary bg-primary/5 font-semibold hover:bg-primary/10",
active && "bg-accent"
active && "bg-[hsl(var(--mail-selected))]"
)}>
<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" />
@@ -3493,7 +3493,7 @@ function AccountHeader({ collapsed, name, email, darkMode, language, onToggleThe
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button type="button" variant="ghost" size="icon" className="size-7 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" onClick={onToggleTheme} title={darkMode ? "切换到浅色模式" : "切换到深色模式"} aria-label={darkMode ? "切换到浅色模式" : "切换到深色模式"}>
<Button type="button" variant="ghost" size="icon" className={cn("size-7 rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground", darkMode && "text-amber-400 hover:text-amber-300")} onClick={onToggleTheme} title={darkMode ? "切换到浅色模式" : "切换到深色模式"} aria-label={darkMode ? "切换到浅色模式" : "切换到深色模式"}>
{darkMode ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
</Button>
<DropdownMenu>
@@ -3830,7 +3830,7 @@ function MessageRow({
return <div onClick={onClick} onContextMenu={onContextMenu} className={cn(
"group cursor-pointer border-b border-l-2 px-3 py-2.5 transition-colors",
message.isRead ? "border-l-transparent hover:bg-accent/60" : "border-l-primary bg-primary/5 font-semibold hover:bg-primary/10",
active && "bg-accent",
active && "bg-[hsl(var(--mail-selected))]",
checked && "bg-accent/70"
)}>
<div className="flex gap-2.5">
+5 -5
View File
@@ -343,7 +343,7 @@ export function ProfilePage() {
if (me.isError || !user) return <div className="grid h-svh place-items-center text-muted-foreground"></div>
const sidebarContent = (
<div className="flex h-full w-[var(--app-sidebar-width)] shrink-0 flex-col border-r border-border bg-card">
<div className="settings-sidebar-panel flex h-full w-[var(--app-sidebar-width)] shrink-0 flex-col border-r border-border bg-card">
<div className="h-[64px] border-b">
<AccountHeader name={user.displayName || selectedMailbox?.address || "NewSzxcn"} email={user.email || selectedMailbox?.address} darkMode={darkMode} onToggleTheme={() => setDarkMode((v) => !v)} onBack={() => navigate("/")} />
</div>
@@ -397,7 +397,7 @@ export function ProfilePage() {
: undefined
return (
<div className="h-svh overflow-hidden bg-background">
<div className="settings-app-shell h-svh overflow-hidden bg-background">
{isMobile ? (
<div className="flex h-full min-h-0 flex-col">
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-3">
@@ -421,9 +421,9 @@ export function ProfilePage() {
</ScrollArea>
</div>
) : (
<div className="flex h-full min-h-0 w-full">
<div className="settings-shell-grid flex h-full min-h-0 w-full">
{sidebarContent}
<section className="min-w-0 flex-1 overflow-y-auto">
<section className="settings-content-panel settings-glass-content min-w-0 flex-1 overflow-y-auto">
<main className="pb-12">
<SettingsPageHeader title={pageTitle} subtitle={pageSubtitle} action={pageAction} activeTab={tab === "profile" ? accountTab : undefined} onAccountTabChange={setAccountTab} />
<div className={contentFrameClass(tab)}>{renderTab()}</div>
@@ -2867,7 +2867,7 @@ function AccountHeader({ name, email, darkMode, onToggleTheme, onBack }: { name:
<div className="min-w-0 truncate text-sm font-semibold leading-5">{displayName}</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button type="button" variant="ghost" size="icon" className="size-[28px] rounded-md text-muted-foreground" aria-label={darkMode ? "切换浅色模式" : "切换深色模式"} title={darkMode ? "浅色模式" : "深色模式"} onClick={onToggleTheme}>
<Button type="button" variant="ghost" size="icon" className={cn("size-[28px] rounded-md text-muted-foreground", darkMode && "text-amber-400 hover:text-amber-300")} aria-label={darkMode ? "切换浅色模式" : "切换深色模式"} title={darkMode ? "浅色模式" : "深色模式"} onClick={onToggleTheme}>
{darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
<Button type="button" variant="ghost" size="icon" className="size-[28px] rounded-md text-muted-foreground" aria-label="返回邮箱" title="返回邮箱" onClick={onBack}>