feat(ui): apply glassmorphism and staggered animations to tables

This commit is contained in:
courtmanr@gmail.com 2025-11-27 10:44:36 +00:00
parent e9ac1429c0
commit 185476750d
19 changed files with 405 additions and 368 deletions

View file

@ -483,6 +483,7 @@ export function ResourceTable(props: ResourceTableProps) {
padding="none" padding="none"
class="overflow-hidden border border-gray-200 dark:border-gray-700" class="overflow-hidden border border-gray-200 dark:border-gray-700"
border={false} border={false}
tone="glass"
> >
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700"> <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<SectionHeader title={props.title} size="sm" /> <SectionHeader title={props.title} size="sm" />
@ -868,7 +869,7 @@ export function ResourceTable(props: ResourceTableProps) {
return ( return (
<tr <tr
class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`} class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors animate-enter ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`}
> >
{/* Alert toggle column */} {/* Alert toggle column */}
<td class="p-1 px-2 text-center align-middle"> <td class="p-1 px-2 text-center align-middle">
@ -1379,7 +1380,7 @@ export function ResourceTable(props: ResourceTableProps) {
return ( return (
<tr <tr
class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`} class={`hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors animate-enter ${resource.disabled || props.globalDisableFlag?.() ? 'opacity-40' : ''}`}
> >
{/* Alert toggle column */} {/* Alert toggle column */}
<td class="p-1 px-2 text-center align-middle"> <td class="p-1 px-2 text-center align-middle">

View file

@ -1,6 +1,7 @@
import { createSignal, createMemo, Show, For, onMount, onCleanup, createEffect } from 'solid-js'; import { createSignal, createMemo, Show, For, onMount, onCleanup, createEffect } from 'solid-js';
import { useNavigate, useLocation } from '@solidjs/router'; import { useNavigate, useLocation } from '@solidjs/router';
import Toggle from '@/components/shared/Toggle'; import Toggle from '@/components/shared/Toggle';
import { Card } from '@/components/shared/Card';
// Workaround for eslint false-positive when `For` is used only in JSX // Workaround for eslint false-positive when `For` is used only in JSX
const __ensureForUsage = For; const __ensureForUsage = For;
@ -2217,8 +2218,7 @@ const cancelEdit = () => {
<button <button
type="button" type="button"
onClick={() => handleTabClick('proxmox')} onClick={() => handleTabClick('proxmox')}
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${ class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'proxmox'
activeTab() === 'proxmox'
? 'border-blue-500 text-blue-600 dark:text-blue-400' ? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`} }`}
@ -2228,8 +2228,7 @@ const cancelEdit = () => {
<button <button
type="button" type="button"
onClick={() => handleTabClick('pmg')} onClick={() => handleTabClick('pmg')}
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${ class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'pmg'
activeTab() === 'pmg'
? 'border-blue-500 text-blue-600 dark:text-blue-400' ? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`} }`}
@ -2239,8 +2238,7 @@ const cancelEdit = () => {
<button <button
type="button" type="button"
onClick={() => handleTabClick('hosts')} onClick={() => handleTabClick('hosts')}
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${ class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'hosts'
activeTab() === 'hosts'
? 'border-blue-500 text-blue-600 dark:text-blue-400' ? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`} }`}
@ -2250,8 +2248,7 @@ const cancelEdit = () => {
<button <button
type="button" type="button"
onClick={() => handleTabClick('docker')} onClick={() => handleTabClick('docker')}
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${ class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer ${activeTab() === 'docker'
activeTab() === 'docker'
? 'border-blue-500 text-blue-600 dark:text-blue-400' ? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`} }`}
@ -2745,7 +2742,7 @@ const cancelEdit = () => {
</Show> </Show>
<Show when={activeTab() === 'docker'}> <Show when={activeTab() === 'docker'}>
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-900"> <Card padding="md" tone="glass" class="mb-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
@ -2780,9 +2777,9 @@ const cancelEdit = () => {
rows={4} rows={4}
class="mt-4 w-full rounded-md border border-gray-300 bg-white p-3 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-sky-400 dark:focus:ring-sky-600/40" class="mt-4 w-full rounded-md border border-gray-300 bg-white p-3 text-sm text-gray-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-sky-400 dark:focus:ring-sky-600/40"
/> />
</div> </Card>
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-900"> <Card padding="md" tone="glass" class="mb-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Swarm service alerts</h3> <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Swarm service alerts</h3>
@ -2865,7 +2862,7 @@ const cancelEdit = () => {
{serviceGapValidationMessage()} {serviceGapValidationMessage()}
</p> </p>
)} )}
</div> </Card>
<Show when={hasSection('dockerHosts')}> <Show when={hasSection('dockerHosts')}>
<div ref={registerSection('dockerHosts')} class="scroll-mt-24"> <div ref={registerSection('dockerHosts')} class="scroll-mt-24">

View file

@ -1864,7 +1864,7 @@ const UnifiedBackups: Component = () => {
/> />
{/* Table */} {/* Table */}
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" tone="glass" class="mb-4 overflow-hidden">
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;"> <div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
<style>{` <style>{`
.overflow-x-auto::-webkit-scrollbar { display: none; } .overflow-x-auto::-webkit-scrollbar { display: none; }
@ -2138,7 +2138,7 @@ const UnifiedBackups: Component = () => {
</tr> </tr>
<For each={group.items}> <For each={group.items}>
{(item) => ( {(item) => (
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30"> <tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30 animate-enter">
<td class="p-0.5 pl-5 pr-1.5 text-sm align-middle">{item.vmid}</td> <td class="p-0.5 pl-5 pr-1.5 text-sm align-middle">{item.vmid}</td>
<td class="p-0.5 px-1.5 align-middle"> <td class="p-0.5 px-1.5 align-middle">
<span <span

View file

@ -929,7 +929,7 @@ export function Dashboard(props: DashboardProps) {
{/* Table View */} {/* Table View */}
<Show when={connected() && initialDataReceived() && filteredGuests().length > 0}> <Show when={connected() && initialDataReceived() && filteredGuests().length > 0}>
<ComponentErrorBoundary name="Guest Table"> <ComponentErrorBoundary name="Guest Table">
<Card padding="none" class="mb-4 bg-white dark:bg-gray-800"> <Card padding="none" tone="glass" class="mb-4">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
{/* Desktop Header */} {/* Desktop Header */}
<div <div

View file

@ -455,7 +455,7 @@ export function GuestRow(props: GuestRowProps) {
}); });
const rowClass = createMemo(() => { const rowClass = createMemo(() => {
const base = 'transition-all duration-200 relative'; const base = 'transition-all duration-200 relative animate-enter';
const hover = 'hover:shadow-sm'; const hover = 'hover:shadow-sm';
const alertBg = hasUnacknowledgedAlert() const alertBg = hasUnacknowledgedAlert()
? props.alertStyles?.severity === 'critical' ? props.alertStyles?.severity === 'critical'

View file

@ -142,7 +142,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
}; };
return ( return (
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" tone="glass" class="mb-4 overflow-hidden">
<ScrollableTable minWidth="300px" persistKey="docker-host-summary"> <ScrollableTable minWidth="300px" persistKey="docker-host-summary">
<table class="w-full table-fixed border-collapse sm:whitespace-nowrap"> <table class="w-full table-fixed border-collapse sm:whitespace-nowrap">
<thead> <thead>
@ -227,7 +227,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
}; };
const rowClass = () => { const rowClass = () => {
const baseHover = 'cursor-pointer transition-all duration-200 relative hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm'; const baseHover = 'cursor-pointer transition-all duration-200 relative hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:shadow-sm animate-enter';
if (selected) { if (selected) {
return 'cursor-pointer transition-all duration-200 relative bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:shadow-sm z-10'; return 'cursor-pointer transition-all duration-200 relative bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 hover:shadow-sm z-10';

View file

@ -1252,7 +1252,7 @@ const DockerContainerRow: Component<{
return ( return (
<> <>
<div <div
class={`grid items-center transition-all duration-200 ${hasDrawerContent() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isRunning() ? 'opacity-60' : ''}`} class={`grid items-center transition-all duration-200 animate-enter ${hasDrawerContent() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isRunning() ? 'opacity-60' : ''}`}
style={{ 'grid-template-columns': props.gridTemplate() }} style={{ 'grid-template-columns': props.gridTemplate() }}
onClick={toggle} onClick={toggle}
aria-expanded={expanded()} aria-expanded={expanded()}
@ -1949,7 +1949,7 @@ const DockerServiceRow: Component<{
return ( return (
<> <>
<div <div
class={`grid items-center transition-all duration-200 ${hasTasks() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isHealthy() ? 'opacity-60' : ''}`} class={`grid items-center transition-all duration-200 animate-enter ${hasTasks() ? 'cursor-pointer' : ''} ${expanded() ? 'bg-gray-50 dark:bg-gray-800/40' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'} ${!isHealthy() ? 'opacity-60' : ''}`}
style={{ 'grid-template-columns': props.gridTemplate() }} style={{ 'grid-template-columns': props.gridTemplate() }}
onClick={toggle} onClick={toggle}
aria-expanded={expanded()} aria-expanded={expanded()}
@ -2407,7 +2407,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</Card> </Card>
} }
> >
<Card padding="none" class="overflow-hidden"> <Card padding="none" tone="glass" class="overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
{/* Header Row */} {/* Header Row */}
<div <div

View file

@ -170,7 +170,7 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
</Card> </Card>
} }
> >
<Card padding="none" class="overflow-hidden"> <Card padding="none" tone="glass" class="overflow-hidden">
<ScrollableTable> <ScrollableTable>
<table class="w-full border-collapse"> <table class="w-full border-collapse">
<thead> <thead>
@ -243,7 +243,7 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
}); });
const rowClass = () => { const rowClass = () => {
const base = 'border-b border-gray-200 dark:border-gray-700 transition-all duration-200'; const base = 'border-b border-gray-200 dark:border-gray-700 transition-all duration-200 animate-enter';
const hover = 'hover:bg-gray-50 dark:hover:bg-gray-800/50'; const hover = 'hover:bg-gray-50 dark:hover:bg-gray-800/50';
const clickable = hasDrawerContent() ? 'cursor-pointer' : ''; const clickable = hasDrawerContent() ? 'cursor-pointer' : '';
const expanded = drawerOpen() ? 'bg-gray-50 dark:bg-gray-800/40' : ''; const expanded = drawerOpen() ? 'bg-gray-50 dark:bg-gray-800/40' : '';

View file

@ -97,7 +97,7 @@ const MailGateway: Component = () => {
const rblPct = (rbl / Math.max(inbound, 1)) * 100; const rblPct = (rbl / Math.max(inbound, 1)) * 100;
return ( return (
<div class="border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> <Card padding="none" tone="glass" class="overflow-hidden">
{/* Instance Header Strip */} {/* Instance Header Strip */}
<div class="sticky top-0 z-10 flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700"> <div class="sticky top-0 z-10 flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800/40 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@ -133,14 +133,14 @@ const MailGateway: Component = () => {
</div> </div>
<table class="w-full text-xs"> <table class="w-full text-xs">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(total)}</div> <div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(total)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400">{formatDec(total / 24)}/hr</div> <div class="text-[11px] text-gray-500 dark:text-gray-400">{formatDec(total / 24)}/hr</div>
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Total processed</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Total processed</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(inbound)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(inbound)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -149,7 +149,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(outbound)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(outbound)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -158,7 +158,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Outbound</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Outbound</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(spam)}</div> <div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(spam)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -167,7 +167,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam caught</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam caught</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(virus)}</div> <div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(virus)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -189,7 +189,7 @@ const MailGateway: Component = () => {
</div> </div>
<table class="w-full text-xs"> <table class="w-full text-xs">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesIn / 1024 / 1024)} MB</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesIn / 1024 / 1024)} MB</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -198,7 +198,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound bytes</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Inbound bytes</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesOut / 1024 / 1024)} MB</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatDec(bytesOut / 1024 / 1024)} MB</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -223,7 +223,7 @@ const MailGateway: Component = () => {
</div> </div>
<table class="w-full text-xs"> <table class="w-full text-xs">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(qSpam)}</div> <div class="text-xs font-medium text-orange-600 dark:text-orange-400">{formatNum(qSpam)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -232,7 +232,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam quarantined</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Spam quarantined</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(qVirus)}</div> <div class="text-xs font-medium text-red-600 dark:text-red-400">{formatNum(qVirus)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -241,7 +241,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Virus quarantined</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Virus quarantined</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-medium text-yellow-600 dark:text-yellow-400">{formatNum(qAttachment)}</div> <div class="text-xs font-medium text-yellow-600 dark:text-yellow-400">{formatNum(qAttachment)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -250,7 +250,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Attachments blocked</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Attachments blocked</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(qBlacklist)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(qBlacklist)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -259,7 +259,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Blacklisted</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Blacklisted</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(qTotal)}</div> <div class="text-xs font-semibold text-gray-900 dark:text-gray-100">{formatNum(qTotal)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -315,7 +315,7 @@ const MailGateway: Component = () => {
</div> </div>
<table class="w-full text-xs"> <table class="w-full text-xs">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesIn)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesIn)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -324,7 +324,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces inbound</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces inbound</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesOut)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(bouncesOut)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -333,7 +333,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces outbound</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Bounces outbound</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(rbl)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(rbl)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -342,7 +342,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">RBL rejects</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">RBL rejects</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(pregreet)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(pregreet)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -351,7 +351,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Pregreet rejects</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Pregreet rejects</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(greylist)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(greylist)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -360,7 +360,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Greylisted</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Greylisted</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(junk)}</div> <div class="text-xs text-gray-900 dark:text-gray-100">{formatNum(junk)}</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400"> <div class="text-[11px] text-gray-500 dark:text-gray-400">
@ -369,7 +369,7 @@ const MailGateway: Component = () => {
</td> </td>
<td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Junk mail</td> <td class="px-2 py-1.5 text-[11px] text-gray-500 dark:text-gray-400">Junk mail</td>
</tr> </tr>
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
<div class="text-xs text-gray-900 dark:text-gray-100"> <div class="text-xs text-gray-900 dark:text-gray-100">
{pmg.mailStats?.averageProcessTimeMs ? formatDec(pmg.mailStats.averageProcessTimeMs / 1000, 2) : '—'} s {pmg.mailStats?.averageProcessTimeMs ? formatDec(pmg.mailStats.averageProcessTimeMs / 1000, 2) : '—'} s
@ -421,7 +421,7 @@ const MailGateway: Component = () => {
const oldestClass = oldestAge > 1800 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300'; const oldestClass = oldestAge > 1800 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300';
return ( return (
<tr> <tr class="animate-enter">
<td class="px-2 py-1.5 text-xs font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">{node.name}</td> <td class="px-2 py-1.5 text-xs font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">{node.name}</td>
<td class="px-2 py-1.5 text-xs text-gray-700 dark:text-gray-300 capitalize">{node.role || '—'}</td> <td class="px-2 py-1.5 text-xs text-gray-700 dark:text-gray-300 capitalize">{node.role || '—'}</td>
<td class="px-2 py-1.5"> <td class="px-2 py-1.5">
@ -463,7 +463,7 @@ const MailGateway: Component = () => {
</div> </div>
</Show> </Show>
</div> </div>
</div> </Card>
); );
}} }}
</For> </For>

View file

@ -101,7 +101,7 @@ const Replication: Component = () => {
</Card> </Card>
} }
> >
<Card padding="none"> <Card padding="none" tone="glass">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-[1000px] w-full divide-y divide-gray-200 dark:divide-gray-700"> <table class="min-w-[1000px] w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900/40"> <thead class="bg-gray-50 dark:bg-gray-900/40">
@ -121,7 +121,7 @@ const Replication: Component = () => {
{(job) => { {(job) => {
const badge = getStatusBadge(job); const badge = getStatusBadge(job);
return ( return (
<tr class="hover:bg-gray-50/80 dark:hover:bg-gray-900/40 transition-colors"> <tr class="hover:bg-gray-50/80 dark:hover:bg-gray-900/40 transition-colors animate-enter">
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]"> <div class="font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
{job.guestName || `VM ${job.guestId ?? ''}`} {job.guestName || `VM ${job.guestId ?? ''}`}

View file

@ -421,15 +421,13 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
</p> </p>
</div> </div>
<div <div
class={`rounded-lg border p-4 text-sm shadow-sm ${ class={`rounded-lg border p-4 text-sm shadow-sm ${hasWildcardTokens()
hasWildcardTokens()
? 'border-amber-300/80 bg-amber-50/80 text-amber-900 dark:border-amber-700/70 dark:bg-amber-900/20 dark:text-amber-100' ? 'border-amber-300/80 bg-amber-50/80 text-amber-900 dark:border-amber-700/70 dark:bg-amber-900/20 dark:text-amber-100'
: 'border-gray-200/70 bg-white/70 text-gray-700 dark:border-gray-700/70 dark:bg-gray-900/40 dark:text-gray-300' : 'border-gray-200/70 bg-white/70 text-gray-700 dark:border-gray-700/70 dark:bg-gray-900/40 dark:text-gray-300'
}`} }`}
> >
<div <div
class={`text-[0.7rem] font-semibold uppercase tracking-wide ${ class={`text-[0.7rem] font-semibold uppercase tracking-wide ${hasWildcardTokens()
hasWildcardTokens()
? 'text-amber-700 dark:text-amber-300' ? 'text-amber-700 dark:text-amber-300'
: 'text-gray-500 dark:text-gray-400' : 'text-gray-500 dark:text-gray-400'
}`} }`}
@ -437,8 +435,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
Full access tokens Full access tokens
</div> </div>
<div <div
class={`mt-1 text-2xl font-semibold ${ class={`mt-1 text-2xl font-semibold ${hasWildcardTokens()
hasWildcardTokens()
? 'text-amber-800 dark:text-amber-100' ? 'text-amber-800 dark:text-amber-100'
: 'text-gray-900 dark:text-gray-100' : 'text-gray-900 dark:text-gray-100'
}`} }`}
@ -446,8 +443,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
{wildcardCount()} {wildcardCount()}
</div> </div>
<p <p
class={`mt-1 text-xs ${ class={`mt-1 text-xs ${hasWildcardTokens()
hasWildcardTokens()
? 'text-amber-700 dark:text-amber-200' ? 'text-amber-700 dark:text-amber-200'
: 'text-gray-500 dark:text-gray-400' : 'text-gray-500 dark:text-gray-400'
}`} }`}
@ -517,7 +513,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
</Card> </Card>
} }
> >
<Card padding="none" class="overflow-hidden border border-gray-200 dark:border-gray-700"> <Card padding="none" tone="glass" class="overflow-hidden">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 bg-gray-50/60 px-5 py-4 dark:border-gray-700 dark:bg-gray-900/40"> <div class="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 bg-gray-50/60 px-5 py-4 dark:border-gray-700 dark:bg-gray-900/40">
<div> <div>
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Token inventory</h4> <h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Token inventory</h4>
@ -586,8 +582,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
return ( return (
<tr <tr
class={`transition-colors ${ class={`transition-colors animate-enter ${rowIsWildcard
rowIsWildcard
? 'bg-amber-50/50 dark:bg-amber-900/10' ? 'bg-amber-50/50 dark:bg-amber-900/10'
: 'bg-white dark:bg-gray-900/10' : 'bg-white dark:bg-gray-900/10'
} hover:bg-blue-50/40 dark:hover:bg-gray-800/50`} } hover:bg-blue-50/40 dark:hover:bg-gray-800/50`}
@ -605,8 +600,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
const isWildcard = scope.value === '*'; const isWildcard = scope.value === '*';
return ( return (
<span <span
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${ class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${isWildcard
isWildcard
? 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200' ? 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
}`} }`}
@ -667,8 +661,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
<Card <Card
padding="lg" padding="lg"
class={`border border-gray-200 dark:border-gray-700 transition-shadow ${ class={`border border-gray-200 dark:border-gray-700 transition-shadow ${createHighlight() ? 'ring-2 ring-blue-500/60 shadow-lg' : ''
createHighlight() ? 'ring-2 ring-blue-500/60 shadow-lg' : ''
}`} }`}
ref={(el: HTMLDivElement) => { ref={(el: HTMLDivElement) => {
createSectionRef = el; createSectionRef = el;
@ -723,8 +716,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button <button
type="button" type="button"
class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${ class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${isFullAccessSelected()
isFullAccessSelected()
? 'border-blue-500 bg-blue-600 text-white shadow-sm' ? 'border-blue-500 bg-blue-600 text-white shadow-sm'
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200' : 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
}`} }`}
@ -738,8 +730,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
{(preset) => ( {(preset) => (
<button <button
type="button" type="button"
class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${ class={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold transition ${presetMatchesSelection(preset.scopes)
presetMatchesSelection(preset.scopes)
? 'border-blue-500 bg-blue-600 text-white shadow-sm' ? 'border-blue-500 bg-blue-600 text-white shadow-sm'
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200' : 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
}`} }`}
@ -771,8 +762,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
return ( return (
<button <button
type="button" type="button"
class={`rounded-full border px-3 py-1 text-xs font-semibold transition ${ class={`rounded-full border px-3 py-1 text-xs font-semibold transition ${isActive()
isActive()
? 'border-blue-500 bg-blue-600 text-white shadow-sm' ? 'border-blue-500 bg-blue-600 text-white shadow-sm'
: 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200' : 'border-gray-300 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-200'
}`} }`}

View file

@ -1,5 +1,6 @@
import { Component, For, Show, createMemo } from 'solid-js'; import { Component, For, Show, createMemo } from 'solid-js';
import type { NodeConfig } from '@/types/nodes'; import type { NodeConfig } from '@/types/nodes';
import { Card } from '@/components/shared/Card';
type NodeConfigWithStatus = NodeConfig & { type NodeConfigWithStatus = NodeConfig & {
hasPassword?: boolean; hasPassword?: boolean;
@ -271,7 +272,7 @@ const resolvePveStatusMeta = (
export const PveNodesTable: Component<PveNodesTableProps> = (props) => { export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
return ( return (
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> <Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/70"> <thead class="bg-gray-50 dark:bg-gray-800/70">
<tr> <tr>
@ -310,7 +311,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
), ),
); );
return ( return (
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors"> <tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors animate-enter">
<td class="align-top py-3 pl-4 pr-3"> <td class="align-top py-3 pl-4 pr-3">
<div class="min-w-0 space-y-1"> <div class="min-w-0 space-y-1">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
@ -492,7 +493,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
</For> </For>
</tbody> </tbody>
</table> </table>
</div> </Card>
); );
}; };
@ -541,7 +542,7 @@ const resolvePbsStatusMeta = (
export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => { export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
return ( return (
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> <Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/70"> <thead class="bg-gray-50 dark:bg-gray-800/70">
<tr> <tr>
@ -567,7 +568,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
{(node) => { {(node) => {
const statusMeta = createMemo(() => resolvePbsStatusMeta(node, props.statePbs)); const statusMeta = createMemo(() => resolvePbsStatusMeta(node, props.statePbs));
return ( return (
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors"> <tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors animate-enter">
<td class="align-top py-3 pl-4 pr-3"> <td class="align-top py-3 pl-4 pr-3">
<div class="min-w-0 space-y-1"> <div class="min-w-0 space-y-1">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
@ -675,7 +676,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
</For> </For>
</tbody> </tbody>
</table> </table>
</div> </Card>
); );
}; };
@ -724,7 +725,7 @@ const resolvePmgStatusMeta = (
export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => { export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
return ( return (
<div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700"> <Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/70"> <thead class="bg-gray-50 dark:bg-gray-800/70">
<tr> <tr>
@ -750,7 +751,7 @@ export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
{(node) => { {(node) => {
const statusMeta = createMemo(() => resolvePmgStatusMeta(node, props.statePmg)); const statusMeta = createMemo(() => resolvePmgStatusMeta(node, props.statePmg));
return ( return (
<tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors"> <tr class="even:bg-gray-50/60 dark:even:bg-gray-800/30 hover:bg-blue-50/40 dark:hover:bg-blue-900/20 transition-colors animate-enter">
<td class="align-top py-3 pl-4 pr-3"> <td class="align-top py-3 pl-4 pr-3">
<div class="min-w-0 space-y-1"> <div class="min-w-0 space-y-1">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
@ -847,6 +848,6 @@ export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
</For> </For>
</tbody> </tbody>
</table> </table>
</div> </Card>
); );
}; };

View file

@ -635,7 +635,7 @@ export const UnifiedAgents: Component = () => {
</div> </div>
</Show> </Show>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"> <Card padding="none" tone="glass" class="overflow-hidden rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800"> <thead class="bg-gray-50 dark:bg-gray-800">
<tr> <tr>
@ -656,7 +656,7 @@ export const UnifiedAgents: Component = () => {
</tr> </tr>
}> }>
{(agent) => ( {(agent) => (
<tr> <tr class="animate-enter">
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100"> <td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
{agent.displayName || agent.hostname} {agent.displayName || agent.hostname}
<Show when={agent.displayName && agent.displayName !== agent.hostname}> <Show when={agent.displayName && agent.displayName !== agent.hostname}>
@ -709,7 +709,7 @@ export const UnifiedAgents: Component = () => {
</For> </For>
</tbody> </tbody>
</table> </table>
</div> </Card>
</Card> </Card>
</div > </div >
); );

View file

@ -149,7 +149,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show> </Show>
<Show when={filteredDisks().length > 0}> <Show when={filteredDisks().length > 0}>
<Card padding="none" class="overflow-hidden"> <Card padding="none" tone="glass" class="overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full"> <table class="w-full">
<thead> <thead>
@ -191,7 +191,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
return ( return (
<> <>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors animate-enter">
<td class="px-2 py-1.5 text-xs"> <td class="px-2 py-1.5 text-xs">
<span class="font-medium text-gray-900 dark:text-gray-100"> <span class="font-medium text-gray-900 dark:text-gray-100">
{disk.node} {disk.node}

View file

@ -742,7 +742,7 @@ const Storage: Component = () => {
{/* Storage Table - shows for both PVE and PBS storage */} {/* Storage Table - shows for both PVE and PBS storage */}
<Show when={connected() && initialDataReceived() && sortedStorage().length > 0}> <Show when={connected() && initialDataReceived() && sortedStorage().length > 0}>
<ComponentErrorBoundary name="Storage Table"> <ComponentErrorBoundary name="Storage Table">
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" tone="glass" class="mb-4 overflow-hidden">
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;"> <div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
<style>{` <style>{`
.overflow-x-auto::-webkit-scrollbar { display: none; } .overflow-x-auto::-webkit-scrollbar { display: none; }
@ -1043,7 +1043,7 @@ const Storage: Component = () => {
return ( return (
<> <>
<tr <tr
class={`${rowClass()} transition-colors`} class={`${rowClass()} transition-colors animate-enter`}
style={rowStyle()} style={rowStyle()}
onClick={toggleDrawer} onClick={toggleDrawer}
aria-expanded={canExpand() && isExpanded() ? 'true' : 'false'} aria-expanded={canExpand() && isExpanded() ? 'true' : 'false'}

View file

@ -1,6 +1,6 @@
import { JSX, splitProps, mergeProps } from 'solid-js'; import { JSX, splitProps, mergeProps } from 'solid-js';
type Tone = 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger'; type Tone = 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger' | 'glass';
type Padding = 'none' | 'sm' | 'md' | 'lg'; type Padding = 'none' | 'sm' | 'md' | 'lg';
type CardProps = { type CardProps = {
@ -17,6 +17,7 @@ const toneClassMap: Record<Tone, string> = {
success: 'bg-green-50/70 dark:bg-green-900/20', success: 'bg-green-50/70 dark:bg-green-900/20',
warning: 'bg-amber-50/80 dark:bg-amber-900/20', warning: 'bg-amber-50/80 dark:bg-amber-900/20',
danger: 'bg-red-50/80 dark:bg-red-900/20', danger: 'bg-red-50/80 dark:bg-red-900/20',
glass: 'glass',
}; };
const paddingClassMap: Record<Padding, string> = { const paddingClassMap: Record<Padding, string> = {

View file

@ -498,7 +498,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
}; };
return ( return (
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" tone="glass" class="mb-4 overflow-hidden">
<div> <div>
{/* Header */} {/* Header */}
<div <div
@ -569,7 +569,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
}); });
const rowClass = createMemo(() => { const rowClass = createMemo(() => {
const baseHover = 'cursor-pointer transition-all duration-200 relative hover:shadow-sm group'; const baseHover = 'cursor-pointer transition-all duration-200 relative hover:shadow-sm group animate-enter';
if (isSelected()) { if (isSelected()) {
return `cursor-pointer transition-all duration-200 relative hover:shadow-sm z-10 group`; return `cursor-pointer transition-all duration-200 relative hover:shadow-sm z-10 group`;

View file

@ -14,9 +14,9 @@ interface StatusDotProps {
} }
const VARIANT_CLASSES: Record<StatusIndicatorVariant, string> = { const VARIANT_CLASSES: Record<StatusIndicatorVariant, string> = {
success: 'bg-emerald-500 dark:bg-emerald-400', success: 'bg-emerald-500 dark:bg-emerald-400 glow-success',
warning: 'bg-amber-500 dark:bg-amber-400', warning: 'bg-amber-500 dark:bg-amber-400 glow-warning',
danger: 'bg-red-500 dark:bg-red-400', danger: 'bg-red-500 dark:bg-red-400 glow-danger',
muted: 'bg-gray-400 dark:bg-gray-500', muted: 'bg-gray-400 dark:bg-gray-500',
}; };

View file

@ -479,3 +479,50 @@ body,
.delay-300 { .delay-300 {
animation-delay: 300ms; animation-delay: 300ms;
} }
/* Glassmorphism Utilities */
.glass {
@apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-md border border-white/20 dark:border-gray-700/30 shadow-sm;
}
.glass-panel {
@apply bg-white/60 dark:bg-gray-900/60 backdrop-blur-xl border border-gray-200/50 dark:border-gray-700/50;
}
.glass-hover {
@apply hover:bg-white/90 dark:hover:bg-gray-800/90 transition-colors duration-200;
}
/* Staggered Entry Animation */
@keyframes enter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-enter {
animation: enter 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
/* Glow effects for status dots */
.glow-success {
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);
}
.glow-warning {
box-shadow: 0 0 8px rgba(245, 158, 11, 0.4);
}
.glow-danger {
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
.glow-info {
box-shadow: 0 0 8px rgba(59, 130, 246, 0.4);
}