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";
/* Tailwind v4: class-based dark mode (set by next-themes on <html>) */
@custom-variant dark (&:where(.dark, .dark *));
/* Design tokens */
:root {
--bg: #ffffff;
--bg-subtle: #f8fafc;
--bg-card: #f1f5f9;
--bg-card: #fdfdfd;
--border: #e2e8f0;
--border-subtle: #f1f5f9;
--text: #0f172a;
--text-muted: #64748b;
--text-faint: #94a3b8;
--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-glow: rgba(16,185,129,0.2);
}
.dark {
--bg: #09090b;
--bg-subtle: #111113;
--bg-card: #18181b;
--bg-card: #121214;
--border: #27272a;
--border-subtle: #1f1f22;
--text: #f4f4f5;
--text-muted: #71717a;
--text-faint: #3f3f46;
--text-muted: #a1a1aa;
--text-faint: #52525b;
--accent: #10b981;
--accent-dim: rgba(16,185,129,0.1);
--accent-border: rgba(16,185,129,0.25);
--accent-glow: rgba(16,185,129,0.15);
}
body {
background: var(--bg);
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]);
return (
<div className="min-h-screen bg-[var(--bg)]">
<div className="min-h-screen bg-[var(--bg)] relative overflow-hidden">
{/* Background accent */}
<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 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 className="mb-10">
<div className="flex items-center gap-3 mb-1">
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)]">
<Monitor className="w-5 h-5 text-emerald-500" />
<div className="flex items-center gap-4 mb-2">
<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-6 h-6 text-emerald-500" />
</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
</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 ·
</p>
</div>
@ -93,7 +94,7 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
</header>
{/* 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
icon={<Server className="w-4 h-4" />}
label="主机总数"
@ -217,17 +218,22 @@ function StatCard({
accent?: boolean;
}) {
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
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)]"
}`}
>
<div className={`p-1.5 rounded-lg ${accent ? 'bg-emerald-500/10' : 'bg-[var(--bg-subtle)]'}`}>
{icon}
</div>
<span>{label}</span>
</div>
<div
className={`text-xl font-bold tracking-tight ${
className={`text-3xl font-extrabold tracking-tight ${
accent ? "text-emerald-500" : "text-[var(--text)]"
}`}
>
@ -239,45 +245,49 @@ function StatCard({
function HostCard({ app }: { app: SshAppData }) {
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">
<a href={`/connect/${app.id}`} className="flex flex-col p-5 flex-1">
<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">
<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 */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2.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">
<Terminal className="w-4 h-4 text-emerald-500" />
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3.5 min-w-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-5 h-5 text-emerald-500" />
</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>
<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>
{/* Domain */}
<div className="flex items-center gap-1.5 text-sm text-[var(--text-muted)] mb-3 truncate">
<Globe className="w-3.5 h-3.5 shrink-0 text-[var(--text-faint)]" />
<span className="truncate">{app.domain}</span>
<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-4 h-4 shrink-0 text-[var(--text-faint)] group-hover:text-emerald-500/70 transition-colors" />
<span className="truncate text-sm font-medium">{app.domain}</span>
</div>
{/* Extra domains */}
{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}
</div>
)}
{/* Tags */}
{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) => (
<span
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}
</span>
))}
{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}
</span>
)}
@ -285,20 +295,20 @@ function HostCard({ app }: { app: SshAppData }) {
)}
{/* 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="flex items-center gap-1">
<Clock className="w-3 h-3" />
<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.5 group-hover:text-[var(--text-muted)] transition-colors">
<Clock className="w-3.5 h-3.5" />
<span>{timeAgo(app.updatedAt)}</span>
</div>
{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>
</a>
{/* Metrics widget (outside the connect link) */}
{app.metricsUrl && (
<div className="px-5 pb-5">
<div className="px-6 pb-6 relative z-10">
<MetricsWidget metricsUrl={app.metricsUrl} />
</div>
)}

View File

@ -56,41 +56,45 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
).length;
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="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 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 */}
<div className="flex items-center gap-3 mb-8">
<div className="w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center">
<Server className="w-5 h-5 text-emerald-500" />
<div className="flex items-center gap-4 mb-10">
<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-6 h-6 text-emerald-500" />
</div>
<div>
<h1 className="text-2xl font-bold text-[var(--text)]"></h1>
<p className="text-sm text-[var(--text-muted)]">
{tunnels.length} · {activeCount} 线
<h1 className="text-3xl font-extrabold tracking-tight text-[var(--text)]"></h1>
<p className="text-sm font-medium text-[var(--text-muted)] mt-0.5">
<span className="text-[var(--text)] font-bold">{tunnels.length}</span> · <span className="text-emerald-500 font-bold">{activeCount}</span> 线
</p>
</div>
</div>
{/* Search */}
<div className="relative mb-6">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-faint)]" />
<div className="relative mb-8">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-faint)]" />
<input
type="text"
placeholder="按名称或 ID 搜索..."
value={search}
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>
{/* Empty */}
{filtered.length === 0 && (
<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>
@ -98,29 +102,36 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
)}
{/* Tunnel cards */}
<div className="space-y-3">
<div className="space-y-4">
{filtered.map((tunnel) => {
const isActive =
tunnel.connections && tunnel.connections.length > 0;
return (
<div
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 */}
<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 ? (
<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}
</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
? "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)]"
@ -129,15 +140,15 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
{isActive ? "在线" : "离线"}
</span>
</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}
</p>
</div>
{/* Right meta */}
<div className="text-right text-xs text-[var(--text-faint)] shrink-0">
<div className="flex items-center gap-1 justify-end">
<Clock className="w-3 h-3" />
<div className="text-right text-xs font-medium text-[var(--text-faint)] shrink-0">
<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.5 h-3.5" />
{timeAgo(tunnel.created_at)}
</div>
</div>
@ -145,14 +156,14 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
{/* Connections */}
{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) => (
<div
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)]" />
{conn.origin_ip}
<MapPin className="w-3.5 h-3.5 text-emerald-500/70" />
<span className="text-[var(--text)]">{conn.origin_ip}</span>
<span className="text-[var(--text-faint)]">·</span>
<span className="text-[var(--text-faint)]">
{timeAgo(conn.opened_at)}