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

@ -232,8 +232,16 @@ function syncedAgo(ts: number | null): string {
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.form a { color: var(--accent); }
.conn { display: flex; align-items: center; gap: 14px; padding: 14px 16px; }
.conn .grow { min-width: 0; }
.conn-icon { font-size: 22px; }
.err { color: var(--danger); }
.err { color: var(--danger); overflow-wrap: anywhere; }
@media (max-width: 640px) {
.conn { flex-wrap: wrap; }
.conn .btn {
width: 100%;
}
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
</style>

View file

@ -101,4 +101,17 @@ const statusLabel = { pending: 'Offen', accepted: 'Angenommen', declined: 'Abgel
font-size: 14px;
white-space: pre-wrap;
}
@media (max-width: 640px) {
.req-head {
flex-direction: column;
align-items: stretch;
}
.req-actions {
justify-content: stretch;
}
.req-actions .btn {
flex: 1;
}
}
</style>

View file

@ -43,4 +43,13 @@ async function copy() {
white-space: nowrap;
color: var(--accent);
}
@media (max-width: 560px) {
.link-row .link {
flex-basis: 100%;
}
.link-row .btn {
width: 100%;
}
}
</style>

View file

@ -11,3 +11,12 @@ const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
// PWA: Service Worker registrieren (nur im Production-Build; in dev stört er HMR).
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {
/* PWA offline-fallback nicht verfügbar App läuft trotzdem */
})
})
}

View file

@ -70,6 +70,10 @@ html[data-theme='light'] {
--ok: #0c9e63;
}
html {
-webkit-text-size-adjust: 100%;
}
* { box-sizing: border-box; }
html, body {
@ -88,11 +92,22 @@ body {
linear-gradient(180deg, var(--bg-1), var(--bg-0));
background-attachment: fixed;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
letter-spacing: 0.01em;
overflow-wrap: break-word;
transition: color 0.2s ease;
}
#app { min-height: 100vh; }
/* dvh berücksichtigt die dynamischen Browser-Leisten auf Mobilgeräten */
#app {
min-height: 100vh;
min-height: 100dvh;
}
/* Doppel-Tap-Zoom auf Steuerflächen unterbinden */
.btn, .tab, .slot-chip, .day-chip, .tile, .weekday {
touch-action: manipulation;
}
/* Farbliche "Orbs" hinter allem geben dem Glass etwas zum Brechen */
.bg-scene { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
@ -372,5 +387,23 @@ textarea.input { resize: vertical; min-height: 84px; }
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(6px); }
@media (max-width: 640px) {
.page { padding: 16px 14px 64px; }
.page {
padding: 14px 12px calc(64px + env(safe-area-inset-bottom, 0px));
}
/* 16px verhindert den automatischen Zoom-Fokus von iOS Safari */
.input, select.input, textarea.input {
font-size: 16px;
}
.btn {
padding: 12px 18px;
}
.slot-chip {
padding: 14px 0;
}
.slot-grid {
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
}
.toasts {
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
}
}

View file

@ -276,8 +276,7 @@ onMounted(async () => {
</template>
<style scoped>
.booking { display: flex; flex-direction: column; gap: 16px; max-width: 780px; }
.state {
.booking { display: flex; flex-direction: column; gap: 16px; max-width: 780px; }.state {
margin-top: 18vh;
padding: 48px 36px;
display: flex;
@ -299,7 +298,7 @@ onMounted(async () => {
box-shadow: 0 14px 44px rgba(70, 224, 165, 0.4);
}
.head { padding: 22px 24px; display: flex; flex-direction: column; gap: 12px; }
.head h1 { margin: 0; font-size: 22px; }
.head h1 { margin: 0; font-size: 22px; overflow-wrap: anywhere; }
.legend { display: flex; align-items: center; gap: 8px; color: var(--text-muted); }
.legend-row { justify-content: space-between; flex-wrap: wrap; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
@ -311,4 +310,13 @@ onMounted(async () => {
.chosen { margin: 0; font-size: 15px; }
.form { display: flex; flex-direction: column; gap: 14px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
@media (max-width: 640px) {
.state { margin-top: 8vh; padding: 36px 22px; }
.head { padding: 18px 16px; }
.head h1 { font-size: 19px; }
.block { padding: 16px; }
.day-chip { min-width: 64px; }
.form button[type='submit'] { width: 100%; }
}
</style>

View file

@ -125,6 +125,8 @@ function logout() {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
padding: 14px 20px;
}
.brand { font-size: 17px; letter-spacing: -0.01em; }

View file

@ -53,6 +53,7 @@ import ThemeToggle from '../components/ThemeToggle.vue'
gap: 22px;
justify-content: center;
min-height: 100vh;
min-height: 100dvh;
padding-top: 6vh;
}
@ -106,4 +107,10 @@ h1 {
.foot { text-align: center; }
a.btn { text-decoration: none; display: inline-block; }
@media (max-width: 640px) {
.hero { padding: 46px 20px 38px; }
.row.center { flex-direction: column; align-items: stretch; }
.features { gap: 12px; }
}
</style>

View file

@ -65,6 +65,7 @@ async function submit() {
<style scoped>
.auth {
min-height: 100vh;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;

View file

@ -73,6 +73,7 @@ async function submit() {
<style scoped>
.auth {
min-height: 100vh;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;