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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
5549
kez-chat/web/package-lock.json
generated
@ -24,10 +24,12 @@
|
|||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@tsconfig/svelte": "^5.0.0",
|
"@tsconfig/svelte": "^5.0.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.6.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 { defineConfig } from "vite";
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
export default defineConfig({
|
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: {
|
server: {
|
||||||
// For dev: proxy API calls to the locally-running chat-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.
|
// The deployed SPA (served by the same chat-server) doesn't need this.
|
||||||
|
|||||||