beter crossfade
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
import { formatZoned } from '$lib/utils/date';
|
||||
import { daySwap, runDayTransition } from '$lib/utils/day-swap';
|
||||
import { buildLocationRoute } from '$lib/utils/location';
|
||||
import { skeletonOut } from '$lib/utils/skeleton-fade';
|
||||
import { syncSearchParams, unlessDefault } from '$lib/utils/url-state';
|
||||
|
||||
import { ChartContainer } from '$lib/components/charts';
|
||||
@@ -473,109 +474,115 @@
|
||||
{locationRoute}
|
||||
/>
|
||||
|
||||
{#if fetchedHourly && fetchedDaily}
|
||||
<div class="day-region-table" 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.
|
||||
Placeholders only fade IN - a fade-out would keep them in the
|
||||
layout while the real content mounts below, and everything below
|
||||
would jump the moment they finally unmount. -->
|
||||
<div
|
||||
in:fade={{ duration: 200 }}
|
||||
class="-mx-3 overflow-hidden border-y border-border/70 bg-card shadow-sm md:mx-0 md:rounded-2xl md:border"
|
||||
>
|
||||
<!-- `relative` is what lets the placeholder fade out on top of the table
|
||||
instead of holding a second slot in the layout (see skeletonOut). -->
|
||||
<div class="relative">
|
||||
{#if fetchedHourly && fetchedDaily}
|
||||
<div class="day-region-table" 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. -->
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-b border-border/70 bg-muted/40 px-4 py-2.5"
|
||||
in:fade={{ duration: 200 }}
|
||||
out:skeletonOut
|
||||
class="-mx-3 overflow-hidden border-y border-border/70 bg-card shadow-sm md:mx-0 md:rounded-2xl md:border"
|
||||
>
|
||||
<div class="h-6 w-44 animate-pulse rounded bg-muted"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-24 animate-pulse rounded-lg bg-muted"></div>
|
||||
<div class="h-8 w-20 animate-pulse rounded-lg bg-muted"></div>
|
||||
</div>
|
||||
</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">
|
||||
<!-- the time + daylight row, which is taller than the variable rows -->
|
||||
<div class="flex items-center gap-4 px-4" style="height: {TABLE_TIME_ROW_PX}px">
|
||||
<div class="h-3.5 w-14 shrink-0 animate-pulse rounded bg-muted"></div>
|
||||
<div class="h-5 flex-1 animate-pulse rounded bg-muted/70"></div>
|
||||
</div>
|
||||
{#each { length: enabledTableRows } as _, i (i)}
|
||||
<div class="flex items-center gap-4 px-4" style="height: {tableRowPx}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
|
||||
class="flex items-center justify-between gap-2 border-b border-border/70 bg-muted/40 px-4 py-2.5"
|
||||
>
|
||||
<div class="h-6 w-44 animate-pulse rounded bg-muted"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-24 animate-pulse rounded-lg bg-muted"></div>
|
||||
<div class="h-8 w-20 animate-pulse rounded-lg bg-muted"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</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">
|
||||
<!-- the time + daylight row, which is taller than the variable rows -->
|
||||
<div class="flex items-center gap-4 px-4" style="height: {TABLE_TIME_ROW_PX}px">
|
||||
<div class="h-3.5 w-14 shrink-0 animate-pulse rounded bg-muted"></div>
|
||||
<div class="h-5 flex-1 animate-pulse rounded bg-muted/70"></div>
|
||||
</div>
|
||||
{#each { length: enabledTableRows } as _, i (i)}
|
||||
<div class="flex items-center gap-4 px-4" style="height: {tableRowPx}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>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if fetchedHourly && fetchedDaily}
|
||||
<div class="day-region-summary" 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. The heights are measured from the
|
||||
real card at each breakpoint: on phones the narrative is clamped to
|
||||
five lines with a fixed toggle row, so that side is exact; from md
|
||||
up the text runs free and this is the typical height. The sun/moon
|
||||
grid reflows at sm, md and lg, which is why all four are needed. -->
|
||||
<section class="mt-6" in:fade={{ duration: 200 }}>
|
||||
<div
|
||||
class="h-96.5 animate-pulse rounded-2xl border border-border/70 bg-card sm:h-85 md:h-73 lg:h-56"
|
||||
></div>
|
||||
</section>
|
||||
{/if}
|
||||
<div class="relative">
|
||||
{#if fetchedHourly && fetchedDaily}
|
||||
<div class="day-region-summary" 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. The heights are measured from the
|
||||
real card at each breakpoint: on phones the narrative is clamped to
|
||||
five lines with a fixed toggle row, so that side is exact; from md
|
||||
up the text runs free and this is the typical height. The sun/moon
|
||||
grid reflows at sm, md and lg, which is why all four are needed. -->
|
||||
<section class="mt-6" in:fade={{ duration: 200 }} out:skeletonOut>
|
||||
<div
|
||||
class="h-96.5 animate-pulse rounded-2xl border border-border/70 bg-card sm:h-85 md:h-73 lg:h-56"
|
||||
></div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if fetchedHourly}
|
||||
<div class="day-region-charts" use:daySwap={selectedDayKey}>
|
||||
<MeteogramCharts
|
||||
data={fetchedHourly}
|
||||
{selectedDay}
|
||||
units={params}
|
||||
{loading}
|
||||
{chartHeight}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- reserve the exact chart area height before the first fetch resolves,
|
||||
header row included -->
|
||||
<section class="mt-8" in:fade={{ duration: 200 }}>
|
||||
<!-- The real header wraps to two rows until the controls fit beside
|
||||
<div class="relative">
|
||||
{#if fetchedHourly}
|
||||
<div class="day-region-charts" use:daySwap={selectedDayKey}>
|
||||
<MeteogramCharts
|
||||
data={fetchedHourly}
|
||||
{selectedDay}
|
||||
units={params}
|
||||
{loading}
|
||||
{chartHeight}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- reserve the exact chart area height before the first fetch resolves,
|
||||
header row included -->
|
||||
<section class="mt-8" in:fade={{ duration: 200 }} out:skeletonOut>
|
||||
<!-- The real header wraps to two rows until the controls fit beside
|
||||
the title, which happens at different widths than you would
|
||||
expect because the sidebar takes its share from md up. These
|
||||
min-heights follow the measured wrap points. -->
|
||||
<div
|
||||
class="mb-3 flex min-h-27 flex-wrap items-center justify-between gap-2 sm:min-h-16.5 md:min-h-27 lg:min-h-16.5 xl:min-h-7.5"
|
||||
>
|
||||
<div class="h-7 w-52 animate-pulse rounded bg-muted"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-7 w-56 animate-pulse rounded-lg bg-muted"></div>
|
||||
<div class="h-7 w-24 animate-pulse rounded-lg bg-muted"></div>
|
||||
<div
|
||||
class="mb-3 flex min-h-27 flex-wrap items-center justify-between gap-2 sm:min-h-16.5 md:min-h-27 lg:min-h-16.5 xl:min-h-7.5"
|
||||
>
|
||||
<div class="h-7 w-52 animate-pulse rounded bg-muted"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-7 w-56 animate-pulse rounded-lg bg-muted"></div>
|
||||
<div class="h-7 w-24 animate-pulse rounded-lg bg-muted"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Each meteogram is its plot plus a title row and axis labels, so
|
||||
<!-- Each meteogram is its plot plus a title row and axis labels, so
|
||||
the reserved box needs that chrome on top of the plot height or
|
||||
everything below lands ~50px per chart too high. -->
|
||||
<ChartContainer
|
||||
loading
|
||||
chartCount={enabledChartCount || 1}
|
||||
chartHeight={chartHeight + (narrowViewport ? 43 : 62)}
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
<ChartContainer
|
||||
loading
|
||||
chartCount={enabledChartCount || 1}
|
||||
chartHeight={chartHeight + (narrowViewport ? 43 : 62)}
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if selectedDayKey}
|
||||
<NearbyCities
|
||||
|
||||
Reference in New Issue
Block a user