diff --git a/pruef-checkliste.png b/pruef-checkliste.png index 81b55520..fa2828f9 100644 Binary files a/pruef-checkliste.png and b/pruef-checkliste.png differ diff --git a/pruef-content-computer.png b/pruef-content-computer.png index 4d052261..9fd7b0de 100644 Binary files a/pruef-content-computer.png and b/pruef-content-computer.png differ diff --git a/pruef-content-creator.png b/pruef-content-creator.png index 45b73a75..7c5c3efd 100644 Binary files a/pruef-content-creator.png and b/pruef-content-creator.png differ diff --git a/pruef-content-handy.png b/pruef-content-handy.png index 62fbda17..7ff6f572 100644 Binary files a/pruef-content-handy.png and b/pruef-content-handy.png differ diff --git a/pruef-handy.png b/pruef-handy.png index b7e5166d..65427faa 100644 Binary files a/pruef-handy.png and b/pruef-handy.png differ diff --git a/pruef-merkzeichen.png b/pruef-merkzeichen.png new file mode 100644 index 00000000..093fa10e Binary files /dev/null and b/pruef-merkzeichen.png differ diff --git a/pruef-start-computer.png b/pruef-start-computer.png index 18d331b2..6d60b07f 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 d7c624b2..6d47b4a4 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 76524287..76c433cf 100644 Binary files a/pruef-start-handy.png and b/pruef-start-handy.png differ diff --git a/pruef-uebersicht-computer.png b/pruef-uebersicht-computer.png index f0758489..6abd0dc1 100644 Binary files a/pruef-uebersicht-computer.png and b/pruef-uebersicht-computer.png differ diff --git a/pruef-uebersicht-creator.png b/pruef-uebersicht-creator.png index 11bb93f1..5e9efe61 100644 Binary files a/pruef-uebersicht-creator.png and b/pruef-uebersicht-creator.png differ diff --git a/pruef-uebersicht-handy.png b/pruef-uebersicht-handy.png index 05fb3b87..47535bf6 100644 Binary files a/pruef-uebersicht-handy.png and b/pruef-uebersicht-handy.png differ diff --git a/server/pruef-checkliste.mjs b/server/pruef-checkliste.mjs index 3e9b57ab..9b64337d 100644 --- a/server/pruef-checkliste.mjs +++ b/server/pruef-checkliste.mjs @@ -315,6 +315,149 @@ for (const [rolle, code, name, darf] of [ ok(/Verbessern/i.test(s?.marke || ""), ` mit lesbarer Marke ("${s?.marke}")`); ok(/Mikro/.test(s?.grund || ""), ` und dem Grund ("${s?.grund}")`); await seite.screenshot({ path: "pruef-checkliste.png", fullPage: false }); + + /* ================================================================= + ZUGEKLAPPTE GRUPPEN (Wunsch 01.09.2026) + + Die gefaehrlichste Stelle des ganzen Umbaus. Alle Pruefungen oben + zaehlen ELEMENTE -- und ein zugeklappter Punkt ist im Dokument + weiterhin vorhanden. Sie waeren also alle gruen geblieben, auch + wenn der Creator seine Markierung nie zu Gesicht bekaeme. + + Gemessen wird deshalb SICHTBARKEIT, nicht Vorhandensein. + ================================================================= */ + const zu = await seite.evaluate(() => { + const gruppen = [...document.querySelectorAll(".fest-gruppe")]; + const sichtbar = (e) => { + if (!e) return false; + const r = e.getBoundingClientRect(); + return r.width > 0 && r.height > 0; + }; + return { + gruppen: gruppen.length, + zuGeklappt: gruppen.filter((g) => g.querySelector(".fest-gruppe__koerper").hidden).length, + ariaZu: gruppen.filter((g) => + g.querySelector(".fest-gruppe__kopf").getAttribute("aria-expanded") === "false").length, + /* Kein einziger Punkt darf sichtbar sein, solange alles zu ist. */ + sichtbarePunkte: [...document.querySelectorAll(".fest-punkt")].filter(sichtbar).length, + /* Das Merkzeichen der markierten Gruppe MUSS sichtbar sein -- + sonst ist die Markierung hinter der zugeklappten Gruppe + verschwunden, und der ganze Umbau waere ein Rueckschritt. */ + merkGruppen: gruppen.filter((g) => g.dataset.markiert === "ja").length, + merkSichtbar: [...document.querySelectorAll(".fest-gruppe__kopf .merk")].filter(sichtbar).length, + /* Gegenprobe: Gruppen OHNE Markierung duerfen kein Zeichen haben. + Ein Zeichen an jeder Gruppe zeigt nichts an, es ist Deko. */ + merkOhneGrund: gruppen.filter((g) => + g.dataset.markiert !== "ja" && g.querySelector(".merk")).length, + /* Der Knopf muss mit dem Daumen treffbar sein. */ + kopfHoehe: Math.round( + document.querySelector(".fest-gruppe__kopf").getBoundingClientRect().height), + hoeheZu: document.documentElement.scrollHeight, + }; + }); + ok(zu.gruppen === 3 && zu.zuGeklappt === 3, `alle ${zu.gruppen} Gruppen sind zugeklappt (${zu.zuGeklappt})`); + ok(zu.ariaZu === 3, ` und melden das auch an Vorleseprogramme (aria-expanded=false: ${zu.ariaZu})`); + ok(zu.sichtbarePunkte === 0, ` kein Punkt ist sichtbar (${zu.sichtbarePunkte})`); + ok(zu.merkGruppen === 1, `genau eine Gruppe traegt eine Markierung (${zu.merkGruppen})`); + ok(zu.merkSichtbar === 1, ` und ihr Merkzeichen ist SICHTBAR (${zu.merkSichtbar})`); + ok(zu.merkOhneGrund === 0, ` Gegenprobe: keine Markierung ohne Grund (${zu.merkOhneGrund})`); + ok(zu.kopfHoehe >= 40, ` der Gruppenkopf ist mit dem Daumen treffbar (${zu.kopfHoehe}px)`); + + /* Aufklappen: der Koerper geht auf, die Punkte werden sichtbar, und + die Seite wird dabei messbar laenger. Genau dieser Unterschied ist + der Grund fuer den Umbau -- er gehoert also gemessen und nicht + geglaubt. */ + await seite.click(".fest-gruppe .fest-gruppe__kopf"); + await seite.waitForTimeout(250); + const auf = await seite.evaluate(() => ({ + offen: [...document.querySelectorAll(".fest-punkt")].filter((e) => { + const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; + }).length, + aria: document.querySelector(".fest-gruppe__kopf").getAttribute("aria-expanded"), + hoehe: document.documentElement.scrollHeight, + })); + ok(auf.offen > 0, `ein Klick auf den Kopf zeigt die Punkte (${auf.offen} sichtbar)`); + ok(auf.aria === "true", " aria-expanded steht auf true"); + ok(auf.hoehe > zu.hoeheZu, + ` die Seite ist zugeklappt kuerzer: ${zu.hoeheZu}px zu ${auf.hoehe}px ` + + `(${Math.round((1 - zu.hoeheZu / auf.hoehe) * 100)}% gespart)`); + + /* Der Zustand ueberlebt das Neuladen -- sonst waere jede Bewertung + ein Rueckschlag zum Anfang der Liste. */ + await seite.reload({ waitUntil: "networkidle" }); + await seite.waitForTimeout(1100); + const nach = await seite.evaluate(() => + document.querySelector(".fest-gruppe__kopf")?.getAttribute("aria-expanded")); + ok(nach === "true", ` und ueberlebt das Neuladen (aria-expanded=${nach})`); + + /* ================================================================= + DAS MERKZEICHEN AUF DER KACHEL + + "und es soll auch oben in der Kachel angezeigt werden." + + Der Fallstrick steht im Stil: `.kachel` traegt `overflow: hidden`. + Ein Zeichen, das ueber den Rand ragt, waere im Quelltext da und + auf dem Bildschirm abgeschnitten. Geprueft wird deshalb, dass es + VOLLSTAENDIG innerhalb der Kachel liegt. + ================================================================= */ + await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(1400); + const kach = await seite.evaluate(() => { + const m = document.querySelector(".kachel__merk"); + if (!m) return { da: false }; + const kachel = m.closest(".kachel"); + const a = m.getBoundingClientRect(); + const b = kachel.getBoundingClientRect(); + return { + da: true, + name: kachel.querySelector(".kachel__name")?.textContent, + sichtbar: a.width > 0 && a.height > 0, + /* 0,5 px Toleranz: Browser runden Teilpixel. */ + drin: a.top >= b.top - 0.5 && a.right <= b.right + 0.5 + && a.bottom <= b.bottom + 0.5 && a.left >= b.left - 0.5, + /* Ueberdeckt es etwas anderes auf der Kachel? Zwei Marken + uebereinander sind schlimmer als eine fehlende. + + Geprueft wird gegen ALLES, was rechts steht -- Zahl ODER + Pfeil. Nur gegen die Zahl zu pruefen waere wertlos: Traegt die + Kachel ein Merkzeichen, faellt die Zahl weg, und die Pruefung + waere ohne ein einziges Element gruen. Deshalb zaehlt sie die + geprueften Nachbarn mit -- null Nachbarn ist ein Fehler, + kein Erfolg. */ + nachbarn: (() => { + const andere = [...kachel.querySelectorAll(".kachel__zahl, .kachel__pfeil")]; + let stoss = 0; + for (const e of andere) { + const c = e.getBoundingClientRect(); + if (!(a.right < c.left || a.left > c.right + || a.bottom < c.top || a.top > c.bottom)) stoss++; + } + return { geprueft: andere.length, stoss }; + })(), + beschriftung: m.getAttribute("aria-label") || "", + anzahl: document.querySelectorAll(".kachel__merk").length, + }; + }); + ok(kach.da === true, `die Kachel traegt das Merkzeichen (${kach.name || "-"})`); + ok(kach.sichtbar === true, " es ist sichtbar"); + ok(kach.drin === true, " und liegt vollstaendig INNERHALB der Kachel (nicht abgeschnitten)"); + ok(kach.nachbarn?.geprueft > 0 && kach.nachbarn?.stoss === 0, + ` es ueberdeckt nichts auf der Kachel ` + + `(${kach.nachbarn?.geprueft} Nachbarn geprueft, ${kach.nachbarn?.stoss} Zusammenstoesse)`); + ok(/markiert/i.test(kach.beschriftung), + ` Vorleseprogramme bekommen einen Satz ("${kach.beschriftung}")`); + await seite.screenshot({ path: "pruef-merkzeichen.png", fullPage: false }); + } + + /* Gegenprobe zur Kachel: Wer nichts markiert bekommen hat, darf auch + kein Zeichen sehen. Ein Zeichen, das immer da ist, zeigt nichts an. + Geprueft am Scout -- er hat selbst markiert, aber niemand hat IHM + etwas markiert. */ + if (rolle === "scout") { + await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(1400); + const n = await seite.evaluate(() => document.querySelectorAll(".kachel__merk").length); + ok(n === 0, `Gegenprobe: der Scout sieht kein Merkzeichen auf seinen Kacheln (${n})`); } await kontext.close(); } diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js index 73a66546..8bcbf387 100644 --- a/server/workspace-hinweise.js +++ b/server/workspace-hinweise.js @@ -165,6 +165,40 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => { } } + /* ---------- Markierte Punkte aus den festen Checklisten ---------- + + Wunsch vom 01.09.2026: "so dass die sehen, da ist was -- und es + soll auch oben in der Kachel angezeigt werden." + + NUR FUER CREATOR, und das ist eine bewusste Entscheidung. Fuer + einen Betreuer waere die Zahl eine Liste dessen, was er selbst + angehakt hat: Sie waechst mit seiner Arbeit, und er kann sie nicht + abbauen -- nur der Creator kann. Ein Zaehler, den man nicht auf + null bringen kann, wird nach zwei Wochen ignoriert, und dann sind + auch die daneben nichts mehr wert. + + Vier Bereiche, vier Hinweise, weil vier verschiedene Kacheln + gemeint sind. Ein Sammelhinweis "6 markierte Punkte" liesse offen, + wo man nachsehen soll. */ + if (person.rolle === "creator") { + const BEREICHE = [ + ["live", "LIVE-Analyse", "bereich.html?b=live"], + ["content", "Content-Ideen", "content.html"], + ["community", "Community", "bereich.html?b=community"], + ["technik", "Technik", "bereich.html?b=technik"], + ]; + for (const [schluessel, name, ziel] of BEREICHE) { + dazu(`markiert_${schluessel}`, "warnung", + `markierter Punkt in ${name}`, ziel, + /* zaehle() faengt Fehler ab -- gibt es die Tabelle noch nicht + (niemand hat je eine Checkliste geoeffnet), steht hier 0 + statt eines Ausfalls der ganzen Uebersicht. */ + zaehle(`SELECT COUNT(*) n FROM punkt_stand + WHERE bereich = ? AND creator_id = ? AND stufe = 'verbessern'`, + [schluessel, person.id])); + } + } + /* ---------- Scout-Pipeline ---------- */ if (istLeitung(person) || person.rolle === "scout") { const nur = istLeitung(person) ? "1=1" : "l.scout_id = ?"; diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index ac57e23c..60dbb693 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -1880,9 +1880,83 @@ body.start #protokoll { } .bilanz-zahl__text { font-size: .82rem; color: var(--text-leise); } -/* ---------- Die Gruppen -------------------------------------------------- */ -.fest-gruppe { margin-bottom: 24px; } -.fest-gruppe__kopf { display: flex; align-items: baseline; gap: 10px; } +/* Die Zahl "zu verbessern" führt zu den markierten Gruppen. Sie sieht + deshalb anfassbar aus -- ein Klickziel, das aussieht wie Text, findet + niemand. */ +.bilanz-zahl[data-klickbar="ja"] { + cursor: pointer; + transition: border-color var(--tempo), background var(--tempo), + transform var(--tempo); +} +.bilanz-zahl[data-klickbar="ja"]:hover, +.bilanz-zahl[data-klickbar="ja"]:focus-visible { + border-color: color-mix(in srgb, var(--b-ton) 55%, var(--rand)); + background: + linear-gradient(180deg, color-mix(in srgb, var(--b-ton) 16%, transparent), transparent 60%), + var(--flaeche); +} +.bilanz-zahl[data-klickbar="ja"]:focus-visible { + outline: 2px solid var(--b-ton); outline-offset: 2px; +} +.bilanz-zahl[data-klickbar="ja"]::after { + content: '→'; margin-left: auto; + color: var(--b-ton); font-size: .9rem; + transition: transform var(--tempo); +} +.bilanz-zahl[data-klickbar="ja"]:hover::after { transform: translateX(3px); } + +/* ---------- Die Gruppen -------------------------------------------------- + + Zugeklappt als Standard (Wunsch 01.09.2026: "die sollen auch zu machen, + damit die Seiten nicht so lang sind"). Vier Bereiche mit je rund + zwanzig Punkten sind aufgeklappt eine Seite, die niemand zu Ende + scrollt -- und was niemand zu Ende scrollt, wird auch nicht gelesen. + + Der Kopf ist ein Knopf über die ganze Breite, kein Pfeilchen daneben: + ein Ziel von 46 Pixeln Höhe trifft man auch mit dem Daumen. */ +.fest-gruppe { margin-bottom: 14px; } +.fest-gruppe__kopf { + display: flex; align-items: center; gap: 10px; + width: 100%; margin: 0; padding: 11px 14px; + text-align: left; font: inherit; cursor: pointer; + color: var(--text); + background: linear-gradient(180deg, + color-mix(in srgb, var(--ton, var(--akzent)) 7%, transparent), transparent 70%), + var(--flaeche); + border: 1px solid var(--rand); + border-radius: var(--radius-klein); + transition: border-color var(--tempo), background var(--tempo); +} +.fest-gruppe__kopf:hover { + border-color: color-mix(in srgb, var(--ton, var(--akzent)) 40%, var(--rand)); +} +.fest-gruppe__kopf:focus-visible { + outline: 2px solid var(--ton, var(--akzent)); outline-offset: 2px; +} +/* Offen: der Kopf wächst optisch mit dem Körper zusammen, sonst sähen es + aus wie zwei Kästen ohne Zusammenhang. */ +.fest-gruppe[data-auf="ja"] .fest-gruppe__kopf { + border-bottom-left-radius: 0; border-bottom-right-radius: 0; + border-bottom-color: transparent; +} + +/* Der Pfeil ist aus Rändern gebaut statt ein Zeichen -- ein ">" wird von + Vorleseprogrammen mitgelesen und richtet sich nach der Schriftart. */ +.fest-gruppe__pfeil { + flex: none; width: 8px; height: 8px; + border-right: 2px solid var(--text-still); + border-bottom: 2px solid var(--text-still); + transform: rotate(-45deg); margin-left: 2px; + transition: transform var(--tempo), border-color var(--tempo); +} +.fest-gruppe[data-auf="ja"] .fest-gruppe__pfeil { transform: rotate(45deg); } +.fest-gruppe__kopf:hover .fest-gruppe__pfeil { + border-color: var(--ton, var(--akzent)); +} +@media (prefers-reduced-motion: reduce) { + .fest-gruppe__pfeil { transition: none; } +} + .fest-gruppe__name { margin: 0; font-size: .96rem; font-weight: 650; color: var(--text); } @@ -1892,12 +1966,101 @@ body.start #protokoll { font-size: .72rem; color: var(--text-still); font-variant-numeric: tabular-nums; } +/* Das Merkzeichen steht ganz rechts -- der Platz, an dem das Auge beim + Überfliegen einer Liste von Köpfen zuletzt hängenbleibt. */ +.fest-gruppe__kopf .merk { margin-left: auto; } + +.fest-gruppe__koerper { + padding: 14px; + border: 1px solid var(--rand); border-top: 0; + border-bottom-left-radius: var(--radius-klein); + border-bottom-right-radius: var(--radius-klein); + background: color-mix(in srgb, var(--flaeche) 62%, transparent); +} +.fest-gruppe__koerper[hidden] { display: none; } .fest-gruppe__text { - margin: 4px 0 12px; max-width: 68ch; + margin: 0 0 12px; max-width: 68ch; font-size: .84rem; line-height: 1.5; color: var(--text-still); } .fest-punkte { display: grid; gap: 9px; } +/* ---------- Das Merkzeichen ---------------------------------------------- + + Der Wunsch war ein Zeichen, "so dass die sehen, da ist was". Drei + Entscheidungen stecken darin, und keine ist Geschmack: + + FORM. Eine Raute. Alles andere auf dem Bildschirm ist rund oder eckig; + eine Spitze nach oben gibt es sonst nirgends. Das Auge findet sie + zwischen zwanzig Kacheln, ohne dass jemand hinsehen muss. + + FARBE. Immer Bernstein, nie die Farbe der Kachel darunter. Ein Zeichen, + das die Farbe wechselt, muss gelesen werden; eines, das immer gleich + aussieht, wird erkannt. Rot wäre falsch: "verbessern" ist ein Auftrag, + kein Fehler -- und eine Seite voller roter Zeichen liest niemand mehr. + + BEWEGUNG. Ein sehr langsames Atmen des Scheins (3,2 s), kein Blinken. + Bei "weniger Bewegung" bleibt der Schein stehen statt zu verschwinden -- + sonst wäre das Zeichen genau für die Leute schwächer, die + Aufmerksamkeitsreize ohnehin schlechter verarbeiten. */ +.merk { + --merk-ton: #e8b45a; + position: relative; + /* Erzeugt einen eigenen Stapelraum. Nur dadurch landet der Schein + (::before, z-index -1) ÜBER dem eigenen Hintergrund und UNTER dem + Zeichen. Ohne das rutscht er hinter den Hintergrund der Gruppenzeile + und ist schlicht weg -- ein Fehler, den man nicht sieht, weil das + Zeichen ja trotzdem da ist. */ + isolation: isolate; + display: inline-flex; align-items: center; gap: 5px; + flex: none; + padding: 3px 7px 3px 4px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--merk-ton) 42%, transparent); + background: color-mix(in srgb, var(--merk-ton) 13%, transparent); + color: var(--merk-ton); + line-height: 1; +} +.merk__svg { width: 17px; height: 17px; flex: none; } +.merk__flaeche { + fill: color-mix(in srgb, var(--merk-ton) 22%, transparent); + stroke: var(--merk-ton); stroke-width: 1.5; stroke-linejoin: round; +} +.merk__strich, .merk__punkt { + fill: none; stroke: var(--merk-ton); + stroke-width: 2.1; stroke-linecap: round; +} +.merk__n { + font-size: .76rem; font-weight: 700; + font-variant-numeric: tabular-nums; +} +/* Der Schein liegt HINTER dem Zeichen und ist unscharf -- er macht die + Stelle heller, ohne selbst als Form sichtbar zu werden. */ +.merk::before { + content: ''; position: absolute; inset: -5px; + border-radius: inherit; + background: radial-gradient(circle at 22% 50%, + color-mix(in srgb, var(--merk-ton) 40%, transparent), transparent 70%); + filter: blur(6px); + opacity: .5; + pointer-events: none; + z-index: -1; +} +@media (prefers-reduced-motion: no-preference) { + .merk::before { animation: merk-atmen 3.2s ease-in-out infinite; } + @keyframes merk-atmen { + 0%, 100% { opacity: .34; transform: scale(.94); } + 50% { opacity: .72; transform: scale(1.06); } + } +} +/* Im Kontrastmodus von Windows werden Farben ersetzt -- dann trägt das + Zeichen die Bedeutung allein über seine Form, und der Schein stört. */ +@media (forced-colors: active) { + .merk { border-color: CanvasText; color: CanvasText; } + .merk::before { display: none; } + .merk__flaeche, .merk__strich, .merk__punkt { stroke: CanvasText; } + .merk__flaeche { fill: none; } +} + /* ---------- Ein Punkt ---------------------------------------------------- */ .fest-punkt { --p-ton: #6f89a8; @@ -1924,6 +2087,12 @@ body.start #protokoll { display: flex; align-items: flex-start; justify-content: space-between; gap: 13px; } +/* Zeichen und Wort stehen zusammen, nicht übereinander -- sonst wandert + die Marke bei markierten Punkten nach unten und die Liste wirkt, als + wären die Zeilen unterschiedlich hoch. */ +.fest-punkt__stufe { + display: flex; align-items: center; gap: 7px; flex: none; +} .fest-punkt__text { min-width: 0; } .fest-punkt__titel { margin: 0 0 5px; font-size: .93rem; font-weight: 650; color: var(--text); @@ -2006,9 +2175,14 @@ body.start #protokoll { @media (max-width: 560px) { .liste-fest__kopf { flex-direction: column; align-items: stretch; } .fest-punkt__kopf { flex-direction: column; } - .fest-punkt__marke-stufe { align-self: flex-start; } + .fest-punkt__stufe { align-self: flex-start; } .fest-punkt__fuss .schritt { margin-left: 0; } .stufe-knopf { flex: 1 1 auto; text-align: center; } + /* Auf einem schmalen Bildschirm bleibt vom Gruppenkopf nur wenig + Breite. Die reine Anzahl der Punkte ist dort das Erste, was gehen + darf -- das Merkzeichen ist das Letzte. */ + .fest-gruppe__zahl { display: none; } + .fest-gruppe__name { min-width: 0; overflow-wrap: anywhere; } } /* Die neuen Klassen der festen Checkliste in der Mindestgroesse fuer @@ -2019,5 +2193,35 @@ body.start #protokoll { body.start .fest-punkt__hookschild, body.start .fest-punkt__marke, body.start .fest-punkt__von, + body.start .merk__n, body.start .fest-gruppe__zahl { font-size: .75rem; } } + +/* ---------- Das Merkzeichen auf der Kachel ------------------------------- + + "und es soll auch oben in der Kachel angezeigt werden." + + Oben rechts, INNERHALB der Kachel. Ein überhängendes Zeichen sähe + besser aus, wäre hier aber unsichtbar: `.kachel` trägt `overflow: + hidden` (für das angeschnittene Wasserzeichen), und alles, was über + den Rand ragt, wird abgeschnitten. Genau so eine Änderung sieht im + Quelltext richtig aus und ist auf dem Bildschirm weg. + + Die Zahl der Kachel sitzt senkrecht mittig, das Merkzeichen oben -- + sie kommen sich deshalb nicht ins Gehege. */ +.kachel__merk { + position: absolute; top: 9px; right: 11px; + z-index: 3; + padding: 2px 6px 2px 3px; + background: color-mix(in srgb, var(--merk-ton) 15%, var(--flaeche)); + box-shadow: 0 2px 10px rgba(0, 0, 0, .45); +} +.kachel__merk .merk__svg { width: 16px; height: 16px; } +/* Zahl und Pfeil sitzen sonst senkrecht mittig -- auf einer 78 Pixel + hohen Kachel stossen sie damit oben an das Merkzeichen. Sie ruecken + deshalb nach unten, sobald eines da ist. */ +.kachel[data-merk="ja"] .kachel__zahl, +.kachel[data-merk="ja"] .kachel__pfeil { align-self: flex-end; } +@media (max-width: 560px) { + .kachel__merk { right: 10px; } +} diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index a830d2b0..6d410783 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -196,5 +196,64 @@ return svg; } - window.Bereiche = { ZEICHEN, GRUPPEN, zuSeite, zeichenBauen }; + /* ---------- Das Merkzeichen ---------------------------------------------- + + Wunsch vom 01.09.2026: "wenn die Ansprechpartner was markieren, sollen + die da so ein Zeichen/Symbol haben, so dass die sehen, da ist was -- + und es soll auch oben in der Kachel angezeigt werden." + + Es ist bewusst das EINZIGE Zeichen der ganzen Anwendung, das eine + eigene Form hat statt der 24×24-Linienzeichen oben: eine Raute mit + Ausrufestrich. Der Grund ist nicht Geschmack, sondern Erkennbarkeit -- + alle anderen Zeichen sind Rechtecke und Kreise; eine Raute ist die + einzige Form auf dem Bildschirm, die spitz nach oben zeigt, und + genau deshalb findet das Auge sie zwischen zwanzig Kacheln sofort. + + Und es ist immer DIESELBE Farbe (Bernstein), egal auf welcher Kachel + es sitzt. Ein Zeichen, das seine Farbe wechselt, muss gelesen werden; + eines, das immer gleich aussieht, wird erkannt. Rot wäre falsch -- + "verbessern" ist kein Fehler, sondern ein Auftrag. + + anzahl > 1 schreibt die Zahl daneben, sonst steht nur das Zeichen. + Der Satz für Vorleseprogramme wird immer gesetzt: Ein Symbol ohne + Text ist für sie nicht vorhanden. */ + function merkzeichenBauen(anzahl = 0, klasse = 'merk') { + const s = document.createElement('span'); + s.className = klasse; + s.setAttribute('role', 'img'); + s.setAttribute('aria-label', anzahl > 0 + ? `${anzahl} ${anzahl === 1 ? 'Punkt ist' : 'Punkte sind'} zum Verbessern markiert` + : 'Zum Verbessern markiert'); + + 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', klasse + '__svg'); + /* Drei Pfade: die gefüllte Raute, der Strich, der Punkt. Getrennt, + damit die Fläche eine andere Deckkraft bekommen kann als die + Linie -- eine durchgehend volle Raute wäre auf dunklem Grund ein + Klecks. */ + const teile = [ + ['M12 1.9 22.1 12 12 22.1 1.9 12z', 'flaeche'], + ['M12 7.5v5.6', 'strich'], + ['M12 16.9h.01', 'punkt'], + ]; + for (const [d, art] of teile) { + const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + p.setAttribute('d', d); + p.setAttribute('class', `${klasse}__${art}`); + svg.append(p); + } + s.append(svg); + if (anzahl > 1) { + const n = document.createElement('span'); + n.className = klasse + '__n'; + n.textContent = String(anzahl); + n.setAttribute('aria-hidden', 'true'); + s.append(n); + } + return s; + } + + window.Bereiche = { ZEICHEN, GRUPPEN, zuSeite, zeichenBauen, merkzeichenBauen }; })(); diff --git a/workspace/assets/js/checkliste.js b/workspace/assets/js/checkliste.js index e7c9f2e9..a9d222c2 100644 --- a/workspace/assets/js/checkliste.js +++ b/workspace/assets/js/checkliste.js @@ -97,32 +97,132 @@ if (daten.creator_id || !daten.darf_bewerten) { const bilanz = el('div', 'liste-fest__bilanz'); bilanz.append(zahl(gut, 'passt so', 'gut')); - bilanz.append(zahl(zuTun, zuTun === 1 ? 'zu verbessern' : 'zu verbessern', 'verbessern')); + bilanz.append(zahl(zuTun, 'zu verbessern', 'verbessern')); bilanz.append(zahl(alle.length - gut - zuTun, 'noch offen', 'offen')); block.append(bilanz); } /* ---- Die Gruppen ---- */ + const aufgeklappt = standLesen(bereich); + const markierte = []; /* Gruppen mit etwas zum Verbessern */ + for (const g of daten.gruppen) { const abschnitt = el('section', 'fest-gruppe'); - const gk = el('div', 'fest-gruppe__kopf'); + const offenZahl = g.punkte + .filter((p) => daten.stand[p.schluessel]?.stufe === 'verbessern').length; + if (offenZahl) markierte.push(g.schluessel); + + /* Der Kopf ist ein KNOPF, nicht eine Überschrift mit einem Knopf + daneben. Ein Ziel von 40 Pixeln Höhe über die ganze Breite trifft + man auch mit dem Daumen -- ein 16-Pixel-Pfeil daneben nicht. */ + const gk = el('button', 'fest-gruppe__kopf'); + gk.type = 'button'; + const koerperId = `fg-${bereich}-${g.schluessel}`; + gk.setAttribute('aria-controls', koerperId); + + const pfeil = el('span', 'fest-gruppe__pfeil'); + pfeil.setAttribute('aria-hidden', 'true'); + gk.append(pfeil); gk.append(el('h3', 'fest-gruppe__name', g.name)); gk.append(el('span', 'fest-gruppe__zahl', String(g.punkte.length))); + + /* Das Merkzeichen am Gruppenkopf ist der Grund, warum zugeklappte + Gruppen überhaupt vertretbar sind: Ohne es wäre eine Markierung + hinter einer geschlossenen Gruppe unsichtbar -- der Betreuer + markiert etwas, und der Creator erfährt es nie. */ + if (offenZahl) { + gk.append(window.Bereiche.merkzeichenBauen(offenZahl, 'merk')); + abschnitt.dataset.markiert = 'ja'; + } abschnitt.append(gk); - if (g.text) abschnitt.append(el('p', 'fest-gruppe__text', g.text)); + + const koerper = el('div', 'fest-gruppe__koerper'); + koerper.id = koerperId; + if (g.text) koerper.append(el('p', 'fest-gruppe__text', g.text)); const liste = el('div', 'fest-punkte'); for (const p of g.punkte) { liste.append(punktBauen(p, daten, bereich, behaelter)); } - abschnitt.append(liste); + koerper.append(liste); + abschnitt.append(koerper); + + /* Zu, sofern nicht ausdrücklich anders gemerkt. Das ist der ganze + Zweck: Vier Bereiche mit je einundzwanzig Punkten sind offen eine + Seite, die niemand zu Ende scrollt. */ + klappZustand(abschnitt, gk, koerper, aufgeklappt.has(g.schluessel)); + gk.addEventListener('click', () => { + const jetztAuf = abschnitt.dataset.auf !== 'ja'; + klappZustand(abschnitt, gk, koerper, jetztAuf); + standSchreiben(bereich, g.schluessel, jetztAuf); + }); + block.append(abschnitt); } + /* Die Zahl "zu verbessern" wird zum Weg dorthin. Eine Zahl, die einen + auf etwas hinweist, das man dann selbst suchen muss, ist eine + halbe Auskunft. */ + const bilanzKnopf = block.querySelector('.bilanz-zahl[data-stufe="verbessern"]'); + if (bilanzKnopf && zuTun > 0 && markierte.length) { + bilanzKnopf.dataset.klickbar = 'ja'; + bilanzKnopf.setAttribute('role', 'button'); + bilanzKnopf.tabIndex = 0; + bilanzKnopf.title = 'Die markierten Punkte aufklappen'; + const hin = () => { + let erste = null; + for (const a of block.querySelectorAll('.fest-gruppe[data-markiert="ja"]')) { + const k = a.querySelector('.fest-gruppe__kopf'); + const b = a.querySelector('.fest-gruppe__koerper'); + klappZustand(a, k, b, true); + standSchreiben(bereich, b.id.split('-').slice(2).join('-'), true); + if (!erste) erste = a; + } + erste?.scrollIntoView({ block: 'start', behavior: ruhig() ? 'auto' : 'smooth' }); + }; + bilanzKnopf.addEventListener('click', hin); + bilanzKnopf.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); hin(); } + }); + } + behaelter.append(block); behaelter.hidden = false; }; + const ruhig = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + function klappZustand(abschnitt, knopf, koerper, auf) { + abschnitt.dataset.auf = auf ? 'ja' : ''; + knopf.setAttribute('aria-expanded', auf ? 'true' : 'false'); + /* hidden statt display:none im Stil: So findet auch die Suche im + Browser (Strg+F) nichts in einer zugeklappten Gruppe -- sonst + springt sie an eine Stelle, an der nichts zu sehen ist. */ + koerper.hidden = !auf; + } + + /* Welche Gruppen offen waren, überlebt das Neuladen. Ohne das wäre + jede Bewertung ein Rückschlag: Die Liste baut sich neu auf, und der + Betreuer sucht die Gruppe, in der er gerade war, von vorn. + localStorage und nicht der Server: Es ist eine Ansichtssache dieses + Geräts, keine Eigenschaft des Creators. */ + const SCHLUESSEL = (bereich) => `dfw-checkliste-auf-${bereich}`; + + function standLesen(bereich) { + try { + const roh = localStorage.getItem(SCHLUESSEL(bereich)); + return new Set(roh ? JSON.parse(roh) : []); + } catch { return new Set(); } + } + + function standSchreiben(bereich, gruppe, auf) { + try { + const menge = standLesen(bereich); + if (auf) menge.add(gruppe); else menge.delete(gruppe); + localStorage.setItem(SCHLUESSEL(bereich), JSON.stringify([...menge])); + } catch { /* privater Modus -- dann eben nur für diesen Aufbau */ } + } + function zahl(n, text, stufe) { const k = el('div', 'bilanz-zahl'); k.dataset.stufe = stufe; @@ -155,11 +255,18 @@ kopf.append(links); /* Die Marke rechts -- immer sichtbar, auch für den Creator, auch - wenn sie "noch offen" lautet. Er soll sehen, wo er steht. */ - const marke = el('span', 'fest-punkt__marke-stufe', + wenn sie "noch offen" lautet. Er soll sehen, wo er steht. + + Bei "verbessern" steht das Merkzeichen davor: dieselbe Raute wie + am Gruppenkopf und auf der Kachel. Wer sie auf der Startseite + gesehen hat, erkennt hier wieder, was gemeint war -- ohne den Text + lesen zu müssen. */ + const rechts = el('div', 'fest-punkt__stufe'); + if (stufe === 'verbessern') rechts.append(window.Bereiche.merkzeichenBauen(0, 'merk')); + rechts.append(el('span', 'fest-punkt__marke-stufe', stufe === 'gut' ? 'Passt so' - : stufe === 'verbessern' ? 'Verbessern' : 'Offen'); - kopf.append(marke); + : stufe === 'verbessern' ? 'Verbessern' : 'Offen')); + kopf.append(rechts); k.append(kopf); /* Der Grund steht in voller Breite -- "verbessern" ohne Grund ist diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 92eadcc3..5bec65b3 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -60,8 +60,20 @@ review_bald: 'Reports & Review', ohne_betreuung: 'Personen & Zugänge', ohne_profil: 'Creator-Profile', + markiert_live: 'LIVE-Analyse', + markiert_content: 'Content-Ideen', + markiert_community: 'Community', + markiert_technik: 'Technik', }; + /* Diese Hinweise bekommen zusätzlich das Merkzeichen auf die Kachel. + Sie sind etwas anderes als "3 Aufgaben offen": Da hat jemand + persönlich etwas markiert und wartet auf eine Reaktion. Eine Zahl + allein macht diesen Unterschied nicht sichtbar. */ + const MIT_MERKZEICHEN = new Set([ + 'markiert_live', 'markiert_content', 'markiert_community', 'markiert_technik', + ]); + /* 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. */ @@ -98,9 +110,10 @@ for (const h of hinweise) { const kachel = HINWEIS_ZU_KACHEL[h.art]; if (!kachel) continue; - const bisher = zahlen.get(kachel) || { n: 0, warn: false }; + const bisher = zahlen.get(kachel) || { n: 0, warn: false, merk: 0 }; bisher.n += h.anzahl; if (h.stufe === 'warnung') bisher.warn = true; + if (MIT_MERKZEICHEN.has(h.art)) bisher.merk += h.anzahl; zahlen.set(kachel, bisher); } @@ -152,7 +165,27 @@ a.append(text); const zahl = zahlen.get(b.name); - if (zahl && zahl.n > 0) { + + /* Das Merkzeichen oben in der Kachel. Es sitzt im Link und nicht + im Text, damit es sich absolut an der Kachelecke ausrichten + kann statt im Fluss mitzuschwimmen. */ + if (zahl && zahl.merk > 0) { + const m = window.Bereiche.merkzeichenBauen(zahl.merk, 'merk'); + m.classList.add('kachel__merk'); + a.append(m); + /* Die Kachel merkt sich das: Zahl und Pfeil ruecken dann nach + unten, damit sie dem Zeichen nicht ins Gehege kommen. Die + Browserpruefung hat genau diese Ueberdeckung gemeldet -- mit + blossem Auge waren es zwei Pixel. */ + li.dataset.merk = 'ja'; + } + + /* Besteht die Zahl NUR aus Markierungen, waere sie dieselbe + Auskunft ein zweites Mal: Das Zeichen sagt bereits "hier ist + etwas", und bei mehreren steht die Anzahl darin. Dann lieber + der Pfeil -- zwei Marken uebereinander auf einer Kachel von + achtzig Pixeln sind Unruhe ohne Gewinn. */ + if (zahl && zahl.n > 0 && zahl.n > zahl.merk) { const marke = el('span', 'kachel__zahl', String(zahl.n)); if (zahl.warn) marke.dataset.warn = 'ja'; /* Vorlesende Programme bekommen einen Satz statt einer nackten @@ -334,6 +367,15 @@ ohne_profil: (n) => `${n} Creator-Profil${n === 1 ? ' ist' : 'e sind'} noch leer`, ohne_startcheck: (n) => `${n} Creator ${n === 1 ? 'hat' : 'haben'} noch keinen Start-Check`, startcheck_handlung: (n) => `${n} ${n === 1 ? 'Punkt im Start-Check braucht' : 'Punkte im Start-Check brauchen'} Handlung`, + /* Diese vier beginnen mit der Zahl, wie alle anderen auch: Die Zeile + schneidet die führende Zahl ab und zeigt sie getrennt davor. Ein + Satz, der anders anfängt ("Deine Betreuung hat 3 …"), stünde + hinter der Marke ein zweites Mal mit der 3 -- kein Fehler, den ein + Programm meldet, aber einer, den jeder sieht. */ + markiert_live: (n) => `${n} markierte${n === 1 ? 'r Punkt' : ' Punkte'} in der LIVE-Analyse`, + markiert_content: (n) => `${n} markierte ${n === 1 ? 'Content-Idee' : 'Content-Ideen'}`, + markiert_community: (n) => `${n} markierte${n === 1 ? 'r Punkt' : ' Punkte'} bei Community`, + markiert_technik: (n) => `${n} markierte${n === 1 ? 'r Punkt' : ' Punkte'} bei Technik`, }; async function hinweiseLaden() { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index f8499f4d..8532f507 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + +
@@ -139,9 +139,9 @@ - - - - + + + +