Files
market_screener/ui/src/routes/+layout.svelte
T
2026-06-04 01:36:28 -04:00

133 lines
3.4 KiB
Svelte

<script>
import { page, navigating } from '$app/stores';
import '../app.css';
let { children } = $props();
// Label shown under the nav progress bar while loading a page
const navLabel = $derived(
$navigating?.to?.url?.pathname === '/portfolio' ? 'Loading portfolio…' :
$navigating?.to?.url?.pathname?.startsWith('/calls') ? 'Loading market calls…' :
$navigating?.to?.url?.pathname === '/safe-buys' ? 'Screening safe buys…' :
'Loading…'
);
</script>
<div class="shell">
<nav>
<span class="brand">📊 Market Screener</span>
<div class="links">
<a href="/" class:active={$page.url.pathname === '/'}>Screener</a>
<a href="/portfolio" class:active={$page.url.pathname === '/portfolio'}>Portfolio</a>
<a href="/calls" class:active={$page.url.pathname.startsWith('/calls')}>Market Calls</a>
<a href="/safe-buys" class:active={$page.url.pathname === '/safe-buys'}>🛡 Safe Buys</a>
</div>
</nav>
<!-- Thin progress bar at top of screen — always visible even on first nav -->
{#if $navigating}
<div class="nav-progress">
<div class="nav-bar"></div>
</div>
{/if}
<main>
{#if $navigating}
<!-- Replace old page content immediately — old page disappears, spinner takes over -->
<div class="nav-overlay">
<div class="nav-spinner"></div>
<span class="nav-label">{navLabel}</span>
</div>
{:else}
{@render children()}
{/if}
</main>
</div>
<style>
.shell { min-height: 100vh; display: flex; flex-direction: column; }
nav {
display: flex;
align-items: center;
gap: 24px;
padding: 14px 32px;
border-bottom: 1px solid #1e293b;
background: #0f1117;
position: sticky;
top: 0;
z-index: 10;
}
.brand { font-size: 15px; font-weight: 700; color: #f1f5f9; }
.links { display: flex; gap: 4px; margin-left: auto; }
.links a {
color: #64748b;
text-decoration: none;
padding: 6px 14px;
border-radius: 6px;
font-weight: 500;
transition: color 0.15s, background 0.15s;
}
.links a:hover { color: #e2e8f0; background: #1e293b; }
.links a.active { color: #e2e8f0; background: #1e293b; }
main { flex: 1; padding: 28px 32px; }
/* ── Navigation progress ─────────────────────────────────────────── */
.nav-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 2px;
z-index: 100;
background: #1e293b;
overflow: hidden;
}
.nav-bar {
height: 100%;
background: #3b82f6;
animation: progress 1.5s ease-in-out infinite;
transform-origin: left;
}
@keyframes progress {
0% { transform: translateX(-100%) scaleX(0.3); }
50% { transform: translateX(0%) scaleX(0.7); }
100% { transform: translateX(100%) scaleX(0.3); }
}
/* Centered spinner + label in the page body */
.nav-overlay {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
padding: 100px 0;
flex: 1;
}
.nav-spinner {
width: 40px;
height: 40px;
border: 3px solid #1e293b;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.nav-label {
font-size: 12px;
color: #475569;
letter-spacing: 0.02em;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>