style: 🎨 Beautify dashboard & tunnel list with glassmorphism, background grids, and smoother transitions

This commit is contained in:
chunzhimoe 2026-04-12 17:24:52 +08:00
parent 3f2ecb89c0
commit f20629f8d9
3 changed files with 105 additions and 69 deletions

View File

@ -1,38 +1,53 @@
@import "tailwindcss"; @import "tailwindcss";
/* Tailwind v4: class-based dark mode (set by next-themes on <html>) */
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
/* Design tokens */
:root { :root {
--bg: #ffffff; --bg: #ffffff;
--bg-subtle: #f8fafc; --bg-subtle: #f8fafc;
--bg-card: #f1f5f9; --bg-card: #fdfdfd;
--border: #e2e8f0; --border: #e2e8f0;
--border-subtle: #f1f5f9; --border-subtle: #f1f5f9;
--text: #0f172a; --text: #0f172a;
--text-muted: #64748b; --text-muted: #64748b;
--text-faint: #94a3b8; --text-faint: #94a3b8;
--accent: #10b981; --accent: #10b981;
--accent-dim: rgba(16,185,129,0.12); --accent-dim: rgba(16,185,129,0.08);
--accent-border: rgba(16,185,129,0.3); --accent-border: rgba(16,185,129,0.3);
--accent-glow: rgba(16,185,129,0.2);
} }
.dark { .dark {
--bg: #09090b; --bg: #09090b;
--bg-subtle: #111113; --bg-subtle: #111113;
--bg-card: #18181b; --bg-card: #121214;
--border: #27272a; --border: #27272a;
--border-subtle: #1f1f22; --border-subtle: #1f1f22;
--text: #f4f4f5; --text: #f4f4f5;
--text-muted: #71717a; --text-muted: #a1a1aa;
--text-faint: #3f3f46; --text-faint: #52525b;
--accent: #10b981; --accent: #10b981;
--accent-dim: rgba(16,185,129,0.1); --accent-dim: rgba(16,185,129,0.1);
--accent-border: rgba(16,185,129,0.25); --accent-border: rgba(16,185,129,0.25);
--accent-glow: rgba(16,185,129,0.15);
} }
body { body {
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
} }
@utility bg-grid {
background-size: 32px 32px;
background-image:
linear-gradient(to right, var(--border) 1px, transparent 1px),
linear-gradient(to bottom, var(--border) 1px, transparent 1px);
mask-image: radial-gradient(circle at top center, black, transparent 80%);
-webkit-mask-image: radial-gradient(circle at top center, black, transparent 80%);
}
@utility glass {
background-color: var(--bg-card);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}

View File

@ -68,24 +68,25 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
}, [apps, search, selectedTag]); }, [apps, search, selectedTag]);
return ( return (
<div className="min-h-screen bg-[var(--bg)]"> <div className="min-h-screen bg-[var(--bg)] relative overflow-hidden">
{/* Background accent */} {/* Background accent */}
<div className="fixed inset-0 -z-10 pointer-events-none"> <div className="fixed inset-0 -z-10 pointer-events-none">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--accent-dim),transparent)]" /> <div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,var(--accent-glow),transparent)]" />
<div className="absolute inset-0 bg-grid opacity-30 dark:opacity-10" />
</div> </div>
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12"> <div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 relative z-10">
{/* Header */} {/* Header */}
<header className="mb-10"> <header className="mb-10">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-4 mb-2">
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)]"> <div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-[var(--accent-dim)] border border-[var(--accent-border)] shadow-sm shadow-[var(--accent-glow)]">
<Monitor className="w-5 h-5 text-emerald-500" /> <Monitor className="w-6 h-6 text-emerald-500" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold tracking-tight text-[var(--text)]"> <h1 className="text-3xl font-extrabold tracking-tight text-[var(--text)]">
SSH SSH
</h1> </h1>
<p className="text-sm text-[var(--text-muted)]"> <p className="text-sm font-medium text-[var(--text-muted)] mt-0.5">
Cloudflare Zero Trust · Cloudflare Zero Trust ·
</p> </p>
</div> </div>
@ -93,7 +94,7 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
</header> </header>
{/* Stats row */} {/* Stats row */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-10">
<StatCard <StatCard
icon={<Server className="w-4 h-4" />} icon={<Server className="w-4 h-4" />}
label="主机总数" label="主机总数"
@ -217,17 +218,22 @@ function StatCard({
accent?: boolean; accent?: boolean;
}) { }) {
return ( return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-4"> <div className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white/50 dark:bg-zinc-900/50 backdrop-blur-xl p-5 shadow-sm transition-all hover:shadow-md hover:border-[var(--accent-border)]">
{accent && (
<div className="absolute top-0 right-0 w-24 h-24 bg-[var(--accent-glow)] rounded-full blur-2xl -mr-10 -mt-10 pointer-events-none" />
)}
<div <div
className={`flex items-center gap-1.5 text-xs mb-1 ${ className={`flex items-center gap-2 text-sm font-medium mb-2 ${
accent ? "text-emerald-500" : "text-[var(--text-muted)]" accent ? "text-emerald-500" : "text-[var(--text-muted)]"
}`} }`}
> >
{icon} <div className={`p-1.5 rounded-lg ${accent ? 'bg-emerald-500/10' : 'bg-[var(--bg-subtle)]'}`}>
{icon}
</div>
<span>{label}</span> <span>{label}</span>
</div> </div>
<div <div
className={`text-xl font-bold tracking-tight ${ className={`text-3xl font-extrabold tracking-tight ${
accent ? "text-emerald-500" : "text-[var(--text)]" accent ? "text-emerald-500" : "text-[var(--text)]"
}`} }`}
> >
@ -239,45 +245,49 @@ function StatCard({
function HostCard({ app }: { app: SshAppData }) { function HostCard({ app }: { app: SshAppData }) {
return ( return (
<div className="group relative flex flex-col rounded-xl border border-[var(--border)] bg-[var(--bg-card)] transition-all duration-200 hover:border-[var(--accent-border)] hover:shadow-lg hover:shadow-[var(--accent-dim)] overflow-hidden"> <div className="group relative flex flex-col rounded-2xl border border-[var(--border)] bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:border-emerald-500/50 hover:shadow-xl hover:shadow-[var(--accent-glow)] overflow-hidden">
<a href={`/connect/${app.id}`} className="flex flex-col p-5 flex-1"> <div className="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
<a href={`/connect/${app.id}`} className="flex flex-col p-6 flex-1 relative z-10">
{/* Top row */} {/* Top row */}
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-3.5 min-w-0">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--accent-dim)] border border-[var(--accent-border)] shrink-0"> <div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-400/20 to-emerald-500/10 border border-emerald-500/30 shrink-0 shadow-inner group-hover:scale-105 transition-transform duration-300">
<Terminal className="w-4 h-4 text-emerald-500" /> <Terminal className="w-5 h-5 text-emerald-500" />
</div> </div>
<span className="font-semibold text-[var(--text)] truncate">{app.name}</span> <span className="font-bold text-[var(--text)] text-lg truncate tracking-tight">{app.name}</span>
</div>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-[var(--bg-subtle)] border border-[var(--border)] group-hover:bg-emerald-500 group-hover:border-emerald-400 transition-colors duration-300 shrink-0">
<ChevronRight className="w-4 h-4 text-[var(--text-muted)] group-hover:text-white group-hover:translate-x-0.5 transition-all duration-300" />
</div> </div>
<ChevronRight className="w-4 h-4 text-[var(--text-faint)] shrink-0 transition-all group-hover:text-emerald-500 group-hover:translate-x-0.5" />
</div> </div>
{/* Domain */} {/* Domain */}
<div className="flex items-center gap-1.5 text-sm text-[var(--text-muted)] mb-3 truncate"> <div className="flex items-center gap-2 text-[var(--text-muted)] mb-4 truncate bg-[var(--bg-subtle)] px-3 py-1.5 rounded-lg border border-[var(--border)] group-hover:border-[var(--text-faint)] transition-colors">
<Globe className="w-3.5 h-3.5 shrink-0 text-[var(--text-faint)]" /> <Globe className="w-4 h-4 shrink-0 text-[var(--text-faint)] group-hover:text-emerald-500/70 transition-colors" />
<span className="truncate">{app.domain}</span> <span className="truncate text-sm font-medium">{app.domain}</span>
</div> </div>
{/* Extra domains */} {/* Extra domains */}
{app.domains.length > 1 && ( {app.domains.length > 1 && (
<div className="text-xs text-[var(--text-faint)] mb-3"> <div className="text-xs font-medium text-[var(--text-faint)] mb-4 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-[var(--text-faint)]"></span>
{app.domains.length - 1} {app.domains.length - 1}
</div> </div>
)} )}
{/* Tags */} {/* Tags */}
{app.tags.length > 0 && ( {app.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-3"> <div className="flex flex-wrap gap-2 mb-4 mt-auto">
{app.tags.slice(0, 3).map((tag) => ( {app.tags.slice(0, 3).map((tag) => (
<span <span
key={tag} key={tag}
className="px-2 py-0.5 rounded-md bg-[var(--bg-subtle)] text-[var(--text-muted)] text-xs border border-[var(--border)]" className="px-2.5 py-1 rounded-lg bg-[var(--bg-subtle)] text-[var(--text-muted)] text-[11px] font-semibold tracking-wide uppercase border border-[var(--border)] group-hover:bg-[var(--bg-card)] transition-colors"
> >
{tag} {tag}
</span> </span>
))} ))}
{app.tags.length > 3 && ( {app.tags.length > 3 && (
<span className="text-xs text-[var(--text-faint)]"> <span className="px-2 py-1 text-[11px] font-bold text-[var(--text-faint)]">
+{app.tags.length - 3} +{app.tags.length - 3}
</span> </span>
)} )}
@ -285,20 +295,20 @@ function HostCard({ app }: { app: SshAppData }) {
)} )}
{/* Bottom info */} {/* Bottom info */}
<div className="mt-auto pt-3 border-t border-[var(--border)] flex items-center justify-between text-xs text-[var(--text-faint)]"> <div className="mt-auto pt-4 border-t border-[var(--border)] flex items-center justify-between text-xs font-medium text-[var(--text-faint)]">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5 group-hover:text-[var(--text-muted)] transition-colors">
<Clock className="w-3 h-3" /> <Clock className="w-3.5 h-3.5" />
<span>{timeAgo(app.updatedAt)}</span> <span>{timeAgo(app.updatedAt)}</span>
</div> </div>
{app.sessionDuration && ( {app.sessionDuration && (
<span>TTL {app.sessionDuration}</span> <span className="px-2 py-0.5 rounded-md bg-[var(--bg-subtle)] border border-[var(--border)]">TTL {app.sessionDuration}</span>
)} )}
</div> </div>
</a> </a>
{/* Metrics widget (outside the connect link) */} {/* Metrics widget (outside the connect link) */}
{app.metricsUrl && ( {app.metricsUrl && (
<div className="px-5 pb-5"> <div className="px-6 pb-6 relative z-10">
<MetricsWidget metricsUrl={app.metricsUrl} /> <MetricsWidget metricsUrl={app.metricsUrl} />
</div> </div>
)} )}

View File

@ -56,41 +56,45 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
).length; ).length;
return ( return (
<div className="min-h-[calc(100vh-3.5rem)]"> <div className="min-h-[calc(100vh-3.5rem)] bg-[var(--bg)] relative overflow-hidden">
{/* Dynamic Backgrounds */}
<div className="fixed inset-0 -z-10 pointer-events-none"> <div className="fixed inset-0 -z-10 pointer-events-none">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--accent-dim),transparent)]" /> <div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,var(--accent-glow),transparent)]" />
<div className="absolute inset-0 bg-grid opacity-30 dark:opacity-10" />
</div> </div>
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12"> <div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 relative z-10">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3 mb-8"> <div className="flex items-center gap-4 mb-10">
<div className="w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center"> <div className="w-12 h-12 rounded-2xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center shadow-sm shadow-[var(--accent-glow)]">
<Server className="w-5 h-5 text-emerald-500" /> <Server className="w-6 h-6 text-emerald-500" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-[var(--text)]"></h1> <h1 className="text-3xl font-extrabold tracking-tight text-[var(--text)]"></h1>
<p className="text-sm text-[var(--text-muted)]"> <p className="text-sm font-medium text-[var(--text-muted)] mt-0.5">
{tunnels.length} · {activeCount} 线 <span className="text-[var(--text)] font-bold">{tunnels.length}</span> · <span className="text-emerald-500 font-bold">{activeCount}</span> 线
</p> </p>
</div> </div>
</div> </div>
{/* Search */} {/* Search */}
<div className="relative mb-6"> <div className="relative mb-8">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-faint)]" /> <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-faint)]" />
<input <input
type="text" type="text"
placeholder="按名称或 ID 搜索..." placeholder="按名称或 ID 搜索..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-xl border border-[var(--border)] bg-[var(--bg-subtle)] pl-10 pr-4 py-2.5 text-sm text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition focus:border-[var(--accent-border)] focus:ring-1 focus:ring-[var(--accent-dim)]" className="w-full rounded-2xl border border-[var(--border)] bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl pl-11 pr-4 py-3.5 text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition-all focus:border-[var(--accent-border)] focus:ring-2 focus:ring-[var(--accent-glow)] shadow-sm"
/> />
</div> </div>
{/* Empty */} {/* Empty */}
{filtered.length === 0 && ( {filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-24 text-[var(--text-faint)]"> <div className="flex flex-col items-center justify-center py-24 text-[var(--text-faint)]">
<Server className="w-12 h-12 opacity-30 mb-4" /> <div className="w-16 h-16 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mb-4">
<Server className="w-8 h-8 opacity-30" />
</div>
<p className="text-lg font-medium text-[var(--text-muted)]"> <p className="text-lg font-medium text-[var(--text-muted)]">
</p> </p>
@ -98,29 +102,36 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
)} )}
{/* Tunnel cards */} {/* Tunnel cards */}
<div className="space-y-3"> <div className="space-y-4">
{filtered.map((tunnel) => { {filtered.map((tunnel) => {
const isActive = const isActive =
tunnel.connections && tunnel.connections.length > 0; tunnel.connections && tunnel.connections.length > 0;
return ( return (
<div <div
key={tunnel.id} key={tunnel.id}
className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-5 transition hover:border-[var(--accent-border)]" className="group rounded-2xl border border-[var(--border)] bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl p-5 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg hover:border-emerald-500/30 overflow-hidden relative"
> >
<div className="flex items-start justify-between gap-4"> {isActive && (
<div className="absolute inset-0 bg-gradient-to-r from-emerald-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
)}
<div className="flex items-start justify-between gap-4 relative z-10">
{/* Left */} {/* Left */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-3 mb-1.5">
{isActive ? ( {isActive ? (
<Wifi className="w-4 h-4 text-emerald-400 shrink-0" /> <div className="relative flex items-center justify-center w-5 h-5 shrink-0">
<Wifi className="w-4 h-4 text-emerald-500 relative z-10" />
<span className="absolute inset-0 rounded-full bg-emerald-500/30 animate-ping" />
</div>
) : ( ) : (
<WifiOff className="w-4 h-4 text-zinc-600 shrink-0" /> <WifiOff className="w-4 h-4 text-zinc-500 shrink-0" />
)} )}
<span className="font-semibold text-[var(--text)] truncate"> <span className="font-bold text-[var(--text)] text-lg truncate tracking-tight">
{tunnel.name} {tunnel.name}
</span> </span>
<span <span
className={`px-2 py-0.5 rounded-full text-xs font-medium ${ className={`px-2.5 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider ${
isActive isActive
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"
: "bg-[var(--bg-subtle)] text-[var(--text-faint)] border border-[var(--border)]" : "bg-[var(--bg-subtle)] text-[var(--text-faint)] border border-[var(--border)]"
@ -129,15 +140,15 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
{isActive ? "在线" : "离线"} {isActive ? "在线" : "离线"}
</span> </span>
</div> </div>
<p className="text-xs text-[var(--text-faint)] font-mono truncate"> <p className="text-sm text-[var(--text-muted)] font-mono truncate bg-[var(--bg-subtle)] inline-block px-2 py-0.5 rounded-md border border-[var(--border)]">
{tunnel.id} {tunnel.id}
</p> </p>
</div> </div>
{/* Right meta */} {/* Right meta */}
<div className="text-right text-xs text-[var(--text-faint)] shrink-0"> <div className="text-right text-xs font-medium text-[var(--text-faint)] shrink-0">
<div className="flex items-center gap-1 justify-end"> <div className="flex items-center gap-1.5 justify-end bg-[var(--bg-subtle)] px-2.5 py-1 rounded-lg border border-[var(--border)]">
<Clock className="w-3 h-3" /> <Clock className="w-3.5 h-3.5" />
{timeAgo(tunnel.created_at)} {timeAgo(tunnel.created_at)}
</div> </div>
</div> </div>
@ -145,14 +156,14 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
{/* Connections */} {/* Connections */}
{isActive && tunnel.connections.length > 0 && ( {isActive && tunnel.connections.length > 0 && (
<div className="mt-3 pt-3 border-t border-[var(--border)] flex flex-wrap gap-2"> <div className="mt-4 pt-4 border-t border-[var(--border)] flex flex-wrap gap-2 relative z-10">
{tunnel.connections.map((conn) => ( {tunnel.connections.map((conn) => (
<div <div
key={conn.id} key={conn.id}
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-[var(--bg-subtle)] text-xs text-[var(--text-muted)]" className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white/80 dark:bg-zinc-800/80 shadow-sm border border-[var(--border)] text-xs font-medium text-[var(--text-muted)] group-hover:border-[var(--text-faint)] transition-colors"
> >
<MapPin className="w-3 h-3 text-[var(--text-faint)]" /> <MapPin className="w-3.5 h-3.5 text-emerald-500/70" />
{conn.origin_ip} <span className="text-[var(--text)]">{conn.origin_ip}</span>
<span className="text-[var(--text-faint)]">·</span> <span className="text-[var(--text-faint)]">·</span>
<span className="text-[var(--text-faint)]"> <span className="text-[var(--text-faint)]">
{timeAgo(conn.opened_at)} {timeAgo(conn.opened_at)}