Files
Joseph.Rawlings 8baf7d6c7d feat: Enhance UI components and add command palette
- Updated dropdown component styles and transitions for improved UX.
- Refined checkbox, input, radio, select, and textarea components with consistent error messaging and styling.
- Introduced a new card component for better layout options.
- Added a stat card component for displaying metrics with trends.
- Implemented a command palette for global search and navigation, accessible via Cmd/Ctrl+K.
- Enhanced modal and navigation components with new styles and transitions.
- Improved layout styles for better responsiveness and visual appeal.
2026-04-22 10:40:33 -04:00

416 lines
15 KiB
PHP

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @if(in_array(app()->getLocale(), config('app.rtl_locales'))) dir="rtl" @endif>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<title>
@if(theme('seo_title'))
{{ theme('seo_title') }}
@isset($title)
- {{ $title }}
@endisset
@else
{{ config('app.name', 'Paymenter') }}
@isset($title)
- {{ $title }}
@endisset
@endif
</title>
@if(theme('seo_description'))
<meta name="description" content="{{ theme('seo_description') }}">
<meta property="og:description" content="{{ theme('seo_description') }}">
@endif
@if(theme('seo_keywords'))
<meta name="keywords" content="{{ theme('seo_keywords') }}">
@endif
@if(theme('seo_author'))
<meta name="author" content="{{ theme('seo_author') }}">
@endif
@vite(['themes/' . config('settings.theme') . '/js/app.js', 'themes/' . config('settings.theme') . '/css/app.css'], config('settings.theme'))
@include('layouts.colors')
@if (config('settings.logo'))
<link rel="icon" href="{{ Storage::url(config('settings.logo')) }}" type="image/png">
@endif
<!-- Open Graph Meta Tags -->
@isset($title)
<meta content="{{ theme('seo_title') ? theme('seo_title') . ' - ' . $title : config('app.name', 'Paymenter') . ' - ' . $title }}" property="og:title">
<meta content="{{ theme('seo_title') ? theme('seo_title') . ' - ' . $title : config('app.name', 'Paymenter') . ' - ' . $title }}" name="title">
@else
<meta content="{{ theme('seo_title') ?: config('app.name', 'Paymenter') }}" property="og:title">
<meta content="{{ theme('seo_title') ?: config('app.name', 'Paymenter') }}" name="title">
@endisset
@isset($description)
<meta content="{{ $description }}" property="og:description">
<meta content="{{ $description }}" name="description">
@endisset
<!-- Open Graph Image -->
@if(theme('og_image'))
<meta content="{{ theme('og_image') }}" property="og:image">
<meta content="{{ theme('og_image') }}" name="image">
<meta content="{{ theme('og_image') }}" property="twitter:image">
@elseif(isset($image))
<meta content="{{ $image }}" property="og:image">
<meta content="{{ $image }}" name="image">
<meta content="{{ $image }}" property="twitter:image">
@endif
<!-- Additional Open Graph Tags -->
<meta property="og:type" content="website">
<meta property="og:url" content="{{ url()->current() }}">
<meta property="og:site_name" content="{{ config('app.name', 'Paymenter') }}">
<!-- Twitter Card Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ theme('seo_title') ?: config('app.name', 'Paymenter') }}{{ isset($title) ? ' - ' . $title : '' }}">
@if(theme('seo_description'))
<meta name="twitter:description" content="{{ theme('seo_description') }}">
@endif
<meta name="theme-color" content="{{ theme('primary') }}">
<meta name="locale" content="{{ app()->getLocale() }}">
<meta name="format-detection" content="telephone=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
{!! hook('head') !!}
<!-- Custom CSS from theme settings -->
@if(theme('custom_css'))
<style>
{!! theme('custom_css') !!}
</style>
@endif
<!-- Custom HTML Head Section -->
@if(theme('custom_head_html'))
{!! theme('custom_head_html') !!}
@endif
<style>
/* Critical animations that should load immediately */
.animate-pulse-optimized {
animation: pulse-optimized 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse-optimized {
0%, 100% { opacity: 0.1; transform: scale(1); }
50% { opacity: 0.25; transform: scale(1.05); }
}
.low-performance * {
animation-duration: 0.1s !important;
transition-duration: 0.1s !important;
}
.low-performance .animate-pulse-optimized {
animation: none !important;
opacity: 0.1 !important;
}
.reveal-on-scroll {
transform: translateY(20px);
opacity: 0;
transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.reveal-on-scroll.revealed {
transform: translateY(0);
opacity: 1;
}
.btn-loading {
position: relative;
color: transparent !important;
}
.btn-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@media (max-width: 768px) {
.mobile-optimized * {
-webkit-tap-highlight-color: transparent;
}
.mobile-optimized button,
.mobile-optimized a {
touch-action: manipulation;
}
}
/* Background Image Styles */
.background-image-layer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -2;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
.background-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
@media (max-width: 768px) {
.background-image-layer {
background-attachment: scroll;
}
}
/* Custom Integration Styles */
.custom-chat-widget {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
}
.custom-notification-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9998;
padding: 10px;
background: var(--primary);
color: white;
text-align: center;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.custom-notification-bar.show {
transform: translateY(0);
}
</style>
</head>
<body class="w-full bg-background text-base min-h-screen flex flex-col antialiased {{ theme('background_image_url') ? '' : 'grid-bg noise-overlay' }}"
x-cloak
x-data="{darkMode: $persist(window.matchMedia('(prefers-color-scheme: dark)').matches), scrolled: false}"
x-init="window.addEventListener('scroll', () => scrolled = window.scrollY > 20, { passive: true })"
:class="{'dark': darkMode, 'is-scrolled': scrolled}">
{!! hook('body') !!}
<!-- Custom HTML Body Top -->
@if(theme('custom_body_top_html'))
{!! theme('custom_body_top_html') !!}
@endif
@if(theme('background_image_url'))
<div class="background-image-layer"
style="background-image: url('{{ theme('background_image_url') }}');
opacity: {{ theme('background_image_opacity', 30) / 100 }};
filter: blur({{ theme('background_image_blur', 5) }}px);">
</div>
<div class="background-overlay"
style="opacity: {{ theme('background_overlay_opacity', 70) / 100 }};">
</div>
@endif
<x-navigation />
@if(!theme('background_image_url'))
{{-- Subtle ambient gradient accents (Volt-inspired). Kept minimal so
content readability stays crisp on both desktop and mobile. --}}
<div class="fixed inset-0 overflow-hidden pointer-events-none z-0" aria-hidden="true">
<div class="absolute top-[-10%] left-[-10%] w-[420px] h-[420px] bg-primary rounded-full filter blur-[110px] opacity-[0.10] sm:opacity-[0.14] animate-pulse-optimized"></div>
<div class="hidden md:block absolute bottom-[-15%] right-[-10%] w-[480px] h-[480px] bg-secondary rounded-full filter blur-[120px] opacity-[0.10] animate-pulse-optimized"
style="animation-delay: -2s;"></div>
<div class="hidden lg:block absolute top-[30%] right-[20%] w-[260px] h-[260px] bg-primary rounded-full filter blur-[90px] opacity-[0.06] animate-pulse-optimized"
style="animation-delay: -4s;"></div>
</div>
@endif
<div class="relative z-10 w-full flex flex-grow">
@if (isset($sidebar) && $sidebar)
<x-navigation.sidebar :title="$title ?? ''" />
@endif
<div class="{{ (isset($sidebar) && $sidebar) ? 'md:ml-64 rtl:ml-0 rtl:md:mr-64' : '' }} flex min-w-0 flex-col flex-grow overflow-y-auto overflow-x-hidden">
<main class="w-full mt-24 mx-auto min-w-0 max-w-full px-4 sm:px-6 md:px-16 lg:px-16 relative z-10 transition-opacity duration-150"
x-data="{ navigating: false }"
x-init="
document.addEventListener('livewire:navigate', () => { navigating = true });
document.addEventListener('livewire:navigated', () => { $nextTick(() => { navigating = false }) });
"
:class="{ 'opacity-60': navigating }">
{{ $slot }}
</main>
<x-notification />
{{-- Same width and horizontal padding as <main> so footer aligns with page containers --}}
<div class="w-full relative z-0 mx-auto mb-3 min-w-0 max-w-full px-4 py-4 sm:px-6 md:px-16 lg:px-16">
<x-navigation.footer />
</div>
</div>
<x-impersonating />
</div>
{{-- Global command palette (Cmd/Ctrl+K) --}}
<x-command-palette />
{!! hook('footer') !!}
<!-- Custom JavaScript from theme settings -->
@if(theme('custom_js'))
<script>
{!! theme('custom_js') !!}
</script>
@endif
<!-- Custom HTML Body Bottom -->
@if(theme('custom_body_bottom_html'))
{!! theme('custom_body_bottom_html') !!}
@endif
<script>
(function() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
const isLowEnd = connection && (connection.saveData || connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g');
const isSlowDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 2;
if (isLowEnd || isSlowDevice) {
document.body.classList.add('low-performance');
}
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
document.querySelectorAll('.reveal-on-scroll').forEach(el => {
observer.observe(el);
});
}
document.addEventListener('click', function(e) {
// Skip Livewire/Alpine handled buttons - they manage their own loading states
if (e.target.hasAttribute('wire:click') ||
e.target.hasAttribute('wire:submit') ||
e.target.hasAttribute('x-on:click') ||
e.target.hasAttribute('@click') ||
e.target.closest('[wire\\:submit]')) {
return;
}
if (e.target.matches('button[type="submit"], .btn-submit')) {
e.target.classList.add('btn-loading');
setTimeout(() => {
e.target.classList.remove('btn-loading');
}, 3000);
}
});
if (window.innerWidth <= 768) {
document.body.classList.add('mobile-optimized');
}
})();
</script>
{{-- Tilt / spotlight directive + scroll-reveal stagger indexing --}}
<script>
(function() {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function bindTilt(el) {
if (el.__tiltBound) return;
el.__tiltBound = true;
if (prefersReduced) return;
let raf = 0;
el.addEventListener('pointermove', (e) => {
const r = el.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width;
const y = (e.clientY - r.top) / r.height;
if (raf) cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
el.style.setProperty('--tilt-x', x.toFixed(3));
el.style.setProperty('--tilt-y', y.toFixed(3));
const rx = ((y - 0.5) * -4).toFixed(2);
const ry = ((x - 0.5) * 4).toFixed(2);
el.style.transform = `perspective(800px) rotateX(${rx}deg) rotateY(${ry}deg)`;
});
});
el.addEventListener('pointerleave', () => {
el.style.transform = '';
el.style.setProperty('--tilt-x', '0.5');
el.style.setProperty('--tilt-y', '0.5');
});
}
function indexStaggerGroups() {
document.querySelectorAll('[data-reveal-stagger]').forEach((group) => {
const items = group.querySelectorAll('.reveal-on-scroll');
items.forEach((el, i) => el.style.setProperty('--reveal-idx', i));
});
}
function init() {
document.querySelectorAll('[data-tilt]').forEach(bindTilt);
indexStaggerGroups();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
document.addEventListener('livewire:navigated', init);
})();
</script>
</body>
</html>