map fade
This commit is contained in:
@@ -142,7 +142,8 @@
|
||||
});
|
||||
|
||||
// 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
|
||||
// the wait reliable - the layout clears the ready flag before the swap rather
|
||||
// than trusting the incoming page to have done it.
|
||||
@@ -153,6 +154,7 @@
|
||||
'/weather/compare',
|
||||
'/weather/seasonal',
|
||||
'/weather/historical',
|
||||
'/weather/maps',
|
||||
'/weather/week/[location]',
|
||||
'/weather/14-day/[location]',
|
||||
'/weather/compare/[location]',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { reportPageReady } from '$lib/stores/page-transition.svelte';
|
||||
import { storedLocation, storedModel, storedTheme } from '$lib/stores/settings';
|
||||
|
||||
import { mapsDomainForModel } from '$lib/utils/maps-domain';
|
||||
@@ -16,6 +17,15 @@
|
||||
let hashOverride = $state<string | null>(null);
|
||||
let iframeEl = $state<HTMLIFrameElement | null>(null);
|
||||
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>) => {
|
||||
iframeEl?.contentWindow?.postMessage(message, MAPS_ORIGIN);
|
||||
@@ -95,6 +105,7 @@
|
||||
allowfullscreen
|
||||
allow="cross-origin-isolated"
|
||||
referrerpolicy="no-referrer"
|
||||
onload={() => (frameLoaded = true)}
|
||||
class="block h-full w-full border-0"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
></iframe>
|
||||
|
||||
@@ -197,35 +197,41 @@
|
||||
and archive pages: the controls then cannot move the heading when they
|
||||
change size. -->
|
||||
<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
|
||||
mounted and merely hidden while the forecast is on its way: mounting
|
||||
them on arrival re-flowed the row and nudged the heading. -->
|
||||
<div
|
||||
class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto"
|
||||
class:invisible={!result}
|
||||
role="group"
|
||||
aria-label={m.seasonal_range_aria()}
|
||||
aria-hidden={!result}
|
||||
>
|
||||
{#each RANGES as range, i (range.label)}
|
||||
{@const disabled = !result || (range.days !== Infinity && range.days > horizonDays)}
|
||||
<button
|
||||
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 ===
|
||||
i
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'} {disabled
|
||||
? 'cursor-not-allowed opacity-40'
|
||||
: ''}"
|
||||
aria-pressed={rangeIndex === i}
|
||||
{disabled}
|
||||
tabindex={result ? undefined : -1}
|
||||
onclick={() => (rangeIndex = i)}
|
||||
>
|
||||
{range.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Range buttons reslice the already-fetched horizon (no refetch). While a
|
||||
forecast is on its way they stay mounted but invisible, because
|
||||
mounting them on arrival re-flowed the row and nudged the heading.
|
||||
Two exceptions, both of which would reserve a gap for something that
|
||||
is never coming: below sm the group is a full-width row of its own,
|
||||
and behind the paywall there is no forecast on its way at all. -->
|
||||
{#if $isSupporter}
|
||||
<div
|
||||
class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto {result
|
||||
? ''
|
||||
: 'hidden sm:flex sm:invisible'}"
|
||||
role="group"
|
||||
aria-label={m.seasonal_range_aria()}
|
||||
aria-hidden={!result}
|
||||
>
|
||||
{#each RANGES as range, i (range.label)}
|
||||
{@const disabled = !result || (range.days !== Infinity && range.days > horizonDays)}
|
||||
<button
|
||||
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 ===
|
||||
i
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'} {disabled
|
||||
? 'cursor-not-allowed opacity-40'
|
||||
: ''}"
|
||||
aria-pressed={rangeIndex === i}
|
||||
{disabled}
|
||||
tabindex={result ? undefined : -1}
|
||||
onclick={() => (rangeIndex = i)}
|
||||
>
|
||||
{range.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ModelSelector
|
||||
selectedModel={seasonalModel}
|
||||
groups={seasonalModelGroups}
|
||||
|
||||
Reference in New Issue
Block a user