diff --git a/assets/css/table.css b/assets/css/table.css index 8a384cb..58468b8 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -420,6 +420,14 @@ white-space: nowrap; } +.col-toggles-edit-btn { + display: none; +} + +.col-toggles-body { + display: contents; +} + /* The little pill buttons */ .col-toggle { padding: 5px 13px; @@ -1132,9 +1140,44 @@ } .col-toggles-label { + display: none; + } + + .col-toggles-edit-btn { + display: flex; + align-items: center; + justify-content: space-between; flex: 0 0 100%; - margin-right: 0; - margin-bottom: 0; + padding: 8px 12px; + background: #f5edd6; + border: 1.5px solid #c9b08a; + color: #7a5c2a; + font-family: Manrope, sans-serif; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: pointer; + text-align: left; + } + + .col-toggles-edit-btn-icon { + width: 12px; + height: 12px; + flex-shrink: 0; + transition: transform 0.2s ease; + } + + .col-toggles-edit-btn--open .col-toggles-edit-btn-icon { + transform: rotate(180deg); + } + + .col-toggles-body { + display: none; + } + + .col-toggles-body--open { + display: contents; } .col-toggle { diff --git a/assets/js/app.js b/assets/js/app.js index 578736f..883240e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -131,6 +131,7 @@ export function UTCIForecast() { // location. The input then overlays the location line to save space. const [searchOpen, setSearchOpen] = useState(false); const searchWrapRef = useRef(null); + const [colTogglesOpen, setColTogglesOpen] = useState(false); const searchInputRef = useRef(null); // Focus the input the moment the search opens. @@ -290,6 +291,7 @@ export function UTCIForecast() { activeProfile, outdoorsVariant, skinType, + visibleCols, ); // Human-readable date for the "Day at a glance" heading, e.g. "Mon 2nd June 2026". @@ -629,6 +631,11 @@ export function UTCIForecast() {
Columns: + +
${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['utciP']) : activeCols['utciP']) && html` <${CustomSelect} @@ -791,6 +798,7 @@ export function UTCIForecast() { Units +
@@ -1013,9 +1021,11 @@ export function UTCIForecast() { ${visibleCols.precip && html` 0 ? scaleBg(r.snow, 0, 4, '90,140,210') : scaleBg(r.precip, 0, 8, '70,145,200') }}> - <${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} /> + <${PrecipIcon} precip=${r.precipProb > 0 ? r.precip : 0} snow=${r.precipProb > 0 ? r.snow : 0} size=${28} /> 0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}> - ${r.snow > 0 ? fmt(r.snow) + u('cm') : r.precip > 0 ? fmt(r.precip) + u('mm') : '—'} + ${r.precipProb > 0 + ? (r.snow > 0 ? fmt(r.snow) + u('cm') : r.precip > 0 ? fmt(r.precip) + u('mm') : '—') + : '—'} `} diff --git a/assets/js/compute.js b/assets/js/compute.js index a9f4099..c76f30e 100644 --- a/assets/js/compute.js +++ b/assets/js/compute.js @@ -363,8 +363,9 @@ export function computeWhyFeelsLike(row, env) { // variant - active sub-variant key e.g. "running", "beach" (or null) // skinType - Fitzpatrick skin type key for UV burn time // ------------------------------------------------------------------------ -export function computeGlanceSummary(todayRows, profile, variant, skinType) { +export function computeGlanceSummary(todayRows, profile, variant, skinType, cols = null) { if (!todayRows || todayRows.length === 0) return []; + const show = (key) => !cols || !!cols[key]; const hhmm = (iso) => iso ? iso.slice(11, 16) : null; const dayRows = todayRows.filter(r => r.elev > 0); @@ -437,20 +438,20 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType) { : 'No suitable window', alert: !fieldWindow, }, - { + ...(show('soilT') ? [{ icon: '🌱', label: 'Soil warms to 10°C by', value: soilWarmRow ? hhmm(soilWarmRow.iso) : 'Not today', alert: !soilWarmRow, - }, - rainItem, - { + }] : []), + ...(show('precipProb') ? [rainItem] : []), + ...(show('soilM') ? [{ icon: '💧', label: 'Soil moisture', value: moistureLabel(soilM) ?? '-', alert: soilM != null && (soilM < 0.10 || soilM > 0.50), - }, - ...(maxPollen >= 10 ? [{ + }] : []), + ...(show('pollen') && maxPollen >= 10 ? [{ icon: '🌿', label: 'Grass pollen', value: pollenLabel(maxPollen), @@ -465,19 +466,18 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType) { const dangerFrom = todayRows.find(r => r.vehicleT != null && r.vehicleT >= 35); return [ - { + ...(show('vehicleT') ? [{ icon: '🌡', label: 'Peak cabin temp', value: peakCabin ? `${Math.round(peakCabin.vehicleT)}° at ${hhmm(peakCabin.iso)}` : '-', alert: !!(peakCabin && peakCabin.vehicleT >= 35), - }, - { + }, { icon: '🧒', label: 'Children/pets in car', value: dangerFrom ? `Unsafe from ${hhmm(dangerFrom.iso)}` : 'Safe all day', alert: !!dangerFrom, - }, - rainItem, + }] : []), + ...(show('precipProb') ? [rainItem] : []), { icon: '🌤', label: 'Best travel comfort', @@ -502,33 +502,33 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType) { const maxPollen = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0)); return [ - { + ...(show('indoorT') ? [{ icon: '🌡', label: 'Peak indoor (unmanaged)', value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-', alert: !!(peakIndoor && peakIndoor.indoorT >= 28), - }, - { + }] : []), + ...(show('managedT') ? [{ icon: '🌡', label: 'Peak indoor (managed)', value: peakManaged ? `${Math.round(peakManaged.managedT)}°` : '-', alert: !!(peakManaged && peakManaged.managedT >= 28), - }, - { + }] : []), + ...(show('indoorT') ? [{ icon: '🪟', label: 'Open windows', value: ventWindow ? `${hhmm(ventWindow.start.iso)} – ${hhmm(ventWindow.end.iso)}` : 'Keep closed', alert: false, - }, - { + }] : []), + ...(show('aqi') ? [{ icon: '💨', label: 'Air quality', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', alert: peakAqi >= 60, - }, - ...(maxPollen >= 10 ? [{ + }] : []), + ...(show('pollen') && maxPollen >= 10 ? [{ icon: '🌼', label: 'Pollen', value: pollenLabel(maxPollen), @@ -558,20 +558,20 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType) { : 'No cool window today', alert: !coolWindow, }, - rainItem, - ...(burnMins ? [{ + ...(show('precipProb') ? [rainItem] : []), + ...(show('burn') && burnMins ? [{ icon: '☀', label: 'UV burn time (peak)', value: burnMins, alert: !!(peakUvRow && peakUvRow.uv >= 6), }] : []), - { + ...(show('aqi') ? [{ icon: '💨', label: 'Air quality', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', alert: peakAqi >= 60, - }, - ...(maxPollen >= 10 ? [{ + }] : []), + ...(show('pollen') && maxPollen >= 10 ? [{ icon: '🌿', label: 'Pollen', value: pollenLabel(maxPollen), @@ -587,6 +587,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType) { const peakFelt = peakRow('utciAdj'); const peakUvRow = peakRow('uv'); const peakAqi = Math.max(0, ...todayRows.map(r => r.aqi ?? 0)); + const maxPollen = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0)); const burnMins = peakUvRow && peakUvRow.uv > 0 ? burnLabel(sunburnMinutes(peakUvRow.uv, skinType)) : null; @@ -606,18 +607,24 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType) { value: peakFelt ? `${Math.round(peakFelt.utciAdj)}° at ${hhmm(peakFelt.iso)}` : '-', alert: !!(peakFelt && (peakFelt.utciAdj >= 32 || peakFelt.utciAdj < 0)), }, - ...(burnMins ? [{ + ...((show('burn') || show('uvA')) && burnMins ? [{ icon: '☀', label: 'UV burn time (peak)', value: burnMins, alert: !!(peakUvRow && peakUvRow.uv >= 6), }] : []), - rainItem, - { + ...(show('precipProb') ? [rainItem] : []), + ...(show('aqi') ? [{ icon: '💨', label: 'Air quality', value: peakAqi > 0 ? aqiLabel(peakAqi) : '-', alert: peakAqi >= 60, - }, + }] : []), + ...(show('pollen') && maxPollen >= 10 ? [{ + icon: '🌿', + label: 'Pollen', + value: pollenLabel(maxPollen), + alert: maxPollen >= 50, + }] : []), ]; } \ No newline at end of file diff --git a/assets/js/config.js b/assets/js/config.js index 6ea8f74..57f30c0 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -29,7 +29,7 @@ export const FILTER_PROFILES = { label: 'Basic', icon: '🌡️', scene: "url('assets/images/profiles/basic.png') center / cover no-repeat, linear-gradient(160deg,#b8d8e8,#daeef8)", - cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false }, + cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true, rh: true }, }, home: { label: 'Home', diff --git a/build.js b/build.js index c637375..621a13d 100644 --- a/build.js +++ b/build.js @@ -83,7 +83,7 @@ function rewriteHTML(html, version) { html = html.replace(/]*src=["'][^"']*assets\/js\/[^"']*["'][^>]*><\/script>/gi, ''); // Rewrite standalone about.css reference to versioned minified file. html = html.replace( - /(]*href=["'][^"']*assets\/)about\.css(["'][^>]*>)/gi, + /(]*href=["'][^"']*assets\/)about\.css[^"']*(["'][^>]*>)/gi, '$1about.min.css?v=' + version + '$2' ); const css = ' '; diff --git a/data/tracking.json b/data/tracking.json index b26d1fe..81f1e40 100644 --- a/data/tracking.json +++ b/data/tracking.json @@ -4,5 +4,17 @@ "profiles": { "showall": 1 } + }, + "2026-06-08": { + "visits": 1, + "profiles": [] + }, + "2026-06-10": { + "visits": 9, + "profiles": { + "basic": 5, + "home": 2, + "vehicle": 1 + } } } \ No newline at end of file