Wer mit der Tastatur bedient, sieht wieder, wo er steht

pruef-barrierefrei-workspace, wissen.html, Scout und Creator: Beim
Durchtabben veraendert sich an den Wissenskacheln NICHTS. Kein Rahmen,
kein Ring, keine Kante. Wer nicht mit der Maus arbeitet, tippt blind.

=== EIN SPEZIFITAETS-UNFALL, UND ER BETRIFFT NICHT NUR DIESE SEITE ===

Die Regel war da und richtig geschrieben:

  wissen.css   .kachel:focus-visible { box-shadow: 0 0 0 3px ... }

Sie kam nur nicht an. Gemessen mit einem neuen Werkzeug
(server/mess-fokus.mjs, echte Tastendruecke, kein focus()):

  :focus true   :focus-visible true
  boxShadow     gleich   rgba(0,0,0,0.95) 7px 7px 14px -10px inset
  outline       gleich   none

`:focus-visible` griff also, und trotzdem blieb alles, wie es war.
Der Grund steht in module.css, in einer Liste von 45 Klassennamen:

  :is(.eintrag-karte, ..., .kachel, ..., .gruppe[data-gruppe], ...)

`:is()` uebernimmt die Spezifitaet seines STAERKSTEN Arguments.
`.gruppe[data-gruppe]` ist eine Klasse PLUS ein Attribut. Damit ist die
ganze Liste (0,2,0) statt (0,1,0) -- genau so stark wie
`.kachel:focus-visible`. Bei Gleichstand gewinnt, was spaeter geladen
wird, und module.css wird zuletzt geladen. Ein einziges Attribut in
einer Aufzaehlung, sechs Zeilen weiter rechts, hat den Fokusring von
jedem Bauteil im Haus verschluckt, dessen Fokusregel aus einer Klasse
besteht.

=== GELOEST WIRD DAS NICHT, INDEM MAN DIE LISTE SCHWAECHER MACHT ===

Das war schon einmal so (`:where()`, Spezifitaet null) und ergab einen
Zwitter aus neuer Form und alter Kante -- der Kommentar in module.css
beschreibt es. Wer die Zahl senkt, verschiebt das Problem auf die
naechste Regel.

Geloest wird es, indem die ANTWORT dort steht: ein Fokusring fuer die
ganze Modulliste, in derselben Datei wie die Form, mit
`:focus-visible` also eine Klasse staerker als die Grundregel. Er gilt
damit fuer jedes Modul im Haus -- auch fuer die, die es noch nicht
gibt, und auch dort, wo nie jemand an eine Fokusregel gedacht hat.

`outline-offset` ist NEGATIV, und das ist kein Geschmack: `clip-path`
(die Fase an der Ecke) schneidet alles ab, was ausserhalb der Form
liegt. Ein Ring mit positivem Abstand waere unsichtbar gewesen -- der
alte war es ja auch. Nach innen gezeichnet bleibt er stehen. Gemessen,
nicht geschlossen.

=== WAS NICHT MITREPARIERT WURDE, UND WARUM ES DASTEHT ===

Derselbe Gleichstand trifft auch Hover-Regeln in frueher geladenen
Dateien: `.ablage:hover` (dateien.css), `.call:hover` (calls.css),
`.kk:hover` (scouting.css), `.fortschritt:hover` (uebersicht.css)
setzen alle `border-color`, und die Grundregel setzt `border: 0`.
Diese vier tun vermutlich nichts.

Angefasst habe ich sie nicht -- es gibt kein Messgeraet dafuer. Fokus
laesst sich pruefen (die Pruefung tabbt und vergleicht), Hover nicht.
Und die naheliegende Loesung wuerde die Form von 45 Bauteilen auf 38
Seiten neu entscheiden; das ohne Messgeraet zu tun waere Raten mit viel
Einsatz. Der Befund steht deshalb als Absatz in module.css, damit der
Naechste nicht wieder bei null anfaengt.

=== UND EINE BEHAUPTUNG VON MIR WIRD ZURUECKGENOMMEN ===

Im Commit d7bb0f7e steht, ein Mindestabstand von 0,10 sei fuer 45
Kachelfarben „rechnerisch nicht mehr moeglich", mit einer Tabelle von
„Decken". Das ist falsch herum gelesen: Das Werkzeug SUCHT eine
Anordnung. Findet es nichts Besseres, sagt das etwas ueber das
Verfahren, nicht ueber die Welt. Nachweisbar ist nur, was es GEFUNDEN
hat -- und wie wenig das eine Decke ist, zeigt der eigene Lauf: Fuer
dieselben 45 Farben kam dasselbe Verfahren je nach Rasterweite einmal
auf 0,0974 und einmal auf 0,0876.

Die Pruefung sagt das jetzt so, und das Werkzeug heisst zwar weiter
kachelton-decke.mjs, erklaert aber in den ersten zwanzig Zeilen, dass
es keine Decke misst. Beide Werkzeuge liegen jetzt ueberhaupt im
Verzeichnis: Sie hiessen `tools/_toene-*.mjs`, und `tools/_*` ist
ausgenommen -- die Kommentare verwiesen also auf Dateien, die es nach
dem Klonen nirgends gibt.

GEMESSEN, alles nach den Aenderungen:

  pruef-barrierefrei-workspace   18 Punkte, 0 Fehler (vorher 2)
  pruef-buehne                  230 Punkte, 0 Fehler
  pruef-kachelraster             15 Punkte, 0 Fehler
  pruef-start-ansicht           157 Punkte, 0 Fehler
  pruef-kopfleiste-farbe          9 Punkte, 0 Fehler
  pruef-rueckmeldung             30 Punkte, 0 Fehler
  pruef-entwicklung-kacheln      34 Punkte, 0 Fehler
  pruef-kachel-universum         13 Punkte, 0 Fehler

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 23:50:54 +02:00
co-authored by Claude Opus 5
parent 80988f2d23
commit 69ba533cee
46 changed files with 1332 additions and 663 deletions
+152
View File
@@ -0,0 +1,152 @@
/* =====================================================================
SIEHT MAN, WO DER TASTATURFOKUS STEHT?
=====================================================================
Gebaut am 25.09.2026, weil `pruef-neue-seiten` einen Kontrast von
1,07 meldete ("Schoen, dass du da bist") und die Stelle im CSS
tadellos aussah. Diese Messung liest die Farben an Ort und Stelle
aus und geht die Elternkette hoch, bis sie eine deckende Flaeche
findet -- genau das, was die Pruefung tut, nur sichtbar.
Ergebnis damals: dunkler Text auf HELLBLAUER Flaeche, rund 10:1.
Die Pruefung konnte das Format `color(srgb ...)` nicht lesen, ging
deshalb zum fast schwarzen Elternteil weiter und rechnete dunkel
gegen dunkel. Ohne diese Messung haette ich an einem CSS geaendert,
an dem nichts war.
Aufrufen mit:
node server/mess-farbe-am-text.mjs <seite.html> "<Textanfang>"
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const SEITE = process.argv[2] || "wissen.html";
const SUCHE = process.argv[3] || ".kachel";
const PORT = 5457;
/* IST DIE NUMMER FREI? Sonst misst dieses Werkzeug an einem fremden
Server -- und liefert ein Ergebnis, das zu einem ganz anderen Stand
gehoert. Die Pruefungen holen sich ihre Nummer ueber helfer-port;
ein Messwerkzeug, das von Hand gestartet wird, braucht wenigstens
diese eine Frage. */
{
const netz = await import("node:net");
const frei = await new Promise((fertig) => {
const s = netz.createServer();
s.once("error", () => fertig(false));
s.once("listening", () => s.close(() => fertig(true)));
s.listen(PORT, "127.0.0.1");
});
if (!frei) {
console.error(`Port ${PORT} ist belegt -- hier laeuft schon etwas. `
+ "Gemessen wuerde sonst ein fremder Stand. Abgebrochen.");
process.exit(3);
}
}
const ordner = mkdtempSync(join(tmpdir(), "ws-kontrast-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) {
return ec.call(this, n, w, { ...(o || {}), secure: false });
};
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(200_000, "mess-kontrast");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const sch = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", sch, jetzt);
const anlegen = (name, rolle, code) => {
const s = randomBytes(16).toString("hex");
const h = scryptSync(code, s, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
return d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
.get(name, rolle, h, s, 32768,
createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
};
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Sina", "scout", "CODE-SCOUT-0001");
d.close();
const an = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "scout", code: "CODE-SCOUT-0001" }),
});
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
const pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
const kontext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
await kontext.addCookies(keks.split("; ").map((k) => {
const [n, ...r] = k.split("=");
return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await kontext.newPage();
await seite.goto(`${BASIS}/workspace/${SEITE}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const stil = (e) => { const c = getComputedStyle(e); return {
outlineStyle: c.outlineStyle, outlineWidth: c.outlineWidth, outlineColor: c.outlineColor,
boxShadow: c.boxShadow, borderColor: c.borderColor, backgroundColor: c.backgroundColor,
textDecorationLine: c.textDecorationLine, filter: c.filter, clipPath: c.clipPath }; };
const vorher = await seite.evaluate((sel) => {
const e = document.querySelector(sel);
if (!e) return null;
e.setAttribute("data-mess", "1");
const c = getComputedStyle(e);
return { text: (e.textContent || "").trim().slice(0, 30), tag: e.tagName,
outline: `${c.outlineStyle} ${c.outlineWidth} ${c.outlineColor}`,
boxShadow: c.boxShadow, borderColor: c.borderColor, backgroundColor: c.backgroundColor,
filter: c.filter, clipPath: c.clipPath.slice(0, 60) };
}, SUCHE);
if (!vorher) { console.log(`${SUCHE} nicht gefunden auf ${SEITE}`); process.exit(1); }
await seite.evaluate(() => document.body.focus?.());
let gefunden = null;
for (let i = 0; i < 40 && !gefunden; i++) {
await seite.keyboard.press("Tab");
await seite.waitForTimeout(140);
gefunden = await seite.evaluate(() => {
const e = document.activeElement;
if (!e || !e.hasAttribute?.("data-mess")) return null;
const c = getComputedStyle(e);
return { outline: `${c.outlineStyle} ${c.outlineWidth} ${c.outlineColor}`,
boxShadow: c.boxShadow, borderColor: c.borderColor, backgroundColor: c.backgroundColor,
filter: c.filter, clipPath: c.clipPath.slice(0, 60),
fv: e.matches(":focus-visible"), f: e.matches(":focus") };
});
}
console.log(`
=== ${SUCHE} auf ${SEITE} (${vorher.tag}, "${vorher.text}") ===`);
if (!gefunden) console.log(" mit der Tastatur NICHT erreicht");
else {
console.log(` :focus ${gefunden.f} :focus-visible ${gefunden.fv}`);
for (const k of ["outline", "boxShadow", "borderColor", "backgroundColor", "filter", "clipPath"]) {
const gleich = vorher[k] === gefunden[k];
console.log(` ${k.padEnd(16)} ${gleich ? "gleich " : "ANDERS "} `
+ `${String(vorher[k]).slice(0, 55)}`
+ (gleich ? "" : `
-> ${String(gefunden[k]).slice(0, 55)}`));
}
}
await browser.close();
process.exit(0);
+43 -32
View File
@@ -120,52 +120,63 @@ for (let i = 0; i < liste.length; i++) {
}
paare.sort((x, y) => x.d - y.d);
/* ==== DIE GRENZE WAR FUER EINUNDZWANZIG FARBEN GERECHNET (25.09.2026)
/* ==== DIE GRENZE WAR FUER EINUNDZWANZIG FARBEN GESETZT (25.09.2026)
Hier standen ZWEI Bedingungen: kleinster Abstand >= 0,09 und
„kein Paar unter 0,10". Die zweite war seit dem 17.09. nicht mehr
erfuellbar -- nicht schwer, sondern UNMOEGLICH.
„kein Paar unter 0,10". Die zweite hat seit dem 17.09. niemand
mehr erfuellt, und niemand konnte sagen, wie es gehen sollte.
Nachgerechnet (tools/_toene-packen.mjs): Aus allen sRGB-Farben,
die 4,8:1 gegen den Grund halten, nicht blenden und bunt genug
sind, laesst sich fuer eine gegebene Anzahl ein groesster
moeglicher Mindestabstand bestimmen -- eine Kugelpackung:
Gesucht wurde mit tools/kachelton-decke.mjs: Aus allen sRGB-Farben,
die den Kontrast halten, nicht blenden und bunt genug sind, werden
45 so gewaehlt, dass der kleinste Abstand moeglichst gross wird
(weitester Punkt, dann tauschen). Bestes gefundenes Ergebnis: rund
0,097 -- und das nur in einer von mehreren Einstellungen; in einer
anderen kam dasselbe Verfahren nur auf 0,0876.
30 Farben -> 0,115 48 Farben -> 0,0925
40 Farben -> 0,103 52 Farben -> 0,0840
45 Farben -> 0,0974 60 Farben -> 0,0805
WAS DAS BEWEIST UND WAS NICHT -- der Unterschied ist wichtig:
Bei 21 Farben war 0,10 muehelos. Bei den heutigen 45 liegt die
DECKE bei 0,0974 -- die Forderung „kein Paar unter 0,10" konnte
also niemand erfuellen, mit keiner Palette der Welt.
Es beweist, dass 0,094 ERREICHBAR ist. Dafuer gibt es eine
Anordnung, und sie steht in start.css.
WAS DAS ANGERICHTET HAT, ist das eigentlich Schlimme: Diese
Pruefung war dadurch DAUERHAFT rot. Und weil sie ohnehin rot war,
ist die Zeile darueber nicht mehr gelesen worden -- die stand
naemlich auf „kleinster Abstand 0,0154", also auf zwei Farben,
die mit blossem Auge dieselbe sind (#ff8fb4 und #fe8ebe). Acht
solche Paare gab es. Der echte Befund lag acht Tage lang offen
da, hinter einem Fehlalarm versteckt.
Es beweist NICHT, dass 0,10 unmoeglich ist. Ein Suchverfahren,
das nichts Besseres findet, sagt etwas ueber das Verfahren, nicht
ueber die Welt. Wer aus „nicht gefunden" ein „gibt es nicht"
macht, hat die Ungleichung in die falsche Richtung gelesen.
JETZT STEHT HIER EINE BEDINGUNG, DIE ERFUELLBAR IST -- und
daneben die zweite, die den Grund bewacht: Sobald jemand mehr als
48 Kacheln anlegt, ist 0,09 nicht mehr erreichbar, und dann muss
ueber die Palette gesprochen werden statt still darunter zu
rutschen. Die Zahl ist gemessen, nicht gesetzt. */
Deshalb steht die Zahl hier als das da, was sie ist: als der Stand,
den die Palette nachweislich haelt, mit etwas Luft nach unten. Sie
heisst nicht mehr „Decke", denn eine Decke ist sie nicht.
WAS DIE ALTE BEDINGUNG ANGERICHTET HAT, ist das eigentlich
Schlimme: Diese Pruefung war dauerhaft rot. Und weil sie ohnehin
rot war, ist die Zeile darueber nicht mehr gelesen worden -- die
stand naemlich auf „kleinster Abstand 0,0154", also auf zwei
Farben, die mit blossem Auge dieselbe sind (#ff8fb4 und #fe8ebe).
Acht solche Paare gab es. Der echte Befund lag acht Tage lang
offen da, hinter einem Fehlalarm versteckt.
Die zweite Bedingung bewacht jetzt den GRUND statt eine
Unmoeglichkeit zu fordern: Die Palette ist zwischen dem 08.09. und
dem 17.09. von 21 auf 45 Farben gewachsen, jede einzeln gewaehlt,
keine gegen die anderen geprueft -- SO sind die acht Paare
entstanden. Ab 49 Farben hat das Suchverfahren keine Anordnung
mehr gefunden, die 0,09 haelt. Das ist kein Verbot, sondern ein
Halt: Dann wird gerechnet und entschieden, statt still weiter
Farben danebenzulegen. */
const GRENZE = 0.09;
const HOECHSTENS = 48; // darueber ist GRENZE nicht mehr erreichbar
const HOECHSTENS = 48; // ab 49 fand die Suche nichts mehr, das GRENZE haelt
ok(paare[0].d >= GRENZE,
`kleinster Abstand ${paare[0].d.toFixed(4)} (Grenze ${GRENZE}, `
+ `Decke fuer ${liste.length} Farben rund 0,097) `
`kleinster Abstand ${paare[0].d.toFixed(4)} (Grenze ${GRENZE}; `
+ `am weitesten gefunden wurden rund 0,097 fuer ${liste.length} Farben) `
+ `-- ${paare[0].a.hex}/${paare[0].b.hex}`);
ok(liste.length <= HOECHSTENS,
liste.length <= HOECHSTENS
? `${liste.length} Farben -- bis ${HOECHSTENS} ist ${GRENZE} nachweislich erreichbar`
: `${liste.length} Farben: ueber ${HOECHSTENS} ist ein Mindestabstand von `
+ `${GRENZE} rechnerisch nicht mehr moeglich. Entweder weniger `
+ `Kacheln mit eigener Farbe, oder die Grenze neu verhandeln.`);
? `${liste.length} Farben -- bis ${HOECHSTENS} ist ${GRENZE} gefunden worden`
: `${liste.length} Farben: fuer so viele hat tools/kachelton-decke.mjs `
+ `keine Anordnung mit ${GRENZE} mehr gefunden. Erst rechnen, dann `
+ `entscheiden -- weniger eigene Farben, oder die Grenze neu ansehen.`);
/* GEGENPROBE. Eine Pruefung, die immer bestaetigt, bestaetigt nichts:
Zwei Farben, die sich um einen einzigen Bildpunktwert