feat: add PWA support and mobile-responsive layouts

- Add web app manifest, maskable/apple-touch icons, and a production-only
  service worker that caches the app shell for offline start
- Improve mobile UX: responsive breakpoints from ~320px, 44px touch
  targets, safe-area insets, dvh heights, 16px inputs to avoid iOS zoom
- Serve .webmanifest with correct content type in the Go static handler
- Document PWA behavior and mobile features in the README
This commit is contained in:
Tronax 2026-08-25 19:12:12 +02:00
parent f220d96e3b
commit cbc03c56bf
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
18 changed files with 277 additions and 9 deletions

View file

@ -1,8 +1,95 @@
import { defineConfig } from 'vite'
import { defineConfig, type Plugin } from 'vite'
import vue from '@vitejs/plugin-vue'
// Service Worker als Build-Artefakt: Die Cache-Version ist der Build-Zeitstempel,
// damit Browser nach jedem Deploy den SW aktualisieren und alte Caches räumen.
const SW_SOURCE = `
const VERSION = 'wp-__SW_VERSION__';
const CACHE_NAME = 'wannpassts-' + VERSION;
const PRECACHE = [
'/',
'/manifest.webmanifest',
'/favicon.svg',
'/icons/icon-192.png',
'/icons/icon-512.png',
'/icons/icon-maskable-512.png',
'/icons/apple-touch-icon.png',
];
self.addEventListener('install', (event) => {
event.waitUntil((async () => {
const cache = await caches.open(CACHE_NAME);
await cache.addAll(PRECACHE).catch(() => {});
await self.skipWaiting();
})());
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)));
await self.clients.claim();
})());
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;
// API-Aufrufe immer live niemals aus dem Cache bedienen.
if (url.pathname.startsWith('/api/')) return;
// Navigation: Netzwerk zuerst, offline App-Shell aus dem Cache.
if (req.mode === 'navigate') {
event.respondWith((async () => {
try {
const res = await fetch(req);
const cache = await caches.open(CACHE_NAME);
cache.put('/', res.clone());
return res;
} catch (err) {
const cached = (await caches.match(req)) || (await caches.match('/'));
if (cached) return cached;
return new Response('Offline bitte Internetverbindung prüfen.', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
})());
return;
}
// Statische Assets: Cache zuerst (gehashte Namen sind unveränderlich).
event.respondWith((async () => {
const cached = await caches.match(req, { ignoreSearch: true });
if (cached) return cached;
const res = await fetch(req);
if (res.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(req, res.clone());
}
return res;
})());
});
`
function serviceWorkerPlugin(): Plugin {
return {
name: 'wannpassts-sw',
apply: 'build',
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'sw.js',
source: SW_SOURCE.replace('__SW_VERSION__', new Date().toISOString()),
})
},
}
}
export default defineConfig({
plugins: [vue()],
plugins: [vue(), serviceWorkerPlugin()],
server: {
port: 5173,
proxy: {