diff --git a/server/pruef-schritt.mjs b/server/pruef-schritt.mjs index f3c4deb6..daf00a8b 100644 --- a/server/pruef-schritt.mjs +++ b/server/pruef-schritt.mjs @@ -341,6 +341,65 @@ melde("=== 8. Im Browser ==="); await seite.click('.e-person:has-text("Frida")'); await seite.waitForTimeout(900); + /* ===== DIE KATEGORIEN KLAPPEN AUF UND ZU (15.09.2026) ============== + + Filipe: „ich will auch dass man die kategorien von den fragen, also + die liste auf und zu drücken kann mit einem button." + + Geprüft wird nicht, dass es einen Knopf GIBT -- sondern dass er + etwas tut, und dass zugeklappt trotzdem dasteht, was drin ist. + ================================================================ */ + const bloecke = await seite.evaluate(() => { + const b = [...document.querySelectorAll(".e-block")]; + return { + anzahl: b.length, + offen: b.filter((x) => x.dataset.offen === "ja").length, + schalter: document.querySelectorAll(".e-block__schalter").length, + staende: [...document.querySelectorAll(".e-block__stand")].map((x) => x.textContent.trim()), + hakt: [...document.querySelectorAll(".e-block__hakt")].map((x) => x.textContent.trim()), + versteckt: b.filter((x) => x.querySelector(".e-block__koerper")?.hidden).length, + alleKnopf: document.querySelector(".e-bloecke__alle")?.textContent.trim() || "", + }; + }); + ok(bloecke.anzahl === 4, `${bloecke.anzahl} Kategorien stehen auf der Karte`); + ok(bloecke.schalter === bloecke.anzahl, "jede hat einen Knopf zum Auf- und Zuklappen"); + /* GENAU EINE STEHT OFFEN -- die erste, in der noch etwas fehlt. Alle + zu hieße jedes Mal suchen, alle auf wäre der Zustand von vorher. */ + ok(bloecke.offen === 1, `genau eine steht offen (${bloecke.offen})`); + ok(bloecke.versteckt === bloecke.anzahl - 1, + `die anderen ${bloecke.versteckt} sind wirklich zugeklappt`); + /* ZUGEKLAPPT MUSS SIE TROTZDEM ETWAS SAGEN. */ + ok(bloecke.staende.length === bloecke.anzahl + && bloecke.staende.every((x) => x.includes(' / ')), + `jeder Kopf sagt, wie weit er ist (${bloecke.staende.join(", ")})`); + ok(bloecke.hakt.some((x) => /hakt/.test(x)), + `und wo etwas hakt, steht es im Kopf (${bloecke.hakt.join(", ") || "nichts"})`); + ok(/Alle aufklappen/.test(bloecke.alleKnopf), + `daneben einer für alle auf einmal („${bloecke.alleKnopf}“)`); + + /* UND JETZT WIRKLICH DRÜCKEN. Ein Knopf, den niemand betätigt hat, + ist kein geprüfter Knopf. */ + await seite.click(".e-block:not([data-offen='ja']) .e-block__schalter"); + await seite.waitForTimeout(250); + const nachKlick = await seite.evaluate(() => ({ + offen: document.querySelectorAll('.e-block[data-offen="ja"]').length, + ausgeklappt: [...document.querySelectorAll(".e-block__schalter")] + .filter((x) => x.getAttribute("aria-expanded") === "true").length, + })); + ok(nachKlick.offen === 2, `nach einem Klick stehen zwei offen (${nachKlick.offen})`); + ok(nachKlick.ausgeklappt === 2, + "und ein Vorleseprogramm erfährt dasselbe (aria-expanded)"); + + await seite.click(".e-bloecke__alle"); + await seite.waitForTimeout(400); + const alle = await seite.evaluate(() => ({ + offen: document.querySelectorAll('.e-block[data-offen="ja"]').length, + knopf: document.querySelector(".e-bloecke__alle")?.textContent.trim() || "", + })); + ok(alle.offen === 4, `„Alle aufklappen“ öffnet alle vier (${alle.offen})`); + ok(/Alle zuklappen/.test(alle.knopf), + `und der Knopf heißt danach anders („${alle.knopf}“)`); + const stand = await seite.evaluate(() => ({ punkte: document.querySelectorAll(".e-punkt").length, laeuft: document.querySelectorAll('.e-schritt[data-lage="laeuft"]').length, diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 0f00e6f8..452ab53b 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151430", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609151438", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151430", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609151438", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151430", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151438", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151430", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151438", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index d52672d0..df2c6293 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -90,10 +90,26 @@ /* --------------------------------------------------------------------- Die Karte ------------------------------------------------------------------- */ -.e-karte { margin: 26px 0 0; } +/* DIE KARTE BEKOMMT EINE EIGENE FLÄCHE (15.09.2026). + + Bis heute hatte sie keine: Name, Rolle und der Weg zum Notizbrett + standen direkt auf der Bühne — also auf einem Foto. Im Bildschirmfoto + lag „Frida · Modi · 2 von 28 angesehen" quer über dem Spicy-Media- + Schriftzug, und der Knopf darunter über einer Chilischote. + + Das ist genau der Fall, für den die Kontrastmessung ihren dritten + Ausgang hat („konnte nicht nachsehen"): Über einem Foto lässt sich + Lesbarkeit nicht ausrechnen, weil das Dahinter von Bild zu Bild + wechselt. Die Antwort darauf ist keine hellere Schrift, sondern eine + deckende Fläche. */ +.e-karte { + margin: 26px 0 0; padding: 18px 20px; + background: color-mix(in srgb, var(--tinte) 92%, #ffffff 8%); + border: 1px solid var(--rand); +} .e-karte__kopf { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; - padding-bottom: 10px; margin-bottom: 16px; + padding-bottom: 12px; margin-bottom: 16px; border-bottom: 1px solid var(--rand-hell); } .e-karte__name { margin: 0; font-size: 1.22rem; font-weight: 660; color: var(--text); } @@ -119,7 +135,87 @@ /* --------------------------------------------------------------------- Ein Block und seine Punkte ------------------------------------------------------------------- */ -.e-block { margin: 0 0 26px; } +/* ===================================================================== + DIE KATEGORIEN LASSEN SICH AUF- UND ZUKLAPPEN (15.09.2026) + + Filipe: „ich will auch dass man die kategorien von den fragen, also + die liste auf und zu drücken kann mit einem button." + + Achtundzwanzig Punkte offen sind rund zwei Meter Seite. Man scrollt, + verliert die Stelle und macht es beim nächsten Mal gar nicht mehr. + + DER KOPF MUSS ZUGEKLAPPT ETWAS SAGEN, sonst klappt man einmal auf und + danach nie wieder zu. Er trägt deshalb drei Dinge: den Namen, wie + weit die Kategorie ist (3 / 7) und ob darin etwas hakt. + ===================================================================== */ +.e-bloecke__leiste { + display: flex; flex-wrap: wrap; align-items: center; gap: 12px; + margin: 0 0 14px; +} +.e-bloecke__alle { + font: inherit; font-size: .81rem; font-weight: 600; + padding: 7px 14px; border-radius: 999px; cursor: pointer; + color: var(--text-leise); + background: transparent; + border: 1px solid var(--rand-hell); + transition: background .16s ease, color .16s ease; +} +.e-bloecke__alle:hover { color: var(--text); background: rgba(255, 255, 255, .05); } +.e-bloecke__alle:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } + +.e-block { + margin: 0 0 10px; + border: 1px solid var(--rand); + background: color-mix(in srgb, var(--tinte) 96%, #ffffff 4%); +} +/* Offen bekommt der Kasten etwas mehr Gewicht -- man soll auf einen + Blick sehen, welcher gerade aufgeschlagen ist. */ +.e-block[data-offen="ja"] { border-color: var(--rand-hell); } + +.e-block__zeile { margin: 0; } +.e-block__schalter { + display: flex; align-items: center; gap: 11px; + width: 100%; text-align: left; + font: inherit; + padding: 13px 15px; cursor: pointer; + background: transparent; border: 0; + color: var(--text); +} +.e-block__schalter:hover { background: rgba(255, 255, 255, .04); } +.e-block__schalter:focus-visible { outline: 2px solid var(--akzent); outline-offset: -2px; } + +.e-block__pfeil { + flex: 0 0 auto; width: 18px; height: 18px; + fill: none; stroke: var(--text-still); stroke-width: 2; + stroke-linecap: round; stroke-linejoin: round; + transition: transform .18s ease; +} +.e-block[data-offen="ja"] .e-block__pfeil { transform: rotate(90deg); stroke: var(--text-leise); } + +.e-block__name { + flex: 1 1 auto; font-size: 1rem; font-weight: 650; color: var(--text); +} +/* Der Stand. Zahlen in Tabellenbreite, damit 3 / 7 und 10 / 12 + untereinander nicht wackeln. */ +.e-block__stand { + flex: 0 0 auto; font-size: .82rem; color: var(--text-still); + font-variant-numeric: tabular-nums; + padding: 2px 9px; border-radius: 999px; border: 1px solid var(--rand); +} +.e-block__stand[data-fertig="ja"] { + color: #7fb08a; border-color: color-mix(in srgb, #668e6e 50%, transparent); +} +/* „2 × hakt" -- die einzige Stelle im Kopf, die eine Farbe bekommt. + Sie ist auch der einzige Grund, eine zugeklappte Kategorie zu + oeffnen, ohne dass man sie durchgehen will. */ +.e-block__hakt { + flex: 0 0 auto; display: inline-flex; align-items: baseline; gap: 4px; + font-size: .78rem; color: #d8a76a; + font-variant-numeric: tabular-nums; +} +.e-block__hakt-wort { font-size: .78rem; } + +.e-block__koerper { padding: 0 15px 13px; } .e-block__titel { margin: 0 0 4px; font-size: 1.02rem; font-weight: 650; color: var(--text); } .e-block__satz { margin: 0 0 12px; max-width: 64ch; @@ -204,8 +300,22 @@ color: var(--text); background: rgba(0, 0, 0, .28); border: 1px solid var(--rand); + transition: border-color .16s ease, background .16s ease; } .e-punkt__anlass::placeholder { color: var(--text-still); } +/* ES BLEIBT SICHTBAR, AUCH WENN ES NICHT GEBRAUCHT WIRD -- ein Feld, + das erst aufklappt, kostet einen zweiten Blick, und genau an der + Stelle hört man auf zu schreiben. Aber es tritt zurück. */ +.e-punkt__anlass[data-noetig="nein"] { + background: rgba(0, 0, 0, .18); + border-color: color-mix(in srgb, var(--rand) 70%, transparent); +} +/* Und es meldet sich, sobald es gebraucht wird. */ +.e-punkt__anlass[data-noetig="ja"] { + border-color: color-mix(in srgb, #cc9451 55%, transparent); + background: rgba(0, 0, 0, .3); +} +.e-punkt__anlass[data-noetig="ja"]::placeholder { color: #c99a62; } .e-punkt__andere, .e-punkt__verdeckt, .e-punkt__wann { margin: 8px 0 0; font-size: .78rem; line-height: 1.5; color: var(--text-still); @@ -355,7 +465,12 @@ .t-fuss a { color: var(--text-leise); } @media (prefers-reduced-motion: reduce) { - .e-person, .e-wahl__knopf, .t-merkmal { transition: none; } + .e-person, .e-wahl__knopf, .t-merkmal, + .e-bloecke__alle, .e-punkt__anlass { transition: none; } + /* Der Pfeil DREHT SICH weiterhin -- er ist keine Verzierung, + sondern die Auskunft "offen oder zu". Nur der Weg dorthin + faellt weg. */ + .e-block__pfeil { transition: none; } } @media (max-width: 620px) { diff --git a/workspace/assets/js/entwicklung.js b/workspace/assets/js/entwicklung.js index df9632f6..8f98464b 100644 --- a/workspace/assets/js/entwicklung.js +++ b/workspace/assets/js/entwicklung.js @@ -40,6 +40,33 @@ } let stufen = []; + + /* WELCHE KATEGORIEN OFFEN STEHEN (15.09.2026). + + Filipe: „ich will auch dass man die kategorien von den fragen, also + die liste auf und zu drücken kann mit einem button." + + Der Zustand liegt hier und nicht an den Elementen: Die Karte wird + nach dem ersten Klick auf einen Punkt neu gezeichnet (erst danach + darf der Stand der anderen ueberhaupt ausgeliefert werden). Waere + er an den Elementen, klappte dabei alles wieder zu -- mitten in der + Arbeit, bei jedem ersten Klick. */ + let offeneBloecke = null; + let karteFuer = null; + + /* Der Pfeil des Aufklappers. Als Zeichnung und nicht als Textzeichen: + „▸" sieht auf jedem Geraet anders aus und faellt bei manchen + Schriften ganz aus. */ + const pfeil = () => { + const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + s.setAttribute('class', 'e-block__pfeil'); + s.setAttribute('viewBox', '0 0 24 24'); + s.setAttribute('aria-hidden', 'true'); + const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + p.setAttribute('d', 'M9 6l6 6-6 6'); + s.append(p); + return s; + }; let offen = null; // die gerade geöffnete Person /* ---- Die vier Knöpfe ------------------------------------------------ @@ -160,12 +187,99 @@ ziel.append(box); } - /* ---- Die Blöcke ---------------------------------------------------- */ + /* ---- Die Blöcke, auf- und zuklappbar -------------------------------- + + ACHTUNDZWANZIG PUNKTE SIND ZU VIEL FÜR EINEN BILDSCHIRM. Offen + sind es rund zwei Meter Seite; man scrollt, verliert die Stelle + und macht es beim nächsten Mal gar nicht mehr. + + WELCHER ZUERST OFFEN STEHT, IST DIE EIGENTLICHE ENTSCHEIDUNG. + Alle zu hieße: jedes Mal erst suchen. Alle auf wäre der Zustand + von vorher. Offen steht deshalb die erste Kategorie, in der noch + etwas fehlt — man landet dort, wo die Arbeit ist. Ist alles + gesetzt, steht die erste offen. + ================================================================ */ + if (karteFuer !== id) { karteFuer = id; offeneBloecke = null; } + if (offeneBloecke === null) { + offeneBloecke = new Set(); + const ersteOffene = r.bloecke.find((b) => b.punkte.some((pt) => !pt.meins)) + || r.bloecke[0]; + if (ersteOffene) offeneBloecke.add(ersteOffene.schluessel); + } + + const leiste = el('div', 'e-bloecke__leiste'); + const alleZu = () => r.bloecke.every((b) => offeneBloecke.has(b.schluessel)); + const alleKnopf = el('button', 'e-bloecke__alle'); + alleKnopf.type = 'button'; + const alleBeschriften = () => { + alleKnopf.textContent = alleZu() ? 'Alle zuklappen' : 'Alle aufklappen'; + }; + alleBeschriften(); + alleKnopf.addEventListener('click', () => { + if (alleZu()) offeneBloecke.clear(); + else for (const b of r.bloecke) offeneBloecke.add(b.schluessel); + karte(id); + }); + leiste.append(alleKnopf); + /* HIER STAND KURZ „2 von 28 angesehen" -- dieselbe Zahl, die zwei + Zeilen hoeher schon im Kopf der Karte steht. Zweimal dasselbe auf + einem Bildschirm laesst einen ueberlegen, ob es zwei verschiedene + Zahlen sind. Gesehen habe ich das im Bildschirmfoto, nicht im + Code. */ + ziel.append(leiste); + for (const b of r.bloecke) { + const gesetzt = b.punkte.filter((pt) => pt.meins).length; + const offen = offeneBloecke.has(b.schluessel); + const haktHier = b.punkte.filter((pt) => pt.meins?.stufe === 'hakt').length; + const abschnitt = el('section', 'e-block'); - abschnitt.append(el('h3', 'e-block__titel', b.name)); - abschnitt.append(el('p', 'e-block__satz', b.text)); - for (const pt of b.punkte) abschnitt.append(punktZeile(id, pt, r.erwartung)); + abschnitt.dataset.offen = offen ? 'ja' : 'nein'; + if (gesetzt === b.punkte.length) abschnitt.dataset.fertig = 'ja'; + + const koerper = el('div', 'e-block__koerper'); + koerper.id = `block-${b.schluessel}`; + koerper.hidden = !offen; + + const titel = el('h3', 'e-block__zeile'); + const schalter = el('button', 'e-block__schalter'); + schalter.type = 'button'; + schalter.setAttribute('aria-expanded', String(offen)); + schalter.setAttribute('aria-controls', koerper.id); + schalter.append(pfeil()); + schalter.append(el('span', 'e-block__name', b.name)); + /* DER STAND STEHT IM KOPF, damit man ihn ZUGEKLAPPT sieht. Eine + zugeklappte Kategorie, die nicht sagt, wie weit sie ist, klappt + man einmal auf und danach nie wieder zu. */ + const stand = el('span', 'e-block__stand', `${gesetzt} / ${b.punkte.length}`); + if (gesetzt === b.punkte.length) stand.dataset.fertig = 'ja'; + schalter.append(stand); + if (haktHier) { + const h = el('span', 'e-block__hakt', `${haktHier} ×`); + h.title = haktHier === 1 ? 'Hier hakt ein Punkt.' : `Hier haken ${haktHier} Punkte.`; + h.append(el('span', 'e-block__hakt-wort', 'hakt')); + schalter.append(h); + } + schalter.addEventListener('click', () => { + if (offeneBloecke.has(b.schluessel)) offeneBloecke.delete(b.schluessel); + else offeneBloecke.add(b.schluessel); + /* Nur DIESEN Abschnitt umschalten statt die Karte neu zu holen: + Ein Neuaufbau bei jedem Klick ließe die Seite unter den + Fingern springen -- und bei 28 Punkten sieht man das. */ + const jetztOffen = offeneBloecke.has(b.schluessel); + abschnitt.dataset.offen = jetztOffen ? 'ja' : 'nein'; + koerper.hidden = !jetztOffen; + schalter.setAttribute('aria-expanded', String(jetztOffen)); + alleBeschriften(); + }); + titel.append(schalter); + abschnitt.append(titel); + + /* Der erklärende Satz gehört in den Körper, nicht in den Kopf: + Zugeklappt will man die Überschrift lesen, nicht einen Absatz. */ + koerper.append(el('p', 'e-block__satz', b.text)); + for (const pt of b.punkte) koerper.append(punktZeile(id, pt, r.erwartung)); + abschnitt.append(koerper); ziel.append(abschnitt); } } @@ -193,7 +307,18 @@ const feld = el('textarea', 'e-punkt__anlass'); feld.rows = 2; feld.value = pt.meins?.anlass || ''; - feld.placeholder = 'Woran hast du das gemerkt? (bei „Da hakt es" nötig)'; + /* DER HINWEIS STAND BISHER ACHTUNDZWANZIGMAL DA -- auch an Punkten, + an denen nichts hakt. Ein Satz, der überall steht, wird nirgends + gelesen. Jetzt sagt das Feld erst dann, dass es gebraucht wird, + wenn es gebraucht wird. */ + const feldSetzen = (stufe) => { + const noetig = stufe === 'hakt'; + feld.dataset.noetig = noetig ? 'ja' : 'nein'; + feld.placeholder = noetig + ? 'Woran hast du das gemerkt? Ein Satz genügt – ohne ihn wird daraus kein Schritt.' + : 'Woran hast du das gemerkt? (kann leer bleiben)'; + }; + feldSetzen(pt.meins?.stufe || null); z.append(knopfsatz(pt.meins?.stufe || null, async (stufe, knopf) => { const reihe = knopf.parentElement; @@ -201,6 +326,8 @@ b.setAttribute('aria-pressed', String(b === knopf)); } z.dataset.stufe = stufe; + feldSetzen(stufe); + if (stufe === 'hakt' && !feld.value.trim()) feld.focus(); const a = await hole(`/workspace/api/entwicklung/person/${personId}/${pt.schluessel}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index d8056446..94c4fa30 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -282,10 +282,10 @@ - - - - - + + + + +