diff --git a/assets/css/ui.css b/assets/css/ui.css index ff34ce3..35d8a3d 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -1726,6 +1726,19 @@ 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 { width: 20px; text-align: center; @@ -1807,6 +1820,17 @@ 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 beneath its label instead of letting long values overflow sideways. Mobile keeps the inline label/value layout (plenty of width there). */ diff --git a/assets/images/profiles/pets.png b/assets/images/profiles/pets.png new file mode 100644 index 0000000..fb38455 Binary files /dev/null and b/assets/images/profiles/pets.png differ diff --git a/assets/js/app.js b/assets/js/app.js index d5b9cb9..a5a9bbf 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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 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` +
${icon} ${label} ${value} @@ -1705,8 +1705,8 @@ export function UTCIForecast() { const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp'); const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : 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 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 climateRows = glanceSummary.filter(i => 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 [ ...restSummary.flatMap(item => [ renderRow(item), @@ -1715,11 +1715,11 @@ export function UTCIForecast() { ? [ ...heatEvents.map(e => renderRow(e, 'ev-')), ...(drivingItem ? [renderRow(drivingItem)] : []), - ...climateRows.map(item => renderRow(item)), ...(comfortItem ? [renderRow(comfortItem)] : []), ] : []), ]), + ...climateRows.map(item => renderRow(item)), ...otherEvents.map(e => renderRow(e, 'ev-')), ]; })()} diff --git a/assets/js/compute.js b/assets/js/compute.js index fd28ea6..316c2bb 100644 --- a/assets/js/compute.js +++ b/assets/js/compute.js @@ -479,6 +479,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Lightning risk', value: `${lightningRisk} at ${hhmm(peakLightningRow.iso)}`, alert: peakLightning >= 5, + grp: 'precip', }] : []; const moistureLabel = (v) => { @@ -513,22 +514,20 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols if (taVals.length === 0) return []; const dayHigh = Math.max(...taVals); 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 nHigh = normals.high?.[doy]; if (nHigh != null) { 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]; if (nMean != null) { 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; })(); @@ -557,6 +556,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Good driving time', value: formatWindows(drivingWins) ?? 'Drive with care', alert: drivingWins.length === 0, + grp: 'felt', }; // ── Farming ──────────────────────────────────────────────────────────── @@ -575,12 +575,14 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Best field work', value: formatWindows(fieldWins) ?? 'No suitable window', alert: fieldWins.length === 0, + grp: 'surface', }, ...(show('soilT') ? [{ icon: '🌱', label: 'Soil warms to 10°C by', value: soilWarmRow ? (hhmm(soilWarmRow.iso) === '00:00' ? 'All Day' : hhmm(soilWarmRow.iso)) : 'Not today', alert: !soilWarmRow, + grp: 'surface', }] : []), ...(show('precipProb') ? [rainItem] : []), ...lightningItem, @@ -589,6 +591,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Soil moisture', value: moistureLabel(soilM) ?? '-', alert: soilM != null && (soilM < 0.10 || soilM > 0.50), + grp: 'surface', }] : []), ...(weekDays ? computeCropAdvice(weekDays, lat) : []), ...(show('pollen') && maxPollen >= 10 ? [{ @@ -596,6 +599,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Grass pollen', value: pollenLabel(maxPollen), alert: maxPollen >= 50, + grp: 'airqual', }] : []), ]; } @@ -612,11 +616,13 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Peak cabin temp', value: peakCabin ? `${Math.round(peakCabin.vehicleT)}° at ${hhmm(peakCabin.iso)}` : '-', alert: !!(peakCabin && peakCabin.vehicleT > 31.5), + grp: 'felt', }, { icon: '🧒', label: 'Children/pets in car', value: dangerFrom ? `Unsafe from ${hhmm(dangerFrom.iso)}` : 'Safe all day', alert: !!dangerFrom, + grp: 'felt', }] : []), ...(show('precipProb') ? [rainItem] : []), ...lightningItem, @@ -630,6 +636,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols return best ? `${hhmm(best.iso)} (${Math.round(best.utciAdj)}° felt)` : '-'; })(), alert: false, + grp: 'felt', }, ]; } @@ -651,18 +658,21 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Peak indoor (unmanaged)', value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-', alert: !!(peakIndoor && peakIndoor.indoorT >= 28), + grp: 'felt', }] : []), ...(show('managedT') ? [{ icon: '🌡', label: 'Peak indoor (managed)', value: peakManaged ? `${Math.round(peakManaged.managedT)}°` : '-', alert: !!(peakManaged && peakManaged.managedT >= 28), + grp: 'felt', }] : []), ...(show('indoorT') ? [{ icon: '🪟', label: 'Open windows', value: formatWindows(ventWins) ?? 'Keep closed', alert: false, + grp: 'felt', }] : []), ...lightningItem, ...(show('aqi') ? [{ @@ -676,6 +686,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Pollen', value: pollenLabel(maxPollen), alert: maxPollen >= 50, + grp: 'airqual', }] : []), ]; } @@ -695,24 +706,28 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Sunsoak for pet', value: peakFur ? `${Math.round(peakFur.furSurfaceT)}° at ${hhmm(peakFur.iso)}` : '-', alert: !!(peakFur && peakFur.furSurfaceT >= 45), + grp: 'surface', }] : []), ...(show('petShadeT') ? [{ icon: '🌳', label: 'Shade for pet', value: peakShade ? `${Math.round(peakShade.shadeT)}° at ${hhmm(peakShade.iso)}` : '-', alert: !!(peakShade && peakShade.shadeT >= 26), + grp: 'ambient', }] : []), ...(show('petHomeT') ? [{ icon: '🏠', label: 'Pet at home', value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-', alert: !!(peakIndoor && peakIndoor.indoorT >= 26), + grp: 'felt', }] : []), ...(show('vehicleT') ? [{ icon: '🧒', label: 'Kids/pets in car', value: carDanger ? `Unsafe from ${hhmm(carDanger.iso)}` : 'Safe all day', alert: !!carDanger, + grp: 'felt', }] : []), ...lightningItem, ...(show('aqi') ? [{ @@ -720,12 +735,14 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Air quality', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', alert: peakAqi >= 60, + grp: 'airqual', }] : []), ...(show('pollen') && maxPollen >= 10 ? [{ icon: '🌼', label: 'Pollen', value: pollenLabel(maxPollen), alert: maxPollen >= 50, + grp: 'airqual', }] : []), ]; } @@ -749,6 +766,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: `Best ${variant} window`, value: formatWindows(coolWins) ?? 'No cool window today', alert: coolWins.length === 0, + grp: 'felt', }, ...(show('precipProb') ? [rainItem] : []), ...lightningItem, @@ -757,6 +775,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'UV burn time (peak)', value: burnMins, alert: !!(peakUvRow && peakUvRow.uv >= 6), + grp: 'solar', }] : []), ...(show('aqi') ? [{ icon: '💨', @@ -793,18 +812,21 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Comfortable window', value: formatWindows(comfortWins) ?? 'No comfortable window', alert: comfortWins.length === 0, + grp: 'felt', }, { icon: '🌡', label: 'Peak felt temp', value: peakFelt ? `${Math.round(peakFelt.utciAdj)}° at ${hhmm(peakFelt.iso)}` : '-', alert: !!(peakFelt && (peakFelt.utciAdj >= 32 || peakFelt.utciAdj < 0)), + grp: 'felt', }, ...((show('burn') || show('uvA')) && burnMins ? [{ icon: '☀', label: 'UV burn time (peak)', value: burnMins, alert: !!(peakUvRow && peakUvRow.uv >= 6), + grp: 'solar', }] : []), ...(show('precipProb') ? [rainItem] : []), ...lightningItem, @@ -812,13 +834,15 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols icon: '💨', label: 'Air quality', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', - alert: peakAqi >= 60, - }] : []), - ...(show('pollen') && maxPollen >= 10 ? [{ +alert: peakAqi >= 60, + grp: 'airqual', + }] : []), + ...(show('pollen') && maxPollen >= 10 ? [{ icon: '🌿', label: 'Pollen', value: pollenLabel(maxPollen), alert: maxPollen >= 50, + grp: 'airqual', }] : []), ]; } @@ -894,11 +918,11 @@ export function computeCropAdvice(weekDays, lat) { const ready = sowSeason.filter(c => weekSoilT != null && weekSoilT >= c.sow.minSoilT && !groundWet); 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', 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 ? { icon: '🌾', label: 'Good to harvest', value: dryDriven ? `${listCrops(ready)} (${dryRange()})` : listCrops(ready), - alert: false, + alert: false, grp: 'surface', } : { 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', }); }