From d075b522d82a4d5ab5a320a5868e988777e7a3bb Mon Sep 17 00:00:00 2001 From: fraxle Date: Thu, 11 Jun 2026 18:37:34 +0100 Subject: [PATCH] Event banner also a popup/flyout --- assets/css/ui.css | 55 ++++++++++++++--------------- assets/js/app.js | 15 +++++--- assets/js/hooks/useAppState.js | 63 +++------------------------------- data/tracking.json | 2 +- index.html | 4 +-- 5 files changed, 43 insertions(+), 96 deletions(-) diff --git a/assets/css/ui.css b/assets/css/ui.css index 364bdeb..213f4aa 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -5,27 +5,26 @@ /* ── EVENT BANNER ───────────────────────────────────────────────────── */ /* Animated slide-down banner for cosmic/weather/promo events. */ -/* Open/close: grow vertically using grid-template-rows (silky smooth, - no max-height hack — it animates from 0fr → 1fr and back). */ +/* Flyout: a fixed panel pinned to the very top of the screen that slides + down over everything (including the nav) and retracts upward on close. */ .event-banner-wrap { - display: grid; - grid-template-rows: 0fr; - transition: grid-template-rows 0.45s cubic-bezier(0.4, 0, 0.2, 1); - position: absolute; + position: fixed; top: 0; left: 0; right: 0; - z-index: 10; -} -.event-banner-wrap > * { - overflow: hidden; + z-index: 9999; + display: flex; + justify-content: center; + padding: 0 14px; + transform: translateY(-130%); + transition: transform 0.5s cubic-bezier(0.34, 1.25, 0.64, 1); + /* The wrap is full-width but the panel is centred — keep the wrap itself + click-through so its transparent sides never block the nav beneath it. + Only the visible banner panel (.event-banner) captures clicks. */ + pointer-events: none; } .event-banner-wrap.visible { - grid-template-rows: 1fr; -} -/* When closing, fade the content out before the row collapses */ -.event-banner-wrap:not(.visible) .event-banner-stage { - opacity: 0; + transform: translateY(0); } /* Slide crossfade: both banners rendered simultaneously, fading into each other */ @@ -38,13 +37,13 @@ to { opacity: 0; } } -/* Container that holds both slides during a crossfade */ -/* Also fades in/out with the wrap open/close */ +/* Container that holds both slides during a crossfade. Constrains the + flyout to a centred panel width and clips the absolute outgoing slide. */ .event-banner-stage { - opacity: 1; - transition: opacity 0.35s ease; position: relative; - margin-bottom: 12px; + overflow: hidden; + width: 100%; + max-width: 720px; } /* True crossfade — outgoing slide sits absolutely on top and fades out */ @@ -52,11 +51,14 @@ display: flex; align-items: center; gap: 14px; - padding: 13px 18px; - border-radius: 4px; + padding: 14px 18px; + border-radius: 0 0 12px 12px; /* rounded bottom — it hangs from the top edge */ position: relative; z-index: 1; border-left: 4px solid rgba(255,255,255,0.25); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3); + cursor: pointer; /* whole banner is click-to-close */ + pointer-events: auto; /* re-enable clicks on the panel itself */ } .event-banner--outgoing { position: absolute; @@ -67,10 +69,6 @@ pointer-events: none; animation: banner-fade-out 0.45s ease both; } -.event-banner-stage { - position: relative; - overflow: hidden; -} .event-banner-emoji { font-size: 24px; flex-shrink: 0; @@ -285,8 +283,7 @@ /* ── EVENT BANNER — mobile fixes ────────────────────────────────────── */ @media (max-width: 640px) { .event-banner-wrap { - margin-top: 0; /* JS pins the banner's top to the nav's bottom edge */ - margin-bottom: 0; + padding: 0; /* full-bleed flyout on small screens */ } .event-banner { @@ -294,7 +291,7 @@ gap: 10px; padding: 18px 12px 14px 14px; margin-bottom: 0; - border-radius: 4px; + border-radius: 0 0 12px 12px; } .event-banner-emoji { diff --git a/assets/js/app.js b/assets/js/app.js index 6b16e21..e6a42e4 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -382,10 +382,11 @@ export function UTCIForecast() { -
- -
+ +
${activeEvents.length > 0 && (() => { const renderSlide = (idx, isOutgoing) => { const ev = activeEvents[idx] || activeEvents[0]; @@ -406,6 +407,9 @@ export function UTCIForecast() { return html`
dismissBanner(ev.id)} + role=${isOutgoing ? undefined : 'button'} + title=${isOutgoing ? undefined : 'Click to dismiss'} > ${ev.emoji}
@@ -422,7 +426,7 @@ export function UTCIForecast() { bannerSlideTo(i)} + onClick=${(e) => { e.stopPropagation(); bannerSlideTo(i); }} style=${{ background: ev.textColor }} /> `)} @@ -432,7 +436,7 @@ export function UTCIForecast() { ${!isOutgoing && html``} @@ -448,6 +452,7 @@ export function UTCIForecast() { })()}
+

diff --git a/assets/js/hooks/useAppState.js b/assets/js/hooks/useAppState.js index 7074844..62feccb 100644 --- a/assets/js/hooks/useAppState.js +++ b/assets/js/hooks/useAppState.js @@ -557,65 +557,10 @@ export function useAppState() { if (bannerIndex >= activeEvents.length) setBannerIndex(0); }, [activeEvents.length]); - // ── BANNER POSITION + SHELL PADDING ─────────────────────────────────── - // The banner is position:absolute so it floats over content. We (1) pin its - // top to the bottom edge of the site-nav so it sits flush below the nav on - // every screen size, and (2) add matching padding-top to the shell so the - // page content sits just below the banner. - // On close: keep the banner pinned to the nav and animate the padding back - // to 0 so content slides up smoothly as the banner collapses. - const bannerMaxHeightRef = useRef(0); - - useEffect(() => { - const shell = document.querySelector('.utci-shell'); - const wrap = document.querySelector('.event-banner-wrap'); - if (!shell || !wrap) return; - - // The site-nav's outer height is the banner's resting top edge. The banner - // lives inside the shell, so translate that into the shell's coordinate - // space by subtracting the shell's own offset from the page top. - const bannerTop = () => { - const navEl = document.getElementById('site-nav'); - const navOuterHeight = navEl ? navEl.offsetHeight : 0; - return navOuterHeight - shell.offsetTop; - }; - - if (!bannerVisible) { - // Keep the collapsing banner pinned to the nav's bottom edge, and animate - // padding back to zero matching the banner collapse duration. - wrap.style.top = bannerTop() + 'px'; - shell.style.transition = 'padding-top 0.45s cubic-bezier(0.4, 0, 0.2, 1)'; - shell.style.paddingTop = '0px'; - const tid = setTimeout(() => { - shell.style.transition = ''; - shell.style.paddingTop = ''; - bannerMaxHeightRef.current = 0; - }, 500); - return () => clearTimeout(tid); - } - - const applyPadding = (allowShrink = false) => { - const top = bannerTop(); - wrap.style.top = top + 'px'; - const h = top + wrap.getBoundingClientRect().height; - if (allowShrink || h > bannerMaxHeightRef.current) { - bannerMaxHeightRef.current = h; - shell.style.transition = ''; // no transition while open/resizing - shell.style.paddingTop = h + 'px'; - } - }; - - // Pin the banner immediately so it appears flush under the nav, then size - // the content padding once the open animation settles. - wrap.style.top = bannerTop() + 'px'; - const onResize = () => applyPadding(true); - const tid = setTimeout(applyPadding, 460); - window.addEventListener('resize', onResize); - return () => { - clearTimeout(tid); - window.removeEventListener('resize', onResize); - }; - }, [bannerVisible, bannerIndex, activeEvents.length]); + // The banner is now a position:fixed flyout pinned to the very top of the + // screen (see .event-banner-wrap in ui.css). It floats over all content and + // slides in/out via a transform, so no JS nav-pinning or shell padding is + // needed. // ── RETURN ALL STATE + HANDLERS ─────────────────────────────────────── return { diff --git a/data/tracking.json b/data/tracking.json index 1efa5cf..5ae3688 100644 --- a/data/tracking.json +++ b/data/tracking.json @@ -18,7 +18,7 @@ } }, "2026-06-11": { - "visits": 15, + "visits": 27, "profiles": [] } } \ No newline at end of file diff --git a/index.html b/index.html index 2938ca5..459a46f 100644 --- a/index.html +++ b/index.html @@ -70,10 +70,10 @@ - +
- +