diff --git a/assets/css/header.css b/assets/css/header.css index ba809ca..bcc121b 100644 --- a/assets/css/header.css +++ b/assets/css/header.css @@ -7,21 +7,15 @@ .utci-header { display: grid; grid-template-columns: 1fr auto 1fr; /* left | dial | why */ - grid-template-areas: - "hleft dial hright" - ". env ."; + grid-template-areas: "hleft dial hright"; column-gap: 32px; - row-gap: 6px; align-items: center; padding-bottom: 6px; margin-bottom: 6px; } -/* Side columns occupy only the dial's row, so they vertically centre against - the dial itself; the environment selector drops to its own row beneath. */ .header-left { grid-area: hleft; } .scope-col { grid-area: dial; } -.scope-env-bar { grid-area: env; } .header-right { grid-area: hright; } /* Left column — wordmark, tagline, search and location stacked together */ @@ -276,10 +270,15 @@ } .scope-env-row .cs-panel, -.scope-env-row .cs-option { +.scope-env-row .cs-option, +.insight-env-footer .cs-panel, +.insight-env-footer .cs-option { text-align: center; } +.insight-env-footer .cs-btn.on:hover .cs-btn-overlay { opacity: 1; } +.insight-env-footer .cs-btn .cs-btn-label { position: relative; z-index: 1; } + /* Per-environment gradient themes — !important keeps the gradient fixed under the brass hover overlay (ui.css :hover rules load later). */ .scope-env-row[data-env="open"] .cs-btn.on { background: linear-gradient(135deg, #5a8a28 0%, #b8d860 100%) !important; border-color: #4a7818 !important; color: #fff; } @@ -291,8 +290,19 @@ .scope-env-row[data-env="openwater"] .cs-btn.on { background: linear-gradient(135deg, #0c4878 0%, #2898d8 100%) !important; border-color: #0c4878 !important; color: #fff; } .scope-env-row[data-env="urban"] .cs-btn.on { background: linear-gradient(135deg, #4a5568 0%, #a89880 100%) !important; border-color: #3a4455 !important; color: #fff; } +/* Duplicate gradient rules for the Solar model button inside the insight panel */ +.insight-env-footer[data-env="open"] .cs-btn.on { background: linear-gradient(135deg, #5a8a28 0%, #b8d860 100%) !important; border-color: #4a7818 !important; color: #fff; } +.insight-env-footer[data-env="forest"] .cs-btn.on { background: linear-gradient(135deg, #1b5e3b 0%, #57bf8a 100%) !important; border-color: #1b5e3b !important; color: #fff; } +.insight-env-footer[data-env="alpine"] .cs-btn.on { background: linear-gradient(135deg, #a8cfe8 0%, #dff0fa 100%) !important; border-color: #6aaedd !important; color: #1a3a58; } +.insight-env-footer[data-env="beach"] .cs-btn.on { background: linear-gradient(135deg, #2a8fc8 0%, #e8d87a 100%) !important; border-color: #1e78b0 !important; color: #fff; } +.insight-env-footer[data-env="river"] .cs-btn.on { background: linear-gradient(135deg, #2e6e5e 0%, #78c4a8 100%) !important; border-color: #2e6e5e !important; color: #fff; } +.insight-env-footer[data-env="desert"] .cs-btn.on { background: linear-gradient(135deg, #b86820 0%, #e8c458 100%) !important; border-color: #a05818 !important; color: #fff; } +.insight-env-footer[data-env="openwater"] .cs-btn.on { background: linear-gradient(135deg, #0c4878 0%, #2898d8 100%) !important; border-color: #0c4878 !important; color: #fff; } +.insight-env-footer[data-env="urban"] .cs-btn.on { background: linear-gradient(135deg, #4a5568 0%, #a89880 100%) !important; border-color: #3a4455 !important; color: #fff; } + /* On hover the brass overlay covers the interior — force white text for contrast */ .scope-env-row .cs-btn.on:hover { color: #fff; } +.insight-env-footer .cs-btn.on:hover { color: #fff; } /* The search box on the right of the header */ .utci-search-wrap { diff --git a/assets/css/table.css b/assets/css/table.css index 95787ad..37b2678 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -11,7 +11,7 @@ /* Wrapper that hosts the scrollable strip, edge fades, and chevrons. */ .utci-day-tabs-wrap { position: relative; - margin-top: 14px; + margin-top: 12px; } /* Edge fade gradients — only show when there's content to scroll to. */ @@ -132,7 +132,7 @@ flex-direction: column; gap: 0; margin-bottom: 0; - padding: 0 14px 0; + padding: 0px; background: transparent; border: none; } @@ -160,8 +160,8 @@ justify-content: safe center; /* fall back to flex-start if it overflows */ overflow-x: auto; overflow-y: auto; /* forced by browser when overflow-x is non-visible */ - padding-bottom: 400px; /* extends clip boundary so the tallest dropdown (Activities) isn't cut off */ - margin-bottom: -400px; /* cancel that extra space in the layout */ + padding-bottom: 6px; + margin-bottom: 0; scrollbar-width: none; -ms-overflow-style: none; padding-right: 4px; /* breathing room at right edge */ @@ -173,6 +173,12 @@ .profile-scroll-wrap { position: relative; width: 100%; + padding-top:8px; + padding-bottom:4px; + background-color: #fffcf2; + border-left: 1.5px solid #c9b08a; + border-right: 1.5px solid #c9b08a; + border-bottom: 1.5px solid #c9b08a; } .profile-scroll-wrap::before, @@ -285,6 +291,47 @@ color: #fff; } +/* Tab strip above the profile scroll row */ +.profile-tabs { + display: flex; + width: 100%; + border-bottom: 1.5px solid #c9b08a; + margin-bottom: 0px; +} +.profile-tab { + flex: 1; + padding: 7px 4px; + background: transparent; + border: none; + border-bottom: 2.5px solid transparent; + border-left: 1.5px solid #f5edd6; + border-right: 1.5px solid #f5edd6; + border-top: 1.5px solid #f5edd6; + margin-bottom: -1.5px; + font-family: Manrope, sans-serif; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #9a7a4a; + cursor: pointer; + transition: color 0.15s, border-color 0.15s; + white-space: nowrap; +} +.profile-tab:hover { + color: #c8922a; + background: #f0e4c4; +} +.profile-tab.active { + color: #c8922a; + background-color: #fffcf2; + border-bottom-color: #fffcf2; + border-left: 1.5px solid #c9b08a; + border-right: 1.5px solid #c9b08a; + border-top: 1.5px solid #c9b08a; + border-radius:7px 7px 0px 0px +} + /* ── 6. COLUMN TOGGLES (the "Columns: Hour Air RH ..." bar) ────────── */ diff --git a/assets/css/ui.css b/assets/css/ui.css index 4a1c39b..127022e 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -1176,12 +1176,64 @@ Sits between the header dial and the day tabs. ════════════════════════════════════════════════════════════════════════ */ -/* "Why it feels like this" — sits in the header's right column, beside the dial */ +/* "Why it feels like this" - sits in the header's right column, beside the dial */ .insight-panel--why { width: 100%; max-width: 300px; } +/* Solar model selector inside the Why it feels like panel */ +.insight-env-footer { + display: flex; + flex-direction: column; + align-items: stretch; + border-bottom: 1px solid #ede4cc; + padding-bottom: 8px; + margin-bottom: 6px; + gap: 4px; +} +.insight-env-footer-label { + font-family: Manrope, sans-serif; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #b09870; + text-align: center; +} +.insight-env-footer .scope-env-row { + width: 100%; + border-top: none; + padding: 0; + align-items: stretch; +} +.insight-env-footer .cs-wrap, +.insight-env-footer .cs-btn { + width: 100%; +} +.insight-env-footer .cs-btn { + justify-content: center; + position: relative; + isolation: isolate; + border-radius: 4px; +} +.insight-env-footer .cs-btn .cs-btn-overlay { + display: block; + position: absolute; + inset: 0; + border-radius: inherit; + background: #c8922a; + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; + z-index: 0; +} +.insight-env-footer .cs-arrow { + position: absolute; + right: 12px; + z-index: 1; +} + /* Hourly table paired with the "Today at a glance" rail on its right */ .table-with-rail { display: grid; @@ -1222,6 +1274,7 @@ letter-spacing: 0.1em; color: #b09870; margin-bottom: 10px; + text-align: center; } /* Each factor row */ diff --git a/assets/js/app.js b/assets/js/app.js index a7a1fd8..915a053 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -441,26 +441,23 @@ export function UTCIForecast() { /> -
-
- Environment - <${CustomSelect} - value=${utciEnv} - isOn=${true} - noHide=${true} - grpClass="grp-felt" - options=${Object.entries(UTCI_ENVIRONMENTS).map(([k, v]) => ({ - value: k, - label: v.label, - }))} - onChange=${(v) => setUtciEnv(v)} - /> -
-
-
${forecast && currentRow && whyFeelsLike && html`
+
Why it feels like this
${[ { label: 'Sun and sky', icon: '☀', value: whyFeelsLike.sunAndSky }, diff --git a/assets/js/components/DayTabs.js b/assets/js/components/DayTabs.js index 9534c9a..8c2ebf2 100644 --- a/assets/js/components/DayTabs.js +++ b/assets/js/components/DayTabs.js @@ -42,7 +42,7 @@ // ------------------------------------------------------------------------ import { h, Fragment } from '../../vendor/preact.js'; -import { useRef, useEffect } from '../../vendor/preact-hooks.js'; +import { useRef, useEffect, useState } from '../../vendor/preact-hooks.js'; import htm from '../../vendor/htm.js'; import { confidenceBand } from '../utils.js'; import { FREE_DAYS, FILTER_PROFILES, OUTDOORS_VARIANTS, profileButtonOrder, activityVariantKeys, placeVariantKeys, workVariantKeys } from '../config.js'; @@ -96,6 +96,17 @@ export function DayTabs({ const profileScrollRef = useRef(null); const profileWrapRef = useRef(null); + // Active tab: common - places - activities - work. + // Auto-derived from the current profile on mount and on change. + const getTab = () => { + if (activeProfile === 'outdoors' && placeVariantKeys.includes(outdoorsVariant)) return 'places'; + if (activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)) return 'activities'; + if (activeProfile === 'farming' || (activeProfile === 'outdoors' && workVariantKeys.includes(outdoorsVariant))) return 'work'; + return 'common'; + }; + const [activeTab, setActiveTab] = useState(getTab); + useEffect(() => { setActiveTab(getTab()); }, [activeProfile, outdoorsVariant]); + useEffect(() => { const el = profileScrollRef.current; const wrap = profileWrapRef.current; @@ -155,139 +166,98 @@ export function DayTabs({ <${Fragment}>
+
+ ${[['common','Common'],['places','Places'],['activities','Activities'],['work','Work']].map(([key, label]) => html` + + `)} +
- ${profileButtonOrder.slice(0, 3).map((key) => { - const profile = FILTER_PROFILES[key]; - const locked = profile.proOnly && !isPro; - return html` + + ${activeTab === 'common' && profileButtonOrder.map((key) => { + const profile = FILTER_PROFILES[key]; + const locked = profile.proOnly && !isPro; + return html` + `; + })} + + ${activeTab === 'places' && placeOptions.map((opt) => html` `; - })} - - <${CustomSelect} - key="places" - value=${placeValue} - isOn=${activeProfile === 'outdoors' && placeVariantKeys.includes(outdoorsVariant)} - noHide=${true} - hideLabel="Places" - buttonLabel=${placeValue === 'off' ? 'Places' : `${placeLabel}`} - grpClass="profile-card" - sceneStyle=${{ background: placeValue !== 'off' && OUTDOORS_VARIANTS[placeValue]?.scene ? OUTDOORS_VARIANTS[placeValue].scene : "url('assets/images/profiles/places.png') center / cover no-repeat, linear-gradient(160deg,#98ccc0,#c4e4dc)" }} - sceneContent=${null} - options=${placeOptions} - onChange=${(v) => { - if (v === 'off') { activateProfile('basic'); return; } - if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) { - setProPromptSource(`variant:${v}`); - setProPromptDay(0); - return; - } - try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ } - setActiveProfile('outdoors'); - setOutdoorsVariantAndSave(v); - setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols }); - setIndoorMode('off'); - setIndoorManaged(false); - }} - /> - <${CustomSelect} - key="activities" - value=${activityValue} - isOn=${activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)} - noHide=${true} - hideLabel="Activities" - buttonLabel=${activityValue === 'off' ? 'Activities' : `${activityLabel}`} - grpClass="profile-card" - sceneStyle=${{ background: activityValue !== 'off' && OUTDOORS_VARIANTS[activityValue]?.scene ? OUTDOORS_VARIANTS[activityValue].scene : "url('assets/images/profiles/activities.png') center / cover no-repeat, linear-gradient(160deg,#e0c0a0,#f0d8c0)" }} - sceneContent=${null} - options=${activityOptions} - onChange=${(v) => { - if (v === 'off') { activateProfile('basic'); return; } - if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) { - setProPromptSource(`variant:${v}`); - setProPromptDay(0); - return; - } - try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ } - setActiveProfile('outdoors'); - setOutdoorsVariantAndSave(v); - setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols }); - setIndoorMode('off'); - setIndoorManaged(false); - }} - /> - <${CustomSelect} - key="work" - value=${workValue} - isOn=${activeProfile === 'farming' || (activeProfile === 'outdoors' && workVariantKeys.includes(outdoorsVariant))} - noHide=${true} - hideLabel="Work" - buttonLabel=${workValue === 'off' ? 'Work' : `${workLabel}`} - grpClass="profile-card" - sceneStyle=${{ background: workValue === 'farming' ? FILTER_PROFILES.farming.scene : workValue !== 'off' && OUTDOORS_VARIANTS[workValue]?.scene ? OUTDOORS_VARIANTS[workValue].scene : "url('assets/images/profiles/work.png') center / cover no-repeat, linear-gradient(160deg,#b0b0d4,#d0d0e8)" }} - sceneContent=${null} - options=${workOptions} - onChange=${(v) => { - if (v === 'off') { activateProfile('basic'); return; } - if (v === 'farming') { activateProfile('farming'); return; } - if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) { - setProPromptSource(`variant:${v}`); - setProPromptDay(0); - return; - } - try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ } - setActiveProfile('outdoors'); - setOutdoorsVariantAndSave(v); - setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols }); - if (v === 'office') { - setBuildingType('office'); - setIndoorMode('on'); - setIndoorManaged(false); - } else { - setIndoorMode('off'); - setIndoorManaged(false); - } - }} - /> - - ${profileButtonOrder.slice(3).map((key) => { - const profile = FILTER_PROFILES[key]; - const locked = profile.proOnly && !isPro; - return html` + + ${opt.name} + + `)} + + ${activeTab === 'activities' && activityOptions.map((opt) => html` `; - })} + + ${opt.name} + + `)} + + ${activeTab === 'work' && workOptions.map((opt) => html` + + `)} +
@@ -363,7 +333,10 @@ export function DayTabs({ style=${{ background: isActive ? wBgActive : wBg, color: wText, - border: '1px solid #c9b08a', + borderTop: '1px solid #c9b08a', + borderRight: '1px solid #c9b08a', + borderBottom: '1px solid #c9b08a', + borderLeft: i === 0 ? '1px solid #c9b08a' : 'none', opacity: locked ? 0.5 : 1, cursor: locked ? 'not-allowed' : 'pointer', position: 'relative', diff --git a/assets/js/compute.js b/assets/js/compute.js index 00791bc..a9f4099 100644 --- a/assets/js/compute.js +++ b/assets/js/compute.js @@ -117,9 +117,11 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v // Suggested multipliers (tune with real data): // Resting: 1.0 (no change) // Walking: 0.85 - // Cycling: 0.75 - // Running: 0.60 - // Sport: 0.50 + // Cycling: 0.75 *This will need a airflow calc as going at speed is cooling + // Running: 0.60 *This will need a airflow calc as going at speed is cooling + // Sport: 0.50 *This will need a airflow calc as going at speed is cooling + // + // *Danny Notes: These faster speed activities might need intergrating into the future "Vehicle Speed" calcs somehow? // // heatStressLevel - a simple label: 'Low' | 'Moderate' | 'High' | 'Very High' // Derived from UTCI + activity heat load. A runner at diff --git a/og-image.png b/og-image.png index 6c9428f..54b1f8f 100644 Binary files a/og-image.png and b/og-image.png differ