This commit is contained in:
Vincent van der Wal
2026-08-06 21:25:29 +02:00
parent c4a74ba91c
commit 810f9c2605
3 changed files with 49 additions and 30 deletions
+3 -1
View File
@@ -142,7 +142,8 @@
}); });
// Routes that are not finished on arrival: the five that fetch a forecast // Routes that are not finished on arrival: the five that fetch a forecast
// after mounting, and the redirect stubs, which render nothing at all and // after mounting, the maps page (whose map is a cross-origin iframe that has
// to load first), and the redirect stubs, which render nothing at all and
// bounce to a located URL from `onMount`. Knowing this up front is what makes // bounce to a located URL from `onMount`. Knowing this up front is what makes
// the wait reliable - the layout clears the ready flag before the swap rather // the wait reliable - the layout clears the ready flag before the swap rather
// than trusting the incoming page to have done it. // than trusting the incoming page to have done it.
@@ -153,6 +154,7 @@
'/weather/compare', '/weather/compare',
'/weather/seasonal', '/weather/seasonal',
'/weather/historical', '/weather/historical',
'/weather/maps',
'/weather/week/[location]', '/weather/week/[location]',
'/weather/14-day/[location]', '/weather/14-day/[location]',
'/weather/compare/[location]', '/weather/compare/[location]',
+11
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { reportPageReady } from '$lib/stores/page-transition.svelte';
import { storedLocation, storedModel, storedTheme } from '$lib/stores/settings'; import { storedLocation, storedModel, storedTheme } from '$lib/stores/settings';
import { mapsDomainForModel } from '$lib/utils/maps-domain'; import { mapsDomainForModel } from '$lib/utils/maps-domain';
@@ -16,6 +17,15 @@
let hashOverride = $state<string | null>(null); let hashOverride = $state<string | null>(null);
let iframeEl = $state<HTMLIFrameElement | null>(null); let iframeEl = $state<HTMLIFrameElement | null>(null);
let mapReady = $state(false); let mapReady = $state(false);
let frameLoaded = $state(false);
// The map is a cross-origin iframe: until it has loaded there is nothing here
// but an empty panel, and a page transition that ends before then cross-fades
// the old page into that blank. Reporting readiness holds the transition (and
// then the loading overlay) until there is actually a map to fade into.
// `om-maps:ready` is the good signal; the iframe's own load event is the
// backstop, so a map that fails to boot still releases the page.
reportPageReady(() => mapReady || frameLoaded);
const postToMap = (message: Record<string, unknown>) => { const postToMap = (message: Record<string, unknown>) => {
iframeEl?.contentWindow?.postMessage(message, MAPS_ORIGIN); iframeEl?.contentWindow?.postMessage(message, MAPS_ORIGIN);
@@ -95,6 +105,7 @@
allowfullscreen allowfullscreen
allow="cross-origin-isolated" allow="cross-origin-isolated"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
onload={() => (frameLoaded = true)}
class="block h-full w-full border-0" class="block h-full w-full border-0"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups" sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe> ></iframe>
@@ -197,35 +197,41 @@
and archive pages: the controls then cannot move the heading when they and archive pages: the controls then cannot move the heading when they
change size. --> change size. -->
<div class="flex w-full flex-wrap items-center gap-3 sm:w-auto lg:absolute lg:top-0 lg:right-0"> <div class="flex w-full flex-wrap items-center gap-3 sm:w-auto lg:absolute lg:top-0 lg:right-0">
<!-- Range buttons reslice the already-fetched horizon (no refetch). Kept <!-- Range buttons reslice the already-fetched horizon (no refetch). While a
mounted and merely hidden while the forecast is on its way: mounting forecast is on its way they stay mounted but invisible, because
them on arrival re-flowed the row and nudged the heading. --> mounting them on arrival re-flowed the row and nudged the heading.
<div Two exceptions, both of which would reserve a gap for something that
class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto" is never coming: below sm the group is a full-width row of its own,
class:invisible={!result} and behind the paywall there is no forecast on its way at all. -->
role="group" {#if $isSupporter}
aria-label={m.seasonal_range_aria()} <div
aria-hidden={!result} class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto {result
> ? ''
{#each RANGES as range, i (range.label)} : 'hidden sm:flex sm:invisible'}"
{@const disabled = !result || (range.days !== Infinity && range.days > horizonDays)} role="group"
<button aria-label={m.seasonal_range_aria()}
type="button" aria-hidden={!result}
class="flex-1 cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold whitespace-nowrap transition-colors sm:flex-none {rangeIndex === >
i {#each RANGES as range, i (range.label)}
? 'bg-primary text-primary-foreground' {@const disabled = !result || (range.days !== Infinity && range.days > horizonDays)}
: 'text-muted-foreground hover:bg-muted hover:text-foreground'} {disabled <button
? 'cursor-not-allowed opacity-40' type="button"
: ''}" class="flex-1 cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold whitespace-nowrap transition-colors sm:flex-none {rangeIndex ===
aria-pressed={rangeIndex === i} i
{disabled} ? 'bg-primary text-primary-foreground'
tabindex={result ? undefined : -1} : 'text-muted-foreground hover:bg-muted hover:text-foreground'} {disabled
onclick={() => (rangeIndex = i)} ? 'cursor-not-allowed opacity-40'
> : ''}"
{range.label} aria-pressed={rangeIndex === i}
</button> {disabled}
{/each} tabindex={result ? undefined : -1}
</div> onclick={() => (rangeIndex = i)}
>
{range.label}
</button>
{/each}
</div>
{/if}
<ModelSelector <ModelSelector
selectedModel={seasonalModel} selectedModel={seasonalModel}
groups={seasonalModelGroups} groups={seasonalModelGroups}