feat: customize NodeSeek-style webmail

This commit is contained in:
zxyszx
2026-08-02 05:55:53 +08:00
parent 47f782a03c
commit 5141917d93
7 changed files with 1978 additions and 343 deletions
+62 -23
View File
@@ -5,39 +5,46 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card-foreground: 222 47% 11%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--popover-foreground: 222 47% 11%;
--primary: 224 44% 12%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--secondary: 213 37% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 213 37% 96%;
--muted-foreground: 216 22% 42%;
--accent: 213 37% 94%;
--accent-foreground: 222 47% 11%;
--destructive: 358 88% 61%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--border: 214 32% 90%;
--input: 214 32% 86%;
--ring: 216 22% 42%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-background: 0 0% 100%;
--sidebar-foreground: 222 47% 11%;
--sidebar-primary: 224 44% 12%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-accent: 213 37% 94%;
--sidebar-accent-foreground: 222 47% 11%;
--sidebar-border: 214 32% 90%;
--sidebar-ring: 216 22% 42%;
}
* { @apply border-border; }
body { @apply bg-background text-foreground antialiased; }
html {
color-scheme: light;
font-size: 15px;
}
body {
@apply bg-background text-foreground antialiased;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
}
html, body, #root { min-height: 100%; }
html { color-scheme: light; }
html.dark { color-scheme: dark; }
.dark {
@@ -71,6 +78,38 @@
}
}
@layer components {
[data-sidebar="content"] {
gap: 0.75rem;
}
[data-sidebar="group"] {
padding: 0 0.25rem;
}
[data-sidebar="menu"] {
gap: 0.25rem;
}
[data-sidebar="menu-button"] {
color: hsl(var(--sidebar-foreground));
}
[data-sidebar="menu-button"] svg {
color: hsl(var(--muted-foreground));
stroke-width: 1.8;
}
[data-sidebar="menu-button"][data-active="true"] {
background: hsl(var(--sidebar-accent));
color: hsl(var(--sidebar-accent-foreground));
}
[data-sidebar="menu-button"][data-active="true"] svg {
color: hsl(var(--muted-foreground));
}
}
html.theme-transition body {
transition: background-color 180ms ease, color 180ms ease;
}
+47 -8
View File
@@ -4,6 +4,44 @@ export * from "./api-types"
const REQUEST_TIMEOUT_MS = 15_000
const MAIL_DELIVERY_TIMEOUT_MS = 60_000
export type MailSearchParams = {
q?: string
from?: string
to?: string
subject?: string
startDate?: string
endDate?: string
attachmentMode?: "all" | "with" | "without"
minSizeKb?: string
maxSizeKb?: string
readStatus?: "all" | "read" | "unread"
flagStatus?: "all" | "starred" | "unstarred"
hasAttachments?: boolean
unread?: boolean
starred?: boolean
}
function appendMailSearchParams(params: URLSearchParams, search: MailSearchParams | string) {
if (typeof search === "string") {
if (search) params.set("q", search)
return
}
if (search.q) params.set("q", search.q)
if (search.from) params.set("from", search.from)
if (search.to) params.set("to", search.to)
if (search.subject) params.set("subject", search.subject)
if (search.startDate) params.set("startDate", search.startDate)
if (search.endDate) params.set("endDate", search.endDate)
if (search.attachmentMode && search.attachmentMode !== "all") params.set("attachmentMode", search.attachmentMode)
else if (search.hasAttachments) params.set("hasAttachments", "1")
if (search.minSizeKb) params.set("minSizeKb", search.minSizeKb)
if (search.maxSizeKb) params.set("maxSizeKb", search.maxSizeKb)
if (search.readStatus && search.readStatus !== "all") params.set("readStatus", search.readStatus)
else if (search.unread) params.set("unread", "1")
if (search.flagStatus && search.flagStatus !== "all") params.set("flagStatus", search.flagStatus)
else if (search.starred) params.set("starred", "1")
}
async function request<T>(path: string, init: RequestInit & { timeoutMs?: number } = {}): Promise<T> {
const { timeoutMs, ...requestInit } = init
const controller = new AbortController()
@@ -152,18 +190,21 @@ export const api = {
return request<MailLabel>(`/api/mail/labels${query}`, { method: "POST", body: JSON.stringify({ name: payload.name, color: payload.color || "" }) })
},
deleteLabel: (id: string, mailboxId?: string) => request<{ labels: MailLabel[] }>(`/api/mail/labels/${id}${mailboxId ? `?mailboxId=${encodeURIComponent(mailboxId)}` : ""}`, { method: "DELETE" }),
messages: (folder: string, q = "", cursor = "", mailboxId?: string) => {
const params = new URLSearchParams({ folder, q, cursor })
messages: (folder: string, search: MailSearchParams | string = "", cursor = "", mailboxId?: string) => {
const params = new URLSearchParams({ folder, cursor })
appendMailSearchParams(params, search)
if (mailboxId) params.set("mailboxId", mailboxId)
return request<ListResponse<MailMessage>>(`/api/mail/messages?${params.toString()}`)
},
labelMessages: (labelId: string, q = "", cursor = "", mailboxId?: string) => {
const params = new URLSearchParams({ labelId, q, cursor })
labelMessages: (labelId: string, search: MailSearchParams | string = "", cursor = "", mailboxId?: string) => {
const params = new URLSearchParams({ labelId, cursor })
appendMailSearchParams(params, search)
if (mailboxId) params.set("mailboxId", mailboxId)
return request<ListResponse<MailMessage>>(`/api/mail/messages?${params.toString()}`)
},
starredMessages: (q = "", cursor = "", mailboxId?: string) => {
const params = new URLSearchParams({ q, cursor })
starredMessages: (search: MailSearchParams | string = "", cursor = "", mailboxId?: string) => {
const params = new URLSearchParams({ cursor })
appendMailSearchParams(params, search)
if (mailboxId) params.set("mailboxId", mailboxId)
return request<ListResponse<MailMessage>>(`/api/mail/starred?${params.toString()}`)
},
@@ -199,5 +240,3 @@ export const api = {
delete: (id: string) => request<{ ok: boolean }>(`/api/mail/messages/${id}`, { method: "DELETE" }),
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff