more fluent texts

This commit is contained in:
Vincent van der Wal
2026-08-01 15:27:26 +02:00
parent 774afa6c65
commit 0d5dbbc61f
18 changed files with 694 additions and 201 deletions
+67 -7
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { get } from 'svelte/store';
import { fade, fly } from 'svelte/transition';
import { afterNavigate, onNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { storedTheme } from '$lib/stores/settings';
@@ -13,6 +15,8 @@
import { routePath } from '$lib/i18n';
import { pageContentReady } from '$lib/stores/page-transition.svelte';
import './layout.css';
let { children } = $props();
@@ -26,21 +30,74 @@
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const apply = () => {
const root = document.documentElement;
const dark = theme === 'dark' || (theme === 'system' && mq.matches);
const paint = () => root.classList.toggle('dark', dark);
// 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);
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!themeSettled || reduced) {
themeSettled = true;
paint();
return;
}
themeSettled = true;
root.classList.toggle('dark', theme === 'dark' || (theme === 'system' && mq.matches));
if (document.startViewTransition) {
// one cross-fade of the whole document; component transitions untouched
document.startViewTransition(paint);
return;
}
// no view transitions: fall back to fading the colours for one window
root.classList.add('theme-transition');
clearTimeout(themeTimer);
themeTimer = window.setTimeout(() => root.classList.remove('theme-transition'), 400);
paint();
};
apply();
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
});
// ── Page cross-fade ───────────────────────────────────────────────────────
// The weather pages fetch their forecast after the route swap, so tying the
// fade to navigation alone would cross-fade one skeleton into another and
// then cut hard to the real content. Instead the outgoing page fades out on
// navigation and the incoming one fades in once it reports that its data has
// landed - with a ceiling so a slow or silent page is never left hidden.
const FADE_OUT_MS = 170;
const REVEAL_CEILING_MS = 2500;
let contentVisible = $state(true);
let revealTimer = 0;
const reducedMotion = () =>
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
onNavigate(() => {
if (reducedMotion()) return;
clearTimeout(revealTimer);
contentVisible = false;
return new Promise((resolve) => setTimeout(resolve, FADE_OUT_MS));
});
afterNavigate(() => {
if (reducedMotion()) {
contentVisible = true;
return;
}
const startedAt = Date.now();
const reveal = () => {
if (get(pageContentReady) || Date.now() - startedAt > REVEAL_CEILING_MS) {
contentVisible = true;
return;
}
revealTimer = window.setTimeout(reveal, 60);
};
reveal();
});
// the maps page embeds a full-bleed map: no padding, no scrolling
let fullBleed = $derived(routePath($page.url.pathname).startsWith('/weather/maps'));
@@ -110,7 +167,10 @@
{: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">
<div
class="page-fade mx-auto w-full max-w-[1536px] flex-1 pb-24"
class:page-fade-hidden={!contentVisible}
>
{@render children()}
</div>
<!-- full-bleed footer inside the scroll area (its own inner max-w),