Add PWA support (manifest, service worker, icons, safe-area CSS)

This commit is contained in:
Nate committed 2026-03-16 10:12:23 -04:00
1 parent 0a2616c5d6
commit 02f49e55f8
5 files changed
+145 -2

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!-- Solid background (fills corners for maskable safe zone) -->
<rect width="512" height="512" fill="#1a1a1a"/>
<!-- Sun -->
<circle cx="205" cy="205" r="70" fill="#e07848"/>
<!-- Cloud bumps (front, overlapping sun) -->
<circle cx="215" cy="325" r="72" fill="#ece7dc"/>
<circle cx="305" cy="297" r="88" fill="#ece7dc"/>
<circle cx="390" cy="328" r="62" fill="#ece7dc"/>
<!-- Cloud base (flat bottom) -->
<rect x="147" y="335" width="305" height="68" fill="#ece7dc"/>
</svg>

After

Width:  |  Height:  |  Size: 559 B

+14 -1
View File
@@ -2,8 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Weather</title>
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icons/icon.svg">
<meta name="theme-color" content="#f5f2ec" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#191510" media="(prefers-color-scheme: dark)">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Weather">
<meta name="mobile-web-app-capable" content="yes">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Playfair+Display:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
@@ -134,5 +142,10 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="app.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Weather",
"short_name": "Weather",
"description": "Self-hosted weather app with animated radar. No API keys required.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#191510",
"theme_color": "#1a1a1a",
"orientation": "portrait-primary",
"categories": ["weather", "utilities"],
"icons": [
{
"src": "/icons/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}
+33 -1
View File
@@ -454,6 +454,38 @@ a:hover { text-decoration: underline; }
padding-top: 0.75rem;
}
/* ── PWA: safe area insets (iPhone notch / home bar) ── */
@supports (padding: env(safe-area-inset-bottom)) {
.app {
padding-left: max(1.5rem, calc(1rem + env(safe-area-inset-left)));
padding-right: max(1.5rem, calc(1rem + env(safe-area-inset-right)));
padding-bottom: max(4rem, calc(2rem + env(safe-area-inset-bottom)));
}
.radar-map-full {
height: calc(100vh - 280px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
}
}
/* ── PWA: status bar area when running standalone ── */
@media (display-mode: standalone) {
.masthead { padding-top: env(safe-area-inset-top); }
}
/* ── Touch-friendly tap targets (min 44 × 44 px) ── */
.tab {
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.search-row input,
.search-row button { min-height: 44px; }
.radar-btn { width: 44px; height: 44px; font-size: 0.8rem; }
.tl-tick { min-height: 36px; }
/* iOS momentum scrolling */
.hourly-scroll { -webkit-overflow-scrolling: touch; }
/* ── Leaflet overrides ── */
.leaflet-container { background: #1a1f2e; }
.leaflet-control-zoom a {
@@ -464,7 +496,7 @@ a:hover { text-decoration: underline; }
/* ── Responsive ── */
@media (max-width: 640px) {
.app { padding: 1.25rem 1rem 3rem; }
.app { padding: 1.25rem 1rem max(3rem, calc(1.5rem + env(safe-area-inset-bottom, 0px))); }
.masthead { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; }
.masthead h1 { font-size: 1.6rem; }
.tagline { display: none; }
+66
View File
@@ -0,0 +1,66 @@
'use strict';
const CACHE = 'weather-v1';
// App shell — cached on install for offline use
const PRECACHE = [
'/',
'/app.js',
'/style.css',
'/manifest.json',
'/icons/icon.svg'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE)
.then(cache => cache.addAll(PRECACHE))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
))
.then(() => self.clients.claim())
);
});
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET, API calls, and map/radar tile requests
if (
request.method !== 'GET' ||
url.pathname.startsWith('/api/') ||
url.hostname.includes('rainviewer') ||
url.hostname.includes('openstreetmap') ||
url.hostname.includes('cartodb') ||
url.hostname.includes('basemaps')
) {
return;
}
event.respondWith(
caches.match(request).then(cached => {
if (cached) return cached;
return fetch(request).then(response => {
// Only cache successful same-origin and trusted CDN responses
if (response.ok && (
url.origin === self.location.origin ||
url.hostname.includes('googleapis') ||
url.hostname.includes('gstatic') ||
url.hostname.includes('unpkg.com')
)) {
const clone = response.clone();
caches.open(CACHE).then(cache => cache.put(request, clone));
}
return response;
});
})
);
});