more translations
This commit is contained in:
@@ -30,3 +30,6 @@ AGENTS.md
|
|||||||
# paraglide compiles the message runtime into the source tree
|
# paraglide compiles the message runtime into the source tree
|
||||||
/src/lib/paraglide
|
/src/lib/paraglide
|
||||||
/.inlang
|
/.inlang
|
||||||
|
|
||||||
|
# Claude Code scratch: throwaway probe scripts, never committed
|
||||||
|
/.scratch
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { chromium } from 'playwright';
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
for (const [label, supporter] of [['plain', false], ['supporter', true]]) {
|
|
||||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
|
|
||||||
if (supporter) await ctx.addInitScript(() => localStorage.setItem('supporter_cache_v1', JSON.stringify({ valid: true, expires: null, checkedAt: 1785578719118 })));
|
|
||||||
const page = await ctx.newPage();
|
|
||||||
await page.goto('http://localhost:5186/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
|
|
||||||
await page.waitForTimeout(9000);
|
|
||||||
const btn = page.getByRole('button', { name: /Support Drizz|Supporter extras/ }).first();
|
|
||||||
await btn.screenshot({ path: `/tmp/badge-${label}.png` });
|
|
||||||
await ctx.close();
|
|
||||||
}
|
|
||||||
// locked panel
|
|
||||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
|
|
||||||
const page = await ctx.newPage();
|
|
||||||
await page.goto('http://localhost:5186/en/weather/seasonal/berlin/', { waitUntil: 'load', timeout: 120000 });
|
|
||||||
await page.waitForTimeout(6000);
|
|
||||||
await page.locator('h2').first().locator('xpath=..').screenshot({ path: '/tmp/badge-panel.png' }).catch(() => {});
|
|
||||||
console.log('ok');
|
|
||||||
await browser.close();
|
|
||||||
+52
-1
@@ -152,5 +152,56 @@
|
|||||||
"calm_3": "Den ganzen Tag so gut wie windstill.",
|
"calm_3": "Den ganzen Tag so gut wie windstill.",
|
||||||
"uv_1": "UV-Index bis {value} ({label}) - mittags an Sonnenschutz denken.",
|
"uv_1": "UV-Index bis {value} ({label}) - mittags an Sonnenschutz denken.",
|
||||||
"uv_2": "Mittags brennt die Sonne: UV {value}, {label}.",
|
"uv_2": "Mittags brennt die Sonne: UV {value}, {label}.",
|
||||||
"uv_3": "Mittags besser in den Schatten - der UV-Index erreicht {value} ({label})."
|
"uv_3": "Mittags besser in den Schatten - der UV-Index erreicht {value} ({label}).",
|
||||||
|
"legal_about": "Über uns",
|
||||||
|
"legal_imprint": "Impressum",
|
||||||
|
"legal_privacy": "Datenschutz",
|
||||||
|
"legal_terms": "Bedingungen",
|
||||||
|
"legal_nav": "Rechtliches",
|
||||||
|
"city_weather": "Wetter in {city}",
|
||||||
|
"var_temperature": "Temperatur",
|
||||||
|
"var_temperature_short": "Temp",
|
||||||
|
"var_icons": "Wettersymbole",
|
||||||
|
"var_icons_short": "Symbole",
|
||||||
|
"var_apparent": "Gefühlte Temp.",
|
||||||
|
"var_apparent_short": "Gefühlt",
|
||||||
|
"var_dew_point": "Taupunkt",
|
||||||
|
"var_dew_point_short": "Tau",
|
||||||
|
"var_cloud": "Bewölkung",
|
||||||
|
"var_cloud_short": "Wolken",
|
||||||
|
"var_cloud_low": "Bewölkung tief",
|
||||||
|
"var_cloud_low_short": "Tief",
|
||||||
|
"var_cloud_mid": "Bewölkung mittel",
|
||||||
|
"var_cloud_mid_short": "Mittel",
|
||||||
|
"var_cloud_high": "Bewölkung hoch",
|
||||||
|
"var_cloud_high_short": "Hoch",
|
||||||
|
"var_precipitation": "Niederschlag",
|
||||||
|
"var_precipitation_short": "Nied.",
|
||||||
|
"var_pop": "Niederschlagswahrsch.",
|
||||||
|
"var_pop_short": "Wahrsch.",
|
||||||
|
"var_rain": "Regen",
|
||||||
|
"var_rain_short": "Regen",
|
||||||
|
"var_showers": "Schauer",
|
||||||
|
"var_showers_short": "Schauer",
|
||||||
|
"var_snowfall": "Schneefall",
|
||||||
|
"var_snowfall_short": "Schnee",
|
||||||
|
"var_wind": "Windgeschwindigkeit",
|
||||||
|
"var_wind_short": "Wind",
|
||||||
|
"var_wind_dir": "Windrichtung",
|
||||||
|
"var_wind_dir_short": "Richt.",
|
||||||
|
"var_gusts": "Windböen",
|
||||||
|
"var_gusts_short": "Böen",
|
||||||
|
"var_humidity": "Luftfeuchte",
|
||||||
|
"var_humidity_short": "RF",
|
||||||
|
"var_pressure": "Luftdruck (MSL)",
|
||||||
|
"var_pressure_short": "MSLP",
|
||||||
|
"var_surface_pressure": "Bodendruck",
|
||||||
|
"var_surface_pressure_short": "Psfc",
|
||||||
|
"var_uv": "UV-Index",
|
||||||
|
"var_uv_short": "UV",
|
||||||
|
"var_visibility": "Sichtweite",
|
||||||
|
"var_visibility_short": "Sicht",
|
||||||
|
"var_cape": "CAPE",
|
||||||
|
"var_cape_short": "CAPE",
|
||||||
|
"var_time": "Zeit"
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-1
@@ -152,5 +152,56 @@
|
|||||||
"calm_3": "Next to no wind all day.",
|
"calm_3": "Next to no wind all day.",
|
||||||
"uv_1": "UV peaks at {value} ({label}), so cover up around midday.",
|
"uv_1": "UV peaks at {value} ({label}), so cover up around midday.",
|
||||||
"uv_2": "The sun bites around noon: UV {value}, {label}.",
|
"uv_2": "The sun bites around noon: UV {value}, {label}.",
|
||||||
"uv_3": "Worth some shade in the middle of the day - UV reaches {value} ({label})."
|
"uv_3": "Worth some shade in the middle of the day - UV reaches {value} ({label}).",
|
||||||
|
"legal_about": "About",
|
||||||
|
"legal_imprint": "Imprint",
|
||||||
|
"legal_privacy": "Privacy",
|
||||||
|
"legal_terms": "Terms",
|
||||||
|
"legal_nav": "Legal",
|
||||||
|
"city_weather": "{city} weather",
|
||||||
|
"var_temperature": "Temperature",
|
||||||
|
"var_temperature_short": "Temp",
|
||||||
|
"var_icons": "Weather icons",
|
||||||
|
"var_icons_short": "Icons",
|
||||||
|
"var_apparent": "Apparent Temp",
|
||||||
|
"var_apparent_short": "Feels",
|
||||||
|
"var_dew_point": "Dew Point",
|
||||||
|
"var_dew_point_short": "Dew",
|
||||||
|
"var_cloud": "Cloud Cover",
|
||||||
|
"var_cloud_short": "Cloud",
|
||||||
|
"var_cloud_low": "Cloud Cover Low",
|
||||||
|
"var_cloud_low_short": "Low",
|
||||||
|
"var_cloud_mid": "Cloud Cover Mid",
|
||||||
|
"var_cloud_mid_short": "Mid",
|
||||||
|
"var_cloud_high": "Cloud Cover High",
|
||||||
|
"var_cloud_high_short": "High",
|
||||||
|
"var_precipitation": "Precipitation",
|
||||||
|
"var_precipitation_short": "Precip",
|
||||||
|
"var_pop": "Precip. Probability",
|
||||||
|
"var_pop_short": "PoP",
|
||||||
|
"var_rain": "Rain",
|
||||||
|
"var_rain_short": "Rain",
|
||||||
|
"var_showers": "Showers",
|
||||||
|
"var_showers_short": "Shwr",
|
||||||
|
"var_snowfall": "Snowfall",
|
||||||
|
"var_snowfall_short": "Snow",
|
||||||
|
"var_wind": "Wind Speed",
|
||||||
|
"var_wind_short": "Wind",
|
||||||
|
"var_wind_dir": "Wind direction",
|
||||||
|
"var_wind_dir_short": "Dir",
|
||||||
|
"var_gusts": "Wind Gusts",
|
||||||
|
"var_gusts_short": "Gusts",
|
||||||
|
"var_humidity": "Humidity",
|
||||||
|
"var_humidity_short": "RH",
|
||||||
|
"var_pressure": "Pressure (MSL)",
|
||||||
|
"var_pressure_short": "MSLP",
|
||||||
|
"var_surface_pressure": "Surface Pressure",
|
||||||
|
"var_surface_pressure_short": "Psfc",
|
||||||
|
"var_uv": "UV Index",
|
||||||
|
"var_uv_short": "UV",
|
||||||
|
"var_visibility": "Visibility",
|
||||||
|
"var_visibility_short": "Vis",
|
||||||
|
"var_cape": "CAPE",
|
||||||
|
"var_cape_short": "CAPE",
|
||||||
|
"var_time": "Time"
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-1
@@ -152,5 +152,56 @@
|
|||||||
"calm_3": "Prácticamente sin viento en todo el día.",
|
"calm_3": "Prácticamente sin viento en todo el día.",
|
||||||
"uv_1": "El índice UV llega a {value} ({label}), así que protégete al mediodía.",
|
"uv_1": "El índice UV llega a {value} ({label}), así que protégete al mediodía.",
|
||||||
"uv_2": "El sol aprieta al mediodía: UV {value}, {label}.",
|
"uv_2": "El sol aprieta al mediodía: UV {value}, {label}.",
|
||||||
"uv_3": "Conviene buscar sombra al mediodía: el UV alcanza {value} ({label})."
|
"uv_3": "Conviene buscar sombra al mediodía: el UV alcanza {value} ({label}).",
|
||||||
|
"legal_about": "Acerca de",
|
||||||
|
"legal_imprint": "Aviso legal",
|
||||||
|
"legal_privacy": "Privacidad",
|
||||||
|
"legal_terms": "Condiciones",
|
||||||
|
"legal_nav": "Legal",
|
||||||
|
"city_weather": "El tiempo en {city}",
|
||||||
|
"var_temperature": "Temperatura",
|
||||||
|
"var_temperature_short": "Temp",
|
||||||
|
"var_icons": "Iconos",
|
||||||
|
"var_icons_short": "Iconos",
|
||||||
|
"var_apparent": "Sensación térmica",
|
||||||
|
"var_apparent_short": "Sensación",
|
||||||
|
"var_dew_point": "Punto de rocío",
|
||||||
|
"var_dew_point_short": "Rocío",
|
||||||
|
"var_cloud": "Nubosidad",
|
||||||
|
"var_cloud_short": "Nubes",
|
||||||
|
"var_cloud_low": "Nubosidad baja",
|
||||||
|
"var_cloud_low_short": "Baja",
|
||||||
|
"var_cloud_mid": "Nubosidad media",
|
||||||
|
"var_cloud_mid_short": "Media",
|
||||||
|
"var_cloud_high": "Nubosidad alta",
|
||||||
|
"var_cloud_high_short": "Alta",
|
||||||
|
"var_precipitation": "Precipitación",
|
||||||
|
"var_precipitation_short": "Precip.",
|
||||||
|
"var_pop": "Prob. de precipitación",
|
||||||
|
"var_pop_short": "Prob.",
|
||||||
|
"var_rain": "Lluvia",
|
||||||
|
"var_rain_short": "Lluvia",
|
||||||
|
"var_showers": "Chubascos",
|
||||||
|
"var_showers_short": "Chub.",
|
||||||
|
"var_snowfall": "Nevadas",
|
||||||
|
"var_snowfall_short": "Nieve",
|
||||||
|
"var_wind": "Velocidad del viento",
|
||||||
|
"var_wind_short": "Viento",
|
||||||
|
"var_wind_dir": "Dirección del viento",
|
||||||
|
"var_wind_dir_short": "Dir.",
|
||||||
|
"var_gusts": "Rachas de viento",
|
||||||
|
"var_gusts_short": "Rachas",
|
||||||
|
"var_humidity": "Humedad",
|
||||||
|
"var_humidity_short": "HR",
|
||||||
|
"var_pressure": "Presión (MSL)",
|
||||||
|
"var_pressure_short": "MSLP",
|
||||||
|
"var_surface_pressure": "Presión en superficie",
|
||||||
|
"var_surface_pressure_short": "Psfc",
|
||||||
|
"var_uv": "Índice UV",
|
||||||
|
"var_uv_short": "UV",
|
||||||
|
"var_visibility": "Visibilidad",
|
||||||
|
"var_visibility_short": "Vis.",
|
||||||
|
"var_cape": "CAPE",
|
||||||
|
"var_cape_short": "CAPE",
|
||||||
|
"var_time": "Hora"
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-1
@@ -152,5 +152,56 @@
|
|||||||
"calm_3": "Quasiment aucun vent de la journée.",
|
"calm_3": "Quasiment aucun vent de la journée.",
|
||||||
"uv_1": "Indice UV jusqu'à {value} ({label}) : protégez-vous en milieu de journée.",
|
"uv_1": "Indice UV jusqu'à {value} ({label}) : protégez-vous en milieu de journée.",
|
||||||
"uv_2": "Le soleil tape à la mi-journée : UV {value}, {label}.",
|
"uv_2": "Le soleil tape à la mi-journée : UV {value}, {label}.",
|
||||||
"uv_3": "Mieux vaut chercher l'ombre à midi : l'UV atteint {value} ({label})."
|
"uv_3": "Mieux vaut chercher l'ombre à midi : l'UV atteint {value} ({label}).",
|
||||||
|
"legal_about": "À propos",
|
||||||
|
"legal_imprint": "Mentions légales",
|
||||||
|
"legal_privacy": "Confidentialité",
|
||||||
|
"legal_terms": "Conditions",
|
||||||
|
"legal_nav": "Informations légales",
|
||||||
|
"city_weather": "Météo à {city}",
|
||||||
|
"var_temperature": "Température",
|
||||||
|
"var_temperature_short": "Temp",
|
||||||
|
"var_icons": "Icônes météo",
|
||||||
|
"var_icons_short": "Icônes",
|
||||||
|
"var_apparent": "Ressenti",
|
||||||
|
"var_apparent_short": "Ressenti",
|
||||||
|
"var_dew_point": "Point de rosée",
|
||||||
|
"var_dew_point_short": "Rosée",
|
||||||
|
"var_cloud": "Nébulosité",
|
||||||
|
"var_cloud_short": "Nuages",
|
||||||
|
"var_cloud_low": "Nébulosité basse",
|
||||||
|
"var_cloud_low_short": "Basse",
|
||||||
|
"var_cloud_mid": "Nébulosité moyenne",
|
||||||
|
"var_cloud_mid_short": "Moyenne",
|
||||||
|
"var_cloud_high": "Nébulosité haute",
|
||||||
|
"var_cloud_high_short": "Haute",
|
||||||
|
"var_precipitation": "Précipitations",
|
||||||
|
"var_precipitation_short": "Précip.",
|
||||||
|
"var_pop": "Probabilité de précip.",
|
||||||
|
"var_pop_short": "Prob.",
|
||||||
|
"var_rain": "Pluie",
|
||||||
|
"var_rain_short": "Pluie",
|
||||||
|
"var_showers": "Averses",
|
||||||
|
"var_showers_short": "Averses",
|
||||||
|
"var_snowfall": "Chutes de neige",
|
||||||
|
"var_snowfall_short": "Neige",
|
||||||
|
"var_wind": "Vitesse du vent",
|
||||||
|
"var_wind_short": "Vent",
|
||||||
|
"var_wind_dir": "Direction du vent",
|
||||||
|
"var_wind_dir_short": "Dir.",
|
||||||
|
"var_gusts": "Rafales",
|
||||||
|
"var_gusts_short": "Rafales",
|
||||||
|
"var_humidity": "Humidité",
|
||||||
|
"var_humidity_short": "HR",
|
||||||
|
"var_pressure": "Pression (MSL)",
|
||||||
|
"var_pressure_short": "MSLP",
|
||||||
|
"var_surface_pressure": "Pression au sol",
|
||||||
|
"var_surface_pressure_short": "Psfc",
|
||||||
|
"var_uv": "Indice UV",
|
||||||
|
"var_uv_short": "UV",
|
||||||
|
"var_visibility": "Visibilité",
|
||||||
|
"var_visibility_short": "Vis.",
|
||||||
|
"var_cape": "CAPE",
|
||||||
|
"var_cape_short": "CAPE",
|
||||||
|
"var_time": "Heure"
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-1
@@ -152,5 +152,56 @@
|
|||||||
"calm_3": "Praticamente senza vento per tutto il giorno.",
|
"calm_3": "Praticamente senza vento per tutto il giorno.",
|
||||||
"uv_1": "Indice UV fino a {value} ({label}): proteggiti a metà giornata.",
|
"uv_1": "Indice UV fino a {value} ({label}): proteggiti a metà giornata.",
|
||||||
"uv_2": "Il sole picchia a mezzogiorno: UV {value}, {label}.",
|
"uv_2": "Il sole picchia a mezzogiorno: UV {value}, {label}.",
|
||||||
"uv_3": "Meglio cercare ombra a metà giornata: l'UV arriva a {value} ({label})."
|
"uv_3": "Meglio cercare ombra a metà giornata: l'UV arriva a {value} ({label}).",
|
||||||
|
"legal_about": "Informazioni",
|
||||||
|
"legal_imprint": "Note legali",
|
||||||
|
"legal_privacy": "Privacy",
|
||||||
|
"legal_terms": "Condizioni",
|
||||||
|
"legal_nav": "Informazioni legali",
|
||||||
|
"city_weather": "Meteo a {city}",
|
||||||
|
"var_temperature": "Temperatura",
|
||||||
|
"var_temperature_short": "Temp",
|
||||||
|
"var_icons": "Icone meteo",
|
||||||
|
"var_icons_short": "Icone",
|
||||||
|
"var_apparent": "Percepita",
|
||||||
|
"var_apparent_short": "Percepita",
|
||||||
|
"var_dew_point": "Punto di rugiada",
|
||||||
|
"var_dew_point_short": "Rugiada",
|
||||||
|
"var_cloud": "Nuvolosità",
|
||||||
|
"var_cloud_short": "Nuvole",
|
||||||
|
"var_cloud_low": "Nuvolosità bassa",
|
||||||
|
"var_cloud_low_short": "Bassa",
|
||||||
|
"var_cloud_mid": "Nuvolosità media",
|
||||||
|
"var_cloud_mid_short": "Media",
|
||||||
|
"var_cloud_high": "Nuvolosità alta",
|
||||||
|
"var_cloud_high_short": "Alta",
|
||||||
|
"var_precipitation": "Precipitazioni",
|
||||||
|
"var_precipitation_short": "Precip.",
|
||||||
|
"var_pop": "Probabilità di precip.",
|
||||||
|
"var_pop_short": "Prob.",
|
||||||
|
"var_rain": "Pioggia",
|
||||||
|
"var_rain_short": "Pioggia",
|
||||||
|
"var_showers": "Rovesci",
|
||||||
|
"var_showers_short": "Rovesci",
|
||||||
|
"var_snowfall": "Nevicate",
|
||||||
|
"var_snowfall_short": "Neve",
|
||||||
|
"var_wind": "Velocità del vento",
|
||||||
|
"var_wind_short": "Vento",
|
||||||
|
"var_wind_dir": "Direzione del vento",
|
||||||
|
"var_wind_dir_short": "Dir.",
|
||||||
|
"var_gusts": "Raffiche",
|
||||||
|
"var_gusts_short": "Raffiche",
|
||||||
|
"var_humidity": "Umidità",
|
||||||
|
"var_humidity_short": "UR",
|
||||||
|
"var_pressure": "Pressione (MSL)",
|
||||||
|
"var_pressure_short": "MSLP",
|
||||||
|
"var_surface_pressure": "Pressione al suolo",
|
||||||
|
"var_surface_pressure_short": "Psfc",
|
||||||
|
"var_uv": "Indice UV",
|
||||||
|
"var_uv_short": "UV",
|
||||||
|
"var_visibility": "Visibilità",
|
||||||
|
"var_visibility_short": "Vis.",
|
||||||
|
"var_cape": "CAPE",
|
||||||
|
"var_cape_short": "CAPE",
|
||||||
|
"var_time": "Ora"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { chromium } from 'playwright';
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
const out = {};
|
|
||||||
for (const [label, vp, mobile] of [['mobile', { width: 390, height: 844 }, true], ['desktop', { width: 1280, height: 950 }, false]]) {
|
|
||||||
const ctx = await browser.newContext({ viewport: vp, isMobile: mobile, hasTouch: mobile });
|
|
||||||
const page = await ctx.newPage();
|
|
||||||
await page.goto('http://localhost:5186/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
|
|
||||||
await page.waitForTimeout(14000);
|
|
||||||
out[label] = await page.evaluate(() => ({
|
|
||||||
rows: [...document.querySelectorAll('tr')].map((r) => ({
|
|
||||||
label: r.querySelector('.hdr')?.textContent?.trim().replace(/\s+/g, ' ').slice(0, 10) || '-',
|
|
||||||
h: Math.round(r.getBoundingClientRect().height)
|
|
||||||
})),
|
|
||||||
tableHeight: Math.round(document.querySelector('table').getBoundingClientRect().height)
|
|
||||||
}));
|
|
||||||
await page.screenshot({ path: `/tmp/rows-${label}.png`, clip: { x: 0, y: 0, width: vp.width, height: Math.min(820, vp.height) } });
|
|
||||||
await ctx.close();
|
|
||||||
}
|
|
||||||
console.log(JSON.stringify(out, null, 1));
|
|
||||||
await browser.close();
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
import { chromium } from 'playwright';
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
const page = await browser.newPage({ viewport: { width: 1280, height: 950 } });
|
|
||||||
await page.goto('http://localhost:5186/en/weather/week/berlin/', { waitUntil: 'load', timeout: 120000 });
|
|
||||||
await page.waitForTimeout(14000);
|
|
||||||
|
|
||||||
// scroll right down to the footer and click a link there
|
|
||||||
await page.evaluate(() => { const m = document.querySelector('main'); m.scrollTop = m.scrollHeight; });
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
const before = await page.evaluate(() => document.querySelector('main').scrollTop);
|
|
||||||
await page.locator('footer a[href*="/weather/14-day"]').first().click();
|
|
||||||
await page.waitForTimeout(7000);
|
|
||||||
const after = await page.evaluate(() => document.querySelector('main').scrollTop);
|
|
||||||
console.log(JSON.stringify({ scrollBeforeClick: Math.round(before), scrollAfterNav: Math.round(after), url: page.url() }));
|
|
||||||
await browser.close();
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
import { getLocale } from '$lib/paraglide/runtime';
|
||||||
|
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks the content component for the current locale.
|
||||||
|
*
|
||||||
|
* Long-form pages (about, imprint, privacy, terms) are kept as one component
|
||||||
|
* per language rather than as message strings: the prose is full of inline
|
||||||
|
* links, lists and headings, and splitting that into placeholders makes both
|
||||||
|
* the source and the translations harder to read and to keep correct.
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
variants: Record<string, Component>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { variants }: Props = $props();
|
||||||
|
|
||||||
|
let Content = $derived.by(() => {
|
||||||
|
// the URL decides the locale, so re-resolve on navigation
|
||||||
|
void $page.url.pathname;
|
||||||
|
return variants[getLocale()] ?? variants.en;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Content />
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<a
|
<a
|
||||||
class="text-foreground/80 underline-offset-2 hover:text-foreground hover:underline"
|
class="text-foreground/80 underline-offset-2 hover:text-foreground hover:underline"
|
||||||
href={href('/weather/week/[location]', { location: city.slug })}
|
href={href('/weather/week/[location]', { location: city.slug })}
|
||||||
>{city.label} weather</a
|
>{m.city_weather({ city: city.label })}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -102,21 +102,21 @@
|
|||||||
class="mt-10 flex flex-col items-start justify-between gap-3 border-t border-border/70 pt-5 text-xs text-muted-foreground sm:flex-row sm:items-center"
|
class="mt-10 flex flex-col items-start justify-between gap-3 border-t border-border/70 pt-5 text-xs text-muted-foreground sm:flex-row sm:items-center"
|
||||||
>
|
>
|
||||||
<span>© {year} Drizz.li</span>
|
<span>© {year} Drizz.li</span>
|
||||||
<nav aria-label="Legal" class="flex flex-wrap items-center gap-x-5 gap-y-1">
|
<nav aria-label={m.legal_nav()} class="flex flex-wrap items-center gap-x-5 gap-y-1">
|
||||||
<a class="underline-offset-2 hover:text-foreground hover:underline" href={href('/about')}
|
<a class="underline-offset-2 hover:text-foreground hover:underline" href={href('/about')}
|
||||||
>About</a
|
>{m.legal_about()}</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="underline-offset-2 hover:text-foreground hover:underline"
|
class="underline-offset-2 hover:text-foreground hover:underline"
|
||||||
href={href('/legal/imprint')}>Imprint</a
|
href={href('/legal/imprint')}>{m.legal_imprint()}</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="underline-offset-2 hover:text-foreground hover:underline"
|
class="underline-offset-2 hover:text-foreground hover:underline"
|
||||||
href={href('/legal/privacy')}>Privacy</a
|
href={href('/legal/privacy')}>{m.legal_privacy()}</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="underline-offset-2 hover:text-foreground hover:underline"
|
class="underline-offset-2 hover:text-foreground hover:underline"
|
||||||
href={href('/legal/terms')}>Terms</a
|
href={href('/legal/terms')}>{m.legal_terms()}</a
|
||||||
>
|
>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-3
@@ -1,3 +1,16 @@
|
|||||||
<svelte:head>
|
<script lang="ts">
|
||||||
<title>Drizzli</title>
|
import { onMount } from 'svelte';
|
||||||
</svelte:head>
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { localizeHref } from '$lib/paraglide/runtime';
|
||||||
|
|
||||||
|
// Deliberately a client-side redirect, not one from `load`: this page is
|
||||||
|
// prerendered, so a redirect resolved at build time would bake in the base
|
||||||
|
// locale and every visitor would land on English. Deciding it here lets the
|
||||||
|
// locale strategy read the URL, the cookie and finally the browser's own
|
||||||
|
// languages before choosing.
|
||||||
|
onMount(() => {
|
||||||
|
goto(localizeHref('/weather/week/'), { replaceState: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
import { localizeHref } from '$lib/paraglide/runtime';
|
|
||||||
|
|
||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async () => {
|
|
||||||
// bare `/` lands in the locale the strategy resolved (URL, cookie, then the
|
|
||||||
// browser's own languages), not always English
|
|
||||||
throw redirect(303, localizeHref('/weather/week/'));
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -1,41 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ProsePage from '$lib/components/prose-page.svelte';
|
import LocalizedContent from '$lib/components/localized-content.svelte';
|
||||||
|
|
||||||
|
import de from './content/de.svelte';
|
||||||
|
import en from './content/en.svelte';
|
||||||
|
import es from './content/es.svelte';
|
||||||
|
import fr from './content/fr.svelte';
|
||||||
|
import it from './content/it.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProsePage title="About Drizz.li" subtitle="A small, open-source weather site.">
|
<LocalizedContent variants={{ en, de, es, fr, it }} />
|
||||||
<p>
|
|
||||||
Drizz.li is an independent weather site. It shows multi-model forecasts, model comparisons,
|
|
||||||
14-day outlooks, weather maps and historical weather - without accounts, ads or trackers.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>How it works</h2>
|
|
||||||
<p>
|
|
||||||
All weather data comes from the excellent open-data APIs of
|
|
||||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which aggregates
|
|
||||||
national weather services (DWD, NOAA, Météo-France, MeteoSwiss and more). Drizz.li is a static
|
|
||||||
site: forecasts are fetched directly by your browser and rendered on your device.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The site is open source. Anyone can read the code, report issues or contribute improvements.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Supporting the project</h2>
|
|
||||||
<p>
|
|
||||||
Drizz.li is free to use. If you find it useful, you can support it with a small monthly
|
|
||||||
contribution (from €3 / month) - it helps cover the domain, hosting and development time. As a
|
|
||||||
thank-you, supporters unlock the extras: historical weather with climate-normal comparisons, and
|
|
||||||
new supporter features as they land.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Contributions are handled by a simple bank transfer - no payment processor, no stored card
|
|
||||||
details. The details of how that works are in the
|
|
||||||
<a href="/legal/terms/">supporter terms</a> and the
|
|
||||||
<a href="/legal/privacy/">privacy policy</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Contact</h2>
|
|
||||||
<p>
|
|
||||||
Questions, feedback or bug reports:
|
|
||||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
|
||||||
</p>
|
|
||||||
</ProsePage>
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ProsePage from '$lib/components/prose-page.svelte';
|
||||||
|
|
||||||
|
import { href } from '$lib/i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProsePage title="Über Drizz.li" subtitle="Eine kleine, quelloffene Wetterseite.">
|
||||||
|
<p>
|
||||||
|
Drizz.li ist eine unabhängige Wetterseite. Sie zeigt Vorhersagen mehrerer Modelle,
|
||||||
|
Modellvergleiche, 14-Tage-Aussichten, Wetterkarten und historisches Wetter - ohne Konten,
|
||||||
|
Werbung oder Tracker.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>So funktioniert es</h2>
|
||||||
|
<p>
|
||||||
|
Alle Wetterdaten stammen aus den hervorragenden Open-Data-Schnittstellen von
|
||||||
|
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, die nationale
|
||||||
|
Wetterdienste bündeln (DWD, NOAA, Météo-France, MeteoSchweiz und weitere). Drizz.li ist eine
|
||||||
|
statische Seite: Die Vorhersagen werden direkt von Ihrem Browser abgerufen und auf Ihrem Gerät
|
||||||
|
dargestellt.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Die Seite ist quelloffen. Jede und jeder kann den Code lesen, Fehler melden oder Verbesserungen
|
||||||
|
beitragen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Das Projekt unterstützen</h2>
|
||||||
|
<p>
|
||||||
|
Drizz.li ist kostenlos nutzbar. Wenn es Ihnen nützt, können Sie es mit einem kleinen monatlichen
|
||||||
|
Beitrag unterstützen (ab 3 € / Monat) - das hilft, Domain, Hosting und Entwicklungszeit zu
|
||||||
|
decken. Als Dankeschön schalten Unterstützerinnen und Unterstützer die Extras frei: historisches
|
||||||
|
Wetter mit Vergleich zu den Klimanormalen sowie neue Funktionen, sobald sie erscheinen.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Beiträge laufen über eine einfache Banküberweisung - kein Zahlungsdienstleister, keine
|
||||||
|
gespeicherten Kartendaten. Wie das genau abläuft, steht in den
|
||||||
|
<a href={href('/legal/terms')}>Unterstützerbedingungen</a> und in der
|
||||||
|
<a href={href('/legal/privacy')}>Datenschutzerklärung</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Kontakt</h2>
|
||||||
|
<p>
|
||||||
|
Fragen, Rückmeldungen oder Fehlerberichte:
|
||||||
|
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||||
|
</p>
|
||||||
|
</ProsePage>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ProsePage from '$lib/components/prose-page.svelte';
|
||||||
|
|
||||||
|
import { href } from '$lib/i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProsePage title="About Drizz.li" subtitle="A small, open-source weather site.">
|
||||||
|
<p>
|
||||||
|
Drizz.li is an independent weather site. It shows multi-model forecasts, model comparisons,
|
||||||
|
14-day outlooks, weather maps and historical weather - without accounts, ads or trackers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>How it works</h2>
|
||||||
|
<p>
|
||||||
|
All weather data comes from the excellent open-data APIs of
|
||||||
|
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which aggregates
|
||||||
|
national weather services (DWD, NOAA, Météo-France, MeteoSwiss and more). Drizz.li is a static
|
||||||
|
site: forecasts are fetched directly by your browser and rendered on your device.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The site is open source. Anyone can read the code, report issues or contribute improvements.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Supporting the project</h2>
|
||||||
|
<p>
|
||||||
|
Drizz.li is free to use. If you find it useful, you can support it with a small monthly
|
||||||
|
contribution (from €3 / month) - it helps cover the domain, hosting and development time. As a
|
||||||
|
thank-you, supporters unlock the extras: historical weather with climate-normal comparisons, and
|
||||||
|
new supporter features as they land.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Contributions are handled by a simple bank transfer - no payment processor, no stored card
|
||||||
|
details. The details of how that works are in the
|
||||||
|
<a href={href('/legal/terms')}>supporter terms</a> and the
|
||||||
|
<a href={href('/legal/privacy')}>privacy policy</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
Questions, feedback or bug reports:
|
||||||
|
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||||
|
</p>
|
||||||
|
</ProsePage>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ProsePage from '$lib/components/prose-page.svelte';
|
||||||
|
|
||||||
|
import { href } from '$lib/i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProsePage title="Sobre Drizz.li" subtitle="Un pequeño sitio meteorológico de código abierto.">
|
||||||
|
<p>
|
||||||
|
Drizz.li es un sitio meteorológico independiente. Muestra pronósticos de varios modelos,
|
||||||
|
comparaciones entre modelos, previsiones a 14 días, mapas meteorológicos y datos históricos, sin
|
||||||
|
cuentas, sin anuncios y sin rastreadores.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Cómo funciona</h2>
|
||||||
|
<p>
|
||||||
|
Todos los datos proceden de las excelentes API de datos abiertos de
|
||||||
|
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, que agrega los
|
||||||
|
servicios meteorológicos nacionales (DWD, NOAA, Météo-France, MeteoSwiss y más). Drizz.li es un
|
||||||
|
sitio estático: tu navegador descarga los pronósticos directamente y los representa en tu
|
||||||
|
dispositivo.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
El sitio es de código abierto. Cualquiera puede leer el código, informar de errores o contribuir
|
||||||
|
con mejoras.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Apoyar el proyecto</h2>
|
||||||
|
<p>
|
||||||
|
Drizz.li es gratuito. Si te resulta útil, puedes apoyarlo con una pequeña aportación mensual
|
||||||
|
(desde 3 € al mes): ayuda a cubrir el dominio, el alojamiento y el tiempo de desarrollo. Como
|
||||||
|
agradecimiento, quienes colaboran desbloquean los extras: clima histórico con comparación frente
|
||||||
|
a las normales climáticas y las nuevas funciones que vayan llegando.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Las aportaciones se gestionan mediante una simple transferencia bancaria: sin pasarela de pago y
|
||||||
|
sin guardar datos de tarjetas. Los detalles están en las
|
||||||
|
<a href={href('/legal/terms')}>condiciones para colaboradores</a> y en la
|
||||||
|
<a href={href('/legal/privacy')}>política de privacidad</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Contacto</h2>
|
||||||
|
<p>
|
||||||
|
Dudas, comentarios o informes de errores:
|
||||||
|
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||||
|
</p>
|
||||||
|
</ProsePage>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ProsePage from '$lib/components/prose-page.svelte';
|
||||||
|
|
||||||
|
import { href } from '$lib/i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProsePage title="À propos de Drizz.li" subtitle="Un petit site météo open source.">
|
||||||
|
<p>
|
||||||
|
Drizz.li est un site météo indépendant. Il propose des prévisions multi-modèles, des
|
||||||
|
comparaisons de modèles, des tendances à 14 jours, des cartes météo et des données historiques,
|
||||||
|
sans compte, sans publicité et sans traceurs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Comment ça marche</h2>
|
||||||
|
<p>
|
||||||
|
Toutes les données proviennent des excellentes API ouvertes d'<a
|
||||||
|
href="https://open-meteo.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener">Open-Meteo</a
|
||||||
|
>, qui agrègent les services météorologiques nationaux (DWD, NOAA, Météo-France, MeteoSwiss et
|
||||||
|
d'autres). Drizz.li est un site statique : les prévisions sont récupérées directement par votre
|
||||||
|
navigateur et affichées sur votre appareil.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Le site est open source. Chacun peut lire le code, signaler un problème ou proposer des
|
||||||
|
améliorations.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Soutenir le projet</h2>
|
||||||
|
<p>
|
||||||
|
Drizz.li est gratuit. S'il vous est utile, vous pouvez le soutenir par une petite contribution
|
||||||
|
mensuelle (à partir de 3 € par mois) : elle aide à couvrir le nom de domaine, l'hébergement et
|
||||||
|
le temps de développement. En remerciement, les contributeurs débloquent les bonus : la météo
|
||||||
|
historique avec comparaison aux normales climatiques, ainsi que les nouvelles fonctions à venir.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Les contributions passent par un simple virement bancaire : aucun prestataire de paiement,
|
||||||
|
aucune donnée de carte conservée. Le détail figure dans les
|
||||||
|
<a href={href('/legal/terms')}>conditions contributeur</a> et dans la
|
||||||
|
<a href={href('/legal/privacy')}>politique de confidentialité</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
Questions, retours ou signalements de bugs :
|
||||||
|
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||||
|
</p>
|
||||||
|
</ProsePage>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ProsePage from '$lib/components/prose-page.svelte';
|
||||||
|
|
||||||
|
import { href } from '$lib/i18n';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProsePage title="Informazioni su Drizz.li" subtitle="Un piccolo sito meteo open source.">
|
||||||
|
<p>
|
||||||
|
Drizz.li è un sito meteo indipendente. Offre previsioni multi-modello, confronti tra modelli,
|
||||||
|
tendenze a 14 giorni, mappe meteo e dati storici, senza account, senza pubblicità e senza
|
||||||
|
tracciatori.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Come funziona</h2>
|
||||||
|
<p>
|
||||||
|
Tutti i dati provengono dalle ottime API aperte di
|
||||||
|
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, che aggregano i
|
||||||
|
servizi meteorologici nazionali (DWD, NOAA, Météo-France, MeteoSwiss e altri). Drizz.li è un
|
||||||
|
sito statico: le previsioni vengono scaricate direttamente dal tuo browser e disegnate sul tuo
|
||||||
|
dispositivo.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Il sito è open source. Chiunque può leggere il codice, segnalare problemi o contribuire con
|
||||||
|
miglioramenti.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Sostenere il progetto</h2>
|
||||||
|
<p>
|
||||||
|
Drizz.li è gratuito. Se lo trovi utile, puoi sostenerlo con un piccolo contributo mensile (da 3
|
||||||
|
€ al mese): aiuta a coprire dominio, hosting e tempo di sviluppo. Come ringraziamento, chi
|
||||||
|
sostiene il progetto sblocca gli extra: meteo storico con confronto rispetto alle normali
|
||||||
|
climatiche e le nuove funzioni man mano che arrivano.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I contributi avvengono con un semplice bonifico bancario: nessun gestore di pagamenti, nessun
|
||||||
|
dato della carta conservato. I dettagli sono nelle
|
||||||
|
<a href={href('/legal/terms')}>condizioni per i sostenitori</a> e nell'<a
|
||||||
|
href={href('/legal/privacy')}>informativa sulla privacy</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Contatti</h2>
|
||||||
|
<p>
|
||||||
|
Domande, riscontri o segnalazioni di bug:
|
||||||
|
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||||
|
</p>
|
||||||
|
</ProsePage>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { localizeHref } from '$lib/paraglide/runtime';
|
||||||
|
|
||||||
|
// Same reasoning as the root redirect: resolved in the browser so the
|
||||||
|
// visitor's language is known (see routes/+page.svelte).
|
||||||
|
onMount(() => {
|
||||||
|
goto(localizeHref('/weather/week/'), { replaceState: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
import { localizeHref } from '$lib/paraglide/runtime';
|
|
||||||
|
|
||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async () => {
|
|
||||||
// keep the visitor in their language when the bare /weather path is hit
|
|
||||||
throw redirect(303, localizeHref('/weather/week/'));
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
@@ -173,15 +173,38 @@
|
|||||||
|
|
||||||
// Apply ?day= once the forecast is in: the parameter is a plain calendar date,
|
// Apply ?day= once the forecast is in: the parameter is a plain calendar date,
|
||||||
// which only means something against the location's own timezone.
|
// which only means something against the location's own timezone.
|
||||||
let appliedDayParam = false;
|
// A `?day=` outside what this page can ever show (more than 3 days back, or
|
||||||
|
// beyond the 15-day horizon) is dropped and we open on today. A day that IS
|
||||||
|
// reachable but sits outside the default 7-day window widens the request up
|
||||||
|
// front, so the link loads straight into the right day instead of showing
|
||||||
|
// today and making the visitor click.
|
||||||
|
let wantedDay: string | null = $state(null);
|
||||||
|
onMount(() => {
|
||||||
|
const param = get(page).url.searchParams.get('day');
|
||||||
|
if (!param || !/^\d{4}-\d{2}-\d{2}$/.test(param)) return;
|
||||||
|
|
||||||
|
const target = Date.parse(`${param}T12:00:00Z`);
|
||||||
|
if (Number.isNaN(target)) return;
|
||||||
|
const today = new Date();
|
||||||
|
today.setUTCHours(12, 0, 0, 0);
|
||||||
|
const offset = Math.round((target - today.getTime()) / 86_400_000);
|
||||||
|
|
||||||
|
if (offset < -3 || offset > 15) return; // out of reach: stay on today
|
||||||
|
wantedDay = param;
|
||||||
|
if (offset < 0) pastDays = 3;
|
||||||
|
if (offset >= 7) forecastDays = 15;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select it as soon as a fetch arrives that actually covers the day.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const fd = fetchedDaily;
|
const fd = fetchedDaily;
|
||||||
if (appliedDayParam || !fd) return;
|
const wanted = wantedDay;
|
||||||
appliedDayParam = true;
|
if (!fd || !wanted) return;
|
||||||
const wanted = get(page).url.searchParams.get('day');
|
|
||||||
if (!wanted) return;
|
|
||||||
const match = fd.dailyDates.find((d) => formatZoned(d, fd.timezone, 'yyyy-MM-dd') === wanted);
|
const match = fd.dailyDates.find((d) => formatZoned(d, fd.timezone, 'yyyy-MM-dd') === wanted);
|
||||||
if (match) selectedDay.setTime(match.getTime());
|
if (match) {
|
||||||
|
selectedDay.setTime(match.getTime());
|
||||||
|
wantedDay = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mirror the open day and the plotted model back into the URL.
|
// Mirror the open day and the plotted model back into the URL.
|
||||||
@@ -189,8 +212,11 @@
|
|||||||
const dayKey = selectedDayKey;
|
const dayKey = selectedDayKey;
|
||||||
const model = params.models?.[0];
|
const model = params.models?.[0];
|
||||||
if (!mounted || !dayKey) return;
|
if (!mounted || !dayKey) return;
|
||||||
|
const isToday = fetchedDaily
|
||||||
|
? dayKey === formatZoned(new Date(), fetchedDaily.timezone, 'yyyy-MM-dd')
|
||||||
|
: false;
|
||||||
syncSearchParams($page.url, {
|
syncSearchParams($page.url, {
|
||||||
day: dayKey,
|
day: isToday ? null : dayKey,
|
||||||
model: model && model !== 'best_match' ? model : null
|
model: model && model !== 'best_match' ? model : null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -235,12 +235,15 @@
|
|||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A value the model actually produced (NaN pads everything past its horizon). */
|
||||||
|
const finite = (v: number | null | undefined): v is number => v != null && Number.isFinite(v);
|
||||||
|
|
||||||
function formatTemp(temp: number | null): string {
|
function formatTemp(temp: number | null): string {
|
||||||
return temp != null ? `${temp.toFixed(0)}°` : '-';
|
return finite(temp) ? `${temp.toFixed(0)}°` : '–';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatValue(val: number | null): string {
|
function formatValue(val: number | null): string {
|
||||||
return val != null ? val.toFixed(0) : '-';
|
return finite(val) ? val.toFixed(0) : '–';
|
||||||
}
|
}
|
||||||
|
|
||||||
let dayIdx = $derived(getDayIndices(data.hourlyDates, selectedDay));
|
let dayIdx = $derived(getDayIndices(data.hourlyDates, selectedDay));
|
||||||
@@ -254,15 +257,48 @@
|
|||||||
const isNow =
|
const isNow =
|
||||||
formatZoned(date, tz, 'yyyy-MM-dd HH') === formatZoned(today, tz, 'yyyy-MM-dd HH');
|
formatZoned(date, tz, 'yyyy-MM-dd HH') === formatZoned(today, tz, 'yyyy-MM-dd HH');
|
||||||
|
|
||||||
|
// a column past the model's horizon: nothing to show in any row
|
||||||
|
const hasData = [
|
||||||
|
data.hourly.temperature_2m,
|
||||||
|
data.hourly.weather_code,
|
||||||
|
data.hourly.windspeed_10m,
|
||||||
|
data.hourly.relative_humidity_2m,
|
||||||
|
data.hourly.cloud_cover,
|
||||||
|
data.hourly.precipitation
|
||||||
|
].some((arr) => arr != null && Number.isFinite(arr[idx]));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
idx,
|
idx,
|
||||||
date,
|
date,
|
||||||
isNow,
|
isNow,
|
||||||
isDaytime: daytimeFlags[i]
|
isDaytime: daytimeFlags[i],
|
||||||
|
hasData
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// A model that doesn't carry a variable returns an empty or all-NaN array.
|
||||||
|
// Those rows are dimmed rather than shown as a wall of dashes.
|
||||||
|
let rowHasData = $derived.by((): Record<string, boolean> => {
|
||||||
|
const has = (arr: number[] | undefined) =>
|
||||||
|
arr != null && cellData.some((c) => finite(arr[c.idx]));
|
||||||
|
return {
|
||||||
|
icons: has(data.hourly.weather_code),
|
||||||
|
temperature: has(data.hourly.temperature_2m),
|
||||||
|
feels: has(data.hourly.apparent_temperature),
|
||||||
|
dew_point: has(data.hourly.dew_point_2m),
|
||||||
|
wind: has(data.hourly.windspeed_10m),
|
||||||
|
gusts: has(data.hourly.wind_gusts_10m),
|
||||||
|
humidity: has(data.hourly.relative_humidity_2m),
|
||||||
|
clouds: has(data.hourly.cloud_cover),
|
||||||
|
pressure: has(data.hourly.pressure_msl),
|
||||||
|
uv: has(data.hourly.uv_index),
|
||||||
|
visibility: has(data.hourly.visibility),
|
||||||
|
precipitation: has(data.hourly.precipitation),
|
||||||
|
snowfall: has(data.hourly.snowfall)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
let sunrisePercent = $derived(
|
let sunrisePercent = $derived(
|
||||||
sunTimes && cellData.length > 0 ? timeToFraction(sunTimes.sunrise) * 100 : null
|
sunTimes && cellData.length > 0 ? timeToFraction(sunTimes.sunrise) * 100 : null
|
||||||
);
|
);
|
||||||
@@ -475,7 +511,7 @@
|
|||||||
<!-- Time + Daylight bar (merged) -->
|
<!-- Time + Daylight bar (merged) -->
|
||||||
<tr class="row">
|
<tr class="row">
|
||||||
<th class="hdr" scope="row" bind:clientWidth={headerColWidth}>
|
<th class="hdr" scope="row" bind:clientWidth={headerColWidth}>
|
||||||
<span class="text-[10px] font-semibold text-muted-foreground">Time</span>
|
<span class="text-[10px] font-semibold text-muted-foreground">{m.var_time()}</span>
|
||||||
</th>
|
</th>
|
||||||
<td
|
<td
|
||||||
colspan={cellData.length}
|
colspan={cellData.length}
|
||||||
@@ -556,6 +592,7 @@
|
|||||||
{@const leftPct = (i / cellData.length) * 100}
|
{@const leftPct = (i / cellData.length) * 100}
|
||||||
{@const widthPct = 100 / cellData.length}
|
{@const widthPct = 100 / cellData.length}
|
||||||
<span
|
<span
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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'}
|
||||||
@@ -591,12 +628,14 @@
|
|||||||
<!-- Weather Icons -->
|
<!-- Weather Icons -->
|
||||||
{#each rowOrder as rowKey (rowKey)}
|
{#each rowOrder as rowKey (rowKey)}
|
||||||
{#if rowKey === 'icons' && showRow('icons')}
|
{#if rowKey === 'icons' && showRow('icons')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-day-cloudy')}
|
{@render rowHeader('wi-day-cloudy')}
|
||||||
{#each cellData as cell, i (cell.idx)}
|
{#each cellData as cell, i (cell.idx)}
|
||||||
{@const wCode = hourly.weather_code[cell.idx]}
|
{@const wCode = hourly.weather_code[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
class="cell h-12 leading-0"
|
class="cell h-12 leading-0"
|
||||||
|
class:cell-empty={!finite(wCode)}
|
||||||
class:icon-day={cell.isDaytime}
|
class:icon-day={cell.isDaytime}
|
||||||
class:icon-night={!cell.isDaytime}
|
class:icon-night={!cell.isDaytime}
|
||||||
class:icon-dawn={!cell.isDaytime && cellData[i + 1]?.isDaytime}
|
class:icon-dawn={!cell.isDaytime && cellData[i + 1]?.isDaytime}
|
||||||
@@ -604,20 +643,23 @@
|
|||||||
cellData[i + 1] &&
|
cellData[i + 1] &&
|
||||||
!cellData[i + 1].isDaytime}
|
!cellData[i + 1].isDaytime}
|
||||||
>
|
>
|
||||||
{@render weatherIcon(getWeatherIconName(wCode, cell.isDaytime), iconPx)}
|
{#if finite(wCode)}
|
||||||
|
{@render weatherIcon(getWeatherIconName(wCode, cell.isDaytime), iconPx)}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Temperature -->
|
<!-- Temperature -->
|
||||||
{:else if rowKey === 'temperature' && showRow('temperature')}
|
{:else if rowKey === 'temperature' && showRow('temperature')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-thermometer', tempUnit, 'Temp')}
|
{@render rowHeader('wi-thermometer', tempUnit, m.var_temperature_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const temp = hourly.temperature_2m[cell.idx]}
|
{@const temp = hourly.temperature_2m[cell.idx]}
|
||||||
{@const style = getTempStyle(temp, String(units.temperature_unit))}
|
{@const style = getTempStyle(temp, String(units.temperature_unit))}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
class="cell h-12 font-bold {is3h ? 'text-lg' : 'text-[15px]'}"
|
class="cell h-12 font-bold {is3h ? 'text-lg' : 'text-[15px]'}"
|
||||||
style="background-color:{style.bg};color:{style.fg}"
|
style={finite(temp) ? `background-color:${style.bg};color:${style.fg}` : ''}
|
||||||
>
|
>
|
||||||
{formatTemp(temp)}
|
{formatTemp(temp)}
|
||||||
</td>
|
</td>
|
||||||
@@ -625,11 +667,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Feels Like (short row) -->
|
<!-- Feels Like (short row) -->
|
||||||
{:else if rowKey === 'feels' && showRow('feels')}
|
{:else if rowKey === 'feels' && showRow('feels')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader(undefined, tempUnit, 'Feels')}
|
{@render rowHeader(undefined, tempUnit, m.var_apparent_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const temp = hourly.apparent_temperature[cell.idx]}
|
{@const temp = hourly.apparent_temperature[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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)}
|
||||||
@@ -638,11 +681,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Dew Point (short row; off by default) -->
|
<!-- Dew Point (short row; off by default) -->
|
||||||
{:else if rowKey === 'dew_point' && showRow('dew_point')}
|
{:else if rowKey === 'dew_point' && showRow('dew_point')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-raindrop', tempUnit, 'Dew')}
|
{@render rowHeader('wi-raindrop', tempUnit, m.var_dew_point_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const temp = hourly.dew_point_2m?.[cell.idx]}
|
{@const temp = hourly.dew_point_2m?.[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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)}
|
||||||
@@ -651,12 +695,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Wind -->
|
<!-- Wind -->
|
||||||
{:else if rowKey === 'wind' && showRow('wind')}
|
{:else if rowKey === 'wind' && showRow('wind')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-strong-wind', windUnit, 'Wind')}
|
{@render rowHeader('wi-strong-wind', windUnit, m.var_wind_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const wind = hourly.windspeed_10m[cell.idx]}
|
{@const wind = hourly.windspeed_10m[cell.idx]}
|
||||||
{@const windDir = hourly.winddirection_10m[cell.idx]}
|
{@const windDir = hourly.winddirection_10m[cell.idx]}
|
||||||
<td class="relative cell h-12 align-middle leading-none">
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
|
class="relative cell h-12 align-middle leading-none"
|
||||||
|
>
|
||||||
{#if windDir != null && !isNaN(windDir)}
|
{#if windDir != null && !isNaN(windDir)}
|
||||||
<span
|
<span
|
||||||
class="absolute top-0 left-1/2 inline-block origin-center leading-0"
|
class="absolute top-0 left-1/2 inline-block origin-center leading-0"
|
||||||
@@ -673,11 +720,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Wind Gusts (off by default) -->
|
<!-- Wind Gusts (off by default) -->
|
||||||
{:else if rowKey === 'gusts' && showRow('gusts')}
|
{:else if rowKey === 'gusts' && showRow('gusts')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-strong-wind', windUnit, 'Gusts')}
|
{@render rowHeader('wi-strong-wind', windUnit, m.var_gusts_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const v = hourly.wind_gusts_10m?.[cell.idx]}
|
{@const v = hourly.wind_gusts_10m?.[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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'}"
|
||||||
@@ -688,24 +736,31 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Humidity (short row) -->
|
<!-- Humidity (short row) -->
|
||||||
{:else if rowKey === 'humidity' && showRow('humidity')}
|
{:else if rowKey === 'humidity' && showRow('humidity')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-humidity', '%', 'Humidity')}
|
{@render rowHeader('wi-humidity', '%', m.var_humidity())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const hum = hourly.relative_humidity_2m[cell.idx]}
|
{@const hum = hourly.relative_humidity_2m[cell.idx]}
|
||||||
<td class="cell h-12" style="background:{getHumidityBg(hum ?? 0)}">
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
|
class="cell h-12"
|
||||||
|
style={finite(hum) ? `background:${getHumidityBg(hum)}` : ''}
|
||||||
|
>
|
||||||
{formatValue(hum)}
|
{formatValue(hum)}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Cloud Cover -->
|
<!-- Cloud Cover -->
|
||||||
{:else if rowKey === 'clouds' && showRow('clouds')}
|
{:else if rowKey === 'clouds' && showRow('clouds')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-cloud', '%', 'Clouds')}
|
{@render rowHeader('wi-cloud', '%', m.var_cloud_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const cloud = hourly.cloud_cover[cell.idx]}
|
{@const cloud = hourly.cloud_cover[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
class="cell h-12"
|
class="cell h-12"
|
||||||
style="background:rgba(140,160,180,{getCloudOpacity(cloud ?? 0)})"
|
style={finite(cloud)
|
||||||
|
? `background:rgba(140,160,180,${getCloudOpacity(cloud)})`
|
||||||
|
: ''}
|
||||||
>
|
>
|
||||||
{formatValue(cloud)}
|
{formatValue(cloud)}
|
||||||
</td>
|
</td>
|
||||||
@@ -713,11 +768,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Pressure (off by default) -->
|
<!-- Pressure (off by default) -->
|
||||||
{:else if rowKey === 'pressure' && showRow('pressure')}
|
{:else if rowKey === 'pressure' && showRow('pressure')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-barometer', 'hPa', 'Pressure')}
|
{@render rowHeader('wi-barometer', 'hPa', m.var_pressure_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const v = hourly.pressure_msl?.[cell.idx]}
|
{@const v = hourly.pressure_msl?.[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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)}"
|
||||||
>
|
>
|
||||||
@@ -727,11 +783,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- UV Index (off by default) -->
|
<!-- UV Index (off by default) -->
|
||||||
{:else if rowKey === 'uv' && showRow('uv')}
|
{:else if rowKey === 'uv' && showRow('uv')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-day-sunny', 'UV', 'UV')}
|
{@render rowHeader('wi-day-sunny', 'UV', m.var_uv_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const v = hourly.uv_index?.[cell.idx]}
|
{@const v = hourly.uv_index?.[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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)}"
|
||||||
>
|
>
|
||||||
@@ -741,23 +798,27 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Visibility (off by default) -->
|
<!-- Visibility (off by default) -->
|
||||||
{:else if rowKey === 'visibility' && showRow('visibility')}
|
{:else if rowKey === 'visibility' && showRow('visibility')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-fog', 'km', 'Visibility')}
|
{@render rowHeader('wi-fog', 'km', m.var_visibility_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const v = hourly.visibility?.[cell.idx]}
|
{@const v = hourly.visibility?.[cell.idx]}
|
||||||
<td class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}">
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
|
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) : '-'}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Precipitation -->
|
<!-- Precipitation -->
|
||||||
{:else if rowKey === 'precipitation' && showRow('precipitation')}
|
{:else if rowKey === 'precipitation' && showRow('precipitation')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-raindrop', precipUnit, 'Precip')}
|
{@render rowHeader('wi-raindrop', precipUnit, m.var_precipitation_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const precip = hourly.precipitation[cell.idx]}
|
{@const precip = hourly.precipitation[cell.idx]}
|
||||||
{@const prob = hourly.precipitation_probability[cell.idx]}
|
{@const prob = hourly.precipitation_probability[cell.idx]}
|
||||||
<td
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
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)}
|
||||||
@@ -773,11 +834,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- Snowfall (off by default) -->
|
<!-- Snowfall (off by default) -->
|
||||||
{:else if rowKey === 'snowfall' && showRow('snowfall')}
|
{:else if rowKey === 'snowfall' && showRow('snowfall')}
|
||||||
<tr class="row">
|
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||||
{@render rowHeader('wi-snow', 'cm', 'Snow')}
|
{@render rowHeader('wi-snow', 'cm', m.var_snowfall_short())}
|
||||||
{#each cellData as cell (cell.idx)}
|
{#each cellData as cell (cell.idx)}
|
||||||
{@const v = hourly.snowfall?.[cell.idx]}
|
{@const v = hourly.snowfall?.[cell.idx]}
|
||||||
<td class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}">
|
<td
|
||||||
|
class:col-empty={!cell.hasData}
|
||||||
|
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) : '-'}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -853,6 +917,24 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A row the selected model carries no data for: legible, clearly inactive,
|
||||||
|
and not competing with the rows that do have values. */
|
||||||
|
.row-empty {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An individual cell past the point where the model stopped: drop the
|
||||||
|
day/night tinting so the gap reads as "no data" rather than clear sky. */
|
||||||
|
.cell-empty {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A whole column the model never reached - dimmed end to end, hour label
|
||||||
|
included, so the point where the forecast runs out is obvious. */
|
||||||
|
.col-empty {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Precipitation ──────────────────────────────────────────── */
|
/* ── Precipitation ──────────────────────────────────────────── */
|
||||||
.precip-cell {
|
.precip-cell {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
import { defaultVariablePrefs } from '$lib/stores/settings';
|
import { defaultVariablePrefs } from '$lib/stores/settings';
|
||||||
|
|
||||||
|
import * as m from '$lib/paraglide/messages';
|
||||||
|
|
||||||
import { getColor } from '../../utils/colors';
|
import { getColor } from '../../utils/colors';
|
||||||
import {
|
import {
|
||||||
type WeatherUnits,
|
type WeatherUnits,
|
||||||
@@ -69,8 +71,8 @@ export interface ChartVariableDef {
|
|||||||
export const CHART_VARIABLES: ChartVariableDef[] = [
|
export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||||
{
|
{
|
||||||
key: 'temperature',
|
key: 'temperature',
|
||||||
label: 'Temperature',
|
label: m.var_temperature(),
|
||||||
short: 'Temp',
|
short: m.var_temperature_short(),
|
||||||
field: 'temperature_2m',
|
field: 'temperature_2m',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'temp',
|
kind: 'temp',
|
||||||
@@ -83,8 +85,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'weather_icons',
|
key: 'weather_icons',
|
||||||
label: 'Weather icons',
|
label: m.var_icons(),
|
||||||
short: 'Icons',
|
short: m.var_icons_short(),
|
||||||
field: 'weather_code',
|
field: 'weather_code',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'temp',
|
kind: 'temp',
|
||||||
@@ -94,8 +96,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'apparent_temperature',
|
key: 'apparent_temperature',
|
||||||
label: 'Apparent Temp',
|
label: m.var_apparent(),
|
||||||
short: 'Feels',
|
short: m.var_apparent_short(),
|
||||||
field: 'apparent_temperature',
|
field: 'apparent_temperature',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'temp',
|
kind: 'temp',
|
||||||
@@ -105,8 +107,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'dew_point',
|
key: 'dew_point',
|
||||||
label: 'Dew Point',
|
label: m.var_dew_point(),
|
||||||
short: 'Dew',
|
short: m.var_dew_point_short(),
|
||||||
field: 'dew_point_2m',
|
field: 'dew_point_2m',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'temp',
|
kind: 'temp',
|
||||||
@@ -115,8 +117,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cloud_cover',
|
key: 'cloud_cover',
|
||||||
label: 'Cloud Cover',
|
label: m.var_cloud(),
|
||||||
short: 'Cloud',
|
short: m.var_cloud_short(),
|
||||||
field: 'cloud_cover',
|
field: 'cloud_cover',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'percent',
|
kind: 'percent',
|
||||||
@@ -128,8 +130,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
// the way the layers actually look from below.
|
// the way the layers actually look from below.
|
||||||
{
|
{
|
||||||
key: 'cloud_cover_low',
|
key: 'cloud_cover_low',
|
||||||
label: 'Cloud Cover Low',
|
label: m.var_cloud_low(),
|
||||||
short: 'Low',
|
short: m.var_cloud_low_short(),
|
||||||
field: 'cloud_cover_low',
|
field: 'cloud_cover_low',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'percent',
|
kind: 'percent',
|
||||||
@@ -139,8 +141,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cloud_cover_mid',
|
key: 'cloud_cover_mid',
|
||||||
label: 'Cloud Cover Mid',
|
label: m.var_cloud_mid(),
|
||||||
short: 'Mid',
|
short: m.var_cloud_mid_short(),
|
||||||
field: 'cloud_cover_mid',
|
field: 'cloud_cover_mid',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'percent',
|
kind: 'percent',
|
||||||
@@ -150,8 +152,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cloud_cover_high',
|
key: 'cloud_cover_high',
|
||||||
label: 'Cloud Cover High',
|
label: m.var_cloud_high(),
|
||||||
short: 'High',
|
short: m.var_cloud_high_short(),
|
||||||
field: 'cloud_cover_high',
|
field: 'cloud_cover_high',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'percent',
|
kind: 'percent',
|
||||||
@@ -161,8 +163,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'precipitation',
|
key: 'precipitation',
|
||||||
label: 'Precipitation',
|
label: m.var_precipitation(),
|
||||||
short: 'Precip',
|
short: m.var_precipitation_short(),
|
||||||
field: 'precipitation',
|
field: 'precipitation',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
kind: 'precip',
|
kind: 'precip',
|
||||||
@@ -170,8 +172,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'precipitation_probability',
|
key: 'precipitation_probability',
|
||||||
label: 'Precip. Probability',
|
label: m.var_pop(),
|
||||||
short: 'PoP',
|
short: m.var_pop_short(),
|
||||||
field: 'precipitation_probability',
|
field: 'precipitation_probability',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'percent',
|
kind: 'percent',
|
||||||
@@ -181,8 +183,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rain',
|
key: 'rain',
|
||||||
label: 'Rain',
|
label: m.var_rain(),
|
||||||
short: 'Rain',
|
short: m.var_rain_short(),
|
||||||
field: 'rain',
|
field: 'rain',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
kind: 'precip',
|
kind: 'precip',
|
||||||
@@ -190,8 +192,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'showers',
|
key: 'showers',
|
||||||
label: 'Showers',
|
label: m.var_showers(),
|
||||||
short: 'Shwr',
|
short: m.var_showers_short(),
|
||||||
field: 'showers',
|
field: 'showers',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
kind: 'precip',
|
kind: 'precip',
|
||||||
@@ -199,8 +201,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'snowfall',
|
key: 'snowfall',
|
||||||
label: 'Snowfall',
|
label: m.var_snowfall(),
|
||||||
short: 'Snow',
|
short: m.var_snowfall_short(),
|
||||||
field: 'snowfall',
|
field: 'snowfall',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
kind: 'snow',
|
kind: 'snow',
|
||||||
@@ -208,8 +210,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'wind',
|
key: 'wind',
|
||||||
label: 'Wind Speed',
|
label: m.var_wind(),
|
||||||
short: 'Wind',
|
short: m.var_wind_short(),
|
||||||
field: 'windspeed_10m',
|
field: 'windspeed_10m',
|
||||||
api: 'wind_speed_10m',
|
api: 'wind_speed_10m',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -221,8 +223,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'wind_direction',
|
key: 'wind_direction',
|
||||||
label: 'Wind direction',
|
label: m.var_wind_dir(),
|
||||||
short: 'Dir',
|
short: m.var_wind_dir_short(),
|
||||||
field: 'winddirection_10m',
|
field: 'winddirection_10m',
|
||||||
api: 'wind_direction_10m',
|
api: 'wind_direction_10m',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -233,8 +235,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'wind_gusts',
|
key: 'wind_gusts',
|
||||||
label: 'Wind Gusts',
|
label: m.var_gusts(),
|
||||||
short: 'Gusts',
|
short: m.var_gusts_short(),
|
||||||
field: 'wind_gusts_10m',
|
field: 'wind_gusts_10m',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'wind',
|
kind: 'wind',
|
||||||
@@ -244,8 +246,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'humidity',
|
key: 'humidity',
|
||||||
label: 'Humidity',
|
label: m.var_humidity(),
|
||||||
short: 'RH',
|
short: m.var_humidity_short(),
|
||||||
field: 'relative_humidity_2m',
|
field: 'relative_humidity_2m',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'percent',
|
kind: 'percent',
|
||||||
@@ -255,8 +257,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pressure_msl',
|
key: 'pressure_msl',
|
||||||
label: 'Pressure (MSL)',
|
label: m.var_pressure(),
|
||||||
short: 'MSLP',
|
short: m.var_pressure_short(),
|
||||||
field: 'pressure_msl',
|
field: 'pressure_msl',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'pressure',
|
kind: 'pressure',
|
||||||
@@ -265,8 +267,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'surface_pressure',
|
key: 'surface_pressure',
|
||||||
label: 'Surface Pressure',
|
label: m.var_surface_pressure(),
|
||||||
short: 'Psfc',
|
short: m.var_surface_pressure_short(),
|
||||||
field: 'surface_pressure',
|
field: 'surface_pressure',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'pressure',
|
kind: 'pressure',
|
||||||
@@ -276,8 +278,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'uv_index',
|
key: 'uv_index',
|
||||||
label: 'UV Index',
|
label: m.var_uv(),
|
||||||
short: 'UV',
|
short: m.var_uv_short(),
|
||||||
field: 'uv_index',
|
field: 'uv_index',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'uv',
|
kind: 'uv',
|
||||||
@@ -288,8 +290,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'visibility',
|
key: 'visibility',
|
||||||
label: 'Visibility',
|
label: m.var_visibility(),
|
||||||
short: 'Vis',
|
short: m.var_visibility_short(),
|
||||||
field: 'visibility',
|
field: 'visibility',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'distance',
|
kind: 'distance',
|
||||||
@@ -299,8 +301,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'cape',
|
key: 'cape',
|
||||||
label: 'CAPE',
|
label: m.var_cape(),
|
||||||
short: 'CAPE',
|
short: m.var_cape_short(),
|
||||||
field: 'cape',
|
field: 'cape',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
kind: 'energy',
|
kind: 'energy',
|
||||||
|
|||||||
Reference in New Issue
Block a user