nimuli/resources/views/layouts/nimuli-app.blade.php

154 lines
6.1 KiB
PHP

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}" data-theme="{{ request()->cookie('nimuli_theme') ?? auth()->user()?->theme ?? 'dark' }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ ($title ?? '') ? $title . ' — Nimuli' : 'Nimuli' }}</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#0a0a0f" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<!-- Theme-init: runs synchronously before any paint eliminates FOUC -->
<script>
(function () {
try {
var saved = localStorage.getItem('nimuli_theme') || '{{ request()->cookie('nimuli_theme') ?? auth()->user()?->theme ?? 'dark' }}';
var theme = saved === 'system'
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: saved;
document.documentElement.setAttribute('data-theme', theme);
} catch (e) {}
})();
</script>
<!-- Critical CSS: paint baseline before Vite/Tailwind loads -->
<style>
:root {
--c-bg: #0a0a0f; --c-s1: #111117; --c-s2: #16161e;
--c-b1: rgba(255,255,255,0.08); --c-accent: #7c3aed;
--c-t1: #f0f0f5; --c-t2: #8888a0;
}
[data-theme="light"] {
--c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb;
--c-b1: #e5e7eb; --c-accent: #7c3aed;
--c-t1: #111827; --c-t2: #6b7280;
}
html { background: var(--c-bg); }
body {
margin: 0; padding: 0;
background: var(--c-bg); color: var(--c-t1);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
opacity: 0;
transition: opacity 120ms ease-in;
min-height: 100vh;
}
body.app-ready { opacity: 1; }
/* Skeleton placeholders visible during JS load */
.skel-sidebar {
position: fixed; top: 0; left: 0; bottom: 0;
width: 240px; background: var(--c-s1);
border-right: 1px solid var(--c-b1); z-index: 1;
}
.skel-topbar {
position: fixed; top: 0; left: 240px; right: 0;
height: 57px; background: var(--c-s1);
border-bottom: 1px solid var(--c-b1); z-index: 1;
}
@media (max-width: 768px) {
.skel-sidebar { display: none; }
.skel-topbar { left: 0; }
}
/* Spinner: visible before app-ready, hidden after */
.app-spinner {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 28px; height: 28px;
border: 2.5px solid var(--c-b1);
border-top-color: var(--c-accent);
border-radius: 50%;
animation: nim-spin 0.55s linear infinite;
z-index: 10;
}
body.app-ready .app-spinner,
body.app-ready .skel-sidebar,
body.app-ready .skel-topbar { display: none; }
@keyframes nim-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
</style>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
@vite(['resources/js/app.js'])
@livewireStyles
</head>
<body>
<!-- Skeleton: visible during JS load to prevent layout flash -->
<div class="skel-sidebar" aria-hidden="true"></div>
<div class="skel-topbar" aria-hidden="true"></div>
<div class="app-spinner" aria-hidden="true"></div>
<div class="bg-bg text-t1 font-sans antialiased flex h-screen overflow-hidden">
<!-- Mobile Sidebar Overlay -->
<div
class="fixed inset-0 z-20 bg-black/60 md:hidden"
x-show="$store.sidebar?.open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="$store.sidebar.open = false"
aria-hidden="true"
x-cloak
></div>
<!-- Sidebar: persisted across wire:navigate to prevent re-render -->
@persist('sidebar')
<div
class="fixed inset-y-0 left-0 z-30 md:relative md:z-auto md:flex md:flex-shrink-0 transition-transform duration-200 ease-in-out"
:class="$store.sidebar?.open ? 'translate-x-0' : '-translate-x-full md:translate-x-0'"
>
<x-sidebar />
</div>
@endpersist
<!-- Main Area -->
<div class="flex-1 flex flex-col overflow-hidden min-w-0">
<x-topbar :title="$title ?? ''" />
<main class="flex-1 overflow-y-auto p-6">
{{ $slot }}
</main>
</div>
</div>
@livewire('wire-elements-modal')
@livewireScripts
<script>
(function () {
function markReady() { document.body.classList.add('app-ready'); }
if (document.readyState === 'complete') {
markReady();
} else {
window.addEventListener('load', markReady);
setTimeout(markReady, 600);
}
// Re-apply on wire:navigate (SPA navigation)
document.addEventListener('livewire:navigated', markReady);
})();
</script>
</body>
</html>