Verwaltung: jeder Bereich bekommt eine eigene Signatur

Aus einem festen Streifen werden sechs. Jeder Reiter hat jetzt ein
eigenes Motiv UND eine eigene Leitfarbe, beides wechselt beim Klick.

Die Zuordnung ist gelesen, nicht ausgewuerfelt:

  Uebersicht  Wappen     Die Zentrale, wo alles zusammenlaeuft.
  Anfragen    Portal     Ein Tor. Hier kommt Neues herein.
  Projekte    Monolith   Etwas, das aufrecht steht und gebaut wird.
  Kunden      Thron      Wer bestellt, steht auf dem Podest.
  Zahlungen   Kristall   Der Wert selbst. Dazu Liquid Gold.
  Postfach    Portal     Wieder ein Tor -- Nachrichten gehen durch.

Farben: Baby Blue, Hyper Aqua, Aurora Violet, Prism Indigo, Liquid Gold,
Ion Blue. Nach ein paar Tagen erkennt man den Bereich an der Farbe,
bevor man den Titel gelesen hat. Aus Deko wird Orientierung.

Das neue Thron-Motiv ist aus dem vierten Bild aufbereitet, im selben
Mass wie die bestehenden (1672x941) und mit kleiner Fassung fuers Handy.

DIE ENTSCHEIDENDE IDEE: Bild und Text teilen sich nicht mehr denselben
Platz. Eine Deckschicht ist links voll deckend und oeffnet sich nach
rechts. Links stehen Titel und Reiter, rechts ist die Flaeche leer --
dort darf das Motiv mit 82 % auftreten statt mit 17 %.

Der erste Versuch war gleichmaessig bei 17 %: ueberall gleich schwach zu
ahnen, ein Fleck statt eines Bildes, und trotzdem hinter der Schrift.
Kurz und kraeftig ist beides besser -- mehr Wirkung dort, wo Platz ist,
null Stoerung dort, wo gearbeitet wird. Der Streifen ist jetzt auch
kuerzer und endet, BEVOR die erste Kachel anfaengt.

Drei Fehler, die der Test gefunden hat und nicht das Auge:

- Alle sechs Bereiche zeigten dasselbe Bild. Die Variablen hingen an
  #vw-bereich, der Schmuckstreifen liegt aber ausserhalb davon -- er
  erbte sie nie und fiel auf den Rueckfallwert zurueck. Die Farben
  wechselten (Reiter und Titel liegen drinnen), die Motive nicht.
- Die waagerechten Ausschnitte bewirkten nichts. Bei "cover" skaliert
  der Browser auf die Breite des Streifens, die volle Bildbreite ist
  immer sichtbar. Erst ein Zoom ueber 100 % schafft Spielraum.
- Das Thron-Motiv schob seine hellen Kristallfluegel bis unter die
  Reiter. Deshalb deckt die Schicht jetzt bis 46 % statt 34 % -- der
  Wert ist gemessen, nicht geschaetzt.

Dazu zwei Dinge, die erst der Screenshot zeigte: angeschnittene Logos im
Streifen (sieht nach Versehen aus, und das Logo steht ohnehin oben
links), und die Knoepfe Suchen/Abmelden lagen ueber dem hellsten Teil
des Bildes. Sie haben jetzt einen eigenen dichten Grund -- Bedienelemente
muessen lesbar sein, egal was dahinter liegt.

Der Test misst nicht mehr "Deckkraft unter 20 %". Dieser Massstab ist
hinfaellig, seit das Motiv nach rechts gerueckt ist: Es darf kraeftig
sein, WEIL es nicht mehr hinter der Schrift liegt. Geprueft wird
stattdessen, wie ruhig der Grund unter der Reiterzeile ist -- mit Motiv
gegen ohne Motiv, in allen sechs Bereichen. Gemessen: plus 0 bis plus 6.

Unveraendert gilt: kein Motiv hinter Listen, Tabellen oder Zahlen. Ein
Bild hinter einer Zahlenspalte ist genau die Art von Schoenheit, die ein
Werkzeug unbrauchbar macht.

Geprueft: 237 Pruefungen gruen (Verwaltung 39, Portfolio 15, CRYONOVA 53,
Blickfang 13, System 5, Abmelden 16, Abbruch 42, Angebot 54).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 00:26:23 +02:00
co-authored by Claude Opus 5
parent 9df21fabe1
commit 09bd6330f7
7 changed files with 571 additions and 107 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

+275 -79
View File
@@ -5,6 +5,10 @@
Kostet es Lesbarkeit? Die Verwaltung ist ein Arbeitsplatz -- ein
Motiv, das hinter einer Zahlenspalte liegt, ist genau die Art von
Schoenheit, die ein Werkzeug unbrauchbar macht.
Seit der Bereichs-Signatur kommt eine zweite Frage dazu: Sind die
sechs Bereiche wirklich unterscheidbar, oder sieht nur der Code so
aus? Deshalb wird jeder Reiter einzeln angeklickt und gemessen.
===================================================================== */
import { chromium } from "playwright";
import http from "http";
@@ -36,81 +40,67 @@ const pruefe = (name, gut, info) => {
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
};
function leuchtkraft([r, g, b]) {
const f = (v) => {
v /= 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
}
function verhaeltnis(a, b) {
const l1 = leuchtkraft(a), l2 = leuchtkraft(b);
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
const BEREICHE = ["uebersicht", "anfragen", "projekte", "kunden", "zahlungen", "postfach"];
/* Misst, wie stark der dunkle Grund in einem Bereich schwankt. Hohe
Schwankung heisst: Da liegt ein Bild unter der Schrift. */
async function unruheAn(seite, wahl) {
const bild = await seite.locator(wahl).first().screenshot();
return seite.evaluate(async (daten) => {
const bl = await fetch("data:image/png;base64," + daten).then((r) => r.blob());
const bm = await createImageBitmap(bl);
const c = new OffscreenCanvas(bm.width, bm.height);
const x = c.getContext("2d");
x.drawImage(bm, 0, 0);
const d = x.getImageData(0, 0, bm.width, bm.height).data;
const grund = [];
for (let i = 0; i < d.length; i += 4) {
const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
if (h < 110) grund.push(h);
}
if (grund.length < 50) return null;
const m = grund.reduce((a, b) => a + b, 0) / grund.length;
return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length));
}, bild.toString("base64"));
}
srv.listen(4404, async () => {
const browser = await chromium.launch();
/* =================================================================
TEIL 1 — die Anmeldemaske
================================================================= */
for (const [gr, vp] of [["Computer", { width: 1440, height: 950 }],
["Handy", { width: 390, height: 844 }]]) {
console.log(`\n===== ${gr} =====`);
console.log(`\n===== ANMELDUNG (${gr}) =====`);
const seite = await browser.newPage({ viewport: vp });
const skriptfehler = [];
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 120)));
await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await seite.waitForTimeout(1500);
await seite.waitForTimeout(1400);
/* ---- Der Schmuckstreifen ---- */
const streifen = await seite.evaluate(() => {
const el = document.querySelector(".vw-schmuck");
if (!el) return null;
const s = getComputedStyle(el);
const r = el.getBoundingClientRect();
const treffer = s.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return {
datei: treffer ? treffer[1] : "(keins)",
deckkraft: parseFloat(s.opacity),
z: parseInt(s.zIndex, 10),
oben: Math.round(r.top),
hoehe: Math.round(r.height),
klicks: s.pointerEvents,
versteckt: el.getAttribute("aria-hidden"),
};
});
if (!streifen) { pruefe(`${gr}: Schmuckstreifen vorhanden`, false); continue; }
const kleinErwartet = vp.width <= 900;
pruefe(`${gr}: richtige Bilddatei`,
kleinErwartet === streifen.datei.includes("-klein"), streifen.datei);
/* Das ist der Kern: Der Streifen darf ANGEDEUTET sein, nicht mehr. */
pruefe(`${gr}: bleibt sehr leise (unter 20 %)`,
streifen.deckkraft > 0 && streifen.deckkraft < 0.20,
Math.round(streifen.deckkraft * 100) + " %");
pruefe(`${gr}: liegt hinter dem Inhalt`, streifen.z < 0, "z-index " + streifen.z);
pruefe(`${gr}: faengt keine Klicks ab`, streifen.klicks === "none", streifen.klicks);
pruefe(`${gr}: ist als Zierde ausgezeichnet`, streifen.versteckt === "true");
/* 67px ist kein Zufallswert, sondern die Hoehe der Kopfleiste: Der
Streifen beginnt direkt darunter. Gemessen wird deshalb gegen die
tatsaechliche Kopfhoehe, nicht gegen eine geratene Zahl. */
const kopfHoehe = await seite.evaluate(() => {
const k = document.querySelector("#wd-kopf .wd-kopf, #wd-kopf header, #wd-kopf");
return k ? Math.round(k.getBoundingClientRect().height) : 0;
});
pruefe(`${gr}: sitzt direkt unter der Kopfleiste`,
streifen.oben <= kopfHoehe + 8, `${streifen.oben}px, Kopf ${kopfHoehe}px`);
/* ---- Die Anmeldekarte traegt das Wappen ---- */
const login = await seite.evaluate(() => {
const el = document.querySelector("#vw-anmeldung");
if (!el) return null;
el.removeAttribute("hidden"); // sonst nichts zu messen
el.removeAttribute("hidden");
const vor = getComputedStyle(el, "::before");
const treffer = vor.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return {
klassen: el.className,
datei: treffer ? treffer[1] : "(keins)",
deckkraft: parseFloat(vor.opacity),
};
return { datei: treffer ? treffer[1] : "(keins)", deckkraft: parseFloat(vor.opacity) };
});
pruefe(`${gr}: die Anmeldekarte hat ein Motiv`,
login && login.datei.includes("wappen"), login && login.datei);
/* Der eigentliche Test. Deckkraft allein sagt nichts: Ein Motiv mit
hellen Kanten ist bei 20 % stoerender als ein ruhiges bei 50 %.
Was zaehlt, ist wie stark der Untergrund UNTER DER SCHRIFT
schwankt. Gemessen wird das an echten Bildpunkten aus dem
Erklaertext -- genau dort lag beim Bauen der Hundekopf, und die
Zeilen liefen quer ueber Schnauze und Schriftzug. */
await seite.waitForTimeout(500);
await seite.waitForTimeout(500);
const messen = async () => {
const bild = await seite.locator("#vw-anmeldung p.wd-klein").first().screenshot();
return seite.evaluate(async (daten) => {
@@ -120,8 +110,6 @@ srv.listen(4404, async () => {
const x = c.getContext("2d");
x.drawImage(bm, 0, 0);
const d = x.getImageData(0, 0, bm.width, bm.height).data;
/* Nur die dunklen Bildpunkte: Die hellen SIND die Schrift, die
soll ja kontrastieren. Interessant ist der Grund dazwischen. */
const grund = [];
for (let i = 0; i < d.length; i += 4) {
const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
@@ -132,28 +120,169 @@ srv.listen(4404, async () => {
return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length));
}, bild.toString("base64"));
};
const mitBild = await messen();
/* Die Vergleichsmessung ohne Motiv ist der Kern. Ein fester
Grenzwert waere geraten: Schon die weichgezeichneten Kanten der
Buchstaben allein erzeugen eine Schwankung um 12 -- ein Test
gegen "unter 14" haette also fast nur diese Kanten gemessen und
waere je nach Schriftgroesse zufaellig gruen oder rot. Gefragt
ist, wie viel Unruhe DAS MOTIV hinzufuegt. */
/* Die Vergleichsmessung ist der Kern. Ein fester Grenzwert waere
geraten: Schon die weichgezeichneten Buchstabenkanten allein
erzeugen eine Schwankung um 12. Gefragt ist, wie viel Unruhe DAS
MOTIV hinzufuegt. */
await seite.evaluate(() =>
document.querySelector("#vw-anmeldung").classList.remove("wd-motiv", "wd-motiv--wappen"));
await seite.waitForTimeout(400);
const ohneBild = await messen();
await seite.evaluate(() =>
document.querySelector("#vw-anmeldung").classList.add("wd-motiv", "wd-motiv--wappen"));
await seite.waitForTimeout(400);
await seite.waitForTimeout(200);
const zusatz = (mitBild ?? 99) - (ohneBild ?? 0);
pruefe(`${gr}: das Motiv macht den Textgrund kaum unruhiger`,
mitBild !== null && ohneBild !== null && zusatz <= 5,
`mit ${mitBild}, ohne ${ohneBild} → plus ${zusatz}`);
/* ---- Und jetzt die eigentliche Regel ---- */
await seite.close();
}
/* =================================================================
TEIL 2 — die sechs Bereichs-Signaturen
================================================================= */
const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } });
const skriptfehler = [];
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await seite.waitForTimeout(1400);
await seite.evaluate(() => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
});
await seite.waitForTimeout(500);
/* Erst EINMAL echt klicken, um zu beweisen, dass der Reiter die
Signatur ueberhaupt umschaltet. Danach geht das nicht mehr: Beim
Klick laedt die Seite Daten, merkt dass niemand angemeldet ist, und
klappt den Arbeitsbereich korrekterweise wieder zu. Das ist die
Zugangspruefung, kein Fehler -- aber es beendet den Rundgang.
Deshalb wird der Bereich danach direkt gesetzt, so wie der
Klick-Listener es auch tut. Geprueft wird dabei das Stilblatt, und
das kennt nur das Kennzeichen am Wurzelelement. */
await seite.click('[data-tab="anfragen"]');
await seite.waitForTimeout(900);
const nachKlick = await seite.evaluate(() =>
document.documentElement.getAttribute("data-vw-bereich"));
console.log("\n===== SCHALTET DER REITER UM? =====");
pruefe("ein echter Reiterklick setzt den Bereich", nachKlick === "anfragen", nachKlick);
/* Zurueck in den sichtbaren Zustand fuer die Messreihe. */
await seite.evaluate(() => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
});
await seite.waitForTimeout(300);
const gesehen = [];
for (const bereich of BEREICHE) {
console.log(`\n===== BEREICH: ${bereich} =====`);
/* Der Datenabruf laeuft im Hintergrund weiter und klappt den
Arbeitsbereich verzoegert wieder zu (Zugangspruefung). Deshalb vor
JEDER Messung erneut aufmachen, nicht nur einmal am Anfang. */
await seite.evaluate((b) => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
document.documentElement.setAttribute("data-vw-bereich", b);
document.querySelectorAll("[data-tab]").forEach((x) =>
x.classList.toggle("vw-an", x.dataset.tab === b));
}, bereich);
await seite.waitForTimeout(500);
const d = await seite.evaluate(() => {
const wurzel = document.documentElement.getAttribute("data-vw-bereich");
const s = document.querySelector(".vw-schmuck");
const st = getComputedStyle(s);
const treffer = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
const reiter = document.querySelector(".vw-filter-knopf.vw-an");
const rs = getComputedStyle(reiter);
const titel = document.querySelector("#vw-titel");
/* Untergrund der Seite suchen -- die Reiterflaeche ist
halbdurchsichtig und muss darueber verrechnet werden. */
const seitenGrund = getComputedStyle(document.body).backgroundColor
.match(/[\d.]+/g).slice(0, 3).map(Number);
return {
wurzel,
datei: treffer ? treffer[1] : "(keins)",
deckkraft: parseFloat(st.opacity),
blick: st.backgroundPosition,
reiterText: reiter.textContent.trim().slice(0, 14),
reiterFarbe: rs.color.match(/[\d.]+/g).slice(0, 3).map(Number),
reiterFlaeche: rs.backgroundColor.match(/[\d.]+/g),
titelFarbe: getComputedStyle(titel).color.match(/[\d.]+/g).slice(0, 3).map(Number),
seitenGrund,
lumen: getComputedStyle(document.querySelector("#vw-bereich")).getPropertyValue("--vw-farbe").trim(),
};
});
pruefe(` Kennzeichen steht am Wurzelelement`, d.wurzel === bereich, d.wurzel);
gesehen.push({ bereich, datei: d.datei, farbe: d.lumen, blick: d.blick });
/* Der aktive Reiter muss lesbar bleiben -- auch im Bereich
"Kunden", dessen Leitfarbe Prism Indigo als SCHRIFT durchfaellt.
Genau deshalb faerbt die Leitfarbe dort nur die Flaeche. */
const a = d.reiterFlaeche.length === 4 ? parseFloat(d.reiterFlaeche[3]) : 1;
const flaeche = d.seitenGrund.map((u, i) =>
Math.round(Number(d.reiterFlaeche[i]) * a + u * (1 - a)));
const v = verhaeltnis(d.reiterFarbe, flaeche);
pruefe(` der aktive Reiter bleibt lesbar (AA = 4,5:1)`, v >= 4.5, v.toFixed(2) + ":1");
const vt = verhaeltnis(d.titelFarbe, d.seitenGrund);
pruefe(` der Titel bleibt lesbar`, vt >= 4.5, vt.toFixed(2) + ":1");
/* Frueher stand hier "Deckkraft unter 20 %". Dieser Massstab ist
hinfaellig, seit das Motiv nach rechts gerueckt ist: Es darf jetzt
kraeftig sein, WEIL es nicht mehr hinter der Schrift liegt. Eine
Zahl allein sagt darueber nichts.
Geprueft wird stattdessen das, worauf es ankommt -- wie ruhig der
Grund unter der Reiterzeile ist, mit Motiv gegen ohne Motiv. */
const offen = () => seite.evaluate(() => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
});
await offen();
const grundMit = await unruheAn(seite, ".vw-tabs");
await seite.evaluate(() => {
document.querySelector(".vw-schmuck").style.visibility = "hidden";
});
await offen();
await seite.waitForTimeout(350);
const grundOhne = await unruheAn(seite, ".vw-tabs");
await seite.evaluate(() => {
document.querySelector(".vw-schmuck").style.visibility = "";
});
await seite.waitForTimeout(250);
const mehr = (grundMit ?? 99) - (grundOhne ?? 0);
pruefe(` der Grund unter den Reitern bleibt ruhig`,
grundMit !== null && grundOhne !== null && mehr <= 6,
`mit ${grundMit}, ohne ${grundOhne} → plus ${mehr}`);
}
/* ---- Die Kernfrage: sind die sechs wirklich unterscheidbar? ---- */
console.log("\n===== SIND DIE BEREICHE UNTERSCHEIDBAR? =====");
for (const g of gesehen) console.log(` ${g.bereich.padEnd(11)} ${g.datei.padEnd(22)} ${g.farbe}`);
const farben = new Set(gesehen.map((g) => g.farbe));
pruefe("jeder Bereich hat eine eigene Leitfarbe", farben.size === 6,
farben.size + " von 6 verschieden");
/* Motive duerfen sich wiederholen -- es gibt fuenf Bilder fuer sechs
Bereiche. Aber dann MUSS der Ausschnitt ein anderer sein, sonst
sehen zwei Bereiche gleich aus. */
const doppelt = {};
for (const g of gesehen) (doppelt[g.datei] ||= []).push(g);
let ausschnittOk = true, hinweis = [];
for (const [datei, liste] of Object.entries(doppelt)) {
if (liste.length < 2) continue;
const blicke = new Set(liste.map((l) => l.blick));
if (blicke.size !== liste.length) { ausschnittOk = false; hinweis.push(datei); }
else hinweis.push(`${datei}: ${liste.length}x, aber je anderer Ausschnitt`);
}
pruefe("mehrfach genutzte Motive zeigen einen anderen Ausschnitt",
ausschnittOk, hinweis.join("; ") || "kein Motiv doppelt");
/* ---- Und die alte Regel gilt weiter ---- */
const stoerung = await seite.evaluate(() => {
const schlimm = [];
for (const el of document.querySelectorAll("table, .wd-tabelle, .vw-liste")) {
@@ -161,7 +290,7 @@ srv.listen(4404, async () => {
while (p && p !== document.body) {
if (p.classList && (p.classList.contains("wd-motiv") ||
p.classList.contains("vw-schmuck"))) {
schlimm.push(el.tagName + "." + (el.className || "").slice(0, 20));
schlimm.push(el.tagName);
break;
}
p = p.parentElement;
@@ -169,14 +298,81 @@ srv.listen(4404, async () => {
}
return schlimm;
});
pruefe(`${gr}: kein Motiv hinter Tabellen oder Listen`,
stoerung.length === 0, stoerung.join(", ") || "keine");
pruefe("kein Motiv hinter Tabellen oder Listen", stoerung.length === 0,
stoerung.join(", ") || "keine");
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
pruefe(`${gr}: kein Skriptfehler`, skriptfehler.length === 0, skriptfehler[0] || "");
await seite.screenshot({ path: `pruef-vw-schmuck-${gr.toLowerCase()}.png` });
await seite.evaluate(() => {
document.documentElement.setAttribute("data-vw-bereich", "uebersicht");
document.querySelectorAll("[data-tab]").forEach((x) =>
x.classList.toggle("vw-an", x.dataset.tab === "uebersicht"));
});
await seite.waitForTimeout(700);
await seite.screenshot({ path: "pruef-vw-schmuck-computer.png" });
await seite.close();
}
/* ---- Bewegung aus, wenn der Nutzer das so eingestellt hat ---- */
console.log("\n===== REDUZIERTE BEWEGUNG =====");
const ruhig = await browser.newPage({
viewport: { width: 1440, height: 950 }, reducedMotion: "reduce" });
await ruhig.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await ruhig.waitForTimeout(1200);
await ruhig.evaluate(() => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
});
await ruhig.click('[data-tab="projekte"]');
await ruhig.waitForTimeout(700);
/* Der Klick klappt den Bereich zu (Zugangspruefung) -- fuer die
Messung wieder aufmachen. Das Kennzeichen hat der Klick gesetzt. */
await ruhig.evaluate(() => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
});
await ruhig.waitForTimeout(300);
const bewegung = await ruhig.evaluate(() => {
const s = document.querySelector(".vw-schmuck");
const st = getComputedStyle(s);
return {
animation: st.animationName,
sichtbar: parseFloat(st.opacity),
bereich: document.documentElement.getAttribute("data-vw-bereich"),
};
});
pruefe("keine Animation bei reduzierter Bewegung",
bewegung.animation === "none", bewegung.animation);
/* Wichtig: abschalten heisst NICHT verschwinden. Das Motiv muss da
sein, nur ohne Bewegung. */
pruefe("das Motiv ist trotzdem sichtbar", bewegung.sichtbar > 0.05,
Math.round(bewegung.sichtbar * 100) + " %");
pruefe("der Bereichswechsel wirkt trotzdem", bewegung.bereich === "projekte",
bewegung.bereich);
await ruhig.close();
/* ---- Handy ---- */
console.log("\n===== HANDY =====");
const handy = await browser.newPage({ viewport: { width: 390, height: 844 } });
await handy.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await handy.waitForTimeout(1200);
await handy.evaluate(() => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
document.querySelector("#vw-bereich").removeAttribute("hidden");
});
await handy.evaluate(() => {
document.documentElement.setAttribute("data-vw-bereich", "kunden");
});
await handy.waitForTimeout(600);
const hd = await handy.evaluate(() => {
const st = getComputedStyle(document.querySelector(".vw-schmuck"));
const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return { datei: t ? t[1] : "(keins)", deckkraft: parseFloat(st.opacity) };
});
pruefe("Handy: kleine Bildfassung", hd.datei.includes("-klein"), hd.datei);
const ueberlauf = await handy.evaluate(() =>
document.documentElement.scrollWidth - document.documentElement.clientWidth);
pruefe("Handy: nichts laeuft seitlich ueber", ueberlauf <= 1, ueberlauf + "px");
await handy.screenshot({ path: "pruef-vw-schmuck-handy.png" });
await handy.close();
const verpasst = fehlt.filter((f) => /\.(webp|jpg|png|css|js|svg)$/.test(f));
pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 123 KiB

+1 -1
View File
@@ -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-v35";
const CACHE_NAME = "dogfather-webdesign-v36";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+281 -13
View File
@@ -50,27 +50,264 @@
Tabellen oder Zahlen. Ein Bild hinter einer Zahlenspalte ist genau
die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. */
/* ---------- DIE BEREICHS-SIGNATUR ----------
Jeder der sechs Bereiche bekommt ein eigenes Motiv UND eine eigene
Leitfarbe. Beides wechselt beim Reiterklick.
Die Zuordnung ist nicht ausgewuerfelt, sondern gelesen:
Uebersicht Wappen Die Zentrale, wo alles zusammenlaeuft.
Anfragen Portal Ein Tor. Hier kommt Neues herein.
Projekte Monolith Etwas, das aufrecht steht und gebaut wird.
Kunden Thron Wer bestellt, steht auf dem Podest.
Zahlungen Kristall Der Wert selbst. Dazu Liquid Gold.
Postfach Portal Wieder ein Tor -- Nachrichten gehen durch.
Anderer Ausschnitt, andere Farbe, damit es
trotzdem nicht wie "Anfragen" aussieht.
Warum das mehr ist als Deko: Nach ein paar Tagen erkennt man den
Bereich an der Farbe, bevor man den Titel gelesen hat. Der Schmuck
wird zur Orientierung.
Die Regel von vorher gilt unveraendert weiter: NIE ein Motiv hinter
Listen, Tabellen oder Zahlen. Der Streifen ist verschwunden, bevor
die erste Tabelle anfaengt. Ein Bild hinter einer Zahlenspalte ist
genau die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. */
/* Die Variablen haengen am WURZELELEMENT, nicht am Arbeitsbereich.
Beim Bauen hingen sie an #vw-bereich -- und alle sechs Bereiche
zeigten dasselbe Bild. Grund: Der Schmuckstreifen liegt ausserhalb
von #vw-bereich, erbt die Variablen also nie und faellt auf den
Rueckfallwert zurueck. Die Farben wechselten (Reiter und Titel
liegen drinnen), die Motive nicht. */
:root {
/* Rueckfall, falls das Kennzeichen mal fehlt: Dann sieht es aus wie
die Uebersicht, statt gar nicht. */
--vw-motiv: url("/assets/img/cryo-wappen.webp");
--vw-motiv-klein: url("/assets/img/cryo-wappen-klein.webp");
--vw-farbe: var(--wd-blau-rgb);
--vw-blick: center top;
}
[data-vw-bereich="uebersicht"] {
--vw-motiv: url("/assets/img/cryo-wappen.webp");
--vw-motiv-klein: url("/assets/img/cryo-wappen-klein.webp");
--vw-farbe: var(--wd-blau-rgb);
--vw-blick: 76% 52%;
}
[data-vw-bereich="anfragen"] {
--vw-motiv: url("/assets/img/cryo-ring.webp");
--vw-motiv-klein: url("/assets/img/cryo-ring-klein.webp");
--vw-farbe: var(--wd-aqua-rgb);
--vw-blick: 72% 46%;
}
[data-vw-bereich="projekte"] {
--vw-motiv: url("/assets/img/cryo-monolith.webp");
--vw-motiv-klein: url("/assets/img/cryo-monolith-klein.webp");
--vw-farbe: var(--wd-lila-rgb);
--vw-blick: 80% 46%;
}
[data-vw-bereich="kunden"] {
--vw-motiv: url("/assets/img/cryo-thron.webp");
--vw-motiv-klein: url("/assets/img/cryo-thron-klein.webp");
/* Prism Indigo -- hier unbedenklich, weil es NUR Flaeche und Kante
faerbt. Als Schriftfarbe faellt es durch (3,25:1), deshalb steht
weiter unten ausdruecklich, dass die Schrift davon unberuehrt
bleibt. */
--vw-farbe: var(--wd-indigo-rgb);
--vw-blick: 84% 52%;
}
[data-vw-bereich="zahlungen"] {
--vw-motiv: url("/assets/img/cryo-kristall.webp");
--vw-motiv-klein: url("/assets/img/cryo-kristall-klein.webp");
--vw-farbe: var(--wd-gold-rgb);
--vw-blick: center 38%;
}
[data-vw-bereich="postfach"] {
--vw-motiv: url("/assets/img/cryo-ring.webp");
--vw-motiv-klein: url("/assets/img/cryo-ring-klein.webp");
--vw-farbe: var(--wd-ion-rgb);
/* Bewusst der andere Bildrand als bei "Anfragen": gleiches Bild,
voellig anderer Ausschnitt. */
--vw-blick: 12% 54%;
}
/* Warum 190% statt cover: Bei "cover" skaliert der Browser das Bild
auf die Breite des Streifens -- die volle Bildbreite ist dann immer
sichtbar, und eine waagerechte Ausschnittsangabe bewirkt schlicht
nichts. Genau das ist hier passiert: Alle Werte von 62% bis 84%
zeigten dasselbe Bild. Erst mit einem Zoom ueber 100% entsteht
ueberhaupt Spielraum, den --vw-blick verschieben kann.
190% ist so gewaehlt, dass der Streifen auf jeder Bildschirmbreite
gefuellt bleibt: Die Bildbreite ist immer 1,9-mal so breit wie der
Streifen, die Hoehe waechst proportional mit.
Zu den Ausschnitten (--vw-blick): Sie sind so gewaehlt, dass KEIN
Logo angeschnitten im Streifen haengt. Ein halber Hundekopf mitten
im Bild sieht nach Versehen aus, nicht nach Absicht -- und das Logo
steht ohnehin schon oben links in der Kopfleiste. Gezeigt werden
stattdessen die Kristallformationen, also das, was an den Motiven
tatsaechlich spektakulaer ist. */
.vw-schmuck {
position: absolute;
top: 0; left: 0; right: 0;
height: clamp(220px, 34vh, 380px);
/* Genau die Kopfzone: Titel plus Reiterreihe, mehr nicht. Der
Streifen ist zu Ende, BEVOR die erste Kachel anfaengt.
Vorher war er fast doppelt so hoch und reichte in den
Kachelbereich hinein. Dadurch musste er blass bleiben, sonst
haette er durch die halbdurchsichtigen Kacheln geschienen. Kurz
und kraeftig ist beides besser: mehr Wirkung dort, wo Platz ist,
und null Stoerung dort, wo gearbeitet wird. */
height: clamp(186px, 25vh, 250px);
z-index: -1;
pointer-events: none;
background: url("/assets/img/cryo-ring.webp") center top / cover no-repeat;
/* Sehr leise. Der Wert ist bewusst niedriger als bei den
oeffentlichen Motiven (.55): Dort traegt das Bild die Stimmung,
hier darf es die Kopfzeile nur andeuten. */
opacity: .16;
/* Nach unten sauber ausblenden, damit es keine sichtbare Kante
gibt, an der das Bild einfach aufhoert. */
-webkit-mask-image: linear-gradient(180deg, #000 0%, rgb(0 0 0 / .55) 45%, transparent 100%);
mask-image: linear-gradient(180deg, #000 0%, rgb(0 0 0 / .55) 45%, transparent 100%);
/* ZWEI Ebenen, und die Reihenfolge ist der ganze Trick:
oben eine Deckschicht, darunter das Motiv.
Die Deckschicht ist LINKS fast undurchsichtig und wird nach
rechts hin offen. Links stehen Titel und Reiter -- dort bleibt es
ruhig. Rechts ist die Flaeche leer, dort darf das Bild richtig
auftreten.
Der erste Versuch war gleichmaessig bei 17 % Deckkraft. Ergebnis:
Das Wappen war ueberall gleich schwach zu ahnen, ein Fleck statt
eines Bildes -- und trotzdem lag es hinter der Schrift. Diese
Loesung ist beides zugleich staerker und ruhiger, weil Bild und
Text sich schlicht nicht mehr denselben Platz teilen. */
background:
/* Bis 46 % voll deckend -- das ist genau die Zone, in der Titel
und Reiterreihe liegen. Der Wert ist gemessen, nicht geschaetzt:
Bei 34 % schob das Thron-Motiv seine hellen Kristallfluegel bis
unter die Reiter und machte den Grund dort merklich unruhiger
als ohne Bild. */
linear-gradient(100deg,
rgb(3 8 16 / 1) 0%,
rgb(3 8 16 / 1) 46%,
rgb(3 8 16 / .52) 64%,
rgb(3 8 16 / .05) 86%,
rgb(3 8 16 / 0) 100%),
var(--vw-motiv, url("/assets/img/cryo-wappen.webp"))
var(--vw-blick, center top) / 190% auto no-repeat;
opacity: .82;
/* Unten ausblenden, damit es keine sichtbare Kante gibt, an der das
Bild einfach aufhoert. */
-webkit-mask-image:
linear-gradient(180deg, #000 0%, #000 55%, rgb(0 0 0 / .45) 82%, transparent 100%);
mask-image:
linear-gradient(180deg, #000 0%, #000 55%, rgb(0 0 0 / .45) 82%, transparent 100%);
}
/* Die Kristallkante als eigene Ebene: ein sehr schmaler Leuchtsaum in
der Leitfarbe, der den Streifen nach unten abschliesst. */
.vw-schmuck::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
rgb(var(--vw-farbe) / .55) 22%,
rgb(var(--vw-farbe) / .18) 50%,
rgb(var(--vw-farbe) / .55) 78%,
transparent 100%);
clip-path: polygon(
0% 0%, 6% 100%, 13% 0%, 21% 100%, 29% 0%, 37% 100%, 45% 0%,
53% 100%, 61% 0%, 69% 100%, 77% 0%, 85% 100%, 93% 0%, 100% 100%);
}
/* Beim Bereichswechsel blendet die Signatur kurz aus und wieder ein.
Ohne das springt das Bild hart um -- mit sieht es aus, als wuerde
der Raum wechseln. */
@keyframes vw-signatur-ein {
from { opacity: 0; transform: scale(1.05); }
to { opacity: .82; transform: none; }
}
.vw-schmuck { animation: vw-signatur-ein .55s ease-out both; }
@media (max-width: 900px) {
/* Auf dem Handy die kleine Fassung -- 34 KB statt 113 KB fuer ein
Bild, das ohnehin nur angedeutet ist. */
.vw-schmuck { background-image: url("/assets/img/cryo-ring-klein.webp"); opacity: .13; }
/* Auf dem Handy die kleine Fassung -- ein Bruchteil der Datenmenge
fuer ein Bild, das ohnehin nur angedeutet ist. */
/* Auf dem Handy ist links kein Platz, den man freihalten koennte --
die Reiter laufen ueber die ganze Breite. Deshalb hier eine
gleichmaessige Deckschicht und deutlich weniger Bild. */
.vw-schmuck {
background:
linear-gradient(180deg,
rgb(3 8 16 / .82) 0%,
rgb(3 8 16 / .88) 60%,
rgb(3 8 16 / .95) 100%),
var(--vw-motiv-klein, url("/assets/img/cryo-wappen-klein.webp"))
var(--vw-blick, center top) / 190% auto no-repeat;
opacity: .62;
height: clamp(200px, 30vh, 300px);
}
@keyframes vw-signatur-ein {
from { opacity: 0; transform: scale(1.04); }
to { opacity: .62; transform: none; }
}
}
/* Wer Bewegung reduziert haben will, bekommt den Wechsel ohne
Animation -- das Motiv ist trotzdem da. */
@media (prefers-reduced-motion: reduce) {
.vw-schmuck { animation: none; }
}
/* Suchen und Abmelden sitzen oben rechts -- also genau dort, wo das
Motiv am hellsten ist. Bedienelemente muessen immer lesbar sein,
egal was dahinter liegt. Deshalb bekommen sie hier einen eigenen,
dichten Grund statt der sonst halbdurchsichtigen Flaeche.
backdrop-filter ist bewusst nur eine Zugabe: Wo der Browser es
nicht kann, traegt schon die Farbe allein. */
#vw-bereich .vw-wer {
background: rgb(3 8 16 / .82);
border-radius: 999px;
padding: .3rem .5rem;
-webkit-backdrop-filter: blur(9px);
backdrop-filter: blur(9px);
}
/* ---------- Die Leitfarbe faerbt mit ---------- */
/* Der aktive Reiter traegt die Farbe seines Bereichs. Das ist der
Moment, in dem aus Deko Orientierung wird. */
#vw-bereich .vw-filter-knopf.vw-an {
border-color: rgb(var(--vw-farbe) / .55);
background: rgb(var(--vw-farbe) / .14);
box-shadow: 0 0 18px -6px rgb(var(--vw-farbe) / .5);
}
/* Die Schrift bleibt ABSICHTLICH hell und wird NICHT eingefaerbt.
Sonst waere der Reiter im Bereich "Kunden" in Prism Indigo
geschrieben -- die einzige Farbe der Palette, die als Text
durchfaellt (3,25:1 gegen die dunklen Flaechen). */
#vw-bereich .vw-filter-knopf.vw-an { color: var(--wd-text); }
/* Ein feiner Leuchtstrich unter dem Titel, in der Leitfarbe.
Traegt keine Bedeutung, die nur ueber Farbe laeuft -- der Titel
daneben sagt dasselbe in Worten. */
#vw-bereich .vw-leiste h1 { position: relative; }
#vw-bereich .vw-leiste h1::after {
content: "";
position: absolute;
left: 0; bottom: -.32em;
width: clamp(46px, 22%, 120px);
height: 2px;
border-radius: 2px;
background: linear-gradient(90deg,
rgb(var(--vw-farbe) / .95), rgb(var(--vw-farbe) / 0));
}
/* Das Licht, das den Kacheln unter dem Mauszeiger folgt, nimmt
ebenfalls die Leitfarbe an. Dadurch fuehlt sich jeder Bereich
anders an, ohne dass irgendwo ein zweites Bild noetig waere. */
#vw-bereich .wd-karte { --wd-lumen: var(--vw-farbe); }
/* Die Anmeldekarte bekommt das Wappen. Sie ist halbdurchsichtig,
das Motiv scheint also durch die Glasflaeche hindurch.
@@ -3067,9 +3304,40 @@
var aktiverTab = "uebersicht";
document.getElementById("vw-titel").textContent = ANSICHTEN[aktiverTab].titel;
/* ---------------------------------------------------------------------
Die Bereichs-Signatur umschalten.
Das Kennzeichen sitzt am Wurzelelement, nicht am Reiter: So kann das
Stilblatt allein daraus Motiv UND Leitfarbe ableiten, und es gibt
genau eine Stelle, an der sich der Bereich aendert.
Der Umweg ueber "animation neu ausloesen" ist noetig, weil eine
CSS-Animation nicht von selbst neu startet, wenn sich nur eine
Variable aendert. Ohne ihn springt das Bild hart um, statt sanft
zu wechseln.
--------------------------------------------------------------------- */
function signaturSetzen(bereich) {
if (!ANSICHTEN[bereich]) return; // unbekannt: lieber nichts tun
document.documentElement.setAttribute("data-vw-bereich", bereich);
var schmuck = document.querySelector(".vw-schmuck");
if (!schmuck) return;
/* Wer Bewegung reduziert haben will, bekommt keinen Neustart -- die
Animation ist fuer ihn ohnehin abgeschaltet. */
try {
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
} catch (e) { /* aeltere Browser: dann eben mit Animation */ }
schmuck.style.animation = "none";
void schmuck.offsetWidth; // erzwingt das Neuberechnen
schmuck.style.animation = "";
}
signaturSetzen(aktiverTab);
document.querySelectorAll("[data-tab]").forEach(function (b) {
b.addEventListener("click", function () {
aktiverTab = b.dataset.tab;
signaturSetzen(aktiverTab);
document.querySelectorAll("[data-tab]").forEach(function (x) {
x.classList.toggle("vw-an", x.dataset.tab === aktiverTab);
});