Dashboard UI overhaul: modern design with system stats, SVG icons, Inter font, theme toggle, loading states, and log modal

This commit is contained in:
Gan, Jimmy
2026-02-19 02:29:25 +08:00
parent ae1b88732d
commit f3db7d3654
13 changed files with 854 additions and 188 deletions
@@ -1,42 +1,106 @@
<script>
let { page = $bindable("dashboard") } = $props();
let { page = $bindable("dashboard"), dark = false, toggleTheme = () => {} } = $props();
const links = [
{ id: "dashboard", label: "Dashboard", icon: "&#9632;" },
{ id: "docker", label: "Docker", icon: "&#9654;" },
{ id: "gitea", label: "Gitea", icon: "&#128193;" },
{ id: "files", label: "Files", icon: "&#128196;" },
{ id: "terminal", label: "Terminal", icon: "&#62;" },
{ id: "dashboard", label: "Overview", icon: "grid" },
{ id: "docker", label: "Docker", icon: "box" },
{ id: "gitea", label: "Repos", icon: "git" },
{ id: "files", label: "Files", icon: "folder" },
{ id: "terminal", label: "Terminal", icon: "terminal" },
];
const external = [
{ label: "Navidrome", href: "//:4533", port: 4533 },
{ label: "Immich", href: "//:2283", port: 2283 },
{ label: "Emby", href: "//:8096", port: 8096 },
{ label: "Gitea Web", href: "//:3300", port: 3300 },
{ label: "Navidrome", port: 4533, icon: "music" },
{ label: "Immich", port: 2283, icon: "image" },
{ label: "Emby", port: 8096, icon: "play" },
{ label: "Gitea Web", port: 3300, icon: "git" },
];
function extHref(port) {
return `${location.protocol}//${location.hostname}:${port}`;
}
</script>
<aside class="w-56 bg-gray-50 border-r border-gray-200 h-screen flex flex-col p-4 shrink-0">
<h1 class="text-lg font-bold mb-6">NAS Dashboard</h1>
<nav class="flex flex-col gap-1">
<aside class="w-[240px] h-screen flex flex-col shrink-0 border-r border-surface-200 bg-white transition-colors duration-200 {dark ? 'bg-surface-800 border-surface-700' : ''}">
<!-- Logo -->
<div class="px-5 py-5 flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-md">
<svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /></svg>
</div>
<div>
<h1 class="text-sm font-bold tracking-tight {dark ? 'text-white' : 'text-surface-900'}">NAS Dashboard</h1>
<p class="text-[10px] text-surface-400 font-medium">DS224+ · Synology</p>
</div>
</div>
<!-- Nav Links -->
<nav class="flex-1 px-3 mt-2">
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2">Main</p>
{#each links as link}
<button
class="text-left px-3 py-2 rounded text-sm {page === link.id ? 'bg-blue-100 text-blue-700 font-medium' : 'hover:bg-gray-100 text-gray-700'}"
class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150
{page === link.id
? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '')
: 'text-surface-500 hover:bg-surface-100 hover:text-surface-700 ' + (dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : '')
}"
onclick={() => page = link.id}
>
<span class="mr-2">{@html link.icon}</span>{link.label}
<span class="w-5 h-5 flex items-center justify-center">
{#if link.icon === "grid"}
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
{:else if link.icon === "box"}
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
{:else if link.icon === "git"}
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
{:else if link.icon === "folder"}
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
{:else if link.icon === "terminal"}
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
{/if}
</span>
{link.label}
</button>
{/each}
</nav>
<hr class="my-4 border-gray-200" />
<p class="text-xs text-gray-400 mb-2 px-3">External Services</p>
<nav class="flex flex-col gap-1">
<div class="my-4 border-t border-surface-200 {dark ? 'border-surface-700' : ''}"></div>
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2">Services</p>
{#each external as svc}
<a href={extHref(svc.port)} target="_blank" class="text-left px-3 py-2 rounded text-sm hover:bg-gray-100 text-gray-700">
{svc.label} &#8599;
<a
href={extHref(svc.port)}
target="_blank"
rel="noopener"
class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 hover:text-surface-700 transition-all duration-150 {dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : ''}"
>
<span class="w-5 h-5 flex items-center justify-center">
{#if svc.icon === "music"}
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z" /></svg>
{:else if svc.icon === "image"}
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
{:else if svc.icon === "play"}
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{:else if svc.icon === "git"}
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
{/if}
</span>
{svc.label}
<svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
</a>
{/each}
</nav>
<!-- Theme Toggle -->
<div class="px-4 pb-4 pt-2 border-t border-surface-200 {dark ? 'border-surface-700' : ''}">
<button
onclick={toggleTheme}
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 {dark ? 'hover:bg-surface-700 text-surface-400' : ''}"
>
{#if dark}
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
Light Mode
{:else}
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
Dark Mode
{/if}
</button>
</div>
</aside>