diff --git a/assets/css/webdesign.css b/assets/css/webdesign.css index 4eec4fbf..82a1dda3 100644 --- a/assets/css/webdesign.css +++ b/assets/css/webdesign.css @@ -905,7 +905,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover { /* Vollbildnavigation auf dem Handy (Masterplan S.5), aber scrollbar — bei 8 Punkten + Sprachschalter passt sonst nicht alles auf ein kleines Display. */ - max-height: calc(100vh - 70px); + max-height: calc(100dvh - 70px); overflow-y: auto; } .wd-nav.wd-offen { display: flex; } @@ -1710,7 +1710,16 @@ button.po-aufg-haken:hover { border-color: var(--wd-gold); background: rgb(var(- margin: 1.2rem 0 1.6rem; } .rl-nav a { - padding: .42rem .85rem; border-radius: 999px; + /* inline-flex + min-height 44px statt nur Innenabstand. + + Mit reinem padding kamen die vier Sprungmarken auf 39px -- fuenf + Pixel unter dem Daumenmass. Genau derselbe Fall wie frueher bei + .wd-nav a (dort waren es 43px): Ein Innenabstand, der auf dem + Bildschirm gut aussieht, ergibt nicht automatisch ein treffbares + Ziel. Und ausgerechnet auf der Rechtsseite muss man verlaesslich + zum Widerrufsrecht springen koennen. */ + display: inline-flex; align-items: center; min-height: 44px; + padding: .42rem .95rem; border-radius: 999px; border: 1px solid var(--wd-rand); background: var(--wd-glas); font-size: .88rem; color: var(--wd-text-mid) !important; text-decoration: none; } @@ -2130,3 +2139,113 @@ a.wd-karte:active, .wd-karte--klickbar:active { @media (prefers-reduced-motion: reduce) { .wd-karte::after { display: none; } } + +/* ===================================================================== + HANDY UND INSTALLIERTE APP — Sicherheitsabstände + + AUSGANGSLAGE (gefunden am 23.08.2026) + Die Seite trug bereits `apple-mobile-web-app-status-bar-style: + black-translucent`. Das heißt wörtlich: Der Seiteninhalt beginnt bei + y=0 und läuft UNTER Uhr, Empfangsbalken und Akkuanzeige durch. Auf + einem iPhone mit Aussparung stand die Kopfzeile also im installierten + Betrieb hinter der Statusleiste. + + Gleichzeitig fehlte `viewport-fit=cover` auf allen dreizehn Seiten. + Ohne diese Angabe liefert jedes `env(safe-area-inset-*)` schlicht + null. Die eine Stelle, die den unteren Abstand bereits benutzte (der + Installations-Hinweis), war damit wirkungslos — sie sah richtig aus + und tat nichts. Genau die Sorte Fehler, die man nie bemerkt, weil der + Code stimmt. + + LÖSUNG IN ZWEI TEILEN + 1. `viewport-fit=cover` in jeder Seite (dort erledigt). + 2. Die Abstände hier einmal zentral benennen, statt sie an fünfzehn + Stellen einzeln zu schreiben. Wer sie einzeln schreibt, vergisst + eine — und dann liegt genau ein Knopf unter der Gestenleiste. + + WARUM `max()` UND NICHT NUR `env()` + Auf einem Gerät ohne Aussparung ist der Wert 0. Ein Kopfbereich mit + 0 Abstand klebt aber am oberen Rand. `max(env(...), )` + gibt beiden Fällen ein brauchbares Ergebnis: Notch-Geräte bekommen + den echten Abstand, alle anderen den normalen. + ===================================================================== */ +:root { + --wd-sicher-oben: env(safe-area-inset-top, 0px); + --wd-sicher-unten: env(safe-area-inset-bottom, 0px); + --wd-sicher-links: env(safe-area-inset-left, 0px); + --wd-sicher-rechts: env(safe-area-inset-right, 0px); +} + +/* Die Kopfzeile klebt oben (position: sticky). Im Browser sitzt sie + unter der Adressleiste und braucht nichts. Installiert gibt es keine + Adressleiste mehr — dann muss sie sich selbst unter die Statusleiste + schieben, sonst steht die Marke hinter der Uhr. */ +.wd-kopf { + padding-top: var(--wd-sicher-oben); +} + +/* Querformat: Bei gedrehtem Gerät liegt die Aussparung seitlich. Ohne + diesen Abstand verschwindet die linke Spalte halb darunter. `92vw` + allein hilft nicht — vw zählt die Aussparung mit. */ +.wd-container, +.wd-container--schmal { + padding-left: var(--wd-sicher-links); + padding-right: var(--wd-sicher-rechts); +} + +/* Unten die Gestenleiste. Betrifft den Fußbereich und alles, was fest + am unteren Rand sitzt. */ +.wd-fuss { + padding-bottom: calc(1.5rem + var(--wd-sicher-unten)); +} + +@media (display-mode: standalone), (display-mode: window-controls-overlay), (display-mode: minimal-ui) { + /* ------------------------------------------------------------------ + NUR IM INSTALLIERTEN BETRIEB + + Installiert fehlt alles, was ein Browser sonst beisteuert: keine + Adressleiste, kein Zurück-Knopf, kein Neuladen durch Herunterziehen + im gewohnten Sinn. Was im Browser eine Kleinigkeit ist, wird hier + zum Fehler. + ------------------------------------------------------------------ */ + + /* Das Gummiband-Nachfedern am Seitenende sieht in einer App nach + einem Darstellungsfehler aus -- im Browser erwartet man es, in + einer App nicht. Innerhalb scrollbarer Bereiche bleibt Scrollen + natürlich unverändert. */ + html, body { + overscroll-behavior-y: none; + } + + /* Der Hinweis "Als App installieren" ist installiert sinnlos. Er wird + zwar schon per JavaScript unterdrückt -- aber nur, wenn das Skript + lädt. Diese Regel greift auch dann, wenn es das nicht tut. */ + .wd-install { display: none !important; } +} + +/* --------------------------------------------------------------------- + TIPP-VERHALTEN + + Auf Android legt der Browser über jede angetippte Fläche ein graues + Rechteck. Bei runden Kacheln und Knöpfen zeichnet es die eckige + Trefferfläche nach und sieht nach einem Fehler aus. Ersetzt wird es + nicht ersatzlos: Jeder Knopf hat einen sichtbaren :active-Zustand, + sonst fehlte die Rückmeldung, dass der Tipp angekommen ist. + --------------------------------------------------------------------- */ +.wd a, .wd button, .wd [role="button"], .wd summary, .wd label { + -webkit-tap-highlight-color: transparent; +} +.wd .wd-btn:active, +.wd .vw-ub-kachel:active, +.wd .vw-ub-projekt:active, +.wd .vw-filter-knopf:active { + /* Kein Verschieben, nur ein kurzes Einsinken: Bewegung nach unten + wird als Druck gelesen und braucht keine Farbe. */ + transform: scale(.985); +} +@media (prefers-reduced-motion: reduce) { + .wd .wd-btn:active, + .wd .vw-ub-kachel:active, + .wd .vw-ub-projekt:active, + .wd .vw-filter-knopf:active { transform: none; } +} diff --git a/pruef-detail.mjs b/pruef-detail.mjs index 0d572fbc..6b80de88 100644 --- a/pruef-detail.mjs +++ b/pruef-detail.mjs @@ -61,6 +61,19 @@ for (const [bezeichnung, breite, hoehe] of [["Computer", 1440, 900], ["Handy", 3 if (u.includes("/anfragen")) { return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE_KNAPP] } }); } + /* Die Uebersicht ist seit dem 23.08.2026 die Startansicht und wird + bei jedem Laden abgerufen. Ohne eigene Ersatzantwort landet sie im + Auffang unten, bekommt ein blankes {ok:true} und der Aufbau bricht + ab, bevor irgendetwas geprueft werden kann. */ + if (u.includes("/uebersicht")) { + return route.fulfill({ json: { ok: true, + beiMir: { anfragen: { n: 1 }, nachrichten: { n: 0 }, wuensche: { n: 0 }, + widerrufe: { n: 0 }, projekte: 0 }, + beiKunde: { projekte: 0, aufgaben: 0, zahlungen: { n: 0, summe: 0 }, zahlungenText: "0,00 €" }, + geld: { monat: { n: 0, brutto: 0, netto: 0, bruttoText: "0,00 €", nettoText: "0,00 €" }, + gesamt: { n: 0, bruttoText: "0,00 €" }, gebuehrenText: "0,00 €" }, + projekte: [], verlauf: [], bestand: {} } }); + } return route.fulfill({ json: { ok: true, kunden: [], projekte: [] } }); }); await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } })); @@ -78,6 +91,12 @@ for (const [bezeichnung, breite, hoehe] of [["Computer", 1440, 900], ["Handy", 3 console.log("\n=== " + bezeichnung + " (" + breite + "x" + hoehe + ") ==="); + /* Erst in die Anfragen wechseln. Die Verwaltung oeffnet seit dem + 23.08.2026 im Cockpit, nicht mehr in der Anfragenliste -- ohne + diesen Klick sucht der Test eine Karte, die es noch gar nicht gibt. */ + await seite.click('[data-tab="anfragen"]'); + await seite.waitForTimeout(600); + const karte = seite.locator(".vw-karte").first(); if (await karte.count()) { await karte.click(); diff --git a/pruef-handy.mjs b/pruef-handy.mjs new file mode 100644 index 00000000..71f21ae0 --- /dev/null +++ b/pruef-handy.mjs @@ -0,0 +1,597 @@ +/* ===================================================================== + pruef-handy.mjs — Handy im Browser UND als installierte App + + WARUM DIESE PRÜFUNG EIGENSTÄNDIG IST + + Ein enges Fenster auf dem Rechner ist kein Handy. Es hat keine + Aussparung für die Kamera, keine Gestenleiste, keine Statusleiste, die + über dem Inhalt liegt — und vor allem: keinen installierten Betrieb, + in dem die Adressleiste fehlt. Genau dort entstehen die Fehler, die + man am Schreibtisch nie sieht. + + Am 23.08.2026 stand deshalb `env(safe-area-inset-bottom)` im CSS und + tat nichts: Ohne `viewport-fit=cover` liefert jedes `env()` null. Der + Code sah richtig aus, das Ergebnis war null Abstand. Diese Prüfung + fängt genau solche Fälle. + + WIE DER INSTALLIERTE BETRIEB GEPRUEFT WIRD + + Nicht durch Emulation -- die gibt es nicht. Beide naheliegenden Wege + wurden versucht und sind gescheitert: + - Emulation.setEmulatedMedia nimmt das Merkmal display-mode + entgegen und antwortet mit {}, wendet es aber nicht an + (nachgemessen: matchMedia meldete weiterhin 'browser'). + - Ein echtes App-Fenster ueber --app= laesst Playwright nicht zu; + der Versuch blieb haengen und musste abgebrochen werden. + + Geprueft wird deshalb zweiteilig (Einzelheiten bei TEIL 3): erst, ob + der Medienblock existiert und woran er haengt, dann, ob sein Inhalt + wirkt, wenn man seine Bedingung zur Laufzeit scharf schaltet. Was das + NICHT beweist, steht dort ebenfalls -- lieber eine benannte Luecke als + eine gruene Zeile, die nichts belegt. + + WIE DIE AUSSPARUNG GEPRÜFT WIRD + + Die tatsächlichen Werte von `env(safe-area-inset-*)` kann kein + Emulator setzen. Prüfbar ist aber das, worauf es ankommt: dass die + Seite die Werte ÜBERHAUPT verwendet und dass ihre Rechnung stimmt. + Dafür werden die Variablen auf einen Notch-Wert gesetzt (wie bei einem + iPhone 14 Pro: 59px oben, 34px unten) und gemessen, ob die Kopfzeile + wirklich nach unten rückt. Bleibt sie stehen, ist der Abstand + wirkungslos — und genau das war der Ausgangsfehler. + ===================================================================== */ +import { chromium, devices } from "playwright"; +import { createServer } from "node:http"; +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { join, extname } from "node:path"; + +const W = process.cwd(); +const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", + ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", + ".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json", + ".webmanifest": "application/manifest+json" }; +const srv = createServer((q, r) => { + const p = join(W, decodeURIComponent(q.url.split("?")[0])); + if (existsSync(p) && !p.endsWith("/")) { + r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" }); + return r.end(readFileSync(p)); + } + r.writeHead(404).end(""); +}); +await new Promise((r) => srv.listen(4341, r)); +const URL_BASIS = "http://127.0.0.1:4341"; + +const ergebnisse = []; +function pruefe(name, ok, zusatz) { + ergebnisse.push({ name, ok }); + console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : "")); +} + +const SEITEN = readdirSync(join(W, "webdesign")).filter((f) => f.endsWith(".html")); + +/* Ersatzantworten, damit Portal und Verwaltung überhaupt etwas zeigen. + Ohne sie prüfte man eine leere Seite — die hat nie einen Überlauf. */ +const UEBERSICHT = { + ok: true, + beiMir: { anfragen: { n: 3, aeltestes: new Date(Date.now() - 8 * 864e5).toISOString() }, + nachrichten: { n: 2, aeltestes: new Date(Date.now() - 3 * 864e5).toISOString() }, + wuensche: { n: 1, aeltestes: new Date().toISOString() }, + widerrufe: { n: 1, aeltestes: new Date().toISOString() }, projekte: 2 }, + beiKunde: { projekte: 1, aufgaben: 4, + zahlungen: { n: 2, summe: 120000, aeltestes: new Date(Date.now() - 5 * 864e5).toISOString() }, + zahlungenText: "1.200,00 €" }, + geld: { monat: { n: 3, brutto: 250000, netto: 241000, bruttoText: "2.500,00 €", nettoText: "2.410,00 €" }, + gesamt: { n: 11, bruttoText: "9.800,00 €" }, gebuehrenText: "90,00 €" }, + projekte: [{ id: "p1", nummer: "P-2026-001", titel: "Website für Musterbau", status: "bau", + wartet_auf: "dogfather", richttermin: "2026-09-15", kunde_name: "Anna Muster", + aufg_gesamt: 20, aufg_fertig: 13 }], + verlauf: [{ bereich: "projekt", aktion: "Aufgabe erledigt: Startseite gebaut", + wer: "dogfather", erstellt_am: new Date().toISOString() }], + bestand: { kunden: 5, projekte: 2, anfragenGesamt: 9 }, + paypal: { eingerichtet: true, live: true }, +}; + +async function vorbereiten(seite) { + await seite.route("**/webdesign/admin/**", (route) => { + const u = new URL(route.request().url()).pathname; + if (u.endsWith("/admin/uebersicht")) return route.fulfill({ json: UEBERSICHT }); + if (u.includes("/admin/postfach-zaehler")) return route.fulfill({ json: { ok: true, ungelesen: 2 } }); + return route.fulfill({ json: { ok: true, anfragen: [], kunden: [], projekte: [], + zahlungen: [], verlaeufe: [], ungelesen: 0 } }); + }); + await seite.route("**/webdesign/portal/**", (r) => r.fulfill({ json: { ok: true } })); + await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } })); + await seite.addInitScript(() => { + sessionStorage.setItem("wd-vw-token", "t"); + sessionStorage.setItem("wd-sitzung-aktiv", "1"); + }); +} + +/* Schaltet die Seite in den installierten Betrieb. + + NICHT ueber Emulation -- die wirkt nicht (siehe Kopfkommentar). Statt + dessen wird die Bedingung des Medienblocks selbst auf 'all' + umgeschrieben. Damit gilt genau der Block, der auf einem installierten + Geraet gelten wuerde, mit genau seinem Inhalt. + + Das ist kein Nachbau: Es ist derselbe Block aus derselben Datei. Was + dabei NICHT mitgeprueft wird, ist die Frage, ob das Geraet die + Bedingung richtig auswertet -- das ist Browser-Standardverhalten. + + Ohne diesen Kunstgriff waeren die Durchgaenge mit der Beschriftung + 'installiert' in Wahrheit ein zweites Mal derselbe Browser-Durchgang + gewesen: zwoelf gruene Zeilen, die nichts Eigenes belegen. + Aufgefallen am 23.08.2026 beim Nachpruefen der Emulation. */ +async function alsAppSchalten(kontext, seite) { + await seite.evaluate(() => { + for (const blatt of document.styleSheets) { + let regeln; + try { regeln = blatt.cssRules; } catch (e) { continue; } // fremde Herkunft + for (const r of regeln) { + if (r.media && /display-mode/.test(r.conditionText || r.media.mediaText)) { + r.media.mediaText = "all"; + } + } + } + }); +} + +const browser = await chromium.launch(); + +/* ===================================================================== + TEIL 1 — jede Seite auf jedem Gerät, im Browser und installiert + ===================================================================== */ +const GERAETE = [ + ["iPhone 14 Pro", { ...devices["iPhone 14 Pro"] }], + ["Pixel 7", { ...devices["Pixel 7"] }], + /* 320px ist das schmalste Gerät, das man realistisch antrifft (iPhone + SE der ersten Reihe, und der Modus mit vergrößerter Schrift macht + aus jedem Handy faktisch ein schmaleres). Wer hier nicht überläuft, + läuft nirgends über. */ + ["sehr schmal (320px)", { viewport: { width: 320, height: 640 }, + isMobile: true, hasTouch: true, deviceScaleFactor: 2, + userAgent: devices["iPhone 14 Pro"].userAgent }], +]; + +for (const [geraet, angaben] of GERAETE) { + for (const modus of ["Browser", "installiert"]) { + console.log(`\n=== ${geraet} · ${modus} ===`); + const kontext = await browser.newContext(angaben); + let ueberlaufFunde = [], kleineZiele = [], zoomFelder = [], fehlerSeiten = []; + + for (const datei of SEITEN) { + const seite = await kontext.newPage(); + const protokoll = []; + seite.on("pageerror", (e) => protokoll.push(datei + ": " + e.message)); + await vorbereiten(seite); + + await seite.goto(URL_BASIS + "/webdesign/" + datei); + /* Erst NACH dem Laden. Playwright setzt beim Navigieren seine + eigene Medien-Emulation und ueberschreibt eine vorher gesetzte + wieder -- gemessen am 23.08.2026: matchMedia meldete trotz + gesetzter Emulation weiterhin false. Medienabfragen werden + ohnehin laufend neu ausgewertet, das Nachreichen genuegt. */ + if (modus === "installiert") await alsAppSchalten(kontext, seite); + await seite.waitForTimeout(500); + + const befund = await seite.evaluate(() => { + const d = document.documentElement; + const raus = { ueberlauf: Math.max(0, d.scrollWidth - d.clientWidth), zuKlein: [], zuKlein2: [] }; + + /* Touch-Ziele. Die WCAG-Vorgabe 2.5.8 nennt 24px als Mindestmaß, + Apple und Google nennen 44 bzw. 48. Geprüft wird gegen 44 — + mit einer Ausnahme: Elemente, die INNERHALB eines größeren + Bedienelements liegen (etwa ein Zähler-Abzeichen in einem + Knopf), sind kein eigenes Ziel und werden übersprungen. Ohne + diese Ausnahme meldet die Prüfung lauter Scheinfunde. */ + for (const el of document.querySelectorAll("a[href], button, input, select, textarea, summary, [role='button']")) { + const r = el.getBoundingClientRect(); + if (r.width < 1 || r.height < 1) continue; // unsichtbar + if (getComputedStyle(el).display === "none") continue; + + /* Ausserhalb des Bildes. Der Honigtopf im Anfrageformular + liegt bei left:-9999px -- absichtlich, denn display:none + wuerde von Spam-Skripten erkannt. Er ist fuer Menschen + unerreichbar und deshalb kein Bedienelement. */ + if (r.right < 0 || r.left > innerWidth) continue; + + /* Alles unter aria-hidden ist fuer niemanden gedacht. */ + if (el.closest('[aria-hidden="true"], [inert]')) continue; + + /* Teil eines groesseren Bedienelements (Abzeichen in einem + Knopf, Symbol in einem Link) -- kein eigenes Ziel. */ + const huelle = el.closest("button, a[href], [role='button']"); + if (huelle && huelle !== el) continue; + + /* Ein Eingabefeld in einer Beschriftung: Getippt wird auf die + BESCHRIFTUNG, sie ist das Ziel. Ein 1x1-Kaestchen mit einem + 48px hohen Label daneben ist voellig in Ordnung. */ + const label = el.closest("label"); + const gemessen = (label && el.tagName === "INPUT") ? label.getBoundingClientRect() : r; + + /* WCAG 2.5.8 nimmt Ziele ausdruecklich aus, die INLINE in + einem Fliesstext stehen ("inline" exception): Eine + E-Mail-Adresse mitten im Satz auf 44px aufzublasen wuerde + den Absatz zerreissen und niemandem helfen. Nur eigenstaendige + Bedienelemente werden gemessen. */ + if (el.tagName === "A" && getComputedStyle(el).display === "inline" && + el.closest("p, li, td, dd, blockquote")) continue; + + if (gemessen.height < 44 || gemessen.width < 24) { + raus.zuKlein.push((el.tagName + "." + (el.className || "").toString().slice(0, 26)).slice(0, 44) + + " " + Math.round(gemessen.width) + "x" + Math.round(gemessen.height)); + } + } + + /* Schrift unter 16px in einem Eingabefeld lässt iOS beim + Antippen in die Seite hineinzoomen — danach ist die halbe + Oberfläche aus dem Bild, und herauszoomen muss man von Hand. */ + for (const el of document.querySelectorAll("input, select, textarea")) { + const r = el.getBoundingClientRect(); + if (r.width < 1 || r.height < 1) continue; + if (r.right < 0 || r.left > innerWidth) continue; // Honigtopf + if (el.closest('[aria-hidden="true"], [inert]')) continue; + const t = (el.getAttribute("type") || "").toLowerCase(); + if (["hidden", "checkbox", "radio", "range", "submit", "button"].includes(t)) continue; + const g = parseFloat(getComputedStyle(el).fontSize); + if (g < 16) raus.zuKlein2.push((el.id || el.name || el.tagName) + " " + g + "px"); + } + return raus; + }); + + if (befund.ueberlauf > 1) ueberlaufFunde.push(datei + " (" + befund.ueberlauf + "px)"); + if (befund.zuKlein.length) kleineZiele.push(datei + ": " + befund.zuKlein.slice(0, 3).join(", ")); + if (befund.zuKlein2.length) zoomFelder.push(datei + ": " + befund.zuKlein2.slice(0, 3).join(", ")); + if (protokoll.length) fehlerSeiten.push(protokoll[0]); + await seite.close(); + } + + pruefe(`${geraet}/${modus}: keine Seite laeuft seitlich ueber`, + ueberlaufFunde.length === 0, ueberlaufFunde.join(" | ")); + pruefe(`${geraet}/${modus}: alle Bedienelemente sind gross genug zum Antippen`, + kleineZiele.length === 0, kleineZiele.slice(0, 4).join(" | ")); + pruefe(`${geraet}/${modus}: kein Eingabefeld loest den iOS-Zoom aus`, + zoomFelder.length === 0, zoomFelder.slice(0, 4).join(" | ")); + pruefe(`${geraet}/${modus}: kein JavaScript-Fehler auf irgendeiner Seite`, + fehlerSeiten.length === 0, fehlerSeiten.slice(0, 2).join(" | ")); + + await kontext.close(); + } +} + +/* ===================================================================== + TEIL 2 — die Aussparung: rueckt die Kopfzeile WIRKLICH nach? + ===================================================================== */ +console.log("\n=== Aussparung (Notch) und Gestenleiste ==="); +{ + const kontext = await browser.newContext(devices["iPhone 14 Pro"]); + const seite = await kontext.newPage(); + await vorbereiten(seite); + await seite.goto(URL_BASIS + "/webdesign/index.html"); + await seite.waitForTimeout(400); + + const messung = await seite.evaluate(() => { + const kopf = document.querySelector(".wd-kopf"); + const bar = document.querySelector(".wd-kopf-bar"); + const ohne = bar.getBoundingClientRect().top; + /* Die echten env()-Werte kann kein Emulator setzen. Die Seite rechnet + aber ueber eigene Variablen -- also werden diese auf die Masse + eines iPhone 14 Pro gesetzt. Rueckt nichts nach, ist der Abstand + wirkungslos. */ + document.documentElement.style.setProperty("--wd-sicher-oben", "59px"); + document.documentElement.style.setProperty("--wd-sicher-unten", "34px"); + kopf.getBoundingClientRect(); + const mit = bar.getBoundingClientRect().top; + return { ohne, mit, differenz: mit - ohne }; + }); + pruefe("die Kopfzeile rueckt unter die Statusleiste, wenn eine da ist", + Math.abs(messung.differenz - 59) < 2, + "verschoben um " + Math.round(messung.differenz) + "px (erwartet 59)"); + + const benutzt = await seite.evaluate(() => { + const w = getComputedStyle(document.documentElement); + return ["--wd-sicher-oben", "--wd-sicher-unten", "--wd-sicher-links", "--wd-sicher-rechts"] + .filter((v) => (w.getPropertyValue(v) || "").trim() !== "").length; + }); + pruefe("alle vier Sicherheitsabstaende sind definiert", benutzt === 4, benutzt + " von 4"); + + const vf = await seite.evaluate(() => + (document.querySelector('meta[name="viewport"]') || {}).content || ""); + pruefe("viewport-fit=cover ist gesetzt (sonst liefert env() immer null)", + vf.includes("viewport-fit=cover"), vf); + + await kontext.close(); +} + +/* Und auf JEDER Seite, nicht nur der Startseite — eine vergessene Seite + waere im installierten Betrieb genau die eine mit dem Fehler. */ +{ + let ohneCover = []; + for (const datei of SEITEN) { + const roh = readFileSync(join(W, "webdesign", datei), "utf8"); + if (!/viewport-fit\s*=\s*cover/.test(roh)) ohneCover.push(datei); + } + pruefe("viewport-fit=cover steht auf ALLEN Seiten", ohneCover.length === 0, + ohneCover.join(", ") || SEITEN.length + " Seiten geprueft"); +} + +/* ===================================================================== + TEIL 3 — was NUR im installierten Betrieb gilt + + EHRLICHE EINORDNUNG, WAS HIER BEWIESEN WIRD UND WAS NICHT. + + Der erste Versuch war, den installierten Betrieb zu emulieren. Das + geht nicht: `Emulation.setEmulatedMedia` nimmt das Merkmal + `display-mode` zwar entgegen und antwortet mit {} -- angewendet wird + es nicht (nachgemessen am 23.08.2026: matchMedia meldete weiter + `browser`). Ein echtes App-Fenster ueber `--app=` laesst Playwright + nicht zu; der Versuch blieb haengen. + + Statt das zu verschweigen und eine gruene Zeile zu schreiben, die + nichts belegt, wird hier ZWEITEILIG geprueft: + + 1. Gibt es den Block ueberhaupt, und haengt er wirklich an + display-mode? (aus dem Stilblatt gelesen) + 2. Bewirkt sein INHALT auch das, was er soll? (Bedingung zur + Laufzeit auf `all` gestellt und die Wirkung gemessen) + + Was damit NICHT bewiesen ist: dass ein echtes Android- oder + iOS-Geraet den Block auswertet. Das ist Standardverhalten und + unstrittig -- aber es ist gemessen, nicht behauptet, wo es geht, und + benannt, wo es nicht geht. + ===================================================================== */ +console.log("\n=== Nur installiert ==="); +{ + const kontext = await browser.newContext(devices["iPhone 14 Pro"]); + const seite = await kontext.newPage(); + await vorbereiten(seite); + await seite.goto(URL_BASIS + "/webdesign/index.html"); + await seite.waitForTimeout(400); + + /* --- Teil 1: Steht der Block da, und woran haengt er? --- */ + const block = await seite.evaluate(() => { + for (const blatt of document.styleSheets) { + let regeln; try { regeln = blatt.cssRules; } catch (e) { continue; } + for (const r of regeln) { + if (r.media && /display-mode/.test(r.conditionText || r.media.mediaText)) { + return { bedingung: r.conditionText || r.media.mediaText, + inhalt: Array.from(r.cssRules).map((x) => x.cssText).join(" ") }; + } + } + } + return null; + }); + pruefe("es gibt einen eigenen Zweig fuer den installierten Betrieb", + !!block, block ? block.bedingung : "kein Block gefunden"); + pruefe("er greift auch bei window-controls-overlay und minimal-ui", + !!block && /window-controls-overlay/.test(block.bedingung) && /minimal-ui/.test(block.bedingung), + block ? block.bedingung : ""); + pruefe("der Installations-Hinweis wird darin ausgeblendet", + !!block && /wd-install/.test(block.inhalt) && /display:\s*none/.test(block.inhalt)); + + /* --- Teil 2: Im Browser darf davon NICHTS gelten --- */ + const imBrowser = await seite.evaluate(() => ({ + erkannt: matchMedia("(display-mode: standalone)").matches, + federn: getComputedStyle(document.body).overscrollBehaviorY, + })); + pruefe("im Browser gilt der Sonderzweig NICHT", imBrowser.erkannt === false, + String(imBrowser.erkannt)); + pruefe("im Browser bleibt das gewohnte Nachfedern erhalten", + imBrowser.federn !== "none", imBrowser.federn); + + /* --- Teil 3: Wirkt der Inhalt, wenn die Bedingung erfuellt ist? --- */ + const wirkung = await seite.evaluate(() => { + /* Die Bedingung der Medienregel selbst umschreiben. Damit wird + genau DIESER Block scharf geschaltet -- nicht ein nachgebauter. */ + for (const blatt of document.styleSheets) { + let regeln; try { regeln = blatt.cssRules; } catch (e) { continue; } + for (const r of regeln) { + if (r.media && /display-mode/.test(r.conditionText || r.media.mediaText)) { + r.media.mediaText = "all"; + } + } + } + const hinweis = document.querySelector(".wd-install"); + return { + federn: getComputedStyle(document.body).overscrollBehaviorY, + hinweis: hinweis ? getComputedStyle(hinweis).display : "nicht auf dieser Seite", + }; + }); + pruefe("scharf geschaltet: das Gummiband-Nachfedern ist wirklich aus", + wirkung.federn === "none", wirkung.federn); + pruefe("scharf geschaltet: der Installations-Hinweis verschwindet wirklich", + wirkung.hinweis === "none" || wirkung.hinweis === "nicht auf dieser Seite", + wirkung.hinweis); + + await kontext.close(); +} + +/* ===================================================================== + TEIL 3b — liegt irgendwo etwas unter der Gestenleiste oder dem Notch? + + Gesucht wird nicht im Quelltext, sondern im fertigen Bild. Ein + Durchsuchen des CSS nach 'position: fixed' findet nur, woran man + gedacht hat -- der 'Ueberspringen'-Knopf des Vorspanns stand mit + 'bottom: 2rem' da und wurde beim Suchen prompt uebersehen, obwohl er + auf einem iPhone 14 Pro genau auf dem Entsperr-Strich lag (der ist + 34px hoch). Getippt haette man dann nicht 'ueberspringen', sondern die + App verlassen. + + Deshalb: Abstaende auf Notch-Masse setzen, dann JEDES fest oder klebend + positionierte Element messen. Was in den geschuetzten Streifen + hineinragt, faellt auf -- unabhaengig davon, ob jemand daran gedacht hat. + ===================================================================== */ +console.log("\n=== Nichts unter Statusleiste oder Gestenleiste ==="); +{ + const kontext = await browser.newContext(devices["iPhone 14 Pro"]); + for (const datei of SEITEN) { + const seite = await kontext.newPage(); + await vorbereiten(seite); + await seite.goto(URL_BASIS + "/webdesign/" + datei); + await seite.waitForTimeout(450); + + const funde = await seite.evaluate(() => { + const OBEN = 59, UNTEN = 34; // iPhone 14 Pro + document.documentElement.style.setProperty("--wd-sicher-oben", OBEN + "px"); + document.documentElement.style.setProperty("--wd-sicher-unten", UNTEN + "px"); + document.body.getBoundingClientRect(); // Neuberechnung erzwingen + const raus = []; + for (const el of document.querySelectorAll("*")) { + const st = getComputedStyle(el); + if (st.position !== "fixed" && st.position !== "sticky") continue; + if (st.display === "none" || st.visibility === "hidden") continue; + const r = el.getBoundingClientRect(); + if (r.width < 8 || r.height < 8) continue; + /* Ganzflaechige Ueberlagerungen (inset: 0) sind Hintergruende -- + sie DUERFEN unter die Leisten laufen, das ist ihr Zweck. Nur + was Text oder Bedienelemente traegt, zaehlt. */ + const vollflaechig = r.top <= 1 && r.left <= 1 && + r.width >= innerWidth - 2 && r.height >= innerHeight - 2; + if (vollflaechig) continue; + const name = el.tagName.toLowerCase() + "." + (el.className || "").toString().trim().slice(0, 24); + + /* Gemessen wird die INHALTSKANTE, nicht die Kastenkante. + + Die Kopfzeile SOLL mit ihrem Kasten unter die Statusleiste + reichen -- ihr Hintergrund fuellt die Flaeche hinter der Uhr, + sonst klaffte dort ein Loch. Verboten ist nur, dass Text oder + Knoepfe dort landen. Genau dafuer hat sie padding-top in Hoehe + des Sicherheitsabstands. + + Die erste Fassung dieser Pruefung mass die Kastenkante und + meldete daraufhin elf Seiten als fehlerhaft, auf denen alles + richtig war. */ + const obenInhalt = r.top + parseFloat(st.paddingTop || 0) + parseFloat(st.borderTopWidth || 0); + const untenInhalt = r.bottom - parseFloat(st.paddingBottom || 0) - parseFloat(st.borderBottomWidth || 0); + + if (untenInhalt > innerHeight - UNTEN && r.top < innerHeight) + raus.push(name + " ragt " + Math.round(untenInhalt - (innerHeight - UNTEN)) + "px in die Gestenleiste"); + else if (obenInhalt < OBEN && r.bottom > 0) + raus.push(name + " liegt " + Math.round(OBEN - obenInhalt) + "px unter der Statusleiste"); + } + return raus; + }); + pruefe("nichts liegt unter den Systemleisten: " + datei, funde.length === 0, + funde.slice(0, 3).join(" | ")); + + /* SELBSTPRUEFUNG -- nur einmal, auf der Startseite. + + Eine Pruefung, die nie etwas findet, kann zweierlei bedeuten: Es + ist alles in Ordnung, ODER sie misst am Problem vorbei. Genau das + war eben der Fall (falsche Kante gemessen). Deshalb wird hier ein + absichtlich falsch platzierter Knopf eingesetzt und geprueft, ob + die Messung ihn WIRKLICH findet. Erst dann ist ein gruenes + Ergebnis oben etwas wert. */ + if (datei === "index.html") { + const gefunden = await seite.evaluate(() => { + const UNTEN = 34; + const k = document.createElement("button"); + k.className = "pruef-koeder"; + k.textContent = "absichtlich falsch"; + k.style.cssText = "position:fixed;left:20px;bottom:4px;width:120px;height:44px;z-index:9999"; + document.body.appendChild(k); + const r = k.getBoundingClientRect(); + const st = getComputedStyle(k); + const untenInhalt = r.bottom - parseFloat(st.paddingBottom || 0) - parseFloat(st.borderBottomWidth || 0); + const erwischt = untenInhalt > innerHeight - UNTEN; + k.remove(); + return erwischt; + }); + pruefe("die Messung erkennt einen absichtlich falsch gesetzten Knopf", + gefunden === true, String(gefunden)); + } + await seite.close(); + } + await kontext.close(); +} + +/* ===================================================================== + TEIL 4 — die Verwaltung auf dem Handy + ===================================================================== */ +console.log("\n=== Verwaltung auf dem Handy ==="); +{ + const kontext = await browser.newContext(devices["iPhone 14 Pro"]); + const seite = await kontext.newPage(); + await vorbereiten(seite); + await seite.goto(URL_BASIS + "/webdesign/verwaltung.html"); + await seite.waitForTimeout(800); + + /* Die Reiter muessen EINE Zeile sein. Gemessen ueber die Oberkanten: + Liegen zwei Reiter auf verschiedenen Hoehen, ist umgebrochen worden. */ + const reiter = await seite.evaluate(() => { + const k = Array.from(document.querySelectorAll(".vw-tabs [data-tab]")); + const oben = new Set(k.map((x) => Math.round(x.getBoundingClientRect().top))); + const streifen = document.querySelector(".vw-tabs"); + return { anzahl: k.length, zeilen: oben.size, + wischbar: streifen.scrollWidth > streifen.clientWidth + 1, + hoehe: Math.round(streifen.getBoundingClientRect().height) }; + }); + pruefe("die sechs Reiter stehen in EINER Zeile", reiter.zeilen === 1, + reiter.zeilen + " Zeilen bei " + reiter.anzahl + " Reitern"); + pruefe("und der Streifen laesst sich wischen (sonst waeren sie abgeschnitten)", + reiter.wischbar, "Hoehe " + reiter.hoehe + "px"); + + /* Der Kopfbereich darf nicht den halben Bildschirm fressen. */ + const bisInhalt = await seite.evaluate(() => + Math.round(document.querySelector(".vw-ub-gitter").getBoundingClientRect().top)); + pruefe("die erste Zahl steht im oberen Drittel des Bildschirms", + bisInhalt < 300, bisInhalt + "px bis zur ersten Kachel"); + + /* Zwei Spalten statt zwoelf Zeilen untereinander. */ + const spalten = await seite.evaluate(() => { + const k = Array.from(document.querySelectorAll("#vw-ub-beimir .vw-ub-kachel")); + const links = new Set(k.map((x) => Math.round(x.getBoundingClientRect().left))); + return { spalten: links.size, anzahl: k.length }; + }); + pruefe("die Kacheln stehen zweispaltig", spalten.spalten === 2, + spalten.spalten + " Spalten bei " + spalten.anzahl + " Kacheln"); + + const laenge = await seite.evaluate(() => document.documentElement.scrollHeight); + pruefe("die Uebersicht ist deutlich kuerzer geworden", laenge < 2000, + laenge + "px (vorher 2635px)"); + + /* Der aktive Reiter muss sichtbar werden, wenn man ueber eine Kachel + springt -- sonst sieht man nirgends, wo man gelandet ist. */ + await seite.locator('#vw-ub-beimir .vw-ub-kachel[data-ziel="postfach"]').first().click(); + await seite.waitForTimeout(800); + const sichtbar = await seite.evaluate(() => { + const a = document.querySelector('.vw-tabs [data-tab="postfach"]'); + const s = document.querySelector(".vw-tabs"); + const ar = a.getBoundingClientRect(), sr = s.getBoundingClientRect(); + return ar.left >= sr.left - 1 && ar.right <= sr.right + 1; + }); + pruefe("nach dem Sprung ins Postfach ist dessen Reiter im Bild", + sichtbar, String(sichtbar)); + + await seite.screenshot({ path: "bild-handy-verwaltung.png", fullPage: true }); + await seite.close(); + await kontext.close(); +} + +/* ===================================================================== + TEIL 5 — Bilder zum Ansehen + ===================================================================== */ +{ + const kontext = await browser.newContext(devices["iPhone 14 Pro"]); + const seite = await kontext.newPage(); + await vorbereiten(seite); + await seite.goto(URL_BASIS + "/webdesign/index.html"); + await alsAppSchalten(kontext, seite); + await seite.waitForTimeout(600); + /* Mit gesetzten Abstaenden aufnehmen, damit man sieht, wie es + installiert auf einem Geraet mit Aussparung wirklich aussieht. */ + await seite.evaluate(() => { + document.documentElement.style.setProperty("--wd-sicher-oben", "59px"); + document.documentElement.style.setProperty("--wd-sicher-unten", "34px"); + }); + await seite.waitForTimeout(200); + await seite.screenshot({ path: "bild-handy-start-installiert.png" }); + await seite.close(); + await kontext.close(); +} + +await browser.close(); +srv.close(); +const f = ergebnisse.filter((e) => !e.ok).length; +console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`); +process.exitCode = f ? 1 : 0; diff --git a/pruef-uebersicht.mjs b/pruef-uebersicht.mjs index 00272278..05f6f67f 100644 --- a/pruef-uebersicht.mjs +++ b/pruef-uebersicht.mjs @@ -124,7 +124,13 @@ for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]] /* ---- 2. Reiter UNTER dem Titel — gemessen, nicht behauptet ---- */ const lage = await seite.evaluate(() => { const t = document.getElementById("vw-titel").getBoundingClientRect(); - const r = document.querySelector(".vw-tabs").getBoundingClientRect(); + /* Gemessen wird der erste KNOPF, nicht der Streifen um ihn herum. + + Auf dem Handy sind die Reiter ein Wischstreifen, der bewusst 4px + ueber den Containerrand hinauslaeuft -- sonst endet er optisch + mitten auf der Seite, obwohl es weitergeht. Sichtbar ausgerichtet + ist aber der Knopf, und nur der zaehlt fuer das Auge. */ + const r = document.querySelector(".vw-tabs [data-tab]").getBoundingClientRect(); return { titelUnten: t.bottom, titelLinks: t.left, reiterOben: r.top, reiterLinks: r.left }; }); pruefe(wo + ": die Reiter beginnen UNTERHALB des Titels", diff --git a/webdesign/ablauf.html b/webdesign/ablauf.html index dd1cc3ce..6064ec1c 100644 --- a/webdesign/ablauf.html +++ b/webdesign/ablauf.html @@ -2,7 +2,12 @@ - + + Ablauf — Dogfather Webdesign diff --git a/webdesign/anfrage.html b/webdesign/anfrage.html index 0d86f478..3c70111c 100644 --- a/webdesign/anfrage.html +++ b/webdesign/anfrage.html @@ -2,7 +2,12 @@ - + + Projekt anfragen — Dogfather Webdesign diff --git a/webdesign/diagnose.html b/webdesign/diagnose.html index 0efa8e67..a024c45b 100644 --- a/webdesign/diagnose.html +++ b/webdesign/diagnose.html @@ -2,7 +2,12 @@ - + + Diagnose — Dogfather Webdesign diff --git a/webdesign/faq.html b/webdesign/faq.html index a98bcacc..879c8a58 100644 --- a/webdesign/faq.html +++ b/webdesign/faq.html @@ -2,7 +2,12 @@ - + + Häufige Fragen — Dogfather Webdesign diff --git a/webdesign/index.html b/webdesign/index.html index 63560b64..65b6f1ca 100644 --- a/webdesign/index.html +++ b/webdesign/index.html @@ -2,7 +2,12 @@ - + + Dogfather Webdesign — individuelle Webseiten, persönlich umgesetzt @@ -45,7 +50,15 @@ animation: wd-intro-auf .9s cubic-bezier(.22,1,.36,1) both .45s; } .wd-intro-skip { - position: absolute; bottom: 2rem; left: 50%; transform: translateX(-50%); + /* Der Abstand nach unten muss die Gestenleiste einrechnen. + + Mit festen 2rem (32px) lag der Knopf auf einem iPhone 14 Pro + praktisch AUF dem Strich zum Entsperren -- der ist 34px hoch. + Man haette daneben getippt und waere aus der App geflogen, statt + den Vorspann zu ueberspringen. */ + position: absolute; + bottom: calc(2rem + var(--wd-sicher-unten, 0px)); + left: 50%; transform: translateX(-50%); background: none; border: 1px solid var(--wd-rand); color: var(--wd-text-soft); border-radius: 999px; padding: .55rem 1.2rem; min-height: 44px; cursor: pointer; font-size: .88rem; } diff --git a/webdesign/leistungen.html b/webdesign/leistungen.html index 0e9d0b10..7815923d 100644 --- a/webdesign/leistungen.html +++ b/webdesign/leistungen.html @@ -2,7 +2,12 @@ - + + Leistungen — Dogfather Webdesign diff --git a/webdesign/portal.html b/webdesign/portal.html index ce1df253..0a7cf24b 100644 --- a/webdesign/portal.html +++ b/webdesign/portal.html @@ -2,7 +2,12 @@ - + + Kundenportal — Dogfather Webdesign diff --git a/webdesign/portfolio.html b/webdesign/portfolio.html index 341b3fdd..82ee0269 100644 --- a/webdesign/portfolio.html +++ b/webdesign/portfolio.html @@ -2,7 +2,12 @@ - + + Portfolio — Dogfather Webdesign diff --git a/webdesign/rechtliches.html b/webdesign/rechtliches.html index e83e7c0e..10aec73d 100644 --- a/webdesign/rechtliches.html +++ b/webdesign/rechtliches.html @@ -2,7 +2,12 @@ - + + Impressum, AGB & Datenschutz — Dogfather Webdesign diff --git a/webdesign/sw.js b/webdesign/sw.js index 40ee60a0..8dd77c9e 100644 --- a/webdesign/sw.js +++ b/webdesign/sw.js @@ -34,7 +34,7 @@ Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten Stand fest. */ -const CACHE_NAME = "dogfather-webdesign-v21"; +const CACHE_NAME = "dogfather-webdesign-v22"; /* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE HTML-Datei in dieser Liste. */ diff --git a/webdesign/ueber.html b/webdesign/ueber.html index 1fc9c610..2ddb420c 100644 --- a/webdesign/ueber.html +++ b/webdesign/ueber.html @@ -2,7 +2,12 @@ - + + Über Dogfather — Dogfather Webdesign diff --git a/webdesign/verwaltung.html b/webdesign/verwaltung.html index 4045dbd9..2ec5079c 100644 --- a/webdesign/verwaltung.html +++ b/webdesign/verwaltung.html @@ -2,7 +2,12 @@ - + + Verwaltung — Dogfather Webdesign @@ -53,10 +58,16 @@ padding-top: .2rem; } @media (max-width: 560px) { - /* Auf dem Handy darf der Name weichen -- der Platz gehört dem Titel. */ - .vw-leiste { grid-template-columns: 1fr; } - .vw-leiste .vw-wer { grid-column: 1; grid-row: 3; justify-content: flex-start; } - .vw-leiste .vw-tabs { grid-row: 2; } + /* Titel und Abmelden bleiben zusammen in Zeile 1. + + Die erste Fassung schob den Abmelden-Knopf in eine dritte Zeile. + Zusammen mit den damals zweizeiligen Reitern gingen so 190px + verloren, bevor die erste Zahl kam -- fast der halbe sichtbare + Bereich eines Handys, nur fuer die Kopfzeile. */ + .vw-leiste { gap: .5rem .6rem; } + .vw-leiste h1 { font-size: 1.35rem; } + /* Der Name steht schon in der App-Leiste; hier kostet er nur Platz. */ + .vw-leiste .vw-wer #vw-name { display: none; } } .vw-wer { display: flex; justify-content: flex-end; align-items: center; gap: .6rem; font-size: .9rem; color: var(--wd-text-soft); } @@ -99,6 +110,18 @@ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); margin-bottom: .4rem; } + @media (max-width: 560px) { + /* Zwei Spalten statt einer. + + Mit einer Kachel pro Zeile war die Uebersicht auf dem Handy + 2635px lang -- man musste an allem vorbeiwischen, um das Geld zu + sehen, und der Sinn einer UEBERSICHT ist, dass man nicht wischen + muss. Zwei Spalten halbieren das. Die Schrift bleibt gleich + gross; verkleinert wuerde sie schwerer lesbar, und lesbar ist + hier wichtiger als kompakt. */ + .vw-ub-gitter { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .55rem; } + .vw-ub-kachel { padding: .8rem .75rem; } + } .vw-ub-kachel { display: flex; flex-direction: column; align-items: flex-start; gap: .18rem; @@ -512,6 +535,41 @@ .vw-notiz p { margin: 0; font-size: .93rem; white-space: pre-wrap; } .vw-tabs { display: flex; flex-wrap: wrap; gap: .4rem; } + @media (max-width: 700px) { + /* Sechs Reiter brauchen umgebrochen zwei bis drei Zeilen. Statt in + die Hoehe zu wachsen, wird daraus EIN Streifen zum Wischen. + + overflow-x statt Umbruch, scroll-snap damit ein Reiter nie halb + abgeschnitten stehen bleibt, und ein Auslauf am rechten Rand + (die Maske unten), damit man SIEHT, dass es weitergeht -- ein + harter Schnitt sieht aus wie das Ende der Liste. */ + .vw-tabs { + flex-wrap: nowrap; + overflow-x: auto; + scroll-snap-type: x proximity; + scrollbar-width: none; + /* Der Streifen darf ueber den Containerrand hinauslaufen und + nimmt den Innenabstand selbst mit -- sonst endet er optisch + mitten auf der Seite. */ + margin-inline: calc(var(--wd-sicher-links, 0px) * -1 - 4px) + calc(var(--wd-sicher-rechts, 0px) * -1 - 4px); + padding-inline: 4px; + /* scroll-padding zusaetzlich zu padding. + + Ohne das richtet scroll-snap den ersten Reiter am SCROLLRAND + aus und schluckt den Innenabstand: Der Knopf sass dann bei 12px + statt bei 16px und stand damit vier Pixel links neben dem Titel + darueber -- eine Fehlstellung, die man sieht, aber schwer + benennen kann. scroll-padding verschiebt die Einrastkante nach + innen, sodass Titel und erster Reiter buendig sind. */ + scroll-padding-inline: 4px; + padding-bottom: 2px; + -webkit-overflow-scrolling: touch; + mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 26px), transparent 100%); + } + .vw-tabs::-webkit-scrollbar { display: none; } + .vw-tabs > * { scroll-snap-align: start; flex: 0 0 auto; } + } .vw-link-anzeige { padding: 1rem; border-radius: 14px; background: rgb(var(--wd-gold-rgb) / .08); border: 1px solid rgb(var(--wd-gold-rgb) / .35); @@ -972,8 +1030,17 @@
Wird geladen…
+

- Wähle links einen Verlauf aus. + Tippe einen Verlauf an, um ihn hier zu lesen.

@@ -1996,6 +2063,19 @@ document.querySelectorAll("[data-tab]").forEach(function (x) { x.classList.toggle("vw-an", x.dataset.tab === aktiverTab); }); + /* Auf dem Handy sind die Reiter ein Wischstreifen. Wer ueber eine + Kachel nach "Postfach" springt, landet sonst in einer Ansicht, + deren Reiter ausserhalb des Streifens liegt -- man sieht dann + nirgends, wo man ist. Deshalb den aktiven Reiter heranholen. + Nur waagerecht, damit die Seite nicht zusaetzlich springt. */ + var aktiv = document.querySelector('[data-tab="' + aktiverTab + '"]'); + if (aktiv && aktiv.scrollIntoView) { + try { + aktiv.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" }); + } catch (e) { + aktiv.scrollIntoView(); // aeltere Browser kennen die Angaben nicht + } + } /* Vorher war das eine Ja/Nein-Umschaltung zwischen zwei Bereichen. Mit vier Reitern geht das nicht mehr: Jede neue Ansicht haette eine weitere Zeile "hidden = ..." gebraucht, und beim naechsten diff --git a/webdesign/widerruf.html b/webdesign/widerruf.html index 7b420ab9..73139f23 100644 --- a/webdesign/widerruf.html +++ b/webdesign/widerruf.html @@ -2,7 +2,12 @@ - + + Vertrag widerrufen — Dogfather Webdesign diff --git a/webdesign/zugang.html b/webdesign/zugang.html index 9abd1470..c88363f1 100644 --- a/webdesign/zugang.html +++ b/webdesign/zugang.html @@ -2,7 +2,12 @@ - + + Zugang — Dogfather Webdesign