single day overview

This commit is contained in:
terraputix
2026-02-15 20:57:45 +01:00
parent 5180c121c4
commit 1c4979e12e
+325 -305
View File
@@ -31,8 +31,8 @@
const CHART_GROUP = 'week-meteogram';
const MS_PER_DAY = 24 * 3600 * 1000;
const CELL_W = 38;
const CURVE_H = 120;
const CURVE_H = 140;
const SVG_UNIT = 100;
let params = $state({
latitude: [$storedLocation.latitude],
@@ -57,7 +57,6 @@
const selectedDay = new SvelteDate();
let selectedDayIndex = $state(1);
let tableScrollDiv: HTMLElement | undefined = $state();
let hourlyInterval = $state<1 | 3>(3);
let showDetailedCharts = $state(false);
@@ -104,22 +103,10 @@
}
}
function scrollTableToDay(day: Date): void {
if (!tableScrollDiv) return;
const cells = tableScrollDiv.querySelectorAll<HTMLElement>('td.hour-cell[data-date]');
for (const cell of cells) {
if (Number(cell.dataset['date']) === day.getDate()) {
tableScrollDiv.scrollTo({ left: cell.offsetLeft - 120, behavior: 'smooth' });
break;
}
}
}
const switchDay = (date: Date, index: number) => {
selectedDay.setTime(date.getTime());
selectedDayIndex = index;
scrollChartsToDay(date);
requestAnimationFrame(() => scrollTableToDay(date));
};
function resetZoom(): void {
@@ -172,7 +159,6 @@
echarts.connect(CHART_GROUP);
requestAnimationFrame(() => {
scrollChartsToDay(selectedDay);
scrollTableToDay(selectedDay);
});
}
}
@@ -247,14 +233,20 @@
return `${date.getMonth() + 1}-${date.getDate()}`;
}
function getFilteredIndices(dates: Date[]): number[] {
if (hourlyInterval === 1) {
return dates.map((_, i) => i);
function getDayIndices(dates: Date[], day: Date): number[] {
const dayDate = day.getDate();
const dayMonth = day.getMonth();
const dayYear = day.getFullYear();
const indices: number[] = [];
for (let i = 0; i < dates.length; i++) {
const d = dates[i];
if (d.getDate() === dayDate && d.getMonth() === dayMonth && d.getFullYear() === dayYear) {
if (hourlyInterval === 1 || d.getHours() % 3 === 0) {
indices.push(i);
}
}
}
return dates.reduce<number[]>((acc, date, i) => {
if (date.getHours() % 3 === 0) acc.push(i);
return acc;
}, []);
return indices;
}
function getPrecipBarHeight(val: number, maxVal: number): number {
@@ -266,9 +258,9 @@
if (indices.length < 2) return '';
const rangeT = maxT - minT || 1;
const points: [number, number][] = indices.map((idx, i) => {
const x = i * CELL_W + CELL_W / 2;
const x = i * SVG_UNIT + SVG_UNIT / 2;
const t = temps[idx] ?? minT;
const y = CURVE_H - 20 - ((t - minT) / rangeT) * (CURVE_H - 40);
const y = CURVE_H - 24 - ((t - minT) / rangeT) * (CURVE_H - 48);
return [x, y];
});
@@ -286,8 +278,9 @@
function buildAreaPath(temps: number[], indices: number[], minT: number, maxT: number): string {
const curvePath = buildCurvePath(temps, indices, minT, maxT);
if (!curvePath) return '';
const lastX = (indices.length - 1) * CELL_W + CELL_W / 2;
const firstX = CELL_W / 2;
const n = indices.length;
const lastX = (n - 1) * SVG_UNIT + SVG_UNIT / 2;
const firstX = SVG_UNIT / 2;
return `${curvePath} L ${lastX},${CURVE_H} L ${firstX},${CURVE_H} Z`;
}
@@ -295,6 +288,10 @@
return Math.min(0.7, (cover ?? 0) / 120);
}
function getIconSize(interval: 1 | 3): number {
return interval === 3 ? 40 : 26;
}
$effect(() => {
const loc = location;
const modelList = params.models;
@@ -870,15 +867,19 @@
{#if fetchedHourly}
{@const hourly = fetchedHourly.hourly}
{@const dates = fetchedHourly.hourlyDates}
{@const filteredIdx = getFilteredIndices(dates)}
{@const dayIdx = getDayIndices(dates, selectedDay)}
{@const numCols = dayIdx.length}
{@const svgW = numCols * SVG_UNIT}
{@const maxPrecip = Math.max(
...hourly.precipitation.filter((v) => v != null && !isNaN(v)),
...dayIdx.map((i) => hourly.precipitation[i]).filter((v) => v != null && !isNaN(v)),
0.1
)}
{@const allTemps = hourly.temperature_2m.filter((t) => t != null && !isNaN(t))}
{@const minTemp = Math.min(...allTemps)}
{@const maxTemp = Math.max(...allTemps)}
{@const totalWidth = filteredIdx.length * CELL_W}
{@const dayTemps = dayIdx
.map((i) => hourly.temperature_2m[i])
.filter((t) => t != null && !isNaN(t))}
{@const minTemp = dayTemps.length ? Math.min(...dayTemps) : 0}
{@const maxTemp = dayTemps.length ? Math.max(...dayTemps) : 10}
{@const iconPx = getIconSize(hourlyInterval)}
<div class="hourly-header">
<h3 class="hourly-title">
@@ -897,86 +898,44 @@
</div>
</div>
<!-- Combined Hourly Meteogram + Table -->
<div bind:this={tableScrollDiv} class="hourly-scroll">
<div class="hourly-inner" style="min-width: {totalWidth + 96}px">
<!-- Temperature Curve + Icons Area -->
<div class="curve-area">
<div class="curve-row-header"></div>
<div class="curve-container" style="width: {totalWidth}px; height: {CURVE_H}px">
<!-- Cloud cover background -->
<svg
class="curve-svg"
width={totalWidth}
height={CURVE_H}
viewBox="0 0 {totalWidth} {CURVE_H}"
>
<defs>
<linearGradient id="tempGrad" x1="0" y1="0" x2="1" y2="0">
{#each filteredIdx as idx, i (idx)}
{@const t = hourly.temperature_2m[idx]}
<stop
offset="{(i / Math.max(filteredIdx.length - 1, 1)) * 100}%"
stop-color={getColor(t, String(params.temperature_unit))}
stop-opacity="0.45"
/>
{/each}
</linearGradient>
</defs>
<!-- Cloud cover bands per cell -->
{#each filteredIdx as idx, i (idx)}
{@const cloud = hourly.cloud_cover[idx]}
<rect
x={i * CELL_W}
y={0}
width={CELL_W}
height={CURVE_H}
fill="rgb(160,175,190)"
opacity={getCloudOpacity(cloud)}
/>
{#if numCols > 0}
<!-- Single-day hourly table (no horizontal scroll) -->
<div class="hourly-container">
<table class="hourly-table {hourlyInterval === 3 ? 'interval-3h' : 'interval-1h'}">
<caption class="sr-only">Hourly weather details for {location.name}</caption>
<colgroup>
<col class="col-header" />
{#each dayIdx as _ (_.toString())}
<col />
{/each}
</colgroup>
<tbody>
<!-- Time row -->
<tr>
<th class="row-header" scope="row"></th>
{#each dayIdx as idx (idx)}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
<td class="hour-cell time-cell {isNow ? 'now' : ''}" data-date={date.getDate()}>
{pad(date.getHours())}<sup class="time-sup">00</sup>
</td>
{/each}
</tr>
<!-- Temperature area fill -->
<path
d={buildAreaPath(hourly.temperature_2m, filteredIdx, minTemp, maxTemp)}
fill="url(#tempGrad)"
/>
<!-- Temperature curve line -->
<path
d={buildCurvePath(hourly.temperature_2m, filteredIdx, minTemp, maxTemp)}
fill="none"
stroke="#d84315"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Temperature value labels -->
{#each filteredIdx as idx, i (idx)}
{@const t = hourly.temperature_2m[idx]}
{@const rangeT = maxTemp - minTemp || 1}
{@const y = CURVE_H - 20 - ((t - minTemp) / rangeT) * (CURVE_H - 40)}
<text
x={i * CELL_W + CELL_W / 2}
y={y - 8}
text-anchor="middle"
class="curve-temp-label"
fill="currentColor"
>
{t?.toFixed(0) ?? '-'}°
</text>
{/each}
</svg>
<!-- Weather icons overlaid on top -->
<div class="curve-icons" style="width: {totalWidth}px">
{#each filteredIdx as idx, i (idx)}
<!-- Weather icons row (prominent) -->
<tr class="icon-row">
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-day-cloudy.svg#Layer_1"></use>
</svg>
</th>
{#each dayIdx as idx (idx)}
{@const wCode = hourly.weather_code[idx]}
{@const daytime = isDaytimeHour(dates[idx].getHours())}
<div class="curve-icon-cell" style="width: {CELL_W}px; left: {i * CELL_W}px">
<svg class="fill-foreground" width="20px" height="20px">
{@const date = dates[idx]}
{@const daytime = isDaytimeHour(date.getHours())}
{@const isNow = isCurrentHour(date)}
<td class="hour-cell weather-icon-cell {isNow ? 'now' : ''}">
<svg class="fill-foreground" width="{iconPx}px" height="{iconPx}px">
<use
xlink:href="/images/weather-icons/wi-{daytime
? 'day'
@@ -984,36 +943,89 @@
'clear'}.svg#Layer_1"
></use>
</svg>
</div>
{/each}
</div>
</div>
</div>
<!-- Hourly Data Table -->
<table class="hourly-table">
<caption class="sr-only">Hourly weather details for {location.name}</caption>
<tbody>
<!-- Time row -->
<tr>
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<use xlink:href="/images/weather-icons/wi-time-4.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell time-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
data-date={date.getDate()}
>
{pad(date.getHours())}<sup class="time-sup">00</sup>
</td>
{/each}
</tr>
<!-- Temperature curve row (colspan) -->
<tr class="curve-row">
<th class="row-header curve-header" scope="row">
<span class="row-label"
>{params.temperature_unit === 'celsius' ? '°C' : '°F'}</span
>
</th>
<td colspan={numCols} class="curve-cell">
<div class="curve-container" style="height: {CURVE_H}px">
<svg
class="curve-svg"
width="100%"
height={CURVE_H}
viewBox="0 0 {svgW} {CURVE_H}"
preserveAspectRatio="none"
>
<defs>
<linearGradient id="tempGrad" x1="0" y1="0" x2="1" y2="0">
{#each dayIdx as idx, i (idx)}
{@const t = hourly.temperature_2m[idx]}
<stop
offset="{(i / Math.max(numCols - 1, 1)) * 100}%"
stop-color={getColor(t, String(params.temperature_unit))}
stop-opacity="0.45"
/>
{/each}
</linearGradient>
</defs>
<!-- Cloud cover bands -->
{#each dayIdx as idx, i (idx)}
{@const cloud = hourly.cloud_cover[idx]}
<rect
x={i * SVG_UNIT}
y={0}
width={SVG_UNIT}
height={CURVE_H}
fill="rgb(160,175,190)"
opacity={getCloudOpacity(cloud)}
/>
{/each}
<!-- Temperature area fill -->
<path
d={buildAreaPath(hourly.temperature_2m, dayIdx, minTemp, maxTemp)}
fill="url(#tempGrad)"
/>
<!-- Temperature curve line -->
<path
d={buildCurvePath(hourly.temperature_2m, dayIdx, minTemp, maxTemp)}
fill="none"
stroke="#d84315"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
/>
</svg>
<!-- Temperature labels (positioned with % so they align with columns) -->
<div class="curve-labels">
{#each dayIdx as idx, i (idx)}
{@const t = hourly.temperature_2m[idx]}
{@const rangeT = maxTemp - minTemp || 1}
{@const yPct =
((1 - ((t ?? minTemp) - minTemp) / rangeT) * 0.7 + 0.05) * 100}
<span
class="curve-label"
style="left: {((i + 0.5) / numCols) * 100}%; top: calc({yPct}% - 18px)"
>
{t?.toFixed(0) ?? '-'}°
</span>
{/each}
</div>
</div>
</td>
</tr>
<!-- Precipitation bars row -->
<tr>
<th class="row-header" scope="row">
@@ -1022,16 +1034,11 @@
<span class="row-unit">{params.precipitation_unit === 'mm' ? 'mm' : 'in'}</span>
</div>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const val = hourly.precipitation[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell precip-cell {isNow ? 'now' : ''} {isMidnight
? 'midnight'
: ''}"
>
<td class="hour-cell precip-cell {isNow ? 'now' : ''}">
<div class="precip-bar-container">
{#if val > 0}
<div
@@ -1050,13 +1057,12 @@
<th class="row-header" scope="row">
<span class="row-unit">%</span>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const prob = hourly.precipitation_probability[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
class="hour-cell {isNow ? 'now' : ''}"
style="background: {getPrecipProbBg(prob ?? 0)}; color: {getPrecipProbColor(
prob ?? 0
)}"
@@ -1069,23 +1075,20 @@
<!-- Precipitation type icons -->
<tr>
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const val = hourly.precipitation[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell icon-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
>
<td class="hour-cell icon-cell {isNow ? 'now' : ''}">
{#if val > 0}
<svg
class="fill-foreground"
width="16px"
height="16px"
width="18px"
height="18px"
style="opacity: {Math.min(1, val / 3)}"
>
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
@@ -1095,25 +1098,21 @@
{/each}
</tr>
<!-- Wind Speed + Direction -->
<!-- Wind Direction Arrows -->
<tr>
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{@const _wind = hourly.windspeed_10m[idx]}
{#each dayIdx as idx (idx)}
{@const windDir = hourly.winddirection_10m[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell wind-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
>
<td class="hour-cell icon-cell {isNow ? 'now' : ''}">
{#if windDir != null && !isNaN(windDir)}
<div class="wind-arrow" style="transform: {getWindArrowRotation(windDir)}">
<svg class="fill-foreground" width="16px" height="16px">
<svg class="fill-foreground" width="20px" height="20px">
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"
></use>
</svg>
@@ -1126,16 +1125,15 @@
<!-- Apparent Temperature (Feels Like) -->
<tr>
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-thermometer.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const temp = hourly.apparent_temperature[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}">
<td class="hour-cell {isNow ? 'now' : ''}">
{temp != null ? temp.toFixed(0) + '°' : '-'}
</td>
{/each}
@@ -1144,15 +1142,16 @@
<!-- Humidity -->
<tr>
<th class="row-header" scope="row">
<span class="row-unit">%</span>
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-humidity.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const hum = hourly.relative_humidity_2m[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
class="hour-cell {isNow ? 'now' : ''}"
style="background: {getHumidityBg(hum ?? 0)}"
>
{hum != null ? hum.toFixed(0) + '%' : '-'}
@@ -1160,50 +1159,23 @@
{/each}
</tr>
<!-- Wind Direction Labels -->
<tr>
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<use xlink:href="/images/weather-icons/wi-wind-deg.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{@const windDir = hourly.winddirection_10m[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td
class="hour-cell icon-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
>
{#if windDir != null && !isNaN(windDir)}
<div style="transform: rotate({windDir}deg); display: inline-block">
<svg class="fill-foreground" width="18px" height="18px">
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"
></use>
</svg>
</div>
{:else}
-
{/if}
</td>
{/each}
</tr>
<!-- Wind Speed values -->
<tr>
<th class="row-header" scope="row">
<div class="row-header-stack">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
</svg>
<span class="row-unit"
>{params.wind_speed_unit === 'kmh' ? 'km/h' : params.wind_speed_unit}</span
>
</div>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const wind = hourly.windspeed_10m[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}">
<td class="hour-cell {isNow ? 'now' : ''}">
{wind?.toFixed(0) ?? '-'}
</td>
{/each}
@@ -1212,16 +1184,15 @@
<!-- Dew Point -->
<tr>
<th class="row-header" scope="row">
<svg class="fill-foreground inline-block" width="14px" height="14px">
<svg class="fill-foreground inline-block" width="16px" height="16px">
<use xlink:href="/images/weather-icons/wi-raindrops.svg#Layer_1"></use>
</svg>
</th>
{#each filteredIdx as idx (idx)}
{#each dayIdx as idx (idx)}
{@const dp = hourly.dew_point_2m[idx]}
{@const date = dates[idx]}
{@const isNow = isCurrentHour(date)}
{@const isMidnight = date.getHours() === 0}
<td class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}">
<td class="hour-cell {isNow ? 'now' : ''}">
{dp != null ? dp.toFixed(0) + '°' : '-'}
</td>
{/each}
@@ -1229,7 +1200,7 @@
</tbody>
</table>
</div>
</div>
{/if}
{/if}
<!-- Sunrise / Sunset Info -->
@@ -1520,86 +1491,25 @@
left: 22px;
}
/* ═══ Horizontal Scroll Container ═══════════════════════════════════════════ */
/* ═══ Hourly Table Container ═══════════════════════════════════════════════ */
.hourly-scroll {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
margin-left: -1.25rem;
margin-right: -1.25rem;
.hourly-container {
border: 1px solid hsl(var(--border));
border-radius: 8px;
}
@media (min-width: 768px) {
.hourly-scroll {
margin-left: 0;
margin-right: 0;
}
}
.hourly-inner {
display: flex;
flex-direction: column;
}
/* ═══ Temperature Curve Area ═══════════════════════════════════════════════ */
.curve-area {
display: flex;
border-bottom: 2px solid hsl(var(--border));
background: hsl(var(--card) / 0.5);
}
.curve-row-header {
position: sticky;
left: 0;
z-index: 10;
min-width: 90px;
max-width: 90px;
background: hsl(var(--background));
border-right: 2px solid hsl(var(--border));
}
.curve-container {
position: relative;
overflow: hidden;
}
.curve-svg {
display: block;
}
.curve-temp-label {
font-size: 10px;
font-weight: 600;
}
.curve-icons {
position: absolute;
top: 2px;
left: 0;
display: flex;
pointer-events: none;
}
.curve-icon-cell {
position: absolute;
top: 0;
display: flex;
align-items: center;
justify-content: center;
height: 22px;
}
/* ═══ Hourly Data Table ═══════════════════════════════════════════════════ */
.hourly-table {
border-collapse: collapse;
white-space: nowrap;
font-size: 11px;
width: 100%;
table-layout: fixed;
}
.hourly-table .col-header {
width: 64px;
}
.hourly-table tr {
@@ -1611,18 +1521,15 @@
}
.row-header {
position: sticky;
left: 0;
z-index: 10;
min-width: 90px;
max-width: 90px;
padding: 3px 6px;
width: 64px;
padding: 4px 4px;
text-align: center;
font-weight: 600;
font-size: 11px;
background: hsl(var(--background));
border-right: 2px solid hsl(var(--border));
white-space: nowrap;
overflow: hidden;
}
.row-header-stack {
@@ -1630,7 +1537,13 @@
flex-direction: column;
align-items: center;
gap: 0;
line-height: 1.1;
line-height: 1.2;
}
.row-label {
font-size: 11px;
font-weight: 600;
color: hsl(var(--muted-foreground));
}
.row-unit {
@@ -1639,17 +1552,24 @@
color: hsl(var(--muted-foreground));
}
/* ═══ Hour Cells ═══════════════════════════════════════════════════════════ */
.hour-cell {
min-width: 38px;
max-width: 38px;
padding: 3px 1px;
padding: 6px 2px;
text-align: center;
font-size: 11px;
border-right: 1px solid hsl(var(--border) / 0.4);
font-size: 13px;
font-weight: 500;
border-right: 1px solid hsl(var(--border) / 0.3);
overflow: hidden;
}
.hour-cell.midnight {
border-left: 2px solid hsl(var(--primary) / 0.5);
.interval-3h .hour-cell {
font-size: 15px;
padding: 8px 4px;
}
.hour-cell:last-child {
border-right: none;
}
.hour-cell.now {
@@ -1659,32 +1579,111 @@
.time-cell {
font-weight: 700;
font-size: 10px;
background: hsl(var(--muted) / 0.3);
padding: 5px 2px;
}
.interval-3h .time-cell {
font-size: 14px;
padding: 6px 4px;
}
.interval-1h .time-cell {
font-size: 11px;
}
.time-sup {
font-size: 7px;
font-size: 8px;
vertical-align: super;
}
/* ═══ Weather Icon Row ═══════════════════════════════════════════════════ */
.icon-row {
background: hsl(var(--muted) / 0.15);
}
.weather-icon-cell {
padding: 6px 2px;
text-align: center;
vertical-align: middle;
line-height: 0;
border-right: 1px solid hsl(var(--border) / 0.3);
}
.weather-icon-cell:last-child {
border-right: none;
}
.interval-3h .weather-icon-cell {
padding: 10px 4px;
}
/* ═══ Temperature Curve ═══════════════════════════════════════════════════ */
.curve-row {
border-top: none !important;
}
.curve-header {
vertical-align: middle;
}
.curve-cell {
padding: 0 !important;
border-right: none !important;
}
.curve-container {
position: relative;
overflow: hidden;
width: 100%;
}
.curve-svg {
display: block;
width: 100%;
}
.curve-labels {
position: absolute;
inset: 0;
pointer-events: none;
}
.curve-label {
position: absolute;
transform: translateX(-50%);
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.interval-3h .curve-label {
font-size: 14px;
}
/* ═══ Precipitation Bar ═══════════════════════════════════════════════════ */
.precip-cell {
vertical-align: bottom;
padding: 1px;
padding: 2px !important;
}
.precip-bar-container {
width: 100%;
height: 22px;
height: 28px;
display: flex;
align-items: flex-end;
justify-content: center;
}
.interval-3h .precip-bar-container {
height: 36px;
}
.precip-bar {
width: 60%;
width: 55%;
min-height: 2px;
background: linear-gradient(to top, rgba(30, 136, 229, 0.5), rgba(30, 136, 229, 0.9));
border-radius: 2px 2px 0 0;
@@ -1692,16 +1691,22 @@
.precip-val {
display: block;
font-size: 9px;
line-height: 1;
font-size: 10px;
line-height: 1.2;
color: hsl(var(--muted-foreground));
text-align: center;
margin-top: 1px;
}
.interval-3h .precip-val {
font-size: 12px;
}
.icon-cell {
padding: 2px;
padding: 4px 2px;
vertical-align: middle;
line-height: 0;
text-align: center;
}
.icon-cell > div,
@@ -1709,12 +1714,7 @@
display: inline-block;
}
/* ═══ Wind Cell ═══════════════════════════════════════════════════════════ */
.wind-cell {
vertical-align: middle;
line-height: 0;
}
/* ═══ Wind Arrow ═══════════════════════════════════════════════════════════ */
.wind-arrow {
display: inline-block;
@@ -1817,5 +1817,25 @@
.card-temp-min {
font-size: 13px;
}
.hourly-table .col-header {
width: 48px;
}
.row-header {
width: 48px;
font-size: 10px;
padding: 3px 2px;
}
.hour-cell {
font-size: 11px;
padding: 4px 1px;
}
.interval-3h .hour-cell {
font-size: 13px;
padding: 6px 2px;
}
}
</style>