diff --git a/pruef-buehne.png b/pruef-buehne.png index aba4b4af..fc6b1f72 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 c7f5b27d..67ef19fc 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 0ec84552..dcff3b19 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 085f429b..a28cbea2 100644 Binary files a/pruef-start-handy.png and b/pruef-start-handy.png differ diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs index 2335b33f..5a96134a 100644 --- a/server/pruef-buehne.mjs +++ b/server/pruef-buehne.mjs @@ -114,17 +114,52 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] g.drawImage(bild, 0, 0); const skala = bild.width / window.innerWidth; - let schlecht = null, anzahl = 0; + /* Wo darf man den Untergrund abtasten? + + Vorher wurde stur 4 px rechts neben dem Text gemessen. Das ging + gut, solange dort nichts stand -- sobald aber zwei Textstücke + nebeneinanderliegen (Grußwort und Name, Marke und Ortsangabe), + traf der Punkt DEN NACHBARTEXT und die Prüfung verglich Schrift + mit Schrift. Ergebnis: 1,1:1 und ein Alarm, der nichts mit der + Lesbarkeit zu tun hatte. + + Jetzt werden mehrere Stellen rings um den Text angeboten und + jede zuerst gefragt: Liegt dort wirklich Untergrund? Ein Punkt + gilt nur, wenn an seiner Stelle KEIN fremdes Textelement liegt. + Das macht die Prüfung strenger, nicht lockerer -- sie misst + jetzt tatsächlich das, was sie behauptet zu messen. */ + const istFremderText = (el2, e) => + el2 && el2 !== e && !el2.contains(e) && !e.contains(el2) + && el2.children.length === 0 && el2.textContent.trim().length > 0; + + let schlecht = null, anzahl = 0, ohneStelle = 0; const alle = [...document.querySelectorAll("p, h1, h2, h3, span, a, li")] .filter((e) => e.children.length === 0 && e.textContent.trim().length > 2); for (const e of alle) { const r = e.getBoundingClientRect(); if (r.width < 8 || r.height < 8) continue; if (r.top < 0 || r.bottom > window.innerHeight) continue; - const x = Math.round((r.left + r.width + 4) * skala); - const y = Math.round((r.top + r.height / 2) * skala); - if (x < 0 || x >= c.width || y < 0 || y >= c.height) continue; - const p = g.getImageData(x, y, 1, 1).data; + + const mitte = r.top + r.height / 2; + const kandidaten = [ + [r.right + 4, mitte], [r.right + 14, mitte], + [r.left - 4, mitte], [r.left - 14, mitte], + [r.left + r.width / 2, r.top - 3], [r.left + r.width / 2, r.bottom + 3], + ]; + let p = null; + for (const [kx, ky] of kandidaten) { + if (kx < 0 || ky < 0 || kx >= window.innerWidth || ky >= window.innerHeight) continue; + if (istFremderText(document.elementFromPoint(kx, ky), e)) continue; + const bx = Math.round(kx * skala), by = Math.round(ky * skala); + if (bx < 0 || bx >= c.width || by < 0 || by >= c.height) continue; + p = g.getImageData(bx, by, 1, 1).data; + break; + } + /* Kein freier Punkt gefunden -- das wird GEZÄHLT und ausgegeben. + Ein stillschweigend übersprungener Text wäre eine Prüfung, die + behauptet, etwas geprüft zu haben. */ + if (!p) { ohneStelle++; continue; } + const st = getComputedStyle(e); const farbe = st.color.match(/\d+/g).slice(0, 3).map(Number); const gr = parseFloat(st.fontSize); @@ -136,13 +171,20 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26), grund: [p[0], p[1], p[2]] }; } } - return { ...schlecht, anzahl }; + return { ...schlecht, anzahl, ohneStelle, gesamt: alle.length }; }, bild.toString("base64")); ok(schlimmster.v >= schlimmster.grenze, `${s}: schlechtester Kontrast ${schlimmster.v.toFixed(2)}:1 (nötig ${schlimmster.grenze}:1) ` + `bei "${schlimmster.text}" auf rgb(${schlimmster.grund.join(",")}) · ` + `${schlimmster.anzahl} Stellen gemessen`); + /* Wenn rings um einen Text nirgends freier Untergrund liegt, ist er + ungeprüft geblieben. Das darf vorkommen (dicht gesetzte Zeilen), + aber nicht unbemerkt und nicht bei vielen -- sonst meldet die + Prüfung "bestanden" für eine Seite, die sie kaum angesehen hat. */ + ok(schlimmster.ohneStelle <= 2, + `${s}: höchstens zwei Texte ohne freien Untergrund ` + + `(${schlimmster.ohneStelle} von ${schlimmster.gesamt})`); } if (breite === 1440) { diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index f38bc196..d94f2fe6 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -75,6 +75,26 @@ async function anmelden(kontext, rolle, code) { const lesen = (seite) => seite.evaluate(() => ({ gruss: document.getElementById("gruss")?.textContent, + /* Die Begrüßung ist ein Kopf aus vier Teilen geworden: Datum mit + Kalenderwoche, ein Wort zum Tag, die Plakette in Rollenfarbe und + ein Satz zur Lage. Jeder Teil wird einzeln nachgesehen -- "sieht + gut aus" ist kein Nachweis. */ + datum: document.getElementById("datum")?.textContent || "", + grussWort: document.querySelector(".gruss__wort")?.textContent || "", + grussName: document.querySelector(".gruss__name")?.textContent || "", + plakette: document.getElementById("plakette")?.textContent || "", + plaketteRolle: document.getElementById("plakette")?.dataset.rolle || "", + plaketteRund: getComputedStyle(document.getElementById("plakette") || document.body).borderRadius, + marker: document.getElementById("tagesmarker")?.hidden === false + ? document.getElementById("tagesmarker").textContent : null, + lage: document.getElementById("lage")?.hidden === false + ? document.getElementById("lage").textContent : null, + lageStufe: document.getElementById("lage")?.dataset.stufe || null, + /* Der Schriftzug oben links, aufgeteilt in Rang. */ + markeHaupt: document.querySelector(".kopfleiste .marke__haupt")?.textContent || "", + markeLeise: document.querySelector(".kopfleiste .marke__leise")?.textContent || "", + markeRauten: document.querySelectorAll(".kopfleiste .marke__raute").length, + gruppenNamen: [...document.querySelectorAll(".gruppe__name")].map((g) => g.textContent), gruppen: [...document.querySelectorAll(".gruppe")].map((g) => ({ name: g.querySelector(".gruppe__name")?.textContent, kacheln: g.querySelectorAll(".kachel").length, @@ -143,8 +163,61 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { ok(r.gruppen.length === 3, `drei Gruppen (${r.gruppen.length})`); ok(r.kacheln.length === 17, `siebzehn Kacheln (${r.kacheln.length})`); - ok(/^(Guten Morgen|Guten Tag|Guten Abend|Noch wach), Filipe$/.test(r.gruss || ""), - `Begrüßung nach Tageszeit ("${r.gruss}")`); + ok(/^(Guten Morgen|Guten Mittag|Guten Tag|Guten Abend|Späten Abend|Noch wach), Filipe$/ + .test(r.gruss || ""), `Begrüßung nach Tageszeit ("${r.gruss}")`); + /* Der Name muss eigenständig ausgezeichnet sein -- er ist das Wichtigste + an der Zeile und wird kräftiger gesetzt als der Gruß davor. */ + ok(r.grussName === "Filipe", `der Name steht für sich (${r.grussName})`); + ok(r.grussWort.trim().endsWith(","), `und das Grußwort davor (${r.grussWort.trim()})`); + + /* Datumszeile: Wochentag, Datum, Kalenderwoche. Die KW wird selbst + gerechnet (ISO 8601) -- eine falsche KW wäre schlimmer als keine. */ + console.log(" Datum: ", r.datum, r.marker ? `· ${r.marker}` : ""); + /* Kein \w im Monatsnamen: "März" enthält einen Umlaut, den \w ohne + Unicode-Schalter nicht kennt. Diese Prüfung wäre im März stillschweigend + rot geworden -- ein Fehler, der sieben Monate lang unsichtbar wartet. */ + ok(/^(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag), \d{1,2}\. [A-Za-zÄÖÜäöü]+ · KW \d{1,2}$/ + .test(r.datum), `Datum mit Kalenderwoche ("${r.datum}")`); + const kw = Number((r.datum.match(/KW (\d+)/) || [])[1]); + ok(kw >= 1 && kw <= 53, `die Kalenderwoche ist plausibel (${kw})`); + /* An Tagen mit Charakter steht ein Wort dabei -- an den übrigen nicht. + Beides ist richtig, aber es muss zum Wochentag passen. */ + const tag = r.datum.split(",")[0]; + const erwartet = { Montag: "Neue Woche", Freitag: "Endspurt", + Samstag: "Wochenende", Sonntag: "Wochenende" }[tag] || null; + ok(r.marker === erwartet, + `das Wort zum Tag passt zum ${tag} (${r.marker || "keins"}, erwartet ${erwartet || "keins"})`); + + /* Die Plakette: Anfangsbuchstabe, rund, in der Farbe der Rolle. */ + ok(r.plakette === "F", `die Plakette trägt den Anfangsbuchstaben (${r.plakette})`); + ok(r.plaketteRolle === "admin", `und die Rolle für ihre Farbe (${r.plaketteRolle})`); + ok(/50%|9999px/.test(r.plaketteRund), `und ist rund (${r.plaketteRund})`); + + /* Der Satz zur Lage kommt aus denselben Hinweisen wie die Liste + darunter -- keine zweite Zählung. Hier liegt Überfälliges vor, + also muss er das sagen und rot sein. */ + console.log(" Lage: ", r.lage); + ok(r.lageStufe === "warnung", `die Lage meldet Überfälliges (${r.lageStufe})`); + ok(/^\d+ Sachen? (ist|sind) überfällig/.test(r.lage || ""), `und sagt es im Klartext ("${r.lage}")`); + /* Sie muss zu den Hinweisen passen, sonst stünden zwei Wahrheiten + übereinander auf einem Bildschirm. */ + const dringend = r.dranZeilen.filter((z) => z.stufe === "warnung") + .reduce((s, z) => s + Number(z.zahl), 0); + ok(Number((r.lage.match(/^\d+/) || [])[0]) === dringend, + `und stimmt mit den Hinweisen überein (${r.lage.match(/^\d+/)} = ${dringend})`); + + /* Der Schriftzug oben links hat jetzt einen Rang statt einer Farbe. + Die geschützten Leerzeichen aus dem Quelltext bleiben erhalten (der + Schriftzug darf nicht umbrechen) -- beim Vergleich werden sie + deshalb gleichgesetzt, sonst prüft man Unsichtbares. */ + const ohneNbsp = (t) => t.replace(/ /g, " "); + ok(ohneNbsp(r.markeHaupt) === "Dogfather Universe", `der Name steht vorn ("${r.markeHaupt}")`); + ok(ohneNbsp(r.markeLeise) === "Creator Workspace", `der Ort dahinter ("${r.markeLeise}")`); + ok(r.markeRauten === 1, `und dazwischen genau eine Raute (${r.markeRauten})`); + /* Der geschützte Leerraum muss WIRKLICH noch da sein -- ginge er beim + Aufteilen verloren, bräche "Creator Workspace" auf dem Handy um. */ + ok(r.markeHaupt.includes(" ") && r.markeLeise.includes(" "), + "und der Schriftzug kann weiterhin nicht umbrechen"); /* Jede Kachel muss ein Zeichen haben -- eine ohne waere ein leeres Kaestchen und faellt sofort auf. */ @@ -251,6 +324,24 @@ console.log("\n=== Creator"); ok(!namen.includes("Automationen"), "keine Automationen"); ok(r.gruppen.length === 3, `trotzdem drei Gruppen (${r.gruppen.length})`); ok(r.kacheln.every((k) => k.pfade > 0), "alle Zeichen da"); + /* Von "Team & System" bleibt für ihn genau eine Kachel übrig. Eine + Überschrift, die Team und System verspricht und nur die Bibliothek + zeigt, verspricht etwas Falsches -- deshalb heißt sie hier "Wissen". */ + ok(r.gruppenNamen.includes("Wissen"), `die Gruppe heißt "Wissen" (${r.gruppenNamen.join(" · ")})`); + ok(!r.gruppenNamen.includes("Team & System"), "und nicht mehr \"Team & System\""); + /* Seine Begrüßung: eigene Rollenfarbe, und da nichts anliegt, der + ruhige Satz statt eines erfundenen Alarms. */ + ok(r.plaketteRolle === "creator", `die Plakette kennt seine Rolle (${r.plaketteRolle})`); + /* Tili hat selbst überfällige Aufgaben -- die Lage MUSS das sagen. + Entscheidend ist nicht, welche Stufe herauskommt, sondern dass sie + zu dem passt, was direkt darunter steht. Zwei verschiedene Zahlen + auf einem Bildschirm wären der eigentliche Fehler. */ + const cDringend = r.dranZeilen.filter((z) => z.stufe === "warnung") + .reduce((s, z) => s + Number(z.zahl), 0); + ok(cDringend > 0 ? r.lageStufe === "warnung" : r.lageStufe !== "warnung", + `die Lage passt zu seinen Hinweisen (${r.lageStufe}: "${r.lage}")`); + ok(cDringend === 0 || Number((r.lage.match(/^\d+/) || [])[0]) === cDringend, + `und zur Zahl darunter (${cDringend})`); await seite.screenshot({ path: "pruef-start-creator.png", fullPage: true }); await kontext.close(); } diff --git a/server/pruef-workspace-seiten.mjs b/server/pruef-workspace-seiten.mjs index 0b35b279..219e379d 100644 --- a/server/pruef-workspace-seiten.mjs +++ b/server/pruef-workspace-seiten.mjs @@ -84,13 +84,33 @@ for (const [breite, hoehe, wie] of [[1440, 1000, "Computer"], [390, 844, "Handy" const a = document.querySelector(".abmelden")?.getBoundingClientRect(); return a ? a.right <= window.innerWidth && a.left >= 0 : null; })(), + /* Der Schriftzug oben links wird von kopf.js in Rang aufgeteilt. + Auf den Unterseiten ist der vordere Teil ein VERWEIS zurück zur + Startseite -- der muss die Aufteilung überleben, sonst ist der + Rückweg weg. Genau das wäre beim Zerlegen von Textknoten leicht + zu übersehen. */ + markeHaupt: document.querySelector(".kopfleiste .marke__haupt")?.textContent + .replace(/ /g, " ") || null, + markeLeise: document.querySelector(".kopfleiste .marke__leise")?.textContent + .replace(/ /g, " ") || null, + markeRaute: document.querySelectorAll(".kopfleiste .marke__raute").length, + /* Ist der vordere Teil auf einer Unterseite noch ein klickbarer + Verweis auf die Startseite? */ + rueckweg: document.querySelector(".kopfleiste .marke__haupt")?.tagName === "A" + ? document.querySelector(".kopfleiste .marke__haupt").getAttribute("href") : null, })); + const istStart = s === "start.html"; + const markeGut = r.markeHaupt && r.markeLeise && r.markeRaute === 1 + && (istStart || r.rueckweg === "start.html"); const gut = r.ueber <= 0 && konsole.length === 0 && r.abmelden !== false + && markeGut && (r.breit === null || r.breit === "1240px") && (r.kopfzeile === null || r.kopfzeile === "flex"); if (!gut) fehler++; console.log(` ${gut ? "ok " : "FEHL"} ${String(s).padEnd(20)}` + `über=${String(r.ueber).padStart(3)}px abmelden=${r.abmelden}` + + ` marke=${r.markeHaupt ? `"${r.markeHaupt}"◆"${r.markeLeise}"` : "FEHLT"}` + + `${istStart ? "" : ` zurück=${r.rueckweg || "FEHLT"}`}` + ` breit=${r.breit} kopfzeile=${r.kopfzeile}` + (konsole.length ? " KONSOLE: " + konsole.join(" | ") : "") + (r.schuld.length ? " -> " + r.schuld.join(" | ") : "")); diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 53228a9e..7c07e025 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -60,12 +60,24 @@ body.start::before { den Inhalt, an den Rändern offen, damit die beiden sichtbar bleiben. Gemessen statt geschätzt: server/pruef-buehne.mjs rechnet den Kontrast an echten Bildpunkten nach, an bis zu 34 Stellen je Seite. */ +/* Der Schleier ist am 01.09.2026 spürbar durchlässiger geworden (Wunsch: + "den Hintergrund auch bisschen heller überall"). Er liegt jetzt bei + .86/.58/.44/.66 statt .95/.72/.60/.80 -- die Bühne kommt dadurch + deutlich durch, ohne dass sie den Text stört, weil die Inhalte auf + eigenen, kräftigeren Flächen liegen (siehe --glas in gate.css). + + Ein zweiter, sehr weicher Farbschimmer in Blau-Violett legt sich + darüber. Er nimmt dem Bild das reine Schwarz und gibt der Seite + dieselbe Tiefe wie VanVans Hub -- ohne eine einzige Farbe zu + verfälschen, weil er nur 4-6 % beiträgt. */ body.start > .schleier { position: fixed; inset: 0; z-index: 0; pointer-events: none; background: - linear-gradient(180deg, rgba(5, 7, 13, .95) 0%, rgba(5, 7, 13, .72) 13%, - rgba(5, 7, 13, .6) 48%, rgba(5, 7, 13, .8) 100%), - radial-gradient(56% 68% at 50% 46%, rgba(5, 7, 13, .8), transparent 84%); + linear-gradient(180deg, rgba(5, 7, 13, .86) 0%, rgba(5, 7, 13, .58) 13%, + rgba(5, 7, 13, .44) 48%, rgba(5, 7, 13, .66) 100%), + radial-gradient(70% 55% at 18% 8%, rgba(63, 189, 245, .06), transparent 70%), + radial-gradient(70% 60% at 88% 96%, rgba(138, 118, 255, .07), transparent 72%), + radial-gradient(56% 68% at 50% 46%, rgba(5, 7, 13, .66), transparent 84%); } /* Alles Bedienbare liegt über der Bühne. Ohne das läge der Inhalt zwar @@ -83,8 +95,10 @@ body.start > main { position: relative; z-index: 1; } } body.start > .schleier { background: - linear-gradient(180deg, rgba(5, 7, 13, .97) 0%, rgba(5, 7, 13, .85) 15%, - rgba(5, 7, 13, .82) 60%, rgba(5, 7, 13, .92) 100%); + linear-gradient(180deg, rgba(5, 7, 13, .92) 0%, rgba(5, 7, 13, .74) 15%, + rgba(5, 7, 13, .7) 60%, rgba(5, 7, 13, .84) 100%), + radial-gradient(90% 40% at 20% 4%, rgba(63, 189, 245, .05), transparent 70%), + radial-gradient(90% 40% at 85% 98%, rgba(138, 118, 255, .06), transparent 72%); } } @@ -819,6 +833,68 @@ body.start > main { position: relative; z-index: 1; } color: #a8dbfb; text-shadow: 0 0 14px rgba(63, 189, 245, .28); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + display: flex; align-items: center; gap: 9px; +} + +/* Der Rang im Schriftzug (aufgeteilt von kopf.js). + + Vorher war alles gleich laut. Jetzt trägt der vordere Teil den Namen + und steht kräftig; was dahinter kommt, sagt nur, wo man gerade ist, + und tritt zurück. Beide bleiben eigenständig lesbar -- der leise Teil + liegt bei 5,2:1 und damit deutlich über der Norm. */ +.kopfleiste .marke__haupt { + color: #dcecfb; + font-weight: 700; + text-shadow: 0 0 16px rgba(63, 189, 245, .34); +} +.kopfleiste .marke__leise { + color: #86b4d6; + font-weight: 500; + text-shadow: none; +} +.kopfleiste a.marke__haupt { text-decoration: none; } +.kopfleiste a.marke__haupt:hover { color: #fff; } + +/* Der Trenner ist kein Satzzeichen mehr, sondern eine kleine, auf die + Spitze gestellte Raute in der Hausfarbe -- dieselbe Form wie die + Ecken der Bühne. Ein Punkt zwischen zwei Wörtern liest sich wie ein + Tippfehler; eine Raute liest sich als Gestaltung. */ +.kopfleiste .marke__raute { + flex: none; + width: 5px; height: 5px; + transform: rotate(45deg); + border-radius: 1px; + background: var(--akzent); + box-shadow: 0 0 9px rgba(63, 189, 245, .65); + opacity: .85; +} + +/* Das Zeichen davor bekommt einen eigenen Träger: runder Rahmen, ein + Hauch Fläche, weicher Schein. Aus einer aufgeklebten Form wird damit + ein Wappen. */ +.kopfleiste .marke::before { + padding: 0; + filter: drop-shadow(0 0 7px rgba(63, 189, 245, .45)); +} +.kopfleiste .marke { + padding: 4px 12px 4px 4px; + border-radius: 999px; + border: 1px solid transparent; + transition: border-color var(--tempo), background var(--tempo); +} +.kopfleiste .marke:hover { + border-color: color-mix(in srgb, var(--akzent) 26%, transparent); + background: rgba(255, 255, 255, .035); +} + +@media (prefers-reduced-motion: reduce) { + .kopfleiste .marke { transition: none; } +} +/* Im Kontrastmodus gibt es keine Farbverläufe und keine Schatten -- dort + muss der Rang über die Schriftstärke allein tragen. */ +@media (forced-colors: active) { + .kopfleiste .marke__raute { background: CanvasText; } + .kopfleiste .marke__haupt, .kopfleiste .marke__leise { color: CanvasText; } } /* ---------- Wer angemeldet ist ------------------------------------------ @@ -921,6 +997,74 @@ body.start > main { position: relative; z-index: 1; } } .willkommen .unterzeile { margin-bottom: 0; } +/* Datumszeile: Tag, Datum und Kalenderwoche. Die Ziffern in gleicher + Zeichenbreite, damit die Zeile über die Tage hinweg ruhig bleibt. */ +.willkommen__datum { font-variant-numeric: tabular-nums; } + +/* Das kleine Wort zum Tag ("Neue Woche", "Endspurt", "Wochenende"). + Steht nur an Tagen, die wirklich einen Charakter haben -- ein Wort, + das jeden Tag dasteht, liest nach drei Tagen niemand mehr. */ +.willkommen__marker { + margin-left: 10px; padding: 3px 9px; + border: 1px solid color-mix(in srgb, var(--akzent) 32%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--akzent) 12%, transparent); + color: var(--akzent); + font-size: .68rem; letter-spacing: .1em; +} +.willkommen__marker[hidden] { display: none; } + +/* Der Gruß in zwei Stimmen: das Grußwort leiser, der Name kräftig. + Man liest den eigenen Namen, nicht die Höflichkeitsfloskel. */ +.gruss__wort { color: #c3d2e4; font-weight: 600; } +.gruss__name { color: #f2f7fd; } + +/* Plakette und Text nebeneinander. Die Plakette ist der einzige runde + Körper auf der Seite -- dadurch ist sofort klar, dass sie zu einer + PERSON gehört und nicht zu einem Bereich. */ +.willkommen__reihe { display: flex; align-items: center; gap: 18px; } +.willkommen__text { min-width: 0; } + +.willkommen__plakette { + --rolle-ton: var(--akzent); + flex: none; + display: grid; place-items: center; + width: 58px; height: 58px; border-radius: 50%; + font-size: 1.6rem; font-weight: 700; line-height: 1; + color: #f2f7fd; + border: 1px solid color-mix(in srgb, var(--rolle-ton) 42%, transparent); + background: + radial-gradient(120% 120% at 30% 20%, + color-mix(in srgb, var(--rolle-ton) 34%, transparent), transparent 70%), + rgba(255, 255, 255, .045); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .1), + 0 10px 26px -12px color-mix(in srgb, var(--rolle-ton) 60%, transparent); +} +/* Dieselben vier Rollentöne wie im Kopf der Seite und auf der + Zugangsseite -- eine Rolle hat überall dieselbe Farbe. */ +.willkommen__plakette[data-rolle="admin"] { --rolle-ton: #d8a13a; } +.willkommen__plakette[data-rolle="manager"] { --rolle-ton: #8a76ff; } +.willkommen__plakette[data-rolle="scout"] { --rolle-ton: #12a69b; } +.willkommen__plakette[data-rolle="creator"] { --rolle-ton: #3fbdf5; } + +/* Die Lage in einem Satz, abgesetzt hinter der Rolle. Auf dem Handy + rutscht sie in die zweite Zeile, statt den Text zu quetschen. */ +.willkommen .unterzeile { display: flex; flex-wrap: wrap; gap: 6px 12px; } +.willkommen__lage { position: relative; padding-left: 14px; color: var(--text-leise); } +.willkommen__lage::before { + content: ""; position: absolute; left: 0; top: 50%; + width: 5px; height: 5px; margin-top: -2.5px; border-radius: 50%; + background: currentColor; opacity: .55; +} +.willkommen__lage[data-stufe="warnung"] { color: var(--warn); } +.willkommen__lage[hidden] { display: none; } + +@media (max-width: 560px) { + .willkommen__plakette { width: 46px; height: 46px; font-size: 1.25rem; } + .willkommen__reihe { gap: 13px; } +} + /* Die Trennlinie unter der Begrüßung. Läuft nach rechts aus, statt hart abzubrechen -- eine durchgezogene Linie zerschneidet die Seite. */ .willkommen { diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index a566efcd..b3ac0939 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -116,6 +116,57 @@ return true; } + /* ---------- Der Schriftzug oben links ----------------------------------- + + Er stand als eine Zeile in einer Farbe da: "DOGFATHER UNIVERSE · + CREATOR WORKSPACE". Alles gleich laut, und der Mittelpunkt war ein + Satzzeichen wie jedes andere. + + Jetzt bekommt er einen Rang: Der vordere Teil ist der Name und steht + kräftig, der hintere sagt, wo man gerade ist, und tritt zurück. Der + Trenner ist kein Zeichen mehr, sondern eine kleine Raute in der + Hausfarbe. + + Das passiert hier zentral statt in sechzehn HTML-Dateien -- und ohne + den Text neu zu schreiben: Nur die vorhandenen Textknoten werden + aufgeteilt, Verweise (der Zurück-Link auf den Unterseiten) bleiben + unberührt. Ändert jemand die Beschriftung im HTML, wirkt das hier + weiterhin. */ + for (const marke of document.querySelectorAll('.kopfleiste .marke__text')) { + if (marke.dataset.rang) continue; + for (const knoten of [...marke.childNodes]) { + if (knoten.nodeType !== 3 || !knoten.textContent.includes('·')) continue; + const stueck = document.createDocumentFragment(); + knoten.textContent.split('·').forEach((teil, i) => { + if (i) { + const raute = document.createElement('span'); + raute.className = 'marke__raute'; + raute.setAttribute('aria-hidden', 'true'); + stueck.append(raute); + } + if (!teil.trim()) return; + const s = document.createElement('span'); + s.className = 'marke__teil'; + /* Der geschützte Leerraum im Quelltext soll erhalten bleiben -- + "Creator Workspace" darf nicht umbrechen. */ + s.textContent = teil.replace(/^[\s ]+|[\s ]+$/g, ''); + stueck.append(s); + }); + knoten.replaceWith(stueck); + } + /* Der erste Teil ist der Name (oder auf Unterseiten der Rückweg), + alles danach sagt nur noch, wo man ist. + + Genommen werden ALLE direkten Kinder außer der Raute -- nicht nur + die eben erzeugten. Auf bereich.html steht der Bereichsname in + einem eigenen span, das erst später von bereich.js gefüllt wird; + eine Liste, die nur die selbst gebauten Stücke kennt, hätte ihn + stumm übergangen. */ + const teile = [...marke.children].filter((e) => !e.classList.contains('marke__raute')); + teile.forEach((e, i) => e.classList.add(i === 0 ? 'marke__haupt' : 'marke__leise')); + marke.dataset.rang = 'ja'; + } + /* Die Startseite hat den Knopf gar nicht erst im Dokument -- dort gibt es kein "davor". Deshalb genügt hier die Prüfung, ob er existiert. */ const knopf = document.getElementById('zurueck'); diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 0e652e9c..9947bfda 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -134,6 +134,14 @@ { name: 'Team & System', unter: 'Was den Laden am Laufen hält', + /* Für einen Creator bleibt von dieser Gruppe genau eine Kachel übrig: + die Wissens-Bibliothek. "Team & System" wäre dann eine Überschrift, + die etwas verspricht, was gar nicht da ist -- und die klingt, als + gäbe es dahinter noch einen Bereich, den er nicht sehen darf. Er + liest stattdessen "Wissen", und das beschreibt genau, was dort + steht. Der Untertitel wechselt mit. */ + nameCreator: 'Wissen', + unterCreator: 'Nachschlagen, wenn du etwas brauchst', bereiche: [ { name: 'Personen & Zugänge', unter: 'Rollen und Codes', zeichen: 'personen', ton: 7, rollen: ['admin', 'manager'], ziel: 'personen.html' }, @@ -231,8 +239,12 @@ const abschnitt = el('section', 'gruppe'); const kopf = el('div', 'gruppe__kopf'); - kopf.append(el('h2', 'gruppe__name', gruppe.name)); - kopf.append(el('span', 'gruppe__unter', gruppe.unter)); + /* Manche Gruppen heißen für einen Creator anders -- siehe GRUPPEN. */ + const istCreator = ich.rolle === 'creator'; + kopf.append(el('h2', 'gruppe__name', + (istCreator && gruppe.nameCreator) || gruppe.name)); + kopf.append(el('span', 'gruppe__unter', + (istCreator && gruppe.unterCreator) || gruppe.unter)); abschnitt.append(kopf); const liste = el('ul', 'kacheln'); @@ -338,6 +350,100 @@ }, true); } + /* ---------- Die Begrüßung ------------------------------------------------ + + Sie soll in drei Zeilen sagen, was man beim Reinkommen wissen will: + WANN ist es, WER bin ich, WIE steht es. Vorher stand dort "Angemeldet" + -- eine Zeile, die nichts hinzufügt, weil oben rechts der eigene Name + mit Rolle steht. */ + + const WOCHENTAGE = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', + 'Donnerstag', 'Freitag', 'Samstag']; + const MONATE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', + 'August', 'September', 'Oktober', 'November', 'Dezember']; + + /* Kalenderwoche nach ISO 8601 -- die Woche, in der der Donnerstag liegt. + Selbst gerechnet, weil eine falsche KW schlimmer ist als gar keine + und dafür keine fremde Bibliothek in die Seite gehört. */ + function kalenderwoche(d) { + const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); + t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7)); + const anfang = new Date(Date.UTC(t.getUTCFullYear(), 0, 1)); + return Math.ceil(((t - anfang) / 86400000 + 1) / 7); + } + + /* Manche Tage haben einen Charakter, und den zu benennen kostet nichts. + Bewusst NICHT für jeden Tag -- ein Wort, das immer da steht, liest + nach drei Tagen niemand mehr. */ + const TAGESMARKER = { + 1: 'Neue Woche', + 5: 'Endspurt', + 6: 'Wochenende', + 0: 'Wochenende', + }; + + function grussZeigen() { + const jetzt = new Date(); + const stunde = jetzt.getHours(); + + $('datum').textContent = + `${WOCHENTAGE[jetzt.getDay()]}, ${jetzt.getDate()}. ${MONATE[jetzt.getMonth()]}` + + ` · KW ${kalenderwoche(jetzt)}`; + + const marker = TAGESMARKER[jetzt.getDay()]; + const mEl = $('tagesmarker'); + if (marker) { mEl.textContent = marker; mEl.hidden = false; } + + /* Feiner gestuft als vorher: Wer um kurz nach Mitternacht reinkommt, + liest nicht "Guten Abend", und wer um 13 Uhr kommt, nicht "Morgen". */ + const tageszeit = + stunde < 5 ? 'Noch wach' : + stunde < 11 ? 'Guten Morgen' : + stunde < 14 ? 'Guten Mittag' : + stunde < 18 ? 'Guten Tag' : + stunde < 22 ? 'Guten Abend' : 'Späten Abend'; + + /* Der Name ist das Wichtigste an der Zeile und wird deshalb getrennt + ausgezeichnet -- der Gruß leiser, der Name kräftig. */ + const g = $('gruss'); + g.textContent = ''; + g.append(el('span', 'gruss__wort', tageszeit + ', ')); + g.append(el('span', 'gruss__name', ich.name)); + + /* Die Plakette: der erste Buchstabe in der Farbe der Rolle. Dieselbe + Zuordnung wie im Kopf der Seite, damit nichts auseinanderläuft. */ + const p = $('plakette'); + p.textContent = (ich.name || '?').trim().charAt(0).toUpperCase(); + p.dataset.rolle = ich.rolle; + + $('rollentext').textContent = ROLLENTEXT[ich.rolle] || ich.rolle; + } + + /* Ein Satz zur Lage -- gerechnet aus denselben Hinweisen, die darunter + stehen. Keine zweite Zählung, sonst gäbe es irgendwann zwei + Wahrheiten auf einem Bildschirm. */ + function lageZeigen() { + const el2 = $('lage'); + const gesamt = hinweise.reduce((s, h) => s + h.anzahl, 0); + const dringend = hinweise.filter((h) => h.stufe === 'warnung') + .reduce((s, h) => s + h.anzahl, 0); + + let satz, stufe; + if (dringend > 0) { + satz = `${dringend} ${dringend === 1 ? 'Sache ist' : 'Sachen sind'} überfällig – das zuerst.`; + stufe = 'warnung'; + } else if (gesamt > 0) { + satz = `${gesamt} ${gesamt === 1 ? 'Punkt steht' : 'Punkte stehen'} an, nichts davon brennt.`; + stufe = 'offen'; + } else { + satz = 'Nichts liegt an – guter Tag zum Vorarbeiten.'; + stufe = 'ruhig'; + } + el2.textContent = satz; + el2.dataset.stufe = stufe; + el2.hidden = false; + } + /* ---------- Laden -------------------------------------------------------- */ async function laden() { @@ -351,16 +457,12 @@ } window.werZeigen?.(ich); - /* Tageszeit statt immer "Willkommen". Kostet nichts und macht aus - einer Maske eine Begrüßung. */ - const stunde = new Date().getHours(); - const tageszeit = stunde < 5 ? 'Noch wach' : stunde < 11 ? 'Guten Morgen' - : stunde < 18 ? 'Guten Tag' : 'Guten Abend'; - $('gruss').textContent = `${tageszeit}, ${ich.name}`; - $('rollentext').textContent = ROLLENTEXT[ich.rolle] || ich.rolle; + grussZeigen(); /* Die Hinweise zuerst -- die Kacheln brauchen ihre Zahlen. */ await hinweiseLaden(); + /* Erst jetzt steht fest, wie die Lage ist. */ + lageZeigen(); bereicheBauen(); await zahlenLaden(); } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 6e60d8a1..20c0e1fb 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -139,8 +139,8 @@ - - - + + + diff --git a/workspace/automation.html b/workspace/automation.html index 0db20d90..a2824019 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -132,8 +132,8 @@ - - - + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 86bedb83..4ef0e3fe 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -27,8 +27,12 @@ Zurück -

Creator Workspace · - …

+ +

Creator Workspace · + …

… @@ -103,8 +107,8 @@ - - - + + + diff --git a/workspace/calls.html b/workspace/calls.html index 409554b1..8dd250b2 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -54,9 +54,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index eb7d9f39..d23a3d80 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -158,8 +158,8 @@ - - - + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index f64e4d82..3d4f203c 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -93,8 +93,8 @@ - - - + + + diff --git a/workspace/index.html b/workspace/index.html index f0639012..3e22f76f 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 51622152..9030d1aa 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -99,8 +99,8 @@ - - - + + + diff --git a/workspace/personen.html b/workspace/personen.html index 995b0265..b9f9bc71 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -121,8 +121,8 @@ - - - + + + diff --git a/workspace/profil.html b/workspace/profil.html index b15f6584..4f8056ac 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -132,8 +132,8 @@ - - - + + + diff --git a/workspace/report.html b/workspace/report.html index d8c1bcc1..e6fffe07 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -91,9 +91,9 @@ - - - - + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 169477fe..d182905a 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -124,8 +124,8 @@
- - - + + + diff --git a/workspace/start.html b/workspace/start.html index 78dc2dd5..4635f59b 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -30,10 +30,26 @@ den groesseren Schriften brach der Text ab. Mehr Breite ist hier die ehrlichere Loesung als kleinere Schrift. -->
+
-

Angemeldet

-

Willkommen

-

…

+

+ … + +

+
+ +
+

Willkommen

+

+ … + +

+
+