Web: gzip, 44-px-Tippflächen mobil, Fokusring, Landmarks, größeres Inhaltsverzeichnis

GZipMiddleware (Buchdaten 1–2 MB → ~400 kB), Werkzeugleiste und
Inhaltsverzeichnis mobil mindestens 44 px, :focus-visible, header/main,
Sprunglink, prefers-reduced-motion, Inhaltsverzeichnis 14 px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-05 16:37:56 +02:00
parent 09908b69c6
commit bbae194554
3 changed files with 19 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, PlainTextResponse from fastapi.responses import FileResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from . import config, montage, pipeline, state from . import config, montage, pipeline, state
@@ -21,6 +22,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="buchwerk", lifespan=lifespan) app = FastAPI(title="buchwerk", lifespan=lifespan)
app.add_middleware(GZipMiddleware, minimum_size=1000) # Buchdaten sind 1–2 MB JSON
class ThemaNeu(BaseModel): class ThemaNeu(BaseModel):

View File

@@ -102,6 +102,7 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
<template> <template>
<div class="app" :class="{ 'tab-buch': tab === 'buch' }"> <div class="app" :class="{ 'tab-buch': tab === 'buch' }">
<a class="skip" href="#inhalt">Zum Inhalt springen</a>
<div v-if="navAuf" class="backdrop" @click="navAuf = false"></div> <div v-if="navAuf" class="backdrop" @click="navAuf = false"></div>
<aside class="sidebar" :class="{ offen: navAuf }"> <aside class="sidebar" :class="{ offen: navAuf }">
<button class="schliessen sekundaer klein" @click="navAuf = false">✕ Schließen</button> <button class="schliessen sekundaer klein" @click="navAuf = false">✕ Schließen</button>
@@ -122,7 +123,7 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
</aside> </aside>
<div class="haupt"> <div class="haupt">
<div class="topbar"> <header class="topbar">
<button class="burger sekundaer klein" title="Themen" @click="navAuf = !navAuf">☰</button> <button class="burger sekundaer klein" title="Themen" @click="navAuf = !navAuf">☰</button>
<span class="titel">{{ aktuell?.titel || '' }}</span> <span class="titel">{{ aktuell?.titel || '' }}</span>
<div class="tabs"> <div class="tabs">
@@ -138,16 +139,16 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
<option value="hell">hell</option> <option value="hell">hell</option>
<option value="auto">auto</option> <option value="auto">auto</option>
</select> </select>
</div> </header>
<div v-if="fehler" class="fehler" @click="fehler = ''">{{ fehler }} — klicken zum Schließen</div> <div v-if="fehler" class="fehler" @click="fehler = ''">{{ fehler }} — klicken zum Schließen</div>
<div class="inhalt"> <main class="inhalt" id="inhalt" tabindex="-1">
<Lauf v-if="tab === 'lauf' && state" :state="state" <Lauf v-if="tab === 'lauf' && state" :state="state"
@start="aktion(() => api.start(topic))" @start="aktion(() => api.start(topic))"
@pause="aktion(() => api.pause(topic))" @pause="aktion(() => api.pause(topic))"
@reset="aktion(() => api.reset(topic))" /> @reset="aktion(() => api.reset(topic))" />
<Guide v-else-if="tab === 'buch' && topic" :topic="topic" :laeuft="laeuft" :fortschritt="fortschritt" /> <Guide v-else-if="tab === 'buch' && topic" :topic="topic" :laeuft="laeuft" :fortschritt="fortschritt" />
<div v-else style="padding: 24px; color: var(--dim)">Thema links anlegen oder wählen.</div> <div v-else style="padding: 24px; color: var(--dim)">Thema links anlegen oder wählen.</div>
</div> </main>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -121,7 +121,7 @@ a.knopf { display: inline-block; text-decoration: none; line-height: normal; }
/* Buch-Ansicht */ /* Buch-Ansicht */
.guide { flex: 1; display: flex; min-height: 0; } .guide { flex: 1; display: flex; min-height: 0; }
.guide-nav { width: 270px; min-width: 240px; overflow-y: auto; padding: 14px 10px; border-right: 1px solid var(--rand); background: var(--panel); font-size: 13px; } .guide-nav { width: 270px; min-width: 240px; overflow-y: auto; padding: 14px 10px; border-right: 1px solid var(--rand); background: var(--panel); font-size: 14px; }
.guide-nav .werkzeuge { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; } .guide-nav .werkzeuge { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.guide-nav .meta { color: var(--dim); font-size: 12px; margin-bottom: 10px; } .guide-nav .meta { color: var(--dim); font-size: 12px; margin-bottom: 10px; }
.guide-nav a.kap { display: flex; align-items: baseline; gap: 6px; padding: 6px 8px; border-radius: 6px; cursor: pointer; color: var(--text); text-decoration: none; font-weight: 600; margin-top: 6px; } .guide-nav a.kap { display: flex; align-items: baseline; gap: 6px; padding: 6px 8px; border-radius: 6px; cursor: pointer; color: var(--text); text-decoration: none; font-weight: 600; margin-top: 6px; }
@@ -129,7 +129,7 @@ a.knopf { display: inline-block; text-decoration: none; line-height: normal; }
.guide-nav a.kap .t { flex: 1; } .guide-nav a.kap .t { flex: 1; }
.guide-nav a.abs { display: flex; justify-content: space-between; gap: 6px; padding: 3px 8px 3px 24px; border-radius: 6px; cursor: pointer; color: var(--dim); text-decoration: none; } .guide-nav a.abs { display: flex; justify-content: space-between; gap: 6px; padding: 3px 8px 3px 24px; border-radius: 6px; cursor: pointer; color: var(--dim); text-decoration: none; }
.guide-nav a.aktiv { background: var(--karte); color: var(--text); } .guide-nav a.aktiv { background: var(--karte); color: var(--text); }
.guide-nav .zeit { color: var(--dim); font-size: 11px; white-space: nowrap; } .guide-nav .zeit { color: var(--dim); font-size: 12px; white-space: nowrap; }
.guide-nav .fortschritt { height: 4px; background: var(--karte); border-radius: 2px; margin: 8px 0 4px; overflow: hidden; } .guide-nav .fortschritt { height: 4px; background: var(--karte); border-radius: 2px; margin: 8px 0 4px; overflow: hidden; }
.guide-nav .fortschritt i { display: block; height: 100%; background: var(--gruen); } .guide-nav .fortschritt i { display: block; height: 100%; background: var(--gruen); }
.guide-nav .leer { color: var(--dim); padding: 8px; } .guide-nav .leer { color: var(--dim); padding: 8px; }
@@ -201,6 +201,12 @@ a.knopf { display: inline-block; text-decoration: none; line-height: normal; }
.lm-fuss button.weiter { background: var(--akzent); color: #fff; border: 0; } .lm-fuss button.weiter { background: var(--akzent); color: #fff; border: 0; }
kbd { font: 11px system-ui, sans-serif; border: 1px solid var(--rand); border-radius: 4px; padding: 0 5px; margin-left: 6px; color: var(--dim); } kbd { font: 11px system-ui, sans-serif; border: 1px solid var(--rand); border-radius: 4px; padding: 0 5px; margin-left: 6px; color: var(--dim); }
/* Barrierefreiheit: sichtbarer Fokus für Tastatur, Sprunglink, keine Animationen auf Wunsch */
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.skip { position: absolute; left: -999px; top: 8px; z-index: 100; background: var(--akzent); color: #fff; padding: 8px 12px; border-radius: 6px; }
.skip:focus { left: 8px; }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; } }
@media (max-width: 800px) { @media (max-width: 800px) {
.topbar { gap: 8px; padding: 8px 10px; flex-wrap: nowrap; } .topbar { gap: 8px; padding: 8px 10px; flex-wrap: nowrap; }
.topbar .badge { display: none; } .topbar .badge { display: none; }
@@ -212,8 +218,10 @@ kbd { font: 11px system-ui, sans-serif; border: 1px solid var(--rand); border-ra
.burger { display: inline-block; } .burger { display: inline-block; }
.guide-nav { display: none; } .guide-nav { display: none; }
.guide-nav.offen { display: block; position: fixed; inset: 0 auto 0 0; z-index: 25; width: 82vw; font-size: 14.5px; } .guide-nav.offen { display: block; position: fixed; inset: 0 auto 0 0; z-index: 25; width: 82vw; font-size: 14.5px; }
.guide-nav.offen a.kap { padding: 10px 8px; } .guide-nav.offen a.kap { padding: 12px 8px; min-height: 44px; box-sizing: border-box; }
.guide-nav.offen a.abs { padding: 8px 8px 8px 24px; } .guide-nav.offen a.abs { padding: 10px 8px 10px 24px; min-height: 44px; box-sizing: border-box; align-items: center; }
.mobil-leiste button, .mobil-leiste a.knopf, .burger, .topbar .tabs button, .lm-kopf button, .guide-nav .schliessen { min-height: 44px; min-width: 44px; padding: 10px 14px; font-size: 14px; }
.mobil-leiste { flex-wrap: wrap; }
.buch { padding: 22px 16px 100px; font-size: 1.02rem; } .buch { padding: 22px 16px 100px; font-size: 1.02rem; }
.lm-screen { padding: 24px 16px 60px; font-size: 1.05rem; } .lm-screen { padding: 24px 16px 60px; font-size: 1.05rem; }
.lm-kopf, .lm-fuss { padding: 8px 12px; } .lm-kopf, .lm-fuss { padding: 8px 12px; }