Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fc91f2ed9 | |||
| c482e5b896 | |||
| 07e62e9c2d | |||
| 5797b472d9 | |||
| 3caee11e38 | |||
| 2dd3647923 |
@@ -0,0 +1,4 @@
|
|||||||
|
- 写信页发送区改为 Gmail 风格拆分按钮,主按钮直接发送,右侧下拉菜单提供“定时发送”。
|
||||||
|
- 移除容易被误认为日期选择器的独立日历方块,保留原有定时预设和自定义发送时间功能。
|
||||||
|
|
||||||
|
**完整更新日志**:[v1.2.36...v1.2.37](https://github.com/zxyszx/NewSzxcn-Email/compare/v1.2.36...v1.2.37)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- 写信发送拆分按钮统一使用 Gmail 风格蓝色主操作,浅色与暗色模式都保持清晰白字及一致悬停状态。
|
||||||
|
- 修复“定时发送”菜单按左侧展开导致右边框越过写信窗口的问题,菜单改为右对齐并增加边缘避让和垂直间距。
|
||||||
|
- 将默认主操作按钮从通用强调色中拆分,暗色主题下后台、个人设置、规则、弹窗确认、登录注册等页面统一使用深灰底白字;勾选框、进度条、选中态等仍保留清晰的强调色。
|
||||||
|
- 已保存的浅色或暗色主题现在会在应用启动时统一恢复,切换到后台、登录及其他页面后不再丢失主题状态。
|
||||||
|
|
||||||
|
**完整更新日志**:[v1.2.37...v1.2.38](https://github.com/zxyszx/NewSzxcn-Email/compare/v1.2.37...v1.2.38)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- 修复桌面端文件夹或标签较多时侧栏被内容撑出视口、底部项目无法访问的问题;账号、邮箱切换和写信入口保持固定,邮件导航区域独立滚动。
|
||||||
|
- 同步包含 v1.2.38 的暗色主按钮与 Gmail 蓝色发送按钮修复,更新完成后写信发送按钮在浅色和暗色模式下均为蓝底白字。
|
||||||
|
|
||||||
|
**完整更新日志**:[v1.2.38...v1.2.39](https://github.com/zxyszx/NewSzxcn-Email/compare/v1.2.38...v1.2.39)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- 修复桌面端文件夹或标签较多时侧栏被内容撑出视口、底部项目无法访问的问题;账号、邮箱切换和写信入口保持固定,邮件导航区域独立滚动。
|
||||||
|
- 写信发送按钮在浅色和暗色模式下统一为 Gmail 风格蓝底白字,定时发送菜单在桌面和手机端均不会越过写信窗口。
|
||||||
|
- 暗色主题默认主操作按钮改为深灰底白字,并与勾选框、进度条、危险按钮等语义颜色分离;进入后台或登录页后也会保持已选择的主题。
|
||||||
|
- 修复备份后台任务测试结束过早导致发布流程偶发失败的问题。
|
||||||
|
|
||||||
|
**完整更新日志**:[v1.2.39...v1.2.40](https://github.com/zxyszx/NewSzxcn-Email/compare/v1.2.39...v1.2.40)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- 调整写信页定时发送菜单宽度,使其与“发送 + 下拉箭头”组合按钮左右边缘完全对齐,同时保留桌面和手机端边界避让。
|
||||||
|
|
||||||
|
**完整更新日志**:[v1.2.40...v1.2.41](https://github.com/zxyszx/NewSzxcn-Email/compare/v1.2.40...v1.2.41)
|
||||||
@@ -316,6 +316,19 @@ func TestManualBackupReusesSavedPassword(t *testing.T) {
|
|||||||
if err != nil || password != "SharedBackupPassword9" {
|
if err != nil || password != "SharedBackupPassword9" {
|
||||||
t.Fatalf("saved password changed: %q, %v", password, err)
|
t.Fatalf("saved password changed: %q, %v", password, err)
|
||||||
}
|
}
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for {
|
||||||
|
a.backupMu.Lock()
|
||||||
|
status := a.backupJob.Status
|
||||||
|
a.backupMu.Unlock()
|
||||||
|
if status != "running" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatal("manual backup did not finish before timeout")
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublicServerIPValidation(t *testing.T) {
|
func TestPublicServerIPValidation(t *testing.T) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
"bg-[hsl(var(--action-primary))] text-[hsl(var(--action-primary-foreground))] hover:bg-[hsl(var(--action-primary)/0.9)]",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
--popover-foreground: 0 0% 9%;
|
--popover-foreground: 0 0% 9%;
|
||||||
--primary: 0 0% 12%;
|
--primary: 0 0% 12%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--action-primary: 0 0% 12%;
|
||||||
|
--action-primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 96.1%;
|
--secondary: 0 0% 96.1%;
|
||||||
--secondary-foreground: 0 0% 12%;
|
--secondary-foreground: 0 0% 12%;
|
||||||
--muted: 0 0% 96.1%;
|
--muted: 0 0% 96.1%;
|
||||||
@@ -33,6 +35,8 @@
|
|||||||
--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%;
|
||||||
|
--compose-send: 217 89% 43%;
|
||||||
|
--compose-send-hover: 216 94% 32%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { @apply border-border; }
|
* { @apply border-border; }
|
||||||
@@ -61,6 +65,8 @@
|
|||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 240 5.9% 10%;
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
--action-primary: 240 4% 24%;
|
||||||
|
--action-primary-foreground: 0 0% 98%;
|
||||||
--secondary: 240 3.7% 15.9%;
|
--secondary: 240 3.7% 15.9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 240 3.7% 15.9%;
|
--muted: 240 3.7% 15.9%;
|
||||||
@@ -80,10 +86,22 @@
|
|||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
--compose-send: 217 89% 52%;
|
||||||
|
--compose-send-hover: 214 94% 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
.compose-send-button.compose-send-button {
|
||||||
|
background: hsl(var(--compose-send));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-send-button.compose-send-button:hover {
|
||||||
|
background: hsl(var(--compose-send-hover));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.mail-shell-grid {
|
.mail-shell-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--app-sidebar-width, 18rem) minmax(0, 1fr);
|
grid-template-columns: var(--app-sidebar-width, 18rem) minmax(0, 1fr);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|||||||
import { Navigate, RouterProvider, createBrowserRouter } from "react-router-dom"
|
import { Navigate, RouterProvider, createBrowserRouter } from "react-router-dom"
|
||||||
import { Toaster } from "@/components/ui/toaster"
|
import { Toaster } from "@/components/ui/toaster"
|
||||||
import { LanguageDomSync } from "@/lib/language"
|
import { LanguageDomSync } from "@/lib/language"
|
||||||
|
import { applyTheme, getInitialTheme } from "@/lib/theme"
|
||||||
import { ProtectedLayout } from "@/components/protected-layout"
|
import { ProtectedLayout } from "@/components/protected-layout"
|
||||||
import { AdminOnly } from "@/components/admin-only"
|
import { AdminOnly } from "@/components/admin-only"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
@@ -15,6 +16,8 @@ const AdminPage = React.lazy(() => import("@/pages/admin").then((module) => ({ d
|
|||||||
const ProfilePage = React.lazy(() => import("@/pages/profile").then((module) => ({ default: module.ProfilePage })))
|
const ProfilePage = React.lazy(() => import("@/pages/profile").then((module) => ({ default: module.ProfilePage })))
|
||||||
const NotFoundPage = React.lazy(() => import("@/pages/not-found").then((module) => ({ default: module.NotFoundPage })))
|
const NotFoundPage = React.lazy(() => import("@/pages/not-found").then((module) => ({ default: module.NotFoundPage })))
|
||||||
|
|
||||||
|
applyTheme(getInitialTheme())
|
||||||
|
|
||||||
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, staleTime: 10_000 } } })
|
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, staleTime: 10_000 } } })
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: "/login", element: <LoginPage /> },
|
{ path: "/login", element: <LoginPage /> },
|
||||||
|
|||||||
@@ -1307,8 +1307,8 @@ export function MailPage() {
|
|||||||
setAdvancedSearchOpen(false)
|
setAdvancedSearchOpen(false)
|
||||||
}
|
}
|
||||||
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 min-h-0 w-full overflow-hidden border-r border-border bg-sidebar text-sidebar-foreground">
|
||||||
<SidebarHeader className={cn("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}
|
||||||
name={me.data?.user.displayName || selectedMailbox?.address || "NewSzxcn"}
|
name={me.data?.user.displayName || selectedMailbox?.address || "NewSzxcn"}
|
||||||
@@ -1352,7 +1352,7 @@ export function MailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent className="px-1">
|
<SidebarContent className="min-h-0 overflow-x-hidden overflow-y-auto overscroll-contain px-1 pb-4">
|
||||||
{!sidebarCollapsed && publicSettings.isError && <SidebarQueryFailure label="邮箱设置读取失败" onRetry={() => { void publicSettings.refetch() }} />}
|
{!sidebarCollapsed && publicSettings.isError && <SidebarQueryFailure label="邮箱设置读取失败" onRetry={() => { void publicSettings.refetch() }} />}
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
@@ -1884,7 +1884,7 @@ export function MailPage() {
|
|||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="w-[86vw] max-w-80 p-0 [&>button]:hidden" aria-describedby={undefined}>
|
<SheetContent side="left" className="w-[86vw] max-w-80 p-0 [&>button]:hidden" aria-describedby={undefined}>
|
||||||
<SheetTitle className="sr-only">邮箱导航</SheetTitle>
|
<SheetTitle className="sr-only">邮箱导航</SheetTitle>
|
||||||
<div className="h-svh">{sidebarContent}</div>
|
<div className="h-svh min-h-0 overflow-hidden">{sidebarContent}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
<div className="min-w-0 flex-1 text-sm font-semibold">{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: labelDotColor(selectedLabel) }} />{selectedLabel.name}</Badge> : viewTitle}</div>
|
<div className="min-w-0 flex-1 text-sm font-semibold">{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: labelDotColor(selectedLabel) }} />{selectedLabel.name}</Badge> : viewTitle}</div>
|
||||||
@@ -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="min-w-0">
|
<div className="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">
|
||||||
@@ -4284,10 +4284,23 @@ function ComposeDialog({ mailboxes, mailbox, open, draft, limits, canSend, canMa
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex flex-row items-center justify-between gap-3 border-t bg-muted/15 px-4 py-3 sm:px-5">
|
<DialogFooter className="flex flex-row items-center justify-between gap-3 border-t bg-muted/15 px-4 py-3 sm:px-5">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
{canSend && (
|
||||||
{canSend && <Button className="min-h-10 px-4" disabled={send.isPending || !senderMailbox}><Send className="h-4 w-4" />{send.isPending ? "发送中..." : "发送"}</Button>}
|
<div className="flex min-w-0 items-center">
|
||||||
{canSchedule && <Button type="button" variant="outline" size="icon" className="h-10 w-10 shrink-0" title="定时发送" aria-label="定时发送" disabled={send.isPending || scheduleSend.isPending || !senderMailbox} onClick={() => setScheduleDialogOpen(true)}><Calendar className="h-4 w-4" /></Button>}
|
<Button className={cn("compose-send-button min-h-10 px-4", canSchedule && "rounded-r-none")} disabled={send.isPending || !senderMailbox}><Send className="h-4 w-4" />{send.isPending ? "发送中..." : "发送"}</Button>
|
||||||
</div>
|
{canSchedule && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button type="button" size="icon" className="compose-send-button h-10 w-9 shrink-0 rounded-l-none border-l border-white/30 px-0" title="发送选项" aria-label="发送选项" disabled={send.isPending || scheduleSend.isPending || !senderMailbox}>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" side="top" sideOffset={8} collisionPadding={12} className="w-[120px] min-w-[120px]">
|
||||||
|
<DropdownMenuItem className={composerMenuItemClass} onSelect={() => setScheduleDialogOpen(true)}><Clock3 className="h-4 w-4" />定时发送</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button type="button" variant="ghost" className="min-h-10 px-3" disabled={closing} onClick={() => { void closeCompose() }}>{closing ? "保存中..." : "取消"}</Button>
|
<Button type="button" variant="ghost" className="min-h-10 px-3" disabled={closing} onClick={() => { void closeCompose() }}>{closing ? "保存中..." : "取消"}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user