feat(kez-chat/web): make the SPA installable as a PWA
Now installable on iOS (Safari → Share → Add to Home Screen) and
Android/desktop Chrome (install prompt or Settings → Install app).
Launches in standalone mode with a dark theme color matching the
lock-icon palette.
Stack:
• vite-plugin-pwa with workbox in generateSW mode, registerType
'autoUpdate' — new SW activates on next page load, no upgrade prompt
(chat needs to stay fresh).
• @vite-pwa/assets-generator for icon variants from a single SVG.
Source kez-icon.svg = dark squircle (#111827) + amber key glyph,
drawn inside the 80% maskable safe zone.
Caching:
• Precaches the SPA shell (~635 KB inc. the zstd WASM, well under
the 5 MB per-file cap).
• runtimeCaching 'NetworkOnly' for /v1/* — never cache authenticated
chat data; every poll must hit the network.
• navigateFallback to index.html so /messages, /claims, /dashboard
survive a refresh while offline. The /v1/, /internal/, /.well-known/
paths are explicitly denylisted from this fallback.
Meta tags (index.html):
• <link rel="manifest"> + theme-color for Android Chrome.
• apple-touch-icon-180x180 + apple-mobile-web-app-* meta for iOS,
including status-bar-style=black-translucent so the dark header
flows into the notch area in standalone.
• viewport-fit=cover so safe-area-inset works on notched devices.
Generated artifacts committed under web/public/:
kez-icon.svg, pwa-{64,192,512}.png, maskable-icon-512x512.png,
apple-touch-icon-180x180.png, favicon.ico.
Verified live: /manifest.webmanifest serves application/manifest+json,
/sw.js serves text/javascript, all icons return 200.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ -2,9 +2,24 @@
|
||||
<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>kez-chat</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='26' font-size='28'>🔑</text></svg>" />
|
||||
<meta name="description" content="End-to-end encrypted chat on top of KEZ — portable cross-app identity." />
|
||||
|
||||
<!-- Browser tab + Android Chrome favicon -->
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||
<link rel="icon" type="image/svg+xml" href="/kez-icon.svg" />
|
||||
|
||||
<!-- iOS Add-to-Home-Screen icon + status-bar styling -->
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||
<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="kez-chat" />
|
||||
|
||||
<!-- Web App Manifest (generated by vite-plugin-pwa) + Android theme color -->
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
5549
kez-chat/web/package-lock.json
generated
@ -24,10 +24,12 @@
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@vite-pwa/assets-generator": "^1.0.2",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^5.4.0"
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-pwa": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
kez-chat/web/public/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 749 B |
BIN
kez-chat/web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 535 B |
17
kez-chat/web/public/kez-icon.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<!-- Solid background. 20% padding around the glyph leaves the safe
|
||||
area for Android "maskable" icons (radial crops, squircle, etc.). -->
|
||||
<rect width="512" height="512" rx="96" ry="96" fill="#111827"/>
|
||||
<!-- A simple key shape: round bow + rectangular shaft + two teeth.
|
||||
Drawn at the center; bounding box ~280px wide, well inside the 80%
|
||||
safe zone (~410px diameter). -->
|
||||
<g transform="translate(106 156)" fill="none" stroke="#fbbf24" stroke-width="28" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Bow (ring) -->
|
||||
<circle cx="80" cy="100" r="64"/>
|
||||
<!-- Shaft (rounded line from the bow to the right edge) -->
|
||||
<line x1="144" y1="100" x2="300" y2="100"/>
|
||||
<!-- Two teeth on the bottom of the shaft -->
|
||||
<line x1="240" y1="100" x2="240" y2="140"/>
|
||||
<line x1="280" y1="100" x2="280" y2="156"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 926 B |
BIN
kez-chat/web/public/maskable-icon-512x512.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
kez-chat/web/public/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 917 B |
BIN
kez-chat/web/public/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
kez-chat/web/public/pwa-64x64.png
Normal file
|
After Width: | Height: | Size: 429 B |
10
kez-chat/web/pwa-assets.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {
|
||||
defineConfig,
|
||||
minimal2023Preset as preset,
|
||||
} from "@vite-pwa/assets-generator/config";
|
||||
|
||||
export default defineConfig({
|
||||
headLinkOptions: { preset: "2023" },
|
||||
preset,
|
||||
images: ["public/kez-icon.svg"],
|
||||
});
|
||||
@ -1,9 +1,66 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), tailwindcss()],
|
||||
plugins: [
|
||||
svelte(),
|
||||
tailwindcss(),
|
||||
VitePWA({
|
||||
// Auto-update: a new SW activates on next page load. No "click to
|
||||
// update" prompt — chat needs to stay fresh and we don't want users
|
||||
// stuck on an old build.
|
||||
registerType: "autoUpdate",
|
||||
injectRegister: "auto",
|
||||
manifest: {
|
||||
name: "kez-chat",
|
||||
short_name: "kez-chat",
|
||||
description:
|
||||
"End-to-end encrypted chat on top of KEZ — portable cross-app identity.",
|
||||
start_url: "/",
|
||||
scope: "/",
|
||||
display: "standalone",
|
||||
background_color: "#111827",
|
||||
theme_color: "#111827",
|
||||
categories: ["social", "communication"],
|
||||
icons: [
|
||||
{ src: "pwa-64x64.png", sizes: "64x64", type: "image/png" },
|
||||
{ src: "pwa-192x192.png", sizes: "192x192", type: "image/png" },
|
||||
{ src: "pwa-512x512.png", sizes: "512x512", type: "image/png" },
|
||||
{
|
||||
src: "maskable-icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
// Precache the SPA shell. Chat data is fetched live from /v1/*
|
||||
// and we DON'T want it cached — see runtimeCaching below.
|
||||
globPatterns: ["**/*.{js,css,html,svg,png,ico,wasm}"],
|
||||
// zstd wasm is ~350 KB; raise the per-file cap.
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
// Same-origin navigation requests fall back to the SPA shell so
|
||||
// /messages, /claims, etc. work after a refresh while offline.
|
||||
navigateFallback: "index.html",
|
||||
navigateFallbackDenylist: [/^\/v1\//, /^\/internal\//, /^\/\.well-known\//],
|
||||
runtimeCaching: [
|
||||
{
|
||||
// Never cache API responses — they're authenticated + dynamic.
|
||||
urlPattern: /\/v1\//,
|
||||
handler: "NetworkOnly",
|
||||
},
|
||||
],
|
||||
navigationPreload: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
},
|
||||
devOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
// For dev: proxy API calls to the locally-running chat-server.
|
||||
// The deployed SPA (served by the same chat-server) doesn't need this.
|
||||
|
||||