CRYONOVA: Farb- und Lichtsystem nach dem Design-System umgesetzt
Umsetzung des Design-Systems "Baby Blue Optical Luxury" (Edition 2.0), Schritt 1 von mehreren: die zentrale Farbquelle. Nach der Kernregel auf Seite 3 ist das ausdrücklich ein Farb- und Licht-Redesign — Aufbau, Navigation, Texte und Funktionen bleiben unangetastet. FARBEN Grundflächen auf die vier Tiefenebenen des Systems (Void, Midnight, Obsidian, Deep Glass). Palette nach Seite 5: Signature Baby, Ion Blue, Hyper Aqua, Aurora Violet, Prism Indigo, Liquid Gold, Signal Coral, Chrome Silver. Die Variablen behalten ihre alten NAMEN (--wd-blau statt --cryo-baby). Ein Umbenennen hätte über 155 Fundstellen anfassen müssen — viel Bewegung ohne sichtbaren Nutzen, mit der realen Gefahr, eine Stelle zu übersehen und danach zwei fast gleiche Blautöne zu haben. Entscheidend ist die Rolle, nicht der Name; die Systembezeichnungen stehen als Kommentar daneben. EIN FEHLER IM DESIGN-SYSTEM, DER BEWUSST NICHT ÜBERNOMMEN WURDE Die Token-Liste auf Seite 20 ist um eine Zeile verrutscht — Namen und Hex-Werte passen dort nicht zusammen. Am folgenreichsten: --cryo-baby stünde auf #0C2740, einem fast schwarzen Navy, und ist laut Seite 21 zugleich die Standard-Lumenfarbe. Das Mauslicht wäre damit praktisch unsichtbar geworden — ausgerechnet der Effekt, den das System auf fünf Seiten als unantastbar schützt. Maßgeblich ist deshalb die Palette auf Seite 5 und die Lumen-Logik auf Seite 9, die untereinander stimmig sind. MAUSLICHT Der bestehende Effekt bleibt vollständig erhalten (Systemauflage) und bekommt eine Farbvariable pro Kachel: --wd-lumen. Vorher war die Farbe im Verlauf fest verdrahtet, und jede weitere Kachelfarbe hätte zwei neue Blöcke gebraucht (Fläche + leuchtende Kante). Bei sechs Lumen-Rollen wären das zwölf fast gleiche Blöcke gewesen, die beim nächsten Feinschliff zwangsläufig auseinanderlaufen. Jetzt setzt die Kachel nur ihre Farbe, der Verlauf steht einmal da — genau das meint Token-Regel 02 mit "Kachelfarbe steuert Lumenfarbe". 38 lose Hex-Codes durch Token ersetzt (Token-Regel 05). Drei davon (#3d9dbd, #7c5cd6, #a8873a) waren noch die ALTEN Markenfarben und hätten still neben den neuen weitergelebt — genau der Mechanismus, durch den Oberflächen mit der Zeit zwei fast gleiche Töne bekommen. KONTRASTE NACHGERECHNET Die Palette ist auf dunklem Grund durchweg stark (9,7 bis 19,8:1) — mit einer Ausnahme: Prism Indigo erreicht auf keiner Fläche 4,5:1 (nur 3,25 bis 4,29). Es ist deshalb ausschließlich für Kanten, Verläufe und große Premium-Flächen zugelassen, nie für Fließtext. Das System sieht Indigo ohnehin nur für "Premium-Momente" vor — die Rechnung bestätigt die Regel, statt ihr zu widersprechen. NEUER TEST: pruef-cryonova.mjs (24 Prüfungen) Palette, Grundflächen, Mauslicht-Erhalt, echte Zeigerbewegung, keine losen Hex-Codes, Kontraste und die Frage, ob alle 12 Seiten wirklich aus derselben Quelle schöpfen. Dabei drei Fehlalarme im eigenen Test gefunden und behoben — jeder davon hätte dauerhaft rote Zeilen erzeugt und irgendwann dazu geführt, dass man eine echte Meldung übersieht: - Halbtransparente Flächen müssen über ihren Untergrund gerechnet werden. Der aktive Reiter kam sonst auf 1:1 statt echter 8,25–10,23:1. - Text auf Farbverläufen liefert rgba(0,0,0,0) als Hintergrund; der Hauptknopf kam so auf 1:1 statt rund 11:1. - Die Maus muss mit Zwischenschritten bewegt werden, sonst feuert pointermove nicht. Eine Direktmessung bestätigte: Das Licht folgt einwandfrei (30px → 723px, Deckkraft 1). pruef-system.mjs auf den neuen Markenton gesetzt. Dass er dort zunächst "0 von 1804 Elementen" meldete, war kein Fehler, sondern der Beweis, dass der alte Ton nirgends mehr vorkommt. Geprüft: 24 (CRYONOVA) + 0 Fundstellen (Design/WCAG über 12 Seiten × 5 Sprachen) + 40 + 56 + 69 + 54 + 42 + 55 + 5 + 16 — alles grün.
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
/* =====================================================================
|
||||
pruef-cryonova.mjs — das Farb- und Lichtsystem
|
||||
|
||||
Prüft, dass die Seite dem Design-System "Baby Blue Optical Luxury"
|
||||
(Edition 2.0) folgt — und zwar an den Stellen, wo ein Abweichen
|
||||
folgenlos aussieht, aber Bedeutung zerstört:
|
||||
|
||||
1. Die Paletten-Werte selbst (ein Tippfehler in einem Hex-Code
|
||||
fällt mit blossem Auge nicht auf).
|
||||
2. Das Mauslicht MUSS erhalten bleiben und die Kachelfarbe
|
||||
übernehmen — das System schützt es auf fünf Seiten
|
||||
ausdrücklich vor Entfernen, Ersetzen und Abschwächen.
|
||||
3. Keine losen Hex-Codes ausserhalb der Token (Token-Regel 05).
|
||||
4. Kontraste — inklusive der Falle, an der ich beim Bauen selbst
|
||||
hereingefallen bin (siehe unten).
|
||||
|
||||
DIE KONTRAST-FALLE, DIE HIER BEWUSST RICHTIG GERECHNET WIRD:
|
||||
Ein erster, schneller Messversuch las den halbtransparenten
|
||||
Kachelhintergrund als fertige Farbe und meldete beim aktiven Reiter
|
||||
1:1 — also scheinbar unsichtbaren Text. In Wahrheit liegt die 13-%-
|
||||
Fläche über einem dunklen Grund, und der echte Wert ist 8,25 bis
|
||||
10,23:1. Wer halbtransparente Flächen nicht über ihren Untergrund
|
||||
rechnet, bekommt Fehlalarme und stumpft irgendwann gegen die
|
||||
Meldungen ab.
|
||||
===================================================================== */
|
||||
import { chromium } 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" };
|
||||
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(4372, r));
|
||||
|
||||
const ergebnisse = [];
|
||||
const pruefe = (n, ok, z) => {
|
||||
ergebnisse.push({ n, ok });
|
||||
console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : ""));
|
||||
};
|
||||
|
||||
/* Die verbindliche Palette (Seite 5 des Systems).
|
||||
|
||||
BEWUSST NICHT aus der Token-Liste auf Seite 20 übernommen: Dort sind
|
||||
Namen und Hex-Werte um eine Zeile verrutscht. Am folgenreichsten
|
||||
wäre --cryo-baby auf #0C2740 gewesen, einem fast schwarzen Navy —
|
||||
und dieser Wert ist laut Seite 21 zugleich die Standard-Lumenfarbe.
|
||||
Das Mauslicht wäre damit unsichtbar geworden, also genau der Effekt,
|
||||
den das System an anderer Stelle als unantastbar bezeichnet. */
|
||||
const PALETTE = {
|
||||
"--wd-blau-rgb": { hex: "#8FD8FF", name: "Signature Baby" },
|
||||
"--wd-ion-rgb": { hex: "#4AC8FF", name: "Ion Blue" },
|
||||
"--wd-aqua-rgb": { hex: "#2EF2E0", name: "Hyper Aqua" },
|
||||
"--wd-lila-rgb": { hex: "#A76BFF", name: "Aurora Violet" },
|
||||
"--wd-indigo-rgb": { hex: "#615CFF", name: "Prism Indigo" },
|
||||
"--wd-gold-rgb": { hex: "#E8C36A", name: "Liquid Gold" },
|
||||
"--wd-fehler-rgb": { hex: "#FF5C7A", name: "Signal Coral" },
|
||||
"--wd-chrome-rgb": { hex: "#AFC4D2", name: "Chrome Silver" },
|
||||
};
|
||||
const FLAECHEN = {
|
||||
"--wd-schwarz": "#030810",
|
||||
"--wd-dunkelblau": "#061323",
|
||||
"--wd-dunkelblau-2": "#091B2D",
|
||||
"--wd-flaeche": "#0C2740",
|
||||
};
|
||||
|
||||
const hexZuRgb = (h) => [1, 3, 5].map((i) => parseInt(h.substr(i, 2), 16));
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } });
|
||||
await seite.goto("http://127.0.0.1:4372/webdesign/index.html");
|
||||
await seite.waitForTimeout(500);
|
||||
|
||||
console.log("\n=== 1. PALETTE ===");
|
||||
const werte = await seite.evaluate((namen) => {
|
||||
const w = getComputedStyle(document.documentElement);
|
||||
const raus = {};
|
||||
for (const n of namen) raus[n] = w.getPropertyValue(n).trim();
|
||||
return raus;
|
||||
}, [...Object.keys(PALETTE), ...Object.keys(FLAECHEN)]);
|
||||
|
||||
for (const [token, soll] of Object.entries(PALETTE)) {
|
||||
const ist = (werte[token] || "").split(/\s+/).map(Number);
|
||||
const erwartet = hexZuRgb(soll.hex);
|
||||
pruefe(`${soll.name} (${token}) ist ${soll.hex}`,
|
||||
ist.length === 3 && ist.every((v, i) => v === erwartet[i]),
|
||||
werte[token] + " statt " + erwartet.join(" "));
|
||||
}
|
||||
for (const [token, soll] of Object.entries(FLAECHEN)) {
|
||||
pruefe(`Grundflaeche ${token} ist ${soll}`,
|
||||
(werte[token] || "").toLowerCase() === soll.toLowerCase(), werte[token]);
|
||||
}
|
||||
|
||||
console.log("\n=== 2. MAUSLICHT (darf nie entfernt werden) ===");
|
||||
const lumen = await seite.evaluate(() => {
|
||||
const css = Array.from(document.styleSheets)
|
||||
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } });
|
||||
const alle = css.map((r) => r.cssText).join("\n");
|
||||
return {
|
||||
hatVariable: /--wd-lumen\s*:/.test(alle),
|
||||
hatPosition: /--wd-lichtx/.test(alle) && /--wd-lichty/.test(alle),
|
||||
nutztVariableImVerlauf: /radial-gradient\([^)]*var\(--wd-lichtx\)[^}]*var\(--wd-lumen\)/.test(alle)
|
||||
|| /var\(--wd-lumen\)[^}]*var\(--wd-lichtx\)/.test(alle),
|
||||
rollen: ["--wd-karte--lila", "aqua", "premium", "vip", "kritisch"]
|
||||
.filter((r) => alle.includes(r)).length,
|
||||
hatUebergang: /transition:\s*opacity/.test(alle),
|
||||
hinterInhalt: /\.wd-karte\s*>\s*\*\s*\{[^}]*z-index:\s*1/.test(alle),
|
||||
};
|
||||
});
|
||||
pruefe("die Lichtposition wird als Variable gefuehrt", lumen.hatPosition);
|
||||
pruefe("es gibt eine Farbvariable pro Kachel (--wd-lumen)", lumen.hatVariable);
|
||||
pruefe("der Lichtverlauf benutzt diese Farbvariable", lumen.nutztVariableImVerlauf);
|
||||
pruefe("mehrere Lumen-Rollen sind hinterlegt", lumen.rollen >= 4, lumen.rollen + " gefunden");
|
||||
pruefe("das Licht blendet weich aus (Uebergang vorhanden)", lumen.hatUebergang);
|
||||
/* Das System verlangt ausdruecklich: Licht hinter dem Inhalt, niemals
|
||||
darueber -- sonst leidet die Lesbarkeit (Seite 9). */
|
||||
pruefe("der Inhalt liegt UEBER dem Licht", lumen.hinterInhalt);
|
||||
|
||||
console.log("\n=== 3. LICHT FOLGT WIRKLICH DEM ZEIGER ===");
|
||||
await seite.goto("http://127.0.0.1:4372/webdesign/leistungen.html");
|
||||
await seite.waitForTimeout(600);
|
||||
const karte = seite.locator(".wd-karte").first();
|
||||
if (await karte.count()) {
|
||||
const box = await karte.boundingBox();
|
||||
|
||||
/* MIT ZWISCHENSCHRITTEN bewegen (steps), nicht in einem Sprung.
|
||||
|
||||
Der Code haengt an "pointermove". Ein Sprung von A nach B erzeugt
|
||||
unter Umstaenden nur ein einziges Ereignis am Ziel -- und die erste
|
||||
Messung sah dann zweimal denselben Wert und meldete faelschlich,
|
||||
das Licht folge dem Zeiger nicht. Eine direkte Nachmessung zeigte:
|
||||
Es folgt einwandfrei (30px -> 723px). Der Fehler lag in der
|
||||
Nachahmung der Mausbewegung, nicht in der Seite. */
|
||||
await seite.mouse.move(box.x + 40, box.y + 30, { steps: 5 });
|
||||
await seite.waitForTimeout(250);
|
||||
const p1 = await karte.evaluate((e) => e.style.getPropertyValue("--wd-lichtx"));
|
||||
|
||||
/* Deckkraft JETZT messen, solange der Zeiger noch auf der Kachel
|
||||
steht -- danach wandert er weiter und das Licht blendet aus. */
|
||||
const sichtbar = await karte.evaluate((e) =>
|
||||
getComputedStyle(e, "::after").opacity);
|
||||
|
||||
await seite.mouse.move(box.x + box.width - 60, box.y + box.height - 40, { steps: 8 });
|
||||
await seite.waitForTimeout(250);
|
||||
const p2 = await karte.evaluate((e) => e.style.getPropertyValue("--wd-lichtx"));
|
||||
|
||||
pruefe("die Lichtposition folgt dem Zeiger",
|
||||
p1 !== p2 && p1 !== "" && p2 !== "", `${p1} -> ${p2}`);
|
||||
pruefe("das Licht ist beim Ueberfahren voll sichtbar",
|
||||
parseFloat(sichtbar) > 0.5, "Deckkraft " + sichtbar);
|
||||
pruefe("die Kachel gibt dem Licht ihre eigene Farbe",
|
||||
(await karte.evaluate((e) => getComputedStyle(e).getPropertyValue("--wd-lumen"))).trim()
|
||||
=== "143 216 255",
|
||||
await karte.evaluate((e) => getComputedStyle(e).getPropertyValue("--wd-lumen")));
|
||||
} else {
|
||||
pruefe("eine Kachel zum Pruefen gefunden", false, "keine .wd-karte");
|
||||
}
|
||||
|
||||
console.log("\n=== 4. KEINE LOSEN HEX-CODES (Token-Regel 05) ===");
|
||||
/* Farben gehoeren in die Token. Verstreute Hex-Codes sind der Grund,
|
||||
warum Oberflaechen mit der Zeit zwei fast gleiche Blautoene bekommen. */
|
||||
const cssText = readFileSync(join(W, "assets/css/webdesign.css"), "utf8");
|
||||
const ohneKommentare = cssText.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
const wurzelEnde = ohneKommentare.indexOf("}", ohneKommentare.indexOf(":root"));
|
||||
const nachWurzel = ohneKommentare.slice(wurzelEnde);
|
||||
const lose = (nachWurzel.match(/#[0-9a-fA-F]{6}\b/g) || []);
|
||||
pruefe("ausserhalb der Token stehen keine Hex-Farben",
|
||||
lose.length === 0, lose.slice(0, 6).join(", ") || "keine");
|
||||
|
||||
console.log("\n=== 5. KONTRASTE (halbtransparente Flaechen korrekt gerechnet) ===");
|
||||
const kontraste = await seite.evaluate(() => {
|
||||
/* Halbtransparente Flaechen ueber ihren Untergrund legen, statt sie
|
||||
als fertige Farbe zu lesen -- sonst kommen Fehlalarme heraus. */
|
||||
function zahlen(c) { return (c.match(/[\d.]+/g) || []).map(Number); }
|
||||
function ueberlagern(el) {
|
||||
let p = el, stapel = [];
|
||||
while (p) {
|
||||
const c = zahlen(getComputedStyle(p).backgroundColor);
|
||||
if (c.length >= 3) {
|
||||
const a = c.length === 4 ? c[3] : 1;
|
||||
if (a > 0) stapel.push([c[0], c[1], c[2], a]);
|
||||
if (a >= 1) break;
|
||||
}
|
||||
p = p.parentElement;
|
||||
}
|
||||
let r = [3, 8, 16];
|
||||
for (let i = stapel.length - 1; i >= 0; i--) {
|
||||
const [cr, cg, cb, a] = stapel[i];
|
||||
r = [cr * a + r[0] * (1 - a), cg * a + r[1] * (1 - a), cb * a + r[2] * (1 - a)];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
function lum(c) {
|
||||
const v = c.map((x) => { x /= 255; return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); });
|
||||
return 0.2126 * v[0] + 0.7152 * v[1] + 0.0722 * v[2];
|
||||
}
|
||||
function kon(a, b) { const l1 = lum(a), l2 = lum(b); const [h, n] = l1 > l2 ? [l1, l2] : [l2, l1]; return +((h + 0.05) / (n + 0.05)).toFixed(2); }
|
||||
|
||||
const schlecht = [];
|
||||
for (const el of document.querySelectorAll("p, h1, h2, h3, h4, a, span, li, label, button, td, th")) {
|
||||
const t = (el.textContent || "").trim();
|
||||
if (!t || el.children.length > 0) continue;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 2 || r.height < 2) continue;
|
||||
|
||||
/* AUSSERHALB DES BILDES -- kein sichtbarer Text.
|
||||
Der Sprunglink ("Direkt zum Inhalt") liegt bei left:-9999px und
|
||||
wird erst beim Tastaturfokus hereingeholt. Er ist technisch
|
||||
sichtbar, aber niemand liest ihn dort. */
|
||||
if (r.right < 0 || r.left > innerWidth) continue;
|
||||
|
||||
const st = getComputedStyle(el);
|
||||
if (st.visibility === "hidden" || st.display === "none" || +st.opacity < 0.1) continue;
|
||||
|
||||
/* TEXT AUF EINEM FARBVERLAUF laesst sich nicht mit einer einzigen
|
||||
Hintergrundfarbe verrechnen -- getComputedStyle liefert dort
|
||||
rgba(0,0,0,0), und die Messung faellt auf den Seitenhintergrund
|
||||
zurueck. Beim Hauptknopf (dunkler Text auf hellem Babyblau-
|
||||
Verlauf) kam so 1:1 heraus, obwohl er in Wirklichkeit rund 11:1
|
||||
hat. Solche Faelle pruefe ich von Hand; pruef-design.mjs meldet
|
||||
sie getrennt als "von Hand zu pruefen". */
|
||||
let ahn = el, aufVerlauf = false;
|
||||
for (let i = 0; ahn && i < 4; i++, ahn = ahn.parentElement) {
|
||||
if (getComputedStyle(ahn).backgroundImage !== "none") { aufVerlauf = true; break; }
|
||||
}
|
||||
if (aufVerlauf) continue;
|
||||
const gr = parseFloat(st.fontSize);
|
||||
const fett = +st.fontWeight >= 700;
|
||||
const ziel = (gr >= 24 || (gr >= 18.66 && fett)) ? 3 : 4.5;
|
||||
const k = kon(zahlen(st.color).slice(0, 3), ueberlagern(el));
|
||||
if (k < ziel) schlecht.push({ t: t.slice(0, 30), k, ziel, gr });
|
||||
}
|
||||
return schlecht;
|
||||
});
|
||||
pruefe("jeder sichtbare Text erreicht seinen Zielkontrast",
|
||||
kontraste.length === 0,
|
||||
kontraste.slice(0, 4).map((x) => `"${x.t}" ${x.k}/${x.ziel}`).join(" | ") || "alle OK");
|
||||
|
||||
console.log("\n=== 6. DAS SYSTEM WIRKT AUF ALLEN SEITEN ===");
|
||||
/* Eine zentrale Quelle heisst: eine Aenderung wirkt ueberall. Wenn eine
|
||||
Seite noch einen alten Ton zeigt, ist genau das nicht der Fall. */
|
||||
const seiten = readdirSync(join(W, "webdesign")).filter((f) => f.endsWith(".html") && f !== "diagnose.html");
|
||||
let abweichend = [];
|
||||
for (const datei of seiten) {
|
||||
await seite.goto("http://127.0.0.1:4372/webdesign/" + datei);
|
||||
await seite.waitForTimeout(180);
|
||||
const baby = await seite.evaluate(() =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--wd-blau-rgb").trim());
|
||||
if (baby !== "143 216 255") abweichend.push(datei + ": " + baby);
|
||||
}
|
||||
pruefe(`alle ${seiten.length} Seiten nutzen dieselbe Farbquelle`,
|
||||
abweichend.length === 0, abweichend.join(", ") || "einheitlich");
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user