draft: save post-v1.2.41 UI experiments
This commit is contained in:
@@ -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 |
@@ -21,7 +21,6 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarRail,
|
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
@@ -64,8 +63,8 @@ function ProtectedContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider className="admin-app-shell">
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="none" className="admin-sidebar-panel">
|
||||||
<SidebarHeader className="border-b">
|
<SidebarHeader className="border-b">
|
||||||
<div className="space-y-1 group-data-[collapsible=icon]:space-y-0">
|
<div className="space-y-1 group-data-[collapsible=icon]:space-y-0">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
@@ -99,7 +98,7 @@ function ProtectedContent() {
|
|||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton size="lg" className="group-data-[collapsible=icon]:!p-0" asChild>
|
<SidebarMenuButton size="lg" asChild>
|
||||||
<Link to="/profile">
|
<Link to="/profile">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarFallback className="rounded-lg bg-muted text-foreground">
|
<AvatarFallback className="rounded-lg bg-muted text-foreground">
|
||||||
@@ -115,15 +114,14 @@ function ProtectedContent() {
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Button variant="outline" size="sm" className="w-full gap-2 text-xs" onClick={logout}>
|
<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" />退出登录
|
<LogOut className="h-3.5 w-3.5" /><span>退出登录</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<SidebarInset>
|
<SidebarInset className="admin-content-panel">
|
||||||
<div className="flex min-h-svh flex-col bg-muted/20">
|
<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">
|
<div className="flex h-12 items-center gap-3 border-b bg-background px-3 md:hidden">
|
||||||
<SidebarTrigger aria-label="打开导航" />
|
<SidebarTrigger aria-label="打开导航" />
|
||||||
<div className="min-w-0 flex-1 truncate text-sm font-semibold">
|
<div className="min-w-0 flex-1 truncate text-sm font-semibold">
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -180,21 +180,6 @@ const Sidebar = React.forwardRef<
|
|||||||
) => {
|
) => {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
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) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
<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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -519,7 +520,7 @@ const SidebarMenuItem = React.forwardRef<
|
|||||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -730,7 +731,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
|||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
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",
|
"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 === "sm" && "text-xs",
|
||||||
size === "md" && "text-sm",
|
size === "md" && "text-sm",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
|||||||
+250
-31
@@ -35,8 +35,11 @@
|
|||||||
--sidebar-accent-foreground: 0 0% 12%;
|
--sidebar-accent-foreground: 0 0% 12%;
|
||||||
--sidebar-border: 0 0% 89.8%;
|
--sidebar-border: 0 0% 89.8%;
|
||||||
--sidebar-ring: 0 0% 42%;
|
--sidebar-ring: 0 0% 42%;
|
||||||
|
--sidebar-active: 207 100% 92%;
|
||||||
|
--sidebar-active-foreground: 202 100% 30%;
|
||||||
--compose-send: 217 89% 43%;
|
--compose-send: 217 89% 43%;
|
||||||
--compose-send-hover: 216 94% 32%;
|
--compose-send-hover: 216 94% 32%;
|
||||||
|
--mail-selected: 210 100% 96%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { @apply border-border; }
|
* { @apply border-border; }
|
||||||
@@ -57,38 +60,42 @@
|
|||||||
html.dark { color-scheme: dark; }
|
html.dark { color-scheme: dark; }
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 240 10% 3.9%;
|
--background: 220 4% 14%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 94%;
|
||||||
--card: 240 10% 3.9%;
|
--card: 220 4% 16%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 94%;
|
||||||
--popover: 240 10% 3.9%;
|
--popover: 220 4% 18%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 94%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 204 100% 55%;
|
||||||
--primary-foreground: 240 5.9% 10%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--action-primary: 240 4% 24%;
|
--action-primary: 220 4% 25%;
|
||||||
--action-primary-foreground: 0 0% 98%;
|
--action-primary-foreground: 0 0% 94%;
|
||||||
--secondary: 240 3.7% 15.9%;
|
--secondary: 220 4% 21%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 94%;
|
||||||
--muted: 240 3.7% 15.9%;
|
--muted: 220 4% 21%;
|
||||||
--muted-foreground: 240 5% 72%;
|
--muted-foreground: 220 3% 72%;
|
||||||
--accent: 240 3.7% 15.9%;
|
--accent: 220 4% 25%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 96%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 359 74% 47%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 240 3.7% 15.9%;
|
--border: 0 0% 100% / 0.12;
|
||||||
--input: 240 3.7% 15.9%;
|
--input: 0 0% 100% / 0.16;
|
||||||
--ring: 240 4.9% 83.9%;
|
--ring: 204 100% 55%;
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--sidebar-background: 220 4% 19%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--sidebar-foreground: 0 0% 86%;
|
||||||
--sidebar-primary: 0 0% 98%;
|
--sidebar-primary: 204 100% 55%;
|
||||||
--sidebar-primary-foreground: 240 5.9% 10%;
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--sidebar-accent: 220 4% 25%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 0 0% 96%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 0 0% 100% / 0.1;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 204 100% 55%;
|
||||||
--compose-send: 217 89% 52%;
|
--sidebar-active: 204 100% 29%;
|
||||||
--compose-send-hover: 214 94% 60%;
|
--sidebar-active-foreground: 204 100% 76%;
|
||||||
|
--compose-send: 204 100% 40%;
|
||||||
|
--compose-send-hover: 204 100% 47%;
|
||||||
|
--mail-selected: 220 5% 24%;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -124,6 +131,167 @@
|
|||||||
max-width: 384px;
|
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) {
|
@media (max-width: 767px) {
|
||||||
.mail-content-grid {
|
.mail-content-grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
@@ -169,7 +337,58 @@
|
|||||||
|
|
||||||
.admin-page [data-slot="card"] {
|
.admin-page [data-slot="card"] {
|
||||||
border-radius: 0.5rem;
|
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"] {
|
.admin-page [data-slot="card-header"] {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from "react"
|
|||||||
import DOMPurify from "dompurify"
|
import DOMPurify from "dompurify"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
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 { 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 { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -134,19 +134,23 @@ export function AdminPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-[calc(100svh-3rem)] md:h-svh">
|
<ScrollArea className="h-[calc(100svh-3rem)] md:h-full">
|
||||||
<main className="admin-page mx-auto w-full max-w-[1180px] px-3 pb-10 pt-3 sm:px-4 sm:pt-4">
|
<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} />
|
<AdminPageHeader section={section} refreshing={refreshing} onRefresh={refreshAdminPage} />
|
||||||
|
|
||||||
{sectionQuery?.isError && <QueryFailure error={sectionQuery.error} onRetry={() => { void sectionQuery.refetch() }} />}
|
{sectionQuery?.isError && <QueryFailure error={sectionQuery.error} onRetry={() => { void sectionQuery.refetch() }} />}
|
||||||
|
|
||||||
{section === "overview" && canOverview && (
|
{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={<Users />} label="账号" value={overview.data?.users || 0} />
|
||||||
<Stat icon={<Globe2 />} label="邮件域名" value={overview.data?.domains || 0} />
|
<Stat icon={<Globe2 />} label="邮件域名" value={overview.data?.domains || 0} />
|
||||||
<Stat icon={<Mailbox />} label="邮箱" value={overview.data?.mailboxes || 0} />
|
<Stat icon={<Mailbox />} label="邮箱" value={overview.data?.mailboxes || 0} />
|
||||||
<Stat icon={<ShieldCheck />} label="存储用量" value={formatBytes(overview.data?.storageBytes || 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 })} />}
|
{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 }) {
|
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))
|
const checklist = setupChecklist(overview, domains, settings).filter((item) => visibleSections.includes(item.section))
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-3">
|
||||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_420px]">
|
<section className="overview-setup-panel rounded-lg border bg-card p-4 text-card-foreground">
|
||||||
<Card>
|
<h2 className="mb-3 text-sm font-semibold">首次配置</h2>
|
||||||
<CardHeader><CardTitle>系统状态</CardTitle></CardHeader>
|
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-5">
|
||||||
<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">
|
|
||||||
{checklist.map((item) => (
|
{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" />}
|
{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="min-w-0 flex-1">
|
||||||
<span className="block font-medium">{item.title}</span>
|
<span className="block font-medium">{item.title}</span>
|
||||||
<span className="block truncate text-xs text-muted-foreground">{item.detail}</span>
|
<span className="block truncate text-xs text-muted-foreground">{item.detail}</span>
|
||||||
</span>
|
</span>
|
||||||
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</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>
|
<Card>
|
||||||
<CardHeader><CardTitle>DNS 状态</CardTitle></CardHeader>
|
<CardHeader className="pb-3"><CardTitle>DNS 状态</CardTitle></CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="grid gap-2 sm:grid-cols-2">
|
||||||
{domains.map((domain) => <DomainBadgeRow key={domain.id} domain={domain} />)}
|
{domains.map((domain) => <DomainBadgeRow key={domain.id} domain={domain} />)}
|
||||||
{domains.length === 0 && <Empty text="暂无域名" />}
|
{domains.length === 0 && <Empty text="暂无域名" />}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>运行提示</CardTitle></CardHeader>
|
<CardHeader className="pb-3"><CardTitle>运行信息</CardTitle></CardHeader>
|
||||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
<CardContent className="grid gap-2 text-sm text-muted-foreground sm:grid-cols-2">
|
||||||
<InfoLine label="公网地址" value={settings?.publicBaseUrl || "-"} />
|
<InfoLine label="公网地址" value={settings?.publicBaseUrl || "-"} />
|
||||||
<InfoLine label="SMTP" value={settings?.smtpHost ? `${settings.smtpHost}:${settings.smtpPort}` : "-"} />
|
<InfoLine label="SMTP" value={settings?.smtpHost ? `${settings.smtpHost}:${settings.smtpPort}` : "-"} />
|
||||||
<InfoLine label="注册" value={settings?.openRegistration ? "已开放" : "关闭"} />
|
<InfoLine label="注册" value={settings?.openRegistration ? "已开放" : "关闭"} />
|
||||||
@@ -2223,8 +2215,8 @@ function sendAuditBadgeVariant(event?: string) {
|
|||||||
return "secondary"
|
return "secondary"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Stat({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
|
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>
|
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 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> }
|
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
@@ -1307,7 +1307,7 @@ export function MailPage() {
|
|||||||
setAdvancedSearchOpen(false)
|
setAdvancedSearchOpen(false)
|
||||||
}
|
}
|
||||||
const sidebarContent = (
|
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")}>
|
<SidebarHeader className={cn("shrink-0 pb-2 pt-3", sidebarCollapsed ? "px-2" : "px-3")}>
|
||||||
<AccountHeader
|
<AccountHeader
|
||||||
collapsed={sidebarCollapsed}
|
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-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={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="shrink-0 border-b px-3 pb-2.5 pt-2.5">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<section className={cn("mail-detail-pane min-w-0", !selectedId && "max-[767px]:hidden")}>
|
<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 && (
|
{!selectedId && (
|
||||||
<div className="grid h-full place-items-center text-muted-foreground">
|
<div className="grid h-full place-items-center text-muted-foreground">
|
||||||
<div className="flex flex-col items-center gap-4 text-center">
|
<div className="flex flex-col items-center gap-4 text-center">
|
||||||
@@ -1872,7 +1872,7 @@ export function MailPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
@@ -1900,7 +1900,7 @@ export function MailPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mail-shell-grid h-full min-h-0 w-full min-w-0 overflow-hidden">
|
<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}
|
{sidebarContent}
|
||||||
</div>
|
</div>
|
||||||
<section className="flex h-full min-h-0 min-w-0 flex-col">
|
<section className="flex h-full min-h-0 min-w-0 flex-col">
|
||||||
@@ -3030,7 +3030,7 @@ function CompactMailView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="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")}>
|
<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)} />
|
<Checkbox aria-label="选择当前页邮件" checked={allSelected ? true : someSelected ? "indeterminate" : false} onCheckedChange={(value) => onSelectAll(value === true)} />
|
||||||
@@ -3121,7 +3121,7 @@ function CompactMessageDetail({
|
|||||||
language: Language
|
language: Language
|
||||||
}) {
|
}) {
|
||||||
return (
|
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="shrink-0 border-b px-3 py-2 sm:px-4">
|
||||||
<div className="flex min-h-10 items-center gap-2 sm:hidden">
|
<div className="flex min-h-10 items-center gap-2 sm:hidden">
|
||||||
<Button variant="ghost" size="icon" onClick={onBack} aria-label="返回">
|
<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(
|
<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",
|
"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",
|
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">
|
<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" />
|
||||||
@@ -3493,7 +3493,7 @@ function AccountHeader({ collapsed, name, email, darkMode, language, onToggleThe
|
|||||||
</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-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" />}
|
{darkMode ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -3830,7 +3830,7 @@ function MessageRow({
|
|||||||
return <div onClick={onClick} onContextMenu={onContextMenu} className={cn(
|
return <div onClick={onClick} onContextMenu={onContextMenu} className={cn(
|
||||||
"group cursor-pointer border-b border-l-2 px-3 py-2.5 transition-colors",
|
"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",
|
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"
|
checked && "bg-accent/70"
|
||||||
)}>
|
)}>
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ export function ProfilePage() {
|
|||||||
if (me.isError || !user) return <div className="grid h-svh place-items-center text-muted-foreground">登录状态已失效</div>
|
if (me.isError || !user) return <div className="grid h-svh place-items-center text-muted-foreground">登录状态已失效</div>
|
||||||
|
|
||||||
const sidebarContent = (
|
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">
|
<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("/")} />
|
<AccountHeader name={user.displayName || selectedMailbox?.address || "NewSzxcn"} email={user.email || selectedMailbox?.address} darkMode={darkMode} onToggleTheme={() => setDarkMode((v) => !v)} onBack={() => navigate("/")} />
|
||||||
</div>
|
</div>
|
||||||
@@ -397,7 +397,7 @@ export function ProfilePage() {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-svh overflow-hidden bg-background">
|
<div className="settings-app-shell h-svh overflow-hidden bg-background">
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<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">
|
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-3">
|
||||||
@@ -421,9 +421,9 @@ export function ProfilePage() {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</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}
|
{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">
|
<main className="pb-12">
|
||||||
<SettingsPageHeader title={pageTitle} subtitle={pageSubtitle} action={pageAction} activeTab={tab === "profile" ? accountTab : undefined} onAccountTabChange={setAccountTab} />
|
<SettingsPageHeader title={pageTitle} subtitle={pageSubtitle} action={pageAction} activeTab={tab === "profile" ? accountTab : undefined} onAccountTabChange={setAccountTab} />
|
||||||
<div className={contentFrameClass(tab)}>{renderTab()}</div>
|
<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 className="min-w-0 truncate text-sm font-semibold leading-5">{displayName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<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" />}
|
{darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="ghost" size="icon" className="size-[28px] rounded-md text-muted-foreground" aria-label="返回邮箱" title="返回邮箱" onClick={onBack}>
|
<Button type="button" variant="ghost" size="icon" className="size-[28px] rounded-md text-muted-foreground" aria-label="返回邮箱" title="返回邮箱" onClick={onBack}>
|
||||||
|
|||||||
Reference in New Issue
Block a user