now improvements

This commit is contained in:
Vincent van der Wal
2026-08-02 09:53:39 +02:00
parent ae50999c1c
commit bde815c35a
2 changed files with 101 additions and 8 deletions
@@ -262,7 +262,7 @@
</span> </span>
<span <span
class="rel-label overflow-hidden leading-[1.25] whitespace-nowrap {isToday class="rel-label leading-[1.25] whitespace-nowrap {isToday
? 'font-semibold text-primary/80' ? 'font-semibold text-primary/80'
: 'text-muted-foreground'}" : 'text-muted-foreground'}"
> >
@@ -328,6 +328,25 @@
: null : null
); );
const NOW_LINE_W = 2;
// The line is painted as a background on the one cell it crosses (see the
// .now-cell rule) rather than as an overlay on top of the table, so it sits
// above each cell's colour tint but below its value and icon.
let nowCell = $derived.by((): { idx: number; offset: number } | null => {
if (nowLeftPx == null || cellData.length === 0) return null;
const colWidth = (tableWidth - headerColWidth) / cellData.length;
if (colWidth <= NOW_LINE_W) return null;
const col = Math.floor((nowLeftPx - headerColWidth) / colWidth);
if (col < 0 || col >= cellData.length) return null;
// keep the full line width inside the cell, which clips its overflow
const offset = nowLeftPx - headerColWidth - col * colWidth - NOW_LINE_W / 2;
return {
idx: cellData[col].idx,
offset: Math.max(0, Math.min(colWidth - NOW_LINE_W, offset))
};
});
// ─── Auto-scroll the (overflowing) table on day / interval change ──────────── // ─── Auto-scroll the (overflowing) table on day / interval change ────────────
// • Day change → today lands on the current-hour cell, other days on 06:00. // • 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. // • Interval change (3h ↔ 1h) → keep whatever cell is currently on the left.
@@ -501,6 +520,7 @@
class="relative {is3h class="relative {is3h
? 'min-w-[400px] md:min-w-[560px]' ? 'min-w-[400px] md:min-w-[560px]'
: 'min-w-[780px] md:min-w-[1100px]'}" : 'min-w-[780px] md:min-w-[1100px]'}"
style="--now-x:{nowCell?.offset ?? 0}px"
bind:clientWidth={tableWidth} bind:clientWidth={tableWidth}
> >
<table class="w-full table-fixed border-collapse whitespace-nowrap"> <table class="w-full table-fixed border-collapse whitespace-nowrap">
@@ -582,6 +602,14 @@
</span> </span>
</div> </div>
{/if} {/if}
<!-- Current-time line across the time row. Placed before the hour
labels so it paints under them, like it does in the rows below. -->
{#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}%"
></div>
{/if}
<!-- "Now" label, aligned with the sunrise/sunset labels along the bottom --> <!-- "Now" label, aligned with the sunrise/sunset labels along the bottom -->
{#if isTodaySelected && nowPercent != null} {#if isTodaySelected && nowPercent != null}
<span <span
@@ -597,6 +625,7 @@
{@const widthPct = 100 / cellData.length} {@const widthPct = 100 / cellData.length}
<span <span
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-label={cell.idx === nowCell?.idx}
class="absolute top-0 flex items-start pl-1 font-bold {is3h class="absolute top-0 flex items-start pl-1 font-bold {is3h
? 'pt-2 text-sm' ? 'pt-2 text-sm'
: 'pt-2.5'} : 'pt-2.5'}
@@ -638,6 +667,7 @@
{@const wCode = hourly.weather_code[cell.idx]} {@const wCode = hourly.weather_code[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 leading-0" class="cell h-12 leading-0"
class:cell-empty={!finite(wCode)} class:cell-empty={!finite(wCode)}
class:icon-day={cell.isDaytime} class:icon-day={cell.isDaytime}
@@ -662,8 +692,11 @@
{@const style = getTempStyle(temp, String(units.temperature_unit))} {@const style = getTempStyle(temp, String(units.temperature_unit))}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 font-bold {is3h ? 'text-lg' : 'text-[15px]'}" class="cell h-12 font-bold {is3h ? 'text-lg' : 'text-[15px]'}"
style={finite(temp) ? `background-color:${style.bg};color:${style.fg}` : ''} style={finite(temp)
? `background-color:${style.bg};color:${style.fg};--now-halo:${style.bg}`
: ''}
> >
{formatTemp(temp)} {formatTemp(temp)}
</td> </td>
@@ -677,6 +710,7 @@
{@const temp = hourly.apparent_temperature[cell.idx]} {@const temp = hourly.apparent_temperature[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 text-muted-foreground {is3h ? 'text-[13px]' : 'text-[11px]'}" class="cell h-12 text-muted-foreground {is3h ? 'text-[13px]' : 'text-[11px]'}"
> >
{formatTemp(temp)} {formatTemp(temp)}
@@ -691,6 +725,7 @@
{@const temp = hourly.dew_point_2m?.[cell.idx]} {@const temp = hourly.dew_point_2m?.[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 text-muted-foreground {is3h ? 'text-[13px]' : 'text-[11px]'}" class="cell h-12 text-muted-foreground {is3h ? 'text-[13px]' : 'text-[11px]'}"
> >
{formatTemp(temp)} {formatTemp(temp)}
@@ -706,6 +741,7 @@
{@const windDir = hourly.winddirection_10m[cell.idx]} {@const windDir = hourly.winddirection_10m[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="relative cell h-12 align-middle leading-none" class="relative cell h-12 align-middle leading-none"
> >
{#if windDir != null && !isNaN(windDir)} {#if windDir != null && !isNaN(windDir)}
@@ -730,6 +766,7 @@
{@const v = hourly.wind_gusts_10m?.[cell.idx]} {@const v = hourly.wind_gusts_10m?.[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 font-semibold text-foreground/80 {is3h class="cell h-12 font-semibold text-foreground/80 {is3h
? 'text-sm' ? 'text-sm'
: 'text-xs'}" : 'text-xs'}"
@@ -746,6 +783,7 @@
{@const hum = hourly.relative_humidity_2m[cell.idx]} {@const hum = hourly.relative_humidity_2m[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12" class="cell h-12"
style={finite(hum) ? `background:${getHumidityBg(hum)}` : ''} style={finite(hum) ? `background:${getHumidityBg(hum)}` : ''}
> >
@@ -761,6 +799,7 @@
{@const cloud = hourly.cloud_cover[cell.idx]} {@const cloud = hourly.cloud_cover[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12" class="cell h-12"
style={finite(cloud) style={finite(cloud)
? `background:rgba(140,160,180,${getCloudOpacity(cloud)})` ? `background:rgba(140,160,180,${getCloudOpacity(cloud)})`
@@ -778,6 +817,7 @@
{@const v = hourly.pressure_msl?.[cell.idx]} {@const v = hourly.pressure_msl?.[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}" class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}"
style="background:{getPressureBg(v ?? null)}" style="background:{getPressureBg(v ?? null)}"
> >
@@ -793,6 +833,7 @@
{@const v = hourly.uv_index?.[cell.idx]} {@const v = hourly.uv_index?.[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 font-semibold {is3h ? 'text-sm' : 'text-xs'}" class="cell h-12 font-semibold {is3h ? 'text-sm' : 'text-xs'}"
style="background:{getUvBg(v ?? null)}" style="background:{getUvBg(v ?? null)}"
> >
@@ -808,6 +849,7 @@
{@const v = hourly.visibility?.[cell.idx]} {@const v = hourly.visibility?.[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}" class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}"
> >
{v != null && !isNaN(v) ? (v / 1000).toFixed(0) : '-'} {v != null && !isNaN(v) ? (v / 1000).toFixed(0) : '-'}
@@ -823,6 +865,7 @@
{@const prob = hourly.precipitation_probability[cell.idx]} {@const prob = hourly.precipitation_probability[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell precip-cell h-12" class="cell precip-cell h-12"
style="background:{getPrecipProbBg(prob ?? 0)}" style="background:{getPrecipProbBg(prob ?? 0)}"
title={formatPrecipTooltip(precip, prob)} title={formatPrecipTooltip(precip, prob)}
@@ -844,6 +887,7 @@
{@const v = hourly.snowfall?.[cell.idx]} {@const v = hourly.snowfall?.[cell.idx]}
<td <td
class:col-empty={!cell.hasData} class:col-empty={!cell.hasData}
class:now-cell={cell.idx === nowCell?.idx}
class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}" class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}"
> >
{v != null && !isNaN(v) && v > 0 ? v.toFixed(1) : '-'} {v != null && !isNaN(v) && v > 0 ? v.toFixed(1) : '-'}
@@ -864,7 +908,8 @@
></div> ></div>
{/if} {/if}
<!-- "Now" column highlight + exact-time line --> <!-- "Now" column highlight. The current-time line itself is painted per
cell (.now-cell) so it stays under the values and icons. -->
{#if nowLeftPx != null} {#if nowLeftPx != null}
{@const colWidth = (tableWidth - headerColWidth) / cellData.length} {@const colWidth = (tableWidth - headerColWidth) / cellData.length}
{@const nowIdx = cellData.findIndex((c) => c.isNow)} {@const nowIdx = cellData.findIndex((c) => c.isNow)}
@@ -874,11 +919,6 @@
style="left:{headerColWidth + nowIdx * colWidth}px;width:{colWidth}px" style="left:{headerColWidth + nowIdx * colWidth}px;width:{colWidth}px"
></div> ></div>
{/if} {/if}
<!-- full-height current-time line (its "Now" label lives in the time row) -->
<div
class="pointer-events-none absolute inset-y-0 z-10 w-0.5 -translate-x-1/2 bg-red-500/75"
style="left:{nowLeftPx}px"
></div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -933,6 +973,59 @@
background: none !important; background: none !important;
} }
/* ── Current-time line ──────────────────────────────────────────
Values sit on top of the line, so they get a soft halo in the colour of
whatever is behind them: the card by default, or the cell's own colour on
rows that paint one (set inline as --now-halo). Just enough to keep the
line from cutting through a glyph. */
.now-cell,
.now-label {
--now-halo: var(--color-card);
text-shadow:
0 0 2px var(--now-halo),
0 0 4px var(--now-halo);
}
/* Same idea for the weather and wind-arrow icons, which text-shadow can't
reach. */
.now-cell svg {
filter: drop-shadow(0 0 2px var(--now-halo)) drop-shadow(0 0 3px var(--now-halo));
}
/* The line itself: a background layer on the single cell it crosses (--now-x
is its offset within that cell). A background paints above the cell's own
colour tint but below its text and icons, which an overlay element on top
of the table can't do. !important because several rows set their tint with
the `background` shorthand inline, which would otherwise reset this image. */
.now-cell {
--now-line-color: color-mix(in oklab, var(--color-red-500) 75%, transparent);
--now-line: linear-gradient(
to right,
transparent var(--now-x),
var(--now-line-color) var(--now-x),
var(--now-line-color) calc(var(--now-x) + 2px),
transparent calc(var(--now-x) + 2px)
);
background-image: var(--now-line) !important;
background-repeat: no-repeat !important;
}
/* The row separator is painted over cell backgrounds, so it would chop the
line into per-row segments. On these cells it is switched off (a cell
border wins the collapse against the row's, same width and style) and
redrawn as a background layer below the line instead. */
.row + .row .now-cell {
border-top: 1px solid transparent;
background-image:
var(--now-line),
linear-gradient(
to bottom,
color-mix(in oklab, var(--color-border) 70%, transparent) 1px,
transparent 1px
) !important;
background-repeat: no-repeat, no-repeat !important;
}
/* A whole column the model never reached - dimmed end to end, hour label /* A whole column the model never reached - dimmed end to end, hour label
included, so the point where the forecast runs out is obvious. */ included, so the point where the forecast runs out is obvious. */
.col-empty { .col-empty {