diff --git a/pruef-buehne.png b/pruef-buehne.png index 2f3b0e61..aba4b4af 100644 Binary files a/pruef-buehne.png and b/pruef-buehne.png differ diff --git a/pruef-start-computer.png b/pruef-start-computer.png index 423a1154..c7f5b27d 100644 Binary files a/pruef-start-computer.png and b/pruef-start-computer.png differ diff --git a/pruef-start-creator.png b/pruef-start-creator.png index c4fd0ce1..0ec84552 100644 Binary files a/pruef-start-creator.png and b/pruef-start-creator.png differ diff --git a/pruef-start-handy.png b/pruef-start-handy.png index b4650f76..085f429b 100644 Binary files a/pruef-start-handy.png and b/pruef-start-handy.png differ diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 8ff0532c..f38bc196 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -98,6 +98,19 @@ const lesen = (seite) => seite.evaluate(() => ({ hinweise: [...document.querySelectorAll("#dran .dran__punkt")] .filter((z) => z.offsetParent !== null).length, hinweiseGesamt: document.querySelectorAll("#dran .dran__punkt").length, + /* Jede Hinweiszeile traegt Zeichen und Farbe der Kachel, zu der sie + fuehrt. Geprueft wird beides an den echten Bildpunkten, nicht an der + Absicht im Quelltext. */ + dranZeilen: [...document.querySelectorAll("#dran .dran__punkt")].map((z) => ({ + ton: z.dataset.ton || null, + stufe: z.dataset.stufe, + pfade: z.querySelectorAll(".dran__svg path").length, + strich: getComputedStyle(z.querySelector(".dran__svg")).stroke, + zahl: z.querySelector(".dran__zahl")?.textContent || null, + zahlFarbe: getComputedStyle(z.querySelector(".dran__zahl")).color, + text: z.querySelector(".dran__text")?.textContent || "", + label: z.querySelector(".dran__link")?.getAttribute("aria-label") || "", + })), zeichenGroesse: Math.round(document.querySelector(".kachel__zeichen")?.getBoundingClientRect().width || 0), wasserzeichen: document.querySelectorAll(".kachel__wasserzeichen").length, wasserzeichenDeckung: getComputedStyle(document.querySelector(".kachel__wasserzeichen") || document.body).opacity, @@ -177,6 +190,44 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { ok(r.abgeschnitten.length === 0, `kein Text wird abgeschnitten (${r.abgeschnitten.join(" | ") || "keiner"})`); ok(r.hinweise <= 4, `höchstens vier Hinweise sichtbar (${r.hinweise} von ${r.hinweiseGesamt})`); + + /* --- Die Hinweiszeilen ------------------------------------------------ + Eine Zeile ohne Zeichen oder ohne Zahl waere ein halbes Bauteil und + faellt beim Ueberfliegen sofort auf. */ + ok(r.dranZeilen.length > 0, `es liegen Hinweise vor (${r.dranZeilen.length})`); + ok(r.dranZeilen.every((z) => z.pfade > 0), + `jede Hinweiszeile trägt ihr Zeichen (${r.dranZeilen.filter((z) => !z.pfade).length} ohne)`); + ok(r.dranZeilen.every((z) => /^[1-9]\d*$/.test(z.zahl || "")), + `jede Hinweiszeile stellt ihre Zahl voran (${r.dranZeilen.map((z) => z.zahl).join(",")})`); + /* Die Zahl steht vorn -- im Satz darf sie nicht noch einmal stehen. */ + ok(r.dranZeilen.every((z) => !/^\d/.test(z.text)), + `und keine Zahl steht doppelt (${r.dranZeilen.map((z) => z.text).find((t) => /^\d/.test(t)) || "keine"})`); + /* Vorleseprogramme bekommen den ganzen Satz, nicht "2" und "Aufgaben sind + überfällig" als zwei Bruchstuecke. */ + ok(r.dranZeilen.every((z) => z.label.includes(z.zahl) && z.label.length > 10), + "und der ganze Satz steht für Vorleseprogramme dran"); + /* Farbe folgt dem Bereich, nicht dem Bauteil: Der Ton der Zeile muss + der Ton einer wirklich vorhandenen Kachel sein. */ + const kachelToene = new Set(r.kacheln.map((k) => k.ton)); + ok(r.dranZeilen.every((z) => z.ton && kachelToene.has(z.ton)), + `jede Zeile trägt den Farbton ihrer Kachel (${r.dranZeilen.map((z) => z.ton).join(",")})`); + /* Und zwar wirklich DIESELBE Farbe, an echten Bildpunkten gemessen -- + "hat einen Ton" reicht nicht, sonst faellt nicht auf, wenn alle + Zeilen denselben tragen. */ + const kachelFarbe = new Map(r.kacheln.map((k) => [k.ton, k.farbe])); + const falsch = r.dranZeilen.filter((z) => z.stufe !== "warnung") + .filter((z) => z.zahlFarbe !== kachelFarbe.get(z.ton)); + ok(falsch.length === 0, + `und die Farbe ist die der Kachel (${falsch.map((z) => `${z.ton}: ${z.zahlFarbe} statt ${kachelFarbe.get(z.ton)}`).join(" | ") || "alle"})`); + /* Ueberfaellig schlaegt Bereichsfarbe -- dort muss Rot stehen, sonst + geht die Dringlichkeit in siebzehn Farben unter. */ + const warnZeilen = r.dranZeilen.filter((z) => z.stufe === "warnung"); + ok(warnZeilen.length > 0 && warnZeilen.every((z) => z.zahlFarbe === "rgb(255, 154, 162)"), + `überfällige Zeilen sind rot (${warnZeilen.length} Stück, ${warnZeilen[0]?.zahlFarbe})`); + /* Und die uebrigen eben NICHT rot -- sonst waere die Regel wertlos. */ + ok(r.dranZeilen.filter((z) => z.stufe !== "warnung") + .every((z) => z.zahlFarbe !== "rgb(255, 154, 162)"), + "und nur die"); ok(r.zahlen === 6, `die Aufgabenzahlen stehen (${r.zahlen})`); ok(!r.zahlenLeer, "und nicht der Leer-Satz"); ok(r.ueber <= 0, `nichts steht über (${r.ueber}px, ${r.ueberstehend.join(", ")})`); @@ -217,6 +268,29 @@ console.log("\n=== Scout"); await kontext.close(); } +/* Der Satz "Nichts offen" ist ein Kasten mit display:flex geworden. Damit + ist er staerker als das hidden des Browsers -- ohne eine eigene Regel + stuende er IMMER da, auch wenn Arbeit offen ist. Genau das wird hier + gemessen, in beide Richtungen. */ +console.log("\n=== Der Satz 'Nichts offen' erscheint nur, wenn nichts offen ist"); +{ + const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } }); + const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001"); + const r = await seite.evaluate(() => { + const k = document.getElementById("zahlen-leer"); + const zu = getComputedStyle(k).display; + k.hidden = false; + const auf = getComputedStyle(k).display; + const haken = k.querySelectorAll("svg path").length; + k.hidden = true; + return { zu, auf, haken }; + }); + ok(r.zu === "none", `versteckt heisst versteckt (${r.zu})`); + ok(r.auf !== "none", `und sichtbar heisst sichtbar (${r.auf})`); + ok(r.haken > 0, `der Haken ist da (${r.haken})`); + await kontext.close(); +} + console.log("\n=== Weniger Bewegung: keine Animation"); { const kontext = await browser.newContext({ diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 519a7b3c..53228a9e 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -346,27 +346,38 @@ body.start > main { position: relative; z-index: 1; } genommen -- LIVE rot, Technik gelb, Reports grün, Personen rot-gold. Geprüft gegen genau diesen Hintergrund: Helligkeitsband, Farbstärke - und Kontrast bestehen für alle siebzehn. */ -.kachel[data-ton="1"] { --ton: #3e95e7; } /* 250° */ -.kachel[data-ton="2"] { --ton: #7089ea; } /* 271° */ -.kachel[data-ton="3"] { --ton: #947de2; } /* 292° */ -.kachel[data-ton="4"] { --ton: #b173d0; } /* 314° */ -.kachel[data-ton="5"] { --ton: #c66ab5; } /* 335° */ -.kachel[data-ton="6"] { --ton: #d56694; } /* 356° */ -.kachel[data-ton="7"] { --ton: #dc666f; } /* 17° */ -.kachel[data-ton="8"] { --ton: #db6c48; } /* 38° */ -.kachel[data-ton="9"] { --ton: #d17717; } /* 59° */ -.kachel[data-ton="10"] { --ton: #ba8610; } /* 81° */ -.kachel[data-ton="11"] { --ton: #a29205; } /* 102° */ -.kachel[data-ton="12"] { --ton: #809e23; } /* 123° */ -.kachel[data-ton="13"] { --ton: #50a752; } /* 144° */ -.kachel[data-ton="14"] { --ton: #07aa7c; } /* 165° */ -.kachel[data-ton="15"] { --ton: #12a69b; } /* 186° */ -.kachel[data-ton="16"] { --ton: #04a3b4; } /* 208° */ -.kachel[data-ton="17"] { --ton: #059ece; } /* 229° */ + und Kontrast bestehen für alle siebzehn. + + Die Töne hängen bewusst NICHT an .kachel, sondern nur am Merkmal + data-ton: Dieselbe Nummer färbt die Kachel und die Hinweiszeile, die + dorthin führt. Eine Farbe gehört hier zu einem Bereich, nicht zu einem + Bauteil. */ +[data-ton="1"] { --ton: #3e95e7; } /* 250° */ +[data-ton="2"] { --ton: #7089ea; } /* 271° */ +[data-ton="3"] { --ton: #947de2; } /* 292° */ +[data-ton="4"] { --ton: #b173d0; } /* 314° */ +[data-ton="5"] { --ton: #c66ab5; } /* 335° */ +[data-ton="6"] { --ton: #d56694; } /* 356° */ +[data-ton="7"] { --ton: #dc666f; } /* 17° */ +[data-ton="8"] { --ton: #db6c48; } /* 38° */ +[data-ton="9"] { --ton: #d17717; } /* 59° */ +[data-ton="10"] { --ton: #ba8610; } /* 81° */ +[data-ton="11"] { --ton: #a29205; } /* 102° */ +[data-ton="12"] { --ton: #809e23; } /* 123° */ +[data-ton="13"] { --ton: #50a752; } /* 144° */ +[data-ton="14"] { --ton: #07aa7c; } /* 165° */ +[data-ton="15"] { --ton: #12a69b; } /* 186° */ +[data-ton="16"] { --ton: #04a3b4; } /* 208° */ +[data-ton="17"] { --ton: #059ece; } /* 229° */ + +/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat. + Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der + Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne + das Kniff hätte diese Zeile (eine Klasse, weiter unten in der Datei) + alle siebzehn Töne überstimmt und jede Kachel wäre blau gewesen. */ +:where(.kachel, .dran__punkt) { --ton: var(--akzent); } .kachel { - --ton: var(--akzent); /* Wo der Zeiger steht. Wird von start.js gesetzt; die Mitte ist der Rückfallwert, damit ohne Zeiger nichts springt. */ --mx: 50%; @@ -568,11 +579,35 @@ body.start > main { position: relative; z-index: 1; } } } +/* Der leere Zustand ist kein Fehler, sondern das Ziel -- deshalb ein + ruhiges Gruen statt gestrichelter Leere. Gedeckt gehalten, kein + Signalgruen: Es soll freuen, nicht blenden. */ .zahlen-leer { - margin: 0; padding: 15px 17px; - font-size: .88rem; color: var(--text-still); - border: 1px dashed var(--rand); border-radius: var(--radius-klein); + display: flex; align-items: center; gap: 13px; + margin: 0; padding: 14px 17px; + font-size: .9rem; color: var(--text); + border: 1px solid rgba(80, 167, 130, .28); + border-radius: var(--radius-klein); + background: linear-gradient(90deg, rgba(80, 167, 130, .1), transparent 60%); } +.zahlen-leer__haken { + flex: none; display: grid; place-items: center; + width: 30px; height: 30px; border-radius: 50%; + border: 1px solid rgba(80, 167, 130, .38); + background: rgba(80, 167, 130, .14); +} +.zahlen-leer__haken svg { + width: 16px; height: 16px; + fill: none; stroke: #6fbd95; stroke-width: 2; + stroke-linecap: round; stroke-linejoin: round; +} +/* Pflicht, sobald hier ein display steht: Das display der Klasse ist + staerker als das hidden des Browsers -- ohne diese Zeile stuende der + Satz IMMER da, auch wenn Aufgaben offen sind. */ +.zahlen-leer[hidden] { display: none; } +.zahlen-leer__still { color: var(--text-leise); } +.zahlen-leer strong { font-weight: 650; } +.zahlen-leer strong::after { content: " "; } @media (max-width: 900px) { .kacheln { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -629,49 +664,109 @@ body.start > main { position: relative; z-index: 1; } .willkommen, .dran__liste, .inhalt .fuss { max-width: 940px; } .dran { margin-bottom: 30px; } -.dran__liste { display: grid; gap: 7px; margin: 0; padding: 0; list-style: none; } +.dran__liste { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } +/* Jede Zeile traegt Zeichen und Farbe ihrer Kachel (data-ton kommt aus + start.js). Ohne Zuordnung bleibt der Hausakzent -- eine Zeile ohne + Farbe gibt es nicht. */ .dran__punkt { + /* --ton kommt aus data-ton bzw. dem Rückfallwert in :where() oben. */ position: relative; + overflow: hidden; border: 1px solid var(--rand); border-radius: var(--radius-klein); - background: rgba(255, 255, 255, .028); - transition: border-color .18s ease, background .18s ease; + background: + linear-gradient(90deg, color-mix(in srgb, var(--ton) 9%, transparent), transparent 46%), + rgba(255, 255, 255, .028); + transition: border-color .18s ease, background .18s ease, transform .18s ease; } +/* Der Streifen links ist die Farbe der Kachel, zu der es geht -- oben + kraeftig, unten auslaufend, damit er nicht wie ein Balken wirkt. */ .dran__punkt::before { - content: ""; position: absolute; left: 0; top: 9px; bottom: 9px; width: 2px; - border-radius: 0 2px 2px 0; - background: var(--rand); + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; + background: linear-gradient(180deg, + var(--ton), + color-mix(in srgb, var(--ton) 35%, transparent)); } + +/* Ueberfaellig schlaegt Bereichsfarbe: Wenn etwas brennt, zaehlt zuerst + DASS es brennt. Das Zeichen sagt weiterhin, wo. */ .dran__punkt[data-stufe="warnung"] { + --ton: var(--warn); border-color: rgba(255, 154, 162, .3); - background: rgba(255, 154, 162, .055); } -.dran__punkt[data-stufe="warnung"]::before { background: var(--warn); } -.dran__punkt[data-stufe="offen"]::before { background: var(--akzent); opacity: .7; } -.dran__punkt[data-stufe="ruhig"] { opacity: .78; } -.dran__punkt:hover { border-color: rgba(255, 255, 255, .2); background: rgba(255, 255, 255, .05); } +.dran__punkt[data-stufe="ruhig"] { opacity: .8; } +.dran__punkt:hover { + border-color: color-mix(in srgb, var(--ton) 42%, var(--rand)); + background: + linear-gradient(90deg, color-mix(in srgb, var(--ton) 15%, transparent), transparent 52%), + rgba(255, 255, 255, .05); +} .dran__punkt[data-stufe="ruhig"]:hover { opacity: 1; } .dran__link { - display: flex; align-items: center; justify-content: space-between; gap: 14px; - padding: 12px 15px 12px 18px; + display: grid; + grid-template-columns: auto auto 1fr auto; + align-items: center; gap: 13px; + padding: 11px 15px 11px 17px; color: var(--text); text-decoration: none; font-size: .9rem; } -.dran__punkt[data-stufe="warnung"] .dran__text { font-weight: 600; } +.dran__zeichen { + display: grid; place-items: center; + width: 30px; height: 30px; border-radius: 9px; + border: 1px solid color-mix(in srgb, var(--ton) 30%, transparent); + background: color-mix(in srgb, var(--ton) 13%, transparent); + transition: transform .18s ease, background .18s ease; +} +.dran__svg { + width: 16px; height: 16px; + fill: none; stroke: var(--ton); stroke-width: 1.7; + stroke-linecap: round; stroke-linejoin: round; +} +.dran__link:hover .dran__zeichen { + transform: scale(1.07); + background: color-mix(in srgb, var(--ton) 20%, transparent); +} +/* Die Zahl steht vorn und in gleicher Zeichenbreite: Beim Ueberfliegen + von sieben Zeilen liest man sie zuerst, und sie sollen untereinander + stehen, nicht huepfen. */ +.dran__zahl { + font-size: 1.15rem; font-weight: 700; line-height: 1; + font-variant-numeric: tabular-nums; + color: var(--ton); + min-width: 1.6ch; text-align: right; +} +.dran__text { color: var(--text-leise); } +.dran__punkt[data-stufe="warnung"] .dran__text { color: var(--text); font-weight: 600; } .dran__pfeil { color: var(--text-still); font-size: .95rem; transition: transform .18s ease, color .18s ease; } -.dran__link:hover .dran__pfeil { color: var(--akzent); transform: translateX(3px); } +.dran__link:hover .dran__pfeil { color: var(--ton); transform: translateX(3px); } + +/* Auf dem Handy ist Platz das knappste Gut: Zeichen und Pfeil weichen, + Zahl und Satz bleiben -- die tragen die Aussage. */ +@media (max-width: 460px) { + .dran__link { grid-template-columns: auto 1fr; gap: 10px; padding: 11px 13px 11px 15px; } + .dran__zeichen, .dran__pfeil { display: none; } +} @media (prefers-reduced-motion: reduce) { .kachel__wasserzeichen, .kachel__zeichen { transition: none; } .kachel:hover .kachel__wasserzeichen { transform: rotate(-8deg); } .kachel:hover .kachel__zeichen { transform: none; } - .dran__punkt, .dran__pfeil { transition: none; } + .dran__punkt, .dran__pfeil, .dran__zeichen { transition: none; } .dran__link:hover .dran__pfeil { transform: none; } + .dran__link:hover .dran__zeichen { transform: none; } +} + +/* Ohne Farbverlaeufe (Kontrastmodus, Druck) bleibt der Streifen als + einfarbige Kante -- sonst waere die Zuordnung dort ganz weg. */ +@media (forced-colors: active) { + .dran__punkt::before { background: CanvasText; } + .dran__svg { stroke: CanvasText; } + .dran__zahl { color: CanvasText; } } /* ===================================================================== diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 1cfa567d..0e652e9c 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -169,6 +169,18 @@ ohne_profil: 'Creator-Profile', }; + /* Findet einen Bereich über seinen Namen -- damit Hinweis und Kachel + dasselbe Zeichen und denselben Farbton benutzen, ohne dass die + Zuordnung ein zweites Mal irgendwo stünde. */ + const bereichZu = (name) => { + if (!name) return null; + for (const g of GRUPPEN) { + const b = g.bereiche.find((x) => x.name === name); + if (b) return b; + } + return null; + }; + const $ = (id) => document.getElementById(id); const el = (tag, klasse, text) => { const k = document.createElement(tag); @@ -177,11 +189,11 @@ return k; }; - function zeichenBauen(name) { + function zeichenBauen(name, klasse = 'kachel__svg') { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true'); - svg.setAttribute('class', 'kachel__svg'); + svg.setAttribute('class', klasse); for (const d of (ZEICHEN[name] || [])) { const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p.setAttribute('d', d); @@ -392,15 +404,40 @@ const li = el('li', 'dran__punkt'); li.dataset.stufe = h.stufe; - /* Jeder Hinweis fuehrt irgendwohin -- ein Hinweis ohne Ziel ist + /* Ein Hinweis trägt ZEICHEN UND FARBE der Kachel, zu der er führt. + Dadurch gehört sichtbar zusammen, was zusammengehört: Wer die + rote Zeile „2 Aufgaben sind überfällig" sieht, erkennt weiter + unten dieselbe Farbe und dasselbe Zeichen auf der Kachel + wieder -- und weiß, wohin er muss, bevor er den Text zu Ende + gelesen hat. Beides kommt aus derselben Zuordnung wie die Zahl + auf der Kachel, es gibt also keine zweite Liste. */ + const kachel = bereichZu(HINWEIS_ZU_KACHEL[h.art]); + if (kachel) li.dataset.ton = String(kachel.ton); + + /* Jeder Hinweis führt irgendwohin -- ein Hinweis ohne Ziel ist nur ein schlechtes Gewissen. */ const a = el('a', 'dran__link'); a.href = h.ziel; - a.append(el('span', 'dran__text', - (EINZAHL[h.art] || ((n) => `${n} × ${h.text}`))(h.anzahl))); + + if (kachel) { + const zeichen = el('span', 'dran__zeichen'); + zeichen.append(zeichenBauen(kachel.zeichen, 'dran__svg')); + a.append(zeichen); + } + + /* Die Zahl steht vorn und getrennt vom Satz. Man liest sie zuerst, + und beim Überfliegen von sieben Zeilen zählt genau das. */ + a.append(el('span', 'dran__zahl', String(h.anzahl))); + + const satz = (EINZAHL[h.art] || ((n) => `${n} × ${h.text}`))(h.anzahl); + /* Die Zahl steht schon davor -- im Satz wäre sie doppelt. */ + a.append(el('span', 'dran__text', satz.replace(/^\d+\s+(×\s+)?/, ''))); + const pfeil = el('span', 'dran__pfeil', '→'); pfeil.setAttribute('aria-hidden', 'true'); a.append(pfeil); + /* Vorleseprogramme bekommen den ganzen Satz, nicht die Bruchstücke. */ + a.setAttribute('aria-label', satz); li.append(a); liste.append(li); } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 4be68bc9..6e60d8a1 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + +
@@ -139,8 +139,8 @@ - - - + + +