diff --git a/index.html b/index.html
index c85cf4b..5b5cf95 100644
--- a/index.html
+++ b/index.html
@@ -6,6 +6,8 @@
+
+
diff --git a/public/favicon-192x192.png b/public/favicon-192x192.png
new file mode 100644
index 0000000..3887e80
Binary files /dev/null and b/public/favicon-192x192.png differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..3231cf2
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,49 @@
+{
+ "name": "Grimoire - A Nostr Client for Magicians",
+ "short_name": "Grimoire",
+ "description": "A tiling window manager interface for exploring the Nostr protocol. Each window is a Nostr app (profile viewer, event feed, NIP documentation, etc.). Commands are launched Unix-style via Cmd+K palette.",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#020817",
+ "theme_color": "#b366ff",
+ "orientation": "any",
+ "icons": [
+ {
+ "src": "/favicon-16x16.png",
+ "sizes": "16x16",
+ "type": "image/png"
+ },
+ {
+ "src": "/favicon-32x32.png",
+ "sizes": "32x32",
+ "type": "image/png"
+ },
+ {
+ "src": "/apple-touch-icon.png",
+ "sizes": "180x180",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/favicon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/favicon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ],
+ "categories": ["social", "utilities", "productivity"],
+ "shortcuts": [
+ {
+ "name": "Command Palette",
+ "short_name": "Commands",
+ "description": "Open the command palette to launch Nostr apps",
+ "url": "/?action=command-palette"
+ }
+ ]
+}
diff --git a/public/sw.js b/public/sw.js
new file mode 100644
index 0000000..8824bde
--- /dev/null
+++ b/public/sw.js
@@ -0,0 +1,84 @@
+// Grimoire Service Worker - v1.0.0
+const CACHE_NAME = "grimoire-v1";
+const RUNTIME_CACHE = "grimoire-runtime";
+
+// Core assets to cache on install
+const PRECACHE_URLS = [
+ "/",
+ "/index.html",
+ "/favicon.ico",
+ "/favicon-192x192.png",
+ "/favicon-512x512.png",
+];
+
+// Install event - precache core assets
+self.addEventListener("install", (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => {
+ return cache.addAll(PRECACHE_URLS);
+ }),
+ );
+ // Activate immediately
+ self.skipWaiting();
+});
+
+// Activate event - clean up old caches
+self.addEventListener("activate", (event) => {
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames
+ .filter((name) => name !== CACHE_NAME && name !== RUNTIME_CACHE)
+ .map((name) => caches.delete(name)),
+ );
+ }),
+ );
+ // Take control immediately
+ self.clients.claim();
+});
+
+// Fetch event - network first, fallback to cache
+self.addEventListener("fetch", (event) => {
+ // Skip non-GET requests
+ if (event.request.method !== "GET") return;
+
+ // Skip cross-origin requests
+ if (!event.request.url.startsWith(self.location.origin)) return;
+
+ // Network first strategy for app shell and assets
+ event.respondWith(
+ fetch(event.request)
+ .then((response) => {
+ // Clone the response before caching
+ const responseClone = response.clone();
+
+ // Cache successful responses
+ if (response.status === 200) {
+ caches.open(RUNTIME_CACHE).then((cache) => {
+ cache.put(event.request, responseClone);
+ });
+ }
+
+ return response;
+ })
+ .catch(() => {
+ // Fallback to cache if network fails
+ return caches.match(event.request).then((cachedResponse) => {
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ // If no cache, return offline page for navigation requests
+ if (event.request.mode === "navigate") {
+ return caches.match("/index.html");
+ }
+
+ // For other requests, just fail
+ return new Response("Offline", {
+ status: 503,
+ statusText: "Service Unavailable",
+ });
+ });
+ }),
+ );
+});
diff --git a/src/main.tsx b/src/main.tsx
index dd08235..3959323 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -35,3 +35,17 @@ createRoot(document.getElementById("root")!).render(
,
);
+
+// Register service worker for PWA functionality
+if ("serviceWorker" in navigator) {
+ window.addEventListener("load", () => {
+ navigator.serviceWorker
+ .register("/sw.js")
+ .then((registration) => {
+ console.log("SW registered:", registration);
+ })
+ .catch((error) => {
+ console.log("SW registration failed:", error);
+ });
+ });
+}