Chat: sechs Ergaenzungen -- und ein Absturz, den nie jemand gesehen hat
Filipe: "die kategorie chat, boah ich will dass du die wirklich ueberall perfektionierst. wirklich alles was gratis ist und hinzugefuegt werden kann ohne problem will ich drin haben bitte. ich will das es hoch profissionel und ultra krass geil ist." Zuerst Bestand aufgenommen, nicht gebaut: Suche, Ungelesen-Zahlen, die "ab hier neu"-Linie, Lesestand, Live-Strom, Antworten mit Zitat, Zuruecknehmen und Enter-zum-Senden gab es schon. Dazugekommen ist, was gefehlt hat und ohne fremde Bibliothek geht: 1. ADRESSEN SIND ANKLICKBAR. Gebaut als echte Knoten, nie ueber innerHTML -- ein Chat ist die eine Stelle, an der jeder schreiben darf. Erkannt wird bewusst wenig (http, https, www.): Wer mehr erkennt, macht irgendwann aus "z.b." einen Link ins Nichts. Satzzeichen am Ende bleiben beim Satz. rel="noopener noreferrer". 2. KOPIEREN je Nachricht, mit Rueckfall auf "Text markieren", wenn der Browser die Zwischenablage nicht hergibt. 3. DER VERLAUF REISST NIEMANDEN MEHR WEG. Vorher sprang er bei JEDEM Neuzeichnen ans Ende -- wer weiter oben nachlas und dabei eine Nachricht bekam, verlor die Stelle. Jetzt bleibt er stehen, und ein Knopf sagt, WIE VIEL unten wartet, nicht nur DASS etwas da ist. 4. ENTWUERFE JE GESPRAECH. Wer mitten im Satz wechselt, findet ihn wieder. Im localStorage, nicht auf dem Server: Ein Entwurf ist nichts, was jemand anders sehen soll. Abgeschickt heisst geloescht. 5. EMOJI-AUSWAHL, dreissig feste Zeichen, eingefuegt an der Cursorstelle. Am Handy hat die Tastatur sie ohnehin -- am Rechner nicht, und dort sitzt die Betreuung. 6. ZEICHENZAEHLER, sichtbar ab 400 Rest. Die Grenze steht NICHT im Skript, sondern kommt aus dem maxlength des Feldes -- zwei Stellen fuer dieselbe Zahl laufen auseinander. --- DER FUND, um den es eigentlich geht --- Die neue Pruefung hoert auf "pageerror". Damit kam sofort neun Mal dieselbe Meldung: "Cannot set properties of null (setting 'hidden')", raumOeffnen, Zeile 278. Ursache: verlaufZeichnen() leerte den Verlaufskasten mit `textContent = ''`. Darin liegt aber #verlauf-leer, der Absatz "Links ein Gespraech auswaehlen". Nach dem ersten Zeichnen gab es ihn nicht mehr, und raumOeffnen faellt sechs Zeilen weiter darueber. Die Folge ist nicht die Fehlermeldung, sondern der Abbruch: Beim ZWEITEN Aufruf liefen history.replaceState, gelesenMelden() und raeumeZeichnen() nie. Der zweite Aufruf ist der Normalfall -- der Ereignisstrom ruft nach jedem Verbindungsaufbau genau das, um nachzuholen, was waehrend der Pause geschrieben wurde. Dieses Nachholen hat nie funktioniert. Von aussen sah alles normal aus; nur die Konsole wusste Bescheid. Behoben, indem nur noch die Nachrichten entfernt werden und der Absatz stehenbleibt. Abschnitt 8 der Pruefung wechselt jetzt dreimal zwischen zwei Gespraechen OHNE Neuladen und verlangt null Abstuerze. --- UND EIN ZWEITER, der schon laenger drin war --- Abschnitt 9 misst den Kontrast an der wirklichen Flaeche (Text kurz unsichtbar, Flaeche fotografiert). Die Fusszeile jeder Nachricht stand auf --text-still: auf der eigenen Blase 3,68:1, unter den 4,5:1 fuer Text dieser Groesse. Aufgefallen ist es nie, weil niemand nachgemessen hat. Jetzt traegt .chat-nachricht__fuss EINE Farbe (--text-leise, 4,81:1) und Uhrzeit, antworten, zuruecknehmen und kopieren erben sie -- statt vier eigener Angaben, die beim naechsten Mal auseinanderlaufen. --- Pruefung --- server/pruef-chat-ausbau.mjs, neu, 64 Pruefungen, alle gruen. Zu jedem Punkt eine Gegenprobe: kein Link ohne Adresse, keine Auszeichnung aus <b>/<img onerror>, kein Kopier-Knopf an zurueckgenommenen Nachrichten, kein Sprungknopf fuer den, der schon unten steht, kein Entwurf nach dem Abschicken, ein absichtlich zu dunkler Text faellt durch, und ein absichtlich geworfener Fehler wird bemerkt (sonst waere Abschnitt 10 auch gruen, wenn niemand zuhoert). Ausserdem gruen: pruef-css-klassen, pruef-chat, pruef-chat-optik. Angesehen bei 1440 px und bei 390 px. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
/* CHAT — DER AUSBAU VOM 09.09.2026
|
||||
|
||||
Filipe: "die kategorie chat, boah ich will dass du die wirklich
|
||||
überall perfektionierst. wirklich alles was gratis ist und
|
||||
hinzugefügt werden kann ohne problem will ich drin haben bitte."
|
||||
|
||||
Sechs Dinge sind dazugekommen. Diese Prüfung misst sie einzeln, an
|
||||
einem echten Browser, mit zwei angemeldeten Personen:
|
||||
|
||||
1. Adressen im Text sind anklickbar -- und zwar als echte Knoten,
|
||||
nicht über innerHTML. Ein Chat ist die eine Stelle, an der
|
||||
JEDER schreiben darf; hier wäre eine Einschleusung am teuersten.
|
||||
2. Jede Nachricht lässt sich kopieren.
|
||||
3. Der Verlauf reisst niemanden mehr weg, der weiter oben liest --
|
||||
stattdessen sagt ein Knopf, wie viel unten wartet.
|
||||
4. Ein angefangener Satz überlebt den Wechsel des Gesprächs.
|
||||
5. Eine Emoji-Auswahl, die an der Cursorstelle einfügt.
|
||||
6. Ein Zeichenzähler, der erst erscheint, wenn es eng wird.
|
||||
|
||||
ZU JEDEM PUNKT EINE GEGENPROBE. Eine Prüfung, die immer bestätigt,
|
||||
bestätigt nichts -- deshalb steht neben jedem "es passiert" auch ein
|
||||
"und es passiert NICHT, wenn es nicht soll".
|
||||
|
||||
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
||||
|
||||
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-chatausbau-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||
await portMussFreiSein(4319, "die Chat-Ausbaupruefung");
|
||||
|
||||
process.env.PORT = "4319";
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||
|
||||
const express = (await import("express")).default;
|
||||
const ec = express.response.cookie;
|
||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||||
await import("./index.js");
|
||||
notbremse(480_000, "pruef-chat-ausbau");
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
const BASIS = "http://127.0.0.1:4319";
|
||||
|
||||
const PROTOKOLL = join(process.cwd(), "pruef-chat-ausbau-lauf.txt");
|
||||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||||
const melde = (z) => { console.log(z); try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ } };
|
||||
let fehler = 0, geprueft = 0;
|
||||
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
|
||||
/* ---- Bestand ------------------------------------------------------------ */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
||||
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
||||
}
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
function anlegen(name, rolle, code) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||||
.run(name, rolle, hash, salt, 32768, jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||||
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
|
||||
d.close();
|
||||
|
||||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||
const browser = await pw.chromium.launch();
|
||||
|
||||
async function alsRolle(rolle, code, breite = 1280) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: breite, height: 900 },
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
});
|
||||
const seite = await ctx.newPage();
|
||||
const konsole = [];
|
||||
/* MIT HERKUNFT. Eine Meldung ohne Datei und Zeile kostet die halbe
|
||||
Suche -- genau das ist mir beim ersten Lauf passiert. */
|
||||
seite.on("pageerror", (f) => konsole.push("Absturz auf " + seite.url() + ": " + f.message
|
||||
+ " || " + String(f.stack || "").split(/\r?\n/).slice(1, 4).join(" / ")));
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||||
await seite.fill("#code", code);
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
||||
await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(900);
|
||||
return { ctx, seite, konsole };
|
||||
}
|
||||
|
||||
/* Eine Nachricht auf dem kurzen Weg -- über die echte Schnittstelle der
|
||||
Seite, damit Herkunftsprüfung und Sitzung mitlaufen. */
|
||||
async function schicke(s, raum, text) {
|
||||
return s.evaluate(async ([r, t]) => {
|
||||
const a = await fetch(`/workspace/api/chat/raeume/${r}/nachrichten`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: t }),
|
||||
});
|
||||
return a.ok;
|
||||
}, [raum, text]);
|
||||
}
|
||||
|
||||
const luna = await alsRolle("creator", "CODE-CREA-0001");
|
||||
const pat = await alsRolle("scout", "CODE-SCOU-0001");
|
||||
|
||||
/* Zwei Gespräche: eines mit Patrick, eines mit DogFather. Das zweite
|
||||
braucht es für die Entwürfe -- ohne Wechsel kein Wechseltest. */
|
||||
async function direkt(s, mit) {
|
||||
return s.evaluate(async (m) => {
|
||||
const a = await fetch("/workspace/api/chat/direkt", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mit: m }) });
|
||||
return (await a.json()).raum_id;
|
||||
}, mit);
|
||||
}
|
||||
const raumId = await direkt(luna.seite, idPat);
|
||||
const raumDogi = await direkt(luna.seite, idDogi);
|
||||
ok(Number.isInteger(raumId) && raumId > 0, `das Gespräch mit Patrick steht (Raum ${raumId})`);
|
||||
ok(Number.isInteger(raumDogi) && raumDogi !== raumId, `und eines mit DogFather (Raum ${raumDogi})`);
|
||||
|
||||
async function oeffne(s, id) {
|
||||
await s.goto(`${BASIS}/workspace/chat.html?raum=${id}`, { waitUntil: "networkidle" });
|
||||
await s.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||||
await s.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
1. ADRESSEN IM TEXT
|
||||
======================================================================= */
|
||||
melde("\n=== 1. Adressen im Text ===");
|
||||
const MIT_LINKS = "Schau mal https://workspace.dogfather-universe.com/start.html "
|
||||
+ "und www.dogfather-universe.com, danke!";
|
||||
{
|
||||
await schicke(luna.seite, raumId, MIT_LINKS);
|
||||
await oeffne(luna.seite, raumId);
|
||||
|
||||
const links = await luna.seite.$$eval(".chat-nachricht:last-of-type .chat-nachricht__link",
|
||||
(a) => a.map((x) => ({ text: x.textContent, href: x.href, ziel: x.target, rel: x.rel })));
|
||||
ok(links.length === 2, `beide Adressen sind Links (${links.length} gefunden)`);
|
||||
ok(links[0]?.href === "https://workspace.dogfather-universe.com/start.html",
|
||||
`die erste zeigt dorthin, wo sie steht (${links[0]?.href})`);
|
||||
ok(links[1]?.href === "https://www.dogfather-universe.com/",
|
||||
`bei "www." wird https ergänzt (${links[1]?.href})`);
|
||||
ok(links[1]?.text === "www.dogfather-universe.com",
|
||||
`das Komma dahinter gehört zum Satz, nicht zur Adresse ("${links[1]?.text}")`);
|
||||
ok(links.every((l) => l.ziel === "_blank"), "sie gehen in einen neuen Tab");
|
||||
ok(links.every((l) => /noopener/.test(l.rel) && /noreferrer/.test(l.rel)),
|
||||
`rel="noopener noreferrer" steht dran (${links[0]?.rel})`);
|
||||
|
||||
/* NICHTS DARF VERLOREN GEHEN. Beim Zerlegen eines Textes in Stücke ist
|
||||
genau das der Fehler, den man nicht sieht: ein Zeichen zu viel oder
|
||||
zu wenig an einer Nahtstelle. */
|
||||
const ganz = await luna.seite.$eval(".chat-nachricht:last-of-type .chat-nachricht__text",
|
||||
(p) => p.textContent);
|
||||
ok(ganz === MIT_LINKS, "der Text ist Zeichen für Zeichen derselbe geblieben");
|
||||
|
||||
/* GEGENPROBE A: ohne Adresse kein Link. */
|
||||
await schicke(luna.seite, raumId, "Hier steht z.b. gar keine Adresse, drei.punkte inklusive.");
|
||||
await oeffne(luna.seite, raumId);
|
||||
const ohne = await luna.seite.$$eval(".chat-nachricht:last-of-type .chat-nachricht__link", (a) => a.length);
|
||||
ok(ohne === 0, `ein Satz ohne Adresse bekommt keinen Link (${ohne})`);
|
||||
|
||||
/* GEGENPROBE B: Auszeichnung wird NICHT ausgeführt. Das ist der
|
||||
eigentliche Grund, warum hier nicht innerHTML steht. */
|
||||
const BOESE = '<b>fett</b> <img src=x onerror="alert(1)"> & "Zitat"';
|
||||
await schicke(luna.seite, raumId, BOESE);
|
||||
await oeffne(luna.seite, raumId);
|
||||
const roh = await luna.seite.$eval(".chat-nachricht:last-of-type .chat-nachricht__text",
|
||||
(p) => ({ text: p.textContent, kinder: p.querySelectorAll("b, img, script").length }));
|
||||
ok(roh.kinder === 0, `keine eingeschleuste Auszeichnung (${roh.kinder} Fremdelemente)`);
|
||||
ok(roh.text === BOESE, "der Text steht als Text da");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. KOPIEREN
|
||||
======================================================================= */
|
||||
melde("\n=== 2. Kopieren ===");
|
||||
{
|
||||
await oeffne(luna.seite, raumId);
|
||||
const anzahl = await luna.seite.$$eval(".chat-nachricht__kopie-knopf", (b) => b.length);
|
||||
const wieviel = await luna.seite.$$eval(".chat-nachricht", (n) => n.length);
|
||||
ok(anzahl === wieviel && anzahl >= 3,
|
||||
`jede der ${wieviel} Nachrichten hat einen Kopier-Knopf (${anzahl})`);
|
||||
|
||||
await luna.seite.click(".chat-nachricht:first-of-type .chat-nachricht__kopie-knopf");
|
||||
await luna.seite.waitForTimeout(300);
|
||||
const inZwischenablage = await luna.seite.evaluate(() => navigator.clipboard.readText());
|
||||
ok(inZwischenablage === MIT_LINKS,
|
||||
`der Text liegt in der Zwischenablage (${inZwischenablage.length} Zeichen)`);
|
||||
const beschriftung = await luna.seite.$eval(
|
||||
".chat-nachricht:first-of-type .chat-nachricht__kopie-knopf", (b) => b.textContent);
|
||||
ok(beschriftung === "kopiert", `der Knopf bestätigt sich selbst ("${beschriftung}")`);
|
||||
await luna.seite.waitForTimeout(1800);
|
||||
const zurueck = await luna.seite.$eval(
|
||||
".chat-nachricht:first-of-type .chat-nachricht__kopie-knopf", (b) => b.textContent);
|
||||
ok(zurueck === "kopieren", `und fällt danach zurück ("${zurueck}")`);
|
||||
|
||||
/* GEGENPROBE: An einer zurückgenommenen Nachricht gibt es nichts zu
|
||||
kopieren -- dort darf der Knopf nicht stehen. */
|
||||
luna.seite.once("dialog", (dlg) => dlg.accept());
|
||||
await luna.seite.click(".chat-nachricht:last-of-type .chat-nachricht__weg-knopf");
|
||||
await luna.seite.waitForTimeout(900);
|
||||
const anWeg = await luna.seite.$$eval('.chat-nachricht[data-weg="ja"] .chat-nachricht__kopie-knopf',
|
||||
(b) => b.length);
|
||||
const wegDa = await luna.seite.$$eval('.chat-nachricht[data-weg="ja"]', (b) => b.length);
|
||||
ok(wegDa === 1 && anWeg === 0,
|
||||
`an der zurückgenommenen Nachricht steht kein Kopier-Knopf (${wegDa} weg, ${anWeg} Knöpfe)`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. DER VERLAUF REISST NIEMANDEN MEHR WEG
|
||||
======================================================================= */
|
||||
melde("\n=== 3. Zum neuesten ===");
|
||||
{
|
||||
for (let i = 1; i <= 40; i++) await schicke(pat.seite, raumId, `Zeile ${i} zum Vollmachen des Verlaufs.`);
|
||||
await oeffne(luna.seite, raumId);
|
||||
await oeffne(pat.seite, raumId);
|
||||
|
||||
const lang = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight > e.clientHeight + 200);
|
||||
ok(lang, "der Verlauf ist lang genug, um überhaupt scrollen zu können");
|
||||
|
||||
const amEnde = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight - e.scrollTop - e.clientHeight < 40);
|
||||
const knopfZu = await luna.seite.$eval("#zum-neuesten", (e) => e.hidden);
|
||||
ok(amEnde && knopfZu, "beim Öffnen steht er unten, und der Knopf ist weg");
|
||||
|
||||
/* Luna liest weiter oben. */
|
||||
await luna.seite.$eval("#verlauf", (e) => { e.scrollTop = 0; });
|
||||
await luna.seite.waitForTimeout(400);
|
||||
const vorher = await luna.seite.$eval("#verlauf", (e) => e.scrollTop);
|
||||
|
||||
await schicke(pat.seite, raumId, "Und noch etwas Neues dazu.");
|
||||
await luna.seite.waitForFunction(
|
||||
() => !document.getElementById("zum-neuesten").hidden, { timeout: 8000 })
|
||||
.then(() => true).catch(() => false);
|
||||
const nachher = await luna.seite.$eval("#verlauf", (e) => e.scrollTop);
|
||||
ok(Math.abs(nachher - vorher) < 3, `die Stelle bleibt stehen (${vorher} -> ${nachher})`);
|
||||
const text1 = await luna.seite.$eval("#zum-neuesten", (e) => e.textContent);
|
||||
ok(text1 === "1 neue Nachricht", `der Knopf sagt, wie viel wartet ("${text1}")`);
|
||||
|
||||
await schicke(pat.seite, raumId, "Und noch eine.");
|
||||
await luna.seite.waitForFunction(
|
||||
() => document.getElementById("zum-neuesten").textContent === "2 neue Nachrichten",
|
||||
{ timeout: 8000 }).then(() => true).catch(() => false);
|
||||
const text2 = await luna.seite.$eval("#zum-neuesten", (e) => e.textContent);
|
||||
ok(text2 === "2 neue Nachrichten", `er zählt mit ("${text2}")`);
|
||||
|
||||
await luna.seite.click("#zum-neuesten");
|
||||
await luna.seite.waitForTimeout(400);
|
||||
const jetztUnten = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight - e.scrollTop - e.clientHeight < 40);
|
||||
const wiederZu = await luna.seite.$eval("#zum-neuesten", (e) => e.hidden);
|
||||
ok(jetztUnten && wiederZu, "ein Klick bringt ans Ende, und der Knopf verschwindet");
|
||||
|
||||
/* GEGENPROBE: Wer schon unten steht, wird weiterhin mitgenommen --
|
||||
sonst hätte ich die eine Unart gegen die andere getauscht. */
|
||||
await schicke(pat.seite, raumId, "Eine letzte, während Luna unten steht.");
|
||||
await luna.seite.waitForFunction(
|
||||
() => [...document.querySelectorAll(".chat-nachricht__text")]
|
||||
.some((t) => /während Luna unten steht/.test(t.textContent)), { timeout: 8000 })
|
||||
.then(() => true).catch(() => false);
|
||||
await luna.seite.waitForTimeout(400);
|
||||
const bleibtUnten = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight - e.scrollTop - e.clientHeight < 40);
|
||||
const bleibtZu = await luna.seite.$eval("#zum-neuesten", (e) => e.hidden);
|
||||
ok(bleibtUnten && bleibtZu, "wer unten steht, wird weiterhin mitgenommen — ohne Knopf");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
4. DER ANGEFANGENE SATZ
|
||||
======================================================================= */
|
||||
melde("\n=== 4. Entwürfe ===");
|
||||
{
|
||||
await oeffne(luna.seite, raumId);
|
||||
await luna.seite.fill("#text", "Halber Satz an Patrick");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
await oeffne(luna.seite, raumDogi);
|
||||
const leerDrueben = await luna.seite.$eval("#text", (e) => e.value);
|
||||
ok(leerDrueben === "", `im anderen Gespräch ist das Feld leer ("${leerDrueben}")`);
|
||||
|
||||
await luna.seite.fill("#text", "Und hier etwas ganz anderes");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
await oeffne(luna.seite, raumId);
|
||||
const wieder = await luna.seite.$eval("#text", (e) => e.value);
|
||||
ok(wieder === "Halber Satz an Patrick", `der Satz ist wieder da ("${wieder}")`);
|
||||
await oeffne(luna.seite, raumDogi);
|
||||
const wieder2 = await luna.seite.$eval("#text", (e) => e.value);
|
||||
ok(wieder2 === "Und hier etwas ganz anderes", "und jedes Gespräch behält seinen eigenen");
|
||||
|
||||
/* GEGENPROBE: Abgeschickt heisst weg. Eine Ablage, die nur wächst,
|
||||
wäre ein Leck -- und beim nächsten Öffnen stünde der Satz doppelt. */
|
||||
await oeffne(luna.seite, raumId);
|
||||
await luna.seite.click("#senden");
|
||||
await luna.seite.waitForTimeout(700);
|
||||
const feldLeer = await luna.seite.$eval("#text", (e) => e.value);
|
||||
const gespeichert = await luna.seite.evaluate((r) => localStorage.getItem(`dfw-chat-entwurf-${r}`), raumId);
|
||||
ok(feldLeer === "" && gespeichert === null,
|
||||
`nach dem Abschicken ist der Entwurf weg (Ablage: ${gespeichert})`);
|
||||
await oeffne(luna.seite, raumId);
|
||||
const nachSenden = await luna.seite.$eval("#text", (e) => e.value);
|
||||
ok(nachSenden === "", "und er kommt beim nächsten Öffnen nicht zurück");
|
||||
|
||||
/* Und ein geleertes Feld löscht ihn auch. */
|
||||
await oeffne(luna.seite, raumDogi);
|
||||
await luna.seite.fill("#text", "");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
const weg = await luna.seite.evaluate((r) => localStorage.getItem(`dfw-chat-entwurf-${r}`), raumDogi);
|
||||
ok(weg === null, `wer sein Feld leert, hat auch keinen Entwurf mehr (${weg})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
5. EMOJI
|
||||
======================================================================= */
|
||||
melde("\n=== 5. Emoji ===");
|
||||
{
|
||||
await oeffne(luna.seite, raumId);
|
||||
const zuAmAnfang = await luna.seite.$eval("#emoji-tafel", (e) => e.hidden);
|
||||
const auf1 = await luna.seite.$eval("#emoji-auf", (e) => e.getAttribute("aria-expanded"));
|
||||
ok(zuAmAnfang && auf1 === "false", "die Tafel ist zu, bis jemand sie aufmacht");
|
||||
|
||||
await luna.seite.click("#emoji-auf");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
const offenJetzt = await luna.seite.$eval("#emoji-tafel", (e) => !e.hidden);
|
||||
const auf2 = await luna.seite.$eval("#emoji-auf", (e) => e.getAttribute("aria-expanded"));
|
||||
const wieviele = await luna.seite.$$eval(".emoji-tafel__knopf", (b) => b.length);
|
||||
ok(offenJetzt && auf2 === "true", "ein Klick macht sie auf, und die Vorlesehilfe weiss es");
|
||||
ok(wieviele === 30, `dreissig Zeichen stehen zur Auswahl (${wieviele})`);
|
||||
const beschriftet = await luna.seite.$$eval(".emoji-tafel__knopf",
|
||||
(b) => b.every((x) => (x.getAttribute("aria-label") || "").length > 6));
|
||||
ok(beschriftet, "jedes hat eine Beschriftung für das Vorleseprogramm");
|
||||
|
||||
/* GEGENPROBE: Der Knopf, mit dem sie aufgeht, darf sie nicht sofort
|
||||
wieder zumachen -- genau das wäre der naheliegende Fehler bei einem
|
||||
Zuhörer auf dem ganzen Dokument. */
|
||||
ok(offenJetzt, "der eigene Knopf schliesst sie nicht versehentlich gleich wieder");
|
||||
|
||||
/* AN DER CURSORSTELLE, nicht am Ende. */
|
||||
await luna.seite.fill("#text", "Hallo Welt");
|
||||
await luna.seite.$eval("#text", (e) => { e.focus(); e.selectionStart = e.selectionEnd = 5; });
|
||||
const zeichen = await luna.seite.$eval(".emoji-tafel__knopf:nth-child(11)", (b) => b.textContent);
|
||||
await luna.seite.click(".emoji-tafel__knopf:nth-child(11)");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
const danach = await luna.seite.$eval("#text", (e) => ({ wert: e.value, pos: e.selectionStart }));
|
||||
ok(danach.wert === "Hallo" + zeichen + " Welt", `es landet an der Cursorstelle ("${danach.wert}")`);
|
||||
ok(danach.pos === 5 + zeichen.length, `und der Cursor steht dahinter (${danach.pos})`);
|
||||
|
||||
/* Sie steht ÜBER dem Feld und ragt nicht seitlich hinaus. */
|
||||
const lage = await luna.seite.evaluate(() => {
|
||||
const t = document.getElementById("emoji-tafel").getBoundingClientRect();
|
||||
const f = document.getElementById("text").getBoundingClientRect();
|
||||
return { l: t.left, r: t.right, u: t.bottom, o: f.top, breite: innerWidth };
|
||||
});
|
||||
ok(lage.u <= lage.o + 2, `sie liegt über dem Schreibfeld (${lage.u.toFixed(0)} <= ${lage.o.toFixed(0)})`);
|
||||
ok(lage.l >= 0 && lage.r <= lage.breite,
|
||||
`und ragt nicht seitlich hinaus (${lage.l.toFixed(0)}–${lage.r.toFixed(0)} in ${lage.breite})`);
|
||||
|
||||
/* Escape und ein Klick daneben schliessen sie. */
|
||||
/* Escape dort, wo der Finger gerade ist -- nach dem Aufmachen steht
|
||||
die Schreibmarke auf dem ersten Zeichen der Tafel. */
|
||||
await luna.seite.keyboard.press("Escape");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
ok(await luna.seite.$eval("#emoji-tafel", (e) => e.hidden), "Escape schliesst sie");
|
||||
await luna.seite.click("#emoji-auf");
|
||||
await luna.seite.waitForTimeout(200);
|
||||
await luna.seite.click("#verlauf-titel");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
const zuNachKlick = await luna.seite.$eval("#emoji-tafel", (e) => e.hidden);
|
||||
const auf3 = await luna.seite.$eval("#emoji-auf", (e) => e.getAttribute("aria-expanded"));
|
||||
ok(zuNachKlick && auf3 === "false", "ein Klick daneben ebenfalls");
|
||||
await luna.seite.fill("#text", "");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
6. ZEICHENZÄHLER
|
||||
======================================================================= */
|
||||
melde("\n=== 6. Zeichenzähler ===");
|
||||
{
|
||||
const grenze = await luna.seite.$eval("#text", (e) => Number(e.getAttribute("maxlength")));
|
||||
ok(grenze === 4000, `die Grenze steht am Feld selbst (${grenze})`);
|
||||
|
||||
await luna.seite.fill("#text", "x".repeat(100));
|
||||
await luna.seite.waitForTimeout(200);
|
||||
ok(await luna.seite.$eval("#zeichen", (e) => e.hidden), "bei 100 Zeichen sagt er nichts");
|
||||
|
||||
await luna.seite.fill("#text", "x".repeat(grenze - 300));
|
||||
await luna.seite.waitForTimeout(250);
|
||||
const nah = await luna.seite.$eval("#zeichen", (e) => ({ zu: e.hidden, text: e.textContent, eng: e.dataset.eng }));
|
||||
ok(!nah.zu && nah.text === "noch 300", `ab 400 Rest meldet er sich ("${nah.text}")`);
|
||||
ok(nah.eng === "nein", "und ist dabei noch nicht rot");
|
||||
|
||||
await luna.seite.fill("#text", "x".repeat(grenze - 40));
|
||||
await luna.seite.waitForTimeout(250);
|
||||
const eng = await luna.seite.$eval("#zeichen", (e) => ({ text: e.textContent, eng: e.dataset.eng }));
|
||||
ok(eng.text === "noch 40" && eng.eng === "ja", `zum Schluss wird er deutlich ("${eng.text}")`);
|
||||
|
||||
/* GEGENPROBE: Er verschwindet auch wieder. Ein Hinweis, der einmal
|
||||
erschienen ist und dann stehen bleibt, ist ein Fehler. */
|
||||
await luna.seite.fill("#text", "kurz");
|
||||
await luna.seite.waitForTimeout(250);
|
||||
ok(await luna.seite.$eval("#zeichen", (e) => e.hidden), "und geht wieder weg, wenn wieder Platz ist");
|
||||
await luna.seite.fill("#text", "");
|
||||
await luna.seite.waitForTimeout(200);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7. AM HANDY — und nichts ragt hinaus
|
||||
======================================================================= */
|
||||
melde("\n=== 7. Am Handy (390 px) ===");
|
||||
{
|
||||
const handy = await alsRolle("creator", "CODE-CREA-0001", 390);
|
||||
await handy.seite.goto(`${BASIS}/workspace/chat.html?raum=${raumId}`, { waitUntil: "networkidle" });
|
||||
await handy.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||||
await handy.seite.waitForTimeout(700);
|
||||
|
||||
await handy.seite.click("#emoji-auf");
|
||||
await handy.seite.waitForTimeout(300);
|
||||
const m = await handy.seite.evaluate(() => {
|
||||
const t = document.getElementById("emoji-tafel").getBoundingClientRect();
|
||||
return { l: t.left, r: t.right, doku: document.documentElement.scrollWidth, sicht: innerWidth };
|
||||
});
|
||||
ok(m.l >= 0 && m.r <= 390, `die Tafel bleibt im Bild (${m.l.toFixed(0)}–${m.r.toFixed(0)})`);
|
||||
ok(m.doku <= m.sicht + 1, `die Seite hat keinen seitlichen Überstand (${m.doku} zu ${m.sicht})`);
|
||||
|
||||
/* Und die Knöpfe sind gross genug zum Treffen. Unter 38 px tippt man
|
||||
daneben -- das ist keine Zierde, das ist Bedienbarkeit. */
|
||||
const klein = await handy.seite.$$eval("#emoji-auf, .chat__senden",
|
||||
(b) => b.map((x) => ({ n: x.id || x.className,
|
||||
h: Math.round(x.getBoundingClientRect().height),
|
||||
w: Math.round(x.getBoundingClientRect().width) }))
|
||||
.filter((x) => x.h < 38 || x.w < 38));
|
||||
ok(klein.length === 0, `Emoji- und Senden-Knopf sind gross genug (${JSON.stringify(klein)})`);
|
||||
await handy.ctx.close();
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
8. ZWISCHEN GESPRÄCHEN WECHSELN, OHNE NEU ZU LADEN
|
||||
|
||||
Diese Prüfung gab es nicht, und deshalb war der Fehler drin: Der
|
||||
Absatz "Links ein Gespräch auswählen" liegt IM Verlaufskasten, und
|
||||
der wurde beim Zeichnen komplett geleert. Ab dem zweiten Öffnen war
|
||||
`#verlauf-leer` weg -- `raumOeffnen` stürzte dort ab und ließ den
|
||||
ganzen Rest liegen: Adresszeile, Lesemeldung, Liste.
|
||||
|
||||
Von aussen sah das normal aus. Nur der Ereignisstrom holte nach
|
||||
einem Verbindungsabriss nichts mehr nach, und die ungelesen-Zahl
|
||||
blieb beim Wechseln stehen.
|
||||
======================================================================= */
|
||||
melde("\n=== 8. Wechseln ohne Neuladen ===");
|
||||
{
|
||||
await luna.seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
||||
await luna.seite.waitForSelector(".chat-raum__knopf", { timeout: 8000 });
|
||||
await luna.seite.waitForTimeout(500);
|
||||
const vorher = luna.konsole.length;
|
||||
|
||||
const zuPatrick = luna.seite.locator(".chat-raum__knopf", { hasText: "Patrick" }).first();
|
||||
const zuDogi = luna.seite.locator(".chat-raum__knopf", { hasText: "Filipe" }).first();
|
||||
|
||||
await zuPatrick.click();
|
||||
await luna.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||||
await luna.seite.waitForTimeout(400);
|
||||
ok(/raum=/.test(luna.seite.url()), `die Adresszeile merkt sich das Gespräch (${luna.seite.url().split("/").pop()})`);
|
||||
|
||||
await zuDogi.click();
|
||||
await luna.seite.waitForTimeout(700);
|
||||
const titel2 = await luna.seite.$eval("#verlauf-titel", (e) => e.textContent);
|
||||
ok(titel2 === "Filipe", `der zweite Wechsel kommt an ("${titel2}")`);
|
||||
|
||||
await zuPatrick.click();
|
||||
await luna.seite.waitForTimeout(700);
|
||||
const titel3 = await luna.seite.$eval("#verlauf-titel", (e) => e.textContent);
|
||||
const zahl = await luna.seite.$$eval(".chat-nachricht", (n) => n.length);
|
||||
ok(titel3 === "Patrick" && zahl > 5,
|
||||
`und der dritte auch, samt Verlauf ("${titel3}", ${zahl} Nachrichten)`);
|
||||
ok(/raum=/.test(luna.seite.url()), "die Adresszeile geht beim Wechseln mit");
|
||||
ok(luna.konsole.length === vorher,
|
||||
`dabei kein einziger Absturz (${luna.konsole.length - vorher} neue Meldungen)`);
|
||||
|
||||
/* GEGENPROBE: Der Absatz, über den es gestolpert ist, muss noch da
|
||||
sein -- und zwar versteckt, nicht gelöscht. Wäre er weg, ginge es
|
||||
beim nächsten Umbau wieder los. */
|
||||
const leer = await luna.seite.evaluate(() => {
|
||||
const e = document.getElementById("verlauf-leer");
|
||||
return e ? { da: true, zu: e.hidden, drin: e.parentElement.id } : { da: false };
|
||||
});
|
||||
ok(leer.da && leer.zu && leer.drin === "verlauf",
|
||||
`"#verlauf-leer" steht noch im Kasten und ist versteckt (${JSON.stringify(leer)})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
9. AUGENSCHONEND — gemessen, nicht behauptet
|
||||
|
||||
Vier neue Textstellen sind dazugekommen. Jede davon ist klein
|
||||
(0,72 rem) und liegt auf einem durchscheinenden Grund, hinter dem ein
|
||||
Bild steht -- die Sorte Stelle, an der Kontrast gern verlorengeht,
|
||||
ohne dass es beim Hinsehen auffällt.
|
||||
|
||||
Gemessen wird gegen die WIRKLICHE Fläche dahinter: Der Text wird kurz
|
||||
unsichtbar gemacht und die Fläche fotografiert. Ein Blick in die
|
||||
CSS-Datei würde nur die oberste Schicht sehen.
|
||||
======================================================================= */
|
||||
melde("\n=== 9. Augenschonend ===");
|
||||
{
|
||||
const leuchtkraft = (r, g, b) => {
|
||||
const f = (w) => { const v = w / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
||||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||||
};
|
||||
const kontrast = (a, b) => {
|
||||
const l1 = leuchtkraft(...a), l2 = leuchtkraft(...b);
|
||||
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
};
|
||||
|
||||
/* Der Text wird unsichtbar gemacht, NICHT entfernt -- ein entferntes
|
||||
Element ändert das Layout, und dann wird an der falschen Stelle
|
||||
gemessen. Und die Farbe wird ZUERST abgeschrieben: getComputedStyle
|
||||
liefert ein lebendes Objekt, das sonst gleich mit auf
|
||||
"transparent" umspringt. */
|
||||
async function messen(waehler) {
|
||||
const stelle = await luna.seite.evaluate((w) => {
|
||||
const e = document.querySelector(w);
|
||||
if (!e) return null;
|
||||
e.scrollIntoView({ block: "center" });
|
||||
const r = e.getBoundingClientRect();
|
||||
if (r.width < 8 || r.height < 6) return null;
|
||||
const farbe = getComputedStyle(e).color.match(/\d+/g).slice(0, 3).map(Number);
|
||||
e.dataset.messen = "ja";
|
||||
e.style.color = "transparent";
|
||||
return { x: Math.round(r.x), y: Math.round(r.y),
|
||||
w: Math.round(r.width), h: Math.round(Math.min(r.height, 40)), farbe };
|
||||
}, waehler);
|
||||
if (!stelle) return null;
|
||||
const bild = await luna.seite.screenshot({
|
||||
clip: { x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h } });
|
||||
await luna.seite.evaluate(() => {
|
||||
const e = document.querySelector('[data-messen="ja"]');
|
||||
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
||||
});
|
||||
const hinten = await luna.seite.evaluate(async (b64) => {
|
||||
const bild = new Image();
|
||||
await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; });
|
||||
const c = document.createElement("canvas");
|
||||
c.width = bild.width; c.height = bild.height;
|
||||
const g = c.getContext("2d", { willReadFrequently: true });
|
||||
g.drawImage(bild, 0, 0);
|
||||
const dd = g.getImageData(0, 0, c.width, c.height).data;
|
||||
let r = 0, gr = 0, bl = 0, n = 0;
|
||||
for (let i = 0; i < dd.length; i += 4) { r += dd[i]; gr += dd[i + 1]; bl += dd[i + 2]; n++; }
|
||||
return n ? [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)] : null;
|
||||
}, bild.toString("base64"));
|
||||
if (!hinten) return null;
|
||||
return { farbe: stelle.farbe, hinten, wert: kontrast(stelle.farbe, hinten) };
|
||||
}
|
||||
|
||||
/* Alle vier gleichzeitig sichtbar machen: den Zähler über die Länge,
|
||||
den Sprungknopf über den Bildlauf. */
|
||||
await luna.seite.fill("#text", "x".repeat(3800));
|
||||
await luna.seite.$eval("#verlauf", (e) => { e.scrollTop = 0; });
|
||||
await luna.seite.waitForTimeout(400);
|
||||
await luna.seite.evaluate(() => { document.getElementById("zum-neuesten").hidden = false; });
|
||||
await luna.seite.waitForTimeout(200);
|
||||
|
||||
for (const [name, waehler] of [
|
||||
["Zeichenzähler", "#zeichen"],
|
||||
["Kopier-Knopf", ".chat-nachricht__kopie-knopf"],
|
||||
["Antwort-Knopf (Vergleich)", ".chat-nachricht__antwort-knopf"],
|
||||
["Zum-neuesten-Knopf", "#zum-neuesten"],
|
||||
["Link in einer Nachricht", ".chat-nachricht__link"],
|
||||
]) {
|
||||
const m = await messen(waehler);
|
||||
ok(m !== null && m.wert >= 4.5,
|
||||
`${name}: ${m ? m.wert.toFixed(2) + ":1 (" + m.farbe.join(",") + " auf " + m.hinten.join(",") + ")" : "nicht gefunden"}`);
|
||||
}
|
||||
|
||||
/* GEGENPROBE: Ein absichtlich zu dunkler Text MUSS durchfallen --
|
||||
sonst misst der Block oben nichts. */
|
||||
await luna.seite.evaluate(() => {
|
||||
const e = document.getElementById("zeichen");
|
||||
e.dataset.probe = "ja"; e.style.color = "#20262e";
|
||||
});
|
||||
const schlecht = await messen("#zeichen");
|
||||
ok(schlecht !== null && schlecht.wert < 4.5,
|
||||
`absichtlich zu dunkler Text fällt durch (${schlecht?.wert.toFixed(2)}:1)`);
|
||||
await luna.seite.evaluate(() => { document.getElementById("zeichen").style.color = ""; });
|
||||
await luna.seite.fill("#text", "");
|
||||
}
|
||||
|
||||
melde("\n=== 10. Stille Fehler ===");
|
||||
{
|
||||
const alle = [...luna.konsole, ...pat.konsole];
|
||||
ok(alle.length === 0, alle.length ? "Fehler in der Konsole: " + alle.join(" | ")
|
||||
: "keine Skript- oder Konsolenfehler in beiden Browsern");
|
||||
|
||||
/* GEGENPROBE: Der Lauscher greift überhaupt. Ohne diesen Nachweis
|
||||
wäre der Haken darüber wertlos -- er wäre auch grün, wenn niemand
|
||||
zuhört. */
|
||||
await luna.seite.evaluate(() => { setTimeout(() => { throw new Error("Absicht"); }, 0); });
|
||||
await luna.seite.waitForTimeout(500);
|
||||
ok(luna.konsole.some((z) => /Absicht/.test(z)),
|
||||
`ein absichtlicher Fehler wird bemerkt (${luna.konsole.length} Meldung(en))`);
|
||||
}
|
||||
|
||||
await luna.ctx.close();
|
||||
await pat.ctx.close();
|
||||
await browser.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
|
||||
melde(`\n${geprueft} Prüfungen`);
|
||||
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||||
process.exit(fehler ? 1 : 0);
|
||||
@@ -321,12 +321,28 @@
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
/* EINE FARBE FÜR DIE GANZE FUSSZEILE — und zwar hier, an einer Stelle.
|
||||
---------------------------------------------------------------------
|
||||
Uhrzeit, "antworten", "zurücknehmen" und "kopieren" standen alle auf
|
||||
var(--text-still) (#7f8fa6). Gemessen (pruef-chat-ausbau, Abschnitt 9)
|
||||
ergibt das auf der eigenen Blase 3,68:1 -- unter den 4,5:1, die für
|
||||
Text dieser Größe gelten. Es ist nie aufgefallen, weil niemand
|
||||
nachgemessen hat; die Blase ist heller als der übrige Grund, und
|
||||
genau dort wird eine leise Farbe zu leise.
|
||||
|
||||
var(--text-leise) (#94a5bb) bringt dieselbe Stelle auf 4,80:1 und
|
||||
bleibt trotzdem deutlich ruhiger als der Nachrichtentext.
|
||||
|
||||
Die Kinder erben sie (color: inherit). Vier eigene Farbangaben wären
|
||||
vier Stellen, die beim nächsten Mal auseinanderlaufen -- der Fehler,
|
||||
der in diesem Haus am häufigsten vorkommt. */
|
||||
.chat-nachricht__fuss {
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.chat-nachricht__zeit {
|
||||
font-size: .75rem; color: var(--text-still);
|
||||
font-size: .75rem; color: inherit;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* Der Zurücknehmen-Knopf erscheint erst beim Darüberfahren -- er ist
|
||||
@@ -335,7 +351,7 @@
|
||||
.chat-nachricht__weg-knopf {
|
||||
padding: 0; border: 0; background: none;
|
||||
font: inherit; font-size: .75rem;
|
||||
color: var(--text-still); cursor: pointer;
|
||||
color: inherit; cursor: pointer;
|
||||
opacity: 0; transition: opacity .16s ease, color .16s ease;
|
||||
}
|
||||
.chat-nachricht:hover .chat-nachricht__weg-knopf,
|
||||
@@ -790,7 +806,124 @@
|
||||
.chat-nachricht__antwort-knopf {
|
||||
border: 0; background: none; padding: 0;
|
||||
font: inherit; font-size: .72rem;
|
||||
color: var(--text-still); cursor: pointer;
|
||||
color: inherit; cursor: pointer;
|
||||
text-decoration: underline; text-underline-offset: 2px;
|
||||
}
|
||||
.chat-nachricht__antwort-knopf:hover { color: var(--text-leise); }
|
||||
.chat-nachricht__antwort-knopf:hover { color: var(--text); }
|
||||
|
||||
/* =====================================================================
|
||||
CHAT: SECHS ERGÄNZUNGEN (09.09.2026)
|
||||
|
||||
Filipe: "die kategorie chat, boah ich will dass du die wirklich
|
||||
überall perfektionierst. wirklich alles was gratis ist und
|
||||
hinzugefügt werden kann ohne problem will ich drin haben."
|
||||
|
||||
Alle sechs kommen ohne fremde Bibliothek aus und laden nichts nach:
|
||||
Links, Kopieren, Emoji-Auswahl, Zeichenzähler, ein Bildlauf der nicht
|
||||
mehr wegreisst, und Entwürfe je Gespräch (dafür braucht es kein CSS).
|
||||
===================================================================== */
|
||||
|
||||
/* Ein Link in einer Nachricht ist unterstrichen, nicht farbig -- eine
|
||||
Blase hat schon eine Farbe, und zwei Farben in einer Blase lesen sich
|
||||
wie zwei Sachen. */
|
||||
.chat-nachricht__link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
|
||||
word-break: break-word;
|
||||
}
|
||||
.chat-nachricht__link:hover { text-decoration-color: currentColor; }
|
||||
.chat-nachricht__link:focus-visible {
|
||||
outline: 2px solid var(--akzent); outline-offset: 2px; border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Der Kopier-Knopf sieht aus wie "antworten" -- es ist dieselbe Sorte
|
||||
Handgriff an derselben Stelle. Ein anders gestalteter Knopf daneben
|
||||
waere eine zweite Sprache in derselben Zeile. */
|
||||
.chat-nachricht__kopie-knopf {
|
||||
appearance: none; border: 0; background: none; padding: 0;
|
||||
font: inherit; font-size: .72rem; font-weight: 600;
|
||||
color: inherit; cursor: pointer;
|
||||
text-decoration: underline; text-underline-offset: 2px;
|
||||
}
|
||||
.chat-nachricht__kopie-knopf:hover { color: var(--text); }
|
||||
.chat-nachricht__kopie-knopf:focus-visible {
|
||||
outline: 2px solid var(--akzent); outline-offset: 2px; border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ---- Emoji ---------------------------------------------------------- */
|
||||
.chat__emoji {
|
||||
flex: none;
|
||||
width: 40px; height: 40px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
appearance: none; border: 1px solid rgba(150, 186, 220, .18);
|
||||
border-radius: 11px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
color: var(--text-leise);
|
||||
font-size: 1.15rem; line-height: 1; cursor: pointer;
|
||||
transition: color var(--tempo), border-color var(--tempo);
|
||||
}
|
||||
.chat__emoji:hover { color: var(--text); border-color: rgba(150, 186, 220, .34); }
|
||||
.chat__emoji:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
|
||||
|
||||
/* Die Tafel steht ÜBER der Eingabe, nicht darunter: Darunter läge sie
|
||||
auf dem Handy hinter der Bildschirmtastatur. */
|
||||
.emoji-tafel {
|
||||
position: absolute;
|
||||
right: 14px; bottom: calc(100% - 6px);
|
||||
z-index: 5;
|
||||
display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;
|
||||
padding: 10px;
|
||||
border-radius: 14px;
|
||||
background: rgba(8, 12, 20, .97);
|
||||
border: 1px solid rgba(150, 186, 220, .2);
|
||||
box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .9);
|
||||
}
|
||||
.emoji-tafel__knopf {
|
||||
appearance: none; border: 0; background: none;
|
||||
width: 34px; height: 34px; border-radius: 8px;
|
||||
font-size: 1.15rem; line-height: 1; cursor: pointer;
|
||||
transition: background var(--tempo);
|
||||
}
|
||||
.emoji-tafel__knopf:hover { background: rgba(255, 255, 255, .1); }
|
||||
.emoji-tafel__knopf:focus-visible { outline: 2px solid var(--akzent); outline-offset: -2px; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.emoji-tafel { left: 14px; right: 14px; grid-template-columns: repeat(8, 1fr); }
|
||||
}
|
||||
|
||||
/* ---- Zeichenzähler --------------------------------------------------- */
|
||||
.chat__zeichen {
|
||||
flex: none; align-self: center;
|
||||
font-size: .72rem; font-weight: 700; letter-spacing: .04em;
|
||||
color: var(--text-still);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* Eng heisst rot -- aber erst dann. Ein dauerhaft roter Zähler ist eine
|
||||
Warnung, die niemand mehr liest. */
|
||||
.chat__zeichen[data-eng="ja"] { color: var(--warn, #ff9aa2); }
|
||||
|
||||
/* ---- Zum neuesten ---------------------------------------------------- */
|
||||
.chat__zum-neuesten {
|
||||
position: absolute; left: 50%; bottom: 96px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 4;
|
||||
padding: 9px 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid color-mix(in srgb, var(--akzent) 40%, transparent);
|
||||
background: rgba(8, 12, 20, .95);
|
||||
color: var(--text);
|
||||
font: inherit; font-size: .8rem; font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 26px -12px rgba(0, 0, 0, .9);
|
||||
}
|
||||
.chat__zum-neuesten:hover { border-color: var(--akzent); }
|
||||
.chat__zum-neuesten:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chat__emoji, .emoji-tafel__knopf { transition: none; }
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.emoji-tafel, .chat__zum-neuesten { border: 1px solid CanvasText; background: Canvas; }
|
||||
}
|
||||
|
||||
+255
-4
@@ -278,7 +278,14 @@
|
||||
$('verlauf-leer').hidden = true;
|
||||
$('chat').dataset.ansicht = 'verlauf';
|
||||
|
||||
/* Ein frisch geoeffnetes Gespraech steht immer am Ende -- man will
|
||||
das Letzte sehen, nicht das Erste. */
|
||||
unten = true; neuUnten = 0;
|
||||
verlaufZeichnen();
|
||||
/* Der Entwurf von vorhin, falls einer da ist. */
|
||||
$('text').value = entwurfHolen(id);
|
||||
hoeheAnpassen();
|
||||
zaehlerZeigen();
|
||||
if (!sanft) $('text').focus();
|
||||
/* Die Adresszeile merkt sich das Gespräch -- so führt die
|
||||
Benachrichtigung direkt hierher, und ein Neuladen bleibt am
|
||||
@@ -290,7 +297,28 @@
|
||||
|
||||
function verlaufZeichnen() {
|
||||
const ziel = $('verlauf');
|
||||
ziel.textContent = '';
|
||||
/* NUR DIE NACHRICHTEN WEG — NICHT DER GANZE KASTEN.
|
||||
------------------------------------------------------------------
|
||||
Hier stand `ziel.textContent = ''`. Das loeschte auch
|
||||
`#verlauf-leer`, den Absatz "Links ein Gespraech auswaehlen", denn
|
||||
der liegt IN diesem Kasten. Nach dem ersten Zeichnen gab es ihn
|
||||
also nicht mehr — und `raumOeffnen` faellt sechs Zeilen weiter
|
||||
genau darueber:
|
||||
|
||||
$('verlauf-leer').hidden = true; -> null
|
||||
|
||||
Folge, und die ist die eigentliche: `raumOeffnen` bricht an dieser
|
||||
Stelle ab. Damit laufen `history.replaceState`, `gelesenMelden()`
|
||||
und `raeumeZeichnen()` beim ZWEITEN Aufruf nie. Der zweite Aufruf
|
||||
ist aber der Normalfall — der Ereignisstrom ruft nach jedem
|
||||
Verbindungsaufbau genau das (`{ sanft: true }`), um nachzuholen,
|
||||
was waehrend der Pause geschrieben wurde. Dieses Nachholen hat
|
||||
nie funktioniert; der Absturz stand still in der Konsole.
|
||||
|
||||
Gefunden hat das nicht das Auge, sondern der Lauscher auf
|
||||
`pageerror` in pruef-chat-ausbau.mjs. Ohne ihn waere es geblieben:
|
||||
Die Seite sieht danach voellig normal aus. */
|
||||
for (const alt of [...ziel.children]) if (alt.id !== 'verlauf-leer') alt.remove();
|
||||
if (!nachrichten.length) {
|
||||
ziel.append(el('p', 'chat__leer', 'Noch nichts geschrieben. Fang an.'));
|
||||
return;
|
||||
@@ -386,7 +414,7 @@
|
||||
/* textContent, nie innerHTML: Eine Nachricht darf keine
|
||||
Auszeichnung einschleusen. Zeilenumbrüche macht das CSS
|
||||
(white-space: pre-wrap). */
|
||||
blase.append(el('p', 'chat-nachricht__text', n.text));
|
||||
blase.append(mitLinks(n.text));
|
||||
}
|
||||
|
||||
const fuss = el('div', 'chat-nachricht__fuss');
|
||||
@@ -410,11 +438,95 @@
|
||||
weg.addEventListener('click', () => zuruecknehmen(n.id));
|
||||
fuss.append(weg);
|
||||
}
|
||||
/* KOPIEREN. Eine Adresse, ein Code, eine Uhrzeit -- man will sie
|
||||
weitergeben, nicht abtippen. Der Knopf bestaetigt sich selbst
|
||||
und faellt danach zurueck; eine Meldung anderswo waere ein
|
||||
zweiter Ort, an dem man nachsehen muesste. */
|
||||
if (!n.zurueckgenommen && n.text) {
|
||||
const kopie = el('button', 'chat-nachricht__kopie-knopf', 'kopieren');
|
||||
kopie.type = 'button';
|
||||
kopie.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(n.text);
|
||||
kopie.textContent = 'kopiert';
|
||||
} catch {
|
||||
/* Ohne Zwischenablage-Recht (aeltere Browser, kein HTTPS)
|
||||
den Text wenigstens auswaehlen -- dann geht Strg+C. */
|
||||
const p = kopie.closest('.chat-nachricht')?.querySelector('.chat-nachricht__text');
|
||||
if (p) { const r = document.createRange(); r.selectNodeContents(p);
|
||||
const sel = getSelection(); sel.removeAllRanges(); sel.addRange(r); }
|
||||
kopie.textContent = 'markiert';
|
||||
}
|
||||
setTimeout(() => { kopie.textContent = 'kopieren'; }, 1600);
|
||||
});
|
||||
fuss.append(kopie);
|
||||
}
|
||||
blase.append(fuss);
|
||||
zeile.append(blase);
|
||||
ziel.append(zeile);
|
||||
}
|
||||
ziel.scrollTop = ziel.scrollHeight;
|
||||
|
||||
/* NUR NACH UNTEN SPRINGEN, WENN MAN SCHON UNTEN WAR.
|
||||
|
||||
Vorher sprang der Verlauf bei JEDEM Neuzeichnen ans Ende. Wer
|
||||
weiter oben etwas nachlas und dabei eine neue Nachricht bekam,
|
||||
wurde mitten im Satz weggerissen -- und das passiert im Live-
|
||||
Strom staendig. Jetzt gilt: Wer unten steht, bleibt unten
|
||||
(das ist der Normalfall). Wer liest, bleibt stehen und bekommt
|
||||
einen Knopf, der sagt, wie viel unten wartet. */
|
||||
if (unten) {
|
||||
ziel.scrollTop = ziel.scrollHeight;
|
||||
neuUnten = 0;
|
||||
}
|
||||
springKnopfZeigen();
|
||||
}
|
||||
|
||||
/* Steht der Verlauf am Ende? 40 Pixel Nachsicht -- ein Bildlauf trifft
|
||||
die letzte Zeile selten auf den Punkt, und ein Knopf, der bei einem
|
||||
Pixel Abstand erscheint, ist ein Flackern. */
|
||||
let unten = true;
|
||||
let neuUnten = 0;
|
||||
function amEnde(e) { return e.scrollHeight - e.scrollTop - e.clientHeight < 40; }
|
||||
|
||||
function springKnopfZeigen() {
|
||||
const k = $('zum-neuesten');
|
||||
if (!k) return;
|
||||
k.hidden = unten || !nachrichten.length;
|
||||
k.textContent = neuUnten
|
||||
? (neuUnten === 1 ? '1 neue Nachricht' : `${neuUnten} neue Nachrichten`)
|
||||
: 'Zum neuesten';
|
||||
}
|
||||
|
||||
/* ---------- Links in Nachrichten (09.09.2026) ---------------------------
|
||||
Eine Adresse im Text soll anklickbar sein. Gebaut wird sie als
|
||||
ECHTER Knoten, nie ueber innerHTML: Sonst koennte ein Absender
|
||||
Auszeichnung einschleusen, und in einem Chat schreibt jeder.
|
||||
|
||||
Erkannt wird bewusst wenig -- http, https und www. Wer mehr
|
||||
erkennt (Mailadressen, nackte Domains), erwischt irgendwann
|
||||
"3.stelle" oder "z.b." und macht daraus einen Link ins Nichts.
|
||||
|
||||
`rel="noopener noreferrer"` ist Pflicht: Ohne `noopener` kann die
|
||||
geoeffnete Seite auf das Fenster zurueckgreifen, aus dem sie kam. */
|
||||
const LINKMUSTER = /(https?:\/\/[^\s<>"']+|www\.[^\s<>"']+)/g;
|
||||
function mitLinks(text) {
|
||||
const p = el('p', 'chat-nachricht__text');
|
||||
let letzte = 0;
|
||||
for (const treffer of String(text).matchAll(LINKMUSTER)) {
|
||||
const roh = treffer[0];
|
||||
/* Satzzeichen am Ende gehoeren zum Satz, nicht zur Adresse. */
|
||||
const sauber = roh.replace(/[.,;:!?)\]]+$/, '');
|
||||
if (treffer.index > letzte) p.append(document.createTextNode(text.slice(letzte, treffer.index)));
|
||||
const a = el('a', 'chat-nachricht__link', sauber);
|
||||
a.href = sauber.startsWith('www.') ? 'https://' + sauber : sauber;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener noreferrer';
|
||||
p.append(a);
|
||||
if (sauber.length < roh.length) p.append(document.createTextNode(roh.slice(sauber.length)));
|
||||
letzte = treffer.index + roh.length;
|
||||
}
|
||||
if (letzte < text.length) p.append(document.createTextNode(text.slice(letzte)));
|
||||
return p;
|
||||
}
|
||||
|
||||
/* ---------- Antworten und Springen (09.09.2026, Punkt 15) ---------------- */
|
||||
@@ -482,8 +594,13 @@
|
||||
: null,
|
||||
};
|
||||
nachrichten.push(vorlaeufig);
|
||||
/* Wer selbst schreibt, will das Ergebnis sehen -- auch wenn er
|
||||
vorher weiter oben gelesen hat. */
|
||||
unten = true; neuUnten = 0;
|
||||
verlaufZeichnen();
|
||||
feld.value = '';
|
||||
entwurfWeg(offen.id);
|
||||
zaehlerZeigen();
|
||||
antwortLoeschen();
|
||||
hoeheAnpassen();
|
||||
melde('');
|
||||
@@ -608,6 +725,9 @@
|
||||
}
|
||||
|
||||
nachrichten.push({ ...n, selbst: n.von_id === ich.id });
|
||||
/* Wer gerade weiter oben liest, soll sehen, WIE VIEL unten
|
||||
dazugekommen ist -- nicht nur, DASS etwas da ist. */
|
||||
if (!unten && n.von_id !== ich.id) neuUnten++;
|
||||
verlaufZeichnen();
|
||||
gelesenMelden();
|
||||
}
|
||||
@@ -789,7 +909,138 @@
|
||||
|
||||
|
||||
$('schreiben').addEventListener('submit', abschicken);
|
||||
$('text').addEventListener('input', hoeheAnpassen);
|
||||
$('text').addEventListener('input', () => {
|
||||
hoeheAnpassen();
|
||||
entwurfMerken();
|
||||
zaehlerZeigen();
|
||||
});
|
||||
|
||||
/* ---------- Entwurf je Gespraech (09.09.2026) ----------------------------
|
||||
Wer mitten im Satz das Gespraech wechselt, findet den Satz beim
|
||||
Zurueckkommen wieder. Ohne das ist jeder Wechsel eine Entscheidung:
|
||||
"wegwerfen oder erst fertig schreiben?"
|
||||
|
||||
Im localStorage und nicht auf dem Server: Ein Entwurf ist nichts,
|
||||
was jemand anders sehen soll, und er gehoert an dieses Geraet.
|
||||
Abgeschickte oder geleerte Entwuerfe werden geloescht -- eine
|
||||
Ablage, die nur waechst, ist ein Leck. */
|
||||
const ENTWURF = (id) => `dfw-chat-entwurf-${id}`;
|
||||
function entwurfMerken() {
|
||||
if (!offen) return;
|
||||
const t = $('text').value;
|
||||
try {
|
||||
if (t.trim()) localStorage.setItem(ENTWURF(offen.id), t);
|
||||
else localStorage.removeItem(ENTWURF(offen.id));
|
||||
} catch { /* privater Modus: dann eben nicht */ }
|
||||
}
|
||||
function entwurfHolen(raumId) {
|
||||
try { return localStorage.getItem(ENTWURF(raumId)) || ''; } catch { return ''; }
|
||||
}
|
||||
function entwurfWeg(raumId) {
|
||||
try { localStorage.removeItem(ENTWURF(raumId)); } catch { /* egal */ }
|
||||
}
|
||||
|
||||
/* ---------- Zeichenzaehler ------------------------------------------------
|
||||
Er erscheint erst, wenn es eng wird. Ein Zaehler, der von Anfang an
|
||||
mitlaeuft, zaehlt die meiste Zeit etwas, das niemanden interessiert --
|
||||
und wird dann auch dann uebersehen, wenn er zaehlt. */
|
||||
/* Die Zahl steht NICHT hier. Sie steht im maxlength des Feldes, und
|
||||
dort steht sie, weil der Server sie so haben will (TEXT_MAX = 4000
|
||||
in workspace-chat.js). Zwei Stellen fuer dieselbe Zahl laufen
|
||||
auseinander -- dann zaehlt der Zaehler bis 4000 herunter, waehrend
|
||||
das Feld schon bei 2000 dichtmacht, und niemand versteht warum. */
|
||||
const GRENZE = Number($('text').getAttribute('maxlength')) || 4000;
|
||||
function zaehlerZeigen() {
|
||||
const z = $('zeichen');
|
||||
if (!z) return;
|
||||
const n = $('text').value.length;
|
||||
const rest = GRENZE - n;
|
||||
z.hidden = rest > 400;
|
||||
z.textContent = `noch ${rest}`;
|
||||
z.dataset.eng = rest <= 100 ? 'ja' : 'nein';
|
||||
}
|
||||
|
||||
/* ---------- Emoji-Auswahl -------------------------------------------------
|
||||
Eine kleine, feste Auswahl statt einer vollstaendigen Tafel: Diese
|
||||
dreissig decken den Alltag ab, und sie laden nichts nach. Eine
|
||||
Emoji-Bibliothek waere ein Megabyte fuer etwas, das die
|
||||
Bildschirmtastatur ohnehin kann -- am Rechner aber nicht.
|
||||
|
||||
Eingefuegt wird an der CURSORSTELLE, nicht am Ende: Wer mitten im
|
||||
Satz eines setzen will, soll nicht erst umsortieren. */
|
||||
const EMOJIS = ['😀','😂','🥹','😍','😎','🤔','😅','🙃','😴','🤝',
|
||||
'👍','👏','🙌','💪','🙏','🔥','✨','⭐','💯','🎉',
|
||||
'❤️','💙','💚','🖤','👀','🎮','🎧','📈','⏰','✅'];
|
||||
function emojiTafelBauen() {
|
||||
const tafel = $('emoji-tafel');
|
||||
if (!tafel || tafel.childElementCount) return;
|
||||
for (const e of EMOJIS) {
|
||||
const b = el('button', 'emoji-tafel__knopf', e);
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-label', `Emoji ${e} einfügen`);
|
||||
b.addEventListener('click', () => {
|
||||
const f = $('text');
|
||||
const a = f.selectionStart ?? f.value.length;
|
||||
const b2 = f.selectionEnd ?? a;
|
||||
f.value = f.value.slice(0, a) + e + f.value.slice(b2);
|
||||
f.focus();
|
||||
f.selectionStart = f.selectionEnd = a + e.length;
|
||||
hoeheAnpassen(); entwurfMerken(); zaehlerZeigen();
|
||||
});
|
||||
tafel.append(b);
|
||||
}
|
||||
}
|
||||
$('emoji-auf')?.addEventListener('click', () => {
|
||||
const tafel = $('emoji-tafel');
|
||||
emojiTafelBauen();
|
||||
const auf = tafel.hidden;
|
||||
tafel.hidden = !auf;
|
||||
$('emoji-auf').setAttribute('aria-expanded', String(auf));
|
||||
if (auf) tafel.querySelector('button')?.focus();
|
||||
});
|
||||
/* Ein Klick daneben schliesst die Tafel -- sonst steht sie im Weg,
|
||||
und man sucht den Knopf, mit dem man sie aufgemacht hat. */
|
||||
document.addEventListener('click', (e) => {
|
||||
const tafel = $('emoji-tafel');
|
||||
if (!tafel || tafel.hidden) return;
|
||||
if (tafel.contains(e.target) || e.target.closest('#emoji-auf')) return;
|
||||
tafel.hidden = true;
|
||||
$('emoji-auf')?.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
/* Escape schliesst sie -- und zwar von ueberall her, nicht nur wenn
|
||||
die Schreibmarke zufaellig noch in der Tafel steht.
|
||||
|
||||
Erster Anlauf hing der Zuhoerer an der Tafel selbst. Das geht gut,
|
||||
solange man sie gerade aufgemacht hat (dann liegt der Fokus auf dem
|
||||
ersten Zeichen). Wer danach ins Schreibfeld zurueckklickt, drueckt
|
||||
Escape aber ins Leere -- und die Tafel steht weiter im Weg. Am
|
||||
Dokument gilt es immer; ist die Tafel zu, tut die Zeile nichts. */
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
const tafel = $('emoji-tafel');
|
||||
if (!tafel || tafel.hidden) return;
|
||||
tafel.hidden = true;
|
||||
$('emoji-auf')?.setAttribute('aria-expanded', 'false');
|
||||
$('text').focus();
|
||||
});
|
||||
|
||||
/* ---------- Zum neuesten springen ---------------------------------------- */
|
||||
$('verlauf')?.addEventListener('scroll', () => {
|
||||
const v = $('verlauf');
|
||||
const jetztUnten = amEnde(v);
|
||||
if (jetztUnten !== unten) {
|
||||
unten = jetztUnten;
|
||||
if (unten) neuUnten = 0;
|
||||
springKnopfZeigen();
|
||||
}
|
||||
});
|
||||
$('zum-neuesten')?.addEventListener('click', () => {
|
||||
const v = $('verlauf');
|
||||
v.scrollTop = v.scrollHeight;
|
||||
unten = true; neuUnten = 0;
|
||||
springKnopfZeigen();
|
||||
});
|
||||
|
||||
/* Enter schickt, Umschalt+Enter macht eine neue Zeile. Am Handy
|
||||
NICHT -- dort ist Enter die Zeilenschaltung der Bildschirmtastatur,
|
||||
und wer eine zweite Zeile will, käme sonst nie dazu. */
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -231,10 +231,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -140,10 +140,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-14
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -149,14 +149,14 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -82,11 +82,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+26
-10
@@ -14,17 +14,17 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
||||
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
||||
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
||||
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
||||
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
||||
Dialog zu sehen bekam. -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -116,6 +116,11 @@
|
||||
|
||||
<!-- aria-live="polite": Ein Vorleseprogramm sagt neue Nachrichten
|
||||
an, ohne den Nutzer mitten im Satz zu unterbrechen. -->
|
||||
<!-- ZUM NEUESTEN (09.09.2026). Vorher sprang der Verlauf bei jeder
|
||||
neuen Nachricht ans Ende -- wer weiter oben nachlas, wurde
|
||||
mitten im Satz weggerissen. Jetzt bleibt er stehen, und hier
|
||||
steht, wie viel unten wartet. -->
|
||||
<button type="button" class="chat__zum-neuesten" id="zum-neuesten" hidden></button>
|
||||
<div class="chat__verlauf" id="verlauf" role="log" aria-live="polite" aria-relevant="additions">
|
||||
<p class="chat__leer" id="verlauf-leer">
|
||||
Links ein Gespräch auswählen – oder mit <strong>Neu</strong> eines anfangen.
|
||||
@@ -158,6 +163,17 @@
|
||||
<textarea id="text" rows="1" maxlength="4000"
|
||||
placeholder="Nachricht schreiben …"
|
||||
title="Enter schickt ab, Umschalt+Enter macht eine neue Zeile"></textarea>
|
||||
<!-- EMOJI-AUSWAHL (09.09.2026). Die Bildschirmtastatur am Handy
|
||||
hat sie ohnehin -- am Rechner nicht, und dort schreibt die
|
||||
Betreuung. Dreissig feste Zeichen statt einer nachgeladenen
|
||||
Tafel: Das deckt den Alltag ab und laedt nichts. -->
|
||||
<button type="button" class="chat__emoji" id="emoji-auf"
|
||||
aria-expanded="false" aria-controls="emoji-tafel"
|
||||
aria-label="Emoji einfügen" title="Emoji einfügen">☺</button>
|
||||
<div class="emoji-tafel" id="emoji-tafel" role="group"
|
||||
aria-label="Emoji auswählen" hidden></div>
|
||||
<!-- Erscheint erst, wenn es eng wird -- siehe chat.js. -->
|
||||
<span class="chat__zeichen" id="zeichen" aria-live="polite" hidden></span>
|
||||
<button type="submit" class="chat__senden" id="senden" aria-label="Abschicken">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 12l16-7-7 16-2.5-6.5z" />
|
||||
@@ -199,10 +215,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609090318"></script>
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318"></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318"></script>
|
||||
<script src="assets/js/chat.js?v=202609090318"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211"></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211"></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211"></script>
|
||||
<script src="assets/js/chat.js?v=202609091211"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -172,12 +172,12 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/content.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/content.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -101,10 +101,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -401,6 +401,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -340,11 +340,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -217,10 +217,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318"></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318"></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318"></script>
|
||||
<script src="assets/js/leistung.js?v=202609090318"></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211"></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211"></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211"></script>
|
||||
<script src="assets/js/leistung.js?v=202609091211"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -335,10 +335,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -207,11 +207,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -99,11 +99,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/report.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/report.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -132,10 +132,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
||||
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -667,10 +667,10 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/start.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/start.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -78,11 +78,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -119,10 +119,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,13 +14,13 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
||||
Dialog "Creator anlegen". -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -142,11 +142,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-10
@@ -14,11 +14,11 @@
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609090318" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609091211" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091211" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -183,10 +183,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609090318" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609091211" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609091211" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user