From 1d7d9d3a53de69a9ecab17b9d73aa4795bc7cfd1 Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Sat, 25 Jul 2026 14:14:49 +0200 Subject: [PATCH] scroll behaviour --- src/lib/components/ui/button/button.svelte | 2 +- .../weather/week/[location]/DailyCards.svelte | 8 +- .../week/[location]/HourlyTable.svelte | 76 ++++++++++++++----- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index a7bd70b..e009250 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -6,7 +6,7 @@ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'; export const buttonVariants = tv({ - base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs', diff --git a/src/routes/weather/week/[location]/DailyCards.svelte b/src/routes/weather/week/[location]/DailyCards.svelte index ada330f..2aab042 100644 --- a/src/routes/weather/week/[location]/DailyCards.svelte +++ b/src/routes/weather/week/[location]/DailyCards.svelte @@ -57,8 +57,10 @@ // defer to after layout so the measured positions and scroll width are final requestAnimationFrame(() => { // scroll so the first day card sits exactly at the content edge (aligned - // with the page hero), leaving the "past days" button fully off to the left - el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left; + // with the page hero), leaving the "past days" button fully off to the + // left. The scroll's pl-3 keeps the lifted/scaled card from being clipped, + // so subtract it here to land the card on the content edge. + el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left - 12; }); }); @@ -129,7 +131,7 @@ clipped, while the first card still lines up with the page content edge. -->
diff --git a/src/routes/weather/week/[location]/HourlyTable.svelte b/src/routes/weather/week/[location]/HourlyTable.svelte index a16637b..1fdbdd5 100644 --- a/src/routes/weather/week/[location]/HourlyTable.svelte +++ b/src/routes/weather/week/[location]/HourlyTable.svelte @@ -281,32 +281,66 @@ : null ); - // ─── Start the scroll at 06:00 on day change (mobile) ─────────────────────── - // When the table overflows sideways, scroll so the 06:00 column is the leftmost - // visible one — the early hours are rarely of interest and this keeps the - // daytime in view on every day switch. + // ─── Auto-scroll the (overflowing) table on day / interval change ──────────── + // • Day change → today lands on the current-hour cell, other days on 06:00. + // • Interval change (3h ↔ 1h) → keep whatever cell is currently on the left. let tableScrollEl = $state(); let autoScrolledDay = -1; + let lastInterval = 0; + let viewLeftHour = 6; // zoned hour at the left edge (plain, non-reactive) + + function colWidthPx(): number { + return (tableWidth - headerColWidth) / cellData.length; + } + + function scrollToIdx(idx: number) { + const el = tableScrollEl; + if (!el) return; + if (el.scrollWidth <= el.clientWidth + 4) { + el.scrollLeft = 0; + return; + } + el.scrollLeft = idx <= 0 ? 0 : Math.max(0, idx * colWidthPx()); + } + + // track the left-most visible cell's hour so an interval switch can restore it + function onTableScroll() { + const el = tableScrollEl; + if (!el || cellData.length === 0 || tableWidth === 0) return; + const idx = Math.min( + cellData.length - 1, + Math.max(0, Math.round(el.scrollLeft / colWidthPx())) + ); + viewLeftHour = getZonedHour(cellData[idx].date, data.timezone); + } + $effect(() => { - const day = selectedDay.getTime(); // re-run on day switch + const day = selectedDay.getTime(); + const interval = is3h ? 3 : 1; const el = tableScrollEl; if (!el || cellData.length === 0 || tableWidth === 0 || headerColWidth === 0) return; - if (autoScrolledDay === day) return; + const dayChanged = autoScrolledDay !== day; + const intervalChanged = lastInterval !== interval; + if (!dayChanged && !intervalChanged) return; autoScrolledDay = day; - requestAnimationFrame(() => { - // only when it actually overflows (i.e. mobile / narrow) - if (el.scrollWidth <= el.clientWidth + 4) { - el.scrollLeft = 0; - return; - } - const sixIdx = cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6); - if (sixIdx <= 0) { - el.scrollLeft = 0; - return; - } - const colWidth = (tableWidth - headerColWidth) / cellData.length; - el.scrollLeft = Math.max(0, sixIdx * colWidth); - }); + lastInterval = interval; + + let targetIdx: number; + if (dayChanged) { + // today → the cell whose block contains "now"; otherwise → 06:00 + const nowMs = today.getTime(); + const stepMs = interval * 3600 * 1000; + const nowIdx = cellData.findIndex( + (c) => nowMs >= c.date.getTime() && nowMs < c.date.getTime() + stepMs + ); + targetIdx = + nowIdx >= 0 ? nowIdx : cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6); + viewLeftHour = targetIdx >= 0 ? getZonedHour(cellData[targetIdx].date, data.timezone) : 6; + } else { + // interval change only: keep the same cell on the left + targetIdx = cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= viewLeftHour); + } + requestAnimationFrame(() => scrollToIdx(targetIdx)); }); // ─── Chart-hover mirror ───────────────────────────────────────────────────── @@ -415,7 +449,7 @@ -
+