From 1509ddb78dae9992c4d8cf858ab2eeab323f7421 Mon Sep 17 00:00:00 2001 From: Fraxle Date: Tue, 26 May 2026 13:15:26 +0100 Subject: [PATCH] 1.9.2 Fix popup overflow Fix pulldowns overflow Update extra popus Update UTCI/SunSoak - Show variant selected --- assets/css/table.css | 26 ++++++++++++++++++++++ assets/css/ui.css | 3 ++- assets/js/app.js | 3 ++- assets/js/components.js | 9 +++++++- assets/js/components/DayTabs.js | 2 +- assets/js/config.js | 14 ++++++------ assets/js/events.js | 17 +++++++++++--- assets/js/hooks/useColumnPopup.js | 37 +++++++++++++++++++++++++++++-- 8 files changed, 95 insertions(+), 16 deletions(-) diff --git a/assets/css/table.css b/assets/css/table.css index 13da546..88a9236 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -468,6 +468,19 @@ margin-top: 2px; } +/* Environment modifier badge — shown in SunSoak th when non-default env active */ +.col-env-badge { + display: block; + font-size: 9px; + font-weight: 700; + font-family: Manrope, sans-serif; + letter-spacing: 0.04em; + color: #a06828; + opacity: 0.85; + margin-top: 2px; + white-space: nowrap; +} + /* col-unit header spans always visible - units toggle only affects body cells */ @media (max-width: 800px) { @@ -542,6 +555,9 @@ position: fixed; /* set by JS — stays in viewport regardless of scroll */ z-index: 200; width: 260px; + max-height: 260px; + display: flex; + flex-direction: column; background: #fffcf2; border: 1.5px solid #c9b08a; border-radius: 6px; @@ -588,6 +604,8 @@ color: #6b4f2a; line-height: 1.5; margin: 0; + overflow-y: auto; /* scrolls when text exceeds max-height cap */ + flex: 1 1 auto; } .col-info-close { position: absolute; @@ -811,6 +829,14 @@ .utci-table .grp-label-row th.grp-airqual { background: rgba(229,240,216,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);} .utci-table .grp-label-row th.grp-solar { background: rgba(250,234,204,0.50); border-left: 2px solid rgba(74,50,24,0.18); border-bottom: 1px solid rgba(74,50,24,0.18);} +/* ── POPUP — mobile centre-alignment ───────────────────────────────── */ +@media (max-width: 640px) { + .col-info-title, + .col-info-desc { + text-align: center; + } +} + /* ── FILTER PROFILES + COLUMN TOGGLES — mobile fixes ───────────────── */ @media (max-width: 640px) { diff --git a/assets/css/ui.css b/assets/css/ui.css index 851c53f..7c7daf8 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -475,7 +475,8 @@ .cs-panel { position: absolute; top: calc(100% + 4px); - left: 0; + right: 0; /* right edge flushes with button's right edge */ + left: auto; z-index: 1000; background: #fffcf2; border: 1.5px solid #c9b08a; diff --git a/assets/js/app.js b/assets/js/app.js index 495ec27..d167007 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -264,6 +264,7 @@ export function UTCIForecast() { dt=${currentRow?.dt ?? new Date()} glob=${currentRow?.glob ?? 0} activeEvent=${lensEvent} + utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null} /> @@ -515,7 +516,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC ${groupLabelRow()} handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>Hour - ${visibleCols.utciP && html` handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>SunSoak °C felt`} + ${visibleCols.utciP && html` handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>SunSoak °C felt${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ? html`${UTCI_ENVIRONMENTS[utciEnv].shortLabel}` : null}`} ${visibleCols.vehicleT && html` handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle °C peak`} ${indoorMode === 'on' && !indoorManaged && html` handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors °C est.`} ${indoorMode === 'on' && indoorManaged && html` handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed °C est.`} diff --git a/assets/js/components.js b/assets/js/components.js index 8f78f65..3f35046 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -398,7 +398,7 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) { // - (value + 10) / 60 maps UTCI -10..50 onto the 270- sweep - // widen the dial range by changing those numbers // ------------------------------------------------------------------- -export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), glob = 0, activeEvent = null }) { +export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), glob = 0, activeEvent = null, utciEnvShort = null }) { // -- Day/night scene behind the readout ----------------------------- // The whole back of the lens is a SkyScope-style scene: sky on top, // grass on the bottom, sun positioned by elevation (or moon at night @@ -620,6 +620,13 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.5"> ${cat.label.toUpperCase()} + ${utciEnvShort && html` + + ${utciEnvShort.toUpperCase()} EST. + `} ` : html` - 🔒 ${promptCopy.title} + 🔒 ${promptCopy.title}
{ if (ev) weatherEvents.push(ev); }); } - return [...cosmicHits, ...weatherEvents]; + // Sort so priority weather warnings (heat, frost, storm) always lead, + // then cosmic events, then the remaining weather items. + const priorityWeather = weatherEvents.filter(ev => PRIORITY_WEATHER_IDS.has(ev.id)); + const otherWeather = weatherEvents.filter(ev => !PRIORITY_WEATHER_IDS.has(ev.id)); + + return [...priorityWeather, ...cosmicHits, ...otherWeather]; } // --- LENS EVENT PICKER --------------------------------------------------- diff --git a/assets/js/hooks/useColumnPopup.js b/assets/js/hooks/useColumnPopup.js index 688a64a..60df4f3 100644 --- a/assets/js/hooks/useColumnPopup.js +++ b/assets/js/hooks/useColumnPopup.js @@ -37,7 +37,10 @@ export function useColumnPopup() { const calcPopupPos = (thEl) => { const rect = thEl.getBoundingClientRect(); - const popupW = 260, popupH = 110, margin = 8, gap = 6; + // Use a generous height estimate so tall (text-heavy) popups prefer going + // below rather than overflowing the top. The post-render clamp effect + // catches any remaining overflow after the real height is known. + const popupW = 260, popupH = 220, margin = 8, gap = 6; let x = rect.left + rect.width / 2; x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin)); const below = rect.top < popupH + gap + margin; @@ -103,10 +106,26 @@ export function useColumnPopup() { }; }, [colPopup]); + // Post-render clamp: after the popup actually paints, measure its real + // bounding rect and nudge y if it overflows the viewport top or bottom. + // The >1 guard prevents floating-point micro-loops. + useEffect(() => { + if (!colPopup || !colPopupRef.current) return; + const rect = colPopupRef.current.getBoundingClientRect(); + const MARGIN = 8; + if (!colPopup.below && rect.top < MARGIN) { + const delta = MARGIN - rect.top; + if (delta > 1) setColPopup(prev => prev ? { ...prev, y: prev.y + delta } : prev); + } else if (colPopup.below && rect.bottom > window.innerHeight - MARGIN) { + const delta = rect.bottom - (window.innerHeight - MARGIN); + if (delta > 1) setColPopup(prev => prev ? { ...prev, y: prev.y - delta } : prev); + } + }, [colPopup]); + // --- Event tag popup helpers ----------------------------------------- const calcEventPopupPos = (spanEl) => { const rect = spanEl.getBoundingClientRect(); - const popupW = 260, popupH = 80, margin = 8, gap = 6; + const popupW = 260, popupH = 160, margin = 8, gap = 6; let x = rect.left + rect.width / 2; x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin)); const below = rect.top < popupH + gap + margin; @@ -206,6 +225,20 @@ export function useColumnPopup() { }; }, [eventTagPopup]); + // Post-render clamp for event-tag popup (same logic as col popup above). + useEffect(() => { + if (!eventTagPopup || !eventTagPopupRef.current) return; + const rect = eventTagPopupRef.current.getBoundingClientRect(); + const MARGIN = 8; + if (!eventTagPopup.below && rect.top < MARGIN) { + const delta = MARGIN - rect.top; + if (delta > 1) setEventTagPopup(prev => prev ? { ...prev, y: prev.y + delta } : prev); + } else if (eventTagPopup.below && rect.bottom > window.innerHeight - MARGIN) { + const delta = rect.bottom - (window.innerHeight - MARGIN); + if (delta > 1) setEventTagPopup(prev => prev ? { ...prev, y: prev.y - delta } : prev); + } + }, [eventTagPopup]); + return { // column-header popup colPopup, colPopupRef,