[claude] PWA manifest + service worker — offline + home screen install (#14) #21
BIN
icons/icon-192.png
Normal file
BIN
icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
icons/icon-512.png
Normal file
BIN
icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
30
index.html
30
index.html
@@ -19,6 +19,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Nexus — Timmy's Sovereign Home</title>
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<meta name="theme-color" content="#4af0c0">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<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="The Nexus">
|
||||
<link rel="apple-touch-icon" href="./icons/icon-192.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
@@ -33,6 +40,11 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Offline Banner -->
|
||||
<div id="offline-banner" role="status" aria-live="polite">
|
||||
◈ NEXUS OFFLINE MODE — Running on cached assets
|
||||
</div>
|
||||
|
||||
<!-- Loading Screen -->
|
||||
<div id="loading-screen">
|
||||
<div class="loader-content">
|
||||
@@ -118,5 +130,23 @@
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
<script>
|
||||
// Offline/online detection
|
||||
const offlineBanner = document.getElementById('offline-banner');
|
||||
function updateOnlineStatus() {
|
||||
offlineBanner.classList.toggle('visible', !navigator.onLine);
|
||||
}
|
||||
window.addEventListener('online', updateOnlineStatus);
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
updateOnlineStatus();
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('./sw.js')
|
||||
.then(reg => console.log('[Nexus SW] Registered:', reg.scope))
|
||||
.catch(err => console.warn('[Nexus SW] Registration failed:', err));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
25
manifest.json
Normal file
25
manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "The Nexus — Timmy's Sovereign Home",
|
||||
"short_name": "The Nexus",
|
||||
"description": "Timmy's sovereign 3D space. A crystalline hub outside time.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "landscape",
|
||||
"background_color": "#050510",
|
||||
"theme_color": "#4af0c0",
|
||||
"categories": ["games", "entertainment"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
style.css
21
style.css
@@ -359,3 +359,24 @@ canvas#nexus-canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* === OFFLINE INDICATOR === */
|
||||
#offline-banner {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 6px 16px;
|
||||
background: rgba(255, 170, 34, 0.15);
|
||||
border-bottom: 1px solid var(--color-warning);
|
||||
color: var(--color-warning);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xs);
|
||||
text-align: center;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
#offline-banner.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
112
sw.js
Normal file
112
sw.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// ◈ The Nexus — Service Worker
|
||||
// Offline-first caching for sovereign space
|
||||
|
||||
const CACHE_VERSION = 'nexus-v1';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const CDN_CACHE = `${CACHE_VERSION}-cdn`;
|
||||
|
||||
// Core local assets — always cache
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/style.css',
|
||||
'/app.js',
|
||||
'/manifest.json',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
];
|
||||
|
||||
// CDN assets for Three.js — cache on first fetch
|
||||
const CDN_ORIGINS = [
|
||||
'https://cdn.jsdelivr.net',
|
||||
'https://fonts.googleapis.com',
|
||||
'https://fonts.gstatic.com',
|
||||
];
|
||||
|
||||
// ═══ INSTALL — pre-cache static assets ═══
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then(cache => cache.addAll(STATIC_ASSETS))
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// ═══ ACTIVATE — clean up old caches ═══
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys
|
||||
.filter(key => key.startsWith('nexus-') && key !== STATIC_CACHE && key !== CDN_CACHE)
|
||||
.map(key => caches.delete(key))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// ═══ FETCH — serve from cache, fall back to network ═══
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
// CDN resources: cache-first with network fallback
|
||||
if (CDN_ORIGINS.some(origin => request.url.startsWith(origin))) {
|
||||
event.respondWith(cdnFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Same-origin static assets: cache-first
|
||||
if (url.origin === self.location.origin) {
|
||||
event.respondWith(staticFirst(request));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Cache-first for CDN (Three.js modules, fonts)
|
||||
async function cdnFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(CDN_CACHE);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
// Offline and not cached — return a minimal error response
|
||||
return new Response('/* offline */', {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cache-first for local static assets
|
||||
async function staticFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
// Offline fallback: serve index.html for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
const fallback = await caches.match('/index.html');
|
||||
if (fallback) return fallback;
|
||||
}
|
||||
return new Response('Nexus offline — cached assets not found.', {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user