diff --git a/assets/css/header.css b/assets/css/header.css index 66b6fd2..d01c4e1 100644 --- a/assets/css/header.css +++ b/assets/css/header.css @@ -9,7 +9,7 @@ grid-template-columns: 1fr auto 1fr; /* left | dial | why */ grid-template-areas: "hleft dial hright"; column-gap: 32px; - align-items: center; + align-items: stretch; padding-bottom: 6px; margin-bottom: 6px; } @@ -186,8 +186,8 @@ /* The big UTCI dial in the middle of the header. Change width/height to resize it. */ .scope-ring { - width: 220px; - height: 220px; + width: 270px; + height: 270px; display: block; } @@ -231,6 +231,7 @@ display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 8px; } @@ -296,7 +297,7 @@ align-items: center; gap: 2px; border-top: 1px solid #c9b08a; - width: 220px; /* match desktop/tablet dial width */ + width: 270px; /* match desktop/tablet dial width */ } .scope-env-label { diff --git a/assets/css/table.css b/assets/css/table.css index 8de4870..b7400ee 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -1398,14 +1398,12 @@ display: none; } .table-main--simple { - align-self: stretch; - display: flex; - flex-direction: column; + display: block; } .table-main--simple .forecast-simple-wrap { display: flex; flex-direction: column; - flex: 1; + height: 420px; border: 1.5px solid #d4c0a0; background: #fdf8ee; } @@ -1423,6 +1421,10 @@ overflow-y: hidden; scrollbar-width: thin; scrollbar-color: #c9b08a transparent; + cursor: grab; +} +.forecast-simple-scroll:active { + cursor: grabbing; } .forecast-simple-scroll::-webkit-scrollbar { height: 4px; @@ -1540,6 +1542,43 @@ min-height: 60px; } +.fvt-model-sep { + width: 1px; + height: 18px; + background: #d4c0a0; + margin: 0 4px; + flex-shrink: 0; +} + +.fsc-table-btn-wrap { + padding: 10px 16px 12px; + border-top: 1px solid #d4c0a0; + background: #fdf8ee; + flex-shrink: 0; +} +.fsc-table-btn { + display: block; + width: 100%; + padding: 10px 16px; + border: 1.5px solid #a87020; + border-radius: 6px; + background: linear-gradient(to bottom, #d4a030, #b8781a); + font-family: Manrope, sans-serif; + font-size: 14px; + font-weight: 700; + color: #fff; + letter-spacing: 0.03em; + cursor: pointer; + text-align: center; + box-shadow: 0 1px 4px rgba(160,100,20,0.22); + text-shadow: 0 1px 2px rgba(0,0,0,0.18); +} +.fsc-table-btn:hover { + background: linear-gradient(to bottom, #e8b848, #cc9030); + border-color: #c8922a; + box-shadow: 0 2px 8px rgba(200,146,42,0.28); +} + @media (max-width: 600px) { .fsc-card { min-width: 55px; diff --git a/assets/js/app.js b/assets/js/app.js index e792c7b..7c10fa7 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -138,6 +138,59 @@ export function UTCIForecast() { const searchWrapRef = useRef(null); const [colTogglesOpen, setColTogglesOpen] = useState(false); const searchInputRef = useRef(null); + const fscScrollRef = useRef(null); + + useEffect(() => { + const el = fscScrollRef.current; + if (!el) return; + let isDown = false, startX = 0, startScroll = 0, hasDragged = false; + const onMouseDown = (e) => { + if (!el.contains(e.target) || e.button !== 0) return; + isDown = true; hasDragged = false; + startX = e.clientX; startScroll = el.scrollLeft; + document.body.style.userSelect = 'none'; + document.body.style.webkitUserSelect = 'none'; + }; + const onMouseMove = (e) => { + if (!isDown) return; + const dx = e.clientX - startX; + if (Math.abs(dx) > 5) { + hasDragged = true; + el.style.cursor = 'grabbing'; + el.scrollLeft = startScroll - dx; + } + }; + const onMouseUp = () => { + if (!isDown) return; + isDown = false; + el.style.cursor = ''; + document.body.style.userSelect = ''; + document.body.style.webkitUserSelect = ''; + }; + const onClickCapture = (e) => { + if (hasDragged) { e.stopPropagation(); e.preventDefault(); hasDragged = false; } + }; + document.addEventListener('mousedown', onMouseDown); + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + el.addEventListener('click', onClickCapture, true); + return () => { + document.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + el.removeEventListener('click', onClickCapture, true); + }; + }, []); + + const [simpleTemp, setSimpleTemp] = useState('utciAdj'); + useEffect(() => { + if (activeProfile === 'vehicle') setSimpleTemp('vehicleT'); + else if (activeProfile === 'home') setSimpleTemp('indoorT'); + else setSimpleTemp('utciAdj'); + if (['alltemps', 'showall', 'custom', 'farming', 'construction', 'market', 'windowcleaning', 'office'].includes(activeProfile)) { + setForecastView('table'); + } + }, [activeProfile]); // Focus the input the moment the search opens. useEffect(() => { @@ -668,6 +721,65 @@ export function UTCIForecast() { ${[1, 2, 3, 4].map(n => html``)} + ${forecastView === 'simple' && (() => { + const showSolar = visibleCols.utciP; + const showVehicle = visibleCols.vehicleT; + const showIndoor = visibleCols.indoorT || visibleCols.managedT; + if (!showSolar && !showVehicle && !showIndoor) return null; + const solarOn = simpleTemp === 'utciAdj'; + const vehicleOn = simpleTemp === 'vehicleT'; + const indoorOn = simpleTemp === 'indoorT' || simpleTemp === 'managedT'; + return html` + + ${showSolar && html` + + <${CustomSelect} + value=${utciEnv} isOn=${solarOn} grpClass="grp-felt" + hideLabel="Solar Model" hidingLabel="Solar Model" + groupedLeft=${true} isLastChild=${true} + options=${Object.entries(UTCI_ENVIRONMENTS).map(([k, v]) => ({ value: k, label: v.label }))} + onChange=${v => { + if (v === 'off') setSimpleTemp(showVehicle ? 'vehicleT' : showIndoor ? 'indoorT' : 'utciAdj'); + else { setUtciEnv(v); setSimpleTemp('utciAdj'); } + }} + /> + `} + ${showVehicle && html` + + <${CustomSelect} + value=${vehicleType} isOn=${vehicleOn} grpClass="grp-felt" + hideLabel="Vehicle" hidingLabel="Vehicle" + groupedLeft=${true} isLastChild=${!vehicleOn} + options=${Object.entries(VEHICLE_TYPES).map(([k, v]) => ({ value: k, label: v.name }))} + onChange=${v => { + if (v === 'off') setSimpleTemp(showSolar ? 'utciAdj' : showIndoor ? 'indoorT' : 'vehicleT'); + else { setVehicleType(v); setSimpleTemp('vehicleT'); } + }} + /> + ${vehicleOn && html` + <${VentPill} checked=${vehicleVent} onChange=${() => setVehicleVent(v => !v)} + grpClass="grp-felt" label="Ventilation" + title="Open windows significantly reduce cabin heat build-up" />`} + `} + ${showIndoor && html` + + <${CustomSelect} + value=${buildingType} isOn=${indoorOn} grpClass="grp-felt" + hideLabel="Indoors" hidingLabel="Indoors" + groupedLeft=${true} isLastChild=${!indoorOn} + options=${Object.entries(BUILDING_TYPES).map(([k, v]) => ({ value: k, label: v.name }))} + onChange=${v => { + if (v === 'off') setSimpleTemp(showSolar ? 'utciAdj' : showVehicle ? 'vehicleT' : 'indoorT'); + else { setBuildingType(v); setIndoorMode('on'); setSimpleTemp(indoorManaged ? 'managedT' : 'indoorT'); } + }} + /> + ${indoorOn && html` + <${VentPill} checked=${indoorManaged} onChange=${() => { const next = !indoorManaged; setIndoorManaged(next); setSimpleTemp(next ? 'managedT' : 'indoorT'); }} + grpClass="grp-felt" label="Managed" + title="Curtains closed by day, windows open when cooler outside" />`} + `} + `; + })()} Columns: +
diff --git a/assets/js/components.js b/assets/js/components.js index 6222871..b127972 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -464,13 +464,13 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date(), fil ${mist(size * 0.18, brass)} ${line(size * 0.18, size * 0.56, size * 0.60, size * 0.56, muted, sw * 0.7, 0.6)}`} ${category === 'scattered' && html` - ${!filled && line(size * 0.20, size * 0.58, size * 0.48, size * 0.58, brass, sw * 0.75, 0.78)}`} ${category === 'overcast' && html` - - `} `; } @@ -1180,7 +1180,7 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g // heavy snow (- 0.5cm/h) - 2 flakes // mixed - 1 drop + 1 flake // ------------------------------------------------------------------- -export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) { +export function PrecipIcon({ precip = 0, snow = 0, size = 28, filled = false }) { const r = size / 2; const hasRain = precip > 0; const hasSnow = snow > 0; @@ -1196,7 +1196,7 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) { C ${size*0.86} ${size*0.36}, ${size*0.88} ${size*0.20}, ${size*0.72} ${size*0.19} C ${size*0.66} ${size*0.04}, ${size*0.42} ${size*0.02}, ${size*0.34} ${size*0.17} C ${size*0.22} ${size*0.14}, ${size*0.14} ${size*0.24}, ${size*0.18} ${size*0.36} Z`} - fill="none" stroke=${ink} stroke-width=${sw} stroke-linecap="round" stroke-linejoin="round" />`; + fill=${filled ? 'rgba(255,255,255,0.88)' : 'none'} stroke=${filled ? 'none' : ink} stroke-width=${sw} stroke-linecap="round" stroke-linejoin="round" />`; const rainStroke = (cx, cy, heavy = false) => html`