Compare commits
1 Commits
30ddfced6d
...
e3f474662e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f474662e |
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 |
@@ -1,30 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#050510"/>
|
||||
<stop offset="100%" stop-color="#0a0f28"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sigil-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#4af0c0"/>
|
||||
<stop offset="100%" stop-color="#7b5cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" rx="96" fill="url(#bg-grad)"/>
|
||||
<!-- Outer ring -->
|
||||
<circle cx="256" cy="256" r="220" fill="none" stroke="url(#sigil-grad)" stroke-width="4" opacity="0.5"/>
|
||||
<!-- Inner ring -->
|
||||
<circle cx="256" cy="256" r="180" fill="none" stroke="url(#sigil-grad)" stroke-width="2" opacity="0.35"/>
|
||||
<!-- Triangle -->
|
||||
<polygon points="256,80 420,360 92,360" fill="none" stroke="#4af0c0" stroke-width="5" opacity="0.75"/>
|
||||
<!-- Inner triangle (inverted) -->
|
||||
<polygon points="256,432 92,152 420,152" fill="none" stroke="#7b5cff" stroke-width="3" opacity="0.45"/>
|
||||
<!-- Core crystal -->
|
||||
<circle cx="256" cy="256" r="32" fill="#4af0c0" opacity="0.9"/>
|
||||
<circle cx="256" cy="256" r="20" fill="#7b5cff" opacity="0.8"/>
|
||||
<circle cx="256" cy="256" r="10" fill="#ffffff" opacity="0.95"/>
|
||||
<!-- Corner accent marks -->
|
||||
<circle cx="256" cy="36" r="5" fill="#4af0c0" opacity="0.8"/>
|
||||
<circle cx="476" cy="360" r="5" fill="#4af0c0" opacity="0.8"/>
|
||||
<circle cx="36" cy="360" r="5" fill="#4af0c0" opacity="0.8"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,22 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#050510"/>
|
||||
<stop offset="100%" stop-color="#0a0f28"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sigil-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#4af0c0"/>
|
||||
<stop offset="100%" stop-color="#7b5cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Full bleed background for maskable -->
|
||||
<rect width="512" height="512" fill="url(#bg-grad)"/>
|
||||
<!-- Safe zone content (within 80% = 204px padding 51px each side) -->
|
||||
<circle cx="256" cy="256" r="195" fill="none" stroke="url(#sigil-grad)" stroke-width="3" opacity="0.5"/>
|
||||
<circle cx="256" cy="256" r="160" fill="none" stroke="url(#sigil-grad)" stroke-width="2" opacity="0.3"/>
|
||||
<polygon points="256,100 392,340 120,340" fill="none" stroke="#4af0c0" stroke-width="5" opacity="0.75"/>
|
||||
<polygon points="256,412 120,172 392,172" fill="none" stroke="#7b5cff" stroke-width="3" opacity="0.4"/>
|
||||
<circle cx="256" cy="256" r="30" fill="#4af0c0" opacity="0.9"/>
|
||||
<circle cx="256" cy="256" r="18" fill="#7b5cff" opacity="0.85"/>
|
||||
<circle cx="256" cy="256" r="9" fill="#ffffff" opacity="0.95"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
24
index.html
24
index.html
@@ -19,14 +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>
|
||||
<!-- PWA -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<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/nexus-icon.svg">
|
||||
<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">
|
||||
@@ -41,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">
|
||||
@@ -127,10 +131,20 @@
|
||||
|
||||
<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')
|
||||
.catch((err) => console.warn('[Nexus SW] Registration failed:', err));
|
||||
navigator.serviceWorker.register('./sw.js')
|
||||
.then(reg => console.log('[Nexus SW] Registered:', reg.scope))
|
||||
.catch(err => console.warn('[Nexus SW] Registration failed:', err));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
{
|
||||
"name": "The Nexus — Timmy's Sovereign Home",
|
||||
"short_name": "The Nexus",
|
||||
"description": "Timmy's sovereign 3D space — a cosmic hub existing outside time, accessible from anywhere.",
|
||||
"description": "Timmy's sovereign 3D space. A crystalline hub outside time.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"orientation": "landscape",
|
||||
"background_color": "#050510",
|
||||
"theme_color": "#4af0c0",
|
||||
"categories": ["entertainment", "games"],
|
||||
"categories": ["games", "entertainment"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/nexus-icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
"src": "icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/nexus-maskable.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
"src": "icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [],
|
||||
"prefer_related_applications": false
|
||||
]
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
149
sw.js
149
sw.js
@@ -1,153 +1,112 @@
|
||||
// ═══════════════════════════════════════════
|
||||
// THE NEXUS — Service Worker
|
||||
// Offline support + asset caching
|
||||
// ═══════════════════════════════════════════
|
||||
// ◈ 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 cached on install
|
||||
// Core local assets — always cache
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/style.css',
|
||||
'/app.js',
|
||||
'/manifest.json',
|
||||
'/icons/nexus-icon.svg',
|
||||
'/icons/nexus-maskable.svg',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
];
|
||||
|
||||
// CDN assets — cached on first use (Three.js + fonts)
|
||||
const CDN_HOSTS = [
|
||||
'cdn.jsdelivr.net',
|
||||
'fonts.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
// 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 ═══
|
||||
self.addEventListener('install', (event) => {
|
||||
// ═══ INSTALL — pre-cache static assets ═══
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then((cache) => cache.addAll(STATIC_ASSETS))
|
||||
.then(cache => cache.addAll(STATIC_ASSETS))
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// ═══ ACTIVATE ═══
|
||||
self.addEventListener('activate', (event) => {
|
||||
// ═══ ACTIVATE — clean up old caches ═══
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(
|
||||
.then(keys => Promise.all(
|
||||
keys
|
||||
.filter((key) => key.startsWith('nexus-') && key !== STATIC_CACHE && key !== CDN_CACHE)
|
||||
.map((key) => caches.delete(key))
|
||||
.filter(key => key.startsWith('nexus-') && key !== STATIC_CACHE && key !== CDN_CACHE)
|
||||
.map(key => caches.delete(key))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// ═══ FETCH ═══
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// ═══ FETCH — serve from cache, fall back to network ═══
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET and chrome-extension requests
|
||||
if (request.method !== 'GET' || url.protocol === 'chrome-extension:') {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// CDN assets — stale-while-revalidate
|
||||
if (CDN_HOSTS.includes(url.hostname)) {
|
||||
event.respondWith(staleWhileRevalidate(CDN_CACHE, request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Local static assets — cache-first
|
||||
// Same-origin static assets: cache-first
|
||||
if (url.origin === self.location.origin) {
|
||||
event.respondWith(cacheFirst(STATIC_CACHE, request));
|
||||
event.respondWith(staticFirst(request));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ═══ STRATEGIES ═══
|
||||
// Cache-first for CDN (Three.js modules, fonts)
|
||||
async function cdnFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
/**
|
||||
* Cache-first: serve from cache, fall back to network, update cache.
|
||||
* Best for versioned static assets.
|
||||
*/
|
||||
async function cacheFirst(cacheName, request) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.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 {
|
||||
// Return offline fallback for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
const fallback = await cache.match('/index.html');
|
||||
if (fallback) return fallback;
|
||||
}
|
||||
return offlineResponse(request);
|
||||
// Offline and not cached — return a minimal error response
|
||||
return new Response('/* offline */', {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stale-while-revalidate: serve cached immediately, refresh in background.
|
||||
* Best for CDN assets where freshness is nice but not critical.
|
||||
*/
|
||||
async function staleWhileRevalidate(cacheName, request) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.match(request);
|
||||
// Cache-first for local static assets
|
||||
async function staticFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
const fetchPromise = fetch(request).then((response) => {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}).catch(() => null);
|
||||
|
||||
return cached || fetchPromise || offlineResponse(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal offline response for requests that have no cached version.
|
||||
*/
|
||||
function offlineResponse(request) {
|
||||
if (request.destination === 'document' || request.mode === 'navigate') {
|
||||
return new Response(
|
||||
`<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Nexus — Offline</title>
|
||||
<style>
|
||||
body { margin: 0; background: #050510; color: #4af0c0; font-family: monospace;
|
||||
display: flex; align-items: center; justify-content: center; height: 100vh; text-align: center; }
|
||||
h1 { font-size: 2rem; letter-spacing: 0.3em; margin-bottom: 1rem; }
|
||||
p { color: #7b5cff; letter-spacing: 0.1em; }
|
||||
.sigil { font-size: 4rem; margin-bottom: 2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div class="sigil">◈</div>
|
||||
<h1>THE NEXUS</h1>
|
||||
<p>Sovereign space is offline.</p>
|
||||
<p style="margin-top:1rem;opacity:0.5;">Reconnect to re-enter.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
{ status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
|
||||
);
|
||||
} 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' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('', { status: 503, statusText: 'Service Unavailable' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user