day swap animation
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
storedVariablePrefs
|
||||
} from '$lib/stores/settings';
|
||||
|
||||
import { formatZoned } from '$lib/utils/date';
|
||||
import { daySwap } from '$lib/utils/day-swap';
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
|
||||
import { ChartContainer } from '$lib/components/charts';
|
||||
@@ -27,6 +29,7 @@
|
||||
import { defaultParameters } from '../../options';
|
||||
import { computeDayNightWeatherCodes } from '../../utils/weather-codes';
|
||||
import DailyStripSticky from './DailyStripSticky.svelte';
|
||||
import DaySummary from './DaySummary.svelte';
|
||||
import HourlyTable from './HourlyTable.svelte';
|
||||
import MeteogramCharts from './MeteogramCharts.svelte';
|
||||
import ModelSelector from './ModelSelector.svelte';
|
||||
@@ -59,13 +62,11 @@
|
||||
// arrives (no layout shift)
|
||||
let enabledChartCount = $derived($storedChartLayout.filter((p) => p.variables.length > 0).length);
|
||||
|
||||
// Height the hourly table will occupy once it renders, so its placeholder
|
||||
// reserves exactly that and nothing below it jumps. The table is a header row
|
||||
// plus one row per enabled variable, so the count drives the estimate.
|
||||
const TABLE_HEADER_PX = 96;
|
||||
// The hourly table is a header row plus one row per enabled variable, so the
|
||||
// placeholder below reserves exactly that and nothing under it jumps when the
|
||||
// real table arrives.
|
||||
const TABLE_ROW_PX = 57;
|
||||
let enabledTableRows = $derived(Object.values($storedVariablePrefs.table).filter(Boolean).length);
|
||||
let tableSkeletonHeight = $derived(TABLE_HEADER_PX + enabledTableRows * TABLE_ROW_PX);
|
||||
|
||||
// Request only the hourly variables the table rows and meteograms actually
|
||||
// show, so unused variables are never fetched. weather_code is always
|
||||
@@ -129,6 +130,13 @@
|
||||
let fetchedHourly: FetchedHourly | null = $state(null);
|
||||
let fetchedDaily: FetchedDaily | null = $state(null);
|
||||
|
||||
// Stable per-day key: the fade only replays when the day actually changes,
|
||||
// not on every clock tick or refetch.
|
||||
let selectedDayKey = $derived.by(() => {
|
||||
const fd = fetchedDaily;
|
||||
return fd ? formatZoned(selectedDay, fd.timezone, 'yyyy-MM-dd') : '';
|
||||
});
|
||||
|
||||
// Charts intentionally keep their current range: they show the full week
|
||||
// unless the user narrows it via the range presets or Ctrl+scroll.
|
||||
const switchDay = (date: Date) => {
|
||||
@@ -320,14 +328,16 @@
|
||||
/>
|
||||
|
||||
{#if fetchedHourly && fetchedDaily}
|
||||
<HourlyTable
|
||||
data={fetchedHourly}
|
||||
daily={fetchedDaily}
|
||||
{selectedDay}
|
||||
units={params}
|
||||
locationName={location.name ?? ''}
|
||||
onCustomize={() => (variableSidebarOpen = true)}
|
||||
/>
|
||||
<div use:daySwap={selectedDayKey}>
|
||||
<HourlyTable
|
||||
data={fetchedHourly}
|
||||
daily={fetchedDaily}
|
||||
{selectedDay}
|
||||
units={params}
|
||||
locationName={location.name ?? ''}
|
||||
onCustomize={() => (variableSidebarOpen = true)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Mirrors the real table: same header bar and the same body height,
|
||||
so the heading doesn't pop in and nothing below moves.
|
||||
@@ -347,12 +357,35 @@
|
||||
<div class="h-8 w-20 animate-pulse rounded-lg bg-muted"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animate-pulse bg-card" style="height: {tableSkeletonHeight}px"></div>
|
||||
<!-- one placeholder per row the real table will render, so the body
|
||||
reads as a loading table rather than a blank panel -->
|
||||
<div class="divide-y divide-border/50">
|
||||
{#each { length: enabledTableRows } as _, i (i)}
|
||||
<div class="flex items-center gap-4 px-4" style="height: {TABLE_ROW_PX}px">
|
||||
<div class="h-3.5 w-14 shrink-0 animate-pulse rounded bg-muted"></div>
|
||||
<div class="h-3.5 flex-1 animate-pulse rounded bg-muted/70"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if fetchedHourly && fetchedDaily}
|
||||
<div use:daySwap={selectedDayKey}>
|
||||
<DaySummary data={fetchedHourly} daily={fetchedDaily} {selectedDay} units={params} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- same footprint as the written forecast, so it doesn't shove the
|
||||
meteograms down when it arrives -->
|
||||
<section class="mt-6" in:fade={{ duration: 200 }}>
|
||||
<div class="h-52 animate-pulse rounded-2xl border border-border/70 bg-card sm:h-40"></div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if fetchedHourly}
|
||||
<MeteogramCharts data={fetchedHourly} {selectedDay} units={params} {loading} />
|
||||
<div use:daySwap={selectedDayKey}>
|
||||
<MeteogramCharts data={fetchedHourly} {selectedDay} units={params} {loading} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- reserve the exact chart area height before the first fetch resolves,
|
||||
header row included -->
|
||||
|
||||
Reference in New Issue
Block a user