Files
Virtnet.bond/extensions/Servers/Proxmox/views/components/layout.blade.php

3484 lines
206 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@props([
'server',
'view',
'status' => null,
'statusPolling' => false,
'statusPollingInterval' => '30s',
])
@php
$vmStatus = optional($status)->status;
$pollSuffix = $statusPollingInterval ? '.' . $statusPollingInterval : '';
$pollAttribute = $statusPolling ? 'wire:poll' . $pollSuffix . '="loadVmStatus"' : '';
@endphp
<div {!! $pollAttribute !!} class="flex flex-col" style="min-height: calc(100vh - 30rem);">
@if($server->status === 'active')
<div class="flex flex-col flex-1" x-data="{
isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced',
isAdvancedOnlyView: {{ in_array($view, ['boot-order', 'cloud-init', 'display', 'system', 'storage', 'subdomain', 'statistics', 'help', 'firewall']) ? 'true' : 'false' }},
toggleView() {
if (!this.isAdvancedOnlyView) {
this.isAdvancedView = !this.isAdvancedView;
localStorage.setItem('proxmox-view-mode', this.isAdvancedView ? 'advanced' : 'basic');
window.dispatchEvent(new CustomEvent('proxmox-view-mode-changed', {
detail: { isAdvancedView: this.isAdvancedView }
}));
}
}
}" x-init="
if (!localStorage.getItem('proxmox-view-mode')) {
localStorage.setItem('proxmox-view-mode', 'basic');
isAdvancedView = false;
}">
@php
$advancedOnlyViews = ['boot-order', 'cloud-init', 'display', 'system', 'subdomain', 'statistics', 'help', 'firewall'];
$isAdvancedOnlyView = in_array($view, $advancedOnlyViews);
@endphp
<div class="flex flex-col gap-4 pb-6 pt-6 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-5" x-data="serverIconModal({
serverId: '{{ $server->id }}',
initialIcon: {{ $server->user_set_icon ? "'" . $server->user_set_icon . "'" : 'null' }},
initialColor: {{ $server->user_set_icon_color ? "'" . $server->user_set_icon_color . "'" : 'null' }},
initialPrefix: {{ $server->user_set_icon_prefix ? "'" . $server->user_set_icon_prefix . "'" : "'ri'" }}
})">
<button @click="openModal()" :title="icon ? 'Customize server icon' : 'Set server icon'" class="group flex items-center gap-5 hover:opacity-80 transition-opacity">
<div class="relative flex items-center justify-center">
<div x-show="icon" :style="color ? 'color: ' + color : ''" x-html="getIconSvg(icon, iconPrefix)" class="size-10 flex items-center justify-center"></div>
<x-ri-server-line x-show="!icon" class="size-10 text-secondary" />
<!-- Edit indicator badge -->
<div class="absolute -top-1 -right-1 size-4 bg-primary/90 rounded-full flex items-center justify-center shadow-sm opacity-60 group-hover:opacity-100 transition-opacity">
<x-ri-pencil-fill class="size-2.5 text-white" />
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-2xl font-bold">
{{ $server->hostname }}
</span>
@if($vmStatus === 'running')
<span class="relative flex h-2.5 w-2.5" title="Server is running">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-success"></span>
</span>
@elseif($vmStatus === 'stopped')
<span class="relative flex h-2.5 w-2.5" title="Server is stopped">
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-warning"></span>
</span>
@elseif($vmStatus)
<span class="relative flex h-2.5 w-2.5" title="Status: {{ $vmStatus }}">
<span class="animate-pulse absolute inline-flex h-full w-full rounded-full bg-info opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-info"></span>
</span>
@endif
</div>
</button>
<!-- Modal -->
<div x-show="show" x-cloak @click.self="closeModal()" @keydown.escape.window="closeModal()" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div class="bg-background-secondary rounded-xl shadow-2xl p-6 w-full max-w-md relative">
<button @click="closeModal()" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<h3 class="text-lg font-semibold mb-4">Customize Server Icon</h3>
<div class="flex items-center gap-4 mb-4">
<div class="size-12 rounded-xl flex items-center justify-center" :style="color ? 'border-color: ' + color : ''">
<div x-show="icon" :style="color ? 'color: ' + color : ''" x-html="getIconSvg(icon, iconPrefix)"></div>
<x-ri-server-line x-show="!icon" class="size-8 text-base/40" />
</div>
<input x-model="search" type="text" placeholder="Search icons..." class="border rounded px-2 py-1 text-sm w-40 dark:bg-background dark:border-base/20" />
</div>
<div class="grid grid-cols-6 gap-2 mb-4 max-h-32 overflow-y-auto p-1">
@php
$iconList = [
// Remix Icons
['icon' => 'server-line', 'prefix' => 'ri'],
['icon' => 'database-2-line', 'prefix' => 'ri'],
['icon' => 'computer-line', 'prefix' => 'ri'],
['icon' => 'cpu-line', 'prefix' => 'ri'],
['icon' => 'hard-drive-2-line', 'prefix' => 'ri'],
['icon' => 'cloud-line', 'prefix' => 'ri'],
['icon' => 'terminal-box-line', 'prefix' => 'ri'],
['icon' => 'code-s-slash-line', 'prefix' => 'ri'],
['icon' => 'braces-line', 'prefix' => 'ri'],
['icon' => 'global-line', 'prefix' => 'ri'],
['icon' => 'earth-line', 'prefix' => 'ri'],
['icon' => 'wifi-line', 'prefix' => 'ri'],
['icon' => 'signal-tower-line', 'prefix' => 'ri'],
['icon' => 'shield-check-line', 'prefix' => 'ri'],
['icon' => 'shield-keyhole-line', 'prefix' => 'ri'],
['icon' => 'lock-line', 'prefix' => 'ri'],
['icon' => 'lock-unlock-line', 'prefix' => 'ri'],
['icon' => 'key-2-line', 'prefix' => 'ri'],
['icon' => 'rocket-line', 'prefix' => 'ri'],
['icon' => 'rocket-2-line', 'prefix' => 'ri'],
['icon' => 'send-plane-line', 'prefix' => 'ri'],
['icon' => 'flashlight-line', 'prefix' => 'ri'],
['icon' => 'fire-line', 'prefix' => 'ri'],
['icon' => 'sparkling-line', 'prefix' => 'ri'],
['icon' => 'star-line', 'prefix' => 'ri'],
['icon' => 'heart-line', 'prefix' => 'ri'],
['icon' => 'home-line', 'prefix' => 'ri'],
['icon' => 'home-2-line', 'prefix' => 'ri'],
['icon' => 'building-line', 'prefix' => 'ri'],
['icon' => 'building-2-line', 'prefix' => 'ri'],
['icon' => 'store-line', 'prefix' => 'ri'],
['icon' => 'box-3-line', 'prefix' => 'ri'],
['icon' => 'archive-line', 'prefix' => 'ri'],
['icon' => 'inbox-line', 'prefix' => 'ri'],
['icon' => 'inbox-2-line', 'prefix' => 'ri'],
['icon' => 'folder-line', 'prefix' => 'ri'],
['icon' => 'folder-open-line', 'prefix' => 'ri'],
['icon' => 'settings-3-line', 'prefix' => 'ri'],
['icon' => 'settings-4-line', 'prefix' => 'ri'],
['icon' => 'tools-line', 'prefix' => 'ri'],
['icon' => 'hammer-line', 'prefix' => 'ri'],
['icon' => 'flask-line', 'prefix' => 'ri'],
['icon' => 'bug-line', 'prefix' => 'ri'],
['icon' => 'puzzle-line', 'prefix' => 'ri'],
['icon' => 'bar-chart-box-line', 'prefix' => 'ri'],
['icon' => 'pie-chart-line', 'prefix' => 'ri'],
['icon' => 'line-chart-line', 'prefix' => 'ri'],
['icon' => 'money-dollar-circle-line', 'prefix' => 'ri'],
['icon' => 'money-euro-circle-line', 'prefix' => 'ri'],
['icon' => 'bank-card-line', 'prefix' => 'ri'],
['icon' => 'wallet-line', 'prefix' => 'ri'],
['icon' => 'user-line', 'prefix' => 'ri'],
['icon' => 'user-3-line', 'prefix' => 'ri'],
['icon' => 'group-line', 'prefix' => 'ri'],
['icon' => 'team-line', 'prefix' => 'ri'],
['icon' => 'mail-line', 'prefix' => 'ri'],
['icon' => 'mail-open-line', 'prefix' => 'ri'],
['icon' => 'chat-3-line', 'prefix' => 'ri'],
['icon' => 'discuss-line', 'prefix' => 'ri'],
['icon' => 'camera-line', 'prefix' => 'ri'],
['icon' => 'vidicon-line', 'prefix' => 'ri'],
['icon' => 'film-line', 'prefix' => 'ri'],
['icon' => 'music-line', 'prefix' => 'ri'],
['icon' => 'book-open-line', 'prefix' => 'ri'],
['icon' => 'file-text-line', 'prefix' => 'ri'],
['icon' => 'sun-line', 'prefix' => 'ri'],
['icon' => 'moon-line', 'prefix' => 'ri'],
['icon' => 'palette-line', 'prefix' => 'ri'],
['icon' => 'brush-line', 'prefix' => 'ri'],
['icon' => 'trophy-line', 'prefix' => 'ri'],
['icon' => 'graduation-cap-line', 'prefix' => 'ri'],
['icon' => 'lightbulb-line', 'prefix' => 'ri'],
['icon' => 'fingerprint-line', 'prefix' => 'ri'],
['icon' => 'eye-line', 'prefix' => 'ri'],
['icon' => 'map-2-line', 'prefix' => 'ri'],
['icon' => 'map-pin-line', 'prefix' => 'ri'],
['icon' => 'time-line', 'prefix' => 'ri'],
['icon' => 'calendar-line', 'prefix' => 'ri'],
['icon' => 'gift-line', 'prefix' => 'ri'],
['icon' => 'cake-2-line', 'prefix' => 'ri'],
['icon' => 'gamepad-line', 'prefix' => 'ri'],
['icon' => 'ghost-line', 'prefix' => 'ri'],
['icon' => 'aliens-line', 'prefix' => 'ri'],
['icon' => 'space-ship-line', 'prefix' => 'ri'],
['icon' => 'android-line', 'prefix' => 'ri'],
['icon' => 'apple-line', 'prefix' => 'ri'],
['icon' => 'windows-line', 'prefix' => 'ri'],
['icon' => 'ubuntu-line', 'prefix' => 'ri'],
['icon' => 'github-line', 'prefix' => 'ri'],
['icon' => 'gitlab-line', 'prefix' => 'ri'],
['icon' => 'discord-line', 'prefix' => 'ri'],
['icon' => 'slack-line', 'prefix' => 'ri'],
['icon' => 'desktop', 'prefix' => 'vaadin'],
['icon' => 'laptop', 'prefix' => 'vaadin'],
['icon' => 'cluster', 'prefix' => 'vaadin'],
['icon' => 'cloud', 'prefix' => 'vaadin'],
['icon' => 'cloud-o', 'prefix' => 'vaadin'],
['icon' => 'cloud-upload', 'prefix' => 'vaadin'],
['icon' => 'cloud-download', 'prefix' => 'vaadin'],
['icon' => 'storage', 'prefix' => 'vaadin'],
['icon' => 'harddrive', 'prefix' => 'vaadin'],
['icon' => 'compile', 'prefix' => 'vaadin'],
['icon' => 'code', 'prefix' => 'vaadin'],
['icon' => 'terminal', 'prefix' => 'vaadin'],
['icon' => 'globe', 'prefix' => 'vaadin'],
['icon' => 'globe-wire', 'prefix' => 'vaadin'],
['icon' => 'connect', 'prefix' => 'vaadin'],
['icon' => 'sitemap', 'prefix' => 'vaadin'],
['icon' => 'automation', 'prefix' => 'vaadin'],
['icon' => 'cogs', 'prefix' => 'vaadin'],
['icon' => 'cog', 'prefix' => 'vaadin'],
['icon' => 'wrench', 'prefix' => 'vaadin'],
['icon' => 'tools', 'prefix' => 'vaadin'],
['icon' => 'hammer', 'prefix' => 'vaadin'],
['icon' => 'shield', 'prefix' => 'vaadin'],
['icon' => 'lock', 'prefix' => 'vaadin'],
['icon' => 'unlock', 'prefix' => 'vaadin'],
['icon' => 'key', 'prefix' => 'vaadin'],
['icon' => 'key-o', 'prefix' => 'vaadin'],
['icon' => 'rocket', 'prefix' => 'vaadin'],
['icon' => 'flash', 'prefix' => 'vaadin'],
['icon' => 'bolt', 'prefix' => 'vaadin'],
['icon' => 'fire', 'prefix' => 'vaadin'],
['icon' => 'star', 'prefix' => 'vaadin'],
['icon' => 'star-o', 'prefix' => 'vaadin'],
['icon' => 'heart', 'prefix' => 'vaadin'],
['icon' => 'heart-o', 'prefix' => 'vaadin'],
['icon' => 'home', 'prefix' => 'vaadin'],
['icon' => 'home-o', 'prefix' => 'vaadin'],
['icon' => 'building', 'prefix' => 'vaadin'],
['icon' => 'building-o', 'prefix' => 'vaadin'],
['icon' => 'office', 'prefix' => 'vaadin'],
['icon' => 'server', 'prefix' => 'vaadin'],
['icon' => 'database', 'prefix' => 'vaadin'],
['icon' => 'cloud', 'prefix' => 'vaadin'],
['icon' => 'cloud-o', 'prefix' => 'vaadin'],
['icon' => 'storage', 'prefix' => 'vaadin'],
['icon' => 'harddrive', 'prefix' => 'vaadin'],
['icon' => 'package', 'prefix' => 'vaadin'],
['icon' => 'archive', 'prefix' => 'vaadin'],
['icon' => 'inbox', 'prefix' => 'vaadin'],
['icon' => 'folder', 'prefix' => 'vaadin'],
['icon' => 'folder-o', 'prefix' => 'vaadin'],
['icon' => 'folder-open', 'prefix' => 'vaadin'],
['icon' => 'folder-open-o', 'prefix' => 'vaadin'],
['icon' => 'bug', 'prefix' => 'vaadin'],
['icon' => 'chart', 'prefix' => 'vaadin'],
['icon' => 'bar-chart', 'prefix' => 'vaadin'],
['icon' => 'line-chart', 'prefix' => 'vaadin'],
['icon' => 'pie-chart', 'prefix' => 'vaadin'],
['icon' => 'dashboard', 'prefix' => 'vaadin'],
['icon' => 'progressbar', 'prefix' => 'vaadin'],
['icon' => 'money', 'prefix' => 'vaadin'],
['icon' => 'dollar', 'prefix' => 'vaadin'],
['icon' => 'euro', 'prefix' => 'vaadin'],
['icon' => 'credit-card', 'prefix' => 'vaadin'],
['icon' => 'wallet', 'prefix' => 'vaadin'],
['icon' => 'user', 'prefix' => 'vaadin'],
['icon' => 'users', 'prefix' => 'vaadin'],
['icon' => 'group', 'prefix' => 'vaadin'],
['icon' => 'user-card', 'prefix' => 'vaadin'],
['icon' => 'male', 'prefix' => 'vaadin'],
['icon' => 'female', 'prefix' => 'vaadin'],
['icon' => 'envelope', 'prefix' => 'vaadin'],
['icon' => 'envelope-o', 'prefix' => 'vaadin'],
['icon' => 'envelope-open', 'prefix' => 'vaadin'],
['icon' => 'mailbox', 'prefix' => 'vaadin'],
['icon' => 'comment', 'prefix' => 'vaadin'],
['icon' => 'comments', 'prefix' => 'vaadin'],
['icon' => 'comments-o', 'prefix' => 'vaadin'],
['icon' => 'camera', 'prefix' => 'vaadin'],
['icon' => 'film', 'prefix' => 'vaadin'],
['icon' => 'music', 'prefix' => 'vaadin'],
['icon' => 'book', 'prefix' => 'vaadin'],
['icon' => 'file-text', 'prefix' => 'vaadin'],
['icon' => 'file-text-o', 'prefix' => 'vaadin'],
['icon' => 'sun-o', 'prefix' => 'vaadin'],
['icon' => 'moon-o', 'prefix' => 'vaadin'],
['icon' => 'palete', 'prefix' => 'vaadin'],
['icon' => 'paintbrush', 'prefix' => 'vaadin'],
['icon' => 'trophy', 'prefix' => 'vaadin'],
['icon' => 'academy-cap', 'prefix' => 'vaadin'],
['icon' => 'lightbulb', 'prefix' => 'vaadin'],
['icon' => 'eye', 'prefix' => 'vaadin'],
['icon' => 'eye-slash', 'prefix' => 'vaadin'],
['icon' => 'map-marker', 'prefix' => 'vaadin'],
['icon' => 'location-arrow', 'prefix' => 'vaadin'],
['icon' => 'clock', 'prefix' => 'vaadin'],
['icon' => 'calendar', 'prefix' => 'vaadin'],
['icon' => 'calendar-o', 'prefix' => 'vaadin'],
['icon' => 'gift', 'prefix' => 'vaadin'],
['icon' => 'gamepad', 'prefix' => 'vaadin'],
['icon' => 'controller', 'prefix' => 'vaadin'],
// File Icons (codeat3/blade-file-icons)
['icon' => 'docker', 'prefix' => 'fileicon'],
['icon' => 'kubernetes', 'prefix' => 'fileicon'],
['icon' => 'nginx', 'prefix' => 'fileicon'],
['icon' => 'alpine-linux', 'prefix' => 'fileicon'],
['icon' => 'arch-linux', 'prefix' => 'fileicon'],
['icon' => 'xubuntu', 'prefix' => 'fileicon'],
['icon' => 'virtualbox', 'prefix' => 'fileicon'],
['icon' => 'virtualbox-alt', 'prefix' => 'fileicon'],
['icon' => 'vmware', 'prefix' => 'fileicon'],
['icon' => 'terminal', 'prefix' => 'fileicon'],
['icon' => 'powershell', 'prefix' => 'fileicon'],
['icon' => 'serverless', 'prefix' => 'fileicon'],
['icon' => 'devcontainer', 'prefix' => 'fileicon'],
['icon' => 'config', 'prefix' => 'fileicon'],
['icon' => 'editorconfig', 'prefix' => 'fileicon'],
['icon' => 'php', 'prefix' => 'fileicon'],
['icon' => 'go', 'prefix' => 'fileicon'],
['icon' => 'kotlin', 'prefix' => 'fileicon'],
['icon' => 'c', 'prefix' => 'fileicon'],
['icon' => 'npm', 'prefix' => 'fileicon'],
['icon' => 'yarn', 'prefix' => 'fileicon'],
['icon' => 'vscode', 'prefix' => 'fileicon'],
['icon' => 'nano', 'prefix' => 'fileicon'],
['icon' => 'gitlab', 'prefix' => 'fileicon'],
['icon' => 'sqlite', 'prefix' => 'fileicon'],
['icon' => 'json-1', 'prefix' => 'fileicon'],
['icon' => 'json-2', 'prefix' => 'fileicon'],
['icon' => 'yaml', 'prefix' => 'fileicon'],
['icon' => 'typescript', 'prefix' => 'fileicon'],
['icon' => 'vue', 'prefix' => 'fileicon'],
['icon' => 'svelte', 'prefix' => 'fileicon'],
['icon' => 'laravel', 'prefix' => 'fileicon'],
['icon' => 'flask', 'prefix' => 'fileicon'],
['icon' => 'terraform', 'prefix' => 'fileicon'],
['icon' => 'ansible', 'prefix' => 'fileicon'],
['icon' => 'ansible-alt', 'prefix' => 'fileicon'],
['icon' => 'puppet', 'prefix' => 'fileicon'],
['icon' => 'jenkins', 'prefix' => 'fileicon'],
['icon' => 'jsx', 'prefix' => 'fileicon'],
['icon' => 'tsx', 'prefix' => 'fileicon'],
['icon' => 'perl6', 'prefix' => 'fileicon'],
['icon' => 'rubygems', 'prefix' => 'fileicon'],
['icon' => 'toml', 'prefix' => 'fileicon'],
['icon' => 'tailwind', 'prefix' => 'fileicon'],
['icon' => 'webpack', 'prefix' => 'fileicon'],
['icon' => 'vite', 'prefix' => 'fileicon'],
['icon' => 'nextjs', 'prefix' => 'fileicon'],
['icon' => 'nestjs', 'prefix' => 'fileicon'],
['icon' => 'nodemon', 'prefix' => 'fileicon'],
['icon' => 'vagrant', 'prefix' => 'fileicon'],
['icon' => 'saltstack', 'prefix' => 'fileicon'],
['icon' => 'chef', 'prefix' => 'fileicon'],
['icon' => 'circleci', 'prefix' => 'fileicon'],
['icon' => 'r', 'prefix' => 'fileicon'],
['icon' => 'apple', 'prefix' => 'fileicon'],
['icon' => 'arduino', 'prefix' => 'fileicon'],
['icon' => 'babel', 'prefix' => 'fileicon'],
['icon' => 'composer', 'prefix' => 'fileicon'],
['icon' => 'snyk', 'prefix' => 'fileicon'],
['icon' => 'storybook', 'prefix' => 'fileicon'],
// Dev Icons (codeat3/blade-devicons) - Technology logos
['icon' => 'docker', 'prefix' => 'devicon'],
['icon' => 'kubernetes', 'prefix' => 'devicon'],
['icon' => 'linux', 'prefix' => 'devicon'],
['icon' => 'ubuntu', 'prefix' => 'devicon'],
['icon' => 'debian', 'prefix' => 'devicon'],
['icon' => 'centos', 'prefix' => 'devicon'],
['icon' => 'redhat', 'prefix' => 'devicon'],
['icon' => 'fedora', 'prefix' => 'devicon'],
['icon' => 'opensuse', 'prefix' => 'devicon'],
['icon' => 'nixos', 'prefix' => 'devicon'],
['icon' => 'gentoo', 'prefix' => 'devicon'],
['icon' => 'android', 'prefix' => 'devicon'],
['icon' => 'apache', 'prefix' => 'devicon'],
['icon' => 'mysql', 'prefix' => 'devicon'],
['icon' => 'postgresql', 'prefix' => 'devicon'],
['icon' => 'mongodb', 'prefix' => 'devicon'],
['icon' => 'redis', 'prefix' => 'devicon'],
['icon' => 'sqlite', 'prefix' => 'devicon'],
['icon' => 'neo4j', 'prefix' => 'devicon'],
['icon' => 'couchdb', 'prefix' => 'devicon'],
['icon' => 'microsoftsqlserver', 'prefix' => 'devicon'],
['icon' => 'python', 'prefix' => 'devicon'],
['icon' => 'php', 'prefix' => 'devicon'],
['icon' => 'java', 'prefix' => 'devicon'],
['icon' => 'javascript', 'prefix' => 'devicon'],
['icon' => 'typescript', 'prefix' => 'devicon'],
['icon' => 'nodejs', 'prefix' => 'devicon'],
['icon' => 'go', 'prefix' => 'devicon'],
['icon' => 'rust', 'prefix' => 'devicon'],
['icon' => 'c', 'prefix' => 'devicon'],
['icon' => 'cplusplus', 'prefix' => 'devicon'],
['icon' => 'csharp', 'prefix' => 'devicon'],
['icon' => 'ruby', 'prefix' => 'devicon'],
['icon' => 'perl', 'prefix' => 'devicon'],
['icon' => 'swift', 'prefix' => 'devicon'],
['icon' => 'kotlin', 'prefix' => 'devicon'],
['icon' => 'scala', 'prefix' => 'devicon'],
['icon' => 'r', 'prefix' => 'devicon'],
['icon' => 'lua', 'prefix' => 'devicon'],
['icon' => 'dart', 'prefix' => 'devicon'],
['icon' => 'elixir', 'prefix' => 'devicon'],
['icon' => 'erlang', 'prefix' => 'devicon'],
['icon' => 'haskell', 'prefix' => 'devicon'],
['icon' => 'julia', 'prefix' => 'devicon'],
['icon' => 'bash', 'prefix' => 'devicon'],
['icon' => 'html5', 'prefix' => 'devicon'],
['icon' => 'css3', 'prefix' => 'devicon'],
['icon' => 'laravel', 'prefix' => 'devicon'],
['icon' => 'django', 'prefix' => 'devicon'],
['icon' => 'rails', 'prefix' => 'devicon'],
['icon' => 'spring', 'prefix' => 'devicon'],
['icon' => 'fastapi', 'prefix' => 'devicon'],
['icon' => 'nestjs', 'prefix' => 'devicon'],
['icon' => 'vuejs', 'prefix' => 'devicon'],
['icon' => 'angularjs', 'prefix' => 'devicon'],
['icon' => 'svelte', 'prefix' => 'devicon'],
['icon' => 'nuxtjs', 'prefix' => 'devicon'],
['icon' => 'gatsby', 'prefix' => 'devicon'],
['icon' => 'bootstrap', 'prefix' => 'devicon'],
['icon' => 'tailwindcss', 'prefix' => 'devicon'],
['icon' => 'materialui', 'prefix' => 'devicon'],
['icon' => 'git', 'prefix' => 'devicon'],
['icon' => 'gitlab', 'prefix' => 'devicon'],
['icon' => 'azure', 'prefix' => 'devicon'],
['icon' => 'googlecloud', 'prefix' => 'devicon'],
['icon' => 'digitalocean', 'prefix' => 'devicon'],
['icon' => 'heroku', 'prefix' => 'devicon'],
['icon' => 'firebase', 'prefix' => 'devicon'],
['icon' => 'terraform', 'prefix' => 'devicon'],
['icon' => 'ansible', 'prefix' => 'devicon'],
['icon' => 'jenkins', 'prefix' => 'devicon'],
['icon' => 'circleci', 'prefix' => 'devicon'],
['icon' => 'travis', 'prefix' => 'devicon'],
['icon' => 'argocd', 'prefix' => 'devicon'],
['icon' => 'vagrant', 'prefix' => 'devicon'],
['icon' => 'podman', 'prefix' => 'devicon'],
['icon' => 'webpack', 'prefix' => 'devicon'],
['icon' => 'babel', 'prefix' => 'devicon'],
['icon' => 'gradle', 'prefix' => 'devicon'],
['icon' => 'vscode', 'prefix' => 'devicon'],
['icon' => 'visualstudio', 'prefix' => 'devicon'],
['icon' => 'intellij', 'prefix' => 'devicon'],
['icon' => 'pycharm', 'prefix' => 'devicon'],
['icon' => 'phpstorm', 'prefix' => 'devicon'],
['icon' => 'webstorm', 'prefix' => 'devicon'],
['icon' => 'vim', 'prefix' => 'devicon'],
['icon' => 'xcode', 'prefix' => 'devicon'],
['icon' => 'androidstudio', 'prefix' => 'devicon'],
['icon' => 'jupyter', 'prefix' => 'devicon'],
['icon' => 'chrome', 'prefix' => 'devicon'],
['icon' => 'firefox', 'prefix' => 'devicon'],
['icon' => 'safari', 'prefix' => 'devicon'],
['icon' => 'opera', 'prefix' => 'devicon'],
['icon' => 'wordpress', 'prefix' => 'devicon'],
['icon' => 'drupal', 'prefix' => 'devicon'],
['icon' => 'woocommerce', 'prefix' => 'devicon'],
['icon' => 'graphql', 'prefix' => 'devicon'],
['icon' => 'arduino', 'prefix' => 'devicon'],
['icon' => 'flutter', 'prefix' => 'devicon'],
['icon' => 'figma', 'prefix' => 'devicon'],
['icon' => 'photoshop', 'prefix' => 'devicon'],
['icon' => 'illustrator', 'prefix' => 'devicon'],
['icon' => 'premierepro', 'prefix' => 'devicon'],
['icon' => 'aftereffects', 'prefix' => 'devicon'],
['icon' => 'slack', 'prefix' => 'devicon'],
['icon' => 'trello', 'prefix' => 'devicon'],
['icon' => 'jira', 'prefix' => 'devicon'],
['icon' => 'putty', 'prefix' => 'devicon'],
['icon' => 'solidity', 'prefix' => 'devicon'],
// Coolicons (codeat3/blade-coolicons)
['icon' => 'globe', 'prefix' => 'coolicon'],
['icon' => 'planet', 'prefix' => 'coolicon'],
['icon' => 'desktop', 'prefix' => 'coolicon'],
['icon' => 'desktop-tower', 'prefix' => 'coolicon'],
['icon' => 'laptop', 'prefix' => 'coolicon'],
['icon' => 'monitor', 'prefix' => 'coolicon'],
['icon' => 'monitor-play', 'prefix' => 'coolicon'],
['icon' => 'tablet', 'prefix' => 'coolicon'],
['icon' => 'tablet-button', 'prefix' => 'coolicon'],
['icon' => 'mobile', 'prefix' => 'coolicon'],
['icon' => 'mobile-button', 'prefix' => 'coolicon'],
['icon' => 'devices', 'prefix' => 'coolicon'],
['icon' => 'cloud', 'prefix' => 'coolicon'],
['icon' => 'cloud-add', 'prefix' => 'coolicon'],
['icon' => 'cloud-check', 'prefix' => 'coolicon'],
['icon' => 'cloud-close', 'prefix' => 'coolicon'],
['icon' => 'cloud-download', 'prefix' => 'coolicon'],
['icon' => 'cloud-upload', 'prefix' => 'coolicon'],
['icon' => 'cloud-off', 'prefix' => 'coolicon'],
['icon' => 'cloud-remove', 'prefix' => 'coolicon'],
['icon' => 'data', 'prefix' => 'coolicon'],
['icon' => 'cylinder', 'prefix' => 'coolicon'],
['icon' => 'instance', 'prefix' => 'coolicon'],
['icon' => 'layers', 'prefix' => 'coolicon'],
['icon' => 'layer', 'prefix' => 'coolicon'],
['icon' => 'terminal', 'prefix' => 'coolicon'],
['icon' => 'code', 'prefix' => 'coolicon'],
['icon' => 'window', 'prefix' => 'coolicon'],
['icon' => 'window-check', 'prefix' => 'coolicon'],
['icon' => 'window-close', 'prefix' => 'coolicon'],
['icon' => 'window-code-block', 'prefix' => 'coolicon'],
['icon' => 'window-sidebar', 'prefix' => 'coolicon'],
['icon' => 'window-terminal', 'prefix' => 'coolicon'],
['icon' => 'keyboard', 'prefix' => 'coolicon'],
['icon' => 'mouse', 'prefix' => 'coolicon'],
['icon' => 'wifi-high', 'prefix' => 'coolicon'],
['icon' => 'wifi-medium', 'prefix' => 'coolicon'],
['icon' => 'wifi-low', 'prefix' => 'coolicon'],
['icon' => 'wifi-none', 'prefix' => 'coolicon'],
['icon' => 'wifi-off', 'prefix' => 'coolicon'],
['icon' => 'wifi-problem', 'prefix' => 'coolicon'],
['icon' => 'shield', 'prefix' => 'coolicon'],
['icon' => 'shield-check', 'prefix' => 'coolicon'],
['icon' => 'shield-warning', 'prefix' => 'coolicon'],
['icon' => 'lock', 'prefix' => 'coolicon'],
['icon' => 'lock-open', 'prefix' => 'coolicon'],
['icon' => 'settings', 'prefix' => 'coolicon'],
['icon' => 'settings-future', 'prefix' => 'coolicon'],
['icon' => 'slider-01', 'prefix' => 'coolicon'],
['icon' => 'slider-02', 'prefix' => 'coolicon'],
['icon' => 'slider-03', 'prefix' => 'coolicon'],
['icon' => 'filter', 'prefix' => 'coolicon'],
['icon' => 'filter-off', 'prefix' => 'coolicon'],
['icon' => 'chart-bar-horizontal-01', 'prefix' => 'coolicon'],
['icon' => 'chart-bar-vertical-01', 'prefix' => 'coolicon'],
['icon' => 'chart-line', 'prefix' => 'coolicon'],
['icon' => 'chart-pie', 'prefix' => 'coolicon'],
['icon' => 'trending-up', 'prefix' => 'coolicon'],
['icon' => 'trending-down', 'prefix' => 'coolicon'],
['icon' => 'bell', 'prefix' => 'coolicon'],
['icon' => 'bell-add', 'prefix' => 'coolicon'],
['icon' => 'bell-notification', 'prefix' => 'coolicon'],
['icon' => 'bell-off', 'prefix' => 'coolicon'],
['icon' => 'bell-ring', 'prefix' => 'coolicon'],
['icon' => 'mail', 'prefix' => 'coolicon'],
['icon' => 'mail-open', 'prefix' => 'coolicon'],
['icon' => 'chat', 'prefix' => 'coolicon'],
['icon' => 'chat-add', 'prefix' => 'coolicon'],
['icon' => 'chat-check', 'prefix' => 'coolicon'],
['icon' => 'chat-close', 'prefix' => 'coolicon'],
['icon' => 'chat-dots', 'prefix' => 'coolicon'],
['icon' => 'chat-conversation', 'prefix' => 'coolicon'],
['icon' => 'folder', 'prefix' => 'coolicon'],
['icon' => 'folder-add', 'prefix' => 'coolicon'],
['icon' => 'folder-check', 'prefix' => 'coolicon'],
['icon' => 'folder-close', 'prefix' => 'coolicon'],
['icon' => 'folder-code', 'prefix' => 'coolicon'],
['icon' => 'folder-document', 'prefix' => 'coolicon'],
['icon' => 'folder-download', 'prefix' => 'coolicon'],
['icon' => 'folder-open', 'prefix' => 'coolicon'],
['icon' => 'folder-upload', 'prefix' => 'coolicon'],
['icon' => 'folders', 'prefix' => 'coolicon'],
['icon' => 'file-blank', 'prefix' => 'coolicon'],
['icon' => 'file-add', 'prefix' => 'coolicon'],
['icon' => 'file-check', 'prefix' => 'coolicon'],
['icon' => 'file-close', 'prefix' => 'coolicon'],
['icon' => 'file-code', 'prefix' => 'coolicon'],
['icon' => 'file-document', 'prefix' => 'coolicon'],
['icon' => 'file-download', 'prefix' => 'coolicon'],
['icon' => 'file-upload', 'prefix' => 'coolicon'],
['icon' => 'files', 'prefix' => 'coolicon'],
['icon' => 'user-01', 'prefix' => 'coolicon'],
['icon' => 'user-02', 'prefix' => 'coolicon'],
['icon' => 'user-03', 'prefix' => 'coolicon'],
['icon' => 'user-add', 'prefix' => 'coolicon'],
['icon' => 'user-check', 'prefix' => 'coolicon'],
['icon' => 'user-circle', 'prefix' => 'coolicon'],
['icon' => 'user-close', 'prefix' => 'coolicon'],
['icon' => 'user-remove', 'prefix' => 'coolicon'],
['icon' => 'user-square', 'prefix' => 'coolicon'],
['icon' => 'users', 'prefix' => 'coolicon'],
['icon' => 'users-group', 'prefix' => 'coolicon'],
['icon' => 'star', 'prefix' => 'coolicon'],
['icon' => 'heart-01', 'prefix' => 'coolicon'],
['icon' => 'heart-02', 'prefix' => 'coolicon'],
['icon' => 'bookmark', 'prefix' => 'coolicon'],
['icon' => 'flag', 'prefix' => 'coolicon'],
['icon' => 'tag', 'prefix' => 'coolicon'],
['icon' => 'label', 'prefix' => 'coolicon'],
['icon' => 'house-01', 'prefix' => 'coolicon'],
['icon' => 'house-02', 'prefix' => 'coolicon'],
['icon' => 'house-03', 'prefix' => 'coolicon'],
['icon' => 'house-add', 'prefix' => 'coolicon'],
['icon' => 'house-check', 'prefix' => 'coolicon'],
['icon' => 'building-01', 'prefix' => 'coolicon'],
['icon' => 'building-02', 'prefix' => 'coolicon'],
['icon' => 'building-03', 'prefix' => 'coolicon'],
['icon' => 'building-04', 'prefix' => 'coolicon'],
['icon' => 'sun', 'prefix' => 'coolicon'],
['icon' => 'moon', 'prefix' => 'coolicon'],
['icon' => 'bulb', 'prefix' => 'coolicon'],
['icon' => 'compass', 'prefix' => 'coolicon'],
['icon' => 'map', 'prefix' => 'coolicon'],
['icon' => 'map-pin', 'prefix' => 'coolicon'],
['icon' => 'navigation', 'prefix' => 'coolicon'],
['icon' => 'clock', 'prefix' => 'coolicon'],
['icon' => 'timer', 'prefix' => 'coolicon'],
['icon' => 'timer-add', 'prefix' => 'coolicon'],
['icon' => 'calendar', 'prefix' => 'coolicon'],
['icon' => 'calendar-add', 'prefix' => 'coolicon'],
['icon' => 'calendar-check', 'prefix' => 'coolicon'],
['icon' => 'calendar-event', 'prefix' => 'coolicon'],
['icon' => 'gift', 'prefix' => 'coolicon'],
['icon' => 'credit-card-01', 'prefix' => 'coolicon'],
['icon' => 'credit-card-02', 'prefix' => 'coolicon'],
['icon' => 'shopping-bag-01', 'prefix' => 'coolicon'],
['icon' => 'shopping-bag-02', 'prefix' => 'coolicon'],
['icon' => 'shopping-cart-01', 'prefix' => 'coolicon'],
['icon' => 'shopping-cart-02', 'prefix' => 'coolicon'],
['icon' => 'suitcase', 'prefix' => 'coolicon'],
['icon' => 'archive', 'prefix' => 'coolicon'],
['icon' => 'book', 'prefix' => 'coolicon'],
['icon' => 'book-open', 'prefix' => 'coolicon'],
['icon' => 'notebook', 'prefix' => 'coolicon'],
['icon' => 'note', 'prefix' => 'coolicon'],
['icon' => 'note-edit', 'prefix' => 'coolicon'],
['icon' => 'note-search', 'prefix' => 'coolicon'],
['icon' => 'camera', 'prefix' => 'coolicon'],
['icon' => 'image-01', 'prefix' => 'coolicon'],
['icon' => 'image-02', 'prefix' => 'coolicon'],
['icon' => 'headphones', 'prefix' => 'coolicon'],
['icon' => 'printer', 'prefix' => 'coolicon'],
['icon' => 'airplay', 'prefix' => 'coolicon'],
['icon' => 'chromecast', 'prefix' => 'coolicon'],
['icon' => 'download', 'prefix' => 'coolicon'],
['icon' => 'download-package', 'prefix' => 'coolicon'],
['icon' => 'cloud-upload', 'prefix' => 'coolicon'],
['icon' => 'link', 'prefix' => 'coolicon'],
['icon' => 'link-horizontal', 'prefix' => 'coolicon'],
['icon' => 'link-vertical', 'prefix' => 'coolicon'],
['icon' => 'link-break', 'prefix' => 'coolicon'],
['icon' => 'external-link', 'prefix' => 'coolicon'],
['icon' => 'share-android', 'prefix' => 'coolicon'],
['icon' => 'share-ios-export', 'prefix' => 'coolicon'],
['icon' => 'qr-code', 'prefix' => 'coolicon'],
['icon' => 'copy', 'prefix' => 'coolicon'],
['icon' => 'save', 'prefix' => 'coolicon'],
['icon' => 'trash-empty', 'prefix' => 'coolicon'],
['icon' => 'trash-full', 'prefix' => 'coolicon'],
['icon' => 'puzzle', 'prefix' => 'coolicon'],
['icon' => 'command', 'prefix' => 'coolicon'],
['icon' => 'option', 'prefix' => 'coolicon'],
['icon' => 'rainbow', 'prefix' => 'coolicon'],
['icon' => 'leaf', 'prefix' => 'coolicon'],
['icon' => 'water-drop', 'prefix' => 'coolicon'],
['icon' => 'cookie', 'prefix' => 'coolicon'],
['icon' => 'coffee', 'prefix' => 'coolicon'],
['icon' => 'coffe-to-go', 'prefix' => 'coolicon'],
['icon' => 'cupcake', 'prefix' => 'coolicon'],
['icon' => 'first-aid', 'prefix' => 'coolicon'],
['icon' => 'car-auto', 'prefix' => 'coolicon'],
['icon' => 'phone', 'prefix' => 'coolicon'],
['icon' => 'alarm', 'prefix' => 'coolicon'],
['icon' => 'help', 'prefix' => 'coolicon'],
['icon' => 'info', 'prefix' => 'coolicon'],
['icon' => 'warning', 'prefix' => 'coolicon'],
['icon' => 'circle-check', 'prefix' => 'coolicon'],
['icon' => 'circle-warning', 'prefix' => 'coolicon'],
['icon' => 'circle-help', 'prefix' => 'coolicon'],
['icon' => 'square-check', 'prefix' => 'coolicon'],
['icon' => 'square-warning', 'prefix' => 'coolicon'],
['icon' => 'square-help', 'prefix' => 'coolicon'],
['icon' => 'triangle-check', 'prefix' => 'coolicon'],
['icon' => 'triangle-warning', 'prefix' => 'coolicon'],
['icon' => 'octagon-check', 'prefix' => 'coolicon'],
['icon' => 'octagon-warning', 'prefix' => 'coolicon'],
['icon' => 'wavy-check', 'prefix' => 'coolicon'],
['icon' => 'wavy-warning', 'prefix' => 'coolicon'],
['icon' => 'wavy-help', 'prefix' => 'coolicon'],
['icon' => 'play', 'prefix' => 'coolicon'],
['icon' => 'play-circle', 'prefix' => 'coolicon'],
['icon' => 'pause', 'prefix' => 'coolicon'],
['icon' => 'pause-circle', 'prefix' => 'coolicon'],
['icon' => 'stop', 'prefix' => 'coolicon'],
['icon' => 'stop-circle', 'prefix' => 'coolicon'],
['icon' => 'stop-sign', 'prefix' => 'coolicon'],
['icon' => 'forward', 'prefix' => 'coolicon'],
['icon' => 'rewind', 'prefix' => 'coolicon'],
['icon' => 'skip-forward', 'prefix' => 'coolicon'],
['icon' => 'skip-back', 'prefix' => 'coolicon'],
['icon' => 'shuffle', 'prefix' => 'coolicon'],
['icon' => 'volume-max', 'prefix' => 'coolicon'],
['icon' => 'volume-min', 'prefix' => 'coolicon'],
['icon' => 'volume-off', 'prefix' => 'coolicon'],
['icon' => 'expand', 'prefix' => 'coolicon'],
['icon' => 'shrink', 'prefix' => 'coolicon'],
['icon' => 'crop', 'prefix' => 'coolicon'],
['icon' => 'move', 'prefix' => 'coolicon'],
['icon' => 'show', 'prefix' => 'coolicon'],
['icon' => 'hide', 'prefix' => 'coolicon'],
['icon' => 'search-magnifying-glass', 'prefix' => 'coolicon'],
['icon' => 'magnifying-glass-plus', 'prefix' => 'coolicon'],
['icon' => 'magnifying-glass-minus', 'prefix' => 'coolicon'],
['icon' => 'edit-pencil-01', 'prefix' => 'coolicon'],
['icon' => 'edit-pencil-02', 'prefix' => 'coolicon'],
['icon' => 'edit-pencil-line-01', 'prefix' => 'coolicon'],
['icon' => 'edit-pencil-line-02', 'prefix' => 'coolicon'],
['icon' => 'check', 'prefix' => 'coolicon'],
['icon' => 'check-big', 'prefix' => 'coolicon'],
['icon' => 'check-all', 'prefix' => 'coolicon'],
['icon' => 'check-all-big', 'prefix' => 'coolicon'],
['icon' => 'close-lg', 'prefix' => 'coolicon'],
['icon' => 'close-md', 'prefix' => 'coolicon'],
['icon' => 'close-sm', 'prefix' => 'coolicon'],
['icon' => 'add-plus', 'prefix' => 'coolicon'],
['icon' => 'add-plus-circle', 'prefix' => 'coolicon'],
['icon' => 'add-plus-square', 'prefix' => 'coolicon'],
['icon' => 'remove-minus', 'prefix' => 'coolicon'],
['icon' => 'remove-minus-circle', 'prefix' => 'coolicon'],
['icon' => 'more-horizontal', 'prefix' => 'coolicon'],
['icon' => 'more-vertical', 'prefix' => 'coolicon'],
['icon' => 'more-grid-big', 'prefix' => 'coolicon'],
['icon' => 'more-grid-small', 'prefix' => 'coolicon'],
['icon' => 'hamburger-lg', 'prefix' => 'coolicon'],
['icon' => 'hamburger-md', 'prefix' => 'coolicon'],
['icon' => 'menu-alt-01', 'prefix' => 'coolicon'],
['icon' => 'menu-alt-02', 'prefix' => 'coolicon'],
['icon' => 'menu-alt-03', 'prefix' => 'coolicon'],
['icon' => 'menu-alt-04', 'prefix' => 'coolicon'],
['icon' => 'menu-alt-05', 'prefix' => 'coolicon'],
['icon' => 'list-checklist', 'prefix' => 'coolicon'],
['icon' => 'list-ordered', 'prefix' => 'coolicon'],
['icon' => 'list-unordered', 'prefix' => 'coolicon'],
['icon' => 'columns', 'prefix' => 'coolicon'],
['icon' => 'rows', 'prefix' => 'coolicon'],
['icon' => 'table', 'prefix' => 'coolicon'],
['icon' => 'table-add', 'prefix' => 'coolicon'],
['icon' => 'table-remove', 'prefix' => 'coolicon'],
['icon' => 'figma', 'prefix' => 'coolicon'],
['icon' => 'swatches-palette', 'prefix' => 'coolicon'],
['icon' => 'ruler', 'prefix' => 'coolicon'],
['icon' => 'path', 'prefix' => 'coolicon'],
['icon' => 'text', 'prefix' => 'coolicon'],
['icon' => 'font', 'prefix' => 'coolicon'],
['icon' => 'bold', 'prefix' => 'coolicon'],
['icon' => 'italic', 'prefix' => 'coolicon'],
['icon' => 'underline', 'prefix' => 'coolicon'],
['icon' => 'strikethrough', 'prefix' => 'coolicon'],
['icon' => 'paragraph', 'prefix' => 'coolicon'],
['icon' => 'heading', 'prefix' => 'coolicon'],
['icon' => 'text-align-left', 'prefix' => 'coolicon'],
['icon' => 'text-align-center', 'prefix' => 'coolicon'],
['icon' => 'text-align-right', 'prefix' => 'coolicon'],
['icon' => 'text-align-justify', 'prefix' => 'coolicon'],
['icon' => 'undo', 'prefix' => 'coolicon'],
['icon' => 'redo', 'prefix' => 'coolicon'],
['icon' => 'log-out', 'prefix' => 'coolicon'],
['icon' => 'exit', 'prefix' => 'coolicon'],
['icon' => 'paper-plane', 'prefix' => 'coolicon'],
['icon' => 'loading', 'prefix' => 'coolicon'],
['icon' => 'arrows-reload-01', 'prefix' => 'coolicon'],
['icon' => 'arrow-reload-02', 'prefix' => 'coolicon'],
// Material Design Icons (postare/blade-mdi)
// Server & Infrastructure
['icon' => 'server', 'prefix' => 'mdi'],
['icon' => 'server-network', 'prefix' => 'mdi'],
['icon' => 'server-network-off', 'prefix' => 'mdi'],
['icon' => 'server-off', 'prefix' => 'mdi'],
['icon' => 'server-security', 'prefix' => 'mdi'],
['icon' => 'database', 'prefix' => 'mdi'],
['icon' => 'database-outline', 'prefix' => 'mdi'],
['icon' => 'database-cog', 'prefix' => 'mdi'],
['icon' => 'database-sync', 'prefix' => 'mdi'],
['icon' => 'database-lock', 'prefix' => 'mdi'],
['icon' => 'database-check', 'prefix' => 'mdi'],
['icon' => 'database-search', 'prefix' => 'mdi'],
['icon' => 'database-export', 'prefix' => 'mdi'],
['icon' => 'database-import', 'prefix' => 'mdi'],
['icon' => 'harddisk', 'prefix' => 'mdi'],
['icon' => 'harddisk-plus', 'prefix' => 'mdi'],
['icon' => 'harddisk-remove', 'prefix' => 'mdi'],
['icon' => 'memory', 'prefix' => 'mdi'],
['icon' => 'chip', 'prefix' => 'mdi'],
['icon' => 'cpu-32-bit', 'prefix' => 'mdi'],
['icon' => 'cpu-64-bit', 'prefix' => 'mdi'],
// Cloud
['icon' => 'cloud', 'prefix' => 'mdi'],
['icon' => 'cloud-outline', 'prefix' => 'mdi'],
['icon' => 'cloud-check', 'prefix' => 'mdi'],
['icon' => 'cloud-sync', 'prefix' => 'mdi'],
['icon' => 'cloud-download', 'prefix' => 'mdi'],
['icon' => 'cloud-upload', 'prefix' => 'mdi'],
['icon' => 'cloud-lock', 'prefix' => 'mdi'],
['icon' => 'cloud-cog', 'prefix' => 'mdi'],
['icon' => 'cloud-refresh', 'prefix' => 'mdi'],
['icon' => 'cloud-off', 'prefix' => 'mdi'],
['icon' => 'cloud-braces', 'prefix' => 'mdi'],
['icon' => 'clouds', 'prefix' => 'mdi'],
// Network
['icon' => 'lan', 'prefix' => 'mdi'],
['icon' => 'lan-connect', 'prefix' => 'mdi'],
['icon' => 'lan-disconnect', 'prefix' => 'mdi'],
['icon' => 'lan-check', 'prefix' => 'mdi'],
['icon' => 'lan-pending', 'prefix' => 'mdi'],
['icon' => 'ethernet', 'prefix' => 'mdi'],
['icon' => 'ethernet-cable', 'prefix' => 'mdi'],
['icon' => 'ethernet-off', 'prefix' => 'mdi'],
['icon' => 'wifi', 'prefix' => 'mdi'],
['icon' => 'wifi-off', 'prefix' => 'mdi'],
['icon' => 'wifi-strength-4', 'prefix' => 'mdi'],
['icon' => 'wifi-strength-3', 'prefix' => 'mdi'],
['icon' => 'wifi-strength-2', 'prefix' => 'mdi'],
['icon' => 'wifi-strength-1', 'prefix' => 'mdi'],
['icon' => 'router', 'prefix' => 'mdi'],
['icon' => 'router-network', 'prefix' => 'mdi'],
['icon' => 'router-wireless', 'prefix' => 'mdi'],
['icon' => 'router-wireless-settings', 'prefix' => 'mdi'],
['icon' => 'ip', 'prefix' => 'mdi'],
['icon' => 'ip-network', 'prefix' => 'mdi'],
['icon' => 'ip-outline', 'prefix' => 'mdi'],
['icon' => 'dns', 'prefix' => 'mdi'],
['icon' => 'dns-outline', 'prefix' => 'mdi'],
['icon' => 'antenna', 'prefix' => 'mdi'],
['icon' => 'access-point', 'prefix' => 'mdi'],
['icon' => 'access-point-network', 'prefix' => 'mdi'],
['icon' => 'broadcast', 'prefix' => 'mdi'],
['icon' => 'broadcast-off', 'prefix' => 'mdi'],
['icon' => 'network', 'prefix' => 'mdi'],
['icon' => 'network-outline', 'prefix' => 'mdi'],
['icon' => 'network-off', 'prefix' => 'mdi'],
['icon' => 'network-off-outline', 'prefix' => 'mdi'],
// Security
['icon' => 'shield', 'prefix' => 'mdi'],
['icon' => 'shield-outline', 'prefix' => 'mdi'],
['icon' => 'shield-check', 'prefix' => 'mdi'],
['icon' => 'shield-lock', 'prefix' => 'mdi'],
['icon' => 'shield-key', 'prefix' => 'mdi'],
['icon' => 'shield-account', 'prefix' => 'mdi'],
['icon' => 'lock', 'prefix' => 'mdi'],
['icon' => 'lock-outline', 'prefix' => 'mdi'],
['icon' => 'lock-open', 'prefix' => 'mdi'],
['icon' => 'lock-open-outline', 'prefix' => 'mdi'],
['icon' => 'key', 'prefix' => 'mdi'],
['icon' => 'key-outline', 'prefix' => 'mdi'],
['icon' => 'key-variant', 'prefix' => 'mdi'],
['icon' => 'key-chain', 'prefix' => 'mdi'],
['icon' => 'certificate', 'prefix' => 'mdi'],
['icon' => 'certificate-outline', 'prefix' => 'mdi'],
['icon' => 'security', 'prefix' => 'mdi'],
['icon' => 'security-network', 'prefix' => 'mdi'],
['icon' => 'fingerprint', 'prefix' => 'mdi'],
['icon' => 'two-factor-authentication', 'prefix' => 'mdi'],
// Development & Code
['icon' => 'code-braces', 'prefix' => 'mdi'],
['icon' => 'code-brackets', 'prefix' => 'mdi'],
['icon' => 'code-tags', 'prefix' => 'mdi'],
['icon' => 'code-json', 'prefix' => 'mdi'],
['icon' => 'code-string', 'prefix' => 'mdi'],
['icon' => 'code-array', 'prefix' => 'mdi'],
['icon' => 'console', 'prefix' => 'mdi'],
['icon' => 'console-line', 'prefix' => 'mdi'],
['icon' => 'console-network', 'prefix' => 'mdi'],
['icon' => 'powershell', 'prefix' => 'mdi'],
['icon' => 'bash', 'prefix' => 'mdi'],
['icon' => 'api', 'prefix' => 'mdi'],
['icon' => 'api-off', 'prefix' => 'mdi'],
['icon' => 'webhook', 'prefix' => 'mdi'],
['icon' => 'function', 'prefix' => 'mdi'],
['icon' => 'function-variant', 'prefix' => 'mdi'],
['icon' => 'variable', 'prefix' => 'mdi'],
['icon' => 'variable-box', 'prefix' => 'mdi'],
['icon' => 'xml', 'prefix' => 'mdi'],
['icon' => 'graphql', 'prefix' => 'mdi'],
// Containers & DevOps
['icon' => 'docker', 'prefix' => 'mdi'],
['icon' => 'kubernetes', 'prefix' => 'mdi'],
['icon' => 'package', 'prefix' => 'mdi'],
['icon' => 'package-variant', 'prefix' => 'mdi'],
['icon' => 'package-variant-closed', 'prefix' => 'mdi'],
['icon' => 'cube', 'prefix' => 'mdi'],
['icon' => 'cube-outline', 'prefix' => 'mdi'],
['icon' => 'cube-scan', 'prefix' => 'mdi'],
['icon' => 'cube-send', 'prefix' => 'mdi'],
['icon' => 'layers', 'prefix' => 'mdi'],
['icon' => 'layers-outline', 'prefix' => 'mdi'],
['icon' => 'layers-triple', 'prefix' => 'mdi'],
['icon' => 'layers-triple-outline', 'prefix' => 'mdi'],
// Operating Systems & Platforms
['icon' => 'linux', 'prefix' => 'mdi'],
['icon' => 'ubuntu', 'prefix' => 'mdi'],
['icon' => 'debian', 'prefix' => 'mdi'],
['icon' => 'fedora', 'prefix' => 'mdi'],
['icon' => 'redhat', 'prefix' => 'mdi'],
['icon' => 'centos', 'prefix' => 'mdi'],
['icon' => 'microsoft-windows', 'prefix' => 'mdi'],
['icon' => 'apple', 'prefix' => 'mdi'],
['icon' => 'android', 'prefix' => 'mdi'],
['icon' => 'freebsd', 'prefix' => 'mdi'],
// Version Control
['icon' => 'git', 'prefix' => 'mdi'],
['icon' => 'github', 'prefix' => 'mdi'],
['icon' => 'gitlab', 'prefix' => 'mdi'],
['icon' => 'source-branch', 'prefix' => 'mdi'],
['icon' => 'source-commit', 'prefix' => 'mdi'],
['icon' => 'source-merge', 'prefix' => 'mdi'],
['icon' => 'source-pull', 'prefix' => 'mdi'],
['icon' => 'source-fork', 'prefix' => 'mdi'],
// Devices
['icon' => 'monitor', 'prefix' => 'mdi'],
['icon' => 'monitor-multiple', 'prefix' => 'mdi'],
['icon' => 'monitor-dashboard', 'prefix' => 'mdi'],
['icon' => 'desktop-tower', 'prefix' => 'mdi'],
['icon' => 'desktop-tower-monitor', 'prefix' => 'mdi'],
['icon' => 'desktop-classic', 'prefix' => 'mdi'],
['icon' => 'laptop', 'prefix' => 'mdi'],
['icon' => 'cellphone', 'prefix' => 'mdi'],
['icon' => 'tablet', 'prefix' => 'mdi'],
['icon' => 'television', 'prefix' => 'mdi'],
// Power & Energy
['icon' => 'power', 'prefix' => 'mdi'],
['icon' => 'power-plug', 'prefix' => 'mdi'],
['icon' => 'power-plug-off', 'prefix' => 'mdi'],
['icon' => 'power-cycle', 'prefix' => 'mdi'],
['icon' => 'power-on', 'prefix' => 'mdi'],
['icon' => 'power-off', 'prefix' => 'mdi'],
['icon' => 'power-standby', 'prefix' => 'mdi'],
['icon' => 'bolt', 'prefix' => 'mdi'],
['icon' => 'lightning-bolt', 'prefix' => 'mdi'],
['icon' => 'lightning-bolt-circle', 'prefix' => 'mdi'],
['icon' => 'battery', 'prefix' => 'mdi'],
['icon' => 'battery-charging', 'prefix' => 'mdi'],
['icon' => 'battery-outline', 'prefix' => 'mdi'],
// Actions & Operations
['icon' => 'sync', 'prefix' => 'mdi'],
['icon' => 'sync-circle', 'prefix' => 'mdi'],
['icon' => 'sync-off', 'prefix' => 'mdi'],
['icon' => 'refresh', 'prefix' => 'mdi'],
['icon' => 'refresh-circle', 'prefix' => 'mdi'],
['icon' => 'reload', 'prefix' => 'mdi'],
['icon' => 'restart', 'prefix' => 'mdi'],
['icon' => 'backup-restore', 'prefix' => 'mdi'],
['icon' => 'download', 'prefix' => 'mdi'],
['icon' => 'download-outline', 'prefix' => 'mdi'],
['icon' => 'download-circle', 'prefix' => 'mdi'],
['icon' => 'upload', 'prefix' => 'mdi'],
['icon' => 'upload-outline', 'prefix' => 'mdi'],
['icon' => 'upload-circle', 'prefix' => 'mdi'],
['icon' => 'content-copy', 'prefix' => 'mdi'],
['icon' => 'content-paste', 'prefix' => 'mdi'],
['icon' => 'content-cut', 'prefix' => 'mdi'],
// Settings & Configuration
['icon' => 'cog', 'prefix' => 'mdi'],
['icon' => 'cog-outline', 'prefix' => 'mdi'],
['icon' => 'cogs', 'prefix' => 'mdi'],
['icon' => 'cog-transfer', 'prefix' => 'mdi'],
['icon' => 'cog-sync', 'prefix' => 'mdi'],
['icon' => 'wrench', 'prefix' => 'mdi'],
['icon' => 'wrench-outline', 'prefix' => 'mdi'],
['icon' => 'hammer-wrench', 'prefix' => 'mdi'],
['icon' => 'toolbox', 'prefix' => 'mdi'],
['icon' => 'toolbox-outline', 'prefix' => 'mdi'],
['icon' => 'tune', 'prefix' => 'mdi'],
['icon' => 'tune-vertical', 'prefix' => 'mdi'],
// Files & Folders
['icon' => 'folder', 'prefix' => 'mdi'],
['icon' => 'folder-outline', 'prefix' => 'mdi'],
['icon' => 'folder-open', 'prefix' => 'mdi'],
['icon' => 'folder-open-outline', 'prefix' => 'mdi'],
['icon' => 'folder-network', 'prefix' => 'mdi'],
['icon' => 'folder-network-outline', 'prefix' => 'mdi'],
['icon' => 'folder-cog', 'prefix' => 'mdi'],
['icon' => 'folder-lock', 'prefix' => 'mdi'],
['icon' => 'folder-zip', 'prefix' => 'mdi'],
['icon' => 'file', 'prefix' => 'mdi'],
['icon' => 'file-outline', 'prefix' => 'mdi'],
['icon' => 'file-document', 'prefix' => 'mdi'],
['icon' => 'file-document-outline', 'prefix' => 'mdi'],
['icon' => 'file-code', 'prefix' => 'mdi'],
['icon' => 'file-code-outline', 'prefix' => 'mdi'],
['icon' => 'file-cog', 'prefix' => 'mdi'],
['icon' => 'file-lock', 'prefix' => 'mdi'],
// Charts & Analytics
['icon' => 'chart-line', 'prefix' => 'mdi'],
['icon' => 'chart-bar', 'prefix' => 'mdi'],
['icon' => 'chart-pie', 'prefix' => 'mdi'],
['icon' => 'chart-donut', 'prefix' => 'mdi'],
['icon' => 'chart-areaspline', 'prefix' => 'mdi'],
['icon' => 'chart-box', 'prefix' => 'mdi'],
['icon' => 'chart-timeline', 'prefix' => 'mdi'],
['icon' => 'chart-gantt', 'prefix' => 'mdi'],
['icon' => 'poll', 'prefix' => 'mdi'],
['icon' => 'finance', 'prefix' => 'mdi'],
['icon' => 'trending-up', 'prefix' => 'mdi'],
['icon' => 'trending-down', 'prefix' => 'mdi'],
['icon' => 'trending-neutral', 'prefix' => 'mdi'],
// Users & Accounts
['icon' => 'account', 'prefix' => 'mdi'],
['icon' => 'account-outline', 'prefix' => 'mdi'],
['icon' => 'account-circle', 'prefix' => 'mdi'],
['icon' => 'account-circle-outline', 'prefix' => 'mdi'],
['icon' => 'account-group', 'prefix' => 'mdi'],
['icon' => 'account-group-outline', 'prefix' => 'mdi'],
['icon' => 'account-multiple', 'prefix' => 'mdi'],
['icon' => 'account-key', 'prefix' => 'mdi'],
['icon' => 'account-lock', 'prefix' => 'mdi'],
['icon' => 'account-cog', 'prefix' => 'mdi'],
// Communication
['icon' => 'email', 'prefix' => 'mdi'],
['icon' => 'email-outline', 'prefix' => 'mdi'],
['icon' => 'email-open', 'prefix' => 'mdi'],
['icon' => 'message', 'prefix' => 'mdi'],
['icon' => 'message-outline', 'prefix' => 'mdi'],
['icon' => 'chat', 'prefix' => 'mdi'],
['icon' => 'chat-outline', 'prefix' => 'mdi'],
['icon' => 'forum', 'prefix' => 'mdi'],
['icon' => 'forum-outline', 'prefix' => 'mdi'],
// Notifications
['icon' => 'bell', 'prefix' => 'mdi'],
['icon' => 'bell-outline', 'prefix' => 'mdi'],
['icon' => 'bell-ring', 'prefix' => 'mdi'],
['icon' => 'bell-ring-outline', 'prefix' => 'mdi'],
['icon' => 'bell-badge', 'prefix' => 'mdi'],
['icon' => 'bell-off', 'prefix' => 'mdi'],
['icon' => 'bell-alert', 'prefix' => 'mdi'],
// Status & Alerts
['icon' => 'check', 'prefix' => 'mdi'],
['icon' => 'check-bold', 'prefix' => 'mdi'],
['icon' => 'check-circle', 'prefix' => 'mdi'],
['icon' => 'check-circle-outline', 'prefix' => 'mdi'],
['icon' => 'check-all', 'prefix' => 'mdi'],
['icon' => 'check-decagram', 'prefix' => 'mdi'],
['icon' => 'close', 'prefix' => 'mdi'],
['icon' => 'close-circle', 'prefix' => 'mdi'],
['icon' => 'close-circle-outline', 'prefix' => 'mdi'],
['icon' => 'alert', 'prefix' => 'mdi'],
['icon' => 'alert-circle', 'prefix' => 'mdi'],
['icon' => 'alert-circle-outline', 'prefix' => 'mdi'],
['icon' => 'alert-octagon', 'prefix' => 'mdi'],
['icon' => 'alert-decagram', 'prefix' => 'mdi'],
['icon' => 'information', 'prefix' => 'mdi'],
['icon' => 'information-outline', 'prefix' => 'mdi'],
['icon' => 'help-circle', 'prefix' => 'mdi'],
['icon' => 'help-circle-outline', 'prefix' => 'mdi'],
// Time & Calendar
['icon' => 'clock', 'prefix' => 'mdi'],
['icon' => 'clock-outline', 'prefix' => 'mdi'],
['icon' => 'clock-fast', 'prefix' => 'mdi'],
['icon' => 'clock-alert', 'prefix' => 'mdi'],
['icon' => 'timer', 'prefix' => 'mdi'],
['icon' => 'timer-outline', 'prefix' => 'mdi'],
['icon' => 'calendar', 'prefix' => 'mdi'],
['icon' => 'calendar-outline', 'prefix' => 'mdi'],
['icon' => 'calendar-clock', 'prefix' => 'mdi'],
['icon' => 'calendar-check', 'prefix' => 'mdi'],
['icon' => 'history', 'prefix' => 'mdi'],
// Navigation & Search
['icon' => 'magnify', 'prefix' => 'mdi'],
['icon' => 'magnify-plus', 'prefix' => 'mdi'],
['icon' => 'magnify-minus', 'prefix' => 'mdi'],
['icon' => 'magnify-scan', 'prefix' => 'mdi'],
['icon' => 'eye', 'prefix' => 'mdi'],
['icon' => 'eye-outline', 'prefix' => 'mdi'],
['icon' => 'eye-off', 'prefix' => 'mdi'],
['icon' => 'eye-off-outline', 'prefix' => 'mdi'],
['icon' => 'filter', 'prefix' => 'mdi'],
['icon' => 'filter-outline', 'prefix' => 'mdi'],
['icon' => 'sort', 'prefix' => 'mdi'],
['icon' => 'sort-ascending', 'prefix' => 'mdi'],
['icon' => 'sort-descending', 'prefix' => 'mdi'],
// World & Globe
['icon' => 'earth', 'prefix' => 'mdi'],
['icon' => 'earth-arrow-right', 'prefix' => 'mdi'],
['icon' => 'earth-box', 'prefix' => 'mdi'],
['icon' => 'earth-off', 'prefix' => 'mdi'],
['icon' => 'web', 'prefix' => 'mdi'],
['icon' => 'web-box', 'prefix' => 'mdi'],
['icon' => 'web-clock', 'prefix' => 'mdi'],
['icon' => 'domain', 'prefix' => 'mdi'],
// Home & Building
['icon' => 'home', 'prefix' => 'mdi'],
['icon' => 'home-outline', 'prefix' => 'mdi'],
['icon' => 'home-city', 'prefix' => 'mdi'],
['icon' => 'home-city-outline', 'prefix' => 'mdi'],
['icon' => 'office-building', 'prefix' => 'mdi'],
['icon' => 'office-building-outline', 'prefix' => 'mdi'],
['icon' => 'office-building-cog', 'prefix' => 'mdi'],
['icon' => 'domain', 'prefix' => 'mdi'],
// Favorites & Ratings
['icon' => 'star', 'prefix' => 'mdi'],
['icon' => 'star-outline', 'prefix' => 'mdi'],
['icon' => 'star-circle', 'prefix' => 'mdi'],
['icon' => 'star-circle-outline', 'prefix' => 'mdi'],
['icon' => 'heart', 'prefix' => 'mdi'],
['icon' => 'heart-outline', 'prefix' => 'mdi'],
['icon' => 'thumb-up', 'prefix' => 'mdi'],
['icon' => 'thumb-up-outline', 'prefix' => 'mdi'],
['icon' => 'thumb-down', 'prefix' => 'mdi'],
['icon' => 'thumb-down-outline', 'prefix' => 'mdi'],
// Misc
['icon' => 'rocket', 'prefix' => 'mdi'],
['icon' => 'rocket-launch', 'prefix' => 'mdi'],
['icon' => 'rocket-launch-outline', 'prefix' => 'mdi'],
['icon' => 'fire', 'prefix' => 'mdi'],
['icon' => 'fire-circle', 'prefix' => 'mdi'],
['icon' => 'bug', 'prefix' => 'mdi'],
['icon' => 'bug-outline', 'prefix' => 'mdi'],
['icon' => 'bug-check', 'prefix' => 'mdi'],
['icon' => 'puzzle', 'prefix' => 'mdi'],
['icon' => 'puzzle-outline', 'prefix' => 'mdi'],
['icon' => 'tag', 'prefix' => 'mdi'],
['icon' => 'tag-outline', 'prefix' => 'mdi'],
['icon' => 'tag-multiple', 'prefix' => 'mdi'],
['icon' => 'bookmark', 'prefix' => 'mdi'],
['icon' => 'bookmark-outline', 'prefix' => 'mdi'],
['icon' => 'link', 'prefix' => 'mdi'],
['icon' => 'link-variant', 'prefix' => 'mdi'],
['icon' => 'link-off', 'prefix' => 'mdi'],
['icon' => 'view-dashboard', 'prefix' => 'mdi'],
['icon' => 'view-dashboard-outline', 'prefix' => 'mdi'],
['icon' => 'view-grid', 'prefix' => 'mdi'],
['icon' => 'view-grid-outline', 'prefix' => 'mdi'],
['icon' => 'view-list', 'prefix' => 'mdi'],
['icon' => 'view-list-outline', 'prefix' => 'mdi'],
['icon' => 'menu', 'prefix' => 'mdi'],
['icon' => 'dots-horizontal', 'prefix' => 'mdi'],
['icon' => 'dots-vertical', 'prefix' => 'mdi'],
['icon' => 'apps', 'prefix' => 'mdi'],
['icon' => 'apps-box', 'prefix' => 'mdi'],
['icon' => 'application', 'prefix' => 'mdi'],
['icon' => 'application-outline', 'prefix' => 'mdi'],
['icon' => 'window-maximize', 'prefix' => 'mdi'],
['icon' => 'window-minimize', 'prefix' => 'mdi'],
['icon' => 'window-restore', 'prefix' => 'mdi'],
['icon' => 'monitor-cellphone', 'prefix' => 'mdi'],
['icon' => 'cellphone-link', 'prefix' => 'mdi'],
['icon' => 'responsive', 'prefix' => 'mdi'],
['icon' => 'table', 'prefix' => 'mdi'],
['icon' => 'table-large', 'prefix' => 'mdi'],
['icon' => 'table-cog', 'prefix' => 'mdi'],
['icon' => 'archive', 'prefix' => 'mdi'],
['icon' => 'archive-outline', 'prefix' => 'mdi'],
['icon' => 'archive-cog', 'prefix' => 'mdi'],
['icon' => 'sun-wireless', 'prefix' => 'mdi'],
['icon' => 'white-balance-sunny', 'prefix' => 'mdi'],
['icon' => 'weather-night', 'prefix' => 'mdi'],
['icon' => 'moon-waning-crescent', 'prefix' => 'mdi'],
['icon' => 'camera', 'prefix' => 'mdi'],
['icon' => 'camera-outline', 'prefix' => 'mdi'],
['icon' => 'image', 'prefix' => 'mdi'],
['icon' => 'image-outline', 'prefix' => 'mdi'],
['icon' => 'image-multiple', 'prefix' => 'mdi'],
['icon' => 'video', 'prefix' => 'mdi'],
['icon' => 'video-outline', 'prefix' => 'mdi'],
['icon' => 'music', 'prefix' => 'mdi'],
['icon' => 'music-note', 'prefix' => 'mdi'],
['icon' => 'microphone', 'prefix' => 'mdi'],
['icon' => 'microphone-outline', 'prefix' => 'mdi'],
['icon' => 'headphones', 'prefix' => 'mdi'],
['icon' => 'speaker', 'prefix' => 'mdi'],
['icon' => 'play', 'prefix' => 'mdi'],
['icon' => 'pause', 'prefix' => 'mdi'],
['icon' => 'stop', 'prefix' => 'mdi'],
['icon' => 'skip-next', 'prefix' => 'mdi'],
['icon' => 'skip-previous', 'prefix' => 'mdi'],
['icon' => 'fast-forward', 'prefix' => 'mdi'],
['icon' => 'rewind', 'prefix' => 'mdi'],
];
@endphp
@foreach($iconList as $iconData)
<button x-show="isIconVisible('{{ $iconData['prefix'] }}', '{{ $iconData['icon'] }}')" @click="icon = '{{ $iconData['icon'] }}'; iconPrefix = '{{ $iconData['prefix'] }}'" :class="icon === '{{ $iconData['icon'] }}' && iconPrefix === '{{ $iconData['prefix'] }}' ? 'ring-2 ring-primary' : ''" class="icon-btn rounded p-2 border flex items-center justify-center hover:bg-secondary/10 dark:border-base/20" data-icon="{{ $iconData['icon'] }}" data-prefix="{{ $iconData['prefix'] }}">
@php
if ($iconData['prefix'] === 'vaadin') {
$componentName = 'vaadin-' . str_replace('_', '-', $iconData['icon']);
} else {
$componentName = $iconData['prefix'] . '-' . $iconData['icon'];
}
@endphp
<x-dynamic-component :component="$componentName" class="size-5" />
</button>
@endforeach
</div>
<div class="flex flex-wrap gap-2 mb-4">
<template x-for="c in colors" :key="c">
<button @click="color = c" :style="'background:' + (c || '#e5e7eb')" :class="color === c ? 'ring-2 ring-primary' : ''" class="size-6 rounded-full border"></button>
</template>
</div>
<div class="flex gap-2 justify-end">
<button @click="resetIcon()" class="text-xs text-base/60 hover:text-danger">Reset</button>
<button @click="saveIcon()" class="px-3 py-1 rounded bg-primary text-white text-xs hover:bg-primary/80">Save</button>
</div>
<template x-if="message">
<div class="mt-2 text-xs text-success" x-text="message"></div>
</template>
</div>
</div>
<script>
function serverIconModal({serverId, initialIcon, initialColor, initialPrefix}) {
return {
show: false,
icon: initialIcon || '',
iconPrefix: initialPrefix || 'ri',
color: initialColor || '',
search: '',
message: '',
colors: [
'', '#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981',
'#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7',
'#d946ef', '#ec4899', '#f43f5e', '#78716c', '#64748b'
],
openModal() {
this.show = true;
this.message = '';
this.search = '';
},
closeModal() { this.show = false; },
resetIcon() {
this.icon = '';
this.iconPrefix = 'ri';
this.color = '';
},
isIconVisible(prefix, iconName) {
if (!this.search) return true;
const query = this.search.toLowerCase();
const searchText = (prefix + '-' + iconName).toLowerCase();
return searchText.includes(query) || iconName.toLowerCase().includes(query);
},
getIconSvg(iconName, prefix) {
const btn = document.querySelector(`.icon-btn[data-icon="${iconName}"][data-prefix="${prefix}"]`);
if (btn) {
const svg = btn.querySelector('svg');
if (svg) {
const cloned = svg.cloneNode(true);
cloned.classList.remove('size-5');
cloned.classList.add('size-8');
return cloned.outerHTML;
}
}
return '';
},
saveIcon() {
fetch(`/proxmox/server/${serverId}/icon`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
},
body: JSON.stringify({ icon: this.icon, color: this.color, prefix: this.iconPrefix })
})
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => {
if (data.success) {
this.message = 'Icon saved!';
setTimeout(() => { this.closeModal(); location.reload(); }, 800);
} else {
this.message = 'Failed to save icon.';
}
})
.catch(() => {
this.message = 'Network error. Please try again.';
});
}
}
}
// Server Tags Manager - Simple tags stored in database
function serverTagsManager({serverId, tag1, tag1color, tag2, tag2color, tag3, tag3color}) {
return {
serverId: serverId,
show: false,
saving: false,
tag1: tag1 || '',
tag1color: tag1color || '#3498db',
tag2: tag2 || '',
tag2color: tag2color || '#2ecc71',
tag3: tag3 || '',
tag3color: tag3color || '#e74c3c',
colorPickerFor: null,
message: '',
messageType: 'success',
tagColors: [
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#1abc9c', '#3498db',
'#9b59b6', '#e91e63', '#00bcd4', '#8bc34a', '#ff5722', '#607d8b',
'#795548', '#9e9e9e', '#673ab7', '#4caf50'
],
openModal() {
this.show = true;
this.message = '';
this.colorPickerFor = null;
},
closeModal() {
this.show = false;
this.colorPickerFor = null;
},
selectColorFor(tagNum) {
this.colorPickerFor = this.colorPickerFor === tagNum ? null : tagNum;
},
setColor(tagNum, color) {
if (tagNum === 1) this.tag1color = color;
else if (tagNum === 2) this.tag2color = color;
else if (tagNum === 3) this.tag3color = color;
this.colorPickerFor = null;
},
getColor(tagNum) {
if (tagNum === 1) return this.tag1color;
if (tagNum === 2) return this.tag2color;
if (tagNum === 3) return this.tag3color;
return '#3498db';
},
saveTags() {
this.saving = true;
this.message = '';
fetch(`/api/proxmox/tags/${this.serverId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({
tag1: this.tag1.trim(),
tag1color: this.tag1color,
tag2: this.tag2.trim(),
tag2color: this.tag2color,
tag3: this.tag3.trim(),
tag3color: this.tag3color
})
})
.then(r => {
if (!r.ok) {
return r.text().then(text => {
throw new Error(`HTTP ${r.status}: ${text.substring(0, 100)}`);
});
}
return r.json();
})
.then(data => {
this.saving = false;
if (data.success) {
this.message = 'Tags saved!';
this.messageType = 'success';
setTimeout(() => { this.closeModal(); location.reload(); }, 1000);
} else {
this.message = data.error || 'Failed to save tags';
this.messageType = 'error';
}
})
.catch(err => {
this.saving = false;
this.message = 'Error: ' + err.message;
this.messageType = 'error';
console.error('Tag save error:', err);
});
}
}
}
</script>
</div>
<!-- Server Tags Component -->
<div x-data="serverTagsManager({
serverId: '{{ $server->id }}',
tag1: '{{ $server->tag1 ?? '' }}',
tag1color: '{{ $server->tag1color ?? '#3498db' }}',
tag2: '{{ $server->tag2 ?? '' }}',
tag2color: '{{ $server->tag2color ?? '#2ecc71' }}',
tag3: '{{ $server->tag3 ?? '' }}',
tag3color: '{{ $server->tag3color ?? '#e74c3c' }}'
})" class="flex items-center gap-2">
<!-- Display Tags -->
<div class="flex items-center gap-1.5 flex-wrap">
<template x-if="tag1">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium text-white cursor-pointer hover:opacity-80 transition-opacity"
:style="'background-color: ' + tag1color"
@click="openModal()">
<span x-text="tag1"></span>
</span>
</template>
<template x-if="tag2">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium text-white cursor-pointer hover:opacity-80 transition-opacity"
:style="'background-color: ' + tag2color"
@click="openModal()">
<span x-text="tag2"></span>
</span>
</template>
<template x-if="tag3">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium text-white cursor-pointer hover:opacity-80 transition-opacity"
:style="'background-color: ' + tag3color"
@click="openModal()">
<span x-text="tag3"></span>
</span>
</template>
</div>
<!-- Add Tag Button -->
<button @click="openModal()"
class="inline-flex items-center justify-center size-6 rounded-full border border-dashed border-base/30 text-base/50 hover:border-primary hover:text-primary transition-colors"
title="Manage tags (up to 3)">
<x-ri-price-tag-3-line class="size-3.5" />
</button>
<!-- Tags Modal -->
<div x-show="show" x-cloak @click.self="closeModal()" @keydown.escape.window="closeModal()" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div class="bg-background-secondary rounded-xl shadow-2xl p-6 w-full max-w-md relative">
<button @click="closeModal()" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<h3 class="text-lg font-semibold mb-4">Manage Server Tags</h3>
<p class="text-sm text-base/60 mb-4">Add up to 3 tags to organize and identify your server.</p>
<!-- Tag 1 -->
<div class="space-y-3 mb-4">
<div class="flex items-center gap-2">
<span class="text-xs text-base/50 w-12">Tag 1:</span>
<input type="text" x-model="tag1" placeholder="Tag 1"
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
maxlength="32" />
<button @click="selectColorFor(1)"
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
:style="'background-color: ' + tag1color"
title="Change color">
</button>
</div>
<!-- Tag 2 -->
<div class="flex items-center gap-2">
<span class="text-xs text-base/50 w-12">Tag 2:</span>
<input type="text" x-model="tag2" placeholder="Tag 2"
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
maxlength="32" />
<button @click="selectColorFor(2)"
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
:style="'background-color: ' + tag2color"
title="Change color">
</button>
</div>
<!-- Tag 3 -->
<div class="flex items-center gap-2">
<span class="text-xs text-base/50 w-12">Tag 3:</span>
<input type="text" x-model="tag3" placeholder="Tag 3"
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
maxlength="32" />
<button @click="selectColorFor(3)"
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
:style="'background-color: ' + tag3color"
title="Change color">
</button>
</div>
</div>
<!-- Color Picker -->
<template x-if="colorPickerFor !== null">
<div class="mb-4 p-3 bg-background rounded-lg border border-neutral">
<p class="text-xs text-base/60 mb-2">Select color:</p>
<div class="flex flex-wrap gap-2">
<template x-for="c in tagColors" :key="c">
<button @click="setColor(colorPickerFor, c)"
:style="'background-color: ' + c"
:class="getColor(colorPickerFor) === c ? 'ring-2 ring-offset-2 ring-primary' : ''"
class="size-7 rounded-full border border-white shadow-sm hover:scale-110 transition-transform">
</button>
</template>
</div>
</div>
</template>
<div class="flex gap-2 justify-end">
<button @click="closeModal()" class="px-4 py-2 text-sm text-base/60 hover:text-base">Cancel</button>
<button @click="saveTags()" class="px-4 py-2 rounded-lg bg-primary text-white text-sm hover:bg-primary/80 transition-colors">
<span x-show="!saving">Save Tags</span>
<span x-show="saving" class="flex items-center gap-2">
<svg class="animate-spin size-4" 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>
Saving...
</span>
</button>
</div>
<template x-if="message">
<div class="mt-3 text-sm" :class="messageType === 'success' ? 'text-success' : 'text-danger'" x-text="message"></div>
</template>
</div>
</div>
</div>
<div class="hidden lg:flex flex-row gap-2 ml-auto justify-end items-center">
<x-button.primary wire:click="do('start')" class="disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'stopped'"
title="Start the virtual machine">
<x-loading target="do('start')" />
<x-ri-play-fill class="size-4" />
<div wire:loading.remove wire:target="do('start')">
Start
</div>
</x-button.primary>
<div x-show="isAdvancedView" x-transition>
<x-button.secondary wire:click="do('shutdown')" class="text-nowrap disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'running'"
title="Graceful shutdown via ACPI signal">
<x-loading target="do('shutdown')" />
<x-ri-stop-fill class="size-4" />
<div wire:loading.remove wire:target="do('shutdown')">
Stop (ACPI)
</div>
</x-button.secondary>
</div>
<div x-show="isAdvancedView" x-transition>
<x-button.secondary wire:click="do('reboot')" class="text-nowrap disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'running'"
title="Graceful reboot via ACPI signal">
<x-loading target="do('reboot')" />
<x-ri-loop-left-line class="size-4" />
<div wire:loading.remove wire:target="do('reboot')">
Reboot (ACPI)
</div>
</x-button.secondary>
</div>
<x-button.danger wire:click="do('stop')" class="text-nowrap disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'running'"
title="Force power off — unsaved data may be lost">
<x-loading target="do('stop')" />
<x-ri-shut-down-line class="size-4" />
<div wire:loading.remove wire:target="do('stop')">
Power Off
</div>
</x-button.danger>
</div>
<div class="flex lg:hidden w-full flex-col gap-2">
<div class="flex flex-col gap-2 w-full">
<x-button.primary wire:click="do('start')" class="w-full disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'stopped'"
title="Start the virtual machine">
<x-loading target="do('start')" />
<x-ri-play-fill class="size-4" />
<div wire:loading.remove wire:target="do('start')">
Start
</div>
</x-button.primary>
<div x-show="isAdvancedView" x-transition>
<x-button.secondary wire:click="do('shutdown')" class="w-full text-nowrap disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'running'"
title="Graceful shutdown via ACPI signal">
<x-loading target="do('shutdown')" />
<x-ri-stop-fill class="size-4" />
<div wire:loading.remove wire:target="do('shutdown')">
Stop (ACPI)
</div>
</x-button.secondary>
</div>
<div x-show="isAdvancedView" x-transition>
<x-button.secondary wire:click="do('reboot')" class="w-full text-nowrap disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'running'"
title="Graceful reboot via ACPI signal">
<x-loading target="do('reboot')" />
<x-ri-loop-left-line class="size-4" />
<div wire:loading.remove wire:target="do('reboot')">
Reboot (ACPI)
</div>
</x-button.secondary>
</div>
<x-button.danger wire:click="do('stop')"
class="text-nowrap w-full disabled:cursor-not-allowed"
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
:disabled="$vmStatus !== 'running'"
title="Force power off — unsaved data may be lost">
<x-loading target="do('stop')" />
<x-ri-shut-down-line class="size-4" />
<div wire:loading.remove wire:target="do('stop')">
Power Off
</div>
</x-button.danger>
</div>
</div>
</div>
@if ($view !== 'statistics')
<!-- Preload Statistics tab content when entering the server page -->
<div
x-init="
if (window.fetch) {
setTimeout(function () {
try {
fetch('{{ route('proxmox.server.statistics', ['server' => $server->id]) }}', {
credentials: 'same-origin',
headers: { 'X-Proxmox-Preload': 'true' },
});
} catch (e) {}
}, 500);
}
"
class="hidden"
></div>
@endif
@php
$tabs = [
['label' => 'Overview', 'view' => 'overview', 'route' => route('proxmox.server.overview', ['server' => $server->id])],
['label' => 'Statistics', 'view' => 'statistics', 'route' => route('proxmox.server.statistics', ['server' => $server->id]), 'advanced' => true],
[
'label' => 'Hardware',
'dropdown' => true,
'items' => [
['label' => 'Boot Settings', 'view' => 'boot-order', 'route' => route('proxmox.server.boot-order', ['server' => $server->id]), 'advanced' => true],
['label' => 'Display', 'view' => 'display', 'route' => route('proxmox.server.display', ['server' => $server->id]), 'advanced' => true],
['label' => 'Storage', 'view' => 'storage', 'route' => route('proxmox.server.storage', ['server' => $server->id]), 'advanced' => true],
['label' => 'System', 'view' => 'system', 'route' => route('proxmox.server.system', ['server' => $server->id]), 'advanced' => true],
]
],
[
'label' => 'Networking',
'dropdown' => true,
'items' => [
['label' => 'Configuration', 'view' => 'network', 'route' => route('proxmox.server.network', ['server' => $server->id])],
['label' => 'Firewall', 'view' => 'firewall', 'route' => route('proxmox.server.firewall', ['server' => $server->id]), 'advanced' => true],
['label' => 'Subdomain', 'view' => 'subdomain', 'route' => route('proxmox.server.subdomain', ['server' => $server->id]), 'advanced' => true],
]
],
['label' => 'Cloud-init', 'view' => 'cloud-init', 'route' => route('proxmox.server.cloud-init', ['server' => $server->id]), 'advanced' => true],
];
if($server->service->product->settings()->where('key', 'backups')->first()?->value > 0) {
$tabs[] = ['label' => 'Backups', 'view' => 'backups', 'route' => route('proxmox.server.backups', ['server' => $server->id])];
}
if($server->node?->vnc_enabled) {
$tabs[] = [
'label' => 'Console',
'dropdown' => true,
'items' => [
['label' => 'NoVNC', 'view' => 'vnc', 'route' => route('proxmox.server.vnc', ['server' => $server->id])],
['label' => 'Xterm.js', 'view' => 'xterm', 'route' => route('proxmox.server.xterm', ['server' => $server->id])],
]
];
}
$tabs[] = ['label' => 'Help', 'view' => 'help', 'route' => route('proxmox.server.help', ['server' => $server->id]), 'advanced' => true];
@endphp
<div class="mb-10 mt-3">
<style>
/* Page Transition Styles - Only slide-in for new content */
.proxmox-page-wrapper {
opacity: 1;
/* Do NOT set transform here — any transform (even translateX(0)) creates a
CSS containing block, which makes 'fixed' children (modal backdrops)
position relative to this element instead of the viewport. */
}
.proxmox-page-wrapper.slide-in {
animation: slideIn 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.proxmox-page-wrapper.slide-in {
animation: none;
opacity: 1;
}
}
/* Modern Navigation Styles */
.proxmox-nav-container {
background: color-mix(in srgb, var(--color-background-secondary) 60%, transparent);
backdrop-filter: blur(12px);
border: 1px solid color-mix(in srgb, var(--color-neutral) 30%, transparent);
border-radius: 14px;
padding: 8px;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.05),
0 0 0 1px rgba(0, 0, 0, 0.02);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 10;
}
.dark .proxmox-nav-container {
background: color-mix(in srgb, var(--color-background-secondary) 80%, transparent);
border-color: color-mix(in srgb, var(--color-neutral) 40%, transparent);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.proxmox-nav-tabs {
display: flex;
gap: 4px;
flex-wrap: wrap;
flex: 1;
}
.proxmox-tab-link {
position: relative;
font-size: 0.8125rem;
font-weight: 600;
padding: 10px 16px;
border-radius: 10px;
letter-spacing: -0.01em;
white-space: nowrap;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
overflow: hidden;
}
.proxmox-tab-link::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg,
rgba(37, 99, 235, 0.08),
rgba(59, 130, 246, 0.05));
opacity: 0;
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 0;
}
.proxmox-tab-link:hover::before {
opacity: 1;
}
.proxmox-tab-link span {
position: relative;
z-index: 1;
}
.proxmox-tab-link:hover {
transform: translateY(-1px);
border-color: rgba(37, 99, 235, 0.15);
}
.proxmox-tab-link.active {
background: linear-gradient(135deg,
rgba(37, 99, 235, 0.12),
rgba(59, 130, 246, 0.08));
border-color: rgba(37, 99, 235, 0.25);
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
color: rgb(37, 99, 235);
}
.dark .proxmox-tab-link.active {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.18),
rgba(37, 99, 235, 0.12));
border-color: rgba(59, 130, 246, 0.35);
color: rgb(96, 165, 250);
}
.proxmox-tab-link:not(.active) {
display: flex;
align-items: center;
justify-content: center;
color: color-mix(in srgb, var(--color-base) 65%, transparent);
}
.dark .proxmox-tab-link:not(.active) {
color: color-mix(in srgb, var(--color-base) 65%, transparent);
}
/* Modern Toggle Switch */
.modern-toggle-container {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 6px 12px;
background: color-mix(in srgb, var(--color-background) 50%, transparent);
border: 1px solid color-mix(in srgb, var(--color-neutral) 40%, transparent);
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.dark .modern-toggle-container {
background: color-mix(in srgb, var(--color-background) 60%, transparent);
border-color: color-mix(in srgb, var(--color-neutral) 50%, transparent);
}
.modern-toggle-label {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
transition: all 0.2s ease;
user-select: none;
}
.modern-toggle-label.inactive {
color: color-mix(in srgb, var(--color-base) 40%, transparent);
}
.modern-toggle-label.active {
color: rgb(37, 99, 235);
}
.dark .modern-toggle-label.active {
color: rgb(96, 165, 250);
}
.dark .modern-toggle-label.inactive {
color: color-mix(in srgb, var(--color-base) 40%, transparent);
}
.modern-toggle-switch {
position: relative;
width: 48px;
height: 26px;
background: linear-gradient(135deg,
rgba(156, 163, 175, 0.4),
rgba(209, 213, 219, 0.4));
border-radius: 13px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(var(--neutral-rgb, 156, 163, 175), 0.2);
box-shadow:
inset 0 1px 3px rgba(0, 0, 0, 0.1),
0 1px 2px rgba(0, 0, 0, 0.05);
}
.dark .modern-toggle-switch {
background: linear-gradient(135deg,
rgba(55, 65, 81, 0.6),
rgba(75, 85, 99, 0.6));
border-color: rgba(var(--neutral-rgb, 75, 85, 99), 0.4);
}
.modern-toggle-switch.active {
background: linear-gradient(135deg,
rgb(37, 99, 235),
rgb(59, 130, 246));
border-color: rgba(37, 99, 235, 0.3);
box-shadow:
0 0 0 3px rgba(37, 99, 235, 0.1),
inset 0 1px 3px rgba(0, 0, 0, 0.15),
0 2px 6px rgba(37, 99, 235, 0.25);
}
.dark .modern-toggle-switch.active {
background: linear-gradient(135deg,
rgb(59, 130, 246),
rgb(96, 165, 250));
box-shadow:
0 0 0 3px rgba(59, 130, 246, 0.15),
inset 0 1px 3px rgba(0, 0, 0, 0.2),
0 2px 8px rgba(59, 130, 246, 0.3);
}
.modern-toggle-handle {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: linear-gradient(135deg,
rgb(255, 255, 255),
rgb(249, 250, 251));
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(0, 0, 0, 0.05);
}
.modern-toggle-switch.active .modern-toggle-handle {
transform: translateX(22px);
box-shadow:
0 3px 8px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.modern-toggle-switch.disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Animation for tab transitions */
@keyframes tabSlideIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.proxmox-tab-wrapper {
animation: tabSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
}
/* Dropdown Styles */
.dropdown-wrapper {
position: relative;
display: flex;
align-items: center;
}
.dropdown-trigger {
display: flex;
align-items: center;
gap: 4px;
}
.dropdown-icon {
transition: transform 0.2s ease;
width: 14px;
height: 14px;
}
.dropdown-wrapper.open .dropdown-icon {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
min-width: 180px;
background: rgba(255, 255, 255, 0.705);
backdrop-filter: blur(10px);
border: 1px solid rgba(209, 213, 219, 0.3);
border-radius: 12px;
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(0, 0, 0, 0.05);
z-index: 100;
overflow: hidden;
}
.dark .dropdown-menu {
background: rgba(30, 30, 35, 0.98);
border-color: rgba(75, 85, 99, 0.4);
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.dropdown-item {
display: block;
padding: 10px 16px;
font-size: 0.8125rem;
font-weight: 600;
transition: all 0.2s ease;
border-left: 3px solid transparent;
color: var(--color-base);
}
.dropdown-item:hover {
background: linear-gradient(135deg,
rgba(37, 99, 235, 0.15),
rgba(59, 130, 246, 0.1));
border-left-color: rgba(37, 99, 235, 0.6);
color: var(--color-base);
}
.dropdown-item.active {
background: linear-gradient(135deg,
rgba(37, 99, 235, 0.15),
rgba(59, 130, 246, 0.1));
border-left-color: rgb(37, 99, 235);
color: rgb(37, 99, 235);
}
.dark .dropdown-item.active {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.15));
border-left-color: rgb(96, 165, 250);
color: rgb(96, 165, 250);
}
.dropdown-divider {
height: 1px;
background: color-mix(in srgb, var(--color-neutral) 30%, transparent);
margin: 4px 0;
}
/* Hide sidebar in problematic resolution range (768px - 1023px) */
@media screen and (min-width: 768px) and (max-width: 1023px) {
html body > div.w-full.flex.flex-grow > aside.w-64.h-screen,
html body > div.w-full.flex > aside.w-64.h-screen,
html body aside.w-64.h-screen.mt-14,
html body aside.w-64.h-screen.mt-24,
body aside.w-64[class*="md:flex"] {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
width: 0 !important;
max-width: 0 !important;
overflow: hidden !important;
position: absolute !important;
left: -9999px !important;
}
html body > div.w-full.flex.flex-grow > div[class*="md:ml-64"],
html body > div.w-full.flex > div[class*="md:ml-64"],
body > div > div[class*="md:ml-64"],
div[class*="md:ml-64"] {
margin-left: 0 !important;
}
html body > div.w-full.flex.flex-grow > div[class*="rtl:md:mr-64"],
html body > div.w-full.flex > div[class*="rtl:md:mr-64"],
body > div > div[class*="rtl:md:mr-64"],
div[class*="rtl:md:mr-64"] {
margin-right: 0 !important;
}
}
/* Loading skeleton animation */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton-loading {
background: linear-gradient(90deg,
transparent 25%,
color-mix(in srgb, var(--color-base) 8%, transparent) 50%,
transparent 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
</style>
<div class="hidden lg:block">
<div class="proxmox-nav-container">
<div class="flex items-center justify-between gap-4">
<div class="proxmox-nav-tabs">
@foreach($tabs as $tabIndex => $tab)
@php
$isAdvancedTab = $tab['advanced'] ?? false;
$isDropdown = $tab['dropdown'] ?? false;
// Check if current view is in dropdown items
$isDropdownActive = false;
$hasVisibleItems = false;
if ($isDropdown && isset($tab['items'])) {
foreach ($tab['items'] as $item) {
if ($view === $item['view']) {
$isDropdownActive = true;
}
$itemAdvanced = $item['advanced'] ?? false;
if (!$itemAdvanced) {
$hasVisibleItems = true;
}
}
}
@endphp
@if($isDropdown)
<div class="proxmox-tab-wrapper dropdown-wrapper"
x-data="{ open: false }"
@click.outside="open = false"
@if(!$hasVisibleItems)
x-show="isAdvancedView"
x-transition
@endif>
<button @click="open = !open"
class="proxmox-tab-link {{ $isDropdownActive ? 'active' : '' }}">
<span class="dropdown-trigger">
{{ $tab['label'] }}
<svg class="dropdown-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</span>
</button>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95 -translate-y-2"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="dropdown-menu">
@foreach($tab['items'] as $itemIndex => $item)
@php
$itemAdvanced = $item['advanced'] ?? false;
@endphp
<div @if($itemAdvanced)
x-show="isAdvancedView"
@endif>
<a href="{{ $item['route'] }}" wire:navigate
class="dropdown-item {{ $view === $item['view'] ? 'active' : '' }}"
@click="open = false">
{{ $item['label'] }}
</a>
@if($itemIndex < count($tab['items']) - 1 && $itemAdvanced && !($tab['items'][$itemIndex + 1]['advanced'] ?? false))
<div x-show="isAdvancedView" class="dropdown-divider"></div>
@endif
</div>
@endforeach
</div>
</div>
@else
<div class="proxmox-tab-wrapper"
@if($isAdvancedTab)
x-show="isAdvancedView"
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"
@endif>
<a href="{{ $tab['route'] }}" wire:navigate
class="proxmox-tab-link {{ $view === $tab['view'] ? 'active' : '' }}">
<span>{{ $tab['label'] }}</span>
</a>
</div>
@endif
@endforeach
</div>
<div class="modern-toggle-container"
:class="isAdvancedOnlyView ? 'opacity-50 cursor-not-allowed pointer-events-none' : ''">
<span class="modern-toggle-label"
:class="!isAdvancedView ? 'active' : 'inactive'">
Standard
</span>
<div class="modern-toggle-switch"
:class="{ 'active': isAdvancedView, 'disabled': isAdvancedOnlyView }"
@click="!isAdvancedOnlyView && toggleView()">
<div class="modern-toggle-handle"></div>
</div>
<span class="modern-toggle-label"
:class="isAdvancedView ? 'active' : 'inactive'">
Advanced
</span>
</div>
</div>
</div>
</div>
<div class="lg:hidden" x-data="{
open: false,
isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced',
isAdvancedOnlyView: {{ in_array($view, ['boot-order', 'cloud-init', 'display', 'system', 'storage', 'subdomain', 'statistics', 'help', 'firewall']) ? 'true' : 'false' }},
toggleView() {
if (!this.isAdvancedOnlyView) {
this.isAdvancedView = !this.isAdvancedView;
localStorage.setItem('proxmox-view-mode', this.isAdvancedView ? 'advanced' : 'basic');
window.dispatchEvent(new CustomEvent('proxmox-view-mode-changed', {
detail: { isAdvancedView: this.isAdvancedView }
}));
}
}
}" x-init="
if (!localStorage.getItem('proxmox-view-mode')) {
localStorage.setItem('proxmox-view-mode', 'basic');
isAdvancedView = false;
}
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
$watch('open', () => {
if (open) {
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
}
});
"
@proxmox-view-mode-changed.window="isAdvancedView = $event.detail.isAdvancedView"
>
<style>
.mobile-nav-button {
background: color-mix(in srgb, var(--color-background-secondary) 60%, transparent);
backdrop-filter: blur(12px);
border: 1px solid color-mix(in srgb, var(--color-neutral) 30%, transparent);
border-radius: 12px;
padding: 12px 16px;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.05),
0 0 0 1px rgba(0, 0, 0, 0.02);
transition: all 0.2s ease;
}
.dark .mobile-nav-button {
background: color-mix(in srgb, var(--color-background-secondary) 80%, transparent);
border-color: color-mix(in srgb, var(--color-neutral) 40%, transparent);
}
.mobile-nav-button:active {
transform: scale(0.98);
}
.mobile-dropdown {
background: rgba(var(--background-secondary-rgb, 255, 255, 255), 0.95);
backdrop-filter: blur(16px);
border: 1px solid rgba(var(--neutral-rgb, 229, 231, 235), 0.3);
border-radius: 14px;
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.03);
}
.dark .mobile-dropdown {
background: rgba(var(--background-secondary-rgb, 31, 41, 55), 0.95);
border-color: rgba(var(--neutral-rgb, 75, 85, 99), 0.4);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.mobile-tab-item {
padding: 12px 16px;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.mobile-tab-item:hover {
background: rgba(37, 99, 235, 0.08);
border-left-color: rgba(37, 99, 235, 0.3);
}
.mobile-tab-item.active {
background: rgba(37, 99, 235, 0.12);
border-left-color: rgb(37, 99, 235);
font-weight: 600;
}
.dark .mobile-tab-item.active {
background: rgba(59, 130, 246, 0.15);
border-left-color: rgb(96, 165, 250);
}
.mobile-toggle-compact {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(var(--background-rgb, 255, 255, 255), 0.5);
border: 1px solid rgba(var(--neutral-rgb, 229, 231, 235), 0.4);
border-radius: 10px;
}
.dark .mobile-toggle-compact {
background: rgba(var(--background-rgb, 17, 24, 39), 0.6);
border-color: rgba(var(--neutral-rgb, 75, 85, 99), 0.5);
}
.mobile-toggle-label-compact {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.mobile-toggle-switch-compact {
width: 40px;
height: 22px;
background: linear-gradient(135deg,
rgba(156, 163, 175, 0.4),
rgba(209, 213, 219, 0.4));
border-radius: 11px;
position: relative;
transition: all 0.3s ease;
border: 1px solid rgba(var(--neutral-rgb, 156, 163, 175), 0.2);
}
.mobile-toggle-switch-compact.active {
background: linear-gradient(135deg,
rgb(37, 99, 235),
rgb(59, 130, 246));
}
.dark .mobile-toggle-switch-compact.active {
background: linear-gradient(135deg,
rgb(59, 130, 246),
rgb(96, 165, 250));
}
.mobile-toggle-handle-compact {
position: absolute;
top: 1px;
left: 1px;
width: 18px;
height: 18px;
background: white;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.mobile-toggle-switch-compact.active .mobile-toggle-handle-compact {
transform: translateX(18px);
}
.mobile-section-header {
background: linear-gradient(135deg,
rgba(37, 99, 235, 0.05),
rgba(59, 130, 246, 0.03));
border-bottom: 1px solid color-mix(in srgb, var(--color-neutral) 20%, transparent);
}
.dark .mobile-section-header {
background: linear-gradient(135deg,
rgba(59, 130, 246, 0.08),
rgba(37, 99, 235, 0.05));
}
</style>
<div class="flex gap-3">
@php
// Find the current tab label, checking both regular tabs and dropdown items
$currentLabel = 'Menu';
foreach ($tabs as $tab) {
if (isset($tab['view']) && $tab['view'] === $view) {
$currentLabel = $tab['label'];
break;
}
if (isset($tab['dropdown']) && $tab['dropdown'] && isset($tab['items'])) {
foreach ($tab['items'] as $item) {
if ($item['view'] === $view) {
$currentLabel = $item['label'];
break 2;
}
}
}
}
@endphp
<button @click="open = !open"
class="mobile-nav-button flex items-center gap-3 flex-1 justify-between font-semibold">
<span class="text-sm">{{ $currentLabel }}</span>
<x-ri-menu-line class="size-5" />
</button>
<div class="mobile-toggle-compact flex-shrink-0"
:class="isAdvancedOnlyView ? 'opacity-50 pointer-events-none' : ''">
<span class="mobile-toggle-label-compact"
:class="!isAdvancedView ? 'text-base' : 'text-base/40'">
Std
</span>
<div class="mobile-toggle-switch-compact"
:class="{ 'active': isAdvancedView }"
@click="!isAdvancedOnlyView && toggleView()">
<div class="mobile-toggle-handle-compact"></div>
</div>
<span class="mobile-toggle-label-compact"
:class="isAdvancedView ? 'text-base' : 'text-base/40'">
Adv
</span>
</div>
</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"
@click.outside="open = false"
class="mobile-dropdown mt-3 overflow-hidden">
<div class="flex flex-col">
@foreach($tabs as $tab)
@php
$isAdvancedTab = $tab['advanced'] ?? false;
$isDropdown = $tab['dropdown'] ?? false;
@endphp
@if($isDropdown)
{{-- Render dropdown items as individual menu items for mobile --}}
<div class="mobile-section-header px-3 py-2 text-xs font-bold uppercase tracking-wider text-base/50">
{{ $tab['label'] }}
</div>
@foreach($tab['items'] as $item)
@php
$itemAdvanced = $item['advanced'] ?? false;
@endphp
<div @if($itemAdvanced)
x-show="isAdvancedView"
x-transition
@endif>
<a href="{{ $item['route'] }}" wire:navigate
class="mobile-tab-item flex items-center justify-between pl-8 {{ $view === $item['view'] ? 'active text-base' : 'text-base/70' }}"
@click="open = false">
<span>{{ $item['label'] }}</span>
@if($view === $item['view'])
<x-ri-check-line class="size-4 text-primary" />
@endif
</a>
</div>
@endforeach
@else
<div @if($isAdvancedTab)
x-show="isAdvancedView"
x-transition
@endif>
<a href="{{ $tab['route'] }}" wire:navigate
class="mobile-tab-item flex items-center justify-between {{ $view === $tab['view'] ? 'active text-base' : 'text-base/70' }}"
@click="open = false">
<span>{{ $tab['label'] }}</span>
@if($view === $tab['view'])
<x-ri-check-line class="size-4 text-primary" />
@endif
</a>
</div>
@endif
@endforeach
</div>
</div>
</div>
</div>
{{-- Fallback x-data provides default values for all page-scoped Alpine variables.
This prevents "not defined" errors during Livewire wire:navigate morphdom
transitions between tabs. Inner x-data scopes on each page override these.
Must be ABOVE the wire:key element so scope persists during key-change replacements. --}}
<div class="flex-1 flex flex-col"
x-data="{
showHostnameEditModal: false, showPasswordViewModal: false, showPasswordEditModal: false,
showExportErrorModal: false, showExportInfoModal: false, showClearStatisticsModal: false,
showAddDnsModal: false, showAddKeyModal: false, showCiUserModal: false,
showDnsDomainModal: false, showKeyModal: false, showNetworkModal: false,
copied: false, copyText: '', isCollapsed: false, loading: false, expanded: false,
chartsLoaded: false, tab: '', ip: '', ipData: null,
directionDescription: '', portDescription: '', protocolDescription: '', stateDescription: '',
statCpu: null, statDate: null, statDisk: null, statId: null,
statMemory: null, statStatus: null, taskDetails: null, taskUpid: null,
activeTab: null, connectionData: null, currentMacro: null, macroDefinitions: null,
protocol: '', error: null, sourceIP: null, sourcePort: null,
destIP: null, destPort: null, timestamp: null,
day: null, month: null, year: null, hour: null, hour12: null, minute: null, second: null,
keyValue: null,
maxQueues: null, queues: null,
activeSection: null, expandedCategories: null, searchQuery: '',
pendingSection: null, transitionDelay: null
}">
<div class="flex-1 flex flex-col proxmox-page-wrapper" wire:key="proxmox-page-{{ $view }}">
{{ $slot }}
</div>
</div>
</div>
@else
@php
$statuses = [
'suspended' => [
'color' => 'text-yellow-500',
'message' => 'This server is suspended. Please contact support for more information.'
],
'installing' => [
'color' => 'text-blue-500',
'message' => ''
],
];
@endphp
<div class="flex flex-col" wire:key='{{ $server->id }}-{{ $server->status }}' style="min-height: calc(100vh - 30rem);">
@if($server->status === 'installing')
<style>
@import url('https://fonts.googleapis.com/css?family=Fira+Mono:400');
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.installing-container {
min-height: calc(100vh - 30rem);
height: calc(100vh - 30rem);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.installing-body {
display: flex;
align-items: center;
justify-content: center;
font-family: "Inter", system-ui, sans-serif;
color: #1d2c3f;
width: 100%;
}
.loading-card {
width: min(460px, 92vw);
background: #ffffff;
border-radius: 20px;
padding: 34px 30px 38px;
box-shadow:
0 25px 50px rgba(86, 121, 168, 0.22),
0 0 0 1px rgba(109, 178, 255, 0.15);
position: relative;
overflow: hidden;
}
.dark .loading-card {
background: var(--background-secondary);
color: var(--base);
}
.loading-card::after {
content: "";
position: absolute;
inset: -2px;
border-radius: 22px;
background: linear-gradient(
90deg,
rgba(44, 185, 149, 0.15),
rgba(0, 163, 255, 0.15),
rgba(0, 84, 255, 0.15),
rgba(85, 0, 255, 0.15)
);
background-size: 200% 200%;
animation: border-glow 8s ease infinite;
z-index: -1;
pointer-events: none;
opacity: 0.3;
}
@keyframes border-glow {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 28px;
}
.logo-shell {
width: 58px;
height: 58px;
display: grid;
place-items: center;
}
.logo-shell img {
width: 42px;
height: auto;
display: block;
animation: pulse 2s infinite;
}
.header span {
text-transform: uppercase;
letter-spacing: 0.26em;
font-size: 0.75rem;
color: rgba(32, 56, 84, 0.6);
}
.dark .header span {
color: rgba(255, 255, 255, 0.6);
}
.header h1 {
margin: 4px 0 0;
font-size: clamp(1.4rem, 1vw, 1.65rem);
}
.line {
position: relative;
height: 6px;
border-radius: 999px;
background: rgba(37, 74, 179, 0.596);
overflow: hidden;
margin-bottom: 32px;
box-shadow:
inset 0 0 12px rgba(0, 0, 0, 0.05),
0 16px 32px rgba(109, 178, 255, 0.2);
}
.line::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
89deg,
#2cb995,
#00a3ff,
#0054ff,
#001cff,
#5500ff
);
background-size: 800% 800%;
animation: line-gradient 13s ease infinite;
}
.line::after {
content: "";
position: absolute;
top: 50%;
left: 0%;
width: 36px;
height: 12px;
border-radius: 999px;
border: 1px solid rgba(0, 20, 60, 0.25);
background: linear-gradient(90deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0));
box-shadow:
0 4px 20px rgba(0, 90, 255, 0.25),
inset 0 0 8px rgba(255, 255, 255, 0.4);
transform: translate(-50%, -50%) skewX(-8deg);
animation: line-marker 2.8s cubic-bezier(0.55, 0.03, 0.35, 0.95) infinite;
}
.status-cycle {
margin-top: 24px;
height: 20px;
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(32, 56, 84, 0.65);
position: relative;
}
.dark .status-cycle {
color: rgba(255, 255, 255, 0.65);
}
.status-cycle ul {
list-style: none;
margin: 0;
padding: 0;
position: relative;
height: 20px;
}
.status-cycle li {
position: absolute;
inset: 0;
display: flex;
align-items: center;
gap: 10px;
opacity: 0;
animation: status-step 30s linear infinite;
}
.status-cycle li::before {
content: "";
width: 12px;
height: 2px;
background: linear-gradient(90deg, #18d4ff, transparent);
}
.status-cycle li:nth-child(2) {
animation-delay: 3s;
}
.status-cycle li:nth-child(3) {
animation-delay: 6s;
}
.status-cycle li:nth-child(4) {
animation-delay: 9s;
}
.status-cycle li:nth-child(5) {
animation-delay: 12s;
}
.status-cycle li:nth-child(6) {
animation-delay: 15s;
}
.status-cycle li:nth-child(7) {
animation-delay: 18s;
}
.status-cycle li:nth-child(8) {
animation-delay: 21s;
}
.status-cycle li:nth-child(9) {
animation-delay: 24s;
}
.status-cycle li:nth-child(10) {
animation-delay: 27s;
}
@keyframes line-gradient {
0% {
background-position: 0% 79%;
}
50% {
background-position: 100% 22%;
}
100% {
background-position: 0% 79%;
}
}
@keyframes line-marker {
0% {
left: 0%;
opacity: 0.15;
transform: translate(-50%, -50%) skewX(-8deg) scaleX(0.85);
}
10% {
opacity: 0.85;
}
50% {
opacity: 1;
transform: translate(-50%, -50%) skewX(-6deg) scaleX(1);
}
85% {
opacity: 0.4;
}
100% {
left: 100%;
opacity: 0.15;
transform: translate(-50%, -50%) skewX(-10deg) scaleX(0.75);
}
}
@keyframes status-step {
0%,
8% {
opacity: 1;
}
10%,
100% {
opacity: 0;
}
}
.watermark {
position: absolute;
bottom: 14px;
right: 24px;
font-size: 0.55rem;
letter-spacing: 0.22em;
color: rgba(32, 56, 84, 0.35);
cursor: default;
text-decoration: none;
}
.dark .watermark {
color: rgba(255, 255, 255, 0.35);
}
.audio-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
animation: fadeIn 0.3s ease;
}
.audio-modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.audio-modal-content {
background: transparent;
border-radius: 20px;
padding: 0;
width: min(400px, 90vw);
max-width: 90vw;
box-shadow:
0 25px 50px rgba(86, 121, 168, 0.3),
0 0 0 1px rgba(109, 178, 255, 0.2);
animation: slideUp 0.3s ease;
overflow: hidden;
position: relative;
}
.audio-modal-content::before {
content: "";
position: absolute;
inset: 0;
background-image: url('https://billing.virtnet.bond/assets/images/glados.jpg');
background-size: cover;
background-position: center;
opacity: .95;
border-radius: 20px;
z-index: -1;
}
.audio-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 28px 10px;
position: relative;
background: rgba(255, 255, 255, 0.9);
}
.dark .audio-modal-header {
background: rgba(0, 0, 0, 0.9);
}
.audio-modal-header h2 {
margin: 0;
position: relative;
}
.audio-modal-header h2 .data-corruption {
display: inline-block;
font-family: 'Fira Mono', monospace;
font-size: 1.1rem;
font-weight: 400;
color: #1d2c3f;
letter-spacing: 0.1em;
position: relative;
}
.dark .audio-modal-header h2 .data-corruption {
color: var(--base);
}
.audio-modal-header h2 .data-corruption::before,
.audio-modal-header h2 .data-corruption::after {
content: attr(title);
position: absolute;
left: 0;
color: #1d2c3f;
}
.dark .audio-modal-header h2 .data-corruption::before,
.dark .audio-modal-header h2 .data-corruption::after {
color: var(--base);
}
.audio-modal-close {
background: none;
border: none;
font-size: 2rem;
color: rgba(32, 56, 84, 0.4);
cursor: pointer;
line-height: 1;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.audio-modal-close:hover {
background: rgba(32, 56, 84, 0.1);
color: rgba(32, 56, 84, 0.7);
}
.dark .audio-modal-close {
color: rgba(255, 255, 255, 0.4);
}
.dark .audio-modal-close:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
}
.audio-modal-body {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 28px;
display: flex;
font-size: small;
flex-direction: column;
justify-content: center;
align-items: left;
gap: 16px;
position: relative;
background: rgba(255, 255, 255, 0.9);
}
.dark .audio-modal-body {
background: rgba(0, 0, 0, 0.9);
}
.audio-modal-body audio {
width: 100%;
outline: none;
}
.audio-modal-body a {
color: inherit;
text-decoration: none;
}
.audio-modal-body a:hover,
.audio-modal-body a:visited,
.audio-modal-body a:active {
color: inherit;
text-decoration: none;
}
.easter-egg-message {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px;
background: linear-gradient(135deg, rgba(118, 124, 128, 0.08), rgba(136, 136, 136, 0.445));
border-radius: 12px;
border-left: 3px solid rgba(54, 62, 66, 0.4);
margin-bottom: 8px;
}
.easter-egg-text {
margin: 0;
font-size: 0.75rem;
line-height: 1.6;
color: rgba(32, 56, 84, 0.85);
letter-spacing: 0.02em;
}
.dark .easter-egg-text {
color: rgba(255, 255, 255, 0.85);
}
.easter-egg-highlight {
font-weight: 700;
font-size: 1.1em;
background: linear-gradient(135deg, #00000070, #4e4d69);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.05em;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
<!-- Auto-refresh every 10s only in installing state -->
<script data-navigate-track>
// Clear any existing timeouts to prevent duplicates on Livewire navigation
if (window.installingRefreshTimeout) {
clearTimeout(window.installingRefreshTimeout);
window.installingRefreshTimeout = null;
}
function scheduleInstallingRefresh() {
if (window.installingRefreshTimeout) {
clearTimeout(window.installingRefreshTimeout);
}
window.installingRefreshTimeout = setTimeout(function() {
window.location.reload();
}, 10000);
}
scheduleInstallingRefresh();
// Audio Modal Functions
function openAudioModal(e) {
e.preventDefault();
const modal = document.getElementById('audioModal');
const audio = document.getElementById('gladosAudio');
const watermark = document.querySelector('.watermark');
if (modal && audio) {
modal.classList.add('active');
audio.currentTime = 5;
audio.play().catch(function() {
// Audio autoplay blocked by browser - expected
});
if (window.installingRefreshTimeout) {
clearTimeout(window.installingRefreshTimeout);
window.installingRefreshTimeout = null;
}
audio.addEventListener('loadedmetadata', function() {
if (audio.currentTime < 5) {
audio.currentTime = 5;
}
}, { once: true });
if (watermark) {
watermark.textContent = 'Nice job breaking it, hero.';
}
}
return false;
}
function closeAudioModal() {
const modal = document.getElementById('audioModal');
const audio = document.getElementById('gladosAudio');
if (modal && audio) {
audio.pause();
audio.currentTime = 0;
modal.classList.remove('active');
scheduleInstallingRefresh();
}
}
window.addEventListener('click', function(event) {
const modal = document.getElementById('audioModal');
if (event.target === modal) {
closeAudioModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeAudioModal();
}
});
</script>
<div class="installing-container">
<div class="installing-body">
<div class="loading-card">
<div class="header">
<div class="logo-shell" aria-hidden="true">
<img src="https://billing.virtnet.bond/assets/images/logofresh.png" alt="Virtnet.bond logo" width="46" height="34" />
</div>
<div>
<span>Virtnet.bond</span>
<h1>Creating Server</h1>
</div>
</div>
<div class="line" aria-hidden="true"></div>
<div class="status-cycle" aria-live="polite">
<ul>
<li>Telling the memory to try harder</li>
<li>Begging the network to behave (again)</li>
<li>Pretending the storage is organized</li>
<li>Asking the CPU to stop being dramatic</li>
<li>Informing telemetry it missed everything</li>
<li>Encouraging the firewall to chill out</li>
<li>Reminding the logs nobody reads them</li>
<li>Lecturing the cluster to stop gossiping</li>
<li>Asking the API politely to behave today</li>
<li>Calming down the swap space (it is stressed)</li>
</ul>
</div>
<a class="watermark" href="#" onclick="openAudioModal(event); return false;">
GLaDOS r4.32-03
</a>
</div>
</div>
</div>
<!-- Audio Modal -->
<div id="audioModal" class="audio-modal">
<div class="audio-modal-content">
<div class="audio-modal-header">
<h2>
<div class="data-corruption">
<b>GLaDOS</b> - <small><em>"Still Alive"</em></small>
</div>
</h2>
<button class="audio-modal-close" onclick="closeAudioModal()">&times;</button>
</div>
<div class="audio-modal-body">
<div class="easter-egg-message">
<p class="easter-egg-text">
<span class="easter-egg-highlight">Congrats!</span> You found a hidden Easter egg! Enjoy the tune and the same love Virtnet.bond has for the Portal series and all video games! We might be serious about work, but we're still gamers at heart.
</p>
</div>
<div>
<strong><a href="https://store.steampowered.com/app/400/Portal/">Portal</a></strong> -
<em>"Still Alive"</em> by Ellen McLain (voice of GLaDOS)
</div>
<audio id="gladosAudio" controls>
<source src="https://billing.virtnet.bond/assets/mp3/glados.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<div>
<small>
Virtnet.bond claims no rights to the "GLaDOS" name or the "Still Alive" audio content. This content is used under the doctrine of fair use (<b>17 U.S. Code § 107</b> – Limitations on exclusive rights: Fair use). It is provided for entertainment and cultural reference purposes only. All rights to the aforementioned content are retained by their respective copyright holders.
</small>
</div>
</div>
</div>
</div>
@else
<div class="text-center">
<h2 class="text-2xl font-bold mb-4">Oops!</h2>
<p class="{{ $statuses[$server->status]['color'] ?? 'text-base/80' }} font-semibold mb-4">
@php
$message = $statuses[$server->status]['message'] ?? "This server is currently inactive. Please view the Services tab in the Dashboard to see more information about the current state of this server. If more information is required, please contact support via the support portal under Tickets. This message can be displayed after a service is suspended, cancelled, revoked, or potentially in error. If you believe this message has been displayed in error, please contact us via a ticket and be sure to include the following information: VM{$server->vm_id}-ID{$server->id}-{$server->status}.";
@endphp
{{ $message }}
</p>
</div>
@endif
</div>
@endif
</div>
<!-- Server Icon Customization Modal -->
<div id="serverIconModal" style="display: none;" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div class="bg-background-secondary rounded-xl shadow-2xl p-6 w-full max-w-md relative">
<button onclick="closeServerIconModal()" class="absolute top-3 right-3 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<h3 class="text-lg font-semibold mb-4">Customize Server Icon</h3>
<div class="flex items-center gap-4 mb-4">
<div id="iconPreview" class="size-12 rounded-xl border flex items-center justify-center text-3xl">
<i id="iconPreviewIcon" class="ri-server-line text-base/40"></i>
</div>
<input id="iconSearch" type="text" placeholder="Search icons..." class="border rounded px-2 py-1 text-sm w-40 dark:bg-background dark:border-base/20" oninput="filterIcons()" />
</div>
<div id="iconGrid" class="grid grid-cols-6 gap-2 mb-4 max-h-32 overflow-y-auto">
@php
$availableIcons = [
'server-line', 'database-2-line', 'computer-line', 'cpu-line', 'hard-drive-2-line',
'cloud-line', 'terminal-box-line', 'code-s-slash-line', 'braces-line',
'global-line', 'earth-line', 'wifi-line', 'signal-tower-line',
'shield-check-line', 'shield-keyhole-line', 'lock-line', 'lock-unlock-line', 'key-2-line',
'rocket-line', 'rocket-2-line', 'send-plane-line', 'flashlight-line', 'fire-line',
'sparkling-line', 'star-line', 'heart-line', 'home-line', 'home-2-line',
'building-line', 'building-2-line', 'store-line', 'box-3-line', 'archive-line',
'inbox-line', 'inbox-2-line', 'folder-line', 'folder-open-line',
'settings-3-line', 'settings-4-line', 'tools-line', 'hammer-line',
'flask-line', 'bug-line', 'puzzle-line', 'bar-chart-box-line', 'pie-chart-line',
'line-chart-line', 'money-dollar-circle-line', 'money-euro-circle-line',
'bank-card-line', 'wallet-line', 'user-line', 'user-3-line', 'group-line', 'team-line',
'mail-line', 'mail-open-line', 'chat-3-line', 'discuss-line',
'camera-line', 'vidicon-line', 'film-line', 'music-line',
'book-open-line', 'file-text-line', 'sun-line', 'moon-line',
'palette-line', 'brush-line', 'trophy-line', 'graduation-cap-line',
'lightbulb-line', 'fingerprint-line', 'eye-line', 'map-2-line', 'map-pin-line',
'time-line', 'calendar-line', 'gift-line', 'cake-2-line',
'gamepad-line', 'ghost-line', 'aliens-line', 'space-ship-line',
'android-line', 'apple-line', 'windows-line', 'ubuntu-line',
'github-line', 'gitlab-line', 'discord-line', 'slack-line'
];
@endphp
@foreach($availableIcons as $iconName)
<button onclick="selectIcon('{{ $iconName }}')" data-icon="{{ $iconName }}" class="icon-button rounded p-1 border flex items-center justify-center hover:bg-secondary/10 text-xl transition-all dark:border-base/20">
<i class="ri-{{ $iconName }}"></i>
</button>
@endforeach
</div>
<div class="flex flex-wrap gap-2 mb-4">
@php
$availableColors = [
'' => 'Default',
'#ef4444' => 'Red',
'#f97316' => 'Orange',
'#f59e0b' => 'Amber',
'#eab308' => 'Yellow',
'#84cc16' => 'Lime',
'#22c55e' => 'Green',
'#10b981' => 'Emerald',
'#14b8a6' => 'Teal',
'#06b6d4' => 'Cyan',
'#0ea5e9' => 'Sky',
'#3b82f6' => 'Blue',
'#6366f1' => 'Indigo',
'#8b5cf6' => 'Violet',
'#a855f7' => 'Purple',
'#d946ef' => 'Fuchsia',
'#ec4899' => 'Pink',
'#f43f5e' => 'Rose',
'#78716c' => 'Stone',
'#64748b' => 'Slate'
];
@endphp
@foreach($availableColors as $colorValue => $colorName)
<button onclick="selectColor('{{ $colorValue }}')" data-color="{{ $colorValue }}" title="{{ $colorName }}" class="color-button size-6 rounded-full border transition-all" style="background: {{ $colorValue ?: '#e5e7eb' }}"></button>
@endforeach
</div>
<div class="flex gap-2 justify-end">
<button onclick="resetServerIcon()" class="text-xs text-base/60 hover:text-danger transition-colors">Reset</button>
<button onclick="saveServerIcon()" class="px-3 py-1 rounded bg-primary text-white text-xs hover:bg-primary/80 transition-colors">Save</button>
</div>
<div id="iconMessage" class="mt-2 text-xs text-success hidden"></div>
</div>
</div>
<script data-navigate-track>
// Define functions only if not already defined (prevent duplicates on navigation)
if (typeof window.serverIconModalData === 'undefined') {
window.serverIconModalData = {
serverId: null,
selectedIcon: '',
selectedColor: ''
};
}
window.openServerIconModal = function(serverId, currentIcon, currentColor) {
serverIconModalData.serverId = serverId;
serverIconModalData.selectedIcon = currentIcon || '';
serverIconModalData.selectedColor = currentColor || '';
updateIconPreview();
updateIconSelection();
updateColorSelection();
document.getElementById('serverIconModal').style.display = 'flex';
document.getElementById('iconSearch').value = '';
filterIcons();
};
window.closeServerIconModal = function() {
document.getElementById('serverIconModal').style.display = 'none';
document.getElementById('iconMessage').classList.add('hidden');
};
window.selectIcon = function(iconName) {
serverIconModalData.selectedIcon = iconName;
updateIconPreview();
updateIconSelection();
};
window.selectColor = function(color) {
serverIconModalData.selectedColor = color;
updateIconPreview();
updateColorSelection();
};
window.resetServerIcon = function() {
serverIconModalData.selectedIcon = '';
serverIconModalData.selectedColor = '';
updateIconPreview();
updateIconSelection();
updateColorSelection();
};
function updateIconPreview() {
const preview = document.getElementById('iconPreview');
const previewIcon = document.getElementById('iconPreviewIcon');
if (!preview || !previewIcon) return;
if (serverIconModalData.selectedIcon) {
previewIcon.className = 'ri-' + serverIconModalData.selectedIcon;
previewIcon.style.color = serverIconModalData.selectedColor || '';
preview.style.borderColor = serverIconModalData.selectedColor || '';
} else {
previewIcon.className = 'ri-server-line text-base/40';
previewIcon.style.color = '';
preview.style.borderColor = '';
}
}
function updateIconSelection() {
document.querySelectorAll('.icon-button').forEach(btn => {
if (btn.dataset.icon === serverIconModalData.selectedIcon) {
btn.classList.add('ring-2', 'ring-primary');
} else {
btn.classList.remove('ring-2', 'ring-primary');
}
});
}
function updateColorSelection() {
document.querySelectorAll('.color-button').forEach(btn => {
if (btn.dataset.color === serverIconModalData.selectedColor) {
btn.classList.add('ring-2', 'ring-primary');
} else {
btn.classList.remove('ring-2', 'ring-primary');
}
});
}
window.filterIcons = function() {
const searchInput = document.getElementById('iconSearch');
if (!searchInput) return;
const search = searchInput.value.toLowerCase();
document.querySelectorAll('.icon-button').forEach(btn => {
const iconName = btn.dataset.icon;
if (iconName && iconName.includes(search)) {
btn.style.display = 'flex';
} else {
btn.style.display = 'none';
}
});
};
window.saveServerIcon = function() {
fetch(`/proxmox/server/${serverIconModalData.serverId}/icon`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
},
body: JSON.stringify({
icon: serverIconModalData.selectedIcon,
color: serverIconModalData.selectedColor
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const messageEl = document.getElementById('iconMessage');
messageEl.textContent = 'Icon saved!';
messageEl.classList.remove('hidden');
setTimeout(() => {
closeServerIconModal();
location.reload();
}, 800);
}
})
.catch(error => {
console.error('Error saving icon:', error);
const messageEl = document.getElementById('iconMessage');
if (messageEl) {
messageEl.textContent = 'Failed to save icon. Please try again.';
messageEl.classList.remove('hidden');
messageEl.classList.add('text-red-400');
}
});
};
// Page transition handling for Livewire navigation
(function() {
let hasNavigated = false;
// Mark that navigation has started (but don't animate yet)
document.addEventListener('livewire:navigate', () => {
hasNavigated = true;
});
// Only animate AFTER the new page content is fully loaded and swapped in
document.addEventListener('livewire:navigated', () => {
if (hasNavigated) {
// Use double requestAnimationFrame to ensure DOM is fully updated
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const pageWrapper = document.querySelector('.proxmox-page-wrapper');
if (pageWrapper) {
// Apply slide-in to reveal the NEW content
pageWrapper.classList.add('slide-in');
pageWrapper.addEventListener('animationend', () => {
pageWrapper.classList.remove('slide-in');
}, { once: true });
}
});
});
hasNavigated = false;
}
});
})();
// Smooth scroll to top on Livewire navigation (only for Proxmox pages)
if (!window._proxmoxScrollListenerAdded) {
window._proxmoxScrollListenerAdded = true;
document.addEventListener('livewire:navigated', () => {
// Only scroll to top if still on a Proxmox page
if (document.querySelector('.proxmox-page-wrapper')) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
}
// Keyboard shortcuts for power actions (deduplicated)
if (!window._proxmoxKeyboardListenerAdded) {
window._proxmoxKeyboardListenerAdded = true;
document.addEventListener('keydown', (e) => {
// Only if on a Proxmox page and no input is focused
if (!document.querySelector('.proxmox-page-wrapper')) return;
if (document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA' ||
document.activeElement.tagName === 'SELECT' ||
document.activeElement.isContentEditable) return;
// Ctrl+Shift+S → Start server
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
const startBtn = document.querySelector('[wire\\:click="do(\'start\')"]');
if (startBtn && !startBtn.disabled) startBtn.click();
}
// Ctrl+Shift+K → Power off (force stop) — avoids Ctrl+Shift+P browser conflict
if (e.ctrlKey && e.shiftKey && e.key === 'K') {
e.preventDefault();
const stopBtn = document.querySelector('[wire\\:click="do(\'stop\')"]');
if (stopBtn && !stopBtn.disabled) stopBtn.click();
}
});
}
</script>
<!-- Back to Top Button -->
<div x-data="{ showBackToTop: false, _scrollHandler: null }"
x-init="_scrollHandler = () => { showBackToTop = window.scrollY > 400 }; window.addEventListener('scroll', _scrollHandler, { passive: true })"
x-destroy="window.removeEventListener('scroll', _scrollHandler)"
class="fixed bottom-20 right-4 z-50">
<button x-show="showBackToTop"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
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-4"
@click="window.scrollTo({ top: 0, behavior: 'smooth' })"
class="flex items-center justify-center w-10 h-10 rounded-full bg-primary/90 text-white shadow-lg hover:bg-primary hover:shadow-xl transition-all duration-200 backdrop-blur-sm"
aria-label="Back to top">
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
</svg>
</button>
</div>
<!-- Toast Notification System -->
<div x-data="{
toasts: [],
addToast(message, type = 'success') {
const id = Date.now();
this.toasts.push({ id, message, type });
setTimeout(() => this.removeToast(id), 3000);
},
removeToast(id) {
this.toasts = this.toasts.filter(t => t.id !== id);
}
}"
@toast.window="addToast($event.detail.message, $event.detail.type || 'success')"
class="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none">
<template x-for="toast in toasts" :key="toast.id">
<div 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"
class="pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-xl shadow-lg border backdrop-blur-sm text-sm font-medium"
:class="{
'bg-success/10 border-success/30 text-success': toast.type === 'success',
'bg-error/10 border-error/30 text-error': toast.type === 'error',
'bg-warning/10 border-warning/30 text-warning': toast.type === 'warning',
'bg-info/10 border-info/30 text-info': toast.type === 'info'
}"
@click="removeToast(toast.id)">
<template x-if="toast.type === 'success'">
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
</template>
<template x-if="toast.type === 'error'">
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</template>
<template x-if="toast.type === 'warning'">
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
</template>
<template x-if="toast.type === 'info'">
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</template>
<span x-text="toast.message"></span>
</div>
</template>
</div>