crossfade
This commit is contained in:
@@ -388,7 +388,18 @@
|
||||
|
||||
// Minimal gutters on narrow screens so the plot uses nearly the full width
|
||||
// (just enough to keep the axis tick labels legible).
|
||||
let isNarrow = $derived(width > 0 && width < 520);
|
||||
// Compact gutters (small axis padding, short icon rows) apply to phones AND
|
||||
// tablets: below `lg` the page is edge-to-edge, so wide desktop-style axis
|
||||
// margins would waste most of the width. Desktop keeps its roomier metrics.
|
||||
let belowDesktop = $state(false);
|
||||
onMount(() => {
|
||||
const mq = window.matchMedia('(max-width: 1023px)');
|
||||
const apply = () => (belowDesktop = mq.matches);
|
||||
apply();
|
||||
mq.addEventListener('change', apply);
|
||||
return () => mq.removeEventListener('change', apply);
|
||||
});
|
||||
let isNarrow = $derived(belowDesktop || (width > 0 && width < 520));
|
||||
let padLeft = $derived(isNarrow ? 26 : 60);
|
||||
// Reserve the right gutter when this chart (or a sibling, via reserveRightAxis)
|
||||
// has a right axis, so a stacked row of charts share the same plot width. On
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import { href, routePath } from '$lib/i18n';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
@@ -18,6 +22,7 @@
|
||||
{
|
||||
title: m.nav_week,
|
||||
url: '/weather/week' as const,
|
||||
route: '/weather/week/[location]' as const,
|
||||
iconPaths: [
|
||||
'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'
|
||||
]
|
||||
@@ -25,11 +30,13 @@
|
||||
{
|
||||
title: m.nav_compare,
|
||||
url: '/weather/compare' as const,
|
||||
route: '/weather/compare/[location]' as const,
|
||||
iconPaths: ['M13 7h8m0 0v8m0-8l-8 8-4-4-6 6']
|
||||
},
|
||||
{
|
||||
title: m.nav_14day,
|
||||
url: '/weather/14-day' as const,
|
||||
route: '/weather/14-day/[location]' as const,
|
||||
iconPaths: [
|
||||
'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z'
|
||||
]
|
||||
@@ -37,12 +44,14 @@
|
||||
{
|
||||
title: m.nav_seasonal,
|
||||
url: '/weather/seasonal' as const,
|
||||
route: '/weather/seasonal/[location]' as const,
|
||||
// rising trend line (long-range outlook)
|
||||
iconPaths: ['M3 17l6-6 4 4 7-7', 'M16 8h5v5']
|
||||
},
|
||||
{
|
||||
title: m.nav_historical,
|
||||
url: '/weather/historical' as const,
|
||||
route: '/weather/historical/[location]' as const,
|
||||
// clock with a counter-clockwise arrow (history)
|
||||
iconPaths: ['M12 8v4l3 2', 'M3.5 9a9 9 0 1 0 2.2-3.6L3 8m0-4.5V8h4.5']
|
||||
},
|
||||
@@ -59,6 +68,11 @@
|
||||
// the URL carries a locale prefix; compare the neutral path behind it
|
||||
let currentPath = $derived(routePath($page.url.pathname));
|
||||
|
||||
// Link straight to the location page instead of the bare redirect route: it
|
||||
// saves a navigation, and the page cross-fade can then wait for the real
|
||||
// page's data instead of flashing through an empty redirect stub.
|
||||
let locationRoute = $derived(buildLocationRoute($storedLocation));
|
||||
|
||||
const isActive = (url: string) => {
|
||||
return currentPath === url || currentPath.startsWith(url + '/');
|
||||
};
|
||||
@@ -73,7 +87,7 @@
|
||||
home link fills the entire row, padding included -->
|
||||
<div class="flex h-14 shrink-0 items-stretch border-b border-sidebar-border">
|
||||
<a
|
||||
href={href('/weather/week')}
|
||||
href={href('/weather/week/[location]', { location: locationRoute })}
|
||||
class="flex flex-1 items-center gap-2.5 transition-colors hover:bg-sidebar-accent {collapsed
|
||||
? 'justify-center'
|
||||
: 'px-4'}"
|
||||
@@ -98,7 +112,7 @@
|
||||
{#each links as link (link.url)}
|
||||
{@const active = isActive(link.url)}
|
||||
<a
|
||||
href={href(link.url)}
|
||||
href={link.route ? href(link.route, { location: locationRoute }) : href(link.url)}
|
||||
class="relative flex items-center rounded-md px-2.5 py-2 text-sm font-medium text-sidebar-foreground opacity-70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:opacity-100 {active
|
||||
? 'bg-sidebar-accent text-sidebar-primary! opacity-100! font-semibold! nav-active'
|
||||
: ''}"
|
||||
|
||||
@@ -5,19 +5,29 @@ import { writable } from 'svelte/store';
|
||||
*
|
||||
* The weather pages fetch their forecast *after* the route swap, so a plain
|
||||
* navigation transition would cross-fade one skeleton into another and then cut
|
||||
* hard to the real content. The layout waits on this instead, so the fade-in
|
||||
* lines up with the page actually being loaded.
|
||||
* hard to the real content. The layout holds its transition open on this flag
|
||||
* instead, so the fade lines up with the page actually being loaded.
|
||||
*/
|
||||
export const pageContentReady = writable(true);
|
||||
|
||||
/**
|
||||
* Declare a page's readiness. Pass a getter for "my data has arrived"; the flag
|
||||
* is released when the page unmounts, so a page that never reports readiness
|
||||
* (static content) is revealed immediately.
|
||||
* Called by the layout before it swaps to a route that fetches its own data.
|
||||
*
|
||||
* The layout owns the "not ready yet" side deliberately: the incoming page's
|
||||
* effects have not necessarily run at that point (rendering is paused inside a
|
||||
* view transition), so a page that cleared the flag itself would sometimes be
|
||||
* announced as ready before it had fetched anything.
|
||||
*/
|
||||
export function markPageLoading(): void {
|
||||
pageContentReady.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a page's readiness. Pass a getter for "my data has arrived". Only
|
||||
* ever sets the flag - clearing it is the layout's job (see above).
|
||||
*/
|
||||
export function reportPageReady(isReady: () => boolean): void {
|
||||
$effect(() => {
|
||||
pageContentReady.set(isReady());
|
||||
return () => pageContentReady.set(true);
|
||||
if (isReady()) pageContentReady.set(true);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user