Tap a Coming up card -> hourly-detail modal, 20s idle auto-close

Backend now stores hourly points for every day the provider returns (not
just today) -- for Open-Meteo this is free, the hourly= block already
covers the full forecast_days=8 window in the one call, today's strip just
discarded everything past today. Met Office's hourly product window isn't
verified this far out (only daily has been exercised live so far); an
empty result for a given date just means "no hourly detail", handled as a
graceful fallback, not an error.

New GET /api/public/forecast/hourly?date=YYYY-MM-DD backs the modal. The
main /api/public/forecast payload's `hourly` field is now its own scoped
query (today only) rather than filtering the much bigger stored set in JS.

Frontend: tapping any "Coming up" card (tomorrow/day-after or days 4-7)
opens DayModal with that day's hourly points, falling back to the
already-known 3-hourly points (or the single daily summary) if the
provider doesn't have hourly detail that far out. Auto-closes after 20s of
no interaction (pointerdown/keydown/wheel/scroll all reset the timer), with
a depleting progress bar so the countdown isn't a surprise. Extracted
PointMeta/AstroRow and the date/format helpers out of Display.tsx into
shared modules since DayModal needs them too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 13:26:57 +00:00
parent c6be990f6b
commit 13fb30d9a3
10 changed files with 370 additions and 75 deletions

View file

@ -239,6 +239,76 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
gap: 12px; background: var(--navy-dark); color: var(--text-muted);
}
/* Every "Coming up" card opens the hourly-detail modal on tap — see DayModal. */
.wx-tappable { cursor: pointer; transition: background .12s, transform .08s; }
.wx-tappable:hover { background: var(--surface-2); }
.wx-tappable:active { transform: scale(0.97); }
/* ── Day-detail modal (opened by tapping a "Coming up" card) ────────────── */
.wx-modal-overlay {
position: fixed;
inset: 0;
background: rgba(15,15,32,0.72);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
z-index: 500;
}
.wx-modal {
background: var(--navy);
border: 1px solid var(--surface-2);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
width: min(92vw, 920px);
max-height: 85vh;
padding: clamp(18px, 2.6vh, 32px);
display: flex;
flex-direction: column;
gap: 16px;
color: var(--text);
overflow: hidden;
}
.wx-modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.wx-modal-title { font-size: clamp(1.3rem, 2.4vw, 2rem); font-weight: 700; color: var(--gold); }
.wx-modal-close {
background: none; border: none; color: var(--text-muted); cursor: pointer;
padding: 4px; display: flex; flex-shrink: 0;
}
.wx-modal-close:hover { color: var(--text); }
.wx-modal-note { color: var(--text-muted); font-size: clamp(0.8rem, 1vw, 0.95rem); margin: 0; }
.wx-modal-empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 10px; color: var(--text-muted); padding: 40px 0;
}
.wx-modal-hourly {
display: flex;
align-items: stretch;
gap: clamp(8px, 1.2vw, 16px);
overflow-x: auto;
padding-bottom: 4px;
}
.wx-modal-point {
flex: 1 0 clamp(64px, 7vw, 90px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: clamp(10px, 1.4vh, 16px) 6px;
border-radius: var(--radius);
background: var(--surface-2);
}
.wx-modal-point-time { font-size: clamp(0.72rem, 0.85vw, 0.9rem); color: var(--text-muted); }
.wx-modal-point-temp { font-size: clamp(1.1rem, 1.3vw, 1.4rem); font-weight: 600; }
/* Idle-timeout progress bar — depletes over the 20s auto-close window, restarting
(via the DayModal's resetTick key, which remounts this element) on interaction. */
.wx-modal-timeout-track { height: 3px; background: var(--surface-2); border-radius: 2px; overflow: hidden; flex-shrink: 0; }
.wx-modal-timeout-bar { height: 100%; background: var(--gold); animation: wx-timeout-deplete 20s linear forwards; }
@keyframes wx-timeout-deplete { from { width: 100%; } to { width: 0%; } }
/* ── Admin (authed) page ──────────────────────────────────────────────── */
.wx-admin-shell { min-height: 100vh; background: var(--body-bg); display: flex; justify-content: center; padding: 32px 16px; }
.wx-admin-panel { width: 100%; max-width: 560px; }