Fix popup overflow
Fix pulldowns overflow
Update extra popus
Update UTCI/SunSoak - Show variant selected
This commit is contained in:
Fraxle
2026-05-26 13:15:26 +01:00
parent f8a220b325
commit 1509ddb78d
8 changed files with 95 additions and 16 deletions
+26
View File
@@ -468,6 +468,19 @@
margin-top: 2px; 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 */ /* col-unit header spans always visible - units toggle only affects body cells */
@media (max-width: 800px) { @media (max-width: 800px) {
@@ -542,6 +555,9 @@
position: fixed; /* set by JS — stays in viewport regardless of scroll */ position: fixed; /* set by JS — stays in viewport regardless of scroll */
z-index: 200; z-index: 200;
width: 260px; width: 260px;
max-height: 260px;
display: flex;
flex-direction: column;
background: #fffcf2; background: #fffcf2;
border: 1.5px solid #c9b08a; border: 1.5px solid #c9b08a;
border-radius: 6px; border-radius: 6px;
@@ -588,6 +604,8 @@
color: #6b4f2a; color: #6b4f2a;
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0;
overflow-y: auto; /* scrolls when text exceeds max-height cap */
flex: 1 1 auto;
} }
.col-info-close { .col-info-close {
position: absolute; 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-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);} .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 ───────────────── */ /* ── FILTER PROFILES + COLUMN TOGGLES — mobile fixes ───────────────── */
@media (max-width: 640px) { @media (max-width: 640px) {
+2 -1
View File
@@ -475,7 +475,8 @@
.cs-panel { .cs-panel {
position: absolute; position: absolute;
top: calc(100% + 4px); top: calc(100% + 4px);
left: 0; right: 0; /* right edge flushes with button's right edge */
left: auto;
z-index: 1000; z-index: 1000;
background: #fffcf2; background: #fffcf2;
border: 1.5px solid #c9b08a; border: 1.5px solid #c9b08a;
+2 -1
View File
@@ -264,6 +264,7 @@ export function UTCIForecast() {
dt=${currentRow?.dt ?? new Date()} dt=${currentRow?.dt ?? new Date()}
glob=${currentRow?.glob ?? 0} glob=${currentRow?.glob ?? 0}
activeEvent=${lensEvent} activeEvent=${lensEvent}
utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null}
/> />
</div> </div>
@@ -515,7 +516,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
${groupLabelRow()} ${groupLabelRow()}
<tr> <tr>
<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>Hour</th> <th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>Hour</th>
${visibleCols.utciP && html`<th class=${`col-info-th ${groupStart('utciP')} ${groupColor('utciP')}`} scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>SunSoak <span class="col-unit">°C felt</span></th>`} ${visibleCols.utciP && html`<th class=${`col-info-th ${groupStart('utciP')} ${groupColor('utciP')}`} scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>SunSoak <span class="col-unit">°C felt</span>${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ? html`<span class="col-env-badge">${UTCI_ENVIRONMENTS[utciEnv].shortLabel}</span>` : null}</th>`}
${visibleCols.vehicleT && html`<th class=${`col-info-th ${groupStart('vehicleT')} ${groupColor('vehicleT')}`} scope="col" onClick=${(e) => handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle <span class="col-unit">°C peak</span></th>`} ${visibleCols.vehicleT && html`<th class=${`col-info-th ${groupStart('vehicleT')} ${groupColor('vehicleT')}`} scope="col" onClick=${(e) => handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle <span class="col-unit">°C peak</span></th>`}
${indoorMode === 'on' && !indoorManaged && html`<th class=${`col-info-th ${groupStart('indoorT')} ${groupColor('indoorT')}`} scope="col" onClick=${(e) => handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors <span class="col-unit">°C est.</span></th>`} ${indoorMode === 'on' && !indoorManaged && html`<th class=${`col-info-th ${groupStart('indoorT')} ${groupColor('indoorT')}`} scope="col" onClick=${(e) => handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors <span class="col-unit">°C est.</span></th>`}
${indoorMode === 'on' && indoorManaged && html`<th class=${`col-info-th ${groupStart('managedT')} ${groupColor('managedT')}`} scope="col" onClick=${(e) => handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed <span class="col-unit">°C est.</span></th>`} ${indoorMode === 'on' && indoorManaged && html`<th class=${`col-info-th ${groupStart('managedT')} ${groupColor('managedT')}`} scope="col" onClick=${(e) => handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed <span class="col-unit">°C est.</span></th>`}
+8 -1
View File
@@ -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 - // - (value + 10) / 60 maps UTCI -10..50 onto the 270- sweep -
// widen the dial range by changing those numbers // 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 ----------------------------- // -- Day/night scene behind the readout -----------------------------
// The whole back of the lens is a SkyScope-style scene: sky on top, // 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 // 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"> font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.5">
${cat.label.toUpperCase()} ${cat.label.toUpperCase()}
</text> </text>
${utciEnvShort && html`
<text x=${cx} y="163" text-anchor="middle"
fill=${readoutMutedColor} font-size="5.5"
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.8"
opacity="0.85">
${utciEnvShort.toUpperCase()} EST.
</text>`}
</>` </>`
: html`<text x=${cx} y=${cy + 5} text-anchor="middle" : html`<text x=${cx} y=${cy + 5} text-anchor="middle"
fill=${readoutMutedColor} font-size="8" fill=${readoutMutedColor} font-size="8"
+1 -1
View File
@@ -249,7 +249,7 @@ export function DayTabs({
marginBottom: '6px', marginBottom: '6px',
lineHeight: 1.25, lineHeight: 1.25,
}}> }}>
🔒 ${promptCopy.title} <span style=${{ fontStyle: 'normal' }}>🔒</span> ${promptCopy.title}
</div> </div>
<div style=${{ <div style=${{
fontFamily: 'Manrope, sans-serif', fontFamily: 'Manrope, sans-serif',
+7 -7
View File
@@ -122,13 +122,13 @@ export const POLLEN_TYPES = {
// label - short display name // label - short display name
// note - shown in column header tooltip to flag modified calculation // note - shown in column header tooltip to flag modified calculation
export const UTCI_ENVIRONMENTS = { export const UTCI_ENVIRONMENTS = {
open: { label: 'Open / Exposed', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null }, open: { shortLabel: null, label: 'Open / Exposed', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null },
forest: { label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling' }, forest: { shortLabel: 'Forest', label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling' },
alpine: { label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions' }, alpine: { shortLabel: 'Alpine', label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions' },
beach: { label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose' }, beach: { shortLabel: 'Beach', label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose' },
river: { label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp' }, river: { shortLabel: 'Riverbank', label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp' },
desert: { label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature' }, desert: { shortLabel: 'Desert', label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature' },
openwater: { label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution' }, openwater: { shortLabel: 'Open Water', label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution' },
}; };
// Maps OUTDOORS_VARIANTS keys to a default UTCI_ENVIRONMENTS key. // Maps OUTDOORS_VARIANTS keys to a default UTCI_ENVIRONMENTS key.
+14 -3
View File
@@ -73,14 +73,20 @@ export function getCellTagEvents(events, row) {
}); });
} }
// --- BANNER PRIORITY -----------------------------------------------------
// Weather event IDs that are safety-relevant and should always appear as the
// first banner slide, ahead of cosmic events and lower-priority weather items.
const PRIORITY_WEATHER_IDS = new Set(['heat-spike', 'frost', 'storm']);
// --- MAIN EXPORT --------------------------------------------------------- // --- MAIN EXPORT ---------------------------------------------------------
// Returns ALL active events for the given rows/date as an array. // Returns ALL active events for the given rows/date as an array.
// Empty array = nothing active. // Empty array = nothing active.
// //
// Order within array: // Order within array:
// 1. PROMO_OVERRIDE (if set, returned alone) // 1. PROMO_OVERRIDE (if set, returned alone)
// 2. All matching cosmic calendar events for this date // 2. Priority weather events (heat spike, frost, storm) — always slide 1
// 3. All matching weather-derived events // 3. All matching cosmic calendar events for this date
// 4. Remaining weather-derived events (sunset, stargazing, …)
export function getActiveEvents(rows, location) { export function getActiveEvents(rows, location) {
// 1. Manual promo override - shown alone, no mixing with other events // 1. Manual promo override - shown alone, no mixing with other events
@@ -107,7 +113,12 @@ export function getActiveEvents(rows, location) {
checks.forEach(ev => { if (ev) weatherEvents.push(ev); }); checks.forEach(ev => { 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 --------------------------------------------------- // --- LENS EVENT PICKER ---------------------------------------------------
+35 -2
View File
@@ -37,7 +37,10 @@ export function useColumnPopup() {
const calcPopupPos = (thEl) => { const calcPopupPos = (thEl) => {
const rect = thEl.getBoundingClientRect(); 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; let x = rect.left + rect.width / 2;
x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin)); x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin));
const below = rect.top < popupH + gap + margin; const below = rect.top < popupH + gap + margin;
@@ -103,10 +106,26 @@ export function useColumnPopup() {
}; };
}, [colPopup]); }, [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 ----------------------------------------- // --- Event tag popup helpers -----------------------------------------
const calcEventPopupPos = (spanEl) => { const calcEventPopupPos = (spanEl) => {
const rect = spanEl.getBoundingClientRect(); 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; let x = rect.left + rect.width / 2;
x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin)); x = Math.max(popupW / 2 + margin, Math.min(x, window.innerWidth - popupW / 2 - margin));
const below = rect.top < popupH + gap + margin; const below = rect.top < popupH + gap + margin;
@@ -206,6 +225,20 @@ export function useColumnPopup() {
}; };
}, [eventTagPopup]); }, [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 { return {
// column-header popup // column-header popup
colPopup, colPopupRef, colPopup, colPopupRef,