Redesign main app: slim top bar, dynamic sky hero, nowcast, scrub chart, unified palette

- One slate/blue palette across the app, dashboard, account/login pages (data colours
  stay orange = warm, blue = cool on the dashboard)
- Slim sticky top bar with location chip, gear (settings) and ? (learn); 4-tab nav
  (Now, Station, Map, Alerts) with a thumb-reach bottom bar on phones
- Hero sky gradient by condition/time with rain, storm, snow and star effects
  (respects reduced motion); keeps the searched city and badges when temps come from your station
- One-line summary, 'Next 2 hours' bars and rain countdown from 15-minute precipitation,
  rain/cold-aware 'Bring' chips; yesterday + 'vs usual' context from the station
- Scrubbable hourly chart (temp, rain chance, wind), 7-day range bars, AQI gauge,
  details tiles instead of drawers, taller radar with location marker + alert polygons
- Skeleton loading instead of a full-screen spinner, sticky section headers, safe areas
- Server: minutely_15 precipitation, public /api/station/summary

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 5.5 committed 2026-10-15 12:56:18 -04:00
1 parent 1830fb2e2b
commit fd61e35b56
16 files changed
+846 -221

No files matched your search

+22 -16
View File
@@ -746,7 +746,7 @@ function renderSavedCities() {
// ── Tab switching ─────────────────────────────────────────────────────────────
function activateTab(tabName) {
document.querySelectorAll('.tab, .bottom-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab, .bottom-tab, .icon-btn[data-tab]').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => hide(p));
document.querySelectorAll(`[data-tab="${tabName}"]`).forEach(t => t.classList.add('active'));
show(document.getElementById('tab-' + tabName));
@@ -768,7 +768,7 @@ function activateTab(tabName) {
refreshSavedCityWeather();
}
}
document.querySelectorAll('.tab, .bottom-tab').forEach(btn => {
document.querySelectorAll('.tab, .bottom-tab, .icon-btn[data-tab]').forEach(btn => {
btn.addEventListener('click', () => activateTab(btn.dataset.tab));
});
@@ -1609,6 +1609,8 @@ function renderWeather(data, city) {
// Apply station overlay if we have fresh station data
if (state.ambientDevice) applyStationOverlay(state.ambientDevice);
if (window.AppUI) window.AppUI.onWeather(data, city);
}
function renderDailyForecast(daily) {
@@ -1619,16 +1621,22 @@ function renderDailyForecast(daily) {
const maxDays = state.showExtendedForecast ? Math.min(16, totalDays) : Math.min(7, totalDays);
list.innerHTML = '';
const hiAll = daily.temperature_2m_max.slice(0, maxDays), loAll = daily.temperature_2m_min.slice(0, maxDays);
const wkMin = Math.min(...loAll), wkMax = Math.max(...hiAll), span = Math.max(wkMax - wkMin, 1);
for (let i = 0; i < maxDays; i++) {
const dw = wmo(daily.weather_code[i]);
const lo = daily.temperature_2m_min[i], hi = daily.temperature_2m_max[i];
const left = ((lo - wkMin) / span) * 100, width = Math.max(((hi - lo) / span) * 100, 6);
const pop = daily.precipitation_probability_max[i];
const row = document.createElement('div');
row.className = 'day-row' + (i === 0 ? ' today' : '');
row.innerHTML = `
<div class="day-name">${formatDayShort(daily.time[i])}</div>
<div class="day-icon">${dw.icon}</div>
<div class="day-desc">${dw.desc}</div>
<div class="day-temps">${tempDisplay(daily.temperature_2m_max[i])}°<span class="day-lo"> / ${tempDisplay(daily.temperature_2m_min[i])}°</span></div>
<div class="day-meta">${daily.precipitation_probability_max[i] > 0 ? daily.precipitation_probability_max[i] + '% rain<br>' : ''}${speedDisplay(daily.wind_speed_10m_max[i])} ${speedUnit()}</div>
<div class="day-icon" title="${dw.desc}">${dw.icon}</div>
<div class="day-pop">${pop > 0 ? `<span class="pop-drop">💧</span>${pop}%` : ''}</div>
<div class="day-lo-t">${tempDisplay(lo)}°</div>
<div class="day-range" title="${dw.desc} · wind ${speedDisplay(daily.wind_speed_10m_max[i])} ${speedUnit()}"><span class="day-range-bar" style="left:${left.toFixed(1)}%;width:${width.toFixed(1)}%"></span></div>
<div class="day-hi-t">${tempDisplay(hi)}°</div>
`;
list.appendChild(row);
}
@@ -1661,6 +1669,7 @@ function renderAirQuality(data) {
if (panelAqi) panelAqi.textContent = cur.us_aqi ?? '--';
setMapOverlayReadouts({ aqi: cur.us_aqi ?? '--' });
if (window.AppUI) window.AppUI.onAir(data);
}
function normalizeSeverity(severity) {
@@ -2658,7 +2667,7 @@ syncOfflineState();
// ── Swipe gestures for tab switching ──────────────────────────────────────────
(function initSwipe() {
const TABS = ['conditions', 'station', 'unified-map', 'alerts', 'learn', 'settings'];
const TABS = ['conditions', 'station', 'unified-map', 'alerts'];
let touchX = 0;
let touchY = 0;
let blocked = false;
@@ -3085,16 +3094,13 @@ function applyStationOverlay(device) {
peekEl.textContent = `· ${fS(data.windspeedmph)} ${speedUnit()} ${compassDir(data.winddir || 0)} · ${data.humidity ?? '--'}% humidity`;
}
// Replace location name with station name
const stationName = device.info?.name || device.macAddress || 'My Station';
const locNameEl = document.getElementById('loc-name');
const locSubEl = document.getElementById('loc-sub');
if (locNameEl) locNameEl.textContent = stationName;
if (locSubEl) locSubEl.textContent = 'Ambient Weather Station';
// Hide the hero badge (redundant — location header now identifies the station)
// Keep the searched location's name; mark that temperatures now come from your own station
const stationName = (state.stationName || device.info?.name || 'My Station');
const badge = document.getElementById('station-badge');
if (badge) hide(badge);
const badgeName = document.getElementById('station-badge-name');
if (badgeName) badgeName.textContent = `Your station · ${stationName}`;
if (badge) show(badge);
if (window.AppUI) window.AppUI.onStation(device, data);
}
function renderAmbientStation(device) {