feat: add site icon and favicon (#137)

* feat: add site icon and favicon

- Downloaded and cropped Grimoire icon from nostr.build
- Created multiple favicon sizes (16x16, 32x32, 180x180, 512x512)
- Generated traditional .ico format and PNG variants
- Added Apple touch icon for iOS devices
- Updated index.html with proper favicon links

* feat: make Grimoire installable as a PWA

- Created web manifest (site.webmanifest) with app metadata
  - Added app name, description, theme colors
  - Configured standalone display mode for native-like experience
  - Included all required icon sizes (192x192, 512x512)
  - Added keyboard shortcut for command palette
- Generated 192x192 icon for PWA requirements
- Added manifest and theme-color meta tags to index.html
- Implemented service worker (sw.js) for offline functionality
  - Network-first caching strategy for optimal performance
  - Precaches core assets on install
  - Provides offline fallback for navigation requests
- Registered service worker in main.tsx

Users can now install Grimoire as a standalone app on desktop and mobile devices.

* fix: properly configure maskable PWA icons

The previous configuration incorrectly marked regular icons as "maskable",
which would cause them to be cropped when displayed in circular or rounded
shapes on Android devices.

Changes:
- Created dedicated maskable icons with 10% padding (safe zone)
- Maskable icons use dark background (#020817) matching app theme
- Separated "any" and "maskable" purposes in manifest
- Regular icons (192x192, 512x512) use full space with purpose="any"
- Maskable icons (192x192-maskable, 512x512-maskable) have padding with purpose="maskable"

This ensures icons display correctly in all contexts:
- Regular icons for browser tabs, shortcuts, splash screens
- Maskable icons for adaptive icon shapes on Android

* chore: simplify PWA manifest

- Simplify name to just 'Grimoire'
- Add 'nostr' to categories for better discoverability
- Remove shortcuts (not needed for initial launch)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-18 18:39:07 +01:00
committed by GitHub
parent b70eb82fea
commit 1756715e30
12 changed files with 157 additions and 1 deletions

View File

@@ -2,7 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#b366ff" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
public/favicon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
public/favicon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

53
public/site.webmanifest Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "Grimoire",
"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"
},
{
"src": "/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/favicon-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/favicon-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"categories": ["social", "utilities", "productivity", "nostr"]
}

84
public/sw.js Normal file
View File

@@ -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",
});
});
}),
);
});

View File

@@ -35,3 +35,17 @@ createRoot(document.getElementById("root")!).render(
</ThemeProvider>
</ErrorBoundary>,
);
// 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);
});
});
}