diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs index 650241bf..e1f815ac 100644 --- a/server/pruef-spicy.mjs +++ b/server/pruef-spicy.mjs @@ -326,6 +326,70 @@ const ruf = async (art, weg, keks, koerper) => { ok(rein.status === 403 || rein.status === 404, `und kommt auch mit der Raumnummer nicht hinein (HTTP ${rein.status})`); } +{ + /* =================================================================== + WAS DOGFATHER GEHOERT, SIEHT SPICY MEDIA NICHT (07.09.2026) + + Filipe: "wieso sieht die spicy rolle meine daten die ich habe und + speichere, meins sollen die nicht sehen von der rolle dogfather." + + Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather + (`1=1`) -- damit stimmte der Ueberblick und nebenbei standen seine + eigenen Termine, Aufgaben, Dateien und Eintraege mit drin. + + Geprueft wird mit ZWEI Datensaetzen je Art: einem von DogFather und + einem von Max. Mit nur einem waere jedes Ergebnis mehrdeutig -- eine + leere Liste koennte auch heissen, dass die Regel ALLES ausblendet. + Genau das ist der Unterschied zwischen "sieht das Richtige nicht" + und "sieht gar nichts". */ + /* ORTSZEIT, nicht UTC. `toISOString().slice(0,10)` liegt zwischen + Mitternacht und zwei Uhr einen Tag daneben -- die Pruefung waere + dann nachts rot, ohne dass sich etwas geaendert haette. Gefunden + von pruef-struktur, das genau danach sucht. */ + const jetztHier = new Date(); + const zz = (n) => String(n).padStart(2, "0"); + const heute = `${jetztHier.getFullYear()}-${zz(jetztHier.getMonth() + 1)}-${zz(jetztHier.getDate())}`; + await ruf("POST", "/workspace/api/bereich/technik", keksDogi, + { art: "problem", titel: "DogFathers Notiz", datum: heute }); + await ruf("POST", "/workspace/api/bereich/technik", keksMax, + { art: "problem", titel: "Max seine Notiz", datum: heute, creator_id: idLuna }); + + const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksSpicy); + const titel = (daten?.eintraege || []).map((e) => e.titel); + ok(!titel.includes("DogFathers Notiz"), + `DogFathers Eintrag ist fuer Spicy Media unsichtbar (sie sieht: ${titel.join(", ") || "nichts"})`); + ok(titel.includes("Max seine Notiz"), + "Max seiner steht aber da -- die Regel blendet nicht einfach alles aus"); + + /* Dasselbe fuer Aufgaben und Termine -- drei Module, drei Wege, und + jeder haette einzeln vergessen werden koennen. */ + await ruf("POST", "/workspace/api/aufgaben", keksDogi, { titel: "DogFathers Aufgabe" }); + await ruf("POST", "/workspace/api/aufgaben", keksMax, + { titel: "Max seine Aufgabe", creator_id: idLuna }); + const auf = await ruf("GET", "/workspace/api/aufgaben", keksSpicy); + const aTitel = (auf.daten?.aufgaben || []).map((a) => a.titel); + ok(!aTitel.includes("DogFathers Aufgabe"), + `auch seine Aufgabe nicht (sie sieht: ${aTitel.join(", ") || "nichts"})`); + ok(aTitel.includes("Max seine Aufgabe"), "Max seine schon"); + + await ruf("POST", "/workspace/api/termine", keksDogi, + { titel: "DogFathers Termin", art: "termin", beginn: heute + "T20:00", dauer_min: 30 }); + await ruf("POST", "/workspace/api/termine", keksMax, + { titel: "Max sein Termin", art: "termin", beginn: heute + "T21:00", dauer_min: 30, + creator_id: idLuna }); + const term = await ruf("GET", `/workspace/api/termine?von=${heute}&bis=${heute}`, keksSpicy); + const tTitel = (term.daten?.termine || []).map((t) => t.titel); + ok(!tTitel.includes("DogFathers Termin"), + `und seinen Termin nicht (sie sieht: ${tTitel.join(", ") || "nichts"})`); + ok(tTitel.includes("Max sein Termin"), "Max seinen schon"); + + /* GEGENPROBE: DogFather selbst sieht weiterhin BEIDE. Ohne sie + bewiesen die Zeilen oben nur, dass irgendetwas gefiltert wird. */ + const beiDogi = await ruf("GET", "/workspace/api/bereich/technik", keksDogi); + const dTitel = (beiDogi.daten?.eintraege || []).map((e) => e.titel); + ok(dTitel.includes("DogFathers Notiz") && dTitel.includes("Max seine Notiz"), + `DogFather sieht weiterhin beide (${dTitel.length})`); +} { /* Und DogFather bleibt der Einzige mit dem letzten Wort. */ const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 49b05279..ac23d26b 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -10,7 +10,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, + db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, } from "./workspace.js"; @@ -58,6 +58,13 @@ export function sichtbar(person) { (01.09.2026) -- und ausdruecklich NUR er. */ case "admin": return { wo: "1=1", werte: [] }; + /* SPICY MEDIA sieht alles ausser dem, was DogFather gehoert + (07.09.2026). Die Bedingung kommt aus workspace.js -- eine + abgeschriebene Fassung waere die Stelle, an der es beim naechsten + Umbau wieder durchsickert. */ + case "spicy": + return { wo: ohneDogFather("a", ["creator_id", "verantwortlich_id", "erstellt_von"]), + werte: [] }; case "creator": return { wo: "(a.creator_id = ? OR a.verantwortlich_id = ? OR a.erstellt_von = ?)", diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 472dc29e..b8ae0463 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -16,7 +16,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, + db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, } from "./workspace.js"; @@ -262,6 +262,7 @@ export function sichtbar(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ + if (istSpicy(person)) return { wo: ohneDogFather("e"), werte: [] }; if (siehtAlles(person)) return { wo: "1=1", werte: [] }; /* "ODER ich habe ihn selbst angelegt" kam am 02.09.2026 dazu -- aus diff --git a/server/workspace-chat.js b/server/workspace-chat.js index 3a8705e5..7ded71d7 100644 --- a/server/workspace-chat.js +++ b/server/workspace-chat.js @@ -88,13 +88,27 @@ function istDrin(raumId, personId) { } /** Alle (noch aktiven) Teilnehmer eines Raums. */ +/* Das Profilbild gehoert dazu (07.09.2026). Ohne es zeigt die + Gespraechsliste Initialen, obwohl ein Bild hochgeladen ist -- der + Fehler liegt dann nicht in der Anzeige, sondern in der Abfrage. Die + fertige ADRESSE wird danach gebaut, nicht der Dateiname + weitergereicht: Sonst setzt jede Stelle im Browser denselben Pfad + zusammen. + + UND DER KOMMENTAR STEHT AUSSERHALB DER ABFRAGE. Beim ersten Anlauf + stand er darin -- mit dem Wort `bild` in schraegen Anfuehrungszeichen. + Die beenden in JavaScript eine Vorlagenzeichenkette; die halbe SQL + wurde damit zu Programmtext, und die Datei liess sich nicht mehr + lesen. Ein Kommentar, der die Zeichenkette schliesst, in der er + steht. */ function teilnehmerVon(raumId) { return db().prepare(` - SELECT t.person_id AS id, p.name, p.rolle, t.leitung, t.gelesen_bis + SELECT t.person_id AS id, p.name, p.rolle, p.bild, t.leitung, t.gelesen_bis FROM chat_teilnehmer t JOIN personen p ON p.id = t.person_id WHERE t.raum_id = ? AND t.raus_am IS NULL - ORDER BY ` + ROLLEN_SORTIERUNG + ", p.name").all(raumId); + ORDER BY ` + ROLLEN_SORTIERUNG + ", p.name").all(raumId) + .map((z) => ({ ...z, bild: z.bild ? `/workspace/api/steckbrief/bild/${z.bild}` : null })); } /** Findet das Zweier-Gespräch der beiden -- oder legt es an. @@ -287,7 +301,8 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => { über `vor`. */ const vor = Number(req.query.vor) || 0; const reihen = db().prepare(` - SELECT n.id, n.person_id, n.text, n.erstellt, n.weg_am, p.name AS von, p.rolle + SELECT n.id, n.person_id, n.text, n.erstellt, n.weg_am, p.name AS von, p.rolle, + p.bild AS von_bild FROM chat_nachrichten n LEFT JOIN personen p ON p.id = n.person_id WHERE n.raum_id = ? ${vor ? "AND n.id < ?" : ""} @@ -300,11 +315,13 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => { raum: { id: raum.id, art: raum.art, name: raumName(raum, leute, req.person.id), - teilnehmer: leute.map((t) => ({ id: t.id, name: t.name, rolle: t.rolle, leitung: !!t.leitung })), + teilnehmer: leute.map((t) => ({ id: t.id, name: t.name, rolle: t.rolle, + bild: t.bild, leitung: !!t.leitung })), }, /* Aufsteigend zurück -- gelesen wird von oben nach unten. */ nachrichten: reihen.reverse().map((n) => ({ id: n.id, von_id: n.person_id, von: n.von || "Gelöscht", rolle: n.rolle, + von_bild: n.von_bild ? `/workspace/api/steckbrief/bild/${n.von_bild}` : null, text: n.weg_am ? null : n.text, zurueckgenommen: !!n.weg_am, erstellt: n.erstellt, diff --git a/server/workspace-dateien.js b/server/workspace-dateien.js index 253665b9..5ffeb585 100644 --- a/server/workspace-dateien.js +++ b/server/workspace-dateien.js @@ -29,7 +29,7 @@ import { randomBytes } from "node:crypto"; import { join, extname, basename } from "node:path"; import { mkdirSync, createReadStream, writeFileSync, unlinkSync, statSync } from "node:fs"; import { - db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, + db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, ROLLEN_SORTIERUNG, externSql, EXTERN_MAX, sichtbarePersonenIds, } from "./workspace.js"; @@ -79,6 +79,7 @@ export function sichtbar(person) { darf (freigeben, aendern, Personen verwalten), haengt weiterhin an istLeitung und bleibt unveraendert -- sonst haette dieser eine Wunsch stillschweigend seine halben Rechte mitgenommen. */ + if (istSpicy(person)) return { wo: ohneDogFather("d", ["creator_id", "hochgeladen_von"]), werte: [] }; if (siehtAlles(person)) return { wo: "1=1", werte: [] }; /* Ausdrueckliche Freigabe -- gilt fuer jede Rolle gleich. Wer in der diff --git a/server/workspace.js b/server/workspace.js index 47fa1249..99a968dc 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -112,6 +112,44 @@ export const istDogFather = (person) => !!person && person.rolle === "admin"; gar keinen Weg gibt -- nicht, weil eine Abfrage es verbietet. ======================================================================= */ export const istSpicy = (person) => !!person && person.rolle === "spicy"; + +/* ======================================================================= + SPICY MEDIA SIEHT ALLES -- AUSSER DEM, WAS DOGFATHER GEHOERT + (07.09.2026, Nachtrag am selben Tag) + + Filipe: "wieso sieht die spicy rolle meine daten die ich habe und + speichere, meins sollen die nicht sehen von der rolle dogfather." + + Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather: + `1=1`. Damit stimmte der Ueberblick ueber Manager, Scouts und Creator + -- und nebenbei standen DogFathers eigene Termine, Aufgaben, Dateien + und Eintraege mit drin. Der Chat war ausgenommen (er haengt an der + Teilnehmerliste), alles andere nicht. + + DIESE FUNKTION IST DIE EINE STELLE DAFUER. Sie baut die Bedingung + "gehoert keinem DogFather" fuer eine beliebige Tabelle. Jede + Sichtbarkeitsregel im Haus benutzt sie -- eine zweite, abgeschriebene + Fassung waere die Stelle, an der es beim naechsten Modul wieder + durchsickert. + + ZWEI SPALTEN, NICHT EINE. `creator_id` sagt, UM WEN es geht; + `erstellt_von` sagt, WER es geschrieben hat. Beide muessen gepruegt + werden: Ein Termin, den DogFather fuer sich selbst anlegt, hat + moeglicherweise gar keine creator_id -- er haenge dann nur an + erstellt_von. Und eine Akte UEBER einen DogFather traegt seine + creator_id, auch wenn ein Manager sie geschrieben hat. + + `IS NULL OR NOT IN` und nicht bloss `NOT IN`: In SQL ist + `NULL NOT IN (...)` weder wahr noch falsch, sondern NULL -- die Zeile + fiele stillschweigend heraus. Genau so verschwinden Daten, ohne dass + jemand einen Fehler sieht. + ======================================================================= */ +export function ohneDogFather(praefix, spalten = ["creator_id", "erstellt_von"]) { + return spalten + .map((sp) => `(${praefix}.${sp} IS NULL OR ${praefix}.${sp} NOT IN ` + + `(SELECT id FROM personen WHERE rolle = 'admin'))`) + .join(" AND "); +} export const siehtAlles = (person) => !!person && (person.rolle === "admin" || person.rolle === "spicy"); @@ -2329,6 +2367,7 @@ export const externSql = (personSpalte, externSpalte) => spaeter auseinandergelaufen -- und dann haette eine Wiederholung jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */ export function termineSichtbar(person, praefix = "t") { + if (istSpicy(person)) return { wo: ohneDogFather(praefix), werte: [] }; if (siehtAlles(person)) return { wo: "1=1", werte: [] }; const p = praefix; diff --git a/workspace/assets/css/chat.css b/workspace/assets/css/chat.css index bcae75c9..37faabb2 100644 --- a/workspace/assets/css/chat.css +++ b/workspace/assets/css/chat.css @@ -480,3 +480,159 @@ .chat-tag::before, .chat-tag::after { background: CanvasText; } .chat__verlauf { mask-image: none; } } + + +/* ===================================================================== + DER CHAT (07.09.2026) + + Wunsch Filipe: "mach die chat fenster auch viel geiler, viel + detaillierter und viel perfekter. ich will dass die leute auch bock + bekommen diesen chat zu benutzen." + + --------------------------------------------------------------------- + WORAN MAN EINEN GUTEN CHAT ERKENNT + + Nicht an Effekten. An vier Dingen, die alle mit ORIENTIERUNG zu tun + haben -- und alle vier fehlten: + + 1. MAN SIEHT, MIT WEM MAN SCHREIBT. Initialen sind ein Platzhalter, + kein Gesicht. Die Bilder liefert der Server seit heute mit; hier + werden sie gezeichnet -- rund, mit einem feinen Rand in der Farbe + der Rolle. + + 2. EINE NACHRICHT HAT EINE RICHTUNG. Eigene Nachrichten kommen von + rechts und tragen den Hausakzent, fremde von links und bleiben + neutral. Die Ecke am Anfang einer Folge ist eckig, die übrigen + rund -- so sieht man, wo ein Gedanke anfängt, ohne den Namen zu + lesen. + + 3. EIN GESPRÄCH HAT EIN GEWICHT. Ungelesenes ist nicht "auch da", + sondern das, weshalb man die Seite geöffnet hat: kräftigerer + Grund, farbige Schiene, die Zahl als Marke. + + 4. DAS EINGABEFELD IST DAS WICHTIGSTE ELEMENT DER SEITE. Es war + eines von vielen. Jetzt sitzt es in einer eigenen Leiste, die + sich beim Hineinschreiben hebt. + + Und was NICHT passiert ist: kein Blinken, kein Springen, keine + Verläufe im Text. Ein Chat wird stundenlang angesehen -- alles, was + einmal auffällt, nervt beim zwanzigsten Mal. + ===================================================================== */ + +/* ---------- Das Gesicht ---------------------------------------------- */ +.chat-raum__zeichen, +.chat-nachricht__zeichen { + position: relative; + overflow: hidden; + background: linear-gradient(150deg, + color-mix(in srgb, var(--rton, var(--akzent)) 30%, rgba(20, 27, 40, .9)), + rgba(12, 17, 26, .95)); + box-shadow: + inset 0 0 0 1px color-mix(in srgb, var(--rton, var(--akzent)) 38%, transparent), + 0 2px 6px -3px rgba(0, 0, 0, .8); +} +/* Die Farbe der Rolle -- dieselbe Zuordnung wie überall im Haus. */ +.chat-raum__zeichen[data-rolle="spicy"], +.chat-nachricht__zeichen[data-rolle="spicy"] { --rton: #e9848c; } +.chat-raum__zeichen[data-rolle="admin"], +.chat-nachricht__zeichen[data-rolle="admin"] { --rton: var(--k-gold); } +.chat-raum__zeichen[data-rolle="manager"], +.chat-nachricht__zeichen[data-rolle="manager"] { --rton: var(--k-mittel); } +.chat-raum__zeichen[data-rolle="scout"], +.chat-nachricht__zeichen[data-rolle="scout"] { --rton: var(--k-gut); } +.chat-raum__zeichen[data-rolle="creator"], +.chat-nachricht__zeichen[data-rolle="creator"] { --rton: var(--akzent); } + +/* Das Bild liegt ÜBER dem Buchstaben, nicht an seiner Stelle. Fällt es + aus, steht der Buchstabe weiterhin da. */ +.chat-raum__zeichen img, +.chat-nachricht__zeichen img { + position: absolute; inset: 0; + width: 100%; height: 100%; + object-fit: cover; + border-radius: inherit; +} + +/* ---------- Die Gesprächsliste --------------------------------------- */ +.chat-raum__knopf { + position: relative; + border-radius: 12px; + margin: 0 8px 4px; + transition: background var(--tempo), box-shadow var(--tempo); +} +/* Das offene Gespräch trägt eine Schiene -- man sieht auch beim Scrollen + der Liste, wo man gerade ist. */ +.chat-raum__knopf[data-an="ja"] { + background: linear-gradient(90deg, + color-mix(in srgb, var(--akzent) 16%, transparent), + rgba(255, 255, 255, .03) 55%); + box-shadow: inset 3px 0 0 var(--akzent); +} +/* Ungelesenes ist der Grund, warum man hier ist. */ +.chat-raum__knopf:has(.chat-raum__zahl) { + background: rgba(255, 255, 255, .045); +} + +/* ---------- Die Nachrichten ------------------------------------------ */ +.chat-nachricht__blase { + border-radius: 16px 16px 16px 5px; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .06), + 0 2px 6px -3px rgba(0, 0, 0, .55); +} +/* Die erste Blase einer Folge bekommt oben eine eckige Ecke -- dort + fängt der Gedanke an. Die folgenden sind rundum weich. */ +.chat-nachricht[data-folge="ja"] .chat-nachricht__blase { + border-top-left-radius: 8px; +} +.chat-nachricht[data-selbst="ja"] .chat-nachricht__blase { + border-radius: 16px 16px 5px 16px; + background: linear-gradient(160deg, + color-mix(in srgb, var(--akzent) 26%, rgba(17, 24, 37, .95)), + color-mix(in srgb, var(--akzent) 12%, rgba(12, 17, 27, .95))); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + 0 2px 8px -3px color-mix(in srgb, var(--akzent) 45%, transparent); +} +.chat-nachricht[data-selbst="ja"][data-folge="ja"] .chat-nachricht__blase { + border-top-right-radius: 8px; + border-top-left-radius: 16px; +} + +/* Der Tagestrenner: eine Linie mit dem Datum darin statt eines Wortes + im Nichts. */ +.chat-tag { + display: flex; align-items: center; gap: 12px; + margin: 18px 0 12px; +} +.chat-tag::before, .chat-tag::after { + content: ""; flex: 1 1 auto; height: 1px; + background: linear-gradient(90deg, transparent, rgba(150, 186, 220, .18), transparent); +} + +/* ---------- Das Eingabefeld ------------------------------------------ */ +/* Es ist das wichtigste Element der Seite und sah aus wie eines von + vielen. Eine eigene Leiste, die sich beim Hineinschreiben hebt. */ +/* Sie heisst .chat__eingabe (chat.html), nicht .chat-neu -- ich hatte + den Namen aus dem Kopf geschrieben. pruef-struktur meldete es als + totes CSS: eine Regel, die auf nichts zeigt, sieht aus wie erledigte + Arbeit und ist keine. */ +.chat__eingabe { + position: relative; + padding: 12px 14px; + border-top: 1px solid rgba(150, 186, 220, .12); + background: linear-gradient(180deg, rgba(13, 18, 28, .6), rgba(9, 12, 20, .92)); + transition: box-shadow var(--tempo); +} +.chat__eingabe:focus-within { + box-shadow: inset 0 1px 0 color-mix(in srgb, var(--akzent) 34%, transparent); +} + +@media (prefers-reduced-motion: reduce) { + .chat-raum__knopf, .chat__eingabe { transition: none; } +} +@media (forced-colors: active) { + .chat-nachricht__blase, + .chat-raum__knopf[data-an="ja"] { box-shadow: none; border: 1px solid CanvasText; } + .chat-raum__zeichen, .chat-nachricht__zeichen { background: Canvas; box-shadow: none; } +} diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index ca754260..02d1a794 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -1303,8 +1303,25 @@ body.gate { /* 1,50 statt 1,59: Bei 1280x800 ist die Tafel 512 px hoch, der Karteninhalt bei 1,59 aber 536 -- gemessen, nicht geschaetzt. 24 px Ueberstand sieht man an einem Neonrahmen sofort. */ - font-size: clamp(12px, 1.50cqh, 16px); - padding: 1.75em 1.6em 1.4em; + /* NACHGEMESSEN, WEIL EINE ROLLE DAZUKAM (07.09.2026). + + Der Kommentar darueber warnt woertlich davor -- und ich habe + genau das gemacht, wovor er warnt: eine fuenfte Rolle eingefuegt + und die Zahl daneben nicht angefasst. Gemessen bei fuenf Rollen: + Der Inhalt war bei 1280x800 573 px hoch in einer 526 px hohen + Tafel, bei 1600x1000 712 in 658 -- die Fusszeile stand jedes Mal + unter dem Rahmen. + + Die Schrift kleiner zu machen half nicht: Bei 1280 lag sie mit + 12 px schon auf dem unteren Anschlag. Also sind die ABSTAENDE + enger geworden -- an neun Stellen ein bis zwei Zehntel, das + ergibt zusammen rund 4 em. Die Schrift bleibt lesbar, die Karte + passt wieder. + + Wer eine SECHSTE Rolle einbaut, misst hier neu. Die Zahl gehoert + zu dem, was sie zaehlt. */ + font-size: clamp(12px, 1.42cqh, 15.5px); + padding: 1.35em 1.5em 1.05em; /* Die Tafel im Bild ist schon dunkel und hat ihren eigenen Rahmen. Die Karte legt sich deshalb NUR noch als leichter Schleier @@ -1326,21 +1343,21 @@ body.gate { weiterhin am Dokument orientieren und die Abstaende blieben stehen, waehrend die Schrift schrumpft. Genau daran erkennt man eine schlecht skalierte Oberflaeche. */ - .tafel .marke { font-size: .74em; margin-bottom: .5em; } - .tafel .titel { font-size: 1.85em; margin-bottom: .3em; } - .tafel .unterzeile { font-size: .92em; margin-bottom: 1.15em; } - .tafel .feldschild { font-size: .76em; margin-bottom: .6em; } - .tafel .rollen { margin-bottom: 1.1em; } - .tafel .rolle { padding: .62em .85em; margin-bottom: .42em; gap: .8em; } + .tafel .marke { font-size: .78em; margin-bottom: .35em; } + .tafel .titel { font-size: 1.78em; margin-bottom: .22em; } + .tafel .unterzeile { font-size: .9em; margin-bottom: .85em; } + .tafel .feldschild { font-size: .76em; margin-bottom: .45em; } + .tafel .rollen { margin-bottom: .8em; } + .tafel .rolle { padding: .50em .8em; margin-bottom: .30em; gap: .75em; } .tafel .rolle__zeichen { width: 1.32em; height: 1.32em; } .tafel .rolle__name { font-size: .94em; } .tafel .rolle__zusatz { font-size: .79em; } .tafel #code { padding: .72em 2.7em .72em .9em; } .tafel .feld__auge { width: 2.2em; height: 2.2em; padding: .5em; right: .35em; } - .tafel .knopf { margin-top: .75em; padding: .7em 1.1em; font-size: .88em; width: 100%; } + .tafel .knopf { margin-top: .6em; padding: .62em 1.1em; font-size: .88em; width: 100%; } .tafel .hinweis, .tafel .fehler { font-size: .82em; margin-top: .5em; } - .tafel .fuss { margin-top: 1.1em; padding-top: .8em; font-size: .76em; } + .tafel .fuss { margin-top: .75em; padding-top: .6em; font-size: .76em; } /* ---- Der Schleier ------------------------------------------------ Er verlief bisher von links dunkel nach rechts klar -- dort stand @@ -1774,8 +1791,21 @@ body.gate { display: flex; align-items: baseline; gap: .42em; flex-wrap: wrap; } -.marke--paar .marke__spicy { color: #e9848c; } -.marke--paar .marke__dogi { color: #63b8ee; } +/* KRAEFTIGER (07.09.2026, Wunsch Filipe: "spicy x dogfather sieht man + nicht richtig"). Die beiden Namen standen in gedeckten Toenen auf der + hellsten Stelle der Karte -- dort, wo der rote Schein von oben links + hereinkommt. Gedeckt ist auf dunklem Grund richtig und auf hellem zu + wenig. Jetzt heller, etwas groesser und mit einem eigenen Schatten, + der sie vom Untergrund abhebt statt sie zu ueberstrahlen. */ +.marke--paar { letter-spacing: .13em; } +.marke--paar .marke__spicy { + color: #ffb3b9; + text-shadow: 0 1px 10px rgba(255, 95, 109, .45); +} +.marke--paar .marke__dogi { + color: #a9dcff; + text-shadow: 0 1px 10px rgba(99, 184, 238, .45); +} .marke--paar .marke__x { /* Kleiner, leiser, und mit etwas Luft -- es verbindet, es benennt nicht. */ @@ -1784,3 +1814,48 @@ body.gate { color: var(--text-still); transform: translateY(-.02em); } + +/* ===================================================================== + DAS GESICHT IN DER PERSONENAUSWAHL (07.09.2026) + + Wunsch Filipe: "jeder soll auch immer das profilbild von denen sehen + mit denen sie verbunden sind." + + Ein rundes Bild in einer Auswahlliste ist der billigste und stärkste + Zugewinn, den eine solche Liste haben kann: Man trifft den richtigen + Namen, ohne ihn zu lesen. Und wer kein Bild hat, bekommt seinen + Anfangsbuchstaben -- kein leerer Kreis, kein Platzhaltersymbol. + + Der Buchstabe bleibt beim Laden des Bildes LIEGEN und wird nicht + ersetzt. Lädt das Bild nicht (Verbindung weg, Datei gelöscht), steht + dort weiterhin etwas Sinnvolles statt eines zerbrochenen Bildsymbols. + ===================================================================== */ +.wahl2__bild { + position: relative; + flex: none; + width: 26px; height: 26px; + border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + background: linear-gradient(150deg, rgba(63, 189, 245, .22), rgba(138, 118, 255, .18)); + box-shadow: inset 0 0 0 1px rgba(150, 186, 220, .22); + color: #dbe4ef; + /* .76rem = 12,16 px. Bei .7rem waren es 11,2 -- unter der Grenze von + 11,5, die pruef-css-klassen zieht. Ein einzelner Grossbuchstabe in + einem 26-px-Kreis wirkt ohnehin kleiner, als die Zahl sagt. */ + font-size: .76rem; font-weight: 700; line-height: 1; + overflow: hidden; +} +.wahl2__bild img { + position: absolute; inset: 0; + width: 100%; height: 100%; + object-fit: cover; + border-radius: 50%; +} +/* Der Name bekommt den restlichen Platz und wird gekürzt statt zu + umbrechen -- eine zweizeilige Zeile in einer Auswahlliste zerreißt + den Rhythmus, an dem das Auge entlangläuft. */ +.wahl2__name { + min-width: 0; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.wahl2__eintrag { display: flex; align-items: center; gap: 10px; } diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index b8bd334f..038e5c64 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -171,8 +171,24 @@ transition: border-color .16s ease, background .16s ease, color .16s ease; } .rollenwahl__knopf:hover { border-color: rgba(255, 255, 255, .22); color: var(--text); } +/* EIN ECHTER RAHMEN, NICHT NUR EIN SCHATTEN (07.09.2026). + + Hier stand `outline: none` und stattdessen ein box-shadow. Das sieht + weicher aus und hat einen Haken: Ein Schatten wird von jedem + `overflow: hidden` eines Elternteils abgeschnitten -- und dann ist + der Fokus unsichtbar, ohne dass es irgendwo auffaellt. Genau das hat + pruef-barrierefrei gemeldet, und zwar NUR beim Manager: Er sieht auf + dieser Seite genau einen Rollenknopf, und der stand am Rand seines + Behaelters. + + Jetzt beides: der Rahmen sorgt dafuer, dass man ihn IMMER sieht, der + Schatten dafuer, dass es gut aussieht. Wer mit der Tastatur arbeitet, + ist auf diesen Rahmen angewiesen -- er ist der einzige Hinweis + darauf, wo er gerade steht. */ .rollenwahl__knopf:focus-visible { - outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--r) 32%, transparent); + outline: 2px solid color-mix(in srgb, var(--r) 85%, #ffffff); + outline-offset: 2px; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--r) 32%, transparent); } /* Eine Farbe je Rolle -- dieselbe Sprache wie überall sonst. */ diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 1a665020..f3e45da2 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -772,6 +772,14 @@ Das laesst sich nicht rueckgaengig machen. Erledigte Aufgaben sollten normalerwe for (const ziel of ['f-creator', 'f-verant', 'b-creator', 'b-verant']) { const o = el('option', null, `${p.name} (${p.rolle})`); o.value = String(p.id); + /* Bild ans