1.9.2
Fix popup overflow Fix pulldowns overflow Update extra popus Update UTCI/SunSoak - Show variant selected
This commit is contained in:
@@ -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) {
|
||||
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+2
-1
@@ -264,6 +264,7 @@ export function UTCIForecast() {
|
||||
dt=${currentRow?.dt ?? new Date()}
|
||||
glob=${currentRow?.glob ?? 0}
|
||||
activeEvent=${lensEvent}
|
||||
utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -515,7 +516,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
|
||||
${groupLabelRow()}
|
||||
<tr>
|
||||
<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>`}
|
||||
${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>`}
|
||||
|
||||
@@ -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()}
|
||||
</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"
|
||||
fill=${readoutMutedColor} font-size="8"
|
||||
|
||||
@@ -249,7 +249,7 @@ export function DayTabs({
|
||||
marginBottom: '6px',
|
||||
lineHeight: 1.25,
|
||||
}}>
|
||||
🔒 ${promptCopy.title}
|
||||
<span style=${{ fontStyle: 'normal' }}>🔒</span> ${promptCopy.title}
|
||||
</div>
|
||||
<div style=${{
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
|
||||
+7
-7
@@ -122,13 +122,13 @@ export const POLLEN_TYPES = {
|
||||
// label - short display name
|
||||
// note - shown in column header tooltip to flag modified calculation
|
||||
export const UTCI_ENVIRONMENTS = {
|
||||
open: { 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' },
|
||||
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' },
|
||||
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' },
|
||||
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' },
|
||||
open: { shortLabel: null, label: 'Open / Exposed', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null },
|
||||
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: { 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: { 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: { 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: { 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: { 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.
|
||||
|
||||
+14
-3
@@ -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 ---------------------------------------------------------
|
||||
// Returns ALL active events for the given rows/date as an array.
|
||||
// Empty array = nothing active.
|
||||
//
|
||||
// Order within array:
|
||||
// 1. PROMO_OVERRIDE (if set, returned alone)
|
||||
// 2. All matching cosmic calendar events for this date
|
||||
// 3. All matching weather-derived events
|
||||
// 2. Priority weather events (heat spike, frost, storm) — always slide 1
|
||||
// 3. All matching cosmic calendar events for this date
|
||||
// 4. Remaining weather-derived events (sunset, stargazing, …)
|
||||
|
||||
export function getActiveEvents(rows, location) {
|
||||
// 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); });
|
||||
}
|
||||
|
||||
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 ---------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user