123 lines
3.8 KiB
Svelte
123 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import { fade, fly } from 'svelte/transition';
|
|
|
|
import { page } from '$app/stores';
|
|
|
|
import { storedTheme } from '$lib/stores/settings';
|
|
|
|
import Footer from '$lib/components/navigation/footer.svelte';
|
|
import Header from '$lib/components/navigation/header.svelte';
|
|
import WeatherNav from '$lib/components/navigation/weather-nav.svelte';
|
|
|
|
import favicon from '$lib/assets/favicon.svg';
|
|
|
|
import './layout.css';
|
|
|
|
let { children } = $props();
|
|
|
|
// keep the .dark class in sync with the persisted theme; in 'system' mode
|
|
// follow the OS preference live
|
|
let themeSettled = false;
|
|
let themeTimer = 0;
|
|
$effect(() => {
|
|
const theme = $storedTheme;
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const apply = () => {
|
|
const root = document.documentElement;
|
|
// The very first application is just painting the stored theme - only
|
|
// an actual switch afterwards is worth cross-fading.
|
|
if (themeSettled) {
|
|
root.classList.add('theme-transition');
|
|
clearTimeout(themeTimer);
|
|
themeTimer = window.setTimeout(() => root.classList.remove('theme-transition'), 400);
|
|
}
|
|
themeSettled = true;
|
|
root.classList.toggle('dark', theme === 'dark' || (theme === 'system' && mq.matches));
|
|
};
|
|
apply();
|
|
mq.addEventListener('change', apply);
|
|
return () => mq.removeEventListener('change', apply);
|
|
});
|
|
|
|
// the maps page embeds a full-bleed map: no padding, no scrolling
|
|
let fullBleed = $derived($page.url.pathname.startsWith('/weather/maps'));
|
|
|
|
let sidebarCollapsed = $state(false);
|
|
let mobileMenuOpen = $state(false);
|
|
|
|
const toggleSidebar = () => {
|
|
sidebarCollapsed = !sidebarCollapsed;
|
|
};
|
|
|
|
const toggleMobileMenu = () => {
|
|
mobileMenuOpen = !mobileMenuOpen;
|
|
};
|
|
|
|
const closeMobileMenu = () => {
|
|
mobileMenuOpen = false;
|
|
};
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<link rel="icon" href={favicon} />
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
</svelte:head>
|
|
|
|
<div class="flex h-screen overflow-hidden bg-background text-foreground">
|
|
<!-- Desktop sidebar -->
|
|
<div class="hidden h-full shrink-0 md:block">
|
|
<WeatherNav collapsed={sidebarCollapsed} onToggle={toggleSidebar} />
|
|
</div>
|
|
|
|
<!-- Mobile overlay -->
|
|
{#if mobileMenuOpen}
|
|
<div class="fixed inset-0 z-50 md:hidden" role="presentation">
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="absolute inset-0 bg-black/30"
|
|
transition:fade={{ duration: 150 }}
|
|
onclick={closeMobileMenu}
|
|
onkeydown={closeMobileMenu}
|
|
></div>
|
|
<div
|
|
class="relative z-1 h-full w-55 shadow-lg"
|
|
transition:fly={{ x: -220, duration: 200, opacity: 1 }}
|
|
>
|
|
<WeatherNav collapsed={false} onMobileClose={closeMobileMenu} />
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Main area: topbar + content -->
|
|
<div class="flex min-w-0 flex-1 flex-col h-full">
|
|
<Header onMenuToggle={toggleMobileMenu} />
|
|
|
|
<!-- The padding stays on <main> itself: the day strip sticks with a
|
|
negative offset that exactly cancels it, so moving it to an inner
|
|
wrapper would dock the strip too high and clip its top row.
|
|
flex-col + flex-1 below keeps the footer on the bottom edge even when
|
|
the page is too short to fill the viewport. -->
|
|
<main
|
|
class={fullBleed
|
|
? 'flex-1 overflow-hidden'
|
|
: 'flex flex-1 flex-col overflow-y-auto p-3 lg:px-8 lg:py-6'}
|
|
>
|
|
{#if fullBleed}
|
|
{@render children()}
|
|
{:else}
|
|
<!-- cap the content width on very large screens; the footer below
|
|
gives the page its ending, so only modest bottom room is needed -->
|
|
<div class="mx-auto w-full max-w-[1536px] flex-1 pb-24">
|
|
{@render children()}
|
|
</div>
|
|
<!-- full-bleed footer inside the scroll area (its own inner max-w),
|
|
cancelling main's padding so it sits flush with the edges -->
|
|
<div class="-mx-3 -mb-3 lg:-mx-8 lg:-mb-6">
|
|
<Footer />
|
|
</div>
|
|
{/if}
|
|
</main>
|
|
</div>
|
|
</div>
|