diff --git a/assets/css/ui.css b/assets/css/ui.css index cf494a4..0c97a42 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -93,6 +93,12 @@ vertical-align: middle; margin-left: 3px; display: inline-block; + cursor: pointer; + border-radius: 3px; + transition: opacity 0.15s; +} +.event-cell-tag:hover { + opacity: 0.75; } /* ── ALMANAC PANEL ──────────────────────────────────────────────────── */ diff --git a/assets/js/app.js b/assets/js/app.js index f3e89c9..c0fad3d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -244,6 +244,67 @@ export function UTCIForecast() { const hoverTimerRef = useRef(null); const closeTimerRef = useRef(null); + // Event tag popup — same behaviour as column popups but for cell emoji icons. + const [eventTagPopup, setEventTagPopup] = useState(null); // { ev, x, y, arrowLeft, below } + const eventTagPopupRef = useRef(null); + const evHoverTimerRef = useRef(null); + const evCloseTimerRef = useRef(null); + + const openEventTagPopup = (ev, spanEl) => { + const rect = spanEl.getBoundingClientRect(); + const popupW = 260, popupH = 80, 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; + const y = below ? rect.bottom + gap : rect.top - gap; + const popupLeft = x - popupW / 2; + const arrowLeft = Math.max(16, Math.min(rect.left + rect.width / 2 - popupLeft, popupW - 16)); + setEventTagPopup({ ev, x, y, arrowLeft, below }); + }; + + const closeEventTagPopup = () => { + setEventTagPopup(null); + clearTimeout(evHoverTimerRef.current); + clearTimeout(evCloseTimerRef.current); + }; + + const handleEventTagClick = (ev, e) => { + e.stopPropagation(); + clearTimeout(evHoverTimerRef.current); + clearTimeout(evCloseTimerRef.current); + if (eventTagPopup?.ev?.id === ev.id) { closeEventTagPopup(); return; } + openEventTagPopup(ev, e.currentTarget); + }; + + const handleEventTagEnter = (ev, e) => { + clearTimeout(evHoverTimerRef.current); + clearTimeout(evCloseTimerRef.current); + if (eventTagPopup?.ev?.id === ev.id) return; + const spanEl = e.currentTarget; + evHoverTimerRef.current = setTimeout(() => openEventTagPopup(ev, spanEl), 700); + }; + + const handleEventTagLeave = () => clearTimeout(evHoverTimerRef.current); + + const handleEventTagPopupEnter = () => clearTimeout(evCloseTimerRef.current); + const handleEventTagPopupLeave = () => { evCloseTimerRef.current = setTimeout(closeEventTagPopup, 200); }; + + useEffect(() => { + if (!eventTagPopup) return; + const onClickOutside = (e) => { + if (eventTagPopupRef.current && !eventTagPopupRef.current.contains(e.target)) closeEventTagPopup(); + }; + const onScrollOrResize = () => closeEventTagPopup(); + document.addEventListener('mousedown', onClickOutside); + window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true }); + window.addEventListener('resize', onScrollOrResize, { passive: true }); + return () => { + document.removeEventListener('mousedown', onClickOutside); + window.removeEventListener('scroll', onScrollOrResize, { capture: true }); + window.removeEventListener('resize', onScrollOrResize); + }; + }, [eventTagPopup]); + const COL_DESCRIPTIONS = { hour: { title: 'Hour', desc: 'Local wall-clock time for this forecast row. Each row covers one hour.' }, air: { title: 'Air Temperature', desc: 'Measured air temperature at 2 m above the ground. This is the standard thermometer reading — it does not account for sun, wind, or humidity.' }, @@ -1510,7 +1571,12 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a <${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${30} /> ${localHHMM} ${getCellTagEvents(selectedDayEvents, r).map(ev => html` - ${ev.emoji} + handleEventTagClick(ev, e)} + onMouseEnter=${(e) => handleEventTagEnter(ev, e)} + onMouseLeave=${handleEventTagLeave} + role="button" tabIndex="0" aria-label=${ev.title} + >${ev.emoji} `)} @@ -1623,6 +1689,25 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
${COL_DESCRIPTIONS[colPopup.key].desc}
`} + ${eventTagPopup && html` +${eventTagPopup.ev.message}
+