refactor: update UI components to shadcn-style

- Refactored select component to align with shadcn design principles.
- Updated server status indicator component for consistency with shadcn style.
- Modified spinner component to follow shadcn design guidelines.
- Enhanced stat card component with shadcn-inspired styles.
- Adjusted tab panel component to reflect shadcn design updates.
- Revamped table row and table data cell components to match shadcn aesthetics.
- Updated table header cell component for improved styling consistency.
- Refined table component to align with shadcn design principles.
- Enhanced tabs component with shadcn-inspired styles.
- Updated toggle component to follow shadcn design guidelines.
- Refactored tooltip component to align with shadcn design principles.
- Updated server card component to reflect shadcn-inspired design.
This commit is contained in:
2025-12-17 11:47:42 -05:00
parent e7b33a2f6c
commit 7190d7a818
34 changed files with 522 additions and 565 deletions

View File

@@ -1,5 +1,5 @@
{{--
Action Toolbar Button Component - TailServer Theme
Action Toolbar Button Component - shadcn-style
Individual button for VM control actions
Usage:
@@ -24,23 +24,23 @@
@php
$variants = [
'default' => 'bg-gradient-to-b from-neutral/5 to-neutral/10 hover:from-neutral/10 hover:to-neutral/15 text-base border border-neutral/20 hover:border-neutral/30 shadow-sm hover:shadow',
'primary' => 'bg-gradient-to-b from-primary/10 to-primary/15 hover:from-primary/15 hover:to-primary/20 text-primary border border-primary/20 hover:border-primary/30 shadow-sm hover:shadow-md hover:shadow-primary/10',
'success' => 'bg-gradient-to-b from-success/10 to-success/15 hover:from-success/15 hover:to-success/20 text-success border border-success/20 hover:border-success/30 shadow-sm hover:shadow-md hover:shadow-success/10',
'warning' => 'bg-gradient-to-b from-warning/10 to-warning/15 hover:from-warning/15 hover:to-warning/20 text-warning border border-warning/20 hover:border-warning/30 shadow-sm hover:shadow-md hover:shadow-warning/10',
'danger' => 'bg-gradient-to-b from-danger/10 to-danger/15 hover:from-danger/15 hover:to-danger/20 text-danger border border-danger/20 hover:border-danger/30 shadow-sm hover:shadow-md hover:shadow-danger/10',
'default' => 'border bg-background hover:bg-accent hover:text-accent-foreground',
'primary' => 'border border-primary/20 bg-primary/10 text-primary hover:bg-primary/20',
'success' => 'border border-success/20 bg-success/10 text-success hover:bg-success/20',
'warning' => 'border border-warning/20 bg-warning/10 text-warning hover:bg-warning/20',
'danger' => 'border border-danger/20 bg-danger/10 text-danger hover:bg-danger/20',
];
$sizes = [
'sm' => 'px-2.5 py-1.5 text-xs',
'md' => 'px-3.5 py-2 text-sm',
'lg' => 'px-5 py-2.5 text-base',
'sm' => 'h-8 px-3 text-xs',
'md' => 'h-9 px-4 text-sm',
'lg' => 'h-10 px-5 text-sm',
];
$iconSizes = [
'sm' => 'size-3.5',
'md' => 'size-4',
'lg' => 'size-5',
'lg' => 'size-4',
];
@endphp
@@ -53,11 +53,11 @@
{{ $attributes->merge([
'disabled' => $disabled || $loading,
])->class([
'inline-flex items-center gap-2 rounded-xl font-medium transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:scale-95',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
$variants[$variant] ?? $variants['default'],
$sizes[$size] ?? $sizes['md'],
'opacity-50 cursor-not-allowed hover:translate-y-0' => $disabled,
'cursor-wait hover:translate-y-0' => $loading,
'pointer-events-none opacity-50' => $disabled,
'cursor-wait' => $loading,
]) }}
>
@if($loading)

View File

@@ -1,5 +1,5 @@
{{--
Alert Component - TailServer Theme
Alert Component - TailServer Theme (shadcn-inspired)
Usage:
<x-proxmox::ui.alert>Default alert</x-proxmox::ui.alert>
@@ -8,7 +8,7 @@
--}}
@props([
'variant' => 'info',
'variant' => 'default',
'title' => null,
'icon' => null,
'dismissible' => false,
@@ -18,46 +18,36 @@
@php
$variantConfig = [
'default' => [
'class' => 'bg-card text-card-foreground',
'icon' => $icon ?? 'ri-information-line',
],
'info' => [
'bg' => 'bg-gradient-to-r from-info/10 via-info/5 to-transparent',
'border' => 'border-info/20',
'text' => 'text-info',
'class' => 'border-info/20 bg-info/5 text-info [&>svg]:text-info',
'icon' => $icon ?? 'ri-information-line',
],
'success' => [
'bg' => 'bg-gradient-to-r from-success/10 via-success/5 to-transparent',
'border' => 'border-success/20',
'text' => 'text-success',
'class' => 'border-success/20 bg-success/5 text-success [&>svg]:text-success',
'icon' => $icon ?? 'ri-checkbox-circle-line',
],
'warning' => [
'bg' => 'bg-gradient-to-r from-warning/10 via-warning/5 to-transparent',
'border' => 'border-warning/20',
'text' => 'text-warning',
'class' => 'border-warning/20 bg-warning/5 text-warning [&>svg]:text-warning',
'icon' => $icon ?? 'ri-error-warning-line',
],
'danger' => [
'bg' => 'bg-gradient-to-r from-danger/10 via-danger/5 to-transparent',
'border' => 'border-danger/20',
'text' => 'text-danger',
'class' => 'border-danger/20 bg-danger/5 text-danger [&>svg]:text-danger',
'icon' => $icon ?? 'ri-error-warning-fill',
],
'default' => [
'bg' => 'bg-gradient-to-r from-neutral/10 via-neutral/5 to-transparent',
'border' => 'border-neutral/20',
'text' => 'text-base/70',
'icon' => $icon ?? 'ri-information-line',
],
];
$config = $variantConfig[$variant] ?? $variantConfig['default'];
@endphp
<div
role="alert"
{{ $attributes->class([
'rounded-xl p-4 border',
$config['bg'],
$config['border'],
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5',
$config['class'],
]) }}
@if($dismissible || $collapsible)
x-data="{
@@ -65,56 +55,54 @@
@if($collapsible) collapsed: {{ $collapsed ? 'true' : 'false' }} @endif
}"
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@endif
>
<div class="flex items-start gap-3">
<x-dynamic-component :component="$config['icon']" class="size-5 {{ $config['text'] }} flex-shrink-0 mt-0.5" />
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
@if($title)
<h4 class="text-sm font-semibold text-base">{{ $title }}</h4>
@endif
<div class="flex items-center gap-1 ml-auto">
@if($collapsible)
<button
type="button"
@click="collapsed = !collapsed"
class="p-1.5 rounded-lg hover:bg-black/10 transition-all duration-200 {{ $config['text'] }}"
>
<x-ri-arrow-down-s-line
class="size-4 transition-transform duration-300 ease-out"
x-bind:class="collapsed ? 'rotate-180' : ''"
/>
</button>
@endif
@if($dismissible)
<button
type="button"
@click="show = false"
class="p-1.5 rounded-lg hover:bg-black/10 hover:rotate-90 transition-all duration-200 {{ $config['text'] }}"
>
<x-ri-close-line class="size-4" />
</button>
@endif
</div>
</div>
<div
class="text-sm text-base/70 leading-relaxed @if($title) mt-1 @endif"
@if($collapsible)
x-show="!collapsed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
@endif
>
{{ $slot }}
</div>
<x-dynamic-component :component="$config['icon']" class="size-4" />
<div class="flex-1 min-w-0">
@if($title)
<h5 class="mb-1 font-medium leading-none tracking-tight">{{ $title }}</h5>
@endif
<div
class="text-sm [&_p]:leading-relaxed opacity-90"
@if($collapsible)
x-show="!collapsed"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
>
{{ $slot }}
</div>
</div>
@if($dismissible || $collapsible)
<div class="absolute top-3 right-3 flex items-center gap-1">
@if($collapsible)
<button
type="button"
@click="collapsed = !collapsed"
class="p-1 rounded hover:bg-black/5 transition-colors"
>
<x-ri-arrow-down-s-line
class="size-4 transition-transform duration-150"
x-bind:class="collapsed ? 'rotate-180' : ''"
/>
</button>
@endif
@if($dismissible)
<button
type="button"
@click="show = false"
class="p-1 rounded hover:bg-black/5 transition-colors"
>
<x-ri-close-line class="size-4" />
</button>
@endif
</div>
@endif
</div>

View File

@@ -1,5 +1,5 @@
{{--
Resource Allocation Card Component - TailServer Theme
Resource Allocation Card Component - shadcn-style
For displaying CPU, Memory, and other resource allocation vs entitlement
Usage:
@@ -26,26 +26,23 @@
'iconColor' => 'text-primary',
])
<div {{ $attributes->class(['group relative border border-neutral/20 rounded-xl p-4 bg-background hover:border-neutral/30 hover:shadow-sm transition-all duration-200']) }}>
{{-- Subtle gradient overlay on hover --}}
<div class="absolute inset-0 rounded-xl bg-gradient-to-br from-primary/[0.02] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
<div class="relative flex flex-col gap-3">
<div {{ $attributes->class(['rounded-xl border bg-card p-4 shadow-sm transition-colors duration-150 hover:bg-accent/50']) }}>
<div class="flex flex-col gap-3">
<div class="flex items-start justify-between gap-3">
<div class="flex items-start gap-3">
@if($icon)
<div class="flex-shrink-0 w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center">
<x-dynamic-component :component="$icon" class="size-4.5 {{ $iconColor }}" />
<div class="flex-shrink-0 size-9 rounded-lg bg-primary/10 flex items-center justify-center">
<x-dynamic-component :component="$icon" class="size-4 {{ $iconColor }}" />
</div>
@endif
<div class="min-w-0">
<p class="text-[11px] font-semibold tracking-wider uppercase text-base/50">{{ $label }}</p>
<p class="text-xl font-bold text-base leading-tight mt-0.5">{{ $usageSummary }}</p>
<p class="text-xs text-base/50 mt-0.5">{{ $entitlementLabel }}</p>
<div class="min-w-0 space-y-0.5">
<p class="text-xs font-medium text-muted-foreground">{{ $label }}</p>
<p class="text-xl font-bold tracking-tight">{{ $usageSummary }}</p>
<p class="text-xs text-muted-foreground">{{ $entitlementLabel }}</p>
</div>
</div>
@if($badge)
<span class="flex-shrink-0 text-[10px] font-bold px-2 py-1 rounded-full {{ $badge['class'] ?? 'text-base/60 bg-neutral/10' }}">
<span class="flex-shrink-0 text-xs font-medium px-2 py-0.5 rounded-full {{ $badge['class'] ?? 'text-muted-foreground bg-muted' }}">
{{ $badge['label'] ?? '' }}
</span>
@endif
@@ -54,12 +51,12 @@
@if(!is_null($percent))
<div class="space-y-1.5">
<div class="flex items-center justify-between text-xs">
<span class="text-base/50">Usage</span>
<span class="font-semibold text-base/70">{{ $percent }}%</span>
<span class="text-muted-foreground">Usage</span>
<span class="font-medium">{{ $percent }}%</span>
</div>
<div class="h-1.5 rounded-full bg-neutral/10 overflow-hidden">
<div class="h-1.5 rounded-full bg-muted overflow-hidden">
<div
class="h-full rounded-full transition-all duration-500 ease-out"
class="h-full rounded-full transition-all duration-300"
style="width: {{ min($percent, 130) }}%; background-image: {{ $gradient }};"
></div>
</div>
@@ -67,7 +64,7 @@
@endif
@if($description)
<p class="text-[11px] text-base/40 leading-relaxed">{{ $description }}</p>
<p class="text-xs text-muted-foreground/70">{{ $description }}</p>
@endif
</div>
</div>

View File

@@ -1,5 +1,5 @@
{{--
Badge Component - TailServer Theme
Badge Component - TailServer Theme (shadcn-inspired)
Usage:
<x-proxmox::ui.badge>Default</x-proxmox::ui.badge>
@@ -18,31 +18,31 @@
])
@php
$baseClasses = 'inline-flex items-center font-semibold rounded-full transition-colors';
$baseClasses = 'inline-flex items-center rounded-md border font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2';
$variantClasses = [
'default' => $outline ? 'bg-transparent border border-neutral/30 text-base/70' : 'bg-neutral/10 text-base/70',
'primary' => $outline ? 'bg-transparent border border-primary/40 text-primary' : 'bg-primary/10 text-primary',
'secondary' => $outline ? 'bg-transparent border border-secondary/40 text-secondary' : 'bg-secondary/10 text-secondary',
'success' => $outline ? 'bg-transparent border border-success/40 text-success' : 'bg-success/10 text-success',
'warning' => $outline ? 'bg-transparent border border-warning/40 text-warning' : 'bg-warning/10 text-warning',
'danger' => $outline ? 'bg-transparent border border-danger/40 text-danger' : 'bg-danger/10 text-danger',
'info' => $outline ? 'bg-transparent border border-info/40 text-info' : 'bg-info/10 text-info',
'muted' => 'bg-neutral/5 text-base/40',
'default' => 'border-transparent bg-primary text-primary-foreground shadow',
'secondary' => 'border-transparent bg-secondary text-secondary-foreground',
'success' => $outline ? 'border-success/40 text-success bg-transparent' : 'border-transparent bg-success/15 text-success',
'warning' => $outline ? 'border-warning/40 text-warning bg-transparent' : 'border-transparent bg-warning/15 text-warning',
'danger' => $outline ? 'border-danger/40 text-danger bg-transparent' : 'border-transparent bg-danger/15 text-danger',
'info' => $outline ? 'border-info/40 text-info bg-transparent' : 'border-transparent bg-info/15 text-info',
'outline' => 'text-foreground border-neutral/20',
'muted' => 'border-transparent bg-muted text-muted-foreground',
];
$sizeClasses = [
'xs' => 'px-1.5 py-0.5 text-[10px] leading-tight',
'sm' => 'px-2 py-0.5 text-[11px] leading-tight',
'md' => 'px-2.5 py-1 text-xs',
'lg' => 'px-3 py-1.5 text-sm',
'xs' => 'px-1.5 py-0.5 text-[10px]',
'sm' => 'px-2 py-0.5 text-[11px]',
'md' => 'px-2.5 py-0.5 text-xs',
'lg' => 'px-3 py-1 text-sm',
];
$iconSizes = [
'xs' => 'size-2.5',
'sm' => 'size-3',
'md' => 'size-3.5',
'lg' => 'size-4',
'md' => 'size-3',
'lg' => 'size-3.5',
];
$dotSizes = [
@@ -62,7 +62,7 @@
$dotSize = $dotSizes[$size] ?? $dotSizes['md'];
@endphp
<span {{ $attributes->class([$classes, 'gap-1.5' => $icon || $dot]) }}>
<span {{ $attributes->class([$classes, 'gap-1' => $icon || $dot]) }}>
@if($dot)
<span class="relative flex {{ $dotSize }}">
@if($pulse)
@@ -72,7 +72,7 @@
</span>
@endif
@if($icon)
<x-dynamic-component :component="$icon" class="{{ $iconSize }} opacity-80" />
<x-dynamic-component :component="$icon" class="{{ $iconSize }}" />
@endif
{{ $slot }}
</span>

View File

@@ -1,5 +1,5 @@
{{--
Button Component - TailServer Theme
Button Component - TailServer Theme (shadcn-inspired)
Usage:
<x-proxmox::ui.button>Click me</x-proxmox::ui.button>
@@ -24,33 +24,36 @@
@php
$tag = $href ? 'a' : 'button';
$baseClasses = 'group relative inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden';
$baseClasses = 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50';
$variantClasses = [
'primary' => 'bg-primary text-white border border-primary hover:bg-primary/90 hover:shadow-md hover:shadow-primary/20 active:scale-[0.98] focus:ring-primary/50',
'secondary' => 'bg-background-secondary text-base border border-neutral/30 hover:bg-background-secondary/80 hover:border-neutral/50 active:scale-[0.98] focus:ring-neutral/30',
'outline' => 'bg-transparent text-base border border-neutral/30 hover:bg-neutral/10 hover:border-neutral/50 focus:ring-neutral/30',
'ghost' => 'bg-transparent text-base hover:bg-neutral/10 focus:ring-neutral/20',
'danger' => 'bg-danger text-white border border-danger hover:bg-danger/90 hover:shadow-md hover:shadow-danger/20 active:scale-[0.98] focus:ring-danger/50',
'success' => 'bg-success text-white border border-success hover:bg-success/90 hover:shadow-md hover:shadow-success/20 active:scale-[0.98] focus:ring-success/50',
'warning' => 'bg-warning text-white border border-warning hover:bg-warning/90 hover:shadow-md hover:shadow-warning/20 active:scale-[0.98] focus:ring-warning/50',
'info' => 'bg-info text-white border border-info hover:bg-info/90 hover:shadow-md hover:shadow-info/20 active:scale-[0.98] focus:ring-info/50',
'primary' => 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
'secondary' => 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
'outline' => 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
'ghost' => 'hover:bg-accent hover:text-accent-foreground',
'danger' => 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
'success' => 'bg-success text-white shadow-sm hover:bg-success/90',
'warning' => 'bg-warning text-white shadow-sm hover:bg-warning/90',
'info' => 'bg-info text-white shadow-sm hover:bg-info/90',
'link' => 'text-primary underline-offset-4 hover:underline',
];
$sizeClasses = [
'xs' => 'px-2 py-1 text-xs',
'sm' => 'px-2.5 py-1.5 text-sm',
'md' => 'px-3.5 py-2 text-sm',
'lg' => 'px-4 py-2.5 text-base',
'xl' => 'px-5 py-3 text-lg',
'xs' => 'h-7 px-2 text-xs',
'sm' => 'h-8 px-3 text-xs',
'md' => 'h-9 px-4 py-2',
'lg' => 'h-10 px-8',
'xl' => 'h-11 px-10 text-base',
'icon' => 'h-9 w-9',
];
$iconSizes = [
'xs' => 'size-3',
'sm' => 'size-3.5',
'md' => 'size-4',
'lg' => 'size-5',
'xl' => 'size-6',
'lg' => 'size-4',
'xl' => 'size-5',
'icon' => 'size-4',
];
$classes = collect([
@@ -69,24 +72,19 @@
@if($disabled || $loading) disabled @endif
{{ $attributes->class([$classes]) }}
>
{{-- Subtle shine effect on hover --}}
<span class="absolute inset-0 -translate-x-full group-hover:translate-x-full transition-transform duration-700 bg-gradient-to-r from-transparent via-white/10 to-transparent pointer-events-none"></span>
<span class="relative inline-flex items-center gap-2">
@if($loading)
<svg class="animate-spin {{ $iconSize }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ $loadingText ?? $slot }}</span>
@else
@if($icon)
<x-dynamic-component :component="$icon" class="{{ $iconSize }} opacity-80 group-hover:opacity-100 transition-opacity" />
@endif
{{ $slot }}
@if($iconRight)
<x-dynamic-component :component="$iconRight" class="{{ $iconSize }} opacity-80 group-hover:opacity-100 transition-opacity" />
@endif
@if($loading)
<svg class="animate-spin {{ $iconSize }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ $loadingText ?? $slot }}</span>
@else
@if($icon)
<x-dynamic-component :component="$icon" class="{{ $iconSize }}" />
@endif
</span>
{{ $slot }}
@if($iconRight)
<x-dynamic-component :component="$iconRight" class="{{ $iconSize }}" />
@endif
@endif
</{{ $tag }}>

View File

@@ -1,5 +1,5 @@
{{--
Card Component - TailServer Theme
Card Component - TailServer Theme (shadcn-inspired)
Usage:
<x-proxmox::ui.card>Content</x-proxmox::ui.card>
@@ -22,42 +22,35 @@
'collapsed' => false,
])
@php
$cardClasses = 'relative flex flex-col rounded-xl border border-neutral/20 bg-background-secondary shadow-sm';
if ($hover) {
$cardClasses .= ' hover:bg-background-secondary/90 hover:border-neutral/30 hover:shadow-md transition-all duration-200';
}
if ($clickable) {
$cardClasses .= ' cursor-pointer';
}
@endphp
<div
{{ $attributes->class([$cardClasses]) }}
{{ $attributes->class([
'flex flex-col gap-6 rounded-xl border border-neutral/15 bg-card text-card-foreground shadow-sm',
'hover:shadow-md hover:border-neutral/25 transition-shadow duration-150' => $hover || $clickable,
'cursor-pointer' => $clickable,
]) }}
@if($collapsible)
x-data="{ isCollapsed: {{ $collapsed ? 'true' : 'false' }} }"
@endif
>
{{-- Subtle gradient overlay for depth --}}
<div class="absolute inset-0 rounded-xl bg-gradient-to-b from-white/[0.02] to-transparent pointer-events-none"></div>
@if($title || $actions)
<div class="flex flex-col items-start justify-between gap-4 border-b border-neutral/20 p-5 sm:flex-row sm:items-center">
<div class="@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6">
<div class="flex items-center gap-3">
@if($icon)
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-5 {{ $iconColor }}" />
<div class="flex items-center justify-center size-9 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-4 {{ $iconColor }}" />
</div>
@endif
<div>
<div class="space-y-1">
@if($title)
<h3 class="text-lg font-semibold text-base">{{ $title }}</h3>
<h3 class="leading-none font-semibold">{{ $title }}</h3>
@endif
@if($subtitle)
<p class="text-sm text-base/60 mt-0.5">{{ $subtitle }}</p>
<p class="text-sm text-muted-foreground">{{ $subtitle }}</p>
@endif
</div>
</div>
<div class="flex items-center gap-2">
@if($actions || $collapsible)
<div class="col-start-2 row-span-2 row-start-1 self-start justify-self-end flex items-center gap-2">
@if($actions)
{{ $actions }}
@endif
@@ -65,38 +58,39 @@
<button
type="button"
@click="isCollapsed = !isCollapsed"
class="p-2 rounded-lg hover:bg-neutral/10 transition-colors"
class="p-1.5 rounded-md hover:bg-neutral/10 transition-colors"
>
<x-ri-arrow-down-s-line
class="size-5 text-base/50 transition-transform duration-200"
class="size-4 text-muted-foreground transition-transform duration-150"
x-bind:class="isCollapsed ? 'rotate-180' : ''"
/>
</button>
@endif
</div>
@endif
</div>
@endif
<div
@class([
'flex-1',
'p-5' => $padding,
'px-6',
'py-6' => $padding && !$title,
])
@if($collapsible)
x-show="!isCollapsed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@endif
>
{{ $slot }}
</div>
@if($footer)
<div class="border-t border-neutral/20 px-5 py-3 {{ $footerClass }}">
<div class="flex items-center px-6 [.border-t]:pt-6 {{ $footerClass }}">
{{ $footer }}
</div>
@endif

View File

@@ -1,5 +1,5 @@
{{--
Confirm Dialog Component - TailServer Theme
Confirm Dialog Component - shadcn-style
For dangerous actions like delete, stop, etc.
Usage:
@@ -28,19 +28,19 @@
'icon' => 'ri-error-warning-line',
'iconBg' => 'bg-danger/10',
'iconColor' => 'text-danger',
'buttonBg' => 'bg-danger hover:bg-danger/90',
'buttonClass' => 'bg-danger text-danger-foreground hover:bg-danger/90',
],
'warning' => [
'icon' => 'ri-alert-line',
'iconBg' => 'bg-warning/10',
'iconColor' => 'text-warning',
'buttonBg' => 'bg-warning hover:bg-warning/90',
'buttonClass' => 'bg-warning text-warning-foreground hover:bg-warning/90',
],
'primary' => [
'icon' => 'ri-question-line',
'iconBg' => 'bg-primary/10',
'iconColor' => 'text-primary',
'buttonBg' => 'bg-primary hover:bg-primary/90',
'buttonClass' => 'bg-primary text-primary-foreground hover:bg-primary/90',
],
];
@@ -59,45 +59,45 @@
<!-- Backdrop -->
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter="ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
class="fixed inset-0 bg-black/60 backdrop-blur-md"
class="fixed inset-0 bg-black/50 backdrop-blur-sm"
></div>
<!-- Dialog -->
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95 translate-y-4"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-4"
class="relative bg-background rounded-2xl shadow-2xl ring-1 ring-neutral/10 w-full max-w-md p-6"
x-transition:enter="ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="relative rounded-lg border bg-background shadow-lg w-full max-w-md p-6"
>
<div class="flex flex-col items-center text-center">
<!-- Icon -->
<div class="p-4 rounded-2xl {{ $config['iconBg'] }} mb-4 shadow-lg">
<x-dynamic-component :component="$config['icon']" class="size-8 {{ $config['iconColor'] }}" />
<div class="p-3 rounded-full {{ $config['iconBg'] }} mb-4">
<x-dynamic-component :component="$config['icon']" class="size-6 {{ $config['iconColor'] }}" />
</div>
<!-- Title -->
<h3 class="text-lg font-semibold text-base mb-2">{{ $title }}</h3>
<h3 class="text-lg font-semibold leading-none tracking-tight mb-2">{{ $title }}</h3>
<!-- Message -->
<p class="text-sm text-base/60 mb-6">{{ $message }}</p>
<p class="text-sm text-muted-foreground mb-6">{{ $message }}</p>
<!-- Actions -->
<div class="flex gap-3 w-full">
<div class="flex gap-2 w-full">
<button
type="button"
@click="open = false"
class="flex-1 px-4 py-2.5 bg-neutral/5 hover:bg-neutral/15 rounded-xl text-sm font-medium text-base transition-all duration-200 hover:shadow-sm active:scale-95"
class="flex-1 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-150 h-9 px-4 py-2 border bg-background hover:bg-accent hover:text-accent-foreground"
>
{{ $cancelLabel }}
</button>
@@ -105,7 +105,7 @@
type="button"
@click="open = false"
{{ $attributes->whereStartsWith('wire:click') }}
class="flex-1 px-4 py-2.5 {{ $config['buttonBg'] }} rounded-xl text-sm font-medium text-white transition-all duration-200 shadow-md hover:shadow-lg active:scale-95"
class="flex-1 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-150 h-9 px-4 py-2 {{ $config['buttonClass'] }}"
>
{{ $confirmLabel }}
</button>

View File

@@ -1,5 +1,5 @@
{{--
Copy Button Component - TailServer Theme
Copy Button Component - shadcn-style
Small inline button to copy text to clipboard
Usage:
@@ -17,7 +17,7 @@
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
setTimeout(() => this.copied = false, 2000);
}
}"
class="inline-flex items-center"
@@ -26,7 +26,7 @@
<button
@click="copyToClipboard()"
type="button"
class="text-base/50 hover:text-base/70 transition-colors relative inline-flex items-center justify-center size-4"
class="text-muted-foreground hover:text-foreground transition-colors duration-150 relative inline-flex items-center justify-center size-4"
title="Copy to clipboard"
>
<x-ri-check-fill x-show="copied" x-transition x-cloak class="absolute size-4 text-success" />

View File

@@ -1,5 +1,5 @@
{{--
Copyable Text Component - TailServer Theme
Copyable Text Component - shadcn-style
Text field with copy to clipboard functionality
Usage:
@@ -14,7 +14,7 @@
<div class="space-y-2">
@if($label)
<label class="text-sm font-semibold text-base/70">{{ $label }}</label>
<label class="text-sm font-medium leading-none">{{ $label }}</label>
@endif
<div
x-data="{
@@ -24,17 +24,17 @@
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
setTimeout(() => this.copied = false, 2000);
}
}"
class="group flex items-center gap-2 border border-neutral/15 rounded-xl bg-gradient-to-r from-neutral/5 to-transparent px-3.5 py-2.5 text-sm text-base hover:border-neutral/30 transition-all duration-200 shadow-sm hover:shadow"
class="flex items-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm transition-colors duration-150"
>
@if($masked)
<span class="truncate flex-1 font-mono" x-text="showValue ? copyText : '••••••••••••'"></span>
<button
@click="showValue = !showValue"
type="button"
class="text-base/40 hover:text-primary transition-colors duration-200"
class="text-muted-foreground hover:text-foreground transition-colors duration-150"
:aria-label="showValue ? 'Hide value' : 'Show value'"
>
<x-ri-eye-line x-show="!showValue" class="size-4" />
@@ -46,7 +46,7 @@
<button
@click="copyToClipboard()"
type="button"
class="text-base/40 hover:text-primary transition-all duration-200 relative inline-flex items-center justify-center size-5 hover:scale-110 active:scale-95"
class="text-muted-foreground hover:text-foreground transition-colors duration-150 relative inline-flex items-center justify-center size-4"
aria-label="Copy to clipboard"
>
<x-ri-check-fill x-show="copied" x-transition x-cloak class="absolute size-4 text-success" />

View File

@@ -1,5 +1,5 @@
{{--
Dropdown Divider Component - TailServer Theme
Dropdown Divider Component - shadcn-style
--}}
<hr class="my-2 border-neutral/20" />
<div class="-mx-1 my-1 h-px bg-border"></div>

View File

@@ -1,5 +1,5 @@
{{--
Dropdown Item Component - TailServer Theme
Dropdown Item Component - shadcn-style
--}}
@props([
@@ -13,9 +13,9 @@
$tag = $href ? 'a' : 'button';
$variantClasses = [
'default' => 'text-base/70 hover:bg-gradient-to-r hover:from-neutral/10 hover:to-transparent hover:text-base',
'danger' => 'text-danger hover:bg-gradient-to-r hover:from-danger/10 hover:to-transparent',
'success' => 'text-success hover:bg-gradient-to-r hover:from-success/10 hover:to-transparent',
'default' => 'text-foreground hover:bg-accent hover:text-accent-foreground',
'danger' => 'text-danger focus:bg-danger/10 focus:text-danger',
'success' => 'text-success focus:bg-success/10 focus:text-success',
];
$variantClass = $variantClasses[$variant] ?? $variantClasses['default'];
@@ -26,13 +26,13 @@
@if($tag === 'button') type="button" @endif
@if($disabled) disabled @endif
{{ $attributes->class([
'group flex w-full items-center gap-2.5 px-4 py-2.5 text-sm font-medium transition-all duration-150',
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-100 w-full',
$variantClass,
'opacity-50 cursor-not-allowed' => $disabled,
'pointer-events-none opacity-50' => $disabled,
]) }}
>
@if($icon)
<x-dynamic-component :component="$icon" class="size-4 opacity-50 group-hover:opacity-80 transition-opacity" />
<x-dynamic-component :component="$icon" class="size-4" />
@endif
<span class="flex-1 text-left">{{ $slot }}</span>
</{{ $tag }}>

View File

@@ -1,5 +1,5 @@
{{--
Dropdown Component - TailServer Theme
Dropdown Component - shadcn-style
Usage:
<x-proxmox::ui.dropdown>
@@ -44,17 +44,17 @@
<div
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95 -translate-y-1"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 -translate-y-1"
class="absolute z-50 mt-2 {{ $widthClasses[$width] ?? 'w-48' }} {{ $alignClasses[$align] ?? $alignClasses['right'] }}"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-1.5 {{ $widthClasses[$width] ?? 'w-48' }} {{ $alignClasses[$align] ?? $alignClasses['right'] }}"
style="display: none;"
@click="open = false"
>
<div class="rounded-xl bg-background-secondary py-2 ring-1 ring-neutral/10 shadow-2xl border border-neutral/5 backdrop-blur-xl {{ $contentClasses }}">
<div class="rounded-md border bg-popover p-1 text-popover-foreground shadow-md {{ $contentClasses }}">
{{ $slot }}
</div>
</div>

View File

@@ -1,5 +1,5 @@
{{--
Empty State Component - TailServer Theme
Empty State Component - shadcn-style
Usage:
<x-proxmox::ui.empty-state title="No servers found" />
@@ -29,25 +29,23 @@
>
@if($icon)
<div @class([
'relative flex items-center justify-center rounded-full',
'mb-4 w-16 h-16 bg-neutral/5' => !$compact,
'mb-3 w-12 h-12 bg-neutral/5' => $compact,
'flex items-center justify-center rounded-full bg-muted',
'mb-4 size-14' => !$compact,
'mb-3 size-10' => $compact,
])>
{{-- Decorative ring --}}
<div class="absolute inset-0 rounded-full border border-dashed border-neutral/20"></div>
<x-dynamic-component
:component="$icon"
@class([
$iconClass ?? 'text-base/25',
'size-8' => !$compact,
'size-6' => $compact,
$iconClass ?? 'text-muted-foreground',
'size-7' => !$compact,
'size-5' => $compact,
])
/>
</div>
@endif
<h3 @class([
'font-semibold text-base/80',
'font-semibold tracking-tight',
'text-base' => !$compact,
'text-sm' => $compact,
])>
@@ -56,7 +54,7 @@
@if($description)
<p @class([
'text-base/50 max-w-sm leading-relaxed',
'text-muted-foreground max-w-sm',
'mt-1.5 text-sm' => !$compact,
'mt-1 text-xs' => $compact,
])>
@@ -65,7 +63,7 @@
@endif
@if($slot->isNotEmpty())
<div @class(['mt-5' => !$compact, 'mt-3' => $compact])>
<div @class(['mt-4' => !$compact, 'mt-3' => $compact])>
{{ $slot }}
</div>
@endif

View File

@@ -1,5 +1,5 @@
{{--
Help Modal Trigger Component - TailServer Theme
Help Modal Trigger Component - shadcn-style
Question mark button that opens an info modal
Usage:
@@ -19,17 +19,17 @@
])
<div class="relative" x-data="{ open: false }" x-cloak {{ $attributes }}>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<button type="button" @click="open = true" class="text-muted-foreground hover:text-foreground transition-colors duration-150">
<x-ri-question-line class="size-4" />
</button>
<!-- Modal Backdrop -->
<div
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@@ -41,38 +41,38 @@
<!-- Modal Content -->
<div
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;"
>
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-xl border bg-background shadow-lg p-6">
<button @click="open = false" class="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity duration-150 hover:opacity-100">
<x-ri-close-line class="size-4" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-5 text-primary" />
<div class="flex items-center justify-center size-9 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-4 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
<h3 class="text-lg font-semibold leading-none tracking-tight">
{{ $title }}
</h6>
</h3>
</div>
@if(isset($description))
<div class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
<div class="text-sm text-muted-foreground mb-4">
{{ $description }}
</div>
@endif
@if(isset($details))
<div class="pt-4 mt-4 border-t border-neutral/20 space-y-2.5 text-xs leading-relaxed text-base/75">
<div class="pt-4 mt-4 border-t space-y-2 text-sm text-muted-foreground">
{{ $details }}
</div>
@endif

View File

@@ -1,5 +1,5 @@
{{--
Info Row / Key-Value Pair Component - TailServer Theme
Info Row / Key-Value Pair Component - shadcn-style
Usage:
<x-proxmox::ui.info-row label="IP Address" value="192.168.1.1" />
@@ -16,8 +16,8 @@
'muted' => false,
])
<div {{ $attributes->class(['flex items-center justify-between py-2 border-b border-neutral/10 last:border-b-0']) }}>
<div class="flex items-center gap-2 text-sm text-base/60">
<div {{ $attributes->class(['flex items-center justify-between py-2 border-b last:border-b-0']) }}>
<div class="flex items-center gap-2 text-sm text-muted-foreground">
@if($icon)
<x-dynamic-component :component="$icon" class="size-4" />
@endif
@@ -26,8 +26,7 @@
<div class="flex items-center gap-2">
<span @class([
'text-sm font-medium',
'text-base/50' => $muted,
'text-base' => !$muted,
'text-muted-foreground' => $muted,
])>
{{ $value ?? $slot }}
</span>
@@ -40,7 +39,7 @@
copied = true;
setTimeout(() => copied = false, 2000);
"
class="p-1 rounded hover:bg-neutral/10 transition-colors text-base/40 hover:text-base/70"
class="rounded-sm p-1 text-muted-foreground hover:text-foreground transition-colors duration-150"
title="Copy to clipboard"
>
<x-ri-file-copy-line x-show="!copied" class="size-4" />

View File

@@ -1,5 +1,5 @@
{{--
Input Component - TailServer Theme
Input Component - shadcn-style
Usage:
<x-proxmox::ui.input name="email" label="Email" />
@@ -24,36 +24,32 @@
@php
$sizeClasses = [
'sm' => 'py-1.5 text-sm',
'md' => 'py-2 text-sm',
'lg' => 'py-2.5 text-base',
'sm' => 'h-8 text-sm',
'md' => 'h-9 text-sm',
'lg' => 'h-10 text-base',
];
$sizeClass = $sizeClasses[$size] ?? $sizeClasses['md'];
$inputClasses = 'block w-full rounded-xl border bg-background leading-6 placeholder-base/40 shadow-sm transition-all duration-200 focus:ring-2 focus:ring-offset-0 focus:shadow-md hover:border-neutral/50 hover:shadow';
$inputClasses = 'flex w-full rounded-md border bg-transparent px-3 py-1 text-sm transition-colors duration-150 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50';
if ($error) {
$inputClasses .= ' border-danger/50 focus:border-danger focus:ring-danger/25 focus:shadow-danger/10';
$inputClasses .= ' border-danger focus-visible:ring-danger';
} else {
$inputClasses .= ' border-neutral/30 focus:border-primary focus:ring-primary/25 focus:shadow-primary/10';
$inputClasses .= ' border-input';
}
if ($icon) {
$inputClasses .= ' ps-10';
} else {
$inputClasses .= ' ps-3';
$inputClasses .= ' ps-9';
}
if ($iconRight) {
$inputClasses .= ' pe-10';
} else {
$inputClasses .= ' pe-3';
$inputClasses .= ' pe-9';
}
@endphp
<div {{ $attributes->only('class')->class(['']) }}>
<div {{ $attributes->only('class')->class(['space-y-2']) }}>
@if($label)
<label @if($name) for="{{ $name }}" @endif class="block text-sm font-medium text-base/80 mb-1.5">
<label @if($name) for="{{ $name }}" @endif class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{{ $label }}
@if($required)
<span class="text-danger">*</span>
@@ -61,10 +57,10 @@
</label>
@endif
<div class="relative group/input">
<div class="relative">
@if($icon)
<div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3 text-base/40 transition-colors duration-200 group-focus-within/input:text-primary">
<x-dynamic-component :component="$icon" class="size-5" />
<div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3 text-muted-foreground">
<x-dynamic-component :component="$icon" class="size-4" />
</div>
@endif
@@ -79,15 +75,15 @@
/>
@if($iconRight)
<div class="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-3 text-base/40 transition-colors duration-200 group-focus-within/input:text-primary">
<x-dynamic-component :component="$iconRight" class="size-5" />
<div class="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-3 text-muted-foreground">
<x-dynamic-component :component="$iconRight" class="size-4" />
</div>
@endif
</div>
@if($error)
<p class="mt-1.5 text-xs text-danger">{{ $error }}</p>
<p class="text-[0.8rem] font-medium text-danger">{{ $error }}</p>
@elseif($hint)
<p class="mt-1.5 text-xs text-base/50">{{ $hint }}</p>
<p class="text-[0.8rem] text-muted-foreground">{{ $hint }}</p>
@endif
</div>

View File

@@ -1,5 +1,5 @@
{{--
Modal Component - TailServer Theme
Modal Component - shadcn-style Dialog
Usage:
<x-proxmox::ui.modal name="my-modal" title="Modal Title">Content</x-proxmox::ui.modal>
@@ -47,66 +47,67 @@
{{-- Backdrop --}}
<div
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@if($closeable) @click="show = false" @endif
class="fixed inset-0 bg-black/60 backdrop-blur-md"
class="fixed inset-0 bg-black/50 backdrop-blur-sm"
></div>
{{-- Modal Panel --}}
<div class="fixed inset-0 flex items-center justify-center p-4">
<div
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95 translate-y-4"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-4"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.stop
class="relative w-full {{ $maxWidthClass }} max-h-[90vh] overflow-y-auto bg-background-secondary rounded-2xl border border-neutral/20 shadow-2xl ring-1 ring-black/5"
class="relative w-full {{ $maxWidthClass }} max-h-[90vh] overflow-y-auto rounded-xl border bg-background-secondary shadow-lg"
>
{{-- Header --}}
@if($title || $closeable)
<div class="flex items-start justify-between gap-4 p-6 border-b border-neutral/10">
<div class="flex items-start justify-between gap-4 px-6 pt-6 pb-4">
<div class="flex items-center gap-3">
@if($icon)
<div class="flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 shadow-sm">
<x-dynamic-component :component="$icon" class="size-5 {{ $iconColor }}" /> />
<div class="flex-shrink-0 flex items-center justify-center size-9 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-4 {{ $iconColor }}" />
</div>
@endif
<div>
<div class="space-y-1">
@if($title)
<h3 class="text-lg font-semibold text-base">{{ $title }}</h3>
<h3 class="text-lg font-semibold leading-none tracking-tight">{{ $title }}</h3>
@endif
@if($subtitle)
<p class="text-sm text-base/60 mt-0.5">{{ $subtitle }}</p>
<p class="text-sm text-muted-foreground">{{ $subtitle }}</p>
@endif
</div>
</div>
@if($closeable)
<button
@click="show = false"
class="flex-shrink-0 p-2 -m-2 text-base/40 hover:text-base transition-all duration-200 rounded-xl hover:bg-neutral/10 hover:rotate-90"
class="rounded-sm opacity-70 ring-offset-background transition-opacity duration-150 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<x-ri-close-line class="size-5" />
<x-ri-close-line class="size-4" />
<span class="sr-only">Close</span>
</button>
@endif
</div>
@endif
{{-- Content --}}
<div class="p-6">
<div class="px-6 pb-6">
{{ $slot }}
</div>
{{-- Footer --}}
@if($footer)
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral/10 bg-gradient-to-t from-neutral/5 to-transparent">
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t">
{{ $footer }}
</div>
@endif

View File

@@ -1,5 +1,5 @@
{{--
Info Notice / Alert Banner Component - TailServer Theme
Info Notice / Alert Banner Component - shadcn-style
Collapsible notice banners for important information
Usage:
@@ -26,25 +26,25 @@
$variants = [
'info' => [
'bg' => 'bg-info/10',
'border' => 'border-info/30',
'border' => 'border-info/20',
'icon' => $icon ?? 'ri-information-line',
'iconColor' => 'text-info',
],
'warning' => [
'bg' => 'bg-warning/10',
'border' => 'border-warning/30',
'border' => 'border-warning/20',
'icon' => $icon ?? 'ri-alert-line',
'iconColor' => 'text-warning',
],
'danger' => [
'bg' => 'bg-danger/10',
'border' => 'border-danger/30',
'border' => 'border-danger/20',
'icon' => $icon ?? 'ri-error-warning-line',
'iconColor' => 'text-danger',
],
'success' => [
'bg' => 'bg-success/10',
'border' => 'border-success/30',
'border' => 'border-success/20',
'icon' => $icon ?? 'ri-checkbox-circle-line',
'iconColor' => 'text-success',
],
@@ -77,20 +77,20 @@
]) }}
>
<div class="flex items-start gap-3">
<x-dynamic-component :component="$config['icon']" class="size-5 {{ $config['iconColor'] }} flex-shrink-0 mt-0.5" />
<x-dynamic-component :component="$config['icon']" class="size-4 {{ $config['iconColor'] }} flex-shrink-0 mt-0.5" />
<div class="flex-1 min-w-0">
@if($title)
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-semibold text-base">{{ $title }}</h3>
<h3 class="text-sm font-medium">{{ $title }}</h3>
@if($collapsible)
<button @click="toggle()" type="button" class="text-base/60 hover:text-base transition-colors">
<button @click="toggle()" type="button" class="text-muted-foreground hover:text-foreground transition-colors duration-150">
<x-ri-arrow-down-s-line x-bind:class="isCollapsed ? 'size-4 transition-transform rotate-180' : 'size-4 transition-transform'" />
</button>
@endif
</div>
@endif
<div @if($collapsible) x-show="!isCollapsed" x-transition @endif>
<div @if($collapsible) x-show="!isCollapsed" x-transition @endif class="text-sm text-muted-foreground">
{{ $slot }}
</div>
</div>

View File

@@ -1,5 +1,5 @@
{{--
Progress Bar Component - TailServer Theme
Progress Bar Component - shadcn-style
Usage:
<x-proxmox::ui.progress :value="75" />
@@ -57,18 +57,16 @@
<div {{ $attributes->class(['w-full']) }}>
@if($showLabel || $label)
<div class="flex items-center justify-between text-xs mb-1.5">
<span class="text-base/50 font-medium">{{ $label ?? 'Usage' }}</span>
<span class="font-semibold text-base/70">{{ number_format($percent, 1) }}%</span>
<span class="text-muted-foreground font-medium">{{ $label ?? 'Usage' }}</span>
<span class="font-medium">{{ number_format($percent, 1) }}%</span>
</div>
@endif
<div class="{{ $sizeClass }} rounded-full bg-neutral/10 overflow-hidden">
<div class="relative {{ $sizeClass }} w-full overflow-hidden rounded-full bg-primary/20">
<div
@class([
$sizeClass,
'rounded-full transition-all duration-500 ease-out',
'flex-1 w-full transition-all duration-300',
$colorClass => !$gradient,
'animate-pulse' => $animated,
'bg-[length:1rem_1rem] bg-gradient-to-r from-white/20 via-transparent to-transparent' => $striped,
])
@if($gradient)
style="width: {{ $percent }}%; background-image: {{ $gradient }};"

View File

@@ -1,5 +1,5 @@
{{--
Resource Gauge Component - TailServer Theme
Resource Gauge Component - shadcn-style
Shows a circular or linear gauge for resource usage
Usage:
@@ -65,39 +65,37 @@
};
@endphp
<div {{ $attributes->class(['border border-neutral/15 rounded-2xl p-5 bg-gradient-to-br from-background to-neutral/5 shadow-sm hover:shadow-md transition-shadow duration-300 flex flex-col gap-4']) }}>
<div {{ $attributes->class(['rounded-xl border bg-card p-4 shadow-sm flex flex-col gap-4']) }}>
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold tracking-wide uppercase text-base/50">{{ $label }}</p>
<div class="space-y-0.5">
<p class="text-xs font-medium text-muted-foreground">{{ $label }}</p>
<p class="text-xl font-bold tracking-tight">
@if($prefix){{ $prefix }}@endif{{ is_numeric($value) ? number_format($value, 1) : $value }}@if($suffix){{ $suffix }}@endif
</p>
@if($sublabel)
<p class="text-xs text-base/50 mt-0.5">{{ $sublabel }}</p>
<p class="text-xs text-muted-foreground">{{ $sublabel }}</p>
@endif
</div>
<span class="text-[10px] font-semibold px-2.5 py-1 rounded-full {{ $badgeClass }} shadow-sm">
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{ $badgeClass }}">
{{ $badgeLabel }}
</span>
</div>
<div>
<div class="flex items-center justify-between text-xs text-base/50 mb-1.5">
<span>Usage</span>
<div class="space-y-1.5">
<div class="flex items-center justify-between text-xs">
<span class="text-muted-foreground">Usage</span>
<span class="font-medium">{{ number_format($percent, 1) }}%</span>
</div>
<div class="h-2.5 rounded-full bg-neutral/10 overflow-hidden shadow-inner">
<div class="h-2 rounded-full bg-primary/20 overflow-hidden">
<div
class="h-full rounded-full transition-all duration-700 ease-out relative overflow-hidden"
class="h-full rounded-full transition-all duration-300"
@if($gradient)
style="width: {{ min($percent, 130) }}%; background-image: {{ $gradient }};"
@else
style="width: {{ min($percent, 130) }}%;"
class="{{ $bgColor }}"
@endif
>
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full animate-shimmer"></div>
</div>
></div>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
{{--
Section Header Component - TailServer Theme
Section Header Component - shadcn-style
Usage:
<x-proxmox::ui.section-header title="Section Title" />
@@ -24,8 +24,8 @@
$titleSizes = [
'sm' => 'text-base font-semibold',
'md' => 'text-lg font-semibold',
'lg' => 'text-xl font-semibold',
'xl' => 'text-2xl font-bold',
'lg' => 'text-xl font-semibold tracking-tight',
'xl' => 'text-2xl font-bold tracking-tight',
];
$titleClass = $titleSizes[$size] ?? $titleSizes['lg'];
@endphp
@@ -33,29 +33,29 @@
<div {{ $attributes->class(['flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between']) }}>
<div class="flex items-start gap-3">
@if($icon)
<div class="flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-5 {{ $iconColor }}" />
<div class="flex-shrink-0 flex items-center justify-center size-9 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-4 {{ $iconColor }}" />
</div>
@endif
<div>
<div class="space-y-1">
<div class="flex items-center gap-2 min-w-0">
<h2 class="{{ $titleClass }} text-base break-words">{{ $title }}</h2>
<h2 class="{{ $titleClass }}">{{ $title }}</h2>
@if($helpText)
<div class="relative" x-data="{ open: false }" x-cloak>
<button
type="button"
@click="open = true"
class="text-base/50 hover:text-base/70 transition-colors"
class="text-muted-foreground hover:text-foreground transition-colors duration-150"
>
<x-ri-question-line class="size-4" />
</button>
{{-- Help Modal --}}
<div
x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@@ -65,29 +65,29 @@
></div>
<div
x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;"
>
<div class="relative w-full max-w-lg max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
<div class="relative w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-lg border bg-background shadow-lg p-6">
<button @click="open = false" class="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity duration-150 hover:opacity-100">
<x-ri-close-line class="size-4" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
@if($icon)
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-5 {{ $iconColor }}" />
<div class="flex items-center justify-center size-9 rounded-lg bg-primary/10">
<x-dynamic-component :component="$icon" class="size-4 {{ $iconColor }}" />
</div>
@endif
<h6 class="text-lg font-semibold">{{ $title }}</h6>
<h3 class="text-lg font-semibold leading-none tracking-tight">{{ $title }}</h3>
</div>
<div class="text-sm text-base/70 leading-relaxed">
<div class="text-sm text-muted-foreground">
{{ $helpText }}
</div>
</div>
@@ -96,7 +96,7 @@
@endif
</div>
@if($subtitle)
<p class="text-sm text-base/60 mt-0.5">{{ $subtitle }}</p>
<p class="text-sm text-muted-foreground">{{ $subtitle }}</p>
@endif
</div>
</div>

View File

@@ -1,5 +1,5 @@
{{--
Select Component - TailServer Theme
Select Component - shadcn-style
Usage:
<x-proxmox::ui.select name="country" label="Country">
@@ -20,24 +20,24 @@
@php
$sizeClasses = [
'sm' => 'py-1.5 text-sm',
'md' => 'py-2 text-sm',
'lg' => 'py-2.5 text-base',
'sm' => 'h-8 text-sm',
'md' => 'h-9 text-sm',
'lg' => 'h-10 text-base',
];
$sizeClass = $sizeClasses[$size] ?? $sizeClasses['md'];
$selectClasses = 'block w-full rounded-xl border bg-background px-3 leading-6 shadow-sm transition-all duration-200 focus:ring-2 focus:ring-offset-0 focus:shadow-md hover:border-neutral/50 hover:shadow appearance-none cursor-pointer';
$selectClasses = 'flex w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm ring-offset-background transition-colors duration-150 placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none cursor-pointer';
if ($error) {
$selectClasses .= ' border-danger/50 focus:border-danger focus:ring-danger/25 focus:shadow-danger/10';
$selectClasses .= ' border-danger focus:ring-danger';
} else {
$selectClasses .= ' border-neutral/30 focus:border-primary focus:ring-primary/25 focus:shadow-primary/10';
$selectClasses .= ' border-input';
}
@endphp
<div {{ $attributes->only('class')->class(['']) }}>
<div {{ $attributes->only('class')->class(['space-y-2']) }}>
@if($label)
<label @if($name) for="{{ $name }}" @endif class="block text-sm font-medium text-base/80 mb-1.5">
<label @if($name) for="{{ $name }}" @endif class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{{ $label }}
@if($required)
<span class="text-danger">*</span>
@@ -45,7 +45,7 @@
</label>
@endif
<div class="relative group/select">
<div class="relative">
<select
@if($name) name="{{ $name }}" id="{{ $name }}" @endif
@if($disabled) disabled @endif
@@ -54,14 +54,14 @@
>
{{ $slot }}
</select>
<div class="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-3 text-base/40 transition-all duration-200 group-focus-within/select:text-primary group-hover/select:text-base/60">
<x-ri-arrow-down-s-line class="size-5 transition-transform duration-200 group-focus-within/select:rotate-180" />
<div class="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-3 text-muted-foreground">
<x-ri-arrow-down-s-line class="size-4 opacity-50" />
</div>
</div>
@if($error)
<p class="mt-1.5 text-xs text-danger">{{ $error }}</p>
<p class="text-[0.8rem] font-medium text-danger">{{ $error }}</p>
@elseif($hint)
<p class="mt-1.5 text-xs text-base/50">{{ $hint }}</p>
<p class="text-[0.8rem] text-muted-foreground">{{ $hint }}</p>
@endif
</div>

View File

@@ -1,5 +1,5 @@
{{--
Server Status Indicator Component - TailServer Theme
Server Status Indicator Component - shadcn-style
Usage:
<x-proxmox::ui.server-status status="running" />
@@ -65,7 +65,7 @@
'warning' => 'text-warning',
'danger' => 'text-danger',
'info' => 'text-info',
'muted' => 'text-base/40',
'muted' => 'text-muted-foreground',
];
$bgClasses = [
@@ -73,7 +73,7 @@
'warning' => 'bg-warning/10',
'danger' => 'bg-danger/10',
'info' => 'bg-info/10',
'muted' => 'bg-neutral/10',
'muted' => 'bg-muted',
];
$dotColorClasses = [
@@ -81,7 +81,7 @@
'warning' => 'bg-warning',
'danger' => 'bg-danger',
'info' => 'bg-info',
'muted' => 'bg-base/40',
'muted' => 'bg-muted-foreground',
];
$sizeClasses = [
@@ -91,9 +91,9 @@
];
$dotSizes = [
'sm' => 'h-1.5 w-1.5',
'md' => 'h-2 w-2',
'lg' => 'h-2.5 w-2.5',
'sm' => 'size-1.5',
'md' => 'size-2',
'lg' => 'size-2.5',
];
$textSizes = [
@@ -120,7 +120,7 @@
{{-- Full badge variant --}}
<div
{{ $attributes->class([
'inline-flex items-center font-semibold rounded-lg',
'inline-flex items-center font-medium rounded-md',
$bgClasses[$config['color']] ?? $bgClasses['muted'],
$colorClasses[$config['color']] ?? $colorClasses['muted'],
$sizeClasses[$size] ?? $sizeClasses['md'],

View File

@@ -1,5 +1,5 @@
{{--
Spinner Component - TailServer Theme
Spinner Component - shadcn-style
Usage:
<x-proxmox::ui.spinner />
@@ -15,9 +15,9 @@
$sizeClasses = [
'xs' => 'size-3',
'sm' => 'size-4',
'md' => 'size-6',
'lg' => 'size-8',
'xl' => 'size-12',
'md' => 'size-5',
'lg' => 'size-6',
'xl' => 'size-8',
];
$colorClasses = [
@@ -28,7 +28,7 @@
'warning' => 'text-warning',
'danger' => 'text-danger',
'info' => 'text-info',
'muted' => 'text-base/50',
'muted' => 'text-muted-foreground',
];
$sizeClass = $sizeClasses[$size] ?? $sizeClasses['md'];
@@ -36,11 +36,11 @@
@endphp
<svg
{{ $attributes->class(['animate-spin drop-shadow-sm', $sizeClass, $colorClass]) }}
{{ $attributes->class(['animate-spin', $sizeClass, $colorClass]) }}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-80" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>

View File

@@ -1,5 +1,5 @@
{{--
Stat Card Component - TailServer Theme
Stat Card Component - TailServer Theme (shadcn-inspired)
Usage:
<x-proxmox::ui.stat-card value="12" label="CPU %" />
@@ -13,7 +13,7 @@
'suffix' => null,
'prefix' => null,
'icon' => null,
'iconClass' => 'text-primary/30',
'iconClass' => 'text-muted-foreground',
'status' => null,
'statusText' => null,
'href' => null,
@@ -21,54 +21,61 @@
'progressColor' => 'bg-primary',
'clickable' => false,
'compact' => false,
'change' => null,
'changeLabel' => null,
])
@php
$tag = $href ? 'a' : 'div';
$statusClasses = [
'success' => 'text-success bg-success/10',
'warning' => 'text-warning bg-warning/10',
'danger' => 'text-danger bg-danger/10',
'info' => 'text-info bg-info/10',
'muted' => 'text-base/50 bg-neutral/10',
'success' => 'text-success',
'warning' => 'text-warning',
'danger' => 'text-danger',
'info' => 'text-info',
'muted' => 'text-muted-foreground',
];
$statusClass = $statusClasses[$status] ?? $statusClasses['muted'];
$statusClass = $statusClasses[$status] ?? '';
@endphp
<{{ $tag }}
@if($href) href="{{ $href }}" @endif
{{ $attributes->class([
'group relative flex flex-col rounded-xl border border-neutral/20 bg-background-secondary overflow-hidden',
'hover:bg-background-secondary/80 hover:border-neutral/30 hover:shadow-sm cursor-pointer transition-all duration-200' => $href || $clickable,
'flex flex-col gap-6 rounded-xl border border-neutral/15 bg-card py-6 text-card-foreground shadow-sm',
'hover:shadow-md transition-shadow duration-150 cursor-pointer' => $href || $clickable,
]) }}
>
{{-- Subtle gradient overlay --}}
<div class="absolute inset-0 bg-gradient-to-br from-primary/[0.02] to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
<div @class(['relative flex grow items-center justify-between', 'p-5' => !$compact, 'p-4' => $compact])>
<dl class="min-w-0">
<dt @class(['font-bold text-base', 'text-2xl' => !$compact, 'text-xl' => $compact])>
@if($prefix)<span class="text-base/40">{{ $prefix }}</span>@endif{{ $value }}@if($suffix)<span @class(['text-base/40', 'text-lg' => !$compact, 'text-base' => $compact])>{{ $suffix }}</span>@endif
</dt>
<dd @class(['font-medium text-base/50', 'text-sm' => !$compact, 'text-xs' => $compact])>{{ $label }}</dd>
</dl>
<div class="flex flex-row items-center justify-between space-y-0 px-6 pb-2">
<h3 @class(['text-sm font-medium text-muted-foreground'])>{{ $label }}</h3>
@if($icon)
<div @class(['flex items-center', $iconClass, 'opacity-60 group-hover:opacity-80 transition-opacity'])>
<x-dynamic-component :component="$icon" @class(['size-10' => !$compact, 'size-8' => $compact]) />
</div>
<x-dynamic-component :component="$icon" class="size-4 {{ $iconClass }}" />
@endif
</div>
<div class="px-6">
<div @class(['font-bold', 'text-2xl' => !$compact, 'text-xl' => $compact])>
@if($prefix)<span class="text-muted-foreground">{{ $prefix }}</span>@endif{{ $value }}@if($suffix)<span class="text-muted-foreground text-lg">{{ $suffix }}</span>@endif
</div>
@if($change !== null || $changeLabel)
<p class="text-xs text-muted-foreground mt-1">
@if($change !== null)
<span @class(['font-medium', $change >= 0 ? 'text-success' : 'text-danger'])>
{{ $change >= 0 ? '+' : '' }}{{ $change }}%
</span>
@endif
{{ $changeLabel ?? 'from last period' }}
</p>
@endif
</div>
@if($progress !== null)
<div class="px-5 pb-4">
<div class="px-6 pb-2">
<div class="h-1.5 w-full rounded-full bg-neutral/10 overflow-hidden">
<div class="h-full {{ $progressColor }} rounded-full transition-all duration-500" style="width: {{ min(100, $progress) }}%"></div>
<div class="h-full {{ $progressColor }} rounded-full transition-all duration-300" style="width: {{ min(100, $progress) }}%"></div>
</div>
</div>
@endif
@if($statusText)
<div class="border-t border-neutral/20 px-5 py-2.5 text-xs font-semibold {{ $statusClass }}">
<div class="border-t border-neutral/10 px-6 pt-4 text-xs font-medium {{ $statusClass }}">
{{ $statusText }}
</div>
@endif

View File

@@ -1,5 +1,5 @@
{{--
Tab Panel Component - TailServer Theme
Tab Panel Component - shadcn-style
--}}
@props([
@@ -8,10 +8,10 @@
<div
x-show="activeTab === '{{ $name }}'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
{{ $attributes }}
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
{{ $attributes->class(['ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2']) }}
>
{{ $slot }}
</div>

View File

@@ -1,5 +1,5 @@
{{--
Table Row Component - TailServer Theme
Table Row Component - shadcn-style
--}}
@props([
@@ -16,8 +16,9 @@
@if($href)
<tr
{{ $attributes->class([
'border-b border-neutral/10 cursor-pointer transition-colors duration-150',
'hover:bg-primary/5' => $isHoverable,
'border-b transition-colors cursor-pointer',
'hover:bg-muted/50' => $isHoverable,
'data-[state=selected]:bg-muted',
]) }}
onclick="window.location='{{ $href }}'"
>
@@ -26,9 +27,10 @@
@else
<tr
{{ $attributes->class([
'border-b border-neutral/10 transition-colors duration-150',
'hover:bg-neutral/5' => $isHoverable,
'cursor-pointer hover:bg-primary/5' => $clickable,
'border-b transition-colors',
'hover:bg-muted/50' => $isHoverable,
'cursor-pointer' => $clickable,
'data-[state=selected]:bg-muted',
]) }}
>
{{ $slot }}

View File

@@ -1,5 +1,5 @@
{{--
Table Data Cell Component - TailServer Theme
Table Data Cell Component - shadcn-style
--}}
@props([
@@ -11,20 +11,20 @@
@php
$alignClasses = [
'start' => 'text-start',
'start' => 'text-left',
'center' => 'text-center',
'end' => 'text-end',
'end' => 'text-right',
];
$alignClass = $alignClasses[$align] ?? $alignClasses['start'];
@endphp
<td
{{ $attributes->class([
'px-3 py-3',
'p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
$alignClass,
'whitespace-nowrap' => $nowrap,
'text-base/50' => $muted,
'font-semibold text-base' => $strong,
'text-muted-foreground' => $muted,
'font-medium' => $strong,
]) }}
>
{{ $slot }}

View File

@@ -1,5 +1,5 @@
{{--
Table Header Cell Component - TailServer Theme
Table Header Cell Component - shadcn-style
--}}
@props([
@@ -12,9 +12,9 @@
@php
$alignClasses = [
'start' => 'text-start',
'start' => 'text-left',
'center' => 'text-center',
'end' => 'text-end',
'end' => 'text-right',
];
$alignClass = $alignClasses[$align] ?? $alignClasses['start'];
@@ -23,9 +23,9 @@
<th
{{ $attributes->class([
'px-3 py-3 text-xs font-semibold tracking-wider text-base/60 uppercase',
'h-10 px-4 font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
$alignClass,
'cursor-pointer hover:text-base hover:bg-neutral/5 select-none transition-colors duration-150' => $sortable,
'cursor-pointer select-none hover:text-foreground transition-colors duration-150' => $sortable,
]) }}
@if($sortable && $sortKey)
wire:click="sort('{{ $sortKey }}')"
@@ -34,7 +34,7 @@
<div class="flex items-center gap-1.5 @if($align === 'end') justify-end @elseif($align === 'center') justify-center @endif">
<span>{{ $slot }}</span>
@if($sortable)
<span class="text-base/40">
<span class="text-muted-foreground">
@if($isSorted)
@if($currentDirection === 'asc')
<x-ri-arrow-up-s-line class="size-4" />

View File

@@ -1,5 +1,5 @@
{{--
Table Component - TailServer Theme
Table Component - shadcn-style
Usage:
<x-proxmox::ui.table>
@@ -21,31 +21,27 @@
'compact' => false,
])
@php
$wrapperClasses = $responsive ? 'min-w-full overflow-x-auto rounded-xl border border-neutral/10' : '';
@endphp
<div class="{{ $wrapperClasses }}">
<div @class(['relative w-full overflow-auto' => $responsive])>
<table
{{ $attributes->class([
'min-w-full align-middle text-sm',
'[&_tbody_tr:nth-child(even)]:bg-neutral/3' => $striped,
'w-full caption-bottom text-sm',
'[&_tbody_tr:nth-child(even)]:bg-muted/50' => $striped,
]) }}
data-hoverable="{{ $hoverable ? 'true' : 'false' }}"
data-compact="{{ $compact ? 'true' : 'false' }}"
>
@if(isset($head))
<thead class="bg-gradient-to-b from-neutral/5 to-transparent">
<tr class="border-b border-neutral/15">
<thead class="[&_tr]:border-b">
<tr class="border-b transition-colors">
{{ $head }}
</tr>
</thead>
@endif
<tbody>
<tbody class="[&_tr:last-child]:border-0">
{{ $slot }}
</tbody>
@if(isset($foot))
<tfoot>
<tfoot class="border-t bg-muted/50 font-medium [&>tr]:last:border-b-0">
{{ $foot }}
</tfoot>
@endif

View File

@@ -1,5 +1,5 @@
{{--
Tabs Component - TailServer Theme
Tabs Component - shadcn-style
Usage:
<x-proxmox::ui.tabs :tabs="['Overview', 'Settings', 'Logs']" active="Overview">
@@ -18,22 +18,22 @@
@php
$variantClasses = [
'default' => [
'container' => 'border-b border-neutral/15',
'tab' => 'px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-all duration-200',
'active' => 'text-primary border-primary',
'inactive' => 'text-base/50 border-transparent hover:text-base hover:border-neutral/30',
'container' => 'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
'tab' => 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'active' => 'bg-background text-foreground shadow',
'inactive' => 'hover:bg-background/50 hover:text-foreground',
],
'pills' => [
'container' => 'flex gap-1.5 p-1.5 bg-neutral/5 rounded-xl border border-neutral/10',
'tab' => 'px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
'active' => 'bg-background-secondary text-base shadow-md',
'inactive' => 'text-base/50 hover:text-base hover:bg-neutral/10',
'container' => 'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
'tab' => 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all duration-150',
'active' => 'bg-background text-foreground shadow',
'inactive' => 'hover:bg-background/50 hover:text-foreground',
],
'buttons' => [
'container' => 'flex gap-2',
'tab' => 'px-4 py-2.5 text-sm font-medium rounded-xl border transition-all duration-200',
'active' => 'bg-primary text-white border-primary shadow-md shadow-primary/25',
'inactive' => 'bg-background-secondary text-base/60 border-neutral/20 hover:border-neutral/40 hover:text-base hover:shadow-sm',
'tab' => 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium transition-colors duration-150',
'active' => 'bg-primary text-primary-foreground shadow',
'inactive' => 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
],
];
@@ -47,32 +47,30 @@
>
{{-- Tab Navigation --}}
<div class="{{ $styles['container'] }}">
<nav class="flex @if($variant === 'default') -mb-px @endif">
@foreach($tabs as $tab)
@php
$tabName = is_array($tab) ? ($tab['name'] ?? $tab['label'] ?? '') : $tab;
$tabLabel = is_array($tab) ? ($tab['label'] ?? $tab['name'] ?? '') : $tab;
$tabIcon = is_array($tab) ? ($tab['icon'] ?? null) : null;
$tabBadge = is_array($tab) ? ($tab['badge'] ?? null) : null;
@endphp
<button
type="button"
@click="activeTab = '{{ $tabName }}'"
:class="activeTab === '{{ $tabName }}' ? '{{ $styles['active'] }}' : '{{ $styles['inactive'] }}'"
class="{{ $styles['tab'] }} flex items-center gap-2"
>
@if($tabIcon)
<x-dynamic-component :component="$tabIcon" class="size-4" />
@endif
<span>{{ $tabLabel }}</span>
@if($tabBadge)
<span class="px-1.5 py-0.5 text-xs font-semibold rounded-full bg-primary/10 text-primary">
{{ $tabBadge }}
</span>
@endif
</button>
@endforeach
</nav>
@foreach($tabs as $tab)
@php
$tabName = is_array($tab) ? ($tab['name'] ?? $tab['label'] ?? '') : $tab;
$tabLabel = is_array($tab) ? ($tab['label'] ?? $tab['name'] ?? '') : $tab;
$tabIcon = is_array($tab) ? ($tab['icon'] ?? null) : null;
$tabBadge = is_array($tab) ? ($tab['badge'] ?? null) : null;
@endphp
<button
type="button"
@click="activeTab = '{{ $tabName }}'"
:class="activeTab === '{{ $tabName }}' ? '{{ $styles['active'] }}' : '{{ $styles['inactive'] }}'"
class="{{ $styles['tab'] }} gap-2"
>
@if($tabIcon)
<x-dynamic-component :component="$tabIcon" class="size-4" />
@endif
<span>{{ $tabLabel }}</span>
@if($tabBadge)
<span class="ml-1.5 rounded-full bg-primary/10 px-1.5 py-0.5 text-xs font-medium text-primary">
{{ $tabBadge }}
</span>
@endif
</button>
@endforeach
</div>
{{-- Tab Panels --}}

View File

@@ -1,5 +1,5 @@
{{--
Toggle/Switch Component - TailServer Theme
Toggle/Switch Component - shadcn-style
Usage:
<x-proxmox::ui.toggle wire:model="enabled" label="Enable Feature" />
@@ -11,14 +11,14 @@
'disabled' => false,
])
<label {{ $attributes->only('class')->class(['flex items-center justify-between gap-4 cursor-pointer group']) }}>
<label {{ $attributes->only('class')->class(['flex items-center justify-between gap-4 cursor-pointer']) }}>
@if($label || $description)
<div class="flex flex-col">
<div class="space-y-0.5">
@if($label)
<span class="text-sm font-medium text-base transition-colors duration-200 group-hover:text-primary">{{ $label }}</span>
<span class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{{ $label }}</span>
@endif
@if($description)
<span class="text-xs text-base/50 transition-colors duration-200 group-hover:text-base/70">{{ $description }}</span>
<p class="text-[0.8rem] text-muted-foreground">{{ $description }}</p>
@endif
</div>
@endif
@@ -30,16 +30,14 @@
class="sr-only peer"
/>
<div @class([
'w-11 h-6 rounded-full transition-all duration-300 ease-out',
'bg-neutral/20 peer-checked:bg-primary peer-checked:shadow-md peer-checked:shadow-primary/30',
'peer-focus:ring-2 peer-focus:ring-primary/25 peer-focus:ring-offset-2 peer-focus:ring-offset-background',
'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed',
'group-hover:bg-neutral/30 peer-checked:group-hover:bg-primary/90',
'inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-150',
'bg-input peer-checked:bg-primary',
'peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background',
'peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
])></div>
<div @class([
'absolute top-0.5 left-0.5 size-5 bg-white rounded-full shadow-md transition-all duration-300 ease-out',
'peer-checked:translate-x-5 peer-checked:shadow-lg',
'group-hover:scale-105 peer-active:scale-95',
'pointer-events-none absolute top-0.5 left-0.5 block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform duration-150',
'peer-checked:translate-x-4',
])></div>
</div>
</label>

View File

@@ -1,5 +1,5 @@
{{--
Tooltip Component - TailServer Theme
Tooltip Component - shadcn-style
Usage:
<x-proxmox::ui.tooltip text="This is a tooltip">
@@ -19,13 +19,6 @@
'left' => 'right-full top-1/2 -translate-y-1/2 mr-2',
'right' => 'left-full top-1/2 -translate-y-1/2 ml-2',
];
$arrows = [
'top' => 'top-full left-1/2 -translate-x-1/2 border-t-zinc-800 border-x-transparent border-b-transparent border-4',
'bottom' => 'bottom-full left-1/2 -translate-x-1/2 border-b-zinc-800 border-x-transparent border-t-transparent border-4',
'left' => 'left-full top-1/2 -translate-y-1/2 border-l-zinc-800 border-y-transparent border-r-transparent border-4',
'right' => 'right-full top-1/2 -translate-y-1/2 border-r-zinc-800 border-y-transparent border-l-transparent border-4',
];
@endphp
<div
@@ -40,17 +33,16 @@
<div
x-show="show"
x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute z-50 {{ $positions[$position] ?? $positions['top'] }} pointer-events-none"
>
<div class="relative bg-zinc-900 text-white text-xs font-medium rounded-lg px-2.5 py-1.5 whitespace-nowrap shadow-xl ring-1 ring-white/10">
<div class="overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground">
{{ $text }}
<div class="absolute {{ $arrows[$position] ?? $arrows['top'] }}"></div>
</div>
</div>
</div>

View File

@@ -1,46 +1,46 @@
{{-- Server Card Component - TailServer Theme --}}
{{-- Server Card Component - TailServer Theme (shadcn-inspired) --}}
<div class="space-y-4">
@if($status)
<a href="{{ route('proxmox.server.overview', ['server' => $server->id]) }}" wire:navigate>
<x-proxmox::ui.card class="hover:border-primary/30 transition-colors cursor-pointer">
<div class="flex flex-col rounded-xl border border-neutral/15 bg-card text-card-foreground shadow-sm hover:shadow-md hover:border-neutral/25 transition-shadow duration-150 cursor-pointer">
{{-- Header: Server Name & Status --}}
<div class="flex items-center justify-between mb-4">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center gap-3">
@if($server->user_set_icon)
@php
$iconColor = $server->user_set_icon_color ?? '#6366f1';
$bgColor = $iconColor . '33';
$bgColor = $iconColor . '20';
$iconPrefix = $server->user_set_icon_prefix ?? 'ri';
$iconName = $server->user_set_icon;
$componentName = $iconPrefix . '-' . $iconName;
@endphp
<div class="p-2 rounded-lg" style="background-color: {{ $bgColor }};">
<x-dynamic-component :component="$componentName" class="size-5" style="color: {{ $iconColor }};" />
<div class="flex items-center justify-center size-9 rounded-lg" style="background-color: {{ $bgColor }};">
<x-dynamic-component :component="$componentName" class="size-4" style="color: {{ $iconColor }};" />
</div>
@else
<div class="bg-primary/10 p-2 rounded-lg">
<x-ri-server-line class="size-5 text-primary" />
<div class="flex items-center justify-center size-9 rounded-lg bg-primary/10">
<x-ri-server-line class="size-4 text-primary" />
</div>
@endif
<span class="font-semibold text-base">{{ $status->name }}</span>
<span class="font-semibold">{{ $status->name }}</span>
</div>
<x-proxmox::ui.server-status :status="$status->status" showLabel />
</div>
{{-- Quick Info Row --}}
<div class="flex flex-row flex-wrap gap-6 py-3 border-y border-neutral/10">
<div class="flex flex-row flex-wrap gap-6 px-6 py-3 border-y border-neutral/10">
<div class="flex flex-col min-w-[120px]">
<span class="text-xs font-medium text-base/50 uppercase tracking-wide">IPv4 Address</span>
<span class="text-sm text-base mt-0.5">{{ $server->primaryIpv4->ip ?? 'None' }}</span>
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">IPv4 Address</span>
<span class="text-sm mt-0.5">{{ $server->primaryIpv4->ip ?? 'None' }}</span>
</div>
<div class="flex flex-col min-w-[120px]">
<span class="text-xs font-medium text-base/50 uppercase tracking-wide">IPv6 Address</span>
<span class="text-sm text-base mt-0.5">{{ $server->primaryIpv6->ip ?? 'None' }}</span>
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">IPv6 Address</span>
<span class="text-sm mt-0.5">{{ $server->primaryIpv6->ip ?? 'None' }}</span>
</div>
@if($status->status == 'running' && isset($status->uptime))
<div class="flex flex-col min-w-[100px]">
<span class="text-xs font-medium text-base/50 uppercase tracking-wide">Uptime</span>
<span class="text-sm text-base mt-0.5">
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Uptime</span>
<span class="text-sm mt-0.5">
@php
$uptime = $status->uptime;
$days = floor($uptime / 86400);
@@ -57,8 +57,8 @@
</div>
@endif
<div class="hidden sm:flex flex-col min-w-[100px]">
<span class="text-xs font-medium text-base/50 uppercase tracking-wide">Memory</span>
<span class="text-sm text-base mt-0.5">
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Memory</span>
<span class="text-sm mt-0.5">
@if($status->status == 'running')
{{ Illuminate\Support\Number::fileSize(bytes: $status->mem) }}/
@endif
@@ -66,72 +66,69 @@
</span>
</div>
<div class="hidden sm:flex flex-col min-w-[80px]">
<span class="text-xs font-medium text-base/50 uppercase tracking-wide">Disk</span>
<span class="text-sm text-base mt-0.5">{{ Illuminate\Support\Number::fileSize(bytes: $status->maxdisk) }}</span>
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Disk</span>
<span class="text-sm mt-0.5">{{ Illuminate\Support\Number::fileSize(bytes: $status->maxdisk) }}</span>
</div>
@if($status->status == 'running')
<div class="hidden sm:flex flex-col min-w-[60px]">
<span class="text-xs font-medium text-base/50 uppercase tracking-wide">CPU</span>
<span class="text-sm text-base mt-0.5">{{ round($status->cpu * 100, 2) }}%</span>
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">CPU</span>
<span class="text-sm mt-0.5">{{ round($status->cpu * 100, 2) }}%</span>
</div>
@endif
</div>
{{-- Server Configuration Grid --}}
<div class="mt-4">
<div class="text-xs font-semibold text-base/60 uppercase tracking-wide mb-3">Server Configuration</div>
<div class="px-6 py-4">
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Server Configuration</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<x-proxmox::ui.stat-card
label="CPU Cores"
:value="($status->cpus ?? 'N/A') . ' ' . (($status->cpus ?? 0) > 1 ? 'Cores' : 'Core')"
icon="ri-cpu-line"
variant="default"
/>
<x-proxmox::ui.stat-card
label="RAM Allocation"
:value="round(($status->maxmem ?? 0) / (1024 * 1024 * 1024), 2) . ' GiB'"
icon="ri-ram-line"
variant="default"
/>
<x-proxmox::ui.stat-card
label="Disk Capacity"
:value="Illuminate\Support\Number::fileSize(bytes: $status->maxdisk ?? 0)"
icon="ri-hard-drive-2-line"
variant="default"
/>
<div class="flex flex-col p-3 bg-neutral/5 rounded-lg">
<div class="flex items-center gap-2 mb-1">
<x-ri-speed-line class="size-4 text-base/50" />
<span class="text-xs text-base/50">Bandwidth</span>
<div class="flex flex-col gap-6 rounded-xl border border-neutral/15 bg-card py-6 text-card-foreground shadow-sm">
<div class="flex flex-row items-center justify-between space-y-0 px-6 pb-2">
<h3 class="text-sm font-medium text-muted-foreground">Bandwidth</h3>
<x-ri-speed-line class="size-4 text-muted-foreground" />
</div>
<div class="px-6">
<div class="text-xl font-bold">{{ $this->monthlyTraffic }} GiB</div>
<p class="text-xs text-muted-foreground mt-1">
of {{ $this->bandwidthThreshold95 }} GiB (95% threshold)
</p>
</div>
<span class="text-sm font-medium text-base">
{{ $this->monthlyTraffic }} GiB / {{ $this->bandwidthThreshold95 }} GiB
</span>
<span class="text-xs text-base/50 mt-1 leading-relaxed">
95% threshold or 10 GiB min. Reduced to 1 MiB/s if exceeded.
</span>
</div>
</div>
</div>
</x-proxmox::ui.card>
</div>
</a>
@else
{{-- Error State: Status unavailable --}}
<a href="{{ route('proxmox.server.overview', ['server' => $server->id]) }}" wire:navigate>
<x-proxmox::ui.card class="border-danger/30 hover:border-danger/50 transition-colors cursor-pointer">
<div class="flex items-center justify-between mb-2">
<div class="flex flex-col rounded-xl border border-danger/20 bg-card text-card-foreground shadow-sm hover:shadow-md hover:border-danger/30 transition-shadow duration-150 cursor-pointer">
<div class="flex items-center justify-between px-6 py-4">
<div class="flex items-center gap-3">
<div class="bg-danger/10 p-2 rounded-lg">
<x-ri-server-line class="size-5 text-danger" />
<div class="flex items-center justify-center size-9 rounded-lg bg-danger/10">
<x-ri-server-line class="size-4 text-danger" />
</div>
<span class="font-semibold text-base">{{ $server->hostname ?? 'Server #' . $server->id }}</span>
<span class="font-semibold">{{ $server->hostname ?? 'Server #' . $server->id }}</span>
</div>
<x-proxmox::ui.badge variant="danger" icon="ri-error-warning-fill">
Unavailable
</x-proxmox::ui.badge>
</div>
<p class="text-sm text-base/60 mt-2">Unable to retrieve server status. Click to view details or try again.</p>
</x-proxmox::ui.card>
<p class="text-sm text-muted-foreground px-6 pb-4">Unable to retrieve server status. Click to view details or try again.</p>
</div>
</a>
@endif
</div>