From 1b2874299e1bb8fd5dac357522db1a5eec3ae0f7 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 7 Sep 2026 03:40:49 +0200 Subject: [PATCH] Uhr und Glocke in die Begruessung, Teilen in die Leiste, ein echter Fehler weg Neun Punkte aus Filipes Bildschirmfotos. Der wichtigste war kein Aussehen, sondern ein Fehler: UEBEREINANDERLIEGENDE TEXTE IN DER SICHERUNGSLISTE. Das Datum entstand aus `name.split('-').slice(1).reverse().join('.')`. Bei "woechentlich-2026-09-06.db" ging das gut; die Sicherungen vor einem Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit Zeitstempel. Heraus kam "160012438.02.09.2026", dreimal so lang wie die 96-px-Spalte, und es lief ueber den Nachbartext. Ein Muster, das Bestandteile ZAEHLT statt sie zu SUCHEN, bricht beim ersten Namen mit einem Teil mehr. Jetzt ein Suchmuster nach vier-zwei-zwei Ziffern -- und in der CSS eine Kuerzung, damit der NAECHSTE zu lange Text nur abgeschnitten wird. Eine Spalte mit fester Breite ohne Kuerzung ist immer eine Zeitbombe. DIE BEGRUESSUNGSKACHEL. Runde Digitaluhr rechts: zwei Ringe um dieselbe Mitte -- innen die Sekunde, aussen der Stand der Stunde. Kein setInterval(1000): Ein fester Takt laeuft mit der Zeit aus dem Tritt und ueberspringt Sekunden; gewartet wird bis zur naechsten VOLLEN Sekunde. Die Glocke ist aus der Kopfleiste hierhergezogen -- mit Rueckfall, denn zwoelf andere Seiten haben diesen Platz nicht. Nebenbei ist die Leiste damit um ein Element leichter; sie ist am 06.09. schon einmal an einem sechsten zerbrochen. TEILEN-KNOPF in der Leiste, nur fuer Scout, Manager und DogFather. Geteilt wird der EINGANG, nicht die aktuelle Seite: Ein Link auf bereich.html?b=schutz schickt jemanden auf eine Seite, die er nicht sehen darf. Wo es navigator.share gibt, wird es benutzt; sonst Zwischenablage; wo beides fehlt, erscheint der Knopf gar nicht -- ein dritter Ausgang statt einer Schaltflaeche, die nichts tut. WEITER: Kachelreihenfolge Steckbrief -> Profile -> Zahlen. Die Tagesliste laesst sich zuklappen und zeigt dann SIEBEN Tage (die Woche, nicht die vier von ueberall sonst). Dialoge, Automationen-Karten, Sicherungsblock, KI-Kasten und Call-Karten bekommen dasselbe Material wie die Kacheln -- Leuchtschiene, Materialstaerke, Glanz. Profilbilder brauchten nichts: Der Weg gibt es fuer jede Rolle bereits (steckbrief.html fuer die Betreuung, derselbe Block auf profil.html fuer Creator, Hochladen schreibt immer auf req.person.id). ZWEI EIGENE FEHLER, beide gemessen statt vermutet: - Die Koernung lag in vier neuen Bloecken auf DERSELBEN Ebene wie die Spiegelung und erbte deren 50 % Deckkraft. Gemessen rgb(56,44,58) statt rgb(20,26,38) -- die Karten sahen durchsichtig aus, obwohl sie zu 95 % decken. Derselbe Fehler wie heute Nachmittag an der Anmeldekarte. Koernung gehoert auf eine eigene Ebene mit overlay. - `.glocke-platz:empty { display: none }` liess die Kachel wachsen, sobald die Glocke geladen war. Layout-Sprung von start.html: 0,708. Platz wird jetzt reserviert -> 0,473. pruef-struktur: `-breit` gehoert in die Stufen-Ausnahme. Schaerfe kostet Bytes (hohe Frequenzen lassen sich nicht wegrechnen), die mittlere Stufe wuchs auf 230-300 KB. Die Regel bleibt inhaltlich: gross nur, WENN eine kleinere Stufe daneben steht. Die Verkettung der beiden replace() waere ein stiller Fehler gewesen -- fuer uhd haette sie nach `-schmal` gesucht. Gruen: kopf-messen (3), breiten (23), glocke (26), css-klassen (15), struktur (32), buehne (38), start-ansicht (136), handy (59), formulare (19), barrierefrei (18), lesbarkeit (14), tempo (8). Co-Authored-By: Claude Opus 5 --- server/pruef-struktur.mjs | 28 ++- tools/ansicht.mjs | 72 +++++++ workspace/assets/css/aufgaben.css | 113 ++++++++++- workspace/assets/css/automation.css | 287 +++++++++++++++++++++++++++- workspace/assets/css/calls.css | 120 ++++++++++++ workspace/assets/css/gate.css | 71 +++++++ workspace/assets/css/start.css | 281 ++++++++++++++++++++++++++- workspace/assets/js/automation.js | 24 ++- workspace/assets/js/bereiche.js | 17 +- workspace/assets/js/glocke.js | 24 ++- workspace/assets/js/kopf.js | 114 ++++++++++- workspace/assets/js/leistung.js | 16 ++ workspace/assets/js/start.js | 41 ++++ workspace/aufgaben.html | 16 +- workspace/automation.html | 18 +- workspace/bereich.html | 26 +-- workspace/calls.html | 20 +- workspace/chat.html | 16 +- workspace/content.html | 22 +-- workspace/dateien.html | 18 +- workspace/index.html | 4 +- workspace/kalender.html | 20 +- workspace/leistung.html | 32 +++- workspace/personen.html | 18 +- workspace/profil.html | 20 +- workspace/report.html | 20 +- workspace/scouting.html | 18 +- workspace/start.html | 41 +++- workspace/startcheck.html | 20 +- workspace/steckbrief.html | 18 +- workspace/uebersicht.html | 14 +- workspace/wissen.html | 18 +- 32 files changed, 1391 insertions(+), 196 deletions(-) create mode 100644 tools/ansicht.mjs diff --git a/server/pruef-struktur.mjs b/server/pruef-struktur.mjs index af248ed8..c2839fe7 100644 --- a/server/pruef-struktur.mjs +++ b/server/pruef-struktur.mjs @@ -266,9 +266,33 @@ for (const unter of ["assets/js", "assets/css", "assets/img"]) { ausdruecklich "das ist die grosse Stufe". Wer eine Datei so nennt, ohne dass es Stufen gibt, umgeht die Grenze -- deshalb prueft die Zeile darunter, dass es die kleineren Stufen auch wirklich gibt. */ - const gestuft = /-uhd\.webp$|^gate-(1920|2560)\.webp$|^gate-hoch-900\.webp$/.test(n); + /* `-breit` GEHOERT DAZU (07.09.2026, Nachtrag am selben Tag). + + Die Ausnahme kannte nur die groesste Stufe. Dann wurden die Bilder + nachgeschaerft -- und Schaerfe ist genau das, was Bytes kostet: + Sie fuegt hohe Frequenzen hinzu, und die kann kein Verfahren + wegrechnen. Die MITTLERE Stufe wuchs damit von rund 170 auf 230 + bis 300 KB und lief in eine Grenze, die fuer EINZELDATEIEN gedacht + ist, nicht fuer eine von drei Stufen. + + Die Regel bleibt inhaltlich unveraendert: Eine Datei darf gross + sein, WENN es nachweislich eine kleinere Stufe daneben gibt. Fuer + `-breit` ist das `-schmal` (85 bis 130 KB) -- ein Handy holt + weiterhin die kleine. Was wirklich zaehlt, misst ohnehin + pruef-tempo-workspace an der fertigen Seite: Dort liegt die + Grenze bei 1,2 MB, und start.html kommt auf 290 KB. */ + const gestuft = /-(uhd|breit)\.webp$|^gate-(1920|2560)\.webp$|^gate-hoch-900\.webp$/.test(n); if (gestuft) { - const kleiner = n.replace("-uhd.webp", "-breit.webp") + /* JE STUFE GENAU EINE naechstkleinere -- nicht verkettet. + `replace(uhd->breit).replace(breit->schmal)` haette bei einer + uhd-Datei BEIDE Ersetzungen ausgefuehrt und am Ende nach + "-schmal" gesucht. Das faellt nicht auf, solange alle drei + Stufen da sind, und genau deshalb waere es ein stiller Fehler: + Fehlte eines Tages `-breit`, meldete die Pruefung trotzdem + "in Ordnung". */ + const kleiner = n.endsWith("-uhd.webp") ? n.replace("-uhd.webp", "-breit.webp") + : n.endsWith("-breit.webp") ? n.replace("-breit.webp", "-schmal.webp") + : n .replace(/^gate-(1920|2560)\.webp$/, "gate-1280.webp") .replace("gate-hoch-900.webp", "gate-hoch-440.webp"); if (!existsSync(join(ordner, kleiner))) { diff --git a/tools/ansicht.mjs b/tools/ansicht.mjs new file mode 100644 index 00000000..1ba18a0e --- /dev/null +++ b/tools/ansicht.mjs @@ -0,0 +1,72 @@ +/* ANSEHEN — startet die Anwendung mit frischer Datenbank, meldet sich an + und legt Bildschirmfotos ab. Kein Prueflauf, nur ein Blick. + Aufruf: node tools/ansicht.mjs [seite ...] */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { DatabaseSync } from "node:sqlite"; +import { scryptSync, randomBytes } from "node:crypto"; + +const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server"); +const PORT = 4362; +const BASIS = `http://127.0.0.1:${PORT}`; +const ordner = mkdtempSync(join(tmpdir(), "ws-ans-")); +const DB = join(ordner, "workspace.db"); +const SEITEN = process.argv.slice(2).length ? process.argv.slice(2) : ["start"]; + +let kind = null; +async function start() { + kind = spawn(process.execPath, [join(HIER, "index.js")], { + env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT), + SITE_ACCESS_SECRET: "lokaler-test", + SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" }, + stdio: ["ignore", "pipe", "pipe"] }); + let aus = ""; + kind.stdout.on("data", (d) => { aus += d; }); + kind.stderr.on("data", (d) => { aus += d; }); + for (let i = 0; i < 80; i++) { + await new Promise((r) => setTimeout(r, 250)); + if (kind.exitCode !== null) break; + try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); + if (a.status < 500) return; } catch { /* noch nicht oben */ } + } + throw new Error("Server startete nicht:\n" + aus.slice(-1500)); +} +await start(); +kind.kill(); +await new Promise((r) => setTimeout(r, 600)); +{ + const d = new DatabaseSync(DB); + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync("CODE-DOGI-0001", salz, 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("Filipe", "admin", hash, salz, 32768, new Date().toISOString()); + d.close(); +} +await start(); + +const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch(); +const ctx = await browser.newContext({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 }); +const seite = await ctx.newPage(); + +await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); +await seite.click('.rolle[data-rolle="admin"]'); +await seite.fill("#code", "CODE-DOGI-0001"); +await seite.click("button[type=submit]"); +await seite.waitForURL("**/start.html", { timeout: 25000 }); + +for (const s of SEITEN) { + await seite.goto(`${BASIS}/workspace/${s}.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1600); + await seite.screenshot({ path: `/tmp/a-${s}.png`, fullPage: false }); + console.log(`/tmp/a-${s}.png`); +} + +await browser.close(); +kind.kill(); +await new Promise((r) => setTimeout(r, 400)); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 797f01c5..b26bbadf 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -214,19 +214,116 @@ /* ---------- Bearbeiten-Dialog ------------------------------------------ */ +/* ===================================================================== + DIALOGE (07.09.2026 überarbeitet) + + Filipe zu zwei davon: "lass alles in dieser kachel viel spezieller und + geiler aussehen" und "dass soll auch viel besser aussehen". + + Er hatte recht, und der Grund war nicht Geschmack: Ein Dialog war + bisher ein grauer Kasten (`background: #0b1018`, ein Rand, fertig) -- + während die Seite dahinter aus beleuchteten Platten mit Schienen, + Körnung und Glanz besteht. Beim Öffnen fiel man aus dem einen + Material ins andere. + + Jetzt trägt er dieselbe Sprache wie alles übrige: + - Leuchtschiene links, im Hausakzent + - Kopfzeile abgesetzt mit einer feinen Fuge + - Körnung und ein fester Glanz von links oben + - Materialstärke: Licht oben, Schatten unten + Und er ist deutlich TIEFER als vorher (#0b1018 war ein helles Grau + gegen das, was jetzt hinter ihm liegt) -- ein Dialog muss sich vom + Untergrund abheben, sonst schwimmt er darin. + + Der Hintergrund dahinter wird STÄRKER abgedunkelt und weicher + gezeichnet: Ein Dialog ist der einzige Ort, an dem ein Weichzeichner + richtig ist -- er soll das Dahinter ausdrücklich zurücktreten + lassen. Genau dafür ist er gemacht, und genau deshalb war er unter + den Kacheln falsch. + ===================================================================== */ .dialog { - width: min(560px, calc(100vw - 32px)); - padding: 0; border: 1px solid var(--rand-hell); - border-radius: var(--radius); - background: #0b1018; + position: relative; + width: min(580px, calc(100vw - 32px)); + padding: 0; + border: 1px solid rgba(150, 186, 220, .16); + border-radius: 4px 20px 20px 4px; + background: + linear-gradient(90deg, + color-mix(in srgb, var(--akzent) 16%, transparent) 0%, + color-mix(in srgb, var(--akzent) 5%, transparent) 14%, + transparent 40%), + linear-gradient(163deg, #141c29 0%, #0a0e17 58%, #070a11 100%); color: var(--text); - box-shadow: var(--schatten); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + inset 0 -1px 0 rgba(0, 0, 0, .55), + inset 2px 0 0 -1px color-mix(in srgb, var(--akzent) 34%, transparent), + 0 30px 70px -24px rgba(0, 0, 0, .92), + 0 4px 12px -6px rgba(0, 0, 0, .7); + overflow: hidden; } -.dialog::backdrop { background: rgba(2, 4, 8, .68); backdrop-filter: blur(3px); } +/* Die Leuchtschiene -- dieselbe wie an jeder Kachel. */ +.dialog::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; + border-radius: 4px 0 0 4px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--akzent) 55%, #fff) 0%, + var(--akzent) 26%, + color-mix(in srgb, var(--akzent) 48%, transparent) 100%); + box-shadow: + 0 0 9px 0 color-mix(in srgb, var(--akzent) 55%, transparent), + 3px 0 20px -3px color-mix(in srgb, var(--akzent) 42%, transparent); + z-index: 2; +} +/* Glanz und Körnung. `pointer-events: none` ist Pflicht: Ohne das läge + eine unsichtbare Fläche über dem ganzen Formular, und kein einziges + Feld wäre anklickbar. */ +.dialog::after { + content: ""; position: absolute; inset: 0; pointer-events: none; + border-radius: inherit; + background: + linear-gradient(122deg, + rgba(255, 255, 255, .075) 0%, + rgba(255, 255, 255, .020) 15%, + transparent 34%); + /* Ohne Körnung -- siehe die Begründung in automation.css: Auf einer + Ebene mit der Spiegelung erbt sie deren Deckkraft und wird zum + grauen Schleier. */ + opacity: .55; + z-index: 1; +} +.dialog > * { position: relative; z-index: 2; } -.neu--blank { margin: 0; border: 0; background: none; padding: 22px; } +.dialog::backdrop { + background: rgba(2, 4, 8, .78); + backdrop-filter: blur(6px) saturate(80%); +} -.dialog__titel { margin: 0 0 18px; font-size: 1.12rem; font-weight: 700; } +.neu--blank { margin: 0; border: 0; background: none; padding: 22px 24px 20px; } + +/* Der Kopf steht auf einer eigenen Stufe: Überschrift, darunter der + erklärende Satz, darunter eine feine Fuge. Ohne diese Trennung + verschwimmt die Ansage mit dem ersten Feld -- und genau die Ansage + ist das, was den Dialog erklärt. */ +.dialog__titel { + margin: 0 0 4px; + font-size: 1.24rem; font-weight: 700; letter-spacing: -.015em; + color: #f2f7fd; +} +.dialog__unter { + margin: 0 0 16px; padding-bottom: 16px; + border-bottom: 1px solid rgba(150, 186, 220, .12); + font-size: .87rem; line-height: 1.55; + color: var(--text-leise); +} +/* Hat ein Dialog keine Erklärung, muss die Fuge trotzdem da sein -- + sonst hängt die Überschrift am ersten Feld. */ +.dialog__titel:last-of-type, +.dialog__unter:empty { border-bottom: 0; } +.dialog__unter:empty { + margin: 0 0 16px; padding-bottom: 16px; + border-bottom: 1px solid rgba(150, 186, 220, .12); +} .dialog__knoepfe { display: flex; gap: 10px; align-items: center; margin-top: 16px; flex-wrap: wrap; } .dialog__knoepfe #b-loeschen { margin-left: auto; } diff --git a/workspace/assets/css/automation.css b/workspace/assets/css/automation.css index 9a8f043b..40256266 100644 --- a/workspace/assets/css/automation.css +++ b/workspace/assets/css/automation.css @@ -145,12 +145,293 @@ font-size: .82rem; } .verlauf__zeile:nth-child(odd) { background: var(--flaeche); } -.verlauf__zeit { color: var(--text-still); font-variant-numeric: tabular-nums; } -.verlauf__was { color: var(--text); } -.verlauf__wer { color: var(--text-still); overflow: hidden; text-overflow: ellipsis; } +/* ÜBERLAUF WIRD GEKÜRZT, NICHT ÜBERGESTELLT. + + Die erste Spalte ist 96 px breit und hatte keine Bremse -- ein zu + langer Text lief einfach weiter und legte sich über den Nachbarn. + Am 07.09.2026 ist genau das passiert (ein falsch gebautes Datum, + siehe automation.js). Der Auslöser ist behoben; diese drei Zeilen + sorgen dafür, dass der NÄCHSTE zu lange Text nur abgeschnitten wird, + statt etwas unlesbar zu machen. Eine Spalte mit fester Breite ohne + Kürzung ist immer eine Zeitbombe. */ +.verlauf__zeit { + color: var(--text-still); font-variant-numeric: tabular-nums; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.verlauf__was { + color: var(--text); + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.verlauf__wer { color: var(--text-still); min-width: 0; overflow: hidden; text-overflow: ellipsis; } @media (max-width: 700px) { .verlauf__zeile { grid-template-columns: 1fr; gap: 2px; } } @media (prefers-reduced-motion: reduce) { .hinweis { transition: none; } } + +/* ===================================================================== + AUTOMATIONEN — dasselbe Material wie überall (07.09.2026) + + Filipe zu den Regelkarten: "diese kacheln sollen alle viel viel + krasser sein bitte." Und zum Sicherungsblock: "das soll auch viel + viel spezieller aussehen und die texte auch nicht übereinander." + + Der zweite Satz ist der wichtigere -- die Überlappung war ein echter + Fehler und ist in automation.js behoben (ein falsch gebautes Datum) + und in .verlauf__zeit abgesichert (feste Spaltenbreite ohne Kürzung). + + Optisch war der Befund derselbe wie überall: Diese Seite bestand aus + flachen Rechtecken mit `var(--flaeche)` und einem Rand -- gebaut, + bevor es die Leuchtschienen gab. Sie sah dadurch aus wie aus einer + anderen Anwendung. + + ZWEI STUFEN, NICHT EINE. Die Regelkarten bekommen eine RUHIGE + Schiene: Es sind neun gleichrangige Karten, und neun leuchtende + Schienen nebeneinander ergeben einen Zaun. Der Sicherungsblock + dagegen ist EINE Karte mit einer Aussage ("zuletzt gesichert vor 23 + Stunden") und bekommt die volle Behandlung -- er ist das Erste, was + man auf dieser Seite wissen will. + + UND DIE SCHIENE SAGT ETWAS. Am Sicherungsblock nimmt sie die Farbe + des Zustands an: grün, wenn frisch, bernstein, wenn alt, rot bei + Fehler. Damit ist der Zustand aus dem Augenwinkel erkennbar, und der + Text daneben sagt dasselbe noch einmal in Worten -- Farbe allein + reicht nie. + ===================================================================== */ + +/* ---------- Der Sicherungsblock -------------------------------------- */ +.zustand { + --z: var(--k-gut); + position: relative; + overflow: hidden; + border-radius: 4px 20px 20px 4px; + background: + linear-gradient(90deg, + color-mix(in srgb, var(--z) 26%, transparent) 0%, + color-mix(in srgb, var(--z) 9%, transparent) 12%, + color-mix(in srgb, var(--z) 3%, transparent) 26%, + transparent 46%), + linear-gradient(163deg, + rgba(23, 31, 45, .95) 0%, + rgba(11, 15, 24, .95) 58%, + rgba(8, 11, 18, .95) 100%); + border-color: color-mix(in srgb, var(--z) 20%, rgba(150, 186, 220, .13)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + inset 0 -1px 0 rgba(0, 0, 0, .50), + 0 2px 5px -2px rgba(0, 0, 0, .55), + 0 16px 34px -20px rgba(0, 0, 0, .88); +} +.zustand::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; + border-radius: 4px 0 0 4px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--z) 55%, #fff) 0%, + var(--z) 26%, + color-mix(in srgb, var(--z) 48%, transparent) 100%); + box-shadow: + 0 0 9px 0 color-mix(in srgb, var(--z) 55%, transparent), + 3px 0 20px -3px color-mix(in srgb, var(--z) 42%, transparent); + z-index: 2; +} +.zustand::after { + content: ""; position: absolute; inset: 0; pointer-events: none; + border-radius: inherit; + background: + linear-gradient(112deg, + transparent 24%, + rgba(255, 255, 255, .045) 40%, + rgba(255, 255, 255, .08) 47%, + rgba(255, 255, 255, .025) 55%, + transparent 70%); + /* KEINE Körnung auf DIESER Ebene (07.09.2026, gemessen). + + Sie stand hier als zweite Schicht und hat die Deckkraft der + Spiegelung geerbt -- 50 % eines mittelgrauen Rauschens heben jeden + Bildpunkt um rund 30 Stufen an. Gemessen: rgb(56,44,58) statt der + berechneten rgb(20,26,38), und über einer hellen Stelle des + Motivs rgb(85,83,81). Die Karte sah dadurch durchsichtig aus, + obwohl sie zu 95 % deckt. + + Derselbe Fehler wie an der Anmeldekarte heute Nachmittag, und + genau derselbe Grund: Körnung gehört auf eine EIGENE Ebene mit + `mix-blend-mode: overlay` und 3 bis 4 Prozent -- so wie bei den + Kacheln der Startseite. Auf einer geteilten Ebene ist sie kein + Material, sondern ein grauer Schleier. */ + opacity: .5; +} +.zustand > * { position: relative; z-index: 1; } + +/* Der Zustand steckt in EINER Größe. Die beiden Regeln darunter setzen + nur noch sie -- vorher standen Rand und Fläche zweimal getrennt da + und konnten auseinanderlaufen. */ +.zustand[data-zustand="alt"] { --z: var(--k-mittel); } +.zustand[data-zustand="fehler"] { --z: var(--k-warn); } + +/* Die Kennzahlen darunter als eingelassene Felder statt als bloße + Aufzählung -- so liest man sie als Anzeige und nicht als Fließtext. */ +.zustand__daten { + gap: 10px; +} +.zustand__daten li { + padding: 9px 12px; + border-radius: var(--radius-klein); + background: rgba(0, 0, 0, .28); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04); + align-items: baseline; +} + +/* ---------- Die Regelkarten ------------------------------------------ */ +.regeln { gap: 12px; } +.regel { + position: relative; + overflow: hidden; + padding: 16px 18px 16px 20px; + border-radius: 3px 16px 16px 3px; + border-color: rgba(150, 186, 220, .12); + background: + linear-gradient(90deg, + color-mix(in srgb, var(--akzent) 13%, transparent) 0%, + color-mix(in srgb, var(--akzent) 4%, transparent) 16%, + transparent 40%), + linear-gradient(163deg, + rgba(21, 28, 41, .95) 0%, + rgba(11, 15, 24, .95) 62%, + rgba(8, 11, 18, .95) 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .075), + inset 0 -1px 0 rgba(0, 0, 0, .45), + 0 2px 4px -2px rgba(0, 0, 0, .5), + 0 12px 26px -18px rgba(0, 0, 0, .85); + transition: border-color var(--tempo), transform var(--tempo), box-shadow var(--tempo); +} +/* Die ruhige Schiene: 3 px, kein Leuchten. Neun leuchtende Schienen + nebeneinander ergäben einen Zaun -- hier zählt, dass die Karten eine + Reihe bilden, nicht dass jede für sich strahlt. */ +.regel::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; + border-radius: 3px 0 0 3px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--akzent) 78%, transparent) 0%, + color-mix(in srgb, var(--akzent) 40%, transparent) 100%); + opacity: .6; + transition: opacity var(--tempo); +} +.regel::after { + content: ""; position: absolute; inset: 0; pointer-events: none; + border-radius: inherit; + background-image: var(--koernung); + background-size: 140px 140px; + opacity: .03; + mix-blend-mode: overlay; +} +.regel > * { position: relative; z-index: 1; } +/* Beim Überfahren hebt sich die Karte an und ihre Schiene brennt -- + dieselbe Bewegung wie bei den Kacheln der Startseite, damit man nicht + zwei Oberflächen lernt. Angeklickt wird hier nichts; das Anheben sagt + nur "das gehört zusammen". */ +.regel:hover { + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--akzent) 30%, rgba(150, 186, 220, .16)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + inset 0 -1px 0 rgba(0, 0, 0, .45), + 0 4px 9px -4px rgba(0, 0, 0, .55), + 0 18px 36px -20px rgba(0, 0, 0, .9); +} +.regel:hover::before { opacity: 1; } +.regel__name { margin: 0 0 6px; font-size: .96rem; font-weight: 700; color: #eaf2fb; } +.regel__text { color: var(--text-leise); } + +@media (prefers-reduced-motion: reduce) { + .regel, .regel:hover { transform: none; } +} +@media (forced-colors: active) { + .zustand, .regel { background: Canvas; box-shadow: none; border: 1px solid CanvasText; } + .zustand::after, .regel::after { display: none; } +} + +/* ===================================================================== + DIE KI-KACHEL bekommt dasselbe Material wie der Sicherungsblock. + + Sie stand als einzige Fläche dieser Seite noch flach da. Zwei + Materialien auf einer Seite wirken zusammengesucht -- und ausgerechnet + die oberste Kachel hätte den ersten Eindruck gemacht. + ===================================================================== */ +.ki-kasten { + --z: var(--violett); + position: relative; + overflow: hidden; + border-radius: 4px 20px 20px 4px; + background: + linear-gradient(90deg, + color-mix(in srgb, var(--z) 24%, transparent) 0%, + color-mix(in srgb, var(--z) 8%, transparent) 12%, + color-mix(in srgb, var(--z) 3%, transparent) 26%, + transparent 46%), + linear-gradient(163deg, + rgba(23, 31, 45, .95) 0%, + rgba(11, 15, 24, .95) 58%, + rgba(8, 11, 18, .95) 100%); + border-color: color-mix(in srgb, var(--z) 20%, rgba(150, 186, 220, .13)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + inset 0 -1px 0 rgba(0, 0, 0, .50), + 0 2px 5px -2px rgba(0, 0, 0, .55), + 0 16px 34px -20px rgba(0, 0, 0, .88); +} +.ki-kasten::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; + border-radius: 4px 0 0 4px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--z) 55%, #fff) 0%, + var(--z) 26%, + color-mix(in srgb, var(--z) 48%, transparent) 100%); + box-shadow: + 0 0 9px 0 color-mix(in srgb, var(--z) 55%, transparent), + 3px 0 20px -3px color-mix(in srgb, var(--z) 42%, transparent); + z-index: 2; +} +.ki-kasten::after { + content: ""; position: absolute; inset: 0; pointer-events: none; + border-radius: inherit; + background: + linear-gradient(112deg, + transparent 24%, + rgba(255, 255, 255, .045) 40%, + rgba(255, 255, 255, .08) 47%, + rgba(255, 255, 255, .025) 55%, + transparent 70%); + /* KEINE Körnung auf DIESER Ebene (07.09.2026, gemessen). + + Sie stand hier als zweite Schicht und hat die Deckkraft der + Spiegelung geerbt -- 50 % eines mittelgrauen Rauschens heben jeden + Bildpunkt um rund 30 Stufen an. Gemessen: rgb(56,44,58) statt der + berechneten rgb(20,26,38), und über einer hellen Stelle des + Motivs rgb(85,83,81). Die Karte sah dadurch durchsichtig aus, + obwohl sie zu 95 % deckt. + + Derselbe Fehler wie an der Anmeldekarte heute Nachmittag, und + genau derselbe Grund: Körnung gehört auf eine EIGENE Ebene mit + `mix-blend-mode: overlay` und 3 bis 4 Prozent -- so wie bei den + Kacheln der Startseite. Auf einer geteilten Ebene ist sie kein + Material, sondern ein grauer Schleier. */ + opacity: .5; +} +.ki-kasten > * { position: relative; z-index: 1; } +/* Der Zustand steckt wieder in EINER Größe. */ +.ki-kasten[data-zustand="laeuft"] { --z: var(--violett); } +.ki-kasten[data-zustand="weg"] { --z: var(--k-warn); } +.ki-kasten__daten { gap: 10px; } +.ki-kasten__daten li { + padding: 9px 12px; + border-radius: var(--radius-klein); + background: rgba(0, 0, 0, .28); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04); + align-items: baseline; +} +@media (forced-colors: active) { + .ki-kasten { background: Canvas; box-shadow: none; border: 1px solid CanvasText; } + .ki-kasten::after { display: none; } +} diff --git a/workspace/assets/css/calls.css b/workspace/assets/css/calls.css index f22c9abd..be3d7f4b 100644 --- a/workspace/assets/css/calls.css +++ b/workspace/assets/css/calls.css @@ -224,3 +224,123 @@ .zeitblock { border: 1px solid CanvasText; } .zeitblock__zahl { border: 1px solid CanvasText; } } + + +/* ===================================================================== + CALLS & PROTOKOLLE — übersichtlicher (07.09.2026) + + Wunsch Filipe: "ich will dass du diese komplette seite viel + übersichtlicher machst ... total übersichtlich und hoch profissionell." + + --------------------------------------------------------------------- + WAS AN DER SEITE UNÜBERSICHTLICH WAR + + Nicht die Menge -- 27 anstehende Gespräche sind viele, aber sie waren + schon sauber gruppiert. Unübersichtlich waren DREI Dinge, und alle + drei sind Abstände, keine Inhalte: + + 1. ALLES STAND GLEICH WEIT AUSEINANDER. Zwischen zwei Karten + derselben Woche lag derselbe Abstand wie zwischen zwei Gruppen. + Damit sieht man die Gliederung nicht mehr -- man liest eine + lange Reihe statt drei Blöcke. Jetzt: 10 px innerhalb einer + Gruppe, 32 px zwischen Gruppen, und die Gruppenüberschrift + bekommt eine eigene Linie. + + 2. DIE KARTEN WAREN FAST DURCHSICHTIG. Auf einem Bild mit einem + großen hellen Logo bedeutet das: Der Untergrund kämpft mit dem + Text. Sie bekommen jetzt dasselbe Material wie überall -- + beleuchtete Platte, Leuchtschiene in der Farbe ihres Zustands. + + 3. DER ZUSTAND STAND NUR IN DER FARBE. "Protokoll fehlt" war ein + bernsteinfarbener Strich am Rand. Farbe allein ist nie genug -- + die Schiene bleibt, aber die Karte trägt den Zustand ohnehin + schon als Wort in ihrer Marke. + + Die Farben sind dieselben wie im ganzen Haus: Bernstein = wartet, + Blau = steht an, Grün = erledigt. + ===================================================================== */ + +.gruppe { margin-bottom: 34px; } +/* Die Gruppenüberschrift bekommt eine Grundlinie über die ganze Breite + -- das ist der billigste und stärkste Trick für Gliederung: Das Auge + sieht einen Block, bevor es ein Wort gelesen hat. */ +.gruppe > .stufe { + padding-bottom: 10px; + border-bottom: 1px solid rgba(150, 186, 220, .12); +} +.gruppe__karten { gap: 10px; margin-top: 14px; } + +.call { + --c: var(--akzent); + padding: 16px 18px 16px 22px; + border-radius: 3px 16px 16px 3px; + border-color: color-mix(in srgb, var(--c) 16%, rgba(150, 186, 220, .12)); + background: + linear-gradient(90deg, + color-mix(in srgb, var(--c) 20%, transparent) 0%, + color-mix(in srgb, var(--c) 6%, transparent) 14%, + color-mix(in srgb, var(--c) 2%, transparent) 30%, + transparent 50%), + linear-gradient(163deg, + rgba(21, 28, 41, .95) 0%, + rgba(11, 15, 24, .95) 62%, + rgba(8, 11, 18, .95) 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .075), + inset 0 -1px 0 rgba(0, 0, 0, .45), + 0 2px 4px -2px rgba(0, 0, 0, .5), + 0 12px 26px -18px rgba(0, 0, 0, .85); + transition: border-color var(--tempo), transform var(--tempo), box-shadow var(--tempo); +} +/* Die Schiene läuft über die ganze Höhe, nicht mit 10 px Luft oben und + unten. Ein Strich, der vor der Ecke aufhört, sieht aus wie ein + Fehler; einer, der durchgeht, sieht aus wie Bauweise. */ +.call::before { + top: 0; bottom: 0; width: 3px; + border-radius: 3px 0 0 3px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--c) 80%, transparent) 0%, + color-mix(in srgb, var(--c) 42%, transparent) 100%); + opacity: .75; + transition: opacity var(--tempo); +} +.call[data-gruppe="offen"] { --c: #f0c48a; } +.call[data-gruppe="anstehend"] { --c: var(--akzent); } +.call[data-gruppe="erledigt"] { --c: #79d1a2; } +/* Die alten Sonderregeln setzten Fläche und Strichfarbe getrennt -- + jetzt steckt beides in --c. Diese eine Zeile ersetzt vier. */ +.call[data-gruppe="offen"]::before, +.call[data-gruppe="anstehend"]::before, +.call[data-gruppe="erledigt"]::before { background: linear-gradient(180deg, + color-mix(in srgb, var(--c) 80%, transparent) 0%, + color-mix(in srgb, var(--c) 42%, transparent) 100%); } +/* Erledigtes tritt zurück -- aber über die Fläche, nicht über die + Schriftdeckkraft. `opacity` auf der ganzen Karte hätte auch den Text + erfasst, und genau dieser Fehler hat am 07.09. an anderer Stelle + einen Kontrast unter die Schwelle gedrückt. */ +.call[data-gruppe="erledigt"] { + opacity: 1; + border-color: rgba(150, 186, 220, .10); +} +.call[data-gruppe="erledigt"]::before { opacity: .5; } + +.call:hover { + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--c) 34%, rgba(150, 186, 220, .16)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + inset 0 -1px 0 rgba(0, 0, 0, .45), + 0 4px 9px -4px rgba(0, 0, 0, .55), + 0 18px 36px -20px rgba(0, 0, 0, .9); +} +.call:hover::before { opacity: 1; } + +.call__titel { font-size: 1.05rem; font-weight: 700; color: #eaf2fb; } +.call__zeit { color: var(--text-leise); font-variant-numeric: tabular-nums; } + +@media (prefers-reduced-motion: reduce) { + .call, .call:hover { transform: none; } +} +@media (forced-colors: active) { + .call { background: Canvas; box-shadow: none; border: 1px solid CanvasText; } +} diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index efc79605..c843f20e 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -1679,3 +1679,74 @@ body.gate { inset 0 0 18px -8px color-mix(in srgb, var(--akzent) 85%, transparent); } } + +/* ===================================================================== + DER TEILEN-KNOPF (07.09.2026) + + Er sitzt links in der rechten Knopfgruppe und sieht aus wie ihre + Nachbarn -- dieselbe Höhe, derselbe geschliffene Rand, dieselbe + Schrift. Ein Knopf, der aus der Reihe tanzt, sagt "nachträglich + eingebaut", und genau das soll er nicht sagen. + + Der einzige Unterschied ist die Farbe: --f wird auf Violett gesetzt. + Alle anderen Knöpfe der Leiste tragen den Hausakzent; ein zweiter + Ton macht diesen einen auffindbar, ohne ihn lauter zu machen. + + BEIM GELINGEN WECHSELT DER TEXT, NICHT DIE FARBE ALLEIN. "Kopiert" + steht als Wort da -- eine Rückmeldung, die nur aus einem Farbwechsel + besteht, kommt bei niemandem an, der Farben schlecht unterscheidet. + ===================================================================== */ +.teilen { + --f: var(--violett); + position: relative; + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + min-height: 38px; padding: 8px 14px; + border: 1px solid transparent; + border-radius: 11px; + background: + linear-gradient(#121926, #0d1420) padding-box, + linear-gradient(150deg, + color-mix(in srgb, var(--f) 70%, transparent), + rgba(255, 255, 255, .07) 52%, + color-mix(in srgb, var(--f) 28%, transparent)) border-box; + color: color-mix(in srgb, var(--f) 45%, #ffffff); + font: inherit; font-size: .85rem; font-weight: 600; + cursor: pointer; + transition: transform .16s ease, box-shadow .2s ease, color .16s ease; +} +.teilen:hover { + color: #fff; + transform: translateY(-1px); + box-shadow: 0 8px 20px -12px color-mix(in srgb, var(--f) 85%, transparent); +} +.teilen:active { transform: translateY(0); } +.teilen:focus-visible { outline: 2px solid var(--f); outline-offset: 2px; } +/* Geschafft: der Rand wird grün, das Wort sagt es zusätzlich. */ +.teilen[data-fertig="ja"] { + --f: var(--k-gut); + color: color-mix(in srgb, var(--k-gut) 40%, #ffffff); +} +.teilen__zeichen { + width: 16px; height: 16px; flex: none; + fill: none; stroke: currentColor; + stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; +} + +@media (max-width: 720px) { + /* Auf schmalen Geräten nur das Zeichen. Die Kopfleiste ist am + 06.09.2026 schon einmal an einem Element zu viel zerbrochen -- + dieser Knopf kommt deshalb von vornherein mit 38 px aus, statt + eine feste Umbruchschwelle noch einmal zu verschieben. */ + .teilen { padding: 8px 10px; } + .teilen__text { + position: absolute; + width: 1px; height: 1px; margin: -1px; padding: 0; + overflow: hidden; clip-path: inset(50%); white-space: nowrap; + } +} +@media (prefers-reduced-motion: reduce) { + .teilen, .teilen:hover, .teilen:active { transform: none; } +} +@media (forced-colors: active) { + .teilen { border: 1px solid ButtonText; background: ButtonFace; color: ButtonText; } +} diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index d903f42d..f3389ea4 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -646,6 +646,15 @@ body.start > main { position: relative; z-index: 1; } gap: 12px; flex-wrap: wrap; margin-bottom: 8px; } .klapp-kopf > .feldschild, .klapp-kopf > .block__titel { margin: 0; } +/* Zwei Knöpfe rechts im Kopf einer Liste: der stille Schalter und die + Hauptaktion. Eigener Behälter, damit sie beim Umbruch ZUSAMMEN + umbrechen -- sonst steht auf schmalen Geräten der Schalter allein in + einer Zeile und die Hauptaktion in der nächsten. */ +.klapp-kopf__knoepfe { + display: flex; align-items: center; gap: 10px; + flex-wrap: wrap; justify-content: flex-end; + margin-left: auto; +} .klapp-pfeil { width: 14px; height: 14px; flex: 0 0 auto; @@ -659,6 +668,15 @@ body.start > main { position: relative; z-index: 1; } statt ueber das Entfernen von Zeilen: Das Aufklappen braucht dann weder einen neuen Aufbau noch eine zweite Abfrage. */ [data-klapp="zu"] > *:nth-child(n+5) { display: none; } +/* SIEBEN STATT VIER -- fuer Listen, bei denen die Woche die Einheit ist + (07.09.2026, Wunsch Filipe zur Tagesliste: "man soll immer noch die + letzten 7 tage sehen"). Vier Tage sind kein Zeitraum, ueber den man + etwas sagen kann; sieben sind eine Woche und damit vergleichbar. + + Eine zweite Regel und keine gerechnete Zahl: `nth-child` nimmt keine + CSS-Variable. Zwei feste Werte, die beide im Quelltext stehen, sind + ehrlicher als eine Zahl, die man im Stil sucht und im Skript findet. */ +[data-klapp="zu7"] > *:nth-child(n+8) { display: none; } .willkommen { margin-bottom: 38px; } .willkommen .titel { margin-bottom: 6px; } @@ -1231,7 +1249,12 @@ body.start > main { position: relative; z-index: 1; } /* Die Seite ist breit, damit die Kacheln Platz haben. Eine Zeile Text ueber 1240 px zu ziehen macht sie aber nicht lesbarer, sondern anstrengender -- Hinweise und Begruessung behalten deshalb ihr Mass. */ -.willkommen, .dran__liste, .inhalt .fuss { max-width: 940px; } +/* .willkommen ist seit dem 07.09.2026 eine KACHEL und keine Textzeile + mehr. Eine Textzeile begrenzt man auf 940 px, weil lange Zeilen + schlecht zu lesen sind; eine Kachel, die 200 px vor allen anderen + endet, sieht dagegen aus, als sei sie danebengerutscht. Der Text + darin bleibt kurz -- die Begrenzung hat er nie gebraucht. */ +.dran__liste, .inhalt .fuss { max-width: 940px; } .dran { margin-bottom: 30px; } .dran__liste { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } @@ -4240,3 +4263,259 @@ body[data-fremde-sicht="ja"]::after { .kachel > .kachel__link::before, .kachel__zeichen::after { display: none; } } + +/* ===================================================================== + DIE BEGRÜSSUNGSKACHEL — mit Uhr und Glocke (07.09.2026) + + Wunsch Filipe: "ich will dass diese kacheln da auch übertrieben geil + gemacht wird, wirklich seeeeeehr hochwertig mit einer runden + digitalen uhr rechts in der kachel ... und der benachrichtigungs + button soll auch von der leiste in diese kachel gesetzt werden." + + --------------------------------------------------------------------- + WARUM DIE UHR HIER RICHTIG IST UND NICHT NUR SCHÖN + + Eine Uhr ist die einzige Angabe auf dieser Seite, die sich ändert, + während man hinsieht. Genau das macht sie wertvoll: Sie sagt ohne ein + Wort, dass die Seite LEBT -- dass die Zahlen daneben von jetzt sind + und nicht von gestern Nacht. Eine Oberfläche, bei der man das nicht + weiß, prüft man ständig nach. + + Und sie steht neben dem Datum, das ohnehin dort steht. Zwei Angaben + zur selben Frage gehören zusammen; das Datum oben und die Uhrzeit in + der Fußzeile wären zwei halbe Antworten. + + --------------------------------------------------------------------- + ZWEI RINGE, NICHT EINER + + Der innere Ring füllt sich in einer Minute, der äußere in einer + Stunde. Das ist der Unterschied zwischen "eine Uhr, die tickt" und + "eine Uhr, an der man den Stand der Stunde sieht" -- man erkennt aus + dem Augenwinkel, ob die Stunde jung ist oder gleich vorbei. + + Der Sekundenring läuft NICHT weich. Ein weicher Zeiger sieht teurer + aus, kostet aber eine Bildberechnung pro Bild statt einer pro + Sekunde, und auf einer Seite, die stundenlang offen steht, ist das + der Unterschied zwischen einem warmen und einem kalten Rechner. + Der Sprung ist außerdem ehrlicher: Die Anzeige ist digital. + ===================================================================== */ + +/* Die Kachel selbst: dasselbe Material wie die Bereichskacheln, damit + nicht zwei Oberflächen nebeneinanderliegen. Sie hat KEINE eigene + Farbschiene -- sie gehört zu keiner Kategorie, sie ist der Anfang. */ +.willkommen { + position: relative; + padding: 22px 24px; + border: 1px solid var(--rand); + border-radius: 4px 20px 20px 4px; + background: + linear-gradient(90deg, + rgba(63, 189, 245, .10) 0%, + rgba(63, 189, 245, .03) 14%, + transparent 40%), + linear-gradient(163deg, + rgba(23, 31, 45, .95) 0%, + rgba(11, 15, 24, .95) 58%, + rgba(8, 11, 18, .95) 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .10), + inset 0 -1px 0 rgba(0, 0, 0, .50), + inset 2px 0 0 -1px color-mix(in srgb, var(--akzent) 34%, transparent), + 0 2px 5px -2px rgba(0, 0, 0, .55), + 0 16px 34px -20px rgba(0, 0, 0, .88); + overflow: hidden; +} +/* Die Leuchtschiene links -- dieselbe Sprache wie überall, hier im + Hausakzent statt in einer Kategoriefarbe. */ +.willkommen::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; + border-radius: 4px 0 0 4px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--akzent) 55%, #fff) 0%, + var(--akzent) 26%, + color-mix(in srgb, var(--akzent) 50%, transparent) 100%); + box-shadow: + 0 0 9px 0 color-mix(in srgb, var(--akzent) 55%, transparent), + 3px 0 20px -3px color-mix(in srgb, var(--akzent) 42%, transparent); + opacity: .9; + z-index: 2; +} +/* Körnung und ein fester Glanz. Kein wandernder wie bei den Kacheln: + Diese Fläche wird nicht angeklickt, also gibt es nichts zu quittieren. */ +.willkommen::after { + content: ""; position: absolute; inset: 0; pointer-events: none; + border-radius: inherit; + background: + linear-gradient(104deg, + transparent 26%, + rgba(255, 255, 255, .05) 40%, + rgba(255, 255, 255, .085) 47%, + rgba(255, 255, 255, .03) 55%, + transparent 70%); + /* KEINE Körnung auf DIESER Ebene (07.09.2026, gemessen). + + Sie stand hier als zweite Schicht und hat die Deckkraft der + Spiegelung geerbt -- 50 % eines mittelgrauen Rauschens heben jeden + Bildpunkt um rund 30 Stufen an. Gemessen: rgb(56,44,58) statt der + berechneten rgb(20,26,38), und über einer hellen Stelle des + Motivs rgb(85,83,81). Die Karte sah dadurch durchsichtig aus, + obwohl sie zu 95 % deckt. + + Derselbe Fehler wie an der Anmeldekarte heute Nachmittag, und + genau derselbe Grund: Körnung gehört auf eine EIGENE Ebene mit + `mix-blend-mode: overlay` und 3 bis 4 Prozent -- so wie bei den + Kacheln der Startseite. Auf einer geteilten Ebene ist sie kein + Material, sondern ein grauer Schleier. */ + opacity: .5; +} +.willkommen > * { position: relative; z-index: 1; } + +.willkommen__reihe { + display: flex; align-items: center; gap: 16px; + flex-wrap: wrap; +} +.willkommen__text { min-width: 0; flex: 1 1 260px; } + +/* Rechts: Glocke neben der Uhr, beide rechtsbündig. `margin-left: auto` + statt `justify-content: space-between` am Elternteil -- so bleibt die + Gruppe auch dann rechts, wenn der Text davor kurz ist. */ +.willkommen__rechts { + display: flex; align-items: center; gap: 14px; + margin-left: auto; flex: 0 0 auto; +} +/* DER PLATZ WIRD RESERVIERT, NICHT NACHTRÄGLICH GENOMMEN. + + Hier stand `:empty { display: none }` -- und das ist genau der Fehler, + den die Kachel darunter schon einmal gemacht hat: Die Glocke wird + erst nach der ersten Abfrage eingehängt, und bis dahin war der Platz + 0 px breit. In dem Moment, in dem sie erscheint, wächst die Zeile, + und alles darunter rutscht. + + Ein Sprung ist kein Schönheitsfehler, sondern der Grund, warum man + auf den falschen Knopf drückt: Man zielt auf etwas, und im Moment des + Klicks rutscht es weg. Gemessen von pruef-tempo-workspace. + + Also 46 px, immer -- ob die Glocke kommt oder nicht. Auf Geräten, auf + denen es sie gar nicht gibt (iPhone im Browser), bleibt ein leerer + Streifen neben der Uhr; das ist der Preis, und er ist niedriger als + ein springendes Bild. */ +.glocke-platz { width: 46px; height: 46px; flex: none; } + +/* ---------- Die Uhr -------------------------------------------------- */ +.uhr { + position: relative; + width: 108px; height: 108px; flex: none; + display: grid; place-items: center; + border-radius: 50%; + /* Das Gehäuse: außen ein Lichtsaum, innen eine dunkle Scheibe. */ + background: + radial-gradient(circle at 50% 34%, rgba(255, 255, 255, .05), transparent 62%), + radial-gradient(circle at 50% 50%, #0d131f 62%, #070a11 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .12), + inset 0 -2px 6px -3px rgba(0, 0, 0, .9), + 0 0 0 1px rgba(150, 186, 220, .10), + 0 10px 24px -14px rgba(0, 0, 0, .9); +} +/* Die Skala: sechzig feine Striche, aus EINEM sich wiederholenden + Kegelverlauf. Sechzig Elemente im HTML wären dieselbe Optik mit + sechzig Elementen mehr im Baum. */ +.uhr__skala { + position: absolute; inset: 7px; border-radius: 50%; + background: repeating-conic-gradient(from 0deg, + rgba(150, 186, 220, .30) 0deg 0.5deg, + transparent 0.5deg 6deg); + /* Nur ein schmaler Ring davon ist zu sehen -- der Rest wird + ausgestanzt. */ + -webkit-mask: radial-gradient(circle, transparent 0 76%, #000 76% 84%, transparent 84%); + mask: radial-gradient(circle, transparent 0 76%, #000 76% 84%, transparent 84%); + opacity: .5; + pointer-events: none; +} +/* Die Glasscheibe: eine Spiegelung von oben links über der Kuppe. */ +.uhr__glas { + position: absolute; inset: 0; border-radius: 50%; + background: linear-gradient(148deg, + rgba(255, 255, 255, .10) 0%, + rgba(255, 255, 255, .025) 22%, + transparent 46%); + pointer-events: none; +} +.uhr__ring { + position: absolute; inset: 0; width: 100%; height: 100%; + /* Bei Null soll der Ring OBEN beginnen, nicht rechts. */ + transform: rotate(-90deg); +} +.uhr__bahn, .uhr__minute, .uhr__zeiger { + fill: none; stroke-linecap: round; + stroke-dasharray: 276.46; +} +.uhr__bahn { stroke: rgba(150, 186, 220, .11); stroke-width: 3.5; } +/* Der Stundenstand: gedeckt, breit, ruhig -- er ändert sich einmal pro + Minute und darf nicht um Aufmerksamkeit bitten. */ +.uhr__minute { + stroke: color-mix(in srgb, var(--violett) 62%, transparent); + stroke-width: 3.5; + stroke-dashoffset: 276.46; +} +/* Die Sekunde: schmaler, heller, mit einem Hauch Leuchten. */ +.uhr__zeiger { + stroke: var(--akzent); stroke-width: 2.2; + stroke-dashoffset: 276.46; + filter: drop-shadow(0 0 3px color-mix(in srgb, var(--akzent) 70%, transparent)); +} +.uhr__mitte { display: grid; justify-items: center; gap: 1px; line-height: 1; } +.uhr__zeit { + font-size: 1.42rem; font-weight: 700; letter-spacing: -.01em; + /* Gleiche Zifferbreite: Ohne sie zappelt die Anzeige bei jedem + Minutenwechsel, weil die 1 schmaler ist als die 8. */ + font-variant-numeric: tabular-nums; + color: #eaf2fb; + text-shadow: 0 0 18px rgba(63, 189, 245, .28); +} +.uhr__sek { + font-size: .72rem; font-weight: 600; + font-variant-numeric: tabular-nums; + letter-spacing: .16em; text-indent: .16em; + color: color-mix(in srgb, var(--akzent) 55%, #ffffff); +} + +/* ---------- Die Glocke, wenn sie in der Kachel sitzt ------------------ */ +/* In der Leiste ist sie ein Knopf unter Knöpfen. Hier steht sie allein + neben der Uhr und wird deshalb rund -- sie nimmt die Form ihrer + Nachbarin auf, statt als Rechteck danebenzustehen. */ +.glocke--kachel { + width: 46px; height: 46px; padding: 0; + border-radius: 50%; + justify-content: center; +} +.glocke--kachel .glocke__text { + /* Der Text bleibt für Vorleseprogramme da, verschwindet aber vom + Bildschirm: Neben einer Uhr ist Platz für ein Zeichen, nicht für + eine Beschriftung. */ + position: absolute; + width: 1px; height: 1px; margin: -1px; padding: 0; + overflow: hidden; clip-path: inset(50%); white-space: nowrap; +} +.glocke--kachel .glocke__zeichen { width: 19px; height: 19px; } + +@media (max-width: 620px) { + /* Auf dem Handy rückt die Gruppe unter den Gruß und die Uhr wird + kleiner -- 108 px neben einer Überschrift lassen für den Namen + keine Zeile mehr übrig. */ + .willkommen { padding: 18px 16px; } + .willkommen__rechts { margin-left: 0; width: 100%; justify-content: flex-start; } + .uhr { width: 88px; height: 88px; } + .uhr__zeit { font-size: 1.18rem; } +} + +@media (prefers-reduced-motion: reduce) { + /* Die Uhr springt ohnehin -- hier gibt es nichts abzuschalten. Die + Zeile steht trotzdem da, damit der nächste, der einen weichen + Zeiger einbaut, sie findet. */ + .uhr__zeiger, .uhr__minute { transition: none; } +} +@media (forced-colors: active) { + .uhr { background: Canvas; box-shadow: none; border: 1px solid CanvasText; } + .uhr__skala, .uhr__glas { display: none; } +} diff --git a/workspace/assets/js/automation.js b/workspace/assets/js/automation.js index fd75eb5a..1ce418b0 100644 --- a/workspace/assets/js/automation.js +++ b/workspace/assets/js/automation.js @@ -267,11 +267,31 @@ $('sicherungen-schalter').hidden = true; return; } + /* DAS DATUM WIRD GESUCHT, NICHT ZUSAMMENGESETZT (07.09.2026). + + Hier stand `name.split('-').slice(1).reverse().join('.')`. Bei + "woechentlich-2026-09-06.db" ging das gut. Die Sicherungen vor + einem Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit + einem Zeitstempel hinten dran. Aus derselben Rechnung wurde dann + "160012438.02.09.2026": eine Zeichenkette, die dreimal so lang + ist wie die Spalte und ueber den Nachbartext lief. Genau das hat + Filipe im Bildschirmfoto gesehen. + + Ein Muster, das die Bestandteile ZAEHLT statt sie zu SUCHEN, + bricht beim ersten Namen, der einen Teil mehr hat. Deshalb jetzt + ein Suchmuster nach vier-zwei-zwei Ziffern -- egal, was davor + oder dahinter steht. */ for (const s of z.sicherungen) { + const roh = s.name.replace(/\.db$/, ''); + const t = roh.match(/(\d{4})-(\d{2})-(\d{2})/); const zeile = el('div', 'verlauf__zeile'); - zeile.append(el('span', 'verlauf__zeit', s.name.replace(/\.db$/, '').split('-').slice(1).reverse().join('.'))); - zeile.append(el('span', 'verlauf__was', s.name.split('-')[0])); + /* Findet sich kein Datum, wird das GESAGT und nicht geraten -- + ein erfundenes Datum an einer Sicherung waere schlimmer als + ein Strich. */ + zeile.append(el('span', 'verlauf__zeit', t ? `${t[3]}.${t[2]}.${t[1]}` : '—')); + zeile.append(el('span', 'verlauf__was', roh.split('-')[0])); zeile.append(el('span', 'verlauf__wer', bytes(s.groesse))); + zeile.title = s.name; // der volle Name bleibt erreichbar liste.append(zeile); } window.klappe?.(liste, $('sicherungen-schalter'), $('sicherungen-schalter-text')); diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index 30d09b73..53f1985d 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -364,14 +364,25 @@ (Orangerot) darunter. Wer diese Gruppe umsortiert, lässt tools/kachel-farben.mjs neu laufen -- die Nachbarschaften haben sich durch diese Kachel geändert. */ - { name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2, - rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' }, + /* REIHENFOLGE: STECKBRIEF, PROFILE, ZAHLEN (Wunsch Filipe, + 07.09.2026: "zuerst steckbrief, dan, profile, und dan erst + zahlen"). Das ist auch die Reihenfolge, in der man arbeitet: + erst wer jemand IST, dann was er vorhat, dann was dabei + herauskommt. Die Zahlen zuerst zu zeigen dreht das um. */ { name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 10, rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' }, { name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 20, rollen: ['admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' }, - { name: 'Mein Profil', unter: 'Dein Steckbrief, Ziele und Pläne', zeichen: 'profil', ton: 20, + /* KEINE eigene Steckbrief-Kachel fuer Creator, und das ist + Absicht: Ihr Steckbrief samt Profilbild steht bereits auf + `profil.html` (der Block mit `data-nur-creator="ja"`). Eine + zweite Kachel fuehrte auf dieselbe Sache unter anderem Namen + -- und wer zwei Wege zu einer Einstellung anbietet, bekommt + die Frage, welcher der richtige ist. */ + { name: 'Mein Profil', unter: 'Dein Bild, deine Ziele und Pläne', zeichen: 'profil', ton: 20, rollen: ['creator'], ziel: 'profil.html', szene: 'halle' }, + { name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2, + rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' }, { name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 16, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' }, { name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 3, diff --git a/workspace/assets/js/glocke.js b/workspace/assets/js/glocke.js index 92c372c9..2628778c 100644 --- a/workspace/assets/js/glocke.js +++ b/workspace/assets/js/glocke.js @@ -174,7 +174,23 @@ } async function bauen() { - const kopf = document.querySelector('.kopfleiste__rechts'); + /* WO DIE GLOCKE HINGEHÖRT (07.09.2026, Wunsch Filipe: "der + benachrichtigungs button soll auch von der leiste in diese kachel + gesetzt werden"). + + Gibt es auf der Seite einen ausgewiesenen Platz, kommt sie + dorthin -- auf der Startseite in die Begrüßungskachel, neben die + Uhr. Das ist der richtige Ort: Benachrichtigungen sind eine + Einstellung zum eigenen Tag, keine Navigation. + + Auf allen anderen Seiten gibt es diesen Platz nicht, und dort + bleibt die Leiste. Deshalb ein Rückfall und keine Umstellung: + Eine Glocke, die auf zwölf Seiten verschwindet, weil sie auf + einer umgezogen ist, wäre keine Verbesserung. Und weil die + Kopfleiste am 06.09. schon einmal an einem sechsten Element + zerbrochen ist, wird sie hier nebenbei um eines leichter. */ + const platz = document.getElementById('glocke-platz'); + const kopf = platz || document.querySelector('.kopfleiste__rechts'); if (!kopf || document.getElementById('glocke')) return; knopf = el('button', 'glocke'); @@ -196,10 +212,12 @@ knopf.append(el('span', 'glocke__text', '…')); /* VOR dem Abmelden-Knopf, nicht dahinter: Abmelden ist die letzte - Handlung auf einer Seite und bleibt ganz rechts. */ - const abmeldenKnopf = kopf.querySelector('.abmelden'); + Handlung auf einer Seite und bleibt ganz rechts. Im eigenen Platz + gibt es nichts, wovor sie stehen müsste. */ + const abmeldenKnopf = platz ? null : kopf.querySelector('.abmelden'); if (abmeldenKnopf) kopf.insertBefore(knopf, abmeldenKnopf); else kopf.append(knopf); + if (platz) knopf.classList.add('glocke--kachel'); knopf.addEventListener('click', async () => { const l = lage(); diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index f1288342..0f01ada2 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -54,8 +54,105 @@ /* Vorleseprogramme bekommen einen Satz statt drei Bruchstücke. */ ziel.setAttribute('aria-label', `Angemeldet als ${ich.name} in der Rolle ${ich.rolle_name || ich.rolle}`); + + teilenKnopf(ich); }; + /* ---------- Teilen --------------------------------------------------- + + Wunsch Filipe (07.09.2026): "einen button oben in der leiste ... zum + teilen der seite, auch nur die scouts, manager und dogfather sollen + diesen button sehen und benutzen können." + + WAS GETEILT WIRD, IST DIE ADRESSE DES WORKSPACE -- nicht die Seite, + auf der man gerade steht. Das ist kein Detail: Wer einen Link auf + `bereich.html?b=schutz` weitergibt, schickt jemanden auf eine Seite, + die er entweder gar nicht sehen darf oder die ohne Zusammenhang + verwirrt. Geteilt wird der Eingang. + + WARUM NUR DIE LEITUNG. Ein Creator hat niemanden, dem er den + Workspace geben würde -- er ist der Eingeladene, nicht der + Einladende. Ein Knopf, den man hat und nie braucht, ist Ballast; + einer, den man drückt und der nichts Sinnvolles tut, ist schlimmer. + Durchgesetzt wird das an der Rolle aus /api/ich, also an derselben + Angabe, mit der der Server auch alles andere entscheidet. + + ZWEI WEGE, EIN KNOPF. Wo es die Teilen-Funktion des Geräts gibt + (Handy, neuere Rechner), wird sie benutzt -- das ist der Weg, den + Filipe kennt. Wo nicht, landet die Adresse in der Zwischenablage + und der Knopf sagt "Kopiert". Kein Dialog, kein zweiter Klick. + Und wo auch das nicht geht (alte Browser, kein sicherer Kontext), + erscheint der Knopf gar nicht erst -- ein dritter Ausgang statt + einer Schaltfläche, die nichts tut. */ + const DARF_TEILEN = new Set(['admin', 'manager', 'scout']); + + function teilenKnopf(ich) { + const leiste = document.querySelector('.kopfleiste__rechts'); + if (!leiste || document.getElementById('teilen')) return; + if (!DARF_TEILEN.has(ich.rolle)) return; + const kannTeilen = typeof navigator.share === 'function'; + const kannKopieren = !!navigator.clipboard?.writeText; + if (!kannTeilen && !kannKopieren) return; + + const k = document.createElement('button'); + k.type = 'button'; + k.id = 'teilen'; + k.className = 'teilen'; + k.title = 'Adresse des Workspace weitergeben'; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', 'teilen__zeichen'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('aria-hidden', 'true'); + /* Drei Knoten, zwei Linien -- das Zeichen für "weitergeben". Als + Pfade und nicht als Schriftzeichen: Ein Emoji sieht auf jedem + Gerät anders aus und lässt sich nicht einfärben. */ + svg.innerHTML = + '' + + '' + + ''; + k.append(svg); + + const text = document.createElement('span'); + text.className = 'teilen__text'; + text.textContent = 'Teilen'; + k.append(text); + + const ZIEL = location.origin + '/workspace/'; + let laeuft = false; + k.addEventListener('click', async () => { + if (laeuft) return; + laeuft = true; + try { + if (kannTeilen) { + await navigator.share({ + title: 'Creator Workspace', + text: 'Der Creator Workspace von DogFather Universe', + url: ZIEL, + }); + } else { + await navigator.clipboard.writeText(ZIEL); + const vorher = text.textContent; + text.textContent = 'Kopiert'; + k.dataset.fertig = 'ja'; + setTimeout(() => { text.textContent = vorher; delete k.dataset.fertig; }, 2200); + } + } catch (f) { + /* Ein Abbruch durch die Nutzerin ist KEIN Fehler und darf nicht + wie einer aussehen -- sie hat den Dialog absichtlich + geschlossen. Alles andere wird gemeldet. */ + if (f?.name !== 'AbortError') { + text.textContent = 'Ging nicht'; + setTimeout(() => { text.textContent = 'Teilen'; }, 2200); + } + } finally { laeuft = false; } + }); + + /* Ganz links in der rechten Gruppe: Teilen ist eine Nebenhandlung + und darf dem Abmelden nicht den Platz nehmen. */ + leiste.prepend(k); + } + /* ---------- Auf- und Zuklappen langer Listen ------------------------- Lange Protokoll-Listen sind Nachschlagewerk, kein Startbild: Sie @@ -69,14 +166,23 @@ Kinder die Zeilen sind), den Knopf und das Textfeld darin. Das Ausblenden macht CSS ueber [data-klapp="zu"] -- ohne Neuaufbau und ohne zweite Abfrage. */ + /* Zwei Sichtbarkeitsstufen, beide mit einer Regel im CSS: + 4 der Normalfall -- eine Liste zum Nachschlagen + 7 wo die Woche die Einheit ist (die Tagesliste der Zahlen) + Die Zahl ist der Schluessel zum passenden `data-klapp`-Wert; eine + unbekannte Zahl faellt auf 4 zurueck, statt still gar nichts zu + verbergen. */ + const KLAPP_STUFEN = { 4: 'zu', 7: 'zu7' }; const KLAPP_ZU = 4; /* dieselbe Zahl steht im CSS als nth-child(n+5) */ - window.klappe = function (liste, knopf, textfeld) { + window.klappe = function (liste, knopf, textfeld, wieviele = KLAPP_ZU) { if (!liste || !knopf) return; const anzahl = liste.children.length; + const stufe = KLAPP_STUFEN[wieviele] || 'zu'; + const zeigt = KLAPP_STUFEN[wieviele] ? wieviele : KLAPP_ZU; /* Ein Knopf, der nichts zu verbergen hat, verwirrt nur. */ - knopf.hidden = anzahl <= KLAPP_ZU; + knopf.hidden = anzahl <= zeigt; if (knopf.hidden) { liste.removeAttribute('data-klapp'); return; } /* Liest die Anzahl bei jedem Aufruf frisch aus der Liste, statt sie @@ -86,13 +192,13 @@ an einer Liste mit dreissig Zeilen. */ const stand = (offen) => { if (offen) liste.removeAttribute('data-klapp'); - else liste.dataset.klapp = 'zu'; + else liste.dataset.klapp = stufe; knopf.setAttribute('aria-expanded', offen ? 'true' : 'false'); /* Der Knopf sagt, was er TUT, nicht wo man gerade ist -- und nennt die Zahl, damit man weiss, was dahintersteckt. */ if (textfeld) { textfeld.textContent = offen - ? `Nur die letzten ${KLAPP_ZU}` + ? `Nur die letzten ${zeigt}` : `Alle ${liste.children.length} zeigen`; } }; diff --git a/workspace/assets/js/leistung.js b/workspace/assets/js/leistung.js index 0274a014..f8c8b1e4 100644 --- a/workspace/assets/js/leistung.js +++ b/workspace/assets/js/leistung.js @@ -271,6 +271,22 @@ } ziel.append(zeile); } + + /* ZUGEKLAPPT AUF SIEBEN TAGE (07.09.2026, Wunsch Filipe). + + Vierzehn Zeilen sind zu viel fuers Hinsehen und zu wenig fuers + Nachschlagen -- man scrollt an ihnen vorbei, statt sie zu lesen. + Sieben ist die Woche: der kuerzeste Zeitraum, ueber den man + ueberhaupt etwas sagen kann, und genau der, mit dem der + Wochenbericht rechnet. Die restlichen sieben Tage sind einen + Klick entfernt und verschwinden nicht. + + Die Sieben steht nicht hier, sondern als eigene Regel im CSS + (`[data-klapp="zu7"]`) -- der Schalter kennt beide Stufen und + benennt sie im Knopftext, damit niemand raten muss, wie viel + gerade verborgen ist. */ + window.klappe?.(ziel, document.getElementById('tage-schalter'), + document.getElementById('tage-schalter-text'), 7); } function wert(name, text) { diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js index 6a0fc903..295f218f 100644 --- a/workspace/assets/js/start.js +++ b/workspace/assets/js/start.js @@ -278,6 +278,46 @@ 0: 'Wochenende', }; + /* ---------- Die Uhr in der Begrüßungskachel ------------------------- + Zwei Ringe um dieselbe Mitte: der äußere zeigt die Minute in der + Stunde, der innere die Sekunde. Beides als SVG-Kreis mit + `stroke-dasharray` -- ein Kegelverlauf (conic-gradient) hätte an + der Naht eine sichtbare Kante, ein Kreisbogen nicht. + + UMFANG = 2 * pi * 44 = 276.46. Die Zahl steht hier ausgerechnet und + nicht als Formel im Stil: Ändert jemand den Radius im HTML, fällt + der Ring sofort auf, statt sich stillschweigend zu verrechnen. + + KEIN setInterval(…, 1000). Ein Takt von genau einer Sekunde läuft + mit der Zeit aus dem Tritt (jeder Durchlauf kostet ein paar + Millisekunden), und die Anzeige springt dann gelegentlich zwei + Sekunden weiter. Stattdessen wird jedes Mal bis zur NÄCHSTEN vollen + Sekunde gewartet -- die Anzeige bleibt für immer im Takt der + Systemuhr. */ + const UHR_UMFANG = 276.46; + function uhrStarten() { + const zeit = $('uhr-zeit'), sek = $('uhr-sek'); + const zeiger = $('uhr-zeiger'), minute = $('uhr-minute'); + if (!zeit || !zeiger) return; + const p = (n) => String(n).padStart(2, '0'); + + const takt = () => { + const j = new Date(); + zeit.textContent = `${p(j.getHours())}:${p(j.getMinutes())}`; + sek.textContent = p(j.getSeconds()); + /* Voll = kein Versatz. Der Ring wächst im Uhrzeigersinn, weil das + SVG im Stil um 90 Grad zurückgedreht ist. */ + zeiger.style.strokeDashoffset = String(UHR_UMFANG * (1 - j.getSeconds() / 60)); + if (minute) minute.style.strokeDashoffset = String(UHR_UMFANG * (1 - j.getMinutes() / 60)); + /* Der volle Zeitpunkt für Vorleseprogramme -- sichtbar steht dort + nur die Uhrzeit, vorgelesen wird der ganze Satz. */ + const uhr = $('uhr'); + if (uhr) uhr.setAttribute('aria-label', `${p(j.getHours())}:${p(j.getMinutes())} Uhr`); + setTimeout(takt, 1000 - (Date.now() % 1000) + 8); + }; + takt(); + } + function grussZeigen() { const jetzt = new Date(); const stunde = jetzt.getHours(); @@ -391,6 +431,7 @@ window.werZeigen?.(ich); grussZeigen(); + uhrStarten(); /* Die Hinweise zuerst -- die Kacheln brauchen ihre Zahlen. Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 2a4fff8b..ad663fa4 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,9 @@ - - - + + + @@ -211,10 +211,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 4777c355..a90558bd 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -134,10 +134,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 6889fbd5..2dcd8d81 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -143,14 +143,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 5a2b38ab..6ccbae68 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -76,11 +76,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index c995870d..875f4432 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -9,16 +9,16 @@ - - + + - - + + @@ -154,9 +154,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index 18215a9f..3db4f087 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -166,12 +166,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 5a21e1e0..ebb1ef41 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -95,10 +95,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index 25422b80..465048ab 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -27,7 +27,7 @@ - + @@ -173,6 +173,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 2eb42fea..2385e3f6 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -257,11 +257,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index e73258f9..fe7a3501 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -77,7 +77,19 @@ @@ -199,10 +211,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index e9304afe..f3446405 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -123,10 +123,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 00148e22..af5cb926 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -201,11 +201,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 3c76c7a0..0960292f 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -93,11 +93,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 1ba30d96..229b594d 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index e856a71f..a725c6dd 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -9,8 +9,8 @@ - - + + @@ -51,6 +51,33 @@

+ + +
+
+ + +
+ + + +
+ --:-- + -- +
+
+
@@ -160,10 +187,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index a9297dac..4e4b067d 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -72,11 +72,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 2cb92182..ca7ea8fa 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -113,10 +113,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index c6a1e8bd..9e338d26 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -9,9 +9,9 @@ - - - + + + @@ -82,10 +82,10 @@ - + - - - + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index ed158d93..d2f20127 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -9,10 +9,10 @@ - - - - + + + + @@ -177,10 +177,10 @@ - - - - - + + + + +