Die Gestaltung faerbt jetzt auch die Bausteine
Gefunden beim Nachbau von kant-it.de: eine Website mit eigener Palette
behielt das Gruen der Vorlage. Symbole, Knoepfe, Rahmen, ruhiger Text,
Fokusring und der Einwilligungsdialog kamen aus festen Token in site.css,
die das Theme nie gesetzt hat - die Farben liessen sich einstellen und
taten nichts.
Das Theme setzt --ink, --paper, --accent, --muted, --line und --mint jetzt
selbst. Als Akzent dient die Linkfarbe, nicht ein Palettenname: das Token
faerbt auch kleine Beschriftungen, und eine helle Akzentfarbe waere dort
nicht lesbar. --muted und --line werden aus Textton und Hintergrund
gemischt; eine Palettenfarbe gleichen Namens gewinnt.
Der Einwilligungsdialog nimmt die Primaerfarbe und laesst sich eigens
faerben ueber eine Palettenfarbe mit dem Schluessel "consent".
Ausserdem aus demselben Nachbau: Ueberschriften werden nicht mehr getrennt
("di-gitale Exzellenz"), und Fliesstext auf dunkel gefaerbten Abschnitten
mischt seine Farbe aus der geerbten statt aus einem Token fuer helle
Hintergruende.
Geprueft: 381 Tests unter PHP 8.2 und 8.4, axe ohne Verstoss auf vier
Seiten des Nachbaus in Desktop- und Handybreite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e34b4dca67
commit
efa285c0d0
6 files changed
+118
-20
No files matched your search
@@ -440,6 +440,26 @@ Vom Verweis bleibt nur der Host. Die vollständige Adresse, aus der jemand kam,
|
||||
|
||||
**Aufbewahrung:** nach 60 Tagen werden die Stunden eines Tages zu einer Zeile verschmolzen, nach 425 Tagen wird gelöscht. Gut ein Jahr, damit ein Vergleich mit dem Vorjahresmonat möglich bleibt. `cms:stats:prune` macht beides und ist für einen Cronjob gedacht.
|
||||
|
||||
## ADR 042 – Der Nachbau von kant-it.de, und was er gefunden hat (12.09.2026)
|
||||
|
||||
Umut: „Baue mal lokal kant-it.de komplett nach. Ich möchte sehen ob das funktioniert.“ Gebaut ist die Startseite mit allen neun Abschnitten, die Projekteseite, vier Projektbeiträge, Impressum und Datenschutzgerüst, Kopf- und Fußbereich, zwei Menüs, das Kontaktformular, zwölf Bilder und die Palette der echten Website – in einer eigenen Datenbank, damit die Entwicklungsinstallation unberührt bleibt.
|
||||
|
||||
**Das Ergebnis der Übung sind nicht die Bilder, sondern fünf Fehler, die sonst ein Kunde gefunden hätte.** Eine echte Website zu bauen ist eine andere Prüfung als ein Test: sie benutzt alles gleichzeitig.
|
||||
|
||||
**1. Die Gestaltung färbte die Bausteine nicht.** Symbole, Knöpfe, Rahmen, ruhiger Text und der Einwilligungsdialog kamen aus festen Token in `site.css` – eine Website mit roter Palette behielt das Grün der Vorlage. Die Farben ließen sich einstellen und taten nichts, dieselbe Familie wie Rahmenstärke und Bildrundung (ADR 036). Das Theme setzt jetzt `--ink`, `--paper`, `--accent`, `--muted`, `--line` und `--mint` selbst. Als Akzent dient die **Linkfarbe**, nicht ein Palettenname: dieses Token färbt auch kleine Beschriftungen, und eine helle Akzentfarbe wäre dort nicht mehr lesbar. `--muted` und `--line` werden aus Textton und Hintergrund gemischt; eine Palettenfarbe gleichen Namens gewinnt.
|
||||
|
||||
**2. Der Einwilligungsdialog war grün**, unabhängig von der Website. Er nimmt jetzt die Primärfarbe – und lässt sich eigens färben, indem im Design eine Farbe mit dem Schlüssel `consent` angelegt wird. Kein zusätzlicher Schalter, nur eine Regel, die eine Palettenfarbe bevorzugt.
|
||||
|
||||
**3. „Di-gitale Exzellenz“.** `hyphens:auto` galt für h1 bis h3. Im Fließtext ist Trennung nützlich, in einer 64-Pixel-Überschrift sieht sie nach einem Defekt aus – und genau dort fällt sie auf. Überschriften trennen nicht mehr.
|
||||
|
||||
**4. Ruhiger Text auf dunklem Grund.** `.block-text` hatte fest `color:var(--muted)`, eine Farbe für helle Hintergründe. In einem dunkel gefärbten Abschnitt stand damit Grau auf Marineblau. Jetzt wird die Farbe aus der geerbten gemischt (`currentColor` zu 82 Prozent), womit dieselbe Regel auf hellem und dunklem Grund funktioniert. Dasselbe gilt für die automatische Kleinbeschriftung des ersten Textblocks: sie mischt den Akzent in die geerbte Farbe, statt ihn zu setzen.
|
||||
|
||||
**5. Ein Kontrastfehler, den ich selbst eingebaut hatte.** Die erste Ableitung von `--muted` mischte zu 72 Prozent und ergab für eine Palette mit hellem Textton 4,09:1 statt 4,5:1. axe hat ihn im selben Durchlauf gefunden, in dem die Farben zum ersten Mal wirkten. Jetzt 80 Prozent.
|
||||
|
||||
**Was sich nicht als Fehler herausstellte, aber auffällt:** Ecken und Seiten müssen einzeln angegeben werden (`borderRadius` als Kurzschreibweise wird abgewiesen) – der Designer schreibt das ohnehin so, von Hand ist es überraschend. Und die Beschriftung eines Einwilligungsfeldes ist auf 120 Zeichen begrenzt; der Einwilligungssatz der echten Website hat 148. Der Rest musste in den Hilfetext.
|
||||
|
||||
**Was der Nachbau belegt:** die Seite läuft mit **null axe-Verstößen** auf vier Seiten in Desktop- und Handybreite, **ohne JavaScript**, mit Bildern als WebP in vier Breiten und einer Startseite, die dem Original so nahe kommt, dass der Unterschied in der Typografie liegt und nicht in der Technik. Was fehlt, ist ehrlich zu benennen: die vollständige Datenschutzerklärung (das Gerüst ist da, der Text der laufenden Website ist länger), die übrigen Projektbeiträge (vier von fünf Seiten Archiv) und die Unterseiten von HausKlar und KantTresor.
|
||||
|
||||
## ADR 041 – Der Installer: sechs Bildschirme, und danach ist er weg (12.09.2026)
|
||||
|
||||
Umut: „Einen Installer brauche ich noch wie wordpress schritt für schritt alles einrichten migration ausführen usw.“ und, später, „einen installer das eine php ist und alle dateien herunterladet“. Beides ist gebaut, und beides hängt an derselben Frage: was darf eine Datei dürfen, die auf einem leeren Webspace liegt und von jedem aufrufbar ist?
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Qualitätsbericht – erster ausführbarer Prototyp
|
||||
|
||||
## Ergänzung: kant-it.de nachgebaut, 12.09.2026
|
||||
|
||||
- **381 PHP-Tests / 1619 Assertions** unter PHP 8.2.12 und 8.4.22, 17 JavaScript-Modelltests, Playwright-Editortest, öffentliche Browserprüfung.
|
||||
- **Der Nachbau ist die Prüfung:** Startseite mit neun Abschnitten, Projekteseite, vier Beiträge, Impressum, Datenschutzgerüst, Kopf- und Fußbereich, zwei Menüs, Kontaktformular, zwölf Bilder, eigene Palette und Schrift – gegen eine eigene Datenbank und einen eigenen Medienordner, nicht gegen die Entwicklungsinstallation.
|
||||
- **Gefunden dabei: fünf Fehler** (ADR 042), darunter der größte: die Palette färbte Symbole, Knöpfe, Rahmen und den Einwilligungsdialog überhaupt nicht. Vier davon sind behoben und durch Tests abgesichert, der fünfte war mein eigener Kontrastfehler beim Beheben des ersten.
|
||||
- **axe: null Verstöße** auf Startseite, Projekteseite, Beitrag und Impressum, in 1440 px und 390 px – gemessen an der nachgebauten Seite, nicht an der Demo.
|
||||
- Gemessen am Nachbau: Startseite 45 KB HTML, 33 KB CSS, rund 430 KB Bilder in WebP, **0 Skripte**, 21 Abfragen. Die Abfragezahl ist die höchste bisher gemessene und erklärt sich aus der Beitragsliste; sie liegt unter der Obergrenze, ist aber der nächste Kandidat fürs Zusammenfassen.
|
||||
- **Nicht geprüft:** die vollständige Datenschutzerklärung der laufenden Website (nur das Gerüst), die restlichen Projektbeiträge, die Produktseiten von HausKlar und KantTresor, und nichts davon auf echtem Hosting. Der Nachbau ist ein Abbild, keine Migration: die Inhalte sind neu geschrieben, nicht aus WordPress exportiert.
|
||||
|
||||
## Ergänzung: Installer, 12.09.2026
|
||||
|
||||
- **379 PHP-Tests / 1606 Assertions** unter PHP 8.2.12 und 8.4.22, 17 JavaScript-Modelltests, Playwright-Editortest, öffentliche Browserprüfung.
|
||||
|
||||
@@ -27,6 +27,7 @@ Erster Commit angelegt (`82b0782`); das Projekt hat damit einen Rückfallpunkt.
|
||||
- Adresshierarchie: neue Spalten `path`, `parent_id`, `type`, `created_at` über Migration `20260912000400`; websiteweit eindeutiger Pfad statt eindeutigem Slug, Tiefe bis vier Ebenen, Beiträge ohne Unterseiten, reservierte Adressen und Adressableitung aus dem Titel. Öffentliche Route bedient verschachtelte Pfade wie `/leistungen/webdesign`; ein Lookahead hält `/admin`, `/media` und `/assets` aus dem Auffangmuster.
|
||||
- **Ladeverhalten gemessen statt vermutet** (ADR 039, Arbeitspaket M): ein Zähler liefert die Datenbankabfragen je Anfrage im Kopf `X-Kant-Queries` (außerhalb der Produktion). Die erste Messung zeigte 18 Abfragen auf der Startseite, weil Einstellungen und Theme je Aufrufer erneut gelesen wurden; beide merken sich ihren Stand jetzt für die Dauer einer Anfrage – **18 → 8**. Die Browserprüfung misst je Seite HTML, CSS, Anfragen, Skripte und Abfragen gegen Obergrenzen: Startseite 7,4 KB HTML, 22,7 KB CSS, 2 Anfragen, 0 Skripte.
|
||||
- **Das erste Bild einer Seite lädt sofort**, alles darunter weiterhin verzögert. Es ist meistens das größte Ding über der Falz.
|
||||
- **Die Gestaltung färbt jetzt auch die Bausteine** (ADR 042): Symbole, Knöpfe, Rahmen, ruhiger Text, Fokusring und Einwilligungsdialog nehmen die Farben des Themes statt fester Werte aus `site.css`. Der Einwilligungsdialog lässt sich zusätzlich eigens färben über eine Palettenfarbe mit dem Schlüssel `consent`. Überschriften werden nicht mehr getrennt.
|
||||
- **Installer (Arbeitspaket N, ADR 041):** `/install` führt in sechs Schritten von einer leeren Installation zu einer Website – Systemprüfung, Datenbank (SQLite oder MySQL, Verbindung sofort geprüft), Migration im Prozess ohne Konsole, Website mit Startseite, Kopf-/Fußbereich, Menü sowie Impressum und Datenschutzgerüst, erstes Administratorkonto, Abschlussliste. Solange keine Installation existiert, führt jede Adresse dorthin; sobald ein Konto existiert, antwortet `/install` mit 404. Davor liegt `scripts/install.php`: eine einzelne Datei, die das Releasearchiv mit fester Adresse und fester SHA-256 lädt, gegen ZIP-Slip gesichert entpackt und sich danach selbst löscht.
|
||||
- **Fassungsverlauf, Einsendungen und Weiterleitungen blättern** statt bei 30, 50 beziehungsweise 2000 Einträgen stillschweigend aufzuhören.
|
||||
- **Sicherung und Wiederherstellung** (ADR 038, Arbeitspaket O): `cms:backup` schreibt Datenbank (über `VACUUM INTO`, nicht als Dateikopie) und Medienordner nach `var/backups/`, mit Manifest samt Prüfsumme, und rotiert die alten. In der Verwaltung gibt es die Übersicht mit Zustand je Sicherung und einen Knopf zum Anlegen. **Zurückspielen nur auf der Konsole** (`cms:restore`): Prüfsumme, Fassungsvergleich, Sicherheitskopie des aktuellen Stands, Wartungszustand, Datenbank ersetzen, Medien **ergänzen** statt ersetzen. Nicht enthalten: `.env`, `vendor/`, Quellcode – steht so im Manifest.
|
||||
|
||||
+24
-20
@@ -1,8 +1,12 @@
|
||||
@charset "UTF-8";
|
||||
h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
|
||||
/* Lange Wörter dürfen umbrechen, damit nichts aus der Spalte läuft. Getrennt wird nur im
|
||||
Fließtext: eine Überschrift mit Trennstrich („di-gitale") sieht nach Fehler aus. */
|
||||
h1,h2,h3,h4,h5,h6,p,a{overflow-wrap:break-word}
|
||||
h1,h2,h3,h4,h5,h6{hyphens:manual}
|
||||
p{hyphens:auto}
|
||||
.block-image{margin:0;max-width:100%}.block-image img{max-width:100%;height:auto}
|
||||
:root{color-scheme:light;--paper:#f7f8f4;--ink:#182d2b;--muted:#50625d;--line:#d7dfd7;--mint:#e5f0b6;--accent:#315b49;font-family:Arial,Helvetica,sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
|
||||
*{box-sizing:border-box}body{margin:0}a{color:inherit;text-underline-offset:5px}a:hover{text-decoration-thickness:2px}:focus-visible{outline:3px solid #b85c12;outline-offset:6px}main:focus{outline:none}.shell{width:min(var(--k-container,1180px),calc(100% - 64px));margin-inline:auto}.site-header{border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:5px;font-weight:800;font-size:28px;letter-spacing:-1.3px;text-decoration:none}.brand-icon{width:37px;height:37px;background:var(--ink);color:var(--mint);border-radius:10px;display:grid;place-items:center;margin-right:7px;font-size:27px;letter-spacing:-2px}.brand-light{font-weight:400}.site-header nav{display:flex;gap:32px;align-items:center;font-size:14px;font-weight:600}.site-header nav a{text-decoration:none;padding:12px 0}.site-header nav [aria-current=page]{text-decoration:underline}.site-header nav .nav-link{border:1px solid var(--ink);padding:13px 18px;border-radius:7px}.nav-link span{margin-left:18px}.block-section{padding:70px 0}.block-section:first-child{padding:74px 0 82px}.section-inner>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 30px}h1,h2,h3{letter-spacing:-.05em;line-height:1.05;margin:0 0 24px}h1{font-size:clamp(48px,6.6vw,84px);font-weight:500;max-width:900px}h2{font-size:clamp(32px,4vw,49px);font-weight:500;max-width:760px}h3{font-size:26px;letter-spacing:-.035em;line-height:1.2}.block-text{font-size:18px;line-height:1.7;max-width:690px;margin:0 0 23px;color:var(--muted);white-space:pre-line}.button{display:inline-flex;align-items:center;gap:32px;min-height:50px;margin-top:9px;padding:15px 21px;background:var(--ink);color:var(--paper);border-radius:7px;text-decoration:none;font-size:14px;font-weight:600}.button:hover{background:#294b41}.block-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr));gap:22px;margin-top:38px;background:transparent!important}.block-column{border:1px solid var(--line);border-radius:12px;padding:30px;min-width:0}.block-column .block-text{font-size:15px}.block-column>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--accent);margin-bottom:34px}.tone-mint{background:var(--mint)}.tone-ink{background:var(--ink);color:var(--paper)}.tone-ink .block-text{color:#dae3da}.tone-ink .section-inner>.block-text:first-child{color:var(--mint)}.tone-ink .button{color:var(--ink);background:var(--mint)}.width-narrow>.section-inner{max-width:800px}.site-footer{border-top:1px solid var(--line)}.site-footer p,.site-footer>span{font-size:12px;color:var(--muted)}.site-footer .brand{font-size:22px}.skip-link{position:fixed;left:16px;top:-100px;z-index:10;background:var(--ink);color:white;padding:16px}.skip-link:focus{top:16px}img{display:block;max-width:100%;height:auto;border-radius:12px}.error-page{padding-block:100px}.error-page h1{max-width:800px}
|
||||
*{box-sizing:border-box}body{margin:0}a{color:inherit;text-underline-offset:5px}a:hover{text-decoration-thickness:2px}:focus-visible{outline:3px solid var(--accent);outline-offset:6px}main:focus{outline:none}.shell{width:min(var(--k-container,1180px),calc(100% - 64px));margin-inline:auto}.site-header{border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:5px;font-weight:800;font-size:28px;letter-spacing:-1.3px;text-decoration:none}.brand-icon{width:37px;height:37px;background:var(--ink);color:var(--mint);border-radius:10px;display:grid;place-items:center;margin-right:7px;font-size:27px;letter-spacing:-2px}.brand-light{font-weight:400}.site-header nav{display:flex;gap:32px;align-items:center;font-size:14px;font-weight:600}.site-header nav a{text-decoration:none;padding:12px 0}.site-header nav [aria-current=page]{text-decoration:underline}.site-header nav .nav-link{border:1px solid var(--ink);padding:13px 18px;border-radius:7px}.nav-link span{margin-left:18px}.block-section{padding:70px 0}.block-section:first-child{padding:74px 0 82px}.section-inner>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:color-mix(in srgb,var(--accent) 68%,currentColor);margin:0 0 30px}h1,h2,h3{letter-spacing:-.05em;line-height:1.05;margin:0 0 24px}h1{font-size:clamp(48px,6.6vw,84px);font-weight:500;max-width:900px}h2{font-size:clamp(32px,4vw,49px);font-weight:500;max-width:760px}h3{font-size:26px;letter-spacing:-.035em;line-height:1.2}.block-text{font-size:18px;line-height:1.7;max-width:690px;margin:0 0 23px;color:color-mix(in srgb,currentColor 82%,transparent);white-space:pre-line}.button{display:inline-flex;align-items:center;gap:32px;min-height:50px;margin-top:9px;padding:15px 21px;background:var(--ink);color:var(--paper);border-radius:7px;text-decoration:none;font-size:14px;font-weight:600}.button:hover{background:color-mix(in srgb,var(--ink) 88%,var(--paper))}.block-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr));gap:22px;margin-top:38px;background:transparent!important}.block-column{border:1px solid var(--line);border-radius:12px;padding:30px;min-width:0}.block-column .block-text{font-size:15px}.block-column>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:1px;color:color-mix(in srgb,var(--accent) 68%,currentColor);margin-bottom:34px}.tone-mint{background:var(--mint)}.tone-ink{background:var(--ink);color:var(--paper)}.tone-ink .block-text{color:color-mix(in srgb,var(--paper) 86%,var(--ink))}.tone-ink .section-inner>.block-text:first-child{color:var(--mint)}.tone-ink .button{color:var(--ink);background:var(--mint)}.width-narrow>.section-inner{max-width:800px}.site-footer{border-top:1px solid var(--line)}.site-footer p,.site-footer>span{font-size:12px;color:var(--muted)}.site-footer .brand{font-size:22px}.skip-link{position:fixed;left:16px;top:-100px;z-index:10;background:var(--ink);color:white;padding:16px}.skip-link:focus{top:16px}img{display:block;max-width:100%;height:auto;border-radius:12px}.error-page{padding-block:100px}.error-page h1{max-width:800px}
|
||||
@media(max-width:650px){.shell{width:calc(100% - 36px)}.site-header nav{width:100%;justify-content:space-between;gap:12px;font-size:12px}.site-header nav .nav-link{padding:10px}.nav-link span{margin-left:6px}.block-section,.block-section:first-child{padding-block:48px}h1{font-size:49px}.block-text{font-size:16px}.block-column{padding:26px}.site-footer{align-items:flex-start;flex-direction:column;gap:10px}.site-footer p{margin:0}.block-row{gap:16px}}
|
||||
|
||||
/* Menus. Submenus open with details/summary, so the navigation works without JavaScript. */
|
||||
@@ -45,7 +49,7 @@ h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
|
||||
.block-quote blockquote{margin:0}
|
||||
.block-quote blockquote p{margin:0;font-size:18px;line-height:1.65}
|
||||
.block-quote figcaption{font-size:14px;font-weight:600}
|
||||
.block-quote figcaption span{font-weight:400;opacity:.75}
|
||||
.block-quote figcaption span{font-weight:400;opacity:.85}
|
||||
.quote-rating{margin:0;letter-spacing:3px}
|
||||
.block-logos{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));align-items:center;gap:24px}
|
||||
.block-logos img{max-height:52px;width:auto;margin-inline:auto;display:block}
|
||||
@@ -71,7 +75,7 @@ h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
|
||||
.tabs-nav [aria-selected=true]{border-color:currentColor;background:#fff}
|
||||
.tabs-ready .tabs-nav a{cursor:pointer}
|
||||
.block-tabitem{padding-block:4px}
|
||||
.block-tabitem:focus-visible{outline:3px solid #b55b12;outline-offset:4px}
|
||||
.block-tabitem:focus-visible{outline:3px solid var(--accent);outline-offset:4px}
|
||||
.tabs-heading{margin:0 0 12px;font-size:17px}
|
||||
|
||||
/* Forms. Server rendered, no JavaScript; every message sits at its field. */
|
||||
@@ -84,7 +88,7 @@ h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
|
||||
.field-error{margin:0;font-size:13px;font-weight:600;color:#8b2f22}
|
||||
.field-note{margin:0;font-size:13px;opacity:.8}
|
||||
.form-error-summary{margin:0;padding:14px 18px;border-radius:8px;background:#fff0eb;color:#8b2f22;border:1px solid #e2b2a7;font-weight:600}
|
||||
.form-success{margin:0;padding:16px 20px;border-radius:8px;background:#edf3dc;color:#315b49;border:1px solid #c5d89a;font-weight:600}
|
||||
.form-success{margin:0;padding:16px 20px;border-radius:8px;background:color-mix(in srgb,var(--accent) 10%,var(--paper));color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 35%,var(--paper));font-weight:600}
|
||||
.form-missing{margin:0;padding:14px 18px;border:1px dashed currentColor;border-radius:8px}
|
||||
.check-row{display:flex;align-items:flex-start;gap:11px}
|
||||
.check-row input{width:20px;min-height:20px;flex-shrink:0;margin-top:3px}
|
||||
@@ -103,27 +107,27 @@ h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
|
||||
unreadable - 1.87:1 in the seeded footer. These two selectors match that specificity and come
|
||||
later, so the dark tone wins where it has to. */
|
||||
.tone-ink .block-column .block-text,
|
||||
.tone-ink .block-column>.block-text:first-child{color:#dae3da}
|
||||
.tone-ink .block-column>.block-text:first-child{color:color-mix(in srgb,var(--paper) 86%,var(--ink))}
|
||||
|
||||
/* The consent question. Fixed to the bottom, but it never covers the whole page: the content
|
||||
stays readable while the decision is open, because a wall would make "accept" the only way
|
||||
forward. Accepting and refusing are the same button in the same row. */
|
||||
.consent-bar{position:fixed;inset-inline:12px;bottom:12px;z-index:50;max-width:620px;margin-inline:auto;background:#fff;color:#203b34;border:1px solid #cfd8d1;border-radius:14px;padding:20px 22px;box-shadow:0 18px 50px #1b2a2426;font-size:14px;line-height:1.6}
|
||||
.consent-bar{position:fixed;inset-inline:12px;bottom:12px;z-index:50;max-width:620px;margin-inline:auto;background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:14px;padding:20px 22px;box-shadow:0 18px 50px color-mix(in srgb,var(--ink) 15%,transparent);font-size:14px;line-height:1.6}
|
||||
.consent-bar h2{font-size:16px;margin:0 0 8px}
|
||||
.consent-bar p{margin:0 0 14px}
|
||||
.consent-actions{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.consent-button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:11px 18px;border-radius:8px;border:1px solid #2c6b4f;background:#2c6b4f;color:#fff;font:inherit;font-weight:600;cursor:pointer}
|
||||
.consent-actions .consent-button:last-child{background:#fff;color:#1b2a24;border-color:#b3c0b7}
|
||||
.consent-button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:11px 18px;border-radius:8px;border:1px solid var(--k-consent,var(--accent));background:var(--k-consent,var(--accent));color:#fff;font:inherit;font-weight:600;cursor:pointer}
|
||||
.consent-actions .consent-button:last-child{background:var(--paper);color:var(--ink);border-color:var(--line)}
|
||||
.consent-button:hover{filter:brightness(.94)}
|
||||
.consent-button:focus-visible,.consent-bar a:focus-visible,.consent-bar summary:focus-visible,.consent-bar input:focus-visible{outline:3px solid #b55b12;outline-offset:2px}
|
||||
.consent-button:focus-visible,.consent-bar a:focus-visible,.consent-bar summary:focus-visible,.consent-bar input:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
|
||||
.consent-details{margin-top:14px}
|
||||
.consent-details>summary{cursor:pointer;font-size:13px;font-weight:600;padding:4px 0}
|
||||
.consent-groups{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:10px}
|
||||
.consent-groups label{display:flex;gap:10px;align-items:flex-start;font-size:13px;line-height:1.5}
|
||||
.consent-groups input{margin-top:3px;width:18px;height:18px;flex-shrink:0}
|
||||
.consent-choice{margin-top:14px;background:#fff;color:#1b2a24;border-color:#b3c0b7}
|
||||
.consent-choice{margin-top:14px;background:var(--paper);color:var(--ink);border-color:var(--line)}
|
||||
.consent-links{margin:14px 0 0;font-size:12px}
|
||||
.consent-links a{color:#a4510f}
|
||||
.consent-links a{color:var(--accent)}
|
||||
/* While the question is open the bar is fixed at the bottom, so the page has to end above it -
|
||||
otherwise it covers the last element, and an embed's own consent button sat right underneath.
|
||||
Found by the browser test, which could not click it. */
|
||||
@@ -134,11 +138,11 @@ body.consent-open{padding-bottom:260px}
|
||||
@media (max-width:620px){.consent-bar{inset-inline:0;bottom:0;border-radius:14px 14px 0 0;padding:16px}.consent-actions .consent-button{flex:1 1 140px}}
|
||||
|
||||
/* An embedded third party, and what stands in its place until it may load. */
|
||||
.block-embed iframe{display:block;width:100%;border:0;border-radius:10px;background:#eef1ed}
|
||||
.embed-blocked{border:1px dashed #b3c0b7;border-radius:10px;padding:24px;background:#f6f7f4;text-align:center}
|
||||
.block-embed iframe{display:block;width:100%;border:0;border-radius:10px;background:color-mix(in srgb,var(--ink) 8%,var(--paper))}
|
||||
.embed-blocked{border:1px dashed var(--line);border-radius:10px;padding:24px;background:color-mix(in srgb,var(--ink) 4%,var(--paper));text-align:center}
|
||||
.embed-title{margin:0 0 8px;font-weight:600}
|
||||
.embed-hint{margin:0 0 10px;font-size:14px;line-height:1.6;color:#4a5751}
|
||||
.embed-provider{margin:0 0 16px;font-size:13px;color:#4a5751}
|
||||
.embed-hint{margin:0 0 10px;font-size:14px;line-height:1.6;color:var(--muted)}
|
||||
.embed-provider{margin:0 0 16px;font-size:13px;color:var(--muted)}
|
||||
|
||||
/* Header and footer hold block documents of their own (ADR 026). They are therefore ordinary
|
||||
block containers - as flex containers they shrank the section inside them until a single word
|
||||
@@ -158,8 +162,8 @@ body.consent-open{padding-bottom:260px}
|
||||
.site-footer .block-column>.block-text:first-child{font-size:14px;font-weight:400;letter-spacing:0;text-transform:none;margin:0}
|
||||
/* One landmark for the three links that must be on every page. Loose text outside a landmark is
|
||||
also what axe objects to, and rightly: a screen reader then has no way to skip to it. */
|
||||
.legal-bar{padding:16px 20px;text-align:center;font-size:12px;color:var(--muted);background:#f1f3ef}
|
||||
.legal-bar a{color:#a4510f}
|
||||
.legal-bar{padding:16px 20px;text-align:center;font-size:12px;color:var(--muted);background:color-mix(in srgb,var(--ink) 6%,var(--paper))}
|
||||
.legal-bar a{color:var(--accent)}
|
||||
|
||||
/* An image takes the rounding of its figure. Without this a radius on an image block rounded the
|
||||
figure and left the picture square - which looked like the setting did nothing. */
|
||||
@@ -178,7 +182,7 @@ body.consent-open{padding-bottom:260px}
|
||||
.block-progress{margin:0 0 22px}
|
||||
.progress-label{display:flex;justify-content:space-between;gap:16px;margin:0 0 8px;font-size:14px;font-weight:600}
|
||||
.progress-value{color:var(--muted);font-weight:400}
|
||||
.progress-track{height:10px;border-radius:999px;background:#e2e7e1;overflow:hidden}
|
||||
.progress-track{height:10px;border-radius:999px;background:color-mix(in srgb,var(--ink) 12%,var(--paper));overflow:hidden}
|
||||
.progress-fill{height:100%;border-radius:999px;background:var(--accent)}
|
||||
.tone-ink .progress-track{background:#ffffff26}
|
||||
.tone-ink .progress-fill{background:var(--mint)}
|
||||
@@ -189,7 +193,7 @@ body.consent-open{padding-bottom:260px}
|
||||
.block-slider.slider-snap{scroll-snap-type:x mandatory}
|
||||
.slider-snap>.slider-slide{scroll-snap-align:start}
|
||||
.slider-slide{min-width:0}
|
||||
.block-slider:focus-visible{outline:3px solid #b85c12;outline-offset:6px}
|
||||
.block-slider:focus-visible{outline:3px solid var(--accent);outline-offset:6px}
|
||||
.slide-desktop-1{grid-auto-columns:100%}.slide-desktop-2{grid-auto-columns:calc(50% - 10px)}
|
||||
.slide-desktop-3{grid-auto-columns:calc(33.333% - 14px)}.slide-desktop-4{grid-auto-columns:calc(25% - 15px)}
|
||||
.slide-desktop-5{grid-auto-columns:calc(20% - 16px)}.slide-desktop-6{grid-auto-columns:calc(16.666% - 17px)}
|
||||
|
||||
@@ -269,6 +269,26 @@ final class Theme
|
||||
$variables[] = sprintf('--k-container:%s', $base['containerWidth']);
|
||||
|
||||
$colour = static fn (string $v): string => str_starts_with($v, '@') ? sprintf('var(--k-%s)', substr($v, 1)) : $v;
|
||||
|
||||
// Die Bausteine selbst - Knopf, Symbol, Rahmen, ruhiger Text - wurden aus festen Werten in
|
||||
// site.css gefaerbt. Eine Website mit eigener Palette behielt damit das Gruen der Vorlage:
|
||||
// die Farben liessen sich einstellen und taten nichts. Hier bekommen diese Token ihre
|
||||
// Werte aus dem Theme. Als Akzent dient die Linkfarbe und nicht ein Palettenname, weil
|
||||
// dieses Token auch kleine Beschriftungen faerbt - eine helle Akzentfarbe waere dort
|
||||
// nicht mehr lesbar.
|
||||
$ink = $colour($base['color']);
|
||||
$paper = $colour($base['backgroundColor']);
|
||||
$variables[] = sprintf('--ink:%s', $ink);
|
||||
$variables[] = sprintf('--paper:%s', $paper);
|
||||
$variables[] = sprintf('--accent:%s', $colour($base['linkColor']));
|
||||
// Abgeleitet statt erfunden: ruhiger Text und Linien sind der Textton, in den Hintergrund
|
||||
// gemischt. Ein Palettenwert gleichen Namens hat Vorrang, damit sich beides setzen laesst.
|
||||
// 80 Prozent, nicht weniger: bei 72 kam fuer eine Palette mit hellem Textton ein Grau
|
||||
// heraus, das die 4,5:1 knapp verfehlte - gefunden beim Nachbau von kant-it.de.
|
||||
$variables[] = sprintf('--muted:var(--k-muted,color-mix(in srgb,%s 80%%,%s))', $ink, $paper);
|
||||
$variables[] = sprintf('--line:var(--k-line,color-mix(in srgb,%s 20%%,%s))', $ink, $paper);
|
||||
$variables[] = sprintf('--mint:var(--k-mint,color-mix(in srgb,%s 14%%,%s))', $colour($base['linkColor']), $paper);
|
||||
|
||||
$scale = (float) $base['headingScale'];
|
||||
$headings = '';
|
||||
// A modular scale from the base size, so one number changes the whole hierarchy.
|
||||
|
||||
@@ -2034,6 +2034,50 @@ final class CmsTest extends TestCase
|
||||
$this->pages->saveDraft('page-1', $document, 0);
|
||||
}
|
||||
|
||||
public function testTheThemeColoursTheBlocksAndNotOnlyTheText(): void
|
||||
{
|
||||
// Beim Nachbau von kant-it.de blieben Symbole, Knöpfe, Rahmen und der Einwilligungsdialog
|
||||
// im Grün der Vorlage, obwohl die Palette rot und marineblau war: site.css färbte sie aus
|
||||
// festen Token, die das Theme nie gesetzt hat. Die Farben ließen sich einstellen und taten
|
||||
// nichts - dieselbe Familie wie Rahmenstärke und Bildrundung.
|
||||
$theme = $this->themes->load();
|
||||
$this->themes->save(
|
||||
[...$theme['colors'], ['key' => 'firma', 'name' => 'Firmenrot', 'value' => '#c1121f']],
|
||||
$theme['fonts'],
|
||||
[...$theme['base'], 'color' => '#0a192f', 'backgroundColor' => '#ffffff', 'linkColor' => '@firma'],
|
||||
);
|
||||
$css = $this->themes->css($this->themes->load());
|
||||
|
||||
// Die Token, aus denen site.css Knöpfe, Symbole, Linien und ruhigen Text färbt.
|
||||
self::assertStringContainsString('--ink:#0a192f', $css);
|
||||
self::assertStringContainsString('--paper:#ffffff', $css);
|
||||
self::assertStringContainsString('--accent:var(--k-firma)', $css);
|
||||
self::assertStringContainsString('--muted:var(--k-muted,color-mix(in srgb,#0a192f 80%,#ffffff))', $css);
|
||||
self::assertStringContainsString('--line:var(--k-line,color-mix(in srgb,#0a192f 20%,#ffffff))', $css);
|
||||
|
||||
// Und die Gegenprobe in der Datei selbst: keine feste Farbe mehr für Symbol, Knopf und
|
||||
// Einwilligungsdialog - Fehlerrot bleibt fest, denn ein Fehler ist rot, kein Firmenton.
|
||||
$site = (string) file_get_contents(__DIR__.'/../public/assets/site.css');
|
||||
self::assertStringContainsString('.block-icon{display:inline-flex;align-items:center;justify-content:center;color:var(--accent)', $site);
|
||||
self::assertStringContainsString('background:var(--accent)', $site);
|
||||
self::assertStringNotContainsString('#2c6b4f', $site, 'Der Einwilligungsdialog nimmt die Farbe der Website.');
|
||||
// Und er lässt sich eigens färben: eine Palettenfarbe mit dem Schlüssel „consent“ gewinnt
|
||||
// über die Primärfarbe, ohne dass es dafür eine weitere Einstellung braucht.
|
||||
self::assertStringContainsString('background:var(--k-consent,var(--accent))', $site);
|
||||
self::assertStringNotContainsString('outline:3px solid #b85c12', $site, 'Auch der Fokusring folgt der Palette.');
|
||||
}
|
||||
|
||||
public function testHeadingsAreNotHyphenated(): void
|
||||
{
|
||||
// Beim Nachbau von kant-it.de stand in der Überschrift „di-gitale Exzellenz“: `hyphens:auto`
|
||||
// galt für h1 bis h3. Im Fließtext ist Trennung nützlich, in einer Überschrift sieht sie
|
||||
// nach einem Fehler aus - und genau dort fällt sie auf, weil die Schrift groß ist.
|
||||
$css = (string) file_get_contents(__DIR__.'/../public/assets/site.css');
|
||||
self::assertStringContainsString('h1,h2,h3,h4,h5,h6{hyphens:manual}', $css);
|
||||
self::assertStringContainsString('p{hyphens:auto}', $css);
|
||||
self::assertStringNotContainsString('hyphens:auto}', explode('p{hyphens:auto}', $css)[0], 'Vor der Regel für Absätze trennt nichts.');
|
||||
}
|
||||
|
||||
public function testTheGlobalContentWidthCanMakeThePageWiderNotOnlyNarrower(): void
|
||||
{
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', DemoContent::pages()['home'], 0), 1);
|
||||
|
||||
Reference in new issue
Block a user