Enhance UI transitions and loading effects across Proxmox views

- Implemented page load animations using Alpine.js for smoother transitions in various views including firewall, help, index, network, overview, reinstall, server, statistics, storage, subdomain, and system.
- Added opacity and translate effects to elements for improved visual feedback during loading.
- Enhanced hover effects on cards and buttons for better user interaction.
- Adjusted transition timings and delays for a more cohesive user experience.
This commit is contained in:
2025-12-17 19:50:05 -05:00
parent 801898e295
commit a7cb561053
21 changed files with 307 additions and 170 deletions

View File

@@ -1,6 +1,8 @@
<x-proxmox::layout :server="$server" view="backups" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="space-y-4" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
<div class="flex items-center justify-between"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold">Backups</h2>
<div class="relative" x-data="{ open: false }" x-cloak>
@@ -37,7 +39,9 @@
</div>
</div>
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-sm">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.15s;">
<div class="overflow-x-auto rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead>
@@ -49,9 +53,11 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 text-gray-700 dark:text-gray-300">
@forelse($backups as $backup)
@forelse($backups as $index => $backup)
@php $backup = (object) $backup; @endphp
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200"
:class="pageLoaded ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-2'"
:style="'transition-delay: {{ ($index + 2) * 75 }}ms'">
<td class="px-3 py-3"><span class="font-medium text-gray-800 dark:text-gray-200">{{ $backup->notes ?: '—' }}</span></td>
<td class="px-3 py-3">{{ Illuminate\Support\Number::fileSize($backup->size, 2) }}</td>
<td class="px-3 py-3">{{ date('Y-m-d H:i', $backup->ctime) }}</td>

View File

@@ -35,8 +35,12 @@
}
}
</style>
<div class="space-y-4 lg:space-y-6 px-2 sm:px-0 overflow-x-hidden">
<div class="bg-info/10 border border-info/30 rounded-lg p-3 lg:p-4"
<div class="space-y-4 lg:space-y-6 px-2 sm:px-0 overflow-x-hidden"
x-data="{ pageLoaded: false }"
x-init="setTimeout(() => pageLoaded = true, 50)">
<div class="bg-info/10 border border-info/30 rounded-lg p-3 lg:p-4 transition-all duration-300"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;"
x-data="{
isCollapsed: localStorage.getItem('boot-order-notice-collapsed') === 'true',
toggle() {
@@ -71,7 +75,9 @@
</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.1s;">
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-lg sm:text-xl font-semibold break-words">Boot Order</h2>
<div class="relative" x-data="{ open: false }" x-cloak>

View File

@@ -17,8 +17,10 @@
width: 100%;
}
</style>
<div class="space-y-6">
<div class="bg-info/10 border border-info/30 rounded-lg p-4"
<div class="space-y-6" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
<div class="bg-info/10 border border-info/30 rounded-lg p-4 transition-all duration-300"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;"
x-data="{
isCollapsed: localStorage.getItem('cloud-init-notice-collapsed') === 'true',
toggle() {
@@ -92,7 +94,9 @@
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-4 space-y-4"
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-4 space-y-4 transition-all duration-300 hover:shadow-md"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.15s;"
x-data="{ showNetworkModal: @entangle('showNetworkModal') }">
<div class="flex items-center justify-between">
<div>

View File

@@ -61,7 +61,7 @@
role="alert"
{{ $attributes->class([
$componentClass,
'lte-fade-in',
'lte-fade-in transition-all duration-300 hover:shadow-sm',
$dismissible ? 'lte-alert-dismissible' : '',
]) }}
@if($dismissible || $collapsible)
@@ -70,16 +70,16 @@
@if($collapsible) collapsed: {{ $collapsed ? 'true' : 'false' }} @endif
}"
x-show="show"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-y-2 scale-95"
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 scale-95"
@endif
>
<div class="flex gap-3 items-start">
<x-dynamic-component :component="$config['icon']" class="size-5 mt-0.5 shrink-0" />
<x-dynamic-component :component="$config['icon']" class="size-5 mt-0.5 shrink-0 transition-transform duration-200 hover:scale-110" />
<div class="flex-1 min-w-0">
@if($title)
<h5 style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;">{{ $title }}</h5>
@@ -87,9 +87,12 @@
<div
@if($collapsible)
x-show="!collapsed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@endif
>
{{ $slot }}
@@ -102,11 +105,11 @@
<button
type="button"
@click="collapsed = !collapsed"
class="lte-btn lte-btn-sm lte-btn-outline-secondary"
class="lte-btn lte-btn-sm lte-btn-outline-secondary transition-all duration-200 hover:scale-105"
style="padding: 0.25rem;"
>
<x-ri-arrow-down-s-line
class="size-4 transition-transform duration-200"
class="size-4 transition-transform duration-300 ease-out"
x-bind:class="collapsed ? 'rotate-180' : ''"
/>
</button>
@@ -115,7 +118,7 @@
<button
type="button"
@click="show = false"
class="lte-btn-close"
class="lte-btn-close transition-opacity duration-200 hover:opacity-70"
aria-label="Close"
></button>
@endif

View File

@@ -39,26 +39,27 @@
<div
{{ $attributes->class([
'lte-card lte-slide-in-up',
'lte-card lte-slide-in-up transition-all duration-300 ease-out hover:shadow-md',
$variantClasses,
'cursor-pointer' => $clickable,
'cursor-pointer hover:-translate-y-0.5 hover:scale-[1.002]' => $clickable,
'hover:border-gray-300 dark:hover:border-gray-600' => $hover,
]) }}
@if($collapsible)
x-data="{ isCollapsed: {{ $collapsed ? 'true' : 'false' }} }"
@endif
>
@if($title || $actions)
<div class="lte-card-header">
<div class="lte-card-header transition-colors duration-200 hover:bg-gray-50/50 dark:hover:bg-gray-800/30">
<h3 class="lte-card-title">
@if($icon)
<span class="lte-info-box-icon lte-bg-light rounded" style="width: 36px; height: 36px; font-size: 1rem;">
<span class="lte-info-box-icon lte-bg-light rounded transition-transform duration-200 hover:scale-110" style="width: 36px; height: 36px; font-size: 1rem;">
<x-dynamic-component :component="$icon" class="size-5 {{ $iconColor }}" />
</span>
@endif
<span>
{{ $title }}
@if($subtitle)
<small class="lte-text-muted d-block" style="font-size: 0.8rem; font-weight: normal;">{{ $subtitle }}</small>
<small class="lte-text-muted d-block transition-opacity duration-200" style="font-size: 0.8rem; font-weight: normal;">{{ $subtitle }}</small>
@endif
</span>
</h3>
@@ -71,11 +72,11 @@
<button
type="button"
@click="isCollapsed = !isCollapsed"
class="lte-btn lte-btn-sm lte-btn-outline-secondary"
class="lte-btn lte-btn-sm lte-btn-outline-secondary transition-all duration-200 hover:scale-105"
style="padding: 0.25rem 0.5rem;"
>
<x-ri-arrow-down-s-line
class="size-4 transition-transform duration-200"
class="size-4 transition-transform duration-300 ease-out"
x-bind:class="isCollapsed ? 'rotate-180' : ''"
/>
</button>
@@ -89,12 +90,12 @@
class="lte-card-body"
@if($collapsible)
x-show="!isCollapsed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-transition:enter="transition ease-out duration-300"
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-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2"
@endif
>
{{ $slot }}

View File

@@ -111,9 +111,15 @@
'empty-state-lte-regular' => !$compact,
'empty-state-lte-compact' => $compact,
]) }}
x-data="{ loaded: false }"
x-init="setTimeout(() => loaded = true, 100)"
:class="loaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.5s ease-out;"
>
@if($icon)
<div class="empty-state-lte-icon {{ $compact ? 'empty-state-lte-icon-compact' : 'empty-state-lte-icon-regular' }}">
<div class="empty-state-lte-icon {{ $compact ? 'empty-state-lte-icon-compact' : 'empty-state-lte-icon-regular' }} transition-transform duration-300 hover:scale-110"
:class="loaded ? 'scale-100' : 'scale-90'"
style="transition: transform 0.5s ease-out 0.1s;">
<x-dynamic-component
:component="$icon"
class="{{ $iconClass ?? '' }}"
@@ -121,18 +127,24 @@
</div>
@endif
<h3 class="empty-state-lte-title {{ $compact ? 'empty-state-lte-title-compact' : 'empty-state-lte-title-regular' }}">
<h3 class="empty-state-lte-title {{ $compact ? 'empty-state-lte-title-compact' : 'empty-state-lte-title-regular' }}"
:class="loaded ? 'opacity-100' : 'opacity-0'"
style="transition: opacity 0.4s ease-out 0.15s;">
{{ $title }}
</h3>
@if($description)
<p class="empty-state-lte-description {{ $compact ? 'empty-state-lte-description-compact' : 'empty-state-lte-description-regular' }}">
<p class="empty-state-lte-description {{ $compact ? 'empty-state-lte-description-compact' : 'empty-state-lte-description-regular' }}"
:class="loaded ? 'opacity-100' : 'opacity-0'"
style="transition: opacity 0.4s ease-out 0.25s;">
{{ $description }}
</p>
@endif
@if($slot->isNotEmpty())
<div class="{{ $compact ? 'empty-state-lte-actions-compact' : 'empty-state-lte-actions-regular' }}">
<div class="{{ $compact ? 'empty-state-lte-actions-compact' : 'empty-state-lte-actions-regular' }}"
:class="loaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'"
style="transition: all 0.4s ease-out 0.35s;">
{{ $slot }}
</div>
@endif

View File

@@ -48,28 +48,28 @@
{{-- Backdrop --}}
<div
x-show="show"
x-transition:enter="transition ease-out duration-200"
x-transition:enter="transition ease-out duration-300"
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-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@if($closeable) @click="show = false" @endif
class="lte-modal-backdrop"
class="lte-modal-backdrop 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-200"
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-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
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"
@click.stop
class="lte-modal-content lte-slide-in-up w-full {{ $maxWidthClass }} max-h-[90vh] overflow-y-auto"
class="lte-modal-content lte-slide-in-up w-full {{ $maxWidthClass }} max-h-[90vh] overflow-y-auto shadow-2xl"
>
{{-- Header --}}
@if($title || $closeable)
@@ -92,7 +92,7 @@
@if($closeable)
<button
@click="show = false"
class="lte-btn-close"
class="lte-btn-close transition-all duration-200 hover:scale-110 hover:opacity-70"
aria-label="Close"
></button>
@endif

View File

@@ -30,11 +30,11 @@
$titleStyle = $titleSizes[$size] ?? $titleSizes['lg'];
@endphp
<div {{ $attributes->class(['flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between']) }}>
<div {{ $attributes->class(['flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between group']) }}>
<div class="flex items-start gap-3.5">
@if($icon)
<div style="width: 2.75rem; height: 2.75rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0; border-radius: .5rem; background: rgba(13, 110, 253, 0.1);">
<x-dynamic-component :component="$icon" class="size-5" style="color: #0d6efd;" />
<div style="width: 2.75rem; height: 2.75rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0; border-radius: .5rem; background: rgba(13, 110, 253, 0.1);" class="transition-transform duration-300 group-hover:scale-105">
<x-dynamic-component :component="$icon" class="size-5 transition-colors duration-200" style="color: #0d6efd;" />
</div>
@endif
<div style="display: flex; flex-direction: column; gap: .25rem;">
@@ -54,25 +54,25 @@
{{-- Help Modal --}}
<div
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter="transition ease-out duration-300"
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-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0"
style="z-index: 99999; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(2px); display: none;"
style="z-index: 99999; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); display: none;"
></div>
<div
x-show="open"
x-transition:enter="transition ease-out duration-200"
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-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
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"
@click.away="open = false"
class="fixed inset-0 flex items-center justify-center p-4"
style="z-index: 99999; display: none;"

View File

@@ -62,22 +62,22 @@
<{{ $tag }}
@if($href) href="{{ $href }}" @endif
{{ $attributes->class([
'lte-small-box lte-fade-in',
'lte-small-box lte-fade-in transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5 hover:scale-[1.01]',
$variantConfig[$variant]['class'] ?? 'lte-bg-primary',
]) }}
>
<div class="lte-inner">
<h3>{{ $prefix }}{{ $value }}@if($suffix)<sup style="font-size: 0.6em;">{{ $suffix }}</sup>@endif</h3>
<p>{{ $label }}</p>
<h3 class="transition-transform duration-200 group-hover:scale-105">{{ $prefix }}{{ $value }}@if($suffix)<sup style="font-size: 0.6em;">{{ $suffix }}</sup>@endif</h3>
<p class="transition-opacity duration-200 opacity-90 group-hover:opacity-100">{{ $label }}</p>
</div>
@if($icon)
<div class="lte-icon">
<div class="lte-icon transition-transform duration-300 hover:scale-110 hover:rotate-3">
<x-dynamic-component :component="$icon" style="font-size: 70px; width: 70px; height: 70px;" />
</div>
@endif
@if($statusText)
<div class="lte-small-box-footer">
{{ $statusText }} <x-ri-arrow-right-line class="inline size-4" />
<div class="lte-small-box-footer transition-colors duration-200 hover:bg-black/10">
{{ $statusText }} <x-ri-arrow-right-line class="inline size-4 transition-transform duration-200 group-hover:translate-x-1" />
</div>
@endif
</{{ $tag }}>
@@ -86,18 +86,18 @@
<{{ $tag }}
@if($href) href="{{ $href }}" @endif
{{ $attributes->class([
'lte-info-box lte-fade-in',
'lte-info-box lte-fade-in group transition-all duration-300 hover:shadow-md hover:-translate-y-0.5',
'cursor-pointer' => $href || $clickable,
]) }}
>
@if($icon)
<span class="lte-info-box-icon lte-bg-primary rounded" style="color: #fff;">
<span class="lte-info-box-icon lte-bg-primary rounded transition-transform duration-300 group-hover:scale-110" style="color: #fff;">
<x-dynamic-component :component="$icon" class="size-6 {{ $iconClass }}" />
</span>
@endif
<div class="lte-info-box-content">
<span class="lte-info-box-text">{{ $label }}</span>
<span class="lte-info-box-number">
<span class="lte-info-box-text transition-opacity duration-200 opacity-80 group-hover:opacity-100">{{ $label }}</span>
<span class="lte-info-box-number transition-colors duration-200 group-hover:text-primary">
@if($prefix)<small class="lte-text-muted">{{ $prefix }}</small>@endif{{ $value }}@if($suffix)<small class="lte-text-muted">{{ $suffix }}</small>@endif
</span>
@if($change !== null || $changeLabel)

View File

@@ -1,5 +1,9 @@
<x-proxmox::layout :server="$server" view="display" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center justify-between mb-6"
x-data="{ pageLoaded: false }"
x-init="setTimeout(() => pageLoaded = true, 50)"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<div>
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-xl font-semibold break-words">Display Configuration</h2>
@@ -66,8 +70,12 @@
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral rounded-lg p-6 flex flex-col gap-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"
x-data="{ cardsLoaded: false }"
x-init="setTimeout(() => cardsLoaded = true, 100)">
<div class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral rounded-lg p-6 flex flex-col gap-6 transition-all duration-300 hover:shadow-md"
:class="cardsLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.1s;">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2 min-w-0">

View File

@@ -54,13 +54,16 @@
}
</style>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-6" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
{{-- Page Header --}}
<x-proxmox::ui.section-header
title="Firewall Management"
subtitle="Configure firewall rules, policies, aliases, and IPSets for your virtual machine"
icon="ri-shield-line"
/>
<div :class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<x-proxmox::ui.section-header
title="Firewall Management"
subtitle="Configure firewall rules, policies, aliases, and IPSets for your virtual machine"
icon="ri-shield-line"
/>
</div>
{{-- Error Modal --}}
@if($error && $showErrorModal)
@@ -80,14 +83,16 @@
@endif
{{-- Main Firewall Toggle & Status --}}
<div class="relative overflow-hidden rounded-xl border border-primary/20 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent p-6 mb-6">
<div class="relative overflow-hidden rounded-xl border border-primary/20 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent p-6 mb-6 transition-all duration-300 hover:shadow-lg"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.1s;">
{{-- Decorative elements --}}
<div class="absolute top-0 right-0 w-32 h-32 bg-primary/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
<div class="absolute bottom-0 left-0 w-24 h-24 bg-primary/5 rounded-full translate-y-1/2 -translate-x-1/2 blur-xl"></div>
<div class="absolute top-0 right-0 w-32 h-32 bg-primary/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl transition-transform duration-500 hover:scale-110"></div>
<div class="absolute bottom-0 left-0 w-24 h-24 bg-primary/5 rounded-full translate-y-1/2 -translate-x-1/2 blur-xl transition-transform duration-500 hover:scale-110"></div>
<div class="relative flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center ring-4 ring-primary/10">
<div class="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center ring-4 ring-primary/10 transition-transform duration-300 hover:scale-110">
<x-ri-shield-line class="size-6 text-primary" />
</div>
<div>

View File

@@ -5,6 +5,7 @@
}
</style>
<div class="space-y-6" x-data="{
pageLoaded: false,
activeSection: 'overview',
expandedCategories: {
'getting-started': false,
@@ -33,6 +34,7 @@
return this.matchesQuery(label, keywords) || this.sectionMatchesContent(sectionId);
},
init() {
setTimeout(() => this.pageLoaded = true, 50);
this.$watch('searchQuery', (value) => {
if (value) {
// Auto-expand categories when searching
@@ -57,8 +59,10 @@
}">
<!-- Sidebar Navigation -->
<div class="lg:grid lg:grid-cols-4 gap-6">
<div class="lg:col-span-1">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-4 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto">
<div class="lg:col-span-1"
:class="pageLoaded ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-4'"
style="transition: all 0.4s ease-out;">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-4 sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto transition-all duration-300 hover:shadow-md">
<div class="flex flex-col gap-3 mb-4">
<div class="flex items-center gap-2">
<x-ri-question-line class="size-5 text-primary" />

View File

@@ -1,22 +1,37 @@
{{-- Server Listing - TailServer Theme --}}
<div class="flex flex-col flex-1" style="min-height: calc(100vh - 30rem);">
<div class="flex flex-col flex-1"
style="min-height: calc(100vh - 30rem);"
x-data="{ pageLoaded: false }"
x-init="setTimeout(() => pageLoaded = true, 50)">
{{-- Page Header --}}
<x-proxmox::ui.section-header
title="Servers"
icon="ri-server-line"
subtitle="Manage your virtual private servers"
/>
<div :class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
class="transition-all duration-500 ease-out">
<x-proxmox::ui.section-header
title="Servers"
icon="ri-server-line"
subtitle="Manage your virtual private servers"
/>
</div>
{{-- Server Cards Grid --}}
<div class="flex flex-col gap-4 flex-1 mt-4">
@forelse($servers as $server)
<livewire:proxmox.server :server="$server" wire:key="proxmox-server-{{ $server->id }}" lazy />
<div class="flex flex-col gap-4 flex-1 mt-4"
:class="pageLoaded ? 'opacity-100' : 'opacity-0'"
style="transition: opacity 0.5s ease-out 0.15s;">
@forelse($servers as $index => $server)
<div class="transform transition-all duration-500 ease-out"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
:style="'transition-delay: {{ ($index + 1) * 100 }}ms'">
<livewire:proxmox.server :server="$server" wire:key="proxmox-server-{{ $server->id }}" lazy />
</div>
@empty
<x-proxmox::ui.empty-state
title="No Servers Found"
description="You don't have any servers yet. Purchase a server to get started."
icon="ri-server-line"
/>
<div :class="pageLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-95'"
class="transition-all duration-500 ease-out delay-200">
<x-proxmox::ui.empty-state
title="No Servers Found"
description="You don't have any servers yet. Purchase a server to get started."
icon="ri-server-line"
/>
</div>
@endforelse
</div>
</div>

View File

@@ -204,7 +204,9 @@
@endif
</div>
<div class="overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-800 mb-4">
<div class="overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-800 mb-4 transition-all duration-300 hover:shadow-md"
x-data="{ tableLoaded: false }"
x-init="setTimeout(() => tableLoaded = true, 150)">
<table class="min-w-full divide-y divide-neutral/30 text-sm">
<thead class="bg-background">
<tr class="text-left text-xs font-semibold text-base/60 uppercase tracking-wide">
@@ -217,8 +219,11 @@
</tr>
</thead>
<tbody class="divide-y divide-neutral/20 text-base text-base/80">
@foreach($interfaces as $name => $type)
<tr>
@foreach($interfaces as $index => $type)
@php $name = array_keys($interfaces)[$loop->index]; @endphp
<tr class="transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-800/50"
:class="tableLoaded ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-2'"
:style="'transition-delay: {{ $loop->index * 50 }}ms'">
<td class="px-4 py-3 font-semibold">{{ strtoupper($name) }}</td>
<td class="px-4 py-3">
<select wire:model.live="interfaceSelections.{{ $name }}"

View File

@@ -342,31 +342,42 @@
@endphp
<!-- Resource Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-5" wire:loading.class="opacity-50" wire:target="checkStatus">
@foreach($resourceAllocations as $resource)
<div class="group bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700 transition-all duration-200 p-4 lg:p-5">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-5"
wire:loading.class="opacity-50"
wire:target="checkStatus"
x-data="{ loaded: false }"
x-init="setTimeout(() => loaded = true, 100)">
@foreach($resourceAllocations as $index => $resource)
<div class="group bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm p-4 lg:p-5
transition-all duration-300 ease-out
hover:shadow-lg hover:border-gray-300 dark:hover:border-gray-700
hover:-translate-y-0.5 hover:scale-[1.01]"
:class="loaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
:style="loaded ? 'transition-delay: {{ $index * 75 }}ms' : ''">
<div class="flex items-start justify-between mb-4">
<h3 class="text-xs font-bold text-gray-600 dark:text-gray-400 uppercase tracking-widest opacity-75">{{ $resource['label'] }}</h3>
<h3 class="text-xs font-bold text-gray-600 dark:text-gray-400 uppercase tracking-widest opacity-75 group-hover:opacity-100 transition-opacity duration-200">{{ $resource['label'] }}</h3>
@if(isset($resource['badge']))
<span class="text-xs font-bold px-2.5 py-1 rounded-full {{ $resource['badge']['label'] === 'Healthy' ? 'bg-success/15 text-success opacity-90' : ($resource['badge']['label'] === 'Watchlist' ? 'bg-warning/15 text-warning opacity-90' : 'bg-error/15 text-error opacity-90') }}">
<span class="text-xs font-bold px-2.5 py-1 rounded-full transition-transform duration-200 group-hover:scale-105 {{ $resource['badge']['label'] === 'Healthy' ? 'bg-success/15 text-success opacity-90' : ($resource['badge']['label'] === 'Watchlist' ? 'bg-warning/15 text-warning opacity-90' : 'bg-error/15 text-error opacity-90') }}">
{{ $resource['badge']['label'] }}
</span>
@endif
</div>
<div class="mb-3">
<div class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight mb-1">{{ $resource['usage_summary'] }}</div>
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-75 font-medium">{{ $resource['entitlement_label'] }}</p>
<div class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight mb-1 transition-colors duration-200 group-hover:text-primary">{{ $resource['usage_summary'] }}</div>
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-75 font-medium group-hover:opacity-90 transition-opacity duration-200">{{ $resource['entitlement_label'] }}</p>
</div>
@if($resource['used_percent'] !== null)
<div class="space-y-2">
<div class="relative w-full h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shadow-inner">
<div class="absolute inset-y-0 left-0 rounded-full transition-all duration-500 ease-out"
style="width: {{ min(100, $resource['used_percent']) }}%; background: {{ $resource['gradient'] }}; box-shadow: 0 0 20px {{ $resource['gradient'] }};"></div>
<div class="absolute inset-y-0 left-0 rounded-full transition-all duration-700 ease-out origin-left"
x-data="{ width: 0 }"
x-init="setTimeout(() => width = {{ min(100, $resource['used_percent']) }}, 300)"
:style="'width: ' + width + '%; background: {{ $resource['gradient'] }}; box-shadow: 0 0 20px {{ $resource['gradient'] }};'"></div>
</div>
<div class="flex items-center justify-between">
<span class="text-xs font-semibold text-gray-700 dark:text-gray-300">{{ number_format($resource['used_percent'], 1) }}%</span>
<span class="text-xs font-semibold text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-white transition-colors duration-200">{{ number_format($resource['used_percent'], 1) }}%</span>
@if($resource['used_percent'] > 100)
<span class="text-xs font-bold text-error opacity-85">Over entitlement</span>
<span class="text-xs font-bold text-error opacity-85 animate-pulse">Over entitlement</span>
@endif
</div>
</div>
@@ -376,26 +387,31 @@
</div>
<!-- Main Grid Layout -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-5">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-5"
x-data="{ cardsLoaded: false }"
x-init="setTimeout(() => cardsLoaded = true, 250)">
<!-- Server Information Card -->
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700 transition-all duration-200 overflow-hidden">
<div class="flex items-center justify-between p-4 lg:p-5 bg-gradient-to-r from-primary/8 to-primary/4 border-b border-primary/20">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden
transition-all duration-300 ease-out
hover:shadow-lg hover:border-gray-300 dark:hover:border-gray-700"
:class="cardsLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'">
<div class="flex items-center justify-between p-4 lg:p-5 bg-gradient-to-r from-primary/8 to-primary/4 border-b border-primary/20 transition-all duration-200 hover:from-primary/12 hover:to-primary/6">
<div class="flex items-center gap-2.5">
<svg class="size-5 text-primary opacity-80" fill="currentColor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="5" rx="1" fill="currentColor"/><rect x="2" y="10" width="20" height="5" rx="1" fill="currentColor"/><circle cx="6" cy="5.5" r="0.7" fill="white" opacity="0.8"/><circle cx="6" cy="12.5" r="0.7" fill="white" opacity="0.8"/></svg>
<svg class="size-5 text-primary opacity-80 transition-transform duration-200 hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="5" rx="1" fill="currentColor"/><rect x="2" y="10" width="20" height="5" rx="1" fill="currentColor"/><circle cx="6" cy="5.5" r="0.7" fill="white" opacity="0.8"/><circle cx="6" cy="12.5" r="0.7" fill="white" opacity="0.8"/></svg>
<h3 class="text-base sm:text-lg font-bold text-gray-900 dark:text-white">Server Information</h3>
</div>
</div>
<div class="p-4 lg:p-5">
<div class="space-y-3.5">
<!-- Status -->
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80">
<svg class="size-4 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:translate-x-0.5 group">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80 group-hover:opacity-100 transition-opacity duration-200">
<svg class="size-4 opacity-75 group-hover:opacity-100 transition-all duration-200 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Status</span>
</div>
<div>
@if ($status && $status->status === 'running')
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-success/15 text-success rounded-full opacity-90">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-success/15 text-success rounded-full opacity-90 transition-all duration-200 hover:opacity-100 hover:bg-success/20">
<span class="w-1.5 h-1.5 bg-success rounded-full animate-pulse"></span>
Running
</span>
@@ -408,22 +424,22 @@
</div>
</div>
<!-- Operating System -->
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80">
<svg class="size-4 opacity-75" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="4" r="1.5" fill="currentColor"/><circle cx="18" cy="15" r="1.5" fill="currentColor"/><circle cx="6" cy="15" r="1.5" fill="currentColor"/></svg>
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:translate-x-0.5 group">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80 group-hover:opacity-100 transition-opacity duration-200">
<svg class="size-4 opacity-75 group-hover:opacity-100 transition-all duration-200 group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="4" r="1.5" fill="currentColor"/><circle cx="18" cy="15" r="1.5" fill="currentColor"/><circle cx="6" cy="15" r="1.5" fill="currentColor"/></svg>
<span>Operating System</span>
</div>
<span class="text-sm text-gray-900 dark:text-white font-medium opacity-90">{{ $server->os->name }}</span>
<span class="text-sm text-gray-900 dark:text-white font-medium opacity-90 group-hover:opacity-100 transition-opacity duration-200">{{ $server->os->name }}</span>
</div>
<!-- Hostname -->
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" x-data="{ showHostnameEditModal: @entangle('isEditingHostname') }">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80">
<svg class="size-4 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 21h8M12 17v4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:translate-x-0.5 group" x-data="{ showHostnameEditModal: @entangle('isEditingHostname') }">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80 group-hover:opacity-100 transition-opacity duration-200">
<svg class="size-4 opacity-75 group-hover:opacity-100 transition-all duration-200 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 21h8M12 17v4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Hostname</span>
</div>
<div class="flex items-center gap-2">
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90">{{ $server->hostname ?? $status->name ?? 'N/A' }}</code>
<button wire:click="startEditingHostname" type="button" class="text-primary opacity-70 hover:opacity-100 transition-opacity">
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90 group-hover:opacity-100 transition-opacity duration-200">{{ $server->hostname ?? $status->name ?? 'N/A' }}</code>
<button wire:click="startEditingHostname" type="button" class="text-primary opacity-70 hover:opacity-100 transition-all duration-200 hover:scale-110">
<x-ri-edit-line class="size-4" />
</button>
</div>

View File

@@ -82,6 +82,7 @@
<div x-data="{
open: false,
pageLoaded: false,
activeSection: null,
pendingSection: null,
transitionDelay: 220,
@@ -106,7 +107,8 @@
this.activeSection = key;
}
}">
}"
x-init="setTimeout(() => pageLoaded = true, 100)">
@if($isProtected)
<x-proxmox::ui.button
@click="open = true"
@@ -122,8 +124,20 @@
@endif
<template x-if="open">
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 overflow-y-auto">
<div class="bg-white rounded-2xl p-6 w-full max-w-4xl shadow-2xl border border-neutral/30 my-8">
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<div class="bg-white rounded-2xl p-6 w-full max-w-4xl shadow-2xl border border-neutral/30 my-8"
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"
x-transition:leave-end="opacity-0 scale-95">
<div class="flex flex-col gap-2 mb-6">
<h2 class="text-2xl font-semibold text-base">Reinstall Server</h2>
@if($isProtected)
@@ -185,7 +199,8 @@
$isSelected = (string) $template->id === (string) ($os ?? '');
@endphp
<label wire:key="os-card-{{ $template->id }}"
class="relative flex items-center gap-4 rounded-xl border p-4 cursor-pointer transition-all duration-150
class="relative flex items-center gap-4 rounded-xl border p-4 cursor-pointer
transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md
@if($isSelected) border-primary bg-primary/5 ring-2 ring-primary/40 shadow-lg @else border-neutral/60 hover:border-primary/60 @endif">
<input type="radio" class="sr-only" wire:model.live="os" value="{{ $template->id }}">
<div class="shrink-0">

View File

@@ -1,10 +1,20 @@
{{-- Server Card Component - Task History Theme --}}
<div class="space-y-2">
<div class="space-y-2"
x-data="{ loaded: false, hovered: false }"
x-init="setTimeout(() => loaded = true, 50)"
x-cloak>
@if($status)
<a href="{{ route('proxmox.server.overview', ['server' => $server->id]) }}" wire:navigate>
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl overflow-hidden shadow-theme-sm hover:shadow-theme-md transition-shadow duration-150 flex flex-col cursor-pointer">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl overflow-hidden shadow-theme-sm flex flex-col cursor-pointer
transition-all duration-300 ease-out
hover:shadow-theme-lg hover:border-gray-300 dark:hover:border-gray-700
hover:-translate-y-0.5 hover:scale-[1.005]"
:class="loaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'"
@mouseenter="hovered = true"
@mouseleave="hovered = false">
{{-- Header: Server Name & Status --}}
<div class="flex items-center justify-between px-4 py-3">
<div class="flex items-center justify-between px-4 py-3 transition-colors duration-200"
:class="hovered ? 'bg-gray-50/50 dark:bg-gray-800/30' : ''">
<div class="flex items-center gap-2">
@if($server->user_set_icon)
@php
@@ -14,21 +24,25 @@
$iconName = $server->user_set_icon;
$componentName = $iconPrefix . '-' . $iconName;
@endphp
<div class="flex items-center justify-center size-7 rounded" style="background-color: {{ $bgColor }};">
<x-dynamic-component :component="$componentName" class="size-3" style="color: {{ $iconColor }};" />
<div class="flex items-center justify-center size-7 rounded transition-transform duration-300"
:class="hovered ? 'scale-110' : ''"
style="background-color: {{ $bgColor }};">
<x-dynamic-component :component="$componentName" class="size-3 transition-opacity duration-200" :class="hovered ? 'opacity-100' : 'opacity-80'" style="color: {{ $iconColor }};" />
</div>
@else
<div class="flex items-center justify-center size-7 rounded bg-blue-50 dark:bg-blue-900">
<x-ri-server-line class="size-3 text-primary" />
<div class="flex items-center justify-center size-7 rounded bg-blue-50 dark:bg-blue-900 transition-transform duration-300"
:class="hovered ? 'scale-110' : ''">
<x-ri-server-line class="size-3 text-primary transition-opacity duration-200" :class="hovered ? 'opacity-100' : 'opacity-80'" />
</div>
@endif
<span class="font-semibold text-sm">{{ $status->name }}</span>
<span class="font-semibold text-sm transition-colors duration-200" :class="hovered ? 'text-primary' : ''">{{ $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-3 px-4 py-2 border-y border-gray-200 dark:border-gray-800 text-xs">
<div class="flex flex-row flex-wrap gap-3 px-4 py-2 border-y border-gray-200 dark:border-gray-800 text-xs transition-colors duration-200"
:class="hovered ? 'bg-gray-50/30 dark:bg-gray-800/20' : ''">
<div class="flex flex-col min-w-[100px]">
<span class="font-medium text-gray-500 dark:text-gray-400 uppercase text-[10px]">IPv4</span>
<span class="text-xs">{{ $server->primaryIpv4->ip ?? 'None' }}</span>
@@ -75,36 +89,36 @@
{{-- Server Configuration Grid --}}
<div class="px-4 py-3">
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Configuration</div>
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 uppercase mb-2 transition-opacity duration-200" :class="hovered ? 'opacity-70' : 'opacity-60'">Configuration</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3">
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3 transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-600">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">CPU</span>
<x-ri-cpu-line class="size-3 text-gray-400" />
</div>
<div class="text-sm font-bold">{{ ($status->cpus ?? 'N/A') . ' Core' . (($status->cpus ?? 0) > 1 ? 's' : '') }}</div>
</div>
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3">
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3 transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-600">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">RAM</span>
<x-ri-ram-line class="size-3 text-gray-400" />
<x-ri-ram-line class="size-3 text-gray-400 transition-colors duration-200 group-hover:text-primary" />
</div>
<div class="text-sm font-bold">{{ round(($status->maxmem ?? 0) / (1024 * 1024 * 1024), 1) }} GiB</div>
</div>
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3">
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3 transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-600">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Disk</span>
<x-ri-hard-drive-2-line class="size-3 text-gray-400" />
<x-ri-hard-drive-2-line class="size-3 text-gray-400 transition-colors duration-200 group-hover:text-primary" />
</div>
<div class="text-sm font-bold">{{ Illuminate\Support\Number::fileSize(bytes: $status->maxdisk ?? 0) }}</div>
</div>
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3">
<div class="flex flex-col gap-1 rounded-lg border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-3 transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-600">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Bandwidth</span>
<x-ri-speed-line class="size-3 text-gray-400" />
<x-ri-speed-line class="size-3 text-gray-400 transition-colors duration-200 group-hover:text-primary" />
</div>
<div class="text-sm font-bold">{{ $this->monthlyTraffic }} GiB</div>
<p class="text-[10px] text-gray-500 dark:text-gray-400">of {{ $this->bandwidthThreshold95 }} GiB (95%)</p>
<p class="text-[10px] text-gray-500 dark:text-gray-400 transition-opacity duration-200" :class="hovered ? 'opacity-70' : 'opacity-60'">of {{ $this->bandwidthThreshold95 }} GiB (95%)</p>
</div>
</div>
</div>
@@ -113,7 +127,11 @@
@else
{{-- Error State --}}
<a href="{{ route('proxmox.server.overview', ['server' => $server->id]) }}" wire:navigate>
<div class="bg-white dark:bg-gray-900 border border-red-200 dark:border-red-800 rounded-2xl overflow-hidden shadow-theme-sm hover:shadow-theme-md transition-shadow duration-150 flex flex-col cursor-pointer">
<div class="bg-white dark:bg-gray-900 border border-red-200 dark:border-red-800 rounded-2xl overflow-hidden shadow-theme-sm flex flex-col cursor-pointer
transition-all duration-300 ease-out
hover:shadow-theme-lg hover:-translate-y-0.5 hover:scale-[1.005]
opacity-90 hover:opacity-100"
:class="loaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'">
<div class="flex items-center justify-between px-4 py-3">
<div class="flex items-center gap-2">
<div class="flex items-center justify-center size-7 rounded bg-red-50 dark:bg-red-900">

View File

@@ -2,7 +2,7 @@
@once
<script src="https://cdn.jsdelivr.net/npm/apexcharts" onload="window.apexChartsLoaded = true"></script>
@endonce
<div class="flex flex-col gap-6" wire:init="loadInitialData">
<div class="flex flex-col gap-6" wire:init="loadInitialData" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
<style>
.formRight select {
background-color: hsl(var(--color-background));
@@ -20,7 +20,9 @@
}
</style>
<!-- Header Section -->
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-4"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex flex-col gap-1">
<x-proxmox::ui.section-header
@@ -59,8 +61,10 @@
wire:poll.120s="stats"
x-data="{ chartsLoaded: false }"
@proxmox-all-stats-loaded.window="chartsLoaded = true"
class="relative bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 space-y-6 shadow-theme-sm overflow-hidden transition-all duration-300 ease-out"
:class="chartsLoaded ? 'opacity-100' : 'opacity-80'"
class="relative bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 space-y-6 shadow-theme-sm overflow-hidden
transition-all duration-500 ease-out hover:shadow-md"
:class="[chartsLoaded ? 'opacity-100' : 'opacity-80', pageLoaded ? 'translate-y-0' : 'translate-y-4']"
style="transition: all 0.4s ease-out 0.15s;"
>
<div>
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">

View File

@@ -36,9 +36,11 @@
$vmRunning = $status && $status->status !== 'stopped';
@endphp
<div class="space-y-6">
<div class="space-y-6" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
{{-- Page Header --}}
<x-proxmox::ui.section-header
<div :class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<x-proxmox::ui.section-header
title="Storage Management"
subtitle="Manage virtual hard disks attached to your VPS. Add, configure, or remove storage drives."
icon="ri-hard-drive-2-line"
@@ -70,7 +72,9 @@
@endif
<!-- Storage Usage Overview -->
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-5 shadow-theme-sm">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-5 shadow-theme-sm transition-all duration-300 hover:shadow-md"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.1s;">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Storage Quota</h3>
@@ -110,7 +114,10 @@
$usagePercent = $maxAllowedStorage > 0 ? min(100, ($totalAllocatedStorage / $maxAllowedStorage) * 100) : 0;
$colorClass = $usagePercent < 70 ? 'bg-success' : ($usagePercent < 90 ? 'bg-warning' : 'bg-danger');
@endphp
<div class="{{ $colorClass }} h-3 rounded-full transition-all duration-500" style="width: {{ $usagePercent }}%"></div>
<div class="{{ $colorClass }} h-3 rounded-full transition-all duration-700 ease-out"
x-data="{ width: 0 }"
x-init="setTimeout(() => width = {{ $usagePercent }}, 300)"
:style="'width: ' + width + '%'"></div>
</div>
<div class="flex justify-between mt-2 text-xs text-base/50">
<span>{{ number_format($this->remainingStorage, 1) }} GB remaining</span>

View File

@@ -1,5 +1,9 @@
<x-proxmox::layout :server="$server" view="subdomain" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center justify-between mb-6"
x-data="{ pageLoaded: false }"
x-init="setTimeout(() => pageLoaded = true, 50)"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<div>
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-xl font-semibold break-words">Subdomain Management</h2>

View File

@@ -1,6 +1,8 @@
<x-proxmox::layout :server="$server" view="system" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<div class="space-y-6">
<div class="flex flex-col gap-2">
<div class="space-y-6" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)">
<div class="flex flex-col gap-2"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
style="transition: all 0.4s ease-out;">
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-xl font-semibold break-words">System Configuration</h2>
<div class="relative" x-data="{ open: false }" x-cloak>
@@ -66,7 +68,9 @@
</div>
<div class="grid gap-6 xl:grid-cols-2">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-5 space-y-4">
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-5 space-y-4 transition-all duration-300 hover:shadow-md"
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
style="transition: all 0.4s ease-out 0.1s;">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2 min-w-0">