Updated At A Glance - re-order and add category colour shades
Pets image
This commit is contained in:
fraxle
2026-07-09 20:16:10 +01:00
parent aee039ad61
commit fb8547c8b8
4 changed files with 68 additions and 20 deletions
+24
View File
@@ -1726,6 +1726,19 @@
padding-bottom: 0; padding-bottom: 0;
} }
/* Group-tinted rows remove the divider border since the background pill
provides the visual separation. */
.insight-row--felt,
.insight-row--surface,
.insight-row--ambient,
.insight-row--precip,
.insight-row--sky,
.insight-row--wind,
.insight-row--airqual,
.insight-row--solar {
border-bottom: none;
}
.insight-icon { .insight-icon {
width: 20px; width: 20px;
text-align: center; text-align: center;
@@ -1807,6 +1820,17 @@
font-weight: 700; font-weight: 700;
} }
/* Group-tinted backgrounds for insight rows — very light shade matching
the column group colour so items feel categorised at a glance. */
.insight-row--felt { background: rgba(223,243,236,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--surface { background: rgba(221,234,248,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--ambient { background: rgba(236,234,227,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--precip { background: rgba(247,228,238,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--sky { background: rgba(234,232,252,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--wind { background: rgba(250,230,224,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--airqual { background: rgba(229,240,216,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
.insight-row--solar { background: rgba(250,234,204,0.50); margin: 0 -10px; padding: 5px 10px; border-radius: 6px; }
/* ── Wide screens - the rail is narrow (240px), so stack each value /* ── Wide screens - the rail is narrow (240px), so stack each value
beneath its label instead of letting long values overflow sideways. beneath its label instead of letting long values overflow sideways.
Mobile keeps the inline label/value layout (plenty of width there). */ Mobile keeps the inline label/value layout (plenty of width there). */
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+5 -5
View File
@@ -1694,8 +1694,8 @@ export function UTCIForecast() {
${(() => { ${(() => {
const heatEvents = eventGlanceItems.filter(e => /heat caution|heat warning|extreme heat/i.test(e.label)).map(e => ({ ...e, alert: true })); const heatEvents = eventGlanceItems.filter(e => /heat caution|heat warning|extreme heat/i.test(e.label)).map(e => ({ ...e, alert: true }));
const otherEvents = eventGlanceItems.filter(e => !/heat caution|heat warning|extreme heat/i.test(e.label)); const otherEvents = eventGlanceItems.filter(e => !/heat caution|heat warning|extreme heat/i.test(e.label));
const renderRow = ({ icon, label, value, alert }, keyPrefix = '') => html` const renderRow = ({ icon, label, value, alert, grp }, keyPrefix = '') => html`
<div class=${`insight-row${alert ? ' insight-row--alert' : ''}`} key=${`${keyPrefix}${label}`}> <div class=${`insight-row${alert ? ' insight-row--alert' : ''}${grp ? ` insight-row--${grp}` : ''}`} key=${`${keyPrefix}${label}`}>
<span class="insight-icon">${icon}</span> <span class="insight-icon">${icon}</span>
<span class="insight-label">${label}</span> <span class="insight-label">${label}</span>
<span class="insight-value">${value}</span> <span class="insight-value">${value}</span>
@@ -1705,8 +1705,8 @@ export function UTCIForecast() {
const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp'); const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp');
const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null; const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null;
const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null; const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null;
const climateRows = hasFeltAnchor ? glanceSummary.filter(i => i.label === '1991-2020 High' || i.label === '1991-2020 Average') : []; const climateRows = glanceSummary.filter(i => i.label === '1991-2020 High' || i.label === '1991-2020 Average');
const restSummary = glanceSummary.filter(i => !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time' || i.label === '1991-2020 High' || i.label === '1991-2020 Average'))); const restSummary = glanceSummary.filter(i => i.label !== '1991-2020 High' && i.label !== '1991-2020 Average' && !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time')));
return [ return [
...restSummary.flatMap(item => [ ...restSummary.flatMap(item => [
renderRow(item), renderRow(item),
@@ -1715,11 +1715,11 @@ export function UTCIForecast() {
? [ ? [
...heatEvents.map(e => renderRow(e, 'ev-')), ...heatEvents.map(e => renderRow(e, 'ev-')),
...(drivingItem ? [renderRow(drivingItem)] : []), ...(drivingItem ? [renderRow(drivingItem)] : []),
...climateRows.map(item => renderRow(item)),
...(comfortItem ? [renderRow(comfortItem)] : []), ...(comfortItem ? [renderRow(comfortItem)] : []),
] ]
: []), : []),
]), ]),
...climateRows.map(item => renderRow(item)),
...otherEvents.map(e => renderRow(e, 'ev-')), ...otherEvents.map(e => renderRow(e, 'ev-')),
]; ];
})()} })()}
+37 -13
View File
@@ -479,6 +479,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Lightning risk', label: 'Lightning risk',
value: `${lightningRisk} at ${hhmm(peakLightningRow.iso)}`, value: `${lightningRisk} at ${hhmm(peakLightningRow.iso)}`,
alert: peakLightning >= 5, alert: peakLightning >= 5,
grp: 'precip',
}] : []; }] : [];
const moistureLabel = (v) => { const moistureLabel = (v) => {
@@ -513,22 +514,20 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
if (taVals.length === 0) return []; if (taVals.length === 0) return [];
const dayHigh = Math.max(...taVals); const dayHigh = Math.max(...taVals);
const dayMean = taVals.reduce((a, b) => a + b, 0) / taVals.length; const dayMean = taVals.reduce((a, b) => a + b, 0) / taVals.length;
const fmt = (anom) => {
const mag = Math.abs(anom);
return mag < 0.5
? 'About average'
: `${anom >= 0 ? '+' : ''}${mag.toFixed(1)}° ${anom >= 0 ? 'warmer' : 'cooler'}`;
};
const out = []; const out = [];
const nHigh = normals.high?.[doy]; const nHigh = normals.high?.[doy];
if (nHigh != null) { if (nHigh != null) {
const a = dayHigh - nHigh; const a = dayHigh - nHigh;
out.push({ icon: '🌡', label: '1991-2020 High', value: fmt(a), alert: a >= 5 }); if (Math.abs(a) >= 3) {
out.push({ icon: '🌡', label: '1991-2020 High', value: `${a >= 0 ? '+' : ''}${Math.abs(a).toFixed(1)}° ${a >= 0 ? 'warmer' : 'cooler'}`, alert: a >= 5, grp: 'ambient' });
}
} }
const nMean = normals.mean?.[doy]; const nMean = normals.mean?.[doy];
if (nMean != null) { if (nMean != null) {
const a = dayMean - nMean; const a = dayMean - nMean;
out.push({ icon: '🌡', label: '1991-2020 Average', value: fmt(a), alert: a >= 5 }); if (Math.abs(a) >= 3) {
out.push({ icon: '🌡', label: '1991-2020 Average', value: `${a >= 0 ? '+' : ''}${Math.abs(a).toFixed(1)}° ${a >= 0 ? 'warmer' : 'cooler'}`, alert: a >= 5, grp: 'ambient' });
}
} }
return out; return out;
})(); })();
@@ -557,6 +556,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Good driving time', label: 'Good driving time',
value: formatWindows(drivingWins) ?? 'Drive with care', value: formatWindows(drivingWins) ?? 'Drive with care',
alert: drivingWins.length === 0, alert: drivingWins.length === 0,
grp: 'felt',
}; };
// ── Farming ──────────────────────────────────────────────────────────── // ── Farming ────────────────────────────────────────────────────────────
@@ -575,12 +575,14 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Best field work', label: 'Best field work',
value: formatWindows(fieldWins) ?? 'No suitable window', value: formatWindows(fieldWins) ?? 'No suitable window',
alert: fieldWins.length === 0, alert: fieldWins.length === 0,
grp: 'surface',
}, },
...(show('soilT') ? [{ ...(show('soilT') ? [{
icon: '🌱', icon: '🌱',
label: 'Soil warms to 10°C by', label: 'Soil warms to 10°C by',
value: soilWarmRow ? (hhmm(soilWarmRow.iso) === '00:00' ? 'All Day' : hhmm(soilWarmRow.iso)) : 'Not today', value: soilWarmRow ? (hhmm(soilWarmRow.iso) === '00:00' ? 'All Day' : hhmm(soilWarmRow.iso)) : 'Not today',
alert: !soilWarmRow, alert: !soilWarmRow,
grp: 'surface',
}] : []), }] : []),
...(show('precipProb') ? [rainItem] : []), ...(show('precipProb') ? [rainItem] : []),
...lightningItem, ...lightningItem,
@@ -589,6 +591,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Soil moisture', label: 'Soil moisture',
value: moistureLabel(soilM) ?? '-', value: moistureLabel(soilM) ?? '-',
alert: soilM != null && (soilM < 0.10 || soilM > 0.50), alert: soilM != null && (soilM < 0.10 || soilM > 0.50),
grp: 'surface',
}] : []), }] : []),
...(weekDays ? computeCropAdvice(weekDays, lat) : []), ...(weekDays ? computeCropAdvice(weekDays, lat) : []),
...(show('pollen') && maxPollen >= 10 ? [{ ...(show('pollen') && maxPollen >= 10 ? [{
@@ -596,6 +599,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Grass pollen', label: 'Grass pollen',
value: pollenLabel(maxPollen), value: pollenLabel(maxPollen),
alert: maxPollen >= 50, alert: maxPollen >= 50,
grp: 'airqual',
}] : []), }] : []),
]; ];
} }
@@ -612,11 +616,13 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Peak cabin temp', label: 'Peak cabin temp',
value: peakCabin ? `${Math.round(peakCabin.vehicleT)}° at ${hhmm(peakCabin.iso)}` : '-', value: peakCabin ? `${Math.round(peakCabin.vehicleT)}° at ${hhmm(peakCabin.iso)}` : '-',
alert: !!(peakCabin && peakCabin.vehicleT > 31.5), alert: !!(peakCabin && peakCabin.vehicleT > 31.5),
grp: 'felt',
}, { }, {
icon: '🧒', icon: '🧒',
label: 'Children/pets in car', label: 'Children/pets in car',
value: dangerFrom ? `Unsafe from ${hhmm(dangerFrom.iso)}` : 'Safe all day', value: dangerFrom ? `Unsafe from ${hhmm(dangerFrom.iso)}` : 'Safe all day',
alert: !!dangerFrom, alert: !!dangerFrom,
grp: 'felt',
}] : []), }] : []),
...(show('precipProb') ? [rainItem] : []), ...(show('precipProb') ? [rainItem] : []),
...lightningItem, ...lightningItem,
@@ -630,6 +636,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
return best ? `${hhmm(best.iso)} (${Math.round(best.utciAdj)}° felt)` : '-'; return best ? `${hhmm(best.iso)} (${Math.round(best.utciAdj)}° felt)` : '-';
})(), })(),
alert: false, alert: false,
grp: 'felt',
}, },
]; ];
} }
@@ -651,18 +658,21 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Peak indoor (unmanaged)', label: 'Peak indoor (unmanaged)',
value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-', value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-',
alert: !!(peakIndoor && peakIndoor.indoorT >= 28), alert: !!(peakIndoor && peakIndoor.indoorT >= 28),
grp: 'felt',
}] : []), }] : []),
...(show('managedT') ? [{ ...(show('managedT') ? [{
icon: '🌡', icon: '🌡',
label: 'Peak indoor (managed)', label: 'Peak indoor (managed)',
value: peakManaged ? `${Math.round(peakManaged.managedT)}°` : '-', value: peakManaged ? `${Math.round(peakManaged.managedT)}°` : '-',
alert: !!(peakManaged && peakManaged.managedT >= 28), alert: !!(peakManaged && peakManaged.managedT >= 28),
grp: 'felt',
}] : []), }] : []),
...(show('indoorT') ? [{ ...(show('indoorT') ? [{
icon: '🪟', icon: '🪟',
label: 'Open windows', label: 'Open windows',
value: formatWindows(ventWins) ?? 'Keep closed', value: formatWindows(ventWins) ?? 'Keep closed',
alert: false, alert: false,
grp: 'felt',
}] : []), }] : []),
...lightningItem, ...lightningItem,
...(show('aqi') ? [{ ...(show('aqi') ? [{
@@ -676,6 +686,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Pollen', label: 'Pollen',
value: pollenLabel(maxPollen), value: pollenLabel(maxPollen),
alert: maxPollen >= 50, alert: maxPollen >= 50,
grp: 'airqual',
}] : []), }] : []),
]; ];
} }
@@ -695,24 +706,28 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Sunsoak for pet', label: 'Sunsoak for pet',
value: peakFur ? `${Math.round(peakFur.furSurfaceT)}° at ${hhmm(peakFur.iso)}` : '-', value: peakFur ? `${Math.round(peakFur.furSurfaceT)}° at ${hhmm(peakFur.iso)}` : '-',
alert: !!(peakFur && peakFur.furSurfaceT >= 45), alert: !!(peakFur && peakFur.furSurfaceT >= 45),
grp: 'surface',
}] : []), }] : []),
...(show('petShadeT') ? [{ ...(show('petShadeT') ? [{
icon: '🌳', icon: '🌳',
label: 'Shade for pet', label: 'Shade for pet',
value: peakShade ? `${Math.round(peakShade.shadeT)}° at ${hhmm(peakShade.iso)}` : '-', value: peakShade ? `${Math.round(peakShade.shadeT)}° at ${hhmm(peakShade.iso)}` : '-',
alert: !!(peakShade && peakShade.shadeT >= 26), alert: !!(peakShade && peakShade.shadeT >= 26),
grp: 'ambient',
}] : []), }] : []),
...(show('petHomeT') ? [{ ...(show('petHomeT') ? [{
icon: '🏠', icon: '🏠',
label: 'Pet at home', label: 'Pet at home',
value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-', value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-',
alert: !!(peakIndoor && peakIndoor.indoorT >= 26), alert: !!(peakIndoor && peakIndoor.indoorT >= 26),
grp: 'felt',
}] : []), }] : []),
...(show('vehicleT') ? [{ ...(show('vehicleT') ? [{
icon: '🧒', icon: '🧒',
label: 'Kids/pets in car', label: 'Kids/pets in car',
value: carDanger ? `Unsafe from ${hhmm(carDanger.iso)}` : 'Safe all day', value: carDanger ? `Unsafe from ${hhmm(carDanger.iso)}` : 'Safe all day',
alert: !!carDanger, alert: !!carDanger,
grp: 'felt',
}] : []), }] : []),
...lightningItem, ...lightningItem,
...(show('aqi') ? [{ ...(show('aqi') ? [{
@@ -720,12 +735,14 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Air quality', label: 'Air quality',
value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
alert: peakAqi >= 60, alert: peakAqi >= 60,
grp: 'airqual',
}] : []), }] : []),
...(show('pollen') && maxPollen >= 10 ? [{ ...(show('pollen') && maxPollen >= 10 ? [{
icon: '🌼', icon: '🌼',
label: 'Pollen', label: 'Pollen',
value: pollenLabel(maxPollen), value: pollenLabel(maxPollen),
alert: maxPollen >= 50, alert: maxPollen >= 50,
grp: 'airqual',
}] : []), }] : []),
]; ];
} }
@@ -749,6 +766,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: `Best ${variant} window`, label: `Best ${variant} window`,
value: formatWindows(coolWins) ?? 'No cool window today', value: formatWindows(coolWins) ?? 'No cool window today',
alert: coolWins.length === 0, alert: coolWins.length === 0,
grp: 'felt',
}, },
...(show('precipProb') ? [rainItem] : []), ...(show('precipProb') ? [rainItem] : []),
...lightningItem, ...lightningItem,
@@ -757,6 +775,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'UV burn time (peak)', label: 'UV burn time (peak)',
value: burnMins, value: burnMins,
alert: !!(peakUvRow && peakUvRow.uv >= 6), alert: !!(peakUvRow && peakUvRow.uv >= 6),
grp: 'solar',
}] : []), }] : []),
...(show('aqi') ? [{ ...(show('aqi') ? [{
icon: '💨', icon: '💨',
@@ -793,18 +812,21 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
label: 'Comfortable window', label: 'Comfortable window',
value: formatWindows(comfortWins) ?? 'No comfortable window', value: formatWindows(comfortWins) ?? 'No comfortable window',
alert: comfortWins.length === 0, alert: comfortWins.length === 0,
grp: 'felt',
}, },
{ {
icon: '🌡', icon: '🌡',
label: 'Peak felt temp', label: 'Peak felt temp',
value: peakFelt ? `${Math.round(peakFelt.utciAdj)}° at ${hhmm(peakFelt.iso)}` : '-', value: peakFelt ? `${Math.round(peakFelt.utciAdj)}° at ${hhmm(peakFelt.iso)}` : '-',
alert: !!(peakFelt && (peakFelt.utciAdj >= 32 || peakFelt.utciAdj < 0)), alert: !!(peakFelt && (peakFelt.utciAdj >= 32 || peakFelt.utciAdj < 0)),
grp: 'felt',
}, },
...((show('burn') || show('uvA')) && burnMins ? [{ ...((show('burn') || show('uvA')) && burnMins ? [{
icon: '☀', icon: '☀',
label: 'UV burn time (peak)', label: 'UV burn time (peak)',
value: burnMins, value: burnMins,
alert: !!(peakUvRow && peakUvRow.uv >= 6), alert: !!(peakUvRow && peakUvRow.uv >= 6),
grp: 'solar',
}] : []), }] : []),
...(show('precipProb') ? [rainItem] : []), ...(show('precipProb') ? [rainItem] : []),
...lightningItem, ...lightningItem,
@@ -812,13 +834,15 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
icon: '💨', icon: '💨',
label: 'Air quality', label: 'Air quality',
value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
alert: peakAqi >= 60, alert: peakAqi >= 60,
grp: 'airqual',
}] : []), }] : []),
...(show('pollen') && maxPollen >= 10 ? [{ ...(show('pollen') && maxPollen >= 10 ? [{
icon: '🌿', icon: '🌿',
label: 'Pollen', label: 'Pollen',
value: pollenLabel(maxPollen), value: pollenLabel(maxPollen),
alert: maxPollen >= 50, alert: maxPollen >= 50,
grp: 'airqual',
}] : []), }] : []),
]; ];
} }
@@ -894,11 +918,11 @@ export function computeCropAdvice(weekDays, lat) {
const ready = sowSeason.filter(c => const ready = sowSeason.filter(c =>
weekSoilT != null && weekSoilT >= c.sow.minSoilT && !groundWet); weekSoilT != null && weekSoilT >= c.sow.minSoilT && !groundWet);
out.push(ready.length ? { out.push(ready.length ? {
icon: '🌱', label: 'Good to sow', value: listCrops(ready), alert: false, icon: '🌱', label: 'Good to sow', value: listCrops(ready), alert: false, grp: 'surface',
} : { } : {
icon: '🌱', label: 'Good to sow', icon: '🌱', label: 'Good to sow',
value: groundWet ? 'Hold off — ground too wet' : 'Hold off — soil still cold', value: groundWet ? 'Hold off — ground too wet' : 'Hold off — soil still cold',
alert: true, alert: true, grp: 'surface',
}); });
} }
@@ -911,10 +935,10 @@ export function computeCropAdvice(weekDays, lat) {
out.push(ready.length ? { out.push(ready.length ? {
icon: '🌾', label: 'Good to harvest', icon: '🌾', label: 'Good to harvest',
value: dryDriven ? `${listCrops(ready)} (${dryRange()})` : listCrops(ready), value: dryDriven ? `${listCrops(ready)} (${dryRange()})` : listCrops(ready),
alert: false, alert: false, grp: 'surface',
} : { } : {
icon: '🌾', label: 'Good to harvest', icon: '🌾', label: 'Good to harvest',
value: 'Hold — too wet to harvest grain', alert: true, value: 'Hold — too wet to harvest grain', alert: true, grp: 'surface',
}); });
} }