finish animations and add elevation

This commit is contained in:
Vincent van der Wal
2026-08-06 22:19:17 +02:00
parent d0f94094ca
commit 83b0e46e64
21 changed files with 328 additions and 112 deletions
@@ -346,12 +346,12 @@
<svelte:head>
<title>Drizz.li | {m.page_week_title()}</title>
<link rel="canonical" href="https://drizz.li/weather/week" />
<meta name="description" content="7-day weather forecast with detailed hourly data" />
<meta name="description" content="Weekly weather forecast with detailed hourly data" />
</svelte:head>
<!-- the model picker rides in the layout's location row (see weather/+layout) -->
{#snippet heroActions()}
<div class="lg:absolute lg:right-0 lg:top-0 flex w-full min-w-0 items-center gap-3 sm:w-auto">
<div class="flex w-full min-w-0 items-center gap-3 sm:w-auto">
<ModelSelector
selectedModel={params.models?.[0] ?? 'best_match'}
onModelChange={(model) => {
@@ -322,6 +322,10 @@
// positioned relative to the data columns only.
let headerColWidth = $state(0);
let tableWidth = $state(0);
// Measured so the centred NOW badge can be clamped fully inside the row:
// centred on a time near midnight it would poke past the last column and
// hand the scroller a sliver of phantom overflow.
let nowBadgeWidth = $state(0);
let nowLeftPx = $derived(
nowPercent != null && tableWidth > 0
? headerColWidth + (nowPercent / 100) * (tableWidth - headerColWidth)
@@ -608,14 +612,17 @@
{#if nowPercent != null}
<div
class="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-red-500/75"
style="left:{nowPercent}%"
style="left:clamp(1px, {nowPercent}%, calc(100% - 1px))"
></div>
{/if}
<!-- "Now" label, aligned with the sunrise/sunset labels along the bottom -->
<!-- "Now" label, aligned with the sunrise/sunset labels along the
bottom; its centre is clamped so the pill never leaves the row -->
{#if isTodaySelected && nowPercent != null}
<span
bind:clientWidth={nowBadgeWidth}
class="absolute bottom-0.5 z-15 -translate-x-1/2 rounded-full bg-red-500 px-1.5 py-px text-[9px] font-bold tracking-wide whitespace-nowrap text-white uppercase shadow-sm"
style="left:{nowPercent}%"
style="left:clamp({nowBadgeWidth /
2}px, {nowPercent}%, calc(100% - {nowBadgeWidth / 2}px))"
>
{m.table_now()}
</span>
@@ -904,24 +911,31 @@
</tbody>
</table>
<!-- Hovered-column highlight, mirroring the meteogram crosshair -->
<!-- Hovered-column highlight, mirroring the meteogram crosshair.
max-width caps the box at the wrapper's true (fractional) right
edge: the widths here derive from rounded clientWidth bindings, so
on the last column left+width can land a fraction of a pixel past
the edge - enough scrollable overflow for a phantom scrollbar. -->
{#if hoveredCol >= 0 && tableWidth > 0}
{@const colWidth = (tableWidth - headerColWidth) / cellData.length}
{@const colLeft = headerColWidth + hoveredCol * colWidth}
<div
class="pointer-events-none absolute inset-y-0 z-10 border-x border-primary/40 bg-primary/10"
style="left:{headerColWidth + hoveredCol * colWidth}px;width:{colWidth}px"
style="left:{colLeft}px;width:{colWidth}px;max-width:calc(100% - {colLeft}px)"
></div>
{/if}
<!-- "Now" column highlight. The current-time line itself is painted per
cell (.now-cell) so it stays under the values and icons. -->
cell (.now-cell) so it stays under the values and icons. Same
max-width cap as the hover highlight above. -->
{#if nowLeftPx != null}
{@const colWidth = (tableWidth - headerColWidth) / cellData.length}
{@const nowIdx = cellData.findIndex((c) => c.isNow)}
{#if nowIdx >= 0}
{@const colLeft = headerColWidth + nowIdx * colWidth}
<div
class="pointer-events-none absolute inset-y-0 z-10 border-x border-red-500/30 bg-red-500/5"
style="left:{headerColWidth + nowIdx * colWidth}px;width:{colWidth}px"
style="left:{colLeft}px;width:{colWidth}px;max-width:calc(100% - {colLeft}px)"
></div>
{/if}
{/if}
@@ -64,9 +64,13 @@
if (val) onModelChange(val);
}}
>
<!-- Fixed width from sm up (mobile stays full-width): the trigger used to hug
its content, so its size changed with every model name and differed
between pages. One constant footprint, sized for the longest label in
the catalogue; anything longer truncates. -->
<Select.Trigger
aria-label={m.model_selector_aria({ label })}
class="group h-auto min-h-12 min-w-0 flex-1 cursor-pointer gap-2.5 rounded-xl border-2 border-primary/35 bg-card py-1.5 ps-2.5 shadow-sm transition-colors hover:border-primary/70 hover:shadow-md data-[size=default]:h-auto data-[state=open]:border-primary sm:min-h-14 sm:gap-3 sm:py-2 sm:min-w-72 sm:flex-none"
class="group h-auto min-h-12 min-w-0 flex-1 cursor-pointer gap-2.5 rounded-xl border-2 border-primary/35 bg-card py-1.5 ps-2.5 shadow-sm transition-colors hover:border-primary/70 hover:shadow-md data-[size=default]:h-auto data-[state=open]:border-primary sm:min-h-14 sm:w-80 sm:gap-3 sm:py-2 sm:flex-none"
>
<div
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/12 text-primary sm:size-9"