diff --git a/assets/css/table.css b/assets/css/table.css index a793d18..3095254 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -710,6 +710,64 @@ border-left: 2px solid rgba(74, 50, 24, 0.18); } +/* ── COLUMN GROUP COLOURS — header cell tints per group ───────────────── + Each grp-* class gives the th a distinct background + text colour. + The hover state darkens slightly. The col-unit underneath inherits + a matching accent colour. */ + +.utci-table th.grp-felt { background: #dff3ec; color: #075c3a; } +.utci-table th.grp-surface { background: #ddeaf8; color: #0b3e72; } +.utci-table th.grp-ambient { background: #eceae3; color: #3c3a35; } +.utci-table th.grp-precip { background: #f7e4ee; color: #6b1e3a; } +.utci-table th.grp-sky { background: #eae8fc; color: #352e7a; } +.utci-table th.grp-wind { background: #fae6e0; color: #6a2210; } +.utci-table th.grp-airqual { background: #e5f0d8; color: #244408; } +.utci-table th.grp-solar { background: #faeacc; color: #5a3004; } + +.utci-table th.grp-felt.col-info-th:hover { background: #c8ead9; } +.utci-table th.grp-surface.col-info-th:hover { background: #c5d9f0; } +.utci-table th.grp-ambient.col-info-th:hover { background: #d8d6ce; } +.utci-table th.grp-precip.col-info-th:hover { background: #eecfdf; } +.utci-table th.grp-sky.col-info-th:hover { background: #d4d0f5; } +.utci-table th.grp-wind.col-info-th:hover { background: #f0cfc5; } +.utci-table th.grp-airqual.col-info-th:hover { background: #d0e4bc; } +.utci-table th.grp-solar.col-info-th:hover { background: #f0d5a8; } + +.utci-table th.grp-felt .col-unit { color: #0f9e63; } +.utci-table th.grp-surface .col-unit { color: #2672c8; } +.utci-table th.grp-ambient .col-unit { color: #7a7870; } +.utci-table th.grp-precip .col-unit { color: #b03060; } +.utci-table th.grp-sky .col-unit { color: #6058c8; } +.utci-table th.grp-wind .col-unit { color: #b04030; } +.utci-table th.grp-airqual .col-unit { color: #4a8018; } +.utci-table th.grp-solar .col-unit { color: #b07820; } + +/* ── GROUP LABEL ROW — spans above the column header row ──────────────── + A second tr in the thead. Each cell spans all visible columns in its + group. The Hour column gets a blank matching-background cell. */ + +.grp-label-row th { + font-family: Manrope, sans-serif; + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.10em; + padding: 4px 8px 2px; + text-align: center; + white-space: nowrap; + border-bottom: 1px solid rgba(0,0,0,0.06); +} + +.grp-label-hour { background: #f0e4c4; } +.grp-label-felt { background: #dff3ec; color: #075c3a; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-surface { background: #ddeaf8; color: #0b3e72; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-ambient { background: #eceae3; color: #3c3a35; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-precip { background: #f7e4ee; color: #6b1e3a; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-sky { background: #eae8fc; color: #352e7a; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-wind { background: #fae6e0; color: #6a2210; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-airqual { background: #e5f0d8; color: #244408; border-left: 2px solid rgba(74,50,24,0.18); } +.grp-label-solar { background: #faeacc; color: #5a3004; border-left: 2px solid rgba(74,50,24,0.18); } + /* ── FILTER PROFILES + COLUMN TOGGLES — mobile fixes ───────────────── */ @media (max-width: 640px) { diff --git a/assets/js/app.js b/assets/js/app.js index 7a56f71..70422dc 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -84,13 +84,14 @@ export function UTCIForecast() { // Columns that mark the start of a logical group - used to draw a faint // vertical border separating groups in the forecast table. const GROUP_ORDER = { - solar: ['sun', 'direct', 'diffuse', 'uvA', 'uvB', 'burn'], - sky: ['cloud', 'vis', 'aqi', 'pollen'], - wind: ['wind', 'dir'], + felt: ['utciP', 'vehicleT', 'indoorT', 'managedT', 'burn', 'utci', 'delta', 'tmrt'], + surface: ['concreteT', 'soilT', 'soilT6', 'soilM'], ambient: ['air', 'rh', 'dew'], - felt: ['tmrt', 'utci', 'delta', 'utciP'], - surface: ['concreteT', 'vehicleT', 'indoorT', 'managedT', 'soilT', 'soilT6', 'soilM'], - precip: ['precip'], + precip: ['precip', 'precipProb'], + sky: ['cloud', 'vis'], + wind: ['wind', 'dir'], + airqual: ['aqi', 'pollen'], + solar: ['uvA', 'uvB', 'sun', 'direct', 'diffuse'], }; const GROUP_OF = Object.fromEntries( Object.entries(GROUP_ORDER).flatMap(([g, keys]) => keys.map(k => [k, g])) @@ -109,6 +110,26 @@ export function UTCIForecast() { const first = GROUP_ORDER[group].find(isColVisible); return first === key ? 'col-group-start' : ''; }; + // Returns a CSS class encoding the group name - used to tint header cells + // and group label spans. + const groupColor = (key) => { + const g = GROUP_OF[key]; + return g ? `grp-${g}` : ''; + }; + // Builds the group label row above the column headers. + // Each visible group gets one spanning cell; groups with no visible + // columns are skipped entirely. Hour always gets a blank lead cell. + const groupLabelRow = () => { + const groups = Object.keys(GROUP_ORDER); + const cells = [html``]; + for (const g of groups) { + const span = GROUP_ORDER[g].filter(isColVisible).length; + if (span === 0) continue; + const labels = { felt: 'Felt', surface: 'Surface', ambient: 'Ambient', precip: 'Precip', sky: 'Sky', wind: 'Wind', airqual: 'Air quality', solar: 'Solar' }; + cells.push(html`${labels[g]}`); + } + return html`${cells}`; + }; // ─── 4. JSX RETURN ─────────────────────────────────────────────────── // Everything below is the actual page markup, written as one big HTM @@ -313,65 +334,7 @@ export function UTCIForecast() {
Columns: -${isPro && (activeProfile === 'custom' || activeCols['air']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['rh']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['dew']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['soilT']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['soilT6']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['soilM']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['concreteT']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['wind']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['dir']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['cloud']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['vis']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['aqi']) && html``} - ${(isPro ? (activeProfile === 'custom' || activeCols['pollen']) : activeCols['pollen']) && html`<${CustomSelect} - value=${pollenType} - isOn=${visibleCols.pollen} - hideLabel="Pollen" - hidingLabel="Hide Pollen" - options=${Object.entries(POLLEN_TYPES).flatMap(([k, v], i) => [ - { value: k, label: v.name }, - ...(i === 0 ? [{ value: '_div', divider: true }] : []), - ])} - onChange=${(v) => { - if (v === 'off') { - setVisibleCols(prev => ({ ...prev, pollen: false })); - } else { - setPollenTypeAndSave(v); - setVisibleCols(prev => ({ ...prev, pollen: true })); - } - }} - />`} - ${isPro && (activeProfile === 'custom' || activeCols['sun']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['direct']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['diffuse']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['tmrt']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['delta']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['utci']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['uvA']) && html``} - ${isPro && (activeProfile === 'custom' || activeCols['uvB']) && html``} - - ${(isPro ? (activeProfile === 'custom' || activeCols['burn']) : activeCols['burn']) && html` - <${CustomSelect} - value=${skinType} - isOn=${visibleCols.burn} - hideLabel="Burn" - hidingLabel="Hide Burn" - options=${Object.entries(SKIN_TYPES).map(([k, v]) => ({ - value: k, - label: v.name.split(' · ')[1] + ' skin', - }))} - onChange=${(v) => { - if (v === 'off') { - setVisibleCols(prev => ({ ...prev, burn: false })); - } else { - setSkinType(v); - setVisibleCols(prev => ({ ...prev, burn: true })); - } - }} - />`} - +${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html``} ${(isPro ? (activeProfile === 'custom' || activeCols['vehicleT']) : activeCols['vehicleT']) && html` <${CustomSelect} @@ -403,7 +366,6 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['delta']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['tmrt']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['concreteT']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['soilT']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['soilT6']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['soilM']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['air']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['rh']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['dew']) && html``} ${isPro && (activeProfile === 'custom' || activeCols['precip']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['precipProb']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['cloud']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['vis']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['wind']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['dir']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['aqi']) && html``} + ${(isPro ? (activeProfile === 'custom' || activeCols['pollen']) : activeCols['pollen']) && html`<${CustomSelect} + value=${pollenType} + isOn=${visibleCols.pollen} + hideLabel="Pollen" + hidingLabel="Hide Pollen" + options=${Object.entries(POLLEN_TYPES).flatMap(([k, v], i) => [ + { value: k, label: v.name }, + ...(i === 0 ? [{ value: '_div', divider: true }] : []), + ])} + onChange=${(v) => { + if (v === 'off') { + setVisibleCols(prev => ({ ...prev, pollen: false })); + } else { + setPollenTypeAndSave(v); + setVisibleCols(prev => ({ ...prev, pollen: true })); + } + }} + />`} + ${isPro && (activeProfile === 'custom' || activeCols['uvA']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['uvB']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['sun']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['direct']) && html``} + ${isPro && (activeProfile === 'custom' || activeCols['diffuse']) && html``}
`} @@ -450,36 +470,38 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`