Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
245f189d57 | ||
|
|
99b3aba39a | ||
|
|
c91f996524 | ||
|
|
0ed4c83e41 | ||
|
|
9e0365f841 | ||
|
|
ff2b57db58 | ||
|
|
741eaa80f7 | ||
|
|
23fdca4edb |
No files matched your search
@@ -4,6 +4,10 @@ Selbst betreibbares Block-CMS für PHP-Webhosting mit visuellem Editor nach dem
|
||||
|
||||
Die übernommenen Originaldokumente sind `AGENTS.md` und `ENTWICKLUNGSPLAN.md`. Den tatsächlichen Stand enthält `docs/STATUS.md`, Entscheidungen und Datenmodell stehen in `docs/ARCHITECTURE.md`.
|
||||
|
||||
## Bildecken unter Design (ab 1.21.0)
|
||||
|
||||
Wie stark die Ecken der Bilder gerundet sind, steht jetzt unter **Design → Bildecken** und gilt für die ganze Website – Bilder, Galerien, Logoleisten und Beitragslisten. Vorgabe ist `12px` wie bisher, `0px` macht sie eckig. Ein einzelnes Bild lässt sich davon abweichend einstellen: im Editor beim ausgewählten Bild unter **Design → Bild → Bildecken**, dort auch je Gerät und je Ecke. Vorher stand die Rundung fest im Stylesheet und war in keinem Feld zu finden.
|
||||
|
||||
## Kaufmännisches Und und Zeilenumbrüche bleiben, wie sie sind (ab 1.20.1)
|
||||
|
||||
**Behoben.** Ein Text ohne Auszeichnung wurde beim Speichern doppelt kodiert: aus `&` wurde `&`, beim nächsten Speichern `&amp;` – irgendwann stand die Entity sichtbar auf der Seite. Ebenso wuchsen Zeilenumbrüche: jedes zweite Speichern machte aus einem `<br>` zwei. Beides ist behoben, und wiederholtes Speichern ändert einen Text jetzt nicht mehr. Was bereits falsch gespeichert ist, räumt `php bin/console cms:repair:entities` auf: ohne Option zeigt er nur, was sich ändern würde, mit `--schreiben` wendet er es an. Angefasst werden die veröffentlichte Fassung und der Entwurf jeder Seite sowie die Bereiche; ältere Fassungen bleiben Historie.
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# Projektstatus
|
||||
|
||||
## Startseite nach der Installation: eine Baustellenseite statt einer Anleitung (16.09.2026)
|
||||
|
||||
**Anlass:** Umut: „Kannst du bitte für die Erstseite bzw. nach der Installation eine schöne Erstseite erstellen – Under Construction oder so, ohne Details mit ‚melde dich an‘ usw. Am besten auch noch Logo von KantSites: ‚Hier entsteht eine neue Webpräsenz mit KantSites‘.“
|
||||
|
||||
**Was los war.** `DemoContent::starterPage()` lieferte zwei Abschnitte mit Arbeitsanweisungen an die Betreiberin oder den Betreiber: „Melde dich an, öffne sie im Editor …“ und „Die nächsten Schritte“ mit Design, Menüs und Einstellungen. Das stand auf der einzigen Seite, die bis zum ersten Bearbeiten **öffentlich** erreichbar ist – gelesen hat es also der Besucher, nicht der Betreiber. Für den stehen dieselben Schritte längst auf dem letzten Installerbildschirm (`templates/install/done.html.twig`) und in der Verwaltung.
|
||||
|
||||
**Neu.** Eine Baustellenseite, die genau das sagt und sonst nichts: das KantSites-Zeichen (`public/assets/kantsites-logo.svg`, dieselbe Form wie das Favicon), die Kleinzeile „In Arbeit“ in der Akzentfarbe, als einzige H1 der Name der Website, darunter „Hier entsteht eine neue Webpräsenz.“, ein kurzer Trennstrich und die Zeile „Erstellt mit KantSites“. Der Abschnitt ist ein Flex-Container mit `min-height: 70vh` und einem Verlauf von `@paper` nach `@mint`, stellt den Inhalt also mittig auf den Bildschirm; die Farben sind Palettenverweise und folgen jedem Designwechsel. Alles sind gewöhnliche Bausteine mit gewöhnlichen Stilwerten – im Editor also vollständig änder- und löschbar. Die Seitenbeschreibung ist jetzt ein echter Satz statt „NOCH AUSZUFÜLLEN“, damit eine frisch installierte Website in Suchergebnissen nicht mit einem Platzhalter steht.
|
||||
|
||||
**Unverändert:** Kopf- und Fußbereich, die beiden Rechtsseiten als Gerüst mit markierten Pflichtangaben, Menüs.
|
||||
|
||||
**Geprüft:** `tests/InstallFlowTest.php` (Installation über die sechs Bildschirme, danach liefert `/` die neue Seite), `php vendor/bin/phpunit` 579 von 579. Im Browser angesehen: eine frisch migrierte Datenbank mit dem Inhalt einer Neuinstallation, Desktop und 375 px.
|
||||
|
||||
## Fassung 1.21.0: Bildecken gehören dem Designbereich (16.09.2026)
|
||||
|
||||
**Anlass:** Umut zum Hero-Bild von DEEG: „Das sollte abgerundet sein, aber wurde von irgendwo gemacht, und die Ecken sehen komisch aus. Es gibt auch im Editor gar nicht Bildecken abrunden, oder?“
|
||||
|
||||
**Was los war.** `.block-image{border-radius:12px}` stand fest in `enhancements.css` – jedes Bild jeder Installation war gerundet, ohne dass ein Feld das gesagt hätte. Auf DEEG (eine durchgehend eckige Vorlage) fiel es auf, weil über dem gerundeten Bild eckige Elemente liegen: der dunkle Verlauf, das Schild, die blaue Aufruf-Fläche. An den Ecken schaute deshalb die eckige Fläche aus der Rundung heraus.
|
||||
|
||||
**Behoben.** Die Ecken sind jetzt eine Einstellung unter **Design → Bildecken** (`theme.base.imageRadius`, geprüft wie die Inhaltsbreite, ausgegeben als `--k-image-radius`). Vorgabe bleibt `12px`, damit bestehende Websites gleich aussehen; `0px` macht alle Bilder eckig. Für ein einzelnes Bild gab es die Einstellung schon immer – im Editor unter **Design → Bild → Bildecken** (`imageRadius` im Stilschema, für Bild, Galerie, Logoleiste und Beitragsliste); sie überschreibt den Wert der Website.
|
||||
|
||||
**DEEG:** `imageRadius` auf `0px`, im Aufbauskript und in der Installation. Das Hero-Bild ist damit eckig wie die Vorlage, und die Ecken stimmen wieder.
|
||||
|
||||
**Geprüft:** neuer Test in `tests/CmsTest.php`: Vorgabe ist `12px`, eine Seite mit Bild trägt `--k-image-radius:12px`, nach dem Speichern von `0px` trägt sie `--k-image-radius:0px`, und „rund“ wird abgewiesen wie jede andere Nicht-Größenangabe.
|
||||
|
||||
## Fassung 1.20.1: das kaufmännische Und, das bei jedem Speichern wuchs (16.09.2026)
|
||||
|
||||
**Anlass:** Umut: „Beim Startseitenbild wird der Text & umgewandelt, aber in der Ansicht nach dem Speichern wird & daraus.“
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.block-gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;margin-block:24px}.block-gallery>.block-image{min-width:0;margin:0}.block-gallery img{width:100%;aspect-ratio:4/3;object-fit:cover}.block-image figcaption{font-size:14px;line-height:1.5;padding:12px 0;color:inherit}.fx-shadow-none{box-shadow:none}.fx-shadow-soft{box-shadow:0 8px 24px #142e251f}.fx-shadow-strong{box-shadow:0 16px 40px #142e2540}.block-image{border-radius:12px}.block-image img{border-radius:inherit}.block-gallery:empty{min-height:32px}.fx-hover-zoom{overflow:hidden}.fx-hover-zoom img{transform-origin:center}.tone-ink figure{color:var(--paper)}
|
||||
.block-gallery{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;margin-block:24px}.block-gallery>.block-image{min-width:0;margin:0}.block-gallery img{width:100%;aspect-ratio:4/3;object-fit:cover}.block-image figcaption{font-size:14px;line-height:1.5;padding:12px 0;color:inherit}.fx-shadow-none{box-shadow:none}.fx-shadow-soft{box-shadow:0 8px 24px #142e251f}.fx-shadow-strong{box-shadow:0 16px 40px #142e2540}.block-image{border-radius:var(--k-image-radius,12px)}.block-image img{border-radius:inherit}.block-gallery:empty{min-height:32px}.fx-hover-zoom{overflow:hidden}.fx-hover-zoom img{transform-origin:center}.tone-ink figure{color:var(--paper)}
|
||||
@media(max-width:1024px){.block-gallery{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:767px){.block-gallery{grid-template-columns:minmax(0,1fr);gap:16px}}
|
||||
.fx-hover-lift,.fx-hover-zoom img{transition:transform .25s ease}.fx-hover-lift:hover,.fx-hover-lift:focus-within{transform:translateY(-4px)}.fx-hover-zoom:hover img,.fx-hover-zoom:focus-within img{transform:scale(1.035)}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128"><rect width="128" height="128" rx="32" fill="#182d2b"/><path d="M36 28h16v42l26-28h22L68 76l34 28H78L52 82v22H36z" fill="#e5f0b6"/></svg>
|
||||
|
After Width: | Height: | Size: 225 B |
+39
-8
@@ -6,6 +6,21 @@ h1,h2,h3,h4,h5,h6{hyphens:manual}
|
||||
p{hyphens:auto}
|
||||
.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.block-image{margin:0;max-width:100%}.block-image img{max-width:100%;height:auto}
|
||||
:root{color-scheme:var(--k-scheme,light);--paper:#f7f8f4;--ink:#182d2b;--muted:#50625d;--line:#d7dfd7;--mint:#e5f0b6;--accent:#315b49;--error:var(--k-error,#8b2f22);--error-bg:var(--k-error-bg,#fff0eb);--error-line:var(--k-error-line,#e2b2a7);font-family:Arial,Helvetica,sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
|
||||
/* Die Hoehe der Browserleiste, die ueber dem Inhalt liegt.
|
||||
Safari auf dem iPhone schiebt seine Adressleiste beim Herunterscrollen zu einem schmalen
|
||||
Balken zusammen und legt den ueber die Seite. Ein fixiertes Element rechnet sein `bottom`
|
||||
aber gegen die volle Ansicht, nicht gegen den freien Teil - was unten klebt, steht damit in
|
||||
dem Streifen, den der Browser sich selbst genommen hat. Sichtbar ist es, aber jeder Tipp
|
||||
landet auf der Leiste. Genau das war mit dem Nach-oben-Knopf: am Schreibtisch tadellos, auf
|
||||
dem Telefon tot, auf jeder Seite jeder Installation.
|
||||
Die Differenz zwischen grosser Ansicht (`lvh`, Leiste zusammengeschoben) und kleiner Ansicht
|
||||
(`svh`, Leiste ausgefahren) ist genau diese Hoehe. Wo es keine solche Leiste gibt, ist sie
|
||||
null, und nichts verschiebt sich.
|
||||
Die Abfrage davor ist Pflicht, nicht Vorsicht: kennt ein Browser die Einheiten nicht, waere
|
||||
die Rechnung erst beim Errechnen ungueltig, und `bottom` fiele auf `auto` zurueck - das Ding
|
||||
stuende dann irgendwo. So bleibt es schlicht bei null. */
|
||||
:root{--kant-browser-chrome:0px}
|
||||
@supports (height:100lvh) and (height:100svh){:root{--kant-browser-chrome:calc(100lvh - 100svh)}}
|
||||
*{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)}.site-header.is-sticky{position:sticky;top:0;z-index:40;background:var(--paper);box-shadow:0 1px 0 var(--line),0 10px 30px color-mix(in srgb,var(--ink) 8%,transparent)}
|
||||
/* Über dem Inhalt: der Kopf nimmt keinen Platz ein, die erste Sektion beginnt unter ihm. Farbe
|
||||
und Schatten kommen aus der Gestaltung des Kopfbereichs, auch die für den gescrollten Zustand. */
|
||||
@@ -182,7 +197,9 @@ p{hyphens:auto}
|
||||
Aussehen aus den Einstellungen: `--kant-consent-*` im Kopf der Seite (Consent::styles), die
|
||||
Anordnung als Klasse. Ohne Einstellung gilt Papier, Tinte und Akzent des Themes. Ein Wert wie
|
||||
`--k-consent` aus der Palette bleibt als Zwischenstufe erhalten. */
|
||||
.consent-bar{position:fixed;inset-inline:16px;bottom:16px;z-index:50;max-width:640px;max-height:calc(100dvh - 24px);overflow:auto;margin-inline:auto;background:var(--kant-consent-bg,var(--paper));color:var(--kant-consent-ink,var(--ink));border:1px solid color-mix(in srgb,currentColor 12%,transparent);border-radius:var(--kant-consent-radius,18px);padding:22px 24px 20px;box-shadow:0 24px 60px color-mix(in srgb,var(--ink) 22%,transparent);font-size:14px;line-height:1.6;overscroll-behavior:contain}
|
||||
/* Das `+ var(--kant-browser-chrome)` hebt die Leiste ueber Safaris zusammengeschobene
|
||||
Adressleiste; sonst liegen „Annehmen“ und „Ablehnen“ unter ihr und sind nicht zu treffen. */
|
||||
.consent-bar{position:fixed;inset-inline:16px;bottom:calc(16px + var(--kant-browser-chrome));z-index:50;max-width:640px;max-height:calc(100dvh - 24px);overflow:auto;margin-inline:auto;background:var(--kant-consent-bg,var(--paper));color:var(--kant-consent-ink,var(--ink));border:1px solid color-mix(in srgb,currentColor 12%,transparent);border-radius:var(--kant-consent-radius,18px);padding:22px 24px 20px;box-shadow:0 24px 60px color-mix(in srgb,var(--ink) 22%,transparent);font-size:14px;line-height:1.6;overscroll-behavior:contain}
|
||||
/* Ein schmaler Streifen in der Akzentfarbe oben: die Fläche gehört sichtbar zur Website. */
|
||||
.consent-bar::before{content:'';position:absolute;inset:0 0 auto 0;height:4px;background:var(--kant-consent-accent,var(--k-consent,var(--accent)))}
|
||||
.kant .consent-bar h2{display:flex;align-items:center;gap:10px;font-size:17px;line-height:1.3;margin:0 0 8px}
|
||||
@@ -214,7 +231,10 @@ p{hyphens:auto}
|
||||
.consent-links a:hover{color:var(--kant-consent-accent,var(--k-consent,var(--accent)))}
|
||||
/* Leiste: über die ganze Breite am unteren Rand, auf breiten Bildschirmen Text links und Knöpfe
|
||||
rechts. Ecke: eine kleine Karte rechts unten, die den Inhalt kaum verdeckt. */
|
||||
.consent-layout-leiste{inset-inline:0;bottom:0;max-width:none;border-radius:var(--kant-consent-radius,18px) var(--kant-consent-radius,18px) 0 0;border-inline:0;border-bottom:0;padding-inline:max(24px,calc((100% - 1180px) / 2))}
|
||||
/* Klebt die Leiste an der Kante, wird sie nicht angehoben - das gaebe einen Spalt, durch den
|
||||
die Seite scheint. Stattdessen waechst ihr unterer Innenabstand um die Hoehe der
|
||||
Browserleiste, sodass die Knoepfe darueber bleiben. */
|
||||
.consent-layout-leiste{inset-inline:0;bottom:0;max-width:none;border-radius:var(--kant-consent-radius,18px) var(--kant-consent-radius,18px) 0 0;border-inline:0;border-bottom:0;padding-inline:max(24px,calc((100% - 1180px) / 2));padding-bottom:calc(20px + var(--kant-browser-chrome))}
|
||||
@media (min-width:900px){.consent-layout-leiste{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 40px;align-items:center}.consent-layout-leiste .consent-body p{margin-bottom:0}.consent-layout-leiste .consent-form{width:360px;max-width:100%}}
|
||||
.consent-layout-ecke{inset-inline:auto 16px;max-width:400px;margin:0}
|
||||
/* While the question is open the bar is fixed at the bottom, so the page has to end above it -
|
||||
@@ -226,7 +246,7 @@ body.consent-open{padding-bottom:320px}
|
||||
@media (min-width:900px){body.consent-open{padding-bottom:200px}}
|
||||
.consent-page main{max-width:680px;margin-inline:auto;padding:40px 20px}
|
||||
.consent-page .consent-bar{position:static;inset:auto;box-shadow:none;max-width:none;max-height:none;margin:24px 0 0;display:block}
|
||||
@media (max-width:620px){.consent-bar,.consent-layout-ecke{inset-inline:0;bottom:0;max-width:none;border-radius:var(--kant-consent-radius,18px) var(--kant-consent-radius,18px) 0 0;border-inline:0;border-bottom:0;padding:18px 18px 16px}.consent-layout-leiste{padding-inline:18px}}
|
||||
@media (max-width:620px){.consent-bar,.consent-layout-ecke{inset-inline:0;bottom:0;max-width:none;border-radius:var(--kant-consent-radius,18px) var(--kant-consent-radius,18px) 0 0;border-inline:0;border-bottom:0;padding:18px 18px calc(16px + var(--kant-browser-chrome))}.consent-layout-leiste{padding-inline:18px}}
|
||||
|
||||
/* 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:color-mix(in srgb,var(--ink) 8%,var(--paper))}
|
||||
@@ -438,9 +458,14 @@ body.consent-open{padding-bottom:320px}
|
||||
.kant .width-full>.section-inner{width:100%;max-width:none;margin-inline:0}
|
||||
@media (max-width:767px){.kant .width-wide>.section-inner{width:calc(100% - 36px)}}
|
||||
|
||||
/* A section that is a flex container centres its content box; the inner container must then be
|
||||
allowed to grow, or a full height section would show a strip in the middle. */
|
||||
.block-section>.section-inner{flex:1 1 auto}
|
||||
/* Ein Abschnitt, der ein Flex-Kasten ist, reiht seinen Inhaltskasten als Flex-Element ein.
|
||||
Wachsen darf der dabei nicht: die Laufrichtung ist voreingestellt die Zeile, und dort frisst
|
||||
das Wachsen die Randluft des Containers. Auf dem Telefon stand der Text eines Hero-Abschnitts
|
||||
damit hart an der Kante, waehrend Kopf und alle uebrigen Abschnitte ihre 18 px hatten - am
|
||||
Schreibtisch fiel es nicht auf, weil dort die Hoechstbreite des Designs vorher bremst.
|
||||
Die Hoehe braucht das Wachsen nicht: quer zur Laufrichtung dehnt sich ein Flex-Element
|
||||
ohnehin ueber die volle Hoehe, und steht "Ausrichtung: Mitte", ist die Mitte gewollt. */
|
||||
.block-section>.section-inner{flex:0 1 auto}
|
||||
|
||||
/* Menu kinds. Horizontal is the default; vertical stacks; burger is a details element, so it
|
||||
opens without a script - a menu that needs one would not open on a page that runs none. */
|
||||
@@ -472,9 +497,13 @@ body.consent-open{padding-bottom:320px}
|
||||
weder faerben noch abschalten. Linie und Schatten kommen dann aus Rahmen und Wirkung. */
|
||||
.site-header.is-sticky.has-own-surface{background:transparent;box-shadow:none}
|
||||
.site-header .menu-burger[open]>summary{border-radius:8px}
|
||||
/* Die Flaeche malt ihren eigenen Untergrund, also setzt sie auch ihre eigene Schriftfarbe.
|
||||
Ohne das erbte sie die Farbe des Kopfbereichs: bei einem Kopf, der ueber einem dunklen Bild
|
||||
schwebt und deshalb weiss schreibt, stand das aufgeklappte Menue weiss auf weiss. */
|
||||
.site-header .menu-burger-body{position:absolute;left:0;right:0;top:100%;z-index:30;
|
||||
padding:15px max(16px,calc((100% - var(--k-container,1180px)) / 2 + 16px)) 25px;
|
||||
background:var(--paper);box-shadow:0 15px 25px color-mix(in srgb,var(--ink) 12%,transparent)}
|
||||
background:var(--paper);color:var(--ink);
|
||||
box-shadow:0 15px 25px color-mix(in srgb,var(--ink) 12%,transparent)}
|
||||
.site-header .menu-burger-body .menu-list{gap:0}
|
||||
.site-header .menu-burger-body .menu-list a{padding:12px 10px;font-size:16px}
|
||||
/* Visibility never overwrites the block's grid/flex/display settings. */
|
||||
@@ -489,10 +518,12 @@ body.consent-open{padding-bottom:320px}
|
||||
Safari), steht er von Anfang an da: sichtbar und nutzbar ist besser als unsichtbar.
|
||||
Aussehen, Größe, Form, Seite und Schwelle kommen aus den Einstellungen. */
|
||||
.scroll-top{position:fixed;bottom:22px;right:22px;z-index:40;display:grid;place-items:center;width:var(--kant-top-size,48px);height:var(--kant-top-size,48px);border-radius:var(--kant-top-radius,999px);background:var(--kant-top-bg,var(--accent));color:var(--kant-top-ink,#fff);text-decoration:none;box-shadow:0 8px 24px color-mix(in srgb,var(--ink) 28%,transparent);font-size:calc(var(--kant-top-size,48px) / 2.4);line-height:1}
|
||||
/* Ueber die Browserleiste hinaus, sonst ist der Knopf auf dem Telefon nicht zu treffen. */
|
||||
.scroll-top{bottom:calc(22px + var(--kant-browser-chrome))}
|
||||
.scroll-top-links{right:auto;left:22px}
|
||||
.scroll-top:hover{filter:brightness(1.08)}
|
||||
.scroll-top:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
|
||||
@media (max-width:767px){.scroll-top{bottom:16px;right:16px}.scroll-top-links{left:16px;right:auto}}
|
||||
@media (max-width:767px){.scroll-top{bottom:calc(16px + var(--kant-browser-chrome));right:16px}.scroll-top-links{left:16px;right:auto}}
|
||||
/* Solange die Einwilligungsfrage offen ist, bleibt der Knopf da. Frueher verschwand er ganz -
|
||||
und auf dem Telefon bleibt die Frage oft einfach stehen, der Knopf kam dort also nie.
|
||||
Bis 799 px deckt die Leiste die Ecke ab, in der er steht: dort setzt er sich per
|
||||
|
||||
@@ -283,7 +283,7 @@ $theme->save(
|
||||
['key' => 'body', 'name' => 'Fließtext', 'family' => 'Arial', 'source' => 'system', 'weights' => [400, 600, 700], 'fallback' => 'sans-serif'],
|
||||
['key' => 'heading', 'name' => 'Überschriften', 'family' => 'Arial', 'source' => 'system', 'weights' => [400, 700], 'fallback' => 'sans-serif'],
|
||||
],
|
||||
['fontFamily' => 'body', 'headingFontFamily' => 'heading', 'fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@anthrazit', 'backgroundColor' => '@papier', 'linkColor' => '@anthrazit', 'containerWidth' => '1320px', 'headingScale' => 1.25, 'linkUnderline' => false],
|
||||
['fontFamily' => 'body', 'headingFontFamily' => 'heading', 'fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@anthrazit', 'backgroundColor' => '@papier', 'linkColor' => '@anthrazit', 'containerWidth' => '1320px', 'headingScale' => 1.25, 'linkUnderline' => false, 'imageRadius' => '0px'],
|
||||
['shape' => 'square', 'appearance' => 'solid', 'size' => 'normal', 'fontFamily' => 'inherit', 'fontSize' => 14, 'fontWeight' => 600, 'letterSpacing' => 0, 'textTransform' => 'none', 'backgroundColor' => '@blau-tief', 'color' => '@papier', 'hoverBackgroundColor' => '@blau-dunkel', 'hoverColor' => '@papier'],
|
||||
);
|
||||
echo "Farben, Schriften und Buttons gesetzt\n";
|
||||
|
||||
+67
-19
@@ -122,34 +122,75 @@ final class DemoContent
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The header and footer a fresh installation starts with. They are ordinary block documents:
|
||||
* everything in them can be changed, moved or removed in the editor. No main heading, because
|
||||
* every page brings its own.
|
||||
*
|
||||
* @return array<string, array{name: string, document: array}>
|
||||
*/
|
||||
/**
|
||||
* The start page of a fresh installation - not demo content.
|
||||
*
|
||||
* Short on purpose. Somebody who has just installed should find a page they can edit, not a
|
||||
* brochure about the CMS they have to delete first. Exactly one H1, as every page requires.
|
||||
* A website that is already reachable but not yet written needs a page that says exactly that:
|
||||
* the name of the site, the notice that something is being built here, and nothing else. No
|
||||
* instructions for the owner - those stand on the last screen of the installer and in the
|
||||
* admin area, not on a page every visitor sees. Exactly one H1, as every page requires.
|
||||
*/
|
||||
public static function starterPage(string $siteName): array
|
||||
{
|
||||
return [
|
||||
'schemaVersion' => 16,
|
||||
'schemaVersion' => 24,
|
||||
'title' => $siteName,
|
||||
'description' => 'NOCH AUSZUFÜLLEN: ein Satz darüber, was es hier gibt. Er erscheint in Suchergebnissen.',
|
||||
'description' => 'Hier entsteht eine neue Webpräsenz. Diese Seite wird gerade eingerichtet.',
|
||||
'blocks' => [
|
||||
self::section('start-hero', [
|
||||
self::heading('start-heading', $siteName, 1),
|
||||
self::text('start-intro', 'Diese Seite ist der Anfang. Melde dich an, öffne sie im Editor und ersetze diesen Text durch deinen eigenen.'),
|
||||
], 'mint'),
|
||||
self::section('start-body', [
|
||||
self::heading('start-next-heading', 'Die nächsten Schritte', 2),
|
||||
self::text('start-next', "Impressum und Datenschutzerklärung sind bereits angelegt und müssen noch ausgefüllt werden – die Pflichtangaben sind darin markiert.\n\nUnter „Design“ legst du Farben, Schriften und die Breite fest, unter „Menüs“ die Navigation, unter „Einstellungen“ den Namen und die Angaben zum Unternehmen."),
|
||||
], 'paper', 'narrow'),
|
||||
[
|
||||
'id' => 'start-hero',
|
||||
'type' => 'section',
|
||||
'layout' => ['tone' => 'mint', 'width' => 'narrow'],
|
||||
'style' => [
|
||||
// Eine Seite ohne Inhalt: der Abschnitt füllt den Bildschirm und stellt das
|
||||
// Wenige, das dasteht, in die Mitte, statt es oben in eine Ecke zu setzen.
|
||||
'desktop' => [
|
||||
'display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center',
|
||||
'minHeight' => '70vh', 'textAlign' => 'center',
|
||||
'padding' => ['top' => '96px', 'bottom' => '96px'],
|
||||
'gradientFrom' => '@paper', 'gradientTo' => '@mint', 'gradientAngle' => 165,
|
||||
],
|
||||
'mobile' => ['minHeight' => 'auto', 'padding' => ['top' => '64px', 'bottom' => '64px']],
|
||||
],
|
||||
'children' => [
|
||||
[
|
||||
'id' => 'start-logo',
|
||||
'type' => 'image',
|
||||
'props' => ['src' => '/assets/kantsites-logo.svg', 'alt' => 'KantSites', 'decorative' => false, 'width' => 128, 'height' => 128],
|
||||
'style' => ['desktop' => ['maxWidth' => '86px', 'margin' => ['left' => 'auto', 'right' => 'auto', 'bottom' => '30px']]],
|
||||
],
|
||||
[
|
||||
'id' => 'start-label',
|
||||
'type' => 'text',
|
||||
'props' => ['text' => 'In Arbeit'],
|
||||
'style' => ['desktop' => [
|
||||
'fontSize' => '13px', 'fontWeight' => '700', 'letterSpacing' => '2px',
|
||||
'textTransform' => 'uppercase', 'color' => '@accent', 'margin' => ['bottom' => '16px'],
|
||||
]],
|
||||
],
|
||||
self::heading('start-heading', $siteName, 1),
|
||||
[
|
||||
'id' => 'start-intro',
|
||||
'type' => 'text',
|
||||
'props' => ['text' => 'Hier entsteht eine neue Webpräsenz.'],
|
||||
'style' => ['desktop' => [
|
||||
'fontSize' => '21px', 'maxWidth' => '38ch',
|
||||
'margin' => ['left' => 'auto', 'right' => 'auto', 'bottom' => '0px'],
|
||||
]],
|
||||
],
|
||||
[
|
||||
'id' => 'start-rule',
|
||||
'type' => 'divider',
|
||||
'style' => ['desktop' => ['width' => '72px', 'margin' => ['top' => '40px', 'right' => 'auto', 'bottom' => '22px', 'left' => 'auto']]],
|
||||
],
|
||||
[
|
||||
'id' => 'start-note',
|
||||
'type' => 'text',
|
||||
'props' => ['text' => 'Erstellt mit KantSites'],
|
||||
'style' => ['desktop' => ['fontSize' => '14px', 'letterSpacing' => '0.4px', 'margin' => ['bottom' => '0px']]],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -164,6 +205,13 @@ final class DemoContent
|
||||
return $areas;
|
||||
}
|
||||
|
||||
/**
|
||||
* The header and footer a fresh installation starts with. They are ordinary block documents:
|
||||
* everything in them can be changed, moved or removed in the editor. No main heading, because
|
||||
* every page brings its own.
|
||||
*
|
||||
* @return array<string, array{name: string, document: array}>
|
||||
*/
|
||||
public static function areas(): array
|
||||
{
|
||||
$row = static fn (string $id, array $children): array => [
|
||||
|
||||
@@ -68,6 +68,10 @@ final class Theme
|
||||
'linkColor' => '@link',
|
||||
'containerWidth' => '1240px',
|
||||
'headingScale' => 1.25,
|
||||
// Die Ecken jedes Bildes auf der Website. Stand bis 1.20.2 fest im Stylesheet -
|
||||
// sichtbar gerundet, aber in keinem Feld zu finden. 12px bleibt die Vorgabe,
|
||||
// damit bestehende Websites gleich aussehen; 0px macht Bilder eckig.
|
||||
'imageRadius' => '12px',
|
||||
// Unterstrichene Links im Fließtext. An, weil ein Link, den nur die Farbe verrät,
|
||||
// für Menschen mit Farbsehschwäche kein Link ist. Wer eine ruhigere Seite will,
|
||||
// schaltet es hier ab - dann bleibt der Unterstrich beim Überfahren.
|
||||
@@ -229,7 +233,7 @@ final class Theme
|
||||
}
|
||||
$base[$field] = $value;
|
||||
}
|
||||
foreach (['fontSize', 'containerWidth'] as $field) {
|
||||
foreach (['fontSize', 'containerWidth', 'imageRadius'] as $field) {
|
||||
$value = trim((string) ($input[$field] ?? $base[$field]));
|
||||
if (1 !== preg_match(StyleSchema::LENGTH, $value)) {
|
||||
throw new InvalidPage(sprintf('„%s“ ist keine Größenangabe wie 18px oder 70rem.', $value));
|
||||
@@ -338,6 +342,7 @@ final class Theme
|
||||
}
|
||||
$base = $theme['base'];
|
||||
$variables[] = sprintf('--k-container:%s', $base['containerWidth']);
|
||||
$variables[] = sprintf('--k-image-radius:%s', $base['imageRadius']);
|
||||
|
||||
$colour = static fn (string $v): string => str_starts_with($v, '@') ? sprintf('var(--k-%s)', substr($v, 1)) : $v;
|
||||
|
||||
|
||||
@@ -162,6 +162,11 @@
|
||||
<input id="base-scale" name="base[headingScale]" type="number" step="0.01" min="1" max="2" value="{{ theme.base.headingScale }}">
|
||||
<p class="field-help">1,25 ergibt eine ruhige Staffelung, 1,5 eine sehr kontrastreiche.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-image-radius">Bildecken</label>
|
||||
<input id="base-image-radius" name="base[imageRadius]" value="{{ theme.base.imageRadius }}" placeholder="12px">
|
||||
<p class="field-help">Gilt für jedes Bild auf der Website – auch in Galerien, Logoleisten und Beitragslisten. <code>0px</code> macht sie eckig. Ein einzelnes Bild lässt sich im Editor unter <strong>Design → Bild → Bildecken</strong> abweichend einstellen.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-container">Inhaltsbreite</label>
|
||||
<input id="base-container" name="base[containerWidth]" value="{{ theme.base.containerWidth }}" placeholder="1240px">
|
||||
|
||||
@@ -3111,6 +3111,34 @@ final class CmsTest extends TestCase
|
||||
$code->assertScriptsUnchanged($added, $before);
|
||||
}
|
||||
|
||||
public function testImageCornersComeFromTheDesignAreaAndCanBeSquare(): void
|
||||
{
|
||||
// Die Ecken jedes Bildes standen fest im Stylesheet: sichtbar gerundet, in keinem Feld
|
||||
// zu finden, nur durch einen eigenen Wert am Block loszuwerden. Jetzt gehoeren sie dem
|
||||
// Designbereich - mit derselben Vorgabe wie vorher, damit sich nichts veraendert.
|
||||
$theme = $this->themes->load();
|
||||
self::assertSame('12px', $theme['base']['imageRadius'], 'Die Vorgabe bleibt, was vorher im Stylesheet stand.');
|
||||
|
||||
$document = DemoContent::starter('Mit Bild');
|
||||
$document['blocks'][0]['children'][] = ['id' => 'bild', 'type' => 'image', 'props' => ['src' => '/assets/image-placeholder.svg', 'alt' => 'Platzhalter', 'decorative' => false, 'width' => 1000, 'height' => 600]];
|
||||
$id = $this->publishedPage('ecken', 'Ecken');
|
||||
$stand = $this->pages->findForEditor($id);
|
||||
$this->pages->publish($id, $this->pages->saveDraft($id, $document, $stand['version']), $this->pages->findForEditor($id)['version']);
|
||||
|
||||
self::assertStringContainsString('--k-image-radius:12px', $this->request('/ecken')->getContent());
|
||||
|
||||
// Eckig, wie es eine Vorlage ohne Rundungen braucht.
|
||||
$this->themes->save($theme['colors'], $theme['fonts'], [...$theme['base'], 'imageRadius' => '0px']);
|
||||
self::assertStringContainsString('--k-image-radius:0px', $this->request('/ecken')->getContent());
|
||||
|
||||
// Und was keine Groessenangabe ist, wird abgewiesen - wie bei der Inhaltsbreite.
|
||||
try {
|
||||
$this->themes->save($theme['colors'], $theme['fonts'], [...$theme['base'], 'imageRadius' => 'rund']);
|
||||
self::fail('„rund“ ist keine Groessenangabe.');
|
||||
} catch (InvalidPage) {
|
||||
}
|
||||
}
|
||||
|
||||
public function testSavingTextTwiceKeepsAmpersandsAndLineBreaksAsTheyAre(): void
|
||||
{
|
||||
// Umut: „Beim Startseitenbild wird & umgewandelt, aber nach dem Speichern steht &
|
||||
|
||||
@@ -98,7 +98,7 @@ final class InstallFlowTest extends TestCase
|
||||
$home = $this->request('/');
|
||||
self::assertSame(200, $home->getStatusCode());
|
||||
self::assertStringContainsString('Beispiel GmbH', $home->getContent());
|
||||
self::assertStringContainsString('Diese Seite ist der Anfang.', $home->getContent());
|
||||
self::assertStringContainsString('Hier entsteht eine neue Webpräsenz.', $home->getContent());
|
||||
// The public page is still a public page: no script, and the header carries the name.
|
||||
self::assertStringContainsString("script-src 'none'", (string) $home->headers->get('Content-Security-Policy'));
|
||||
self::assertSame(200, $this->request('/impressum')->getStatusCode());
|
||||
|
||||
Reference in new issue
Block a user