From ae50999c1c49565016dcae9e20eb2637fb2d5891 Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Sun, 2 Aug 2026 09:03:35 +0200 Subject: [PATCH] alignment and now --- .mcp.json | 0 src/lib/charts/CanvasChart.svelte | 10 +++- src/lib/utils/now.svelte.ts | 56 +++++++++++++++++++ .../week/[location]/DailyStripSticky.svelte | 12 ++-- .../week/[location]/HourlyTable.svelte | 6 +- 5 files changed, 75 insertions(+), 9 deletions(-) delete mode 100644 .mcp.json create mode 100644 src/lib/utils/now.svelte.ts diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/charts/CanvasChart.svelte b/src/lib/charts/CanvasChart.svelte index d0904e9..ac480fe 100644 --- a/src/lib/charts/CanvasChart.svelte +++ b/src/lib/charts/CanvasChart.svelte @@ -130,6 +130,7 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { formatZoned, getZonedHour } from '$lib/utils/date'; + import { useNow } from '$lib/utils/now.svelte'; import { CHART_COLORS } from './data'; @@ -224,6 +225,10 @@ class: className = '' }: Props = $props(); + // Minute-resolution clock shared with the rest of the app; read in draw() so + // the current-time marker stays put as time passes. + const clock = useNow(); + // ─── Constants ────────────────────────────────────────────────────────────── const PAD_BOTTOM = 34; @@ -1386,9 +1391,10 @@ } } - // Current time marker + // Current time marker. Reading the shared clock (rather than Date.now()) + // makes the enclosing draw effect re-run every minute, so the line moves. if (showNow) { - const now = Date.now() / 1000; + const now = clock.current.getTime() / 1000; if (now >= viewStart && now <= viewEnd) { const x = xPix(now); ctx.strokeStyle = CHART_COLORS.currentTimeLine; diff --git a/src/lib/utils/now.svelte.ts b/src/lib/utils/now.svelte.ts new file mode 100644 index 0000000..04a5dcf --- /dev/null +++ b/src/lib/utils/now.svelte.ts @@ -0,0 +1,56 @@ +import { browser } from '$app/environment'; + +/** + * Shared "now" clock. + * + * One timer, aligned to the wall-clock minute, drives every current-time marker + * in the app (the hourly table's NOW line, the meteogram's time marker) so they + * stay accurate without a page reload. + * + * The timer only runs while at least one component is subscribed, and re-aligns + * after each tick so it can't drift or fire twice within a minute (e.g. after + * the tab has been suspended). + */ + +let current = $state(new Date()); +let subscribers = 0; +let timer: ReturnType | undefined; + +const MINUTE_MS = 60_000; + +function scheduleTick() { + timer = setTimeout( + () => { + current = new Date(); + scheduleTick(); + }, + MINUTE_MS - (Date.now() % MINUTE_MS) + ); +} + +/** + * Reactive current time, refreshed on every minute boundary while the calling + * component is mounted. On the server it just reports render time. + */ +export function useNow(): { readonly current: Date } { + if (browser) { + $effect(() => { + if (subscribers++ === 0) { + current = new Date(); + scheduleTick(); + } + return () => { + if (--subscribers === 0) { + clearTimeout(timer); + timer = undefined; + } + }; + }); + } + + return { + get current() { + return browser ? current : new Date(); + } + }; +} diff --git a/src/routes/weather/week/[location]/DailyStripSticky.svelte b/src/routes/weather/week/[location]/DailyStripSticky.svelte index 7f64bba..2b75ea9 100644 --- a/src/routes/weather/week/[location]/DailyStripSticky.svelte +++ b/src/routes/weather/week/[location]/DailyStripSticky.svelte @@ -489,8 +489,8 @@ /* full state only: gap above the icons, and the day / night icon offsets that tighten the pair around the cell centre */ --icon-gap-full: 2px; - --day-icon-nudge: 2px; - --night-icon-nudge: 4px; + --day-icon-nudge: 3px; + --night-icon-nudge: 0px; /* compact only: signed shift that lands the lone icon dead centre in the square tile (measured against the tile's mid-line) */ --icon-lift: 1px; @@ -534,7 +534,7 @@ --tmax-nudge: 0px; --tmin-nudge: 8px; --icon-gap-full: 6px; - --day-icon-nudge: 3px; + --day-icon-nudge: 4px; --night-icon-nudge: 6px; --pb-full: 10px; --pb-min: 3px; @@ -769,11 +769,11 @@ and melts away completely when compact so the day icon re-centers. */ .night-icon { display: block; - width: calc(var(--icon) * 0.42 * var(--rel)); - height: calc(var(--icon) * 0.42 * var(--rel)); + width: calc(var(--icon) * 0.55 * var(--rel)); + height: calc(var(--icon) * 0.55 * var(--rel)); opacity: var(--rel); margin-left: calc(-4px * var(--rel)); - margin-bottom: calc(6px * var(--rel)); + margin-bottom: calc(-4px * var(--rel)); transform: translateX(calc(-1 * var(--night-icon-nudge) * var(--k))); } /* weekday centered when full, pushed to the edges when compact */ diff --git a/src/routes/weather/week/[location]/HourlyTable.svelte b/src/routes/weather/week/[location]/HourlyTable.svelte index 997ec09..3e4c9e5 100644 --- a/src/routes/weather/week/[location]/HourlyTable.svelte +++ b/src/routes/weather/week/[location]/HourlyTable.svelte @@ -10,6 +10,7 @@ } from '$lib/stores/settings'; import { formatUtcOffset, formatZoned, getZonedHour, isSameDayInZone } from '$lib/utils/date'; + import { useNow } from '$lib/utils/now.svelte'; import { groupHover, setGroupHover } from '$lib/charts'; import * as m from '$lib/paraglide/messages'; @@ -74,7 +75,10 @@ $storedVariablePrefs.table?.[key] ?? defaultVariablePrefs.table[key] ?? true ); - const today = new Date(); + // Ticks every minute, so the NOW line/label and the highlighted current-hour + // column keep up with the clock instead of freezing at page load. + const clock = useNow(); + let today = $derived(clock.current); const tempUnit = $derived(getTempUnit(units)); const windUnit = $derived(getWindUnit(units)); const precipUnit = $derived(getPrecipUnit(units));