This repository has been archived on 2026-08-10. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
drizzli/src/routes/+layout.svelte
T
2026-08-01 13:37:04 +02:00

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>