Add three icons, a styleable menu button and a reliable back-to-top in 1.13.0
The icon set had no drawing for people, music or a house - the three pictures
the Tagespflege Moldovan template asks for. They join as `people`, `music` and
`home`; a test keeps the list and the drawings together, because a missing path
renders an empty `<path d="">`: invisible, without an error.
An icon block's own colour never reached its drawing: the embedded SVG carries
the same `block-icon` class as its wrapper and set the accent colour again.
`.block-icon .block-icon{color:inherit}` fixes it at the root.
The menu button becomes a thing you can shape. `burgerLabel` chooses symbol and
text, text only or symbol only (which keeps the name for screen readers), and
the new style group `menu` targets the button itself, the way `img` targets an
image and `controls` the slider. Inside the header the opened menu now lies
under the header across the full width instead of pushing the header apart -
on a phone that looked like a missing navigation.
The back-to-top button no longer depends on `animation-timeline: scroll()`. A
browser that parses the property but does not run the timeline left it in the
first frame of the animation: permanently invisible. Two classes from
scrolled.js decide now, and without a script the button simply stays visible.
Also: the Tagespflege Moldovan rebuild as its own installation, built only by
`scripts/seed-tagespflege-moldovan.php`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
215ab064ae
commit
4ee8d3832c
19 files changed
+948
-15
No files matched your search
@@ -4,6 +4,14 @@ Lokaler CMS-Prototyp für PHP-Webhosting mit erstem visuellen Editor. Symfony 7.
|
||||
|
||||
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`.
|
||||
|
||||
## Mehr Symbole, Menü-Knopf gestalten, Nach-oben-Knopf zuverlässig (ab 1.13.0)
|
||||
|
||||
Der Symbolsatz hat drei Zeichnungen mehr: **Menschen**, **Musik** und **Haus**. Sie stehen überall zur Wahl, wo es Symbole gibt – Symbolbaustein, Symbolkarte und Menüeinträge. Eine am Symbolbaustein eingestellte **Farbe** wirkt jetzt auch wirklich auf die Zeichnung; bisher blieb sie in der Akzentfarbe der Website stecken.
|
||||
|
||||
Der **Menü-Knopf** (der aufklappbare „Menü“-Knopf auf Tablet und Telefon) lässt sich gestalten: Navigation auswählen → **Inhalt → Menü-Knopf** wählt zwischen *Symbol und Text*, *Nur Text* und *Nur Symbol*; „Nur Symbol“ behält den Namen für Vorlesesoftware. Unter **Design → Menü-Knopf** stehen Schriftfarbe, Fläche, Rahmenfarbe, Rahmenstärke, Ecken und Schriftgröße, je Gerät und mit Palettenfarben. Das aufgeklappte Menü legt sich im Kopfbereich als Fläche über die volle Breite unter den Kopf, statt die Kopfzeile auseinanderzuschieben.
|
||||
|
||||
Der **Nach-oben-Knopf** erscheint wieder überall. Er blendete sich über eine Scroll-Animation ein; Browser, die diese Eigenschaft zwar kennen, aber nicht ausführen, ließen ihn dauerhaft unsichtbar. Jetzt entscheidet eine Klasse, die das kleine Scroll-Skript setzt – und ohne JavaScript bleibt der Knopf einfach sichtbar.
|
||||
|
||||
## Soziale Profile (ab 1.12.0)
|
||||
|
||||
Unter **Einstellungen → Unternehmensdaten → Soziale Profile** je Netzwerk eine Adresse eintragen (Facebook, Instagram, LinkedIn, YouTube, TikTok, X, Threads, Pinterest, Xing, WhatsApp, Google-Unternehmensprofil). Das Element **Soziale Profile** (Werkzeugkasten → Bausteine) zeigt sie dann an jeder Stelle – im Kopf- oder Fußbereich, in einer Kontaktsektion – und bestimmt dort Größe, Form (nur Symbol, runde oder eckige Fläche), Anordnung, Beschriftung und ob nur bestimmte Netzwerke erscheinen. Farbe, Abstand und Ausrichtung wie bei jedem Element unter **Design**. Eine Adresse ändern heißt: überall geändert. Die Profile stehen automatisch in den strukturierten Daten (`sameAs`). Ohne Eintrag zeigt das Element auf der Website nichts.
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
Stand: 14.09.2026. Arbeitstitel KantSites; Produktname offen.
|
||||
|
||||
## ADR 094 – Drei Symbole mehr, ein Menü-Knopf mit eigener Gestalt, ein Nach-oben-Knopf ohne Scroll-Animation (14.09.2026)
|
||||
|
||||
**Symbole.** Der Satz hatte vierzehn Zeichnungen und keine für Menschen, Musik und Haus – drei Bilder, die eine Pflege- oder Vereinsseite als erstes braucht. Sie kommen als `people`, `music` und `home` hinzu, gezeichnet im selben Strich, weiter als reine Pfadangabe in `blocks/icon.html.twig`. Kein Schemawechsel: `ICONS` ist eine Werteliste, kein Bausteintyp; ein Dokument mit einem neuen Namen ist für ältere Fassungen ungültig, was für Werte dieser Art im Projekt bereits gilt. Ein Test hält Liste und Zeichnungen zusammen, denn ein fehlender Pfad ergibt ein leeres `<path d="">`: unsichtbar, ohne Fehlermeldung.
|
||||
|
||||
**Die Farbe des Symbolbausteins.** Das eingebettete SVG trägt dieselbe Klasse `block-icon` wie seine Hülle und setzte damit `color:var(--accent)` erneut – eine am Block eingestellte Farbe erreichte die Zeichnung nie, und beide bisherigen Nachbauten haben sie mit eigenem CSS überschrieben. `.block-icon .block-icon{color:inherit}` löst es an der Wurzel und trifft ausschließlich das Paar aus Hülle und Zeichnung; Symbole in Karten, Kontaktangaben und Menüs liegen nicht ineinander und bleiben unberührt.
|
||||
|
||||
**Der Menü-Knopf.** Er ist kein eigener Baustein, sondern der Knopf im Menüblock, und war bisher unveränderlich: Striche plus „Menü“, Rahmen in der Textfarbe, 8 px Ecken. Seine Beschriftung wird zu einer Eigenschaft (`burgerLabel`: `both`, `text`, `icon`) – „Nur Symbol“ behält den Namen als `visually-hidden`, denn ein Knopf ohne Namen ist für Vorlesesoftware ein Knopf ohne Zweck. Seine Farben werden zu freien Blockstilen der neuen Gruppe `menu` mit dem Ziel `burger`, wie `img` beim Bild und `controls` beim Slider: der Compiler schreibt `#id .menu-burger>summary{…}`. So bleibt die Gestaltung dort, wo jede andere Gestaltung liegt, statt eine zweite Einstellungsart zu erfinden.
|
||||
|
||||
**Das aufgeklappte Menü im Kopfbereich.** Das `details` klappte inline auf, also innerhalb der schmalen Spalte neben der Marke, und schob den Kopf auseinander; auf dem Telefon sah es aus, als fehle das Menü. Im Kopfbereich – und nur dort, erkennbar an `.site-header` – liegt die geöffnete Fläche jetzt absolut unter dem Kopf über die volle Breite. Außerhalb des Kopfbereichs bleibt es beim Aufklappen im Fluss, weil ein Menü in einer Sektion keinen positionierten Anker hat, auf den es sich verlassen könnte.
|
||||
|
||||
**Der Nach-oben-Knopf.** Er blendete sich über `animation-timeline: scroll(root)` ein, abgesichert mit `@supports`. Ein Browser, der die Eigenschaft kennt, die Zeitachse aber nicht ausführt, besteht die Abfrage und lässt den Knopf im ersten Bild der Animation stehen – bei `animation-fill-mode: both` heißt das dauerhaft unsichtbar. Statt einer Fähigkeitsabfrage entscheidet jetzt eine Klasse: `scrolled.js` setzt `kant-top-armed` sofort und `kant-top-shown` ab der eingestellten Entfernung, das Stylesheet blendet zwischen beiden Zuständen über. Ohne Skript bleibt der Knopf sichtbar – erreichbar ist besser als unsichtbar –, und das Skript wird jetzt auch geladen, wenn nur der Knopf es braucht.
|
||||
|
||||
## ADR 093 – Soziale Profile: einmal gepflegt, überall gezeigt (Dokumentschema v23, 14.09.2026)
|
||||
|
||||
Ein Link zu Instagram im Fuß, derselbe im Kopf, ein dritter in der Kontaktsektion – bisher wären das drei von Hand gesetzte Symbol-Links, und eine geänderte Adresse hätte drei Stellen zu finden. Deshalb gehören die Profile zur Website, nicht zur Seite: `SiteSettings` führt unter `social` je Netzwerk eine HTTPS-Adresse (`SocialProfiles::NETWORKS`, feste Liste, weil jedes Netzwerk ein gezeichnetes Symbol braucht), gepflegt unter Einstellungen → Unternehmensdaten. Der Baustein `social` (v23) trägt nur die Darstellung: Größe aus `ICON_SIZES` als Klasse, Form (Symbol, runde oder eckige Fläche), Anordnung, Beschriftung an oder aus, optional eine Teilmenge der Netzwerke und `newTab`. Farbe, Abstand und Ausrichtung kommen aus den freien Blockstilen wie überall. Die Vorlage liest die Profile über das Twig-Global `siteSettings`, darum braucht die Rekursion in `block.html.twig` keine neue Durchreichvariable. Ohne eingetragene Profile gibt der Block auf der öffentlichen Seite nichts aus und zeigt im Editor einen Hinweis, wo sie einzutragen sind. Die Adressen stehen zugleich in `sameAs` der strukturierten Daten – dieselbe Aussage, ohne zweite Pflege. Symbole sind eigene Umrisszeichnungen im Strich des vorhandenen Symbolsatzes, keine Markenbilder und keine fremde Datei. Kein Datenbankwechsel: `social` ist ein Schlüssel im Einstellungs-JSON und fällt auf eine leere Liste zurück.
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Projektstatus
|
||||
|
||||
## 1.13.0 – Drei Symbole mehr, gestaltbarer Menü-Knopf, Nachbau „Tagespflege Moldovan“ (14.09.2026)
|
||||
|
||||
**Anlass:** Umut ließ die ZIP-Vorlage „Tagespflege Moldovan“ nachbauen. Beim Nachbau fehlten dem Symbolsatz genau die drei Bilder, die die Vorlage zeigt; danach kamen zwei Meldungen vom Telefon: das Menü sei nicht zu sehen, und „Scroll to top“ funktioniere nicht.
|
||||
|
||||
**Nachbau (`scripts/seed-tagespflege-moldovan.php`, `scripts/reset-tagespflege-moldovan.sh`).** Der Onepager aus dem Paket vom 09.09.2026, gebaut aus gewöhnlichen Bausteinen: Kopfleiste mit Telefonzeile, klebender weißer Kopf mit Wortmarke, Hero mit der limettenen Schnittfläche hinter dem Foto (`clip-path` als eigenes CSS auf einer Fläche), Notizkarte und Bildunterschrift, Werteband, Willkommen mit Porträt-Signatur, drei Angebotskarten, Zeitleiste mit Verbindungslinie aus Flächen, dunkler Kontaktbereich mit schräger Fläche, Formularkarte und Fuß mit limettenem Abschluss. Farben, Schriftgrößen, Abstände und Umbrüche sind aus `style.css` übernommen, ebenso die drei Gerätestufen. Eigene Installation: `var/tagespflege-moldovan.db`, `var/tagespflege-moldovan-media`, Bildquellen in `var/tagespflege-moldovan-assets`, Startkonfiguration auf Port 8089. Zwei bewusste Abweichungen: das Formular verschickt serverseitig statt eine Mailto-Nachricht vorzubereiten, und das Markenzeichen liegt als PNG vor, weil die Medienverwaltung JPEG, PNG und WebP nimmt. Impressum und Datenschutz fehlen wie in der Vorlage – für einen Livegang nachzutragen.
|
||||
|
||||
**Symbole (ADR 094).** `people`, `music` und `home` ergänzen den Satz in `DocumentValidator::ICONS`, `blocks/icon.html.twig`, `editor.js` und `menu-editor.js`. Ein Test hält Liste und Zeichnungen zusammen. `.block-icon .block-icon{color:inherit}` lässt die am Block eingestellte Farbe endlich an der Zeichnung ankommen – das eingebettete SVG trug dieselbe Klasse wie seine Hülle und setzte die Akzentfarbe erneut.
|
||||
|
||||
**Menü-Knopf (ADR 094).** Neue Eigenschaft `burgerLabel` (`both`, `text`, `icon`; „Nur Symbol“ behält den Namen als `visually-hidden`) und die neue Stilgruppe `menu` mit dem Ziel `burger`: `burgerColor`, `burgerBackgroundColor`, `burgerBorderColor`, `burgerBorderWidth`, `burgerBorderRadius`, `burgerFontSize`. Der Compiler schreibt `#id .menu-burger>summary{…}`. Im Kopfbereich klappt das Menü als Fläche über die volle Breite unter den Kopf auf statt inline in der schmalen Spalte neben der Marke – das war die gemeldete „unsichtbare“ Navigation.
|
||||
|
||||
**Nach-oben-Knopf (ADR 094).** Die Einblendung hängt nicht mehr an `animation-timeline: scroll()`, sondern an den Klassen `kant-top-armed` und `kant-top-shown`, die `scrolled.js` setzt; das Skript wird jetzt auch geladen, wenn nur dieser Knopf es braucht. Ohne Skript bleibt der Knopf sichtbar.
|
||||
|
||||
**Geprüft:** PHP-Suite 498 Tests (fünf neue: Symbolliste gegen Zeichnungen, drei Knopfvarianten, abgewiesene Variante, Knopffarben am `summary`); einziger Fehlschlag ist der bekannte Altfehler `testAnyPublishedPageCanBecomeTheRoot…`. 33 JavaScript-Tests, Twig-Lint, PHP-Stilprüfung sauber, Editor-Browserprüfung vollständig erfolgreich einschließlich axe. Playwright auf dem Nachbau bei 1440, 900 und 390 px sowie als iPhone 13: Seite gegen die Vorlage verglichen, Menü öffnet als Fläche unter dem Kopf, Sprungziele, 404-Seite, keine Konsolenfehler. Nach-oben-Knopf auf der Elektro-Bernhard-Installation als iPhone geprüft: oben verborgen, ab der Schwelle sichtbar, springt an den Anfang.
|
||||
|
||||
## 1.12.1 – Elektro Bernhard gemessen: Lighthouse und axe (14.09.2026)
|
||||
|
||||
**Anlass:** Umut fragte nach Google-Bewertung, SEO, llms.txt und Barrierefreiheit der neuen Website.
|
||||
|
||||
@@ -18,6 +18,7 @@ export const GROUPS = {
|
||||
image: 'Bild',
|
||||
layout: 'Layout',
|
||||
effect: 'Wirkung',
|
||||
menu: 'Menü-Knopf',
|
||||
slider: 'Slider-Steuerung',
|
||||
};
|
||||
|
||||
|
||||
@@ -571,6 +571,7 @@ export function relevantProperty(block, key, property) {
|
||||
if (Object.values(block.style ?? {}).some(values => values[key] !== undefined || values.hover?.[key] !== undefined || values.scrolled?.[key] !== undefined)) return true;
|
||||
const container = isContainer(block);
|
||||
if (property.group === 'slider') return block.type === 'slider';
|
||||
if (property.group === 'menu') return block.type === 'menu';
|
||||
if (property.group === 'image') return ['image', 'gallery', 'logos', 'postlist'].includes(block.type);
|
||||
if (property.group === 'text' || key === 'textShadow') return !['image', 'gallery', 'logos', 'spacer', 'divider', 'embed'].includes(block.type);
|
||||
if (['flexDirection', 'flexWrap', 'justifyContent', 'alignItems', 'gridColumns', 'gap', 'rowGap', 'columnGap'].includes(key)) return container || ['button', 'menu', 'form', 'postlist', 'contact', 'social'].includes(block.type);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { initSliders } from './slider.js';
|
||||
import { VISIBILITY, setVisibility } from './editor-model.js';
|
||||
|
||||
// Mirrors DocumentValidator::ICONS; the server rejects anything outside this list.
|
||||
const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check'];
|
||||
const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check', 'people', 'music', 'home'];
|
||||
const ICON_SIZES = [12, 16, 20, 24, 32, 40, 48, 56, 64, 80, 96, 120, 160, 200, 240];
|
||||
// Mirrors SocialProfiles::NETWORKS; the addresses themselves live in the settings.
|
||||
const SOCIAL_NETWORKS = { facebook: 'Facebook', instagram: 'Instagram', linkedin: 'LinkedIn', youtube: 'YouTube', tiktok: 'TikTok', x: 'X', threads: 'Threads', pinterest: 'Pinterest', xing: 'Xing', whatsapp: 'WhatsApp', google: 'Google-Unternehmensprofil' };
|
||||
@@ -389,6 +389,7 @@ function renderContent(block, panel) {
|
||||
if (block.type === 'menu') {
|
||||
field('Menüplatz', 'slot', block.props.slot, { choices: { header: 'Hauptnavigation', secondary: 'Zweite Navigation', footer: 'Fußzeile', legal: 'Rechtliches' }, help: 'Welche Einträge gezeigt werden. Gepflegt werden sie unter Menüs.' });
|
||||
field('Menüart', 'layout', block.props.layout || 'horizontal', { choices: { horizontal: 'Waagerecht', vertical: 'Untereinander', burger: 'Aufklappbar (Burger)' }, help: 'Aufklappbar funktioniert ohne JavaScript. Untermenüs klappen in allen Arten per Klick auf, nicht beim Überfahren – das ist auf einem Touchgerät die einzige Art, die funktioniert.' });
|
||||
field('Menü-Knopf', 'burgerLabel', block.props.burgerLabel || 'both', { choices: { both: 'Symbol und Text', text: 'Nur Text', icon: 'Nur Symbol' }, help: 'Wie der aufklappbare Knopf beschriftet ist. „Nur Symbol“ behält den Namen für Vorlesesoftware. Farben, Rahmen und Schriftgröße des Knopfes stehen unter Design → Menü-Knopf.' });
|
||||
field('Automatisch als Hamburger', 'collapse', block.props.collapse || 'tablet', { choices: { tablet: 'Tablet und Mobil (Standard)', mobile: 'Nur Mobil', never: 'Nie' }, help: 'Waagerechte Menüs wechseln standardmäßig bis 1024 px und auf Touchgeräten ohne Maus, auch großen iPads. „Nur Mobil“ gilt bis 767 px. Die Menüarten „Untereinander“ und „Burger“ bleiben unverändert.' });
|
||||
}
|
||||
if (block.type === 'partialref') field('Adresse der Sektion', 'partial', block.props.partial, { help: 'Die Adresse aus der Bereichsverwaltung. Die Sektion wird verknüpft, nicht kopiert: eine Änderung dort wirkt auf allen Seiten, die sie einbinden.' });
|
||||
|
||||
@@ -7,7 +7,7 @@ const error = form.querySelector('#menu-error');
|
||||
const history = [];
|
||||
let serial = list.children.length, pendingPage = null, busy = false;
|
||||
const el = (tag, className = '', text = '') => { const node = document.createElement(tag); node.className = className; node.textContent = text; return node; };
|
||||
const iconNames = { app: 'App', web: 'Web', design: 'Gestaltung', network: 'Netzwerk', hardware: 'Computer', shop: 'Shop', support: 'Support', shield: 'Sicherheit', clock: 'Uhr', star: 'Stern', phone: 'Telefon', mail: 'E-Mail', pin: 'Standort', check: 'Haken' };
|
||||
const iconNames = { app: 'App', web: 'Web', design: 'Gestaltung', network: 'Netzwerk', hardware: 'Computer', shop: 'Shop', support: 'Support', shield: 'Sicherheit', clock: 'Uhr', star: 'Stern', phone: 'Telefon', mail: 'E-Mail', pin: 'Standort', check: 'Haken', people: 'Menschen', music: 'Musik', home: 'Haus' };
|
||||
const menuIcons = JSON.parse(form.dataset.icons || '[]');
|
||||
const chrome = () => {
|
||||
const handle = el('button', 'menu-drag-handle'); handle.type = 'button'; handle.dataset.menuDrag = '';
|
||||
|
||||
@@ -8,12 +8,27 @@
|
||||
*/
|
||||
export function initScrolled(doc = document) {
|
||||
const view = doc.defaultView;
|
||||
const root = doc.documentElement;
|
||||
// Der Nach-oben-Knopf blendete sich ueber eine Scroll-Animation ein. Browser, die
|
||||
// `animation-timeline: scroll()` zwar kennen, aber nicht ausfuehren, liessen ihn im ersten
|
||||
// Bild der Animation stehen - unsichtbar, dauerhaft. Deshalb entscheidet jetzt eine Klasse.
|
||||
// Ohne dieses Skript bleibt der Knopf sichtbar: erreichbar ist besser als unsichtbar.
|
||||
const after = parseInt(getComputedStyle(root).getPropertyValue('--kant-top-after'), 10);
|
||||
const schwelle = Number.isFinite(after) && after > 0 ? after : 400;
|
||||
let queued = false;
|
||||
const sync = () => { queued = false; doc.documentElement.classList.toggle('is-scrolled', view.scrollY > 40); };
|
||||
const sync = () => {
|
||||
queued = false;
|
||||
root.classList.toggle('is-scrolled', view.scrollY > 40);
|
||||
root.classList.toggle('kant-top-shown', view.scrollY > schwelle);
|
||||
};
|
||||
const onScroll = () => { if (!queued) { queued = true; view.requestAnimationFrame(sync); } };
|
||||
view.addEventListener('scroll', onScroll, { passive: true });
|
||||
root.classList.add('kant-top-armed');
|
||||
sync();
|
||||
return () => { view.removeEventListener('scroll', onScroll); doc.documentElement.classList.remove('is-scrolled'); };
|
||||
return () => {
|
||||
view.removeEventListener('scroll', onScroll);
|
||||
root.classList.remove('is-scrolled', 'kant-top-armed', 'kant-top-shown');
|
||||
};
|
||||
}
|
||||
|
||||
if (!document.querySelector('#editor')) initScrolled();
|
||||
+18
-6
@@ -178,6 +178,9 @@ body.consent-open{padding-bottom:260px}
|
||||
/* A single icon, sized by class because inline styles are forbidden by the policy. */
|
||||
.block-icon{display:inline-flex;align-items:center;justify-content:center;color:var(--accent);line-height:0}
|
||||
.block-icon svg{width:100%;height:100%;display:block}
|
||||
/* Das eingebettete SVG traegt dieselbe Klasse wie seine Huelle und setzte deshalb die
|
||||
Akzentfarbe erneut - eine am Symbolblock eingestellte Farbe kam nie an. */
|
||||
.block-icon .block-icon{color:inherit}
|
||||
.icon-size-12{width:12px;height:12px}.icon-size-16{width:16px;height:16px}.icon-size-20{width:20px;height:20px}
|
||||
.icon-size-24{width:24px;height:24px}.icon-size-32{width:32px;height:32px}.icon-size-40{width:40px;height:40px}
|
||||
.icon-size-48{width:48px;height:48px}.icon-size-56{width:56px;height:56px}.icon-size-64{width:64px;height:64px}
|
||||
@@ -377,6 +380,16 @@ body.consent-open{padding-bottom:260px}
|
||||
}
|
||||
.menu-burger>summary{min-height:44px;min-width:44px}
|
||||
.menu-burger-body{overflow-wrap:anywhere}
|
||||
/* Im Kopfbereich klappt das Menue als Flaeche unter dem Kopf auf, ueber die volle Breite.
|
||||
Inline geoeffnet stand es sonst in der schmalen Spalte neben der Marke und schob den
|
||||
ganzen Kopf auseinander - auf dem Telefon sah es aus, als fehle das Menue. */
|
||||
.site-header{position:relative}
|
||||
.site-header .menu-burger[open]>summary{border-radius:8px}
|
||||
.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)}
|
||||
.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. */
|
||||
.visibility-hidden{display:none!important}
|
||||
.slider-controls button[hidden]{display:none}
|
||||
@@ -395,12 +408,11 @@ body.consent-open{padding-bottom:260px}
|
||||
@media (max-width:767px){.scroll-top{bottom:16px;right:16px}.scroll-top-links{left:16px;right:auto}}
|
||||
/* Solange der Einwilligungsdialog unten steht, würde der Knopf darauf liegen. */
|
||||
body.consent-open .scroll-top{display:none}
|
||||
@supports (animation-timeline: scroll()){
|
||||
.scroll-top{animation:kant-scroll-top linear both;animation-timeline:scroll(root);animation-range:var(--kant-top-after,400px) calc(var(--kant-top-after,400px) + 160px)}
|
||||
@keyframes kant-scroll-top{from{opacity:0;visibility:hidden;transform:translateY(10px)}to{opacity:1;visibility:visible;transform:none}}
|
||||
/* Beachtet die Website die Browservorgabe (Einstellungen → Bewegung), steht er von Anfang an da. */
|
||||
@media (prefers-reduced-motion: reduce){html.motion-respect .scroll-top{animation:none}}
|
||||
}
|
||||
/* Sichtbar, sobald der Leser ein Stueck gescrollt hat - gesteuert von scrolled.js ueber
|
||||
`kant-top-armed` und `kant-top-shown`. Ohne Skript bleibt der Knopf einfach sichtbar. */
|
||||
html.kant-top-armed .scroll-top{opacity:0;visibility:hidden;transform:translateY(10px);transition:opacity .25s,visibility .25s,transform .25s}
|
||||
html.kant-top-armed.kant-top-shown .scroll-top{opacity:1;visibility:visible;transform:none}
|
||||
@media (prefers-reduced-motion: reduce){html.motion-respect .scroll-top{transition:none}}
|
||||
/* Der Bildbetrachter. Die Fläche daneben schließt, das Bild bleibt in seinen Grenzen, und die
|
||||
Bedienknöpfe sind groß genug für einen Daumen. Ohne Skript wird nichts davon gebraucht - dann
|
||||
öffnet der Verweis einfach das große Bild. */
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Wirft die Moldovan-Installation weg und baut sie neu auf. Nur fuer den Aufbau gedacht.
|
||||
set -e
|
||||
root="C:/Django Project/KantSites"
|
||||
php="${PHP_BINARY:-C:/php84/php.exe}"
|
||||
export DATABASE_URL="sqlite:///$root/var/tagespflege-moldovan.db"
|
||||
export MEDIA_DIRECTORY="$root/var/tagespflege-moldovan-media"
|
||||
rm -f "$root/var/tagespflege-moldovan.db"
|
||||
rm -rf "$root/var/tagespflege-moldovan-media"
|
||||
"$php" vendor/bin/doctrine-migrations migrate --no-interaction -q
|
||||
assets="${1:-$root/var/tagespflege-moldovan-assets}"
|
||||
"$php" -d memory_limit=512M scripts/seed-tagespflege-moldovan.php "$assets"
|
||||
@@ -0,0 +1,755 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Baut die Website „Tagespflege Jacqueline Moldovan“ als Inhalt dieses CMS auf.
|
||||
*
|
||||
* Vorlage ist das gelieferte Paket „Tagespflege_Moldovan_Website.zip“ (09.09.2026): ein Onepager
|
||||
* aus index.html, style.css und script.js. Wortlaut, Farben, Schriftgrößen, Abstände und die
|
||||
* Reihenfolge der Abschnitte sind daraus übernommen. Gebaut ist alles aus gewöhnlichen
|
||||
* Bausteinen – kein eigener Baustein, kein eigenes Skript, keine externe Schrift.
|
||||
*
|
||||
* Zwei Stellen weichen bewusst ab:
|
||||
* - Das Kontaktformular verschickt serverseitig, statt im Mailprogramm des Besuchers eine
|
||||
* vorbereitete E-Mail zu öffnen. Der Hinweistext darunter sagt das entsprechend.
|
||||
* - Das Markenzeichen liegt in der Vorlage als SVG vor; die Medienverwaltung nimmt JPEG, PNG und
|
||||
* WebP. Verwendet wird deshalb marke.png, die verlustfreie Umsetzung derselben Zeichnung.
|
||||
*
|
||||
* Das Skript schreibt ausschließlich in die Datenbank aus DATABASE_URL und in MEDIA_DIRECTORY,
|
||||
* beide müssen auf eine frische, migrierte Installation zeigen (siehe reset-tagespflege-moldovan.sh).
|
||||
* Erstes Argument: der Ordner mit gemeinsam.webp, jacqueline-moldovan.png und marke.png.
|
||||
*/
|
||||
|
||||
require dirname(__DIR__).'/config/bootstrap.php';
|
||||
|
||||
use App\Content\DocumentValidator;
|
||||
use App\Content\FormRepository;
|
||||
use App\Content\MediaLibrary;
|
||||
use App\Content\MenuRepository;
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\PartialRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use App\Content\Theme;
|
||||
use App\Infrastructure\ConnectionFactory;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$assets = $argv[1] ?? null;
|
||||
if (null === $assets || !is_dir($assets)) {
|
||||
fwrite(STDERR, "Bitte den Ordner mit den Bildern als Argument angeben.\n");
|
||||
exit(1);
|
||||
}
|
||||
$url = str_replace('%kernel.project_dir%', str_replace('\\', '/', $root), (string) $_SERVER['DATABASE_URL']);
|
||||
$mediaDirectory = str_replace('%kernel.project_dir%', $root, (string) $_SERVER['MEDIA_DIRECTORY']);
|
||||
if (!str_contains($url, 'tagespflege-moldovan')) {
|
||||
fwrite(STDERR, "Sicherung: DATABASE_URL muss auf die Moldovan-Datenbank zeigen.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$db = ConnectionFactory::create($url);
|
||||
if (false !== $db->fetchOne('SELECT id FROM page LIMIT 1')) {
|
||||
fwrite(STDERR, "Diese Datenbank enthält bereits Seiten. Abbruch, damit nichts überschrieben wird.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$media = new MediaLibrary($db, $mediaDirectory);
|
||||
$settings = new SiteSettings($db, $media);
|
||||
$pages = new PageRepository($db, new DocumentValidator(), $settings);
|
||||
$partials = new PartialRepository($db, new DocumentValidator());
|
||||
$menus = new MenuRepository($db, $settings);
|
||||
$forms = new FormRepository($db);
|
||||
$theme = new Theme($db);
|
||||
|
||||
$mail = 'info@tagespflege-moldovan.com';
|
||||
$telefon = '07221 / 922 7961';
|
||||
$telHref = 'tel:+4972219227961';
|
||||
$SCHEMA = 23;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Bausteine
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$ecken = static fn (string $r): array => ['topLeft' => $r, 'topRight' => $r, 'bottomRight' => $r, 'bottomLeft' => $r];
|
||||
$seiten = static fn (string $w): array => ['top' => $w, 'right' => $w, 'bottom' => $w, 'left' => $w];
|
||||
$nurOben = static fn (string $w): array => ['top' => $w, 'right' => '0px', 'bottom' => '0px', 'left' => '0px'];
|
||||
$nurUnten = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => $w, 'left' => '0px'];
|
||||
$nurLinks = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => '0px', 'left' => $w];
|
||||
|
||||
$section = static function (string $id, array $children, array $style = [], ?string $anchor = null): array {
|
||||
$block = ['id' => $id, 'type' => 'section', 'children' => $children, 'style' => $style];
|
||||
if (null !== $anchor) {
|
||||
$block['anchor'] = $anchor;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
/** Der Rhythmus der Vorlage: 100 px am Schreibtisch, 75 auf dem Tablet, 61 auf dem Telefon. */
|
||||
$abschnitt = static function (string $id, array $children, array $style = [], ?string $anchor = null) use ($section): array {
|
||||
$basis = [
|
||||
'desktop' => ['padding' => ['top' => '100px', 'bottom' => '100px']],
|
||||
'tablet' => ['padding' => ['top' => '75px', 'bottom' => '75px']],
|
||||
'mobile' => ['padding' => ['top' => '61px', 'bottom' => '61px']],
|
||||
];
|
||||
foreach ($style as $device => $values) {
|
||||
$basis[$device] = [...($basis[$device] ?? []), ...$values];
|
||||
}
|
||||
|
||||
return $section($id, $children, $basis, $anchor);
|
||||
};
|
||||
|
||||
$row = static function (string $id, array $children, array $design = [], array $style = []): array {
|
||||
$block = ['id' => $id, 'type' => 'row', 'children' => $children];
|
||||
if ([] !== $design) {
|
||||
$block['layout'] = ['design' => $design];
|
||||
}
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
/** Eine Zeile, die nebeneinander stellt statt zu rastern – Marke, Signatur, Wertepunkt, Zeitleiste. */
|
||||
$zeile = static function (string $id, array $children, string $gap, array $extra = [], array $mobil = [], array $tablet = []) use ($row): array {
|
||||
$style = ['desktop' => ['display' => 'flex', 'alignItems' => 'center', 'gap' => $gap, 'margin' => ['top' => '0px'], ...$extra]];
|
||||
if ([] !== $tablet) {
|
||||
$style['tablet'] = $tablet;
|
||||
}
|
||||
if ([] !== $mobil) {
|
||||
$style['mobile'] = $mobil;
|
||||
}
|
||||
|
||||
return $row($id, $children, [], $style);
|
||||
};
|
||||
|
||||
$column = static function (string $id, array $children, array $style = [], string $sichtbarkeit = 'all'): array {
|
||||
$block = ['id' => $id, 'type' => 'column', 'children' => $children];
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
if ('all' !== $sichtbarkeit) {
|
||||
$block['layout'] = ['visibility' => $sichtbarkeit];
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$heading = static function (string $id, string $text, int $level, array $style = [], string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'heading', 'props' => ['text' => $text, 'level' => $level]];
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$text = static function (string $id, string $inhalt, array $style = [], string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'text', 'props' => ['text' => $inhalt]];
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
/** Eine reine Fläche: der Strich vor einer Kleinzeile, die Linie der Zeitleiste, die grüne Form. */
|
||||
$flaeche = static function (string $id, array $style, string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'spacer', 'props' => ['size' => 'small'], 'style' => $style];
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$button = static function (string $id, string $text, string $href, array $style = []): array {
|
||||
return ['id' => $id, 'type' => 'button', 'props' => ['text' => $text, 'href' => $href], 'style' => $style];
|
||||
};
|
||||
|
||||
$bild = static function (string $id, array $eintrag, string $alt, array $style = [], ?string $href = null, string $css = ''): array {
|
||||
$gross = end($eintrag['variants']);
|
||||
$props = [
|
||||
'src' => $gross['src'], 'alt' => $alt, 'decorative' => '' === $alt, 'width' => $gross['width'], 'height' => $gross['height'],
|
||||
'mediaId' => $eintrag['id'],
|
||||
'sources' => array_map(static fn (array $v): array => ['src' => $v['src'], 'width' => $v['width']], $eintrag['variants']),
|
||||
];
|
||||
if (null !== $href) {
|
||||
$props['href'] = $href;
|
||||
$props['click'] = 'link';
|
||||
}
|
||||
|
||||
$block = ['id' => $id, 'type' => 'image', 'props' => $props, 'style' => $style];
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$symbol = static function (string $id, string $name, int $groesse, array $style = []): array {
|
||||
return ['id' => $id, 'type' => 'icon', 'props' => ['icon' => $name, 'size' => $groesse, 'decorative' => true, 'label' => ''], 'style' => $style];
|
||||
};
|
||||
|
||||
// Wiederkehrende Gestaltungswerte der Vorlage.
|
||||
$serif = ['fontFamily' => 'heading', 'fontWeight' => '400', 'letterSpacing' => '-0.045em', 'lineHeight' => 1.1];
|
||||
$h2Stil = ['desktop' => [...$serif, 'fontSize' => '53px', 'margin' => ['bottom' => '0px']], 'tablet' => ['fontSize' => '42px'], 'mobile' => ['fontSize' => '38px']];
|
||||
$emGruen = '& em{color:#718f3d;font-weight:400}';
|
||||
$fliess = ['desktop' => ['color' => '@gedaempft', 'fontSize' => '16px', 'lineHeight' => 1.65, 'margin' => ['bottom' => '20px']]];
|
||||
$klein = ['desktop' => ['fontSize' => '11px', 'fontWeight' => '700', 'letterSpacing' => '0.13em', 'color' => '#6a795e', 'lineHeight' => 1.5, 'margin' => ['bottom' => '0px']]];
|
||||
$h3Stil = ['desktop' => ['fontFamily' => 'body', 'fontSize' => '19px', 'fontWeight' => '500', 'letterSpacing' => '-0.025em', 'lineHeight' => 1.35, 'margin' => ['bottom' => '13px']]];
|
||||
|
||||
/** Die Kleinzeile über einer Überschrift: ein kurzer Strich, dann Versalien. */
|
||||
$kleinzeile = static function (string $id, string $inhalt, string $farbe = '#527236', string $strich = '@gruen', string $abstand = '25px') use ($zeile, $flaeche, $text): array {
|
||||
return $zeile($id, [
|
||||
$flaeche($id.'-strich', ['desktop' => ['width' => '24px', 'height' => '3px', 'backgroundColor' => $strich, 'flexShrink' => 0]]),
|
||||
$text($id.'-text', $inhalt, [
|
||||
'desktop' => ['fontSize' => '12px', 'fontWeight' => '700', 'letterSpacing' => '0.16em', 'textTransform' => 'uppercase', 'lineHeight' => 1.7, 'color' => $farbe, 'margin' => ['bottom' => '0px']],
|
||||
'mobile' => ['fontSize' => '11px'],
|
||||
]),
|
||||
], '11px', ['margin' => ['top' => '0px', 'bottom' => $abstand]], ['margin' => ['top' => '0px', 'bottom' => '18px']]);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Website, Gestaltung, Medien, Formular
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$db->insert('website', ['id' => 'default', 'name' => 'Tagespflege Jacqueline Moldovan', 'default_locale' => 'de', 'theme' => '{}']);
|
||||
|
||||
$theme->save(
|
||||
[
|
||||
['key' => 'tinte', 'name' => 'Tinte', 'value' => '#294655'],
|
||||
['key' => 'blau', 'name' => 'Blau', 'value' => '#2e7595'],
|
||||
['key' => 'blau-dunkel', 'name' => 'Blau dunkel', 'value' => '#245e79'],
|
||||
['key' => 'dunkelblau', 'name' => 'Kontaktbereich', 'value' => '#284f63'],
|
||||
['key' => 'gruen', 'name' => 'Grün', 'value' => '#9abf56'],
|
||||
['key' => 'limette', 'name' => 'Limette', 'value' => '#c0d348'],
|
||||
['key' => 'oliv', 'name' => 'Oliv', 'value' => '#718f3d'],
|
||||
['key' => 'marke', 'name' => 'Markengrün', 'value' => '#83a944'],
|
||||
['key' => 'gedaempft', 'name' => 'Gedämpft', 'value' => '#647078'],
|
||||
['key' => 'blass', 'name' => 'Blass', 'value' => '#f3f6ef'],
|
||||
['key' => 'leiste', 'name' => 'Kopfleiste', 'value' => '#f1f4ee'],
|
||||
['key' => 'sand', 'name' => 'Werteband', 'value' => '#fafbf8'],
|
||||
['key' => 'linie', 'name' => 'Trennlinie', 'value' => '#dce3dd'],
|
||||
['key' => 'papier', 'name' => 'Papier', 'value' => '#ffffff'],
|
||||
],
|
||||
[
|
||||
// Beide Schriften stehen auf jedem Gerät bereit; die Vorlage lädt bewusst keine Webschrift.
|
||||
['key' => 'body', 'name' => 'Fließtext', 'family' => 'Arial', 'source' => 'system', 'weights' => [400, 600, 700], 'fallback' => 'sans-serif'],
|
||||
['key' => 'heading', 'name' => 'Überschriften', 'family' => 'Georgia', 'source' => 'system', 'weights' => [400], 'fallback' => 'serif'],
|
||||
],
|
||||
['fontFamily' => 'body', 'headingFontFamily' => 'heading', 'fontSize' => '16px', 'lineHeight' => 1.65, 'color' => '@tinte', 'backgroundColor' => '@papier', 'linkColor' => '@tinte', 'containerWidth' => '1240px', 'headingScale' => 1.25, 'linkUnderline' => false],
|
||||
['shape' => 'square', 'appearance' => 'solid', 'size' => 'normal', 'fontFamily' => 'inherit', 'fontSize' => 15, 'fontWeight' => 600, 'letterSpacing' => 0, 'textTransform' => 'none', 'backgroundColor' => '@blau', 'color' => '@papier', 'hoverBackgroundColor' => '@blau-dunkel', 'hoverColor' => '@papier'],
|
||||
);
|
||||
echo "Farben, Schriften und Buttons gesetzt\n";
|
||||
|
||||
$medien = [];
|
||||
foreach ([
|
||||
'marke' => ['marke.png', 'Markenzeichen der Tagespflege Jacqueline Moldovan: ein Schmetterling in Grün und Blau'],
|
||||
'gemeinsam' => ['gemeinsam.webp', 'Illustrative Szene: Eine Seniorin und eine Begleiterin lachen gemeinsam an einem Tisch.'],
|
||||
'portraet' => ['jacqueline-moldovan.png', 'Jacqueline Moldovan'],
|
||||
] as $schluessel => [$datei, $alt]) {
|
||||
$pfad = rtrim($assets, '/\\').'/'.$datei;
|
||||
if (!is_file($pfad)) {
|
||||
fwrite(STDERR, "Bild fehlt: $pfad\n");
|
||||
exit(1);
|
||||
}
|
||||
$eintrag = $media->upload(new UploadedFile($pfad, $datei, null, null, true));
|
||||
$media->update($eintrag['id'], ['title' => pathinfo($datei, PATHINFO_FILENAME), 'alt' => $alt, 'archived' => false, 'version' => $eintrag['version']]);
|
||||
$medien[$schluessel] = $media->find($eintrag['id']);
|
||||
echo "Bild übernommen: $datei\n";
|
||||
}
|
||||
|
||||
$forms->save('kontakt', 'Kontaktanfrage', [
|
||||
['name' => 'name', 'type' => 'text', 'label' => 'Ihr Name', 'required' => true],
|
||||
['name' => 'email', 'type' => 'email', 'label' => 'E-Mail-Adresse', 'required' => true],
|
||||
['name' => 'telefon', 'type' => 'phone', 'label' => 'Telefon', 'required' => false, 'help' => 'Für einen persönlichen Rückruf'],
|
||||
['name' => 'nachricht', 'type' => 'textarea', 'label' => 'Ihre Nachricht', 'required' => true, 'help' => 'Erzählen Sie uns, was Ihnen wichtig ist.'],
|
||||
], ['recipient' => $mail, 'subject' => 'Neue Nachricht über tagespflege-moldovan.com', 'success' => 'Vielen Dank. Ihre Nachricht ist angekommen – wir melden uns bei Ihnen.', 'replyToField' => 'email'], 0);
|
||||
echo "Formular angelegt: kontakt\n";
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Kopfbereich: schmale Leiste, darunter der weiße Kopf, der beim Scrollen oben bleibt
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/** Die Wortmarke: Schmetterling, darüber „Tagespflege“, darunter der Name. */
|
||||
$wortmarke = static function (string $praefix, string $bildhoehe, string $bildbreite, string $gross, string $klein) use ($zeile, $column, $bild, $text, $medien, $seiten): array {
|
||||
return $zeile($praefix.'-zeile', [
|
||||
$column($praefix.'-bild', [
|
||||
$bild($praefix.'-logo', $medien['marke'], 'Tagespflege Jacqueline Moldovan', [
|
||||
'desktop' => ['maxWidth' => $bildbreite, 'imageHeight' => $bildhoehe, 'objectFit' => 'contain', 'margin' => $seiten('0px')],
|
||||
], '/'),
|
||||
], ['desktop' => ['flexShrink' => 0]]),
|
||||
$column($praefix.'-schrift', [
|
||||
$text($praefix.'-name', 'Tagespflege', ['desktop' => ['fontSize' => $gross, 'fontWeight' => '700', 'letterSpacing' => '-0.04em', 'lineHeight' => 1.3, 'color' => '@marke', 'margin' => ['bottom' => '0px']]]),
|
||||
$text($praefix.'-zusatz', 'Jacqueline Moldovan', ['desktop' => ['fontSize' => $klein, 'color' => '#62676b', 'lineHeight' => 1.3, 'margin' => ['top' => '3px', 'bottom' => '0px']]]),
|
||||
]),
|
||||
], '11px', ['flexShrink' => 0]);
|
||||
};
|
||||
|
||||
$kopf = [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Kopfbereich', 'description' => 'Erscheint auf jeder Seite über dem Inhalt.',
|
||||
'blocks' => [
|
||||
$section('kopf-leiste', [
|
||||
$row('kopf-leiste-zeile', [
|
||||
$column('kopf-leiste-ort', [
|
||||
$text('kopf-leiste-ort-text', 'Mitten in Baden-Baden. Mitten im Leben.', ['desktop' => ['fontSize' => '14px', 'lineHeight' => 1.5, 'margin' => ['bottom' => '0px']]]),
|
||||
], [], 'hide-mobile'),
|
||||
$column('kopf-leiste-kontakt', [
|
||||
$text('kopf-leiste-telefon', '<a href="'.$telHref.'">Ihr direkter Kontakt: '.$telefon.'</a>', [
|
||||
'desktop' => ['fontSize' => '14px', 'lineHeight' => 1.5, 'textAlign' => 'right', 'margin' => ['bottom' => '0px']],
|
||||
'mobile' => ['fontSize' => '12px', 'textAlign' => 'center'],
|
||||
], '& a:hover{text-decoration:underline}'),
|
||||
]),
|
||||
], ['desktop' => ['columns' => 'two'], 'mobile' => ['columns' => 'one']], ['desktop' => ['alignItems' => 'center', 'margin' => ['top' => '0px']]]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@leiste', 'color' => '#506054', 'padding' => ['top' => '8px', 'bottom' => '8px']],
|
||||
]),
|
||||
$section('kopf-haupt', [
|
||||
$row('kopf-haupt-zeile', [
|
||||
$column('kopf-marke', [$wortmarke('kopf-marke', '60px', '40px', '24px', '15px')], ['desktop' => ['flexShrink' => 0]]),
|
||||
$column('kopf-navigation', [
|
||||
['id' => 'kopf-menue', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'horizontal', 'collapse' => 'mobile', 'burgerLabel' => 'both'],
|
||||
'style' => ['desktop' => [
|
||||
'fontSize' => '15px', 'justifyContent' => 'flex-end', 'display' => 'flex',
|
||||
// Der Menü-Knopf im Zuschnitt der Vorlage: heller Rahmen, kaum gerundet.
|
||||
'burgerColor' => '@tinte', 'burgerBorderColor' => '#d8e0d1', 'burgerBorderRadius' => '3px', 'burgerFontSize' => '14px',
|
||||
]],
|
||||
'css' => "& .menu-list{gap:4px 29px;justify-content:flex-end}\n& .menu-list a{transition:color .2s}\n& .menu-list a:hover{color:#688f30;text-decoration:none}\n& .menu-list>li:last-child>a{border:1px solid #cfd9ce;border-radius:4px;padding:12px 19px}",
|
||||
],
|
||||
], ['desktop' => ['display' => 'flex', 'justifyContent' => 'flex-end', 'alignItems' => 'center']]),
|
||||
], [], [
|
||||
'desktop' => ['display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'space-between', 'gap' => '25px', 'minHeight' => '100px', 'margin' => ['top' => '0px']],
|
||||
'tablet' => ['minHeight' => '86px'],
|
||||
'mobile' => ['minHeight' => '80px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '#fffffff7', 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '#e7ece7', 'padding' => ['top' => '0px', 'bottom' => '0px']],
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Fußbereich
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$fuss = [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Fußbereich', 'description' => 'Erscheint auf jeder Seite unter dem Inhalt.',
|
||||
'blocks' => [
|
||||
$section('fuss', [
|
||||
$zeile('fuss-oben', [
|
||||
$column('fuss-marke', [$wortmarke('fuss-marke', '51px', '34px', '19px', '13px')], ['desktop' => ['flexShrink' => 0]]),
|
||||
$column('fuss-spruch', [
|
||||
$text('fuss-spruch-text', 'Mit Aufmerksamkeit. Mit Freude. Mit Herz.', [
|
||||
'desktop' => ['fontFamily' => 'heading', 'fontStyle' => 'italic', 'fontSize' => '17px', 'color' => '#737b78', 'textAlign' => 'center', 'margin' => ['bottom' => '0px']],
|
||||
'tablet' => ['maxWidth' => '210px'],
|
||||
'mobile' => ['fontSize' => '15px', 'textAlign' => 'left', 'maxWidth' => '100%'],
|
||||
]),
|
||||
], ['mobile' => ['order' => 3, 'flexBasis' => '100%']]),
|
||||
$column('fuss-nachoben', [
|
||||
$text('fuss-nachoben-text', '<a href="#start">Nach oben ↑</a>', [
|
||||
'desktop' => ['fontSize' => '13px', 'textAlign' => 'right', 'margin' => ['bottom' => '0px']],
|
||||
'mobile' => ['margin' => ['left' => 'auto']],
|
||||
], '& a:hover{text-decoration:underline}'),
|
||||
]),
|
||||
], '25px', ['justifyContent' => 'space-between', 'padding' => ['top' => '37px', 'bottom' => '37px']], ['flexWrap' => 'wrap', 'gap' => '22px', 'padding' => ['top' => '30px', 'bottom' => '30px']]),
|
||||
$zeile('fuss-unten', [
|
||||
$column('fuss-recht', [
|
||||
$text('fuss-copyright', '© 2026 Tagespflege Jacqueline Moldovan', ['desktop' => ['fontSize' => '12px', 'margin' => ['bottom' => '0px']], 'mobile' => ['fontSize' => '11px']]),
|
||||
]),
|
||||
$column('fuss-hinweis', [
|
||||
$text('fuss-hinweis-text', 'Gestaltungsentwurf · Beispieltexte & illustratives KI-Bild', ['desktop' => ['fontSize' => '12px', 'textAlign' => 'right', 'margin' => ['bottom' => '0px']], 'mobile' => ['fontSize' => '11px', 'textAlign' => 'left']]),
|
||||
]),
|
||||
], '20px', ['justifyContent' => 'space-between', 'alignItems' => 'flex-start', 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#e7ebe4', 'padding' => ['top' => '18px', 'bottom' => '18px'], 'color' => '#78817b'], ['flexDirection' => 'column', 'gap' => '7px']),
|
||||
], [
|
||||
'desktop' => ['borderWidth' => $nurUnten('7px'), 'borderStyle' => 'solid', 'borderColor' => '@limette', 'padding' => ['top' => '0px', 'bottom' => '0px']],
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$partials->create('header', 'default', 'Kopfbereich', $kopf);
|
||||
$partials->create('footer', 'default', 'Fußbereich', $fuss);
|
||||
echo "Kopf- und Fußbereich angelegt\n";
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Startseite
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$werte = ['Persönlich begleitet', 'Gemeinsam aktiv', 'Mit Freude durch den Tag'];
|
||||
|
||||
// Nummer, Symbol, Farbe des oberen Rands, Titel, Text – die drei Karten aus „Unser Angebot“.
|
||||
$angebote = [
|
||||
['01 / MITEINANDER', 'people', '#a9c563', '#f0f5e7', '#6b8f36', 'Gesellschaft genießen', 'Erzählen, zuhören, gemeinsam lachen. Hier können aus Begegnungen vertraute Beziehungen werden.'],
|
||||
['02 / LEBENSFREUDE', 'music', '#4285a2', '#edf5f8', '#347895', 'Mit Freude aktiv sein', 'Musik, Bewegung und kreative Momente bringen Abwechslung in den Tag. Jeder macht mit, wie es sich gut anfühlt.'],
|
||||
['03 / GEBORGENHEIT', 'home', '#bdce62', '#f0f5e7', '#6b8f36', 'Sich aufgehoben fühlen', 'Ein liebevoll gedeckter Tisch, Zeit zum Durchatmen und aufmerksame Begleitung geben dem Tag einen vertrauten Rahmen.'],
|
||||
];
|
||||
|
||||
$tagesablauf = [
|
||||
['01', 'ANKOMMEN', 'In Ruhe in den Tag starten', 'Ein freundliches Willkommen, ein gemeinsames Frühstück und Zeit für die ersten Gespräche.'],
|
||||
['02', 'GEMEINSAM ERLEBEN', 'Den Vormittag gestalten', 'Ein Spiel, etwas Bewegung oder gemeinsam kreativ werden – passend zu Lust und Möglichkeiten.'],
|
||||
['03', 'GENIESSEN & AUSRUHEN', 'Zeit für eine Pause', 'Zusammen Mittag essen und anschließend entspannen. Jeder darf seinen eigenen Rhythmus finden.'],
|
||||
['04', 'DEN TAG AUSKLINGEN LASSEN', 'Mit einem Lächeln nach Hause', 'Kaffee, eine gemütliche Runde und schöne Erinnerungen, die mit nach Hause gehen.'],
|
||||
];
|
||||
|
||||
$startseite = [
|
||||
'schemaVersion' => $SCHEMA,
|
||||
'title' => 'Tagespflege Moldovan',
|
||||
'description' => 'Tagespflege Jacqueline Moldovan in Baden-Baden. Ein Ort für Begegnung, Geborgenheit und schöne gemeinsame Tage.',
|
||||
'blocks' => [
|
||||
// 1 – Hero: Text links, Foto rechts auf einer limettenen Form, Notizkarte darüber.
|
||||
$section('hero', [
|
||||
$row('hero-zeile', [
|
||||
$column('hero-text', [
|
||||
$kleinzeile('hero-label', 'Tagespflege in Baden-Baden'),
|
||||
$heading('hero-titel', 'Ein schöner Tag.<br>Ein gutes Gefühl.<br><em>Ein Stück Zuhause.</em>', 1, [
|
||||
'desktop' => [...$serif, 'fontSize' => '68px', 'margin' => ['bottom' => '25px']],
|
||||
'tablet' => ['fontSize' => '42px'],
|
||||
'mobile' => ['fontSize' => '43px', 'lineHeight' => 1.12, 'margin' => ['bottom' => '21px']],
|
||||
], $emGruen),
|
||||
$text('hero-intro', 'Gemeinsam lachen, Neues erleben und sich gut aufgehoben fühlen. Wir geben dem Tag Leben – und Angehörigen ein wenig Freiraum.', [
|
||||
'desktop' => ['maxWidth' => '465px', 'color' => '@gedaempft', 'fontSize' => '17px', 'lineHeight' => 1.8, 'margin' => ['bottom' => '28px']],
|
||||
'mobile' => ['fontSize' => '16px', 'maxWidth' => '520px'],
|
||||
]),
|
||||
$zeile('hero-aktionen', [
|
||||
$column('hero-aktion-knopf', [
|
||||
$button('hero-knopf', 'Lernen wir uns kennen', '#kontakt', ['desktop' => ['borderRadius' => $ecken('4px'), 'margin' => ['top' => '0px']]]),
|
||||
], ['desktop' => ['flexShrink' => 0]]),
|
||||
$column('hero-aktion-link', [
|
||||
$text('hero-textlink', '<a href="#bei-uns">Unsere Tagespflege entdecken ↓</a>', [
|
||||
'desktop' => ['fontSize' => '14px', 'padding' => ['top' => '15px', 'bottom' => '15px'], 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '#cad4c8', 'margin' => ['bottom' => '0px']],
|
||||
]),
|
||||
], ['desktop' => ['flexShrink' => 0]]),
|
||||
], '18px', ['alignItems' => 'flex-start', 'flexWrap' => 'wrap'], ['rowGap' => '8px', 'columnGap' => '20px']),
|
||||
$text('hero-notiz', "Tagsüber in guter Gesellschaft.\nAbends im vertrauten Zuhause.", [
|
||||
'desktop' => ['fontSize' => '14px', 'lineHeight' => 1.6, 'color' => '#737a7a', 'borderWidth' => $nurLinks('3px'), 'borderStyle' => 'solid', 'borderColor' => '@limette', 'padding' => ['left' => '15px'], 'margin' => ['top' => '29px', 'bottom' => '0px']],
|
||||
'mobile' => ['margin' => ['top' => '20px', 'bottom' => '0px']],
|
||||
]),
|
||||
]),
|
||||
$column('hero-visual', [
|
||||
// Die limettene Form hinter dem Foto – dieselbe Schnittkante wie in der Vorlage.
|
||||
$flaeche('hero-form', [
|
||||
'desktop' => ['position' => 'absolute', 'top' => '-19px', 'right' => '-17px', 'bottom' => '35px', 'left' => '35px', 'height' => 'auto', 'width' => 'auto', 'backgroundColor' => '@limette', 'zIndex' => 0],
|
||||
'mobile' => ['top' => '-14px', 'right' => '-9px', 'bottom' => '28px', 'left' => '30px'],
|
||||
], 'clip-path:polygon(18% 0,100% 0,81% 100%,0 100%)'),
|
||||
$bild('hero-bild', $medien['gemeinsam'], 'Illustrative Szene: Eine Seniorin und eine Begleiterin lachen gemeinsam an einem Tisch.', [
|
||||
'desktop' => ['position' => 'relative', 'zIndex' => 1, 'imageHeight' => '465px', 'objectFit' => 'cover', 'objectPosition' => 'center', 'imageRadius' => ['topLeft' => '110px', 'topRight' => '6px', 'bottomRight' => '6px', 'bottomLeft' => '6px'], 'margin' => $seiten('0px')],
|
||||
'tablet' => ['imageHeight' => '390px'],
|
||||
'mobile' => ['imageHeight' => '380px', 'imageRadius' => ['topLeft' => '85px', 'topRight' => '6px', 'bottomRight' => '6px', 'bottomLeft' => '6px']],
|
||||
], null, '& img{object-position:54% 45%}'),
|
||||
$column('hero-karte', [
|
||||
$text('hero-karte-label', 'WAS WIR UNS WÜNSCHEN', $klein),
|
||||
$text('hero-karte-text', "Dass Sie gerne\n<em>wiederkommen.</em>", [
|
||||
'desktop' => [...$serif, 'fontSize' => '30px', 'lineHeight' => 1.15, 'letterSpacing' => '-0.02em', 'margin' => ['top' => '11px', 'bottom' => '15px']],
|
||||
'mobile' => ['fontSize' => '27px'],
|
||||
], '& em{color:#73933d}'),
|
||||
$flaeche('hero-karte-strich', ['desktop' => ['width' => '36px', 'height' => '3px', 'backgroundColor' => '@limette']]),
|
||||
], [
|
||||
'desktop' => ['position' => 'absolute', 'left' => '-25px', 'bottom' => '-23px', 'zIndex' => 2, 'backgroundColor' => '@papier', 'minWidth' => '245px', 'padding' => ['top' => '22px', 'right' => '28px', 'bottom' => '17px', 'left' => '28px'], 'borderWidth' => $seiten('1px'), 'borderStyle' => 'solid', 'borderColor' => '#edf0e9', 'shadowY' => '8px', 'shadowBlur' => '35px', 'shadowColor' => '#29465513'],
|
||||
'tablet' => ['left' => '-12px', 'minWidth' => '190px', 'padding' => ['top' => '18px', 'right' => '22px', 'bottom' => '14px', 'left' => '22px']],
|
||||
'mobile' => ['left' => '-12px', 'bottom' => '-25px', 'minWidth' => '190px', 'padding' => ['top' => '20px', 'right' => '25px', 'bottom' => '16px', 'left' => '25px']],
|
||||
]),
|
||||
$text('hero-caption', 'Gemeinschaft, die guttut.', [
|
||||
'desktop' => ['position' => 'absolute', 'right' => '0px', 'bottom' => '-31px', 'fontSize' => '12px', 'color' => '#737b79', 'margin' => ['bottom' => '0px']],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['position' => 'relative', 'margin' => ['left' => '10px', 'bottom' => '25px']],
|
||||
'tablet' => ['margin' => ['left' => '0px', 'bottom' => '25px']],
|
||||
'mobile' => ['margin' => ['top' => '0px', 'right' => '6px', 'bottom' => '22px', 'left' => '12px']],
|
||||
]),
|
||||
], ['desktop' => ['columns' => 'two'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '45px', 'alignItems' => 'center', 'margin' => ['top' => '0px']],
|
||||
'tablet' => ['gap' => '25px'],
|
||||
'mobile' => ['gap' => '45px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['minHeight' => '650px', 'display' => 'flex', 'alignItems' => 'center', 'padding' => ['top' => '67px', 'bottom' => '75px']],
|
||||
'tablet' => ['minHeight' => 'auto', 'padding' => ['top' => '50px', 'bottom' => '75px']],
|
||||
'mobile' => ['minHeight' => 'auto', 'padding' => ['top' => '42px', 'bottom' => '57px']],
|
||||
], 'start'),
|
||||
|
||||
// 2 – Das Band mit den drei Versprechen.
|
||||
$section('werte', [
|
||||
$row('werte-zeile', array_map(
|
||||
static fn (string $wert, int $i): array => $column('wert-'.($i + 1), [
|
||||
$symbol('wert-'.($i + 1).'-haken', 'check', 16, ['desktop' => ['color' => '#789d3e', 'flexShrink' => 0]]),
|
||||
$text('wert-'.($i + 1).'-text', $wert, ['desktop' => ['fontSize' => '15px', 'letterSpacing' => '0.015em', 'margin' => ['bottom' => '0px']], 'mobile' => ['fontSize' => '14px']]),
|
||||
], [
|
||||
'desktop' => ['display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center', 'gap' => '13px']
|
||||
+ (0 === $i ? [] : ['borderWidth' => ['top' => '0px', 'right' => '0px', 'bottom' => '0px', 'left' => '1px'], 'borderStyle' => 'solid', 'borderColor' => '#dbe2d6']),
|
||||
'mobile' => ['justifyContent' => 'flex-start', 'gap' => '16px', 'borderStyle' => 'none'],
|
||||
]),
|
||||
$werte, array_keys($werte),
|
||||
), ['desktop' => ['columns' => 'three'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['margin' => ['top' => '0px']],
|
||||
'mobile' => ['gap' => '12px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@sand', 'borderWidth' => ['top' => '1px', 'right' => '0px', 'bottom' => '1px', 'left' => '0px'], 'borderStyle' => 'solid', 'borderColor' => '@linie', 'padding' => ['top' => '22px', 'bottom' => '22px']],
|
||||
'mobile' => ['padding' => ['top' => '20px', 'bottom' => '20px']],
|
||||
]),
|
||||
|
||||
// 3 – Willkommen: Überschrift links, Text und Signatur rechts.
|
||||
$abschnitt('vorstellung', [
|
||||
$row('vorstellung-zeile', [
|
||||
$column('vorstellung-titel', [
|
||||
$kleinzeile('vorstellung-label', 'Willkommen bei uns'),
|
||||
$heading('vorstellung-h2', 'Hier zählt der Mensch.<br><em>Und das Miteinander.</em>', 2, $h2Stil, $emGruen),
|
||||
]),
|
||||
$column('vorstellung-text', [
|
||||
$text('vorstellung-lead', 'Manchmal braucht es nur ein vertrautes Gesicht, ein gutes Gespräch und das Gefühl, dazuzugehören.', [
|
||||
'desktop' => ['fontSize' => '20px', 'lineHeight' => 1.7, 'color' => '@tinte', 'margin' => ['bottom' => '20px']],
|
||||
'mobile' => ['fontSize' => '18px'],
|
||||
]),
|
||||
$text('vorstellung-p1', 'Unsere Tagespflege ist ein Ort für ältere Menschen, die ihren Tag in Gesellschaft verbringen möchten. Dabei ist uns wichtig, Raum für persönliche Gewohnheiten zu lassen: für Aktivität ebenso wie für eine ruhige Pause.', $fliess),
|
||||
$text('vorstellung-p2', 'Mit Aufmerksamkeit und einem offenen Ohr möchten wir den Alltag unserer Gäste bereichern und ihre Angehörigen entlasten.', $fliess),
|
||||
$zeile('vorstellung-signatur', [
|
||||
$column('signatur-bild', [
|
||||
$bild('signatur-portraet', $medien['portraet'], 'Jacqueline Moldovan', [
|
||||
'desktop' => ['maxWidth' => '104px', 'imageHeight' => '104px', 'objectFit' => 'cover', 'objectPosition' => 'top', 'imageRadius' => $ecken('50%'), 'imageBorderWidth' => $seiten('4px'), 'imageBorderStyle' => 'solid', 'imageBorderColor' => '@papier', 'margin' => $seiten('0px')],
|
||||
'tablet' => ['maxWidth' => '86px', 'imageHeight' => '86px'],
|
||||
'mobile' => ['maxWidth' => '86px', 'imageHeight' => '86px'],
|
||||
], null),
|
||||
], ['desktop' => ['flexShrink' => 0]]),
|
||||
$flaeche('signatur-strich', ['desktop' => ['width' => '28px', 'height' => '2px', 'backgroundColor' => '@gruen', 'flexShrink' => 0], 'tablet' => ['width' => '20px']]),
|
||||
$column('signatur-name', [
|
||||
$text('signatur-person', '<strong>Jacqueline Moldovan</strong>', ['desktop' => ['fontSize' => '15px', 'fontWeight' => '600', 'color' => '#2e6682', 'margin' => ['bottom' => '0px']]]),
|
||||
$text('signatur-rolle', 'Geschäftsführerin', ['desktop' => ['fontSize' => '13px', 'color' => '@gedaempft', 'margin' => ['top' => '4px', 'bottom' => '0px']]]),
|
||||
]),
|
||||
], '18px', ['flexWrap' => 'wrap', 'margin' => ['top' => '29px']], ['gap' => '14px']),
|
||||
]),
|
||||
], ['desktop' => ['columns' => 'two'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '80px', 'margin' => ['top' => '0px']],
|
||||
'tablet' => ['gap' => '50px'],
|
||||
'mobile' => ['gap' => '28px'],
|
||||
]),
|
||||
], [], 'bei-uns'),
|
||||
|
||||
// 4 – Unser Angebot: drei Karten auf blasser Fläche.
|
||||
$abschnitt('angebot-abschnitt', [
|
||||
$zeile('angebot-kopf', [
|
||||
$column('angebot-kopf-titel', [
|
||||
$kleinzeile('angebot-label', 'Raum für einen guten Tag'),
|
||||
$heading('angebot-h2', 'Was den Alltag<br><em>schöner macht.</em>', 2, $h2Stil, $emGruen),
|
||||
]),
|
||||
$column('angebot-kopf-text', [
|
||||
$text('angebot-intro', "Die kleinen Dinge machen den Unterschied.\nUnd Menschen, die sich Zeit nehmen.", [
|
||||
'desktop' => ['fontSize' => '15px', 'color' => '@gedaempft', 'margin' => ['bottom' => '5px']],
|
||||
'tablet' => ['maxWidth' => '260px'],
|
||||
'mobile' => ['margin' => ['top' => '23px', 'bottom' => '0px']],
|
||||
]),
|
||||
]),
|
||||
], '35px', ['alignItems' => 'flex-end', 'justifyContent' => 'space-between', 'margin' => ['top' => '0px', 'bottom' => '46px']], ['flexDirection' => 'column', 'alignItems' => 'flex-start', 'gap' => '0px', 'margin' => ['top' => '0px', 'bottom' => '30px']]),
|
||||
$row('angebot-raster', array_map(
|
||||
static function (array $a, int $i) use ($column, $text, $symbol, $heading, $h3Stil, $seiten, $ecken): array {
|
||||
$n = $i + 1;
|
||||
|
||||
return $column('angebot-'.$n, [
|
||||
$text('angebot-'.$n.'-nummer', $a[0], ['desktop' => ['fontSize' => '11px', 'fontWeight' => '600', 'letterSpacing' => '0.14em', 'color' => '#788574', 'margin' => ['bottom' => '0px']]]),
|
||||
$symbol('angebot-'.$n.'-symbol', $a[1], 32, [
|
||||
'desktop' => ['width' => '55px', 'height' => '55px', 'display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center', 'borderRadius' => $ecken('50%'), 'backgroundColor' => $a[3], 'color' => $a[4], 'margin' => ['top' => '31px', 'bottom' => '23px']],
|
||||
'mobile' => ['width' => '48px', 'height' => '48px'],
|
||||
]),
|
||||
$heading('angebot-'.$n.'-titel', $a[5], 3, $h3Stil),
|
||||
$text('angebot-'.$n.'-text', $a[6], ['desktop' => ['fontSize' => '15px', 'color' => '@gedaempft', 'lineHeight' => 1.8, 'margin' => ['bottom' => '0px']]]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@papier', 'borderWidth' => ['top' => '3px', 'right' => '0px', 'bottom' => '0px', 'left' => '0px'], 'borderStyle' => 'solid', 'borderColor' => $a[2], 'padding' => ['top' => '30px', 'right' => '29px', 'bottom' => '28px', 'left' => '29px']],
|
||||
'tablet' => ['padding' => ['top' => '23px', 'right' => '19px', 'bottom' => '23px', 'left' => '19px']],
|
||||
'mobile' => ['padding' => ['top' => '25px', 'right' => '27px', 'bottom' => '25px', 'left' => '27px']],
|
||||
]);
|
||||
},
|
||||
$angebote, array_keys($angebote),
|
||||
), ['desktop' => ['columns' => 'three'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '25px', 'margin' => ['top' => '0px']],
|
||||
'tablet' => ['gap' => '15px'],
|
||||
'mobile' => ['gap' => '17px'],
|
||||
]),
|
||||
], ['desktop' => ['backgroundColor' => '@blass']], 'angebot'),
|
||||
|
||||
// 5 – Ein Tag bei uns: Einleitung links, die Zeitleiste rechts.
|
||||
$abschnitt('tagesablauf-abschnitt', [
|
||||
$row('tag-zeile', [
|
||||
$column('tag-intro', [
|
||||
$kleinzeile('tag-label', 'Ein Tag bei uns'),
|
||||
$heading('tag-h2', 'Ein guter Rhythmus.<br><em>Ganz ohne Eile.</em>', 2, $h2Stil, $emGruen),
|
||||
$text('tag-intro-text', 'So könnte ein gemeinsamer Tag aussehen. Mit vertrauten Ritualen, neuen Impulsen und Zeit für das, was Ihnen Freude macht.', [
|
||||
'desktop' => ['maxWidth' => '375px', 'color' => '@gedaempft', 'margin' => ['top' => '27px', 'bottom' => '0px']],
|
||||
'mobile' => ['maxWidth' => '100%'],
|
||||
]),
|
||||
$bild('tag-marke', $medien['marke'], '', [
|
||||
'desktop' => ['maxWidth' => '110px', 'opacity' => 0.65, 'margin' => ['top' => '36px']],
|
||||
]) + ['layout' => ['visibility' => 'hide-mobile']],
|
||||
]),
|
||||
$column('tag-liste', array_map(
|
||||
static function (array $s, int $i) use ($zeile, $column, $text, $heading, $flaeche, $klein, $h3Stil, $ecken, $seiten, $tagesablauf): array {
|
||||
$n = $i + 1;
|
||||
$letzte = $n === count($tagesablauf);
|
||||
$links = [
|
||||
$text('tag-'.$n.'-zahl', $s[0], ['desktop' => [
|
||||
'width' => '42px', 'height' => '42px', 'flexShrink' => 0, 'display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center',
|
||||
'backgroundColor' => '#f2f6e9', 'borderWidth' => $seiten('1px'), 'borderStyle' => 'solid', 'borderColor' => '#dbe7cc', 'borderRadius' => $ecken('50%'),
|
||||
'fontSize' => '12px', 'color' => '#72943d', 'margin' => ['bottom' => '0px'],
|
||||
]]),
|
||||
];
|
||||
if (!$letzte) {
|
||||
$links[] = $flaeche('tag-'.$n.'-linie', ['desktop' => ['width' => '1px', 'height' => 'auto', 'flexGrow' => 1, 'backgroundColor' => '#dbe3d4']]);
|
||||
}
|
||||
|
||||
return $zeile('tag-schritt-'.$n, [
|
||||
$column('tag-'.$n.'-spur', $links, ['desktop' => ['width' => '43px', 'flexShrink' => 0, 'display' => 'flex', 'flexDirection' => 'column', 'alignItems' => 'center']]),
|
||||
$column('tag-'.$n.'-inhalt', [
|
||||
$text('tag-'.$n.'-label', $s[1], $klein),
|
||||
$heading('tag-'.$n.'-titel', $s[2], 3, ['desktop' => [...$h3Stil['desktop'], 'margin' => ['top' => '7px', 'bottom' => '9px']]]),
|
||||
$text('tag-'.$n.'-text', $s[3], ['desktop' => ['fontSize' => '15px', 'color' => '@gedaempft', 'lineHeight' => 1.75, 'margin' => ['bottom' => '0px']]]),
|
||||
]),
|
||||
], '24px', ['alignItems' => 'stretch', 'padding' => ['bottom' => $letzte ? '0px' : '34px']], ['gap' => '19px']);
|
||||
},
|
||||
$tagesablauf, array_keys($tagesablauf),
|
||||
)),
|
||||
], ['desktop' => ['columns' => 'two'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '90px', 'margin' => ['top' => '0px']],
|
||||
'tablet' => ['gap' => '50px'],
|
||||
'mobile' => ['gap' => '28px'],
|
||||
]),
|
||||
], [], 'tagesablauf'),
|
||||
|
||||
// 6 – Kontakt: dunkelblaue Fläche, rechts die weiße Formularkarte.
|
||||
$section('kontakt-abschnitt', [
|
||||
// Die schräge hellere Fläche am rechten Rand, mit dem limettenen Streifen davor.
|
||||
$flaeche('kontakt-form', [
|
||||
'desktop' => ['position' => 'absolute', 'top' => '0px', 'bottom' => '0px', 'right' => '-80px', 'width' => '180px', 'height' => 'auto', 'backgroundColor' => '#315d71', 'borderWidth' => $nurLinks('12px'), 'borderStyle' => 'solid', 'borderColor' => '#a1bb59', 'zIndex' => 0],
|
||||
'mobile' => ['right' => '-150px'],
|
||||
], 'transform:skewX(-16deg);pointer-events:none'),
|
||||
$row('kontakt-zeile', [
|
||||
$column('kontakt-text', [
|
||||
$kleinzeile('kontakt-label', 'Der erste Schritt ist ein Gespräch', '#c4d998', '@limette', '22px'),
|
||||
$heading('kontakt-h2', 'Ein gutes Gefühl<br>beginnt mit einem<br><em>persönlichen Hallo.</em>', 2, [
|
||||
'desktop' => [...$serif, 'fontSize' => '59px', 'color' => '@papier', 'margin' => ['bottom' => '24px']],
|
||||
'tablet' => ['fontSize' => '45px'],
|
||||
'mobile' => ['fontSize' => '43px'],
|
||||
], '& em{color:#c1d786;font-weight:400}'),
|
||||
$text('kontakt-intro', 'Sie möchten unsere Tagespflege kennenlernen oder haben Fragen? Erzählen Sie uns, was Ihnen wichtig ist. Wir freuen uns auf Sie.', [
|
||||
'desktop' => ['maxWidth' => '430px', 'color' => '#d5e0e4', 'fontSize' => '15px', 'lineHeight' => 1.8, 'margin' => ['bottom' => '28px']],
|
||||
]),
|
||||
// Ein Telefonlink, kein Button-Baustein: Buttons verweisen auf Seiten, nicht auf tel:.
|
||||
$text('kontakt-anrufen', '<a href="'.$telHref.'">Rufen Sie uns an ↗</a>', ['desktop' => ['margin' => ['bottom' => '0px']]],
|
||||
"& a{display:inline-flex;align-items:center;gap:25px;background:#c3d886;color:#27434a;border-radius:4px;padding:15px 22px;font-size:15px;font-weight:600;transition:background .2s,transform .2s}\n& a:hover{background:#d1e3a5;transform:translateY(-2px)}"),
|
||||
['id' => 'kontakt-daten', 'type' => 'contact',
|
||||
'props' => ['phone' => $telefon, 'email' => $mail, 'address' => "Fremersbergstraße 75\n76530 Baden-Baden"],
|
||||
'style' => ['desktop' => ['fontSize' => '15px', 'color' => '#dce7eb', 'gap' => '10px', 'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#ffffff30', 'padding' => ['top' => '26px'], 'margin' => ['top' => '30px']]],
|
||||
// Die Vorlage führt die Angaben ohne Symbole; die Adresse behält ihren Umbruch.
|
||||
'css' => "& svg{display:none}\n& span{display:block}\n& a{overflow-wrap:anywhere}\n& a:hover{text-decoration:underline}\n& span:first-child a{font-size:22px;color:#ffffff;letter-spacing:-0.025em}\n& span:last-child{white-space:pre-line;line-height:1.65;margin-top:5px}",
|
||||
],
|
||||
], ['desktop' => ['alignSelf' => 'center']]),
|
||||
$column('kontakt-karte', [
|
||||
$text('kontakt-karte-label', 'WIR FREUEN UNS AUF IHRE NACHRICHT', ['desktop' => [...$klein['desktop'], 'margin' => ['bottom' => '18px']]]),
|
||||
$heading('kontakt-karte-titel', 'Was können wir<br>für Sie tun?', 3, [
|
||||
'desktop' => ['fontFamily' => 'heading', 'fontWeight' => '400', 'fontSize' => '30px', 'lineHeight' => 1.25, 'letterSpacing' => '-0.02em', 'margin' => ['bottom' => '25px']],
|
||||
]),
|
||||
['id' => 'kontakt-formular', 'type' => 'form', 'props' => ['form' => 'kontakt'],
|
||||
// Felder und Knopf im Zuschnitt der Vorlage; der Wortlaut des Knopfes kommt vom Baustein.
|
||||
'css' => "& .form-field label{font-size:14px;font-weight:600;color:#294655}
|
||||
& .form-field input{background:#fafcf8;border:1px solid #cfd9ce;border-radius:4px;padding:12px 13px;font-size:16px;color:#294655}
|
||||
& .form-field textarea{background:#fafcf8;border:1px solid #cfd9ce;border-radius:4px;padding:12px 13px;font-size:16px;color:#294655;min-height:120px}
|
||||
& .field-note{font-size:12px;color:#748078}
|
||||
& .cms-button{width:100%;justify-content:center;border-radius:4px;margin-top:4px}",
|
||||
],
|
||||
$text('kontakt-formular-hinweis', 'Ihre Nachricht geht direkt an die Tagespflege. Wir verwenden Ihre Angaben ausschließlich, um Ihre Anfrage zu beantworten.', [
|
||||
'desktop' => ['fontSize' => '13px', 'lineHeight' => 1.65, 'color' => '#64736b', 'margin' => ['top' => '19px', 'bottom' => '0px']],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['alignSelf' => 'center', 'backgroundColor' => '@papier', 'color' => '@tinte', 'borderRadius' => $ecken('3px'), 'borderWidth' => $nurOben('4px'), 'borderStyle' => 'solid', 'borderColor' => '#bdd26f', 'padding' => ['top' => '32px', 'right' => '35px', 'bottom' => '32px', 'left' => '35px']],
|
||||
'tablet' => ['padding' => ['top' => '28px', 'right' => '25px', 'bottom' => '28px', 'left' => '25px']],
|
||||
'mobile' => ['padding' => ['top' => '28px', 'right' => '25px', 'bottom' => '28px', 'left' => '25px']],
|
||||
]),
|
||||
], ['desktop' => ['columns' => 'two'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '100px', 'position' => 'relative', 'zIndex' => 1, 'margin' => ['top' => '0px']],
|
||||
'tablet' => ['gap' => '35px'],
|
||||
'mobile' => ['gap' => '35px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@dunkelblau', 'color' => '@papier', 'position' => 'relative', 'overflow' => 'hidden', 'padding' => ['top' => '83px', 'bottom' => '83px']],
|
||||
'mobile' => ['padding' => ['top' => '60px', 'bottom' => '60px']],
|
||||
], 'kontakt'),
|
||||
],
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Seiten anlegen und veröffentlichen
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$erstellt = [];
|
||||
$anlegen = static function (string $slug, array $dokument, string $seoTitel = '', string $teilbild = '') use ($pages, $db, &$erstellt): string {
|
||||
$id = $pages->createPage($slug, null, 'page', $dokument);
|
||||
if ('' !== $seoTitel) {
|
||||
$db->executeStatement('UPDATE page SET seo_title = ? WHERE id = ?', [$seoTitel, $id]);
|
||||
}
|
||||
if ('' !== $teilbild) {
|
||||
$db->executeStatement('UPDATE page SET og_media_id = ? WHERE id = ?', [$teilbild, $id]);
|
||||
}
|
||||
$seite = $db->fetchAssociative('SELECT draft_revision_id, version FROM page WHERE id = ?', [$id]);
|
||||
$pages->publish($id, $seite['draft_revision_id'], (int) $seite['version']);
|
||||
$erstellt[$slug] = $id;
|
||||
echo 'Seite angelegt: /'.$db->fetchOne('SELECT path FROM page WHERE id = ?', [$id])."\n";
|
||||
|
||||
return $id;
|
||||
};
|
||||
|
||||
$anlegen('home', $startseite, 'Tagespflege Moldovan · Gemeinsam den Tag genießen', $medien['gemeinsam']['id']);
|
||||
|
||||
$fehlerId = $anlegen('404', [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Seite nicht gefunden',
|
||||
'description' => 'Die gesuchte Seite gibt es nicht oder sie wurde verschoben.',
|
||||
'blocks' => [
|
||||
$abschnitt('fehler', [
|
||||
$kleinzeile('fehler-label', 'Fehler 404'),
|
||||
$heading('fehler-titel', 'Diese Seite<br><em>haben wir nicht.</em>', 1, [
|
||||
'desktop' => [...$serif, 'fontSize' => '59px', 'margin' => ['bottom' => '25px']],
|
||||
'mobile' => ['fontSize' => '40px'],
|
||||
], $emGruen),
|
||||
$text('fehler-text', 'Vielleicht hat sich ein Tippfehler in die Adresse geschlichen. Über die Startseite finden Sie zurück.', [
|
||||
'desktop' => ['maxWidth' => '465px', 'color' => '@gedaempft', 'fontSize' => '17px', 'lineHeight' => 1.8, 'margin' => ['bottom' => '28px']],
|
||||
]),
|
||||
$button('fehler-knopf', 'Zur Startseite', '/', ['desktop' => ['borderRadius' => $ecken('4px'), 'margin' => ['top' => '0px']]]),
|
||||
], ['desktop' => ['padding' => ['top' => '120px', 'bottom' => '140px']]]),
|
||||
],
|
||||
]);
|
||||
$db->executeStatement('UPDATE page SET robots = ? WHERE id = ?', ['noindex', $fehlerId]);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Menü, Einstellungen, Konto
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$sprung = static fn (string $label, string $anchor): array => ['label' => $label, 'type' => 'page', 'pageId' => $erstellt['home'], 'anchor' => $anchor, 'children' => []];
|
||||
|
||||
$menus->save('header', 'Hauptnavigation', [
|
||||
$sprung('Bei uns', 'bei-uns'),
|
||||
$sprung('Unser Angebot', 'angebot'),
|
||||
$sprung('Ein Tag bei uns', 'tagesablauf'),
|
||||
$sprung('Kontakt aufnehmen ↗', 'kontakt'),
|
||||
], 0);
|
||||
echo "Menü angelegt\n";
|
||||
|
||||
$settings->assignNotFoundPage($fehlerId, '');
|
||||
$settings->save([
|
||||
'legalName' => 'Tagespflege Jacqueline Moldovan',
|
||||
'description' => 'Tagespflege in Baden-Baden: ein Ort für ältere Menschen, die ihren Tag in Gesellschaft verbringen möchten. Persönliche Begleitung, gemeinsame Aktivitäten und Zeit für Ruhe – abends wieder im vertrauten Zuhause.',
|
||||
'phone' => $telefon, 'email' => $mail,
|
||||
'street' => 'Fremersbergstraße 75', 'postalCode' => '76530', 'city' => 'Baden-Baden', 'country' => 'DE',
|
||||
'localBusiness' => true, 'llmsTxt' => true,
|
||||
'stickyHeader' => true, 'headerOverlay' => false,
|
||||
'motion' => 'always',
|
||||
'faviconMediaId' => $medien['marke']['id'],
|
||||
'homePath' => 'home',
|
||||
'consentBanner' => false, 'scrollTop' => false, 'postMetaHeader' => true,
|
||||
]);
|
||||
echo "Einstellungen gespeichert\n";
|
||||
|
||||
$passwort = bin2hex(random_bytes(12));
|
||||
$db->insert('cms_user', ['id' => bin2hex(random_bytes(16)), 'username' => 'umut', 'password_hash' => password_hash($passwort, PASSWORD_BCRYPT), 'roles' => '["ROLE_ADMIN"]']);
|
||||
|
||||
echo "\nFertig.\nVerwaltung: Benutzer „umut“, Passwort: $passwort\n";
|
||||
@@ -13,7 +13,7 @@ final class DocumentValidator
|
||||
/** Blocks introduced with schema v6 for the sections a marketing page actually needs. */
|
||||
public const V6_TYPES = ['iconcard', 'stat', 'quote', 'logos', 'cta', 'contact', 'divider', 'spacer', 'accordion', 'accordionitem'];
|
||||
/** Icons ship as inline SVG from this fixed list; no icon font and no external request. */
|
||||
public const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check'];
|
||||
public const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check', 'people', 'music', 'home'];
|
||||
public const SPACER_SIZES = ['small', 'medium', 'large', 'huge'];
|
||||
/** Schema v7: tabs, the one pattern that needs a script to become a real widget. */
|
||||
public const V7_TYPES = ['tabs', 'tabitem'];
|
||||
@@ -265,6 +265,11 @@ final class DocumentValidator
|
||||
if (array_key_exists('collapse', $props)) {
|
||||
$this->require($schemaVersion >= 22 && in_array($props['collapse'], ['tablet', 'mobile', 'never'], true), 'Unbekannte Hamburger-Umschaltung oder Dokumentschema vor v22.');
|
||||
}
|
||||
if (array_key_exists('burgerLabel', $props)) {
|
||||
// Wie der Knopf beschriftet ist. „Nur Symbol“ behält den Text für Vorlesesoftware:
|
||||
// ein Knopf ohne Namen ist für sie ein Knopf ohne Zweck.
|
||||
$this->require(in_array($props['burgerLabel'], ['both', 'text', 'icon'], true), 'Für den Menü-Knopf sind „both", „text" und „icon" vorgesehen.');
|
||||
}
|
||||
if (isset($props['layout'])) {
|
||||
$this->require($schemaVersion >= 16, 'Die Menüart braucht Schema v16.');
|
||||
$this->require(in_array($props['layout'], ['horizontal', 'vertical', 'burger'], true), 'Unbekannte Menüart.');
|
||||
|
||||
@@ -191,6 +191,11 @@ final class StyleCompiler
|
||||
if ('' !== $controls) {
|
||||
$rules .= sprintf('#%s,#%s + .slider-controls{%s}', $id, $id, $controls);
|
||||
}
|
||||
// Der Menü-Knopf: die Farben liegen am Block, gelten aber dem aufklappbaren Knopf.
|
||||
$burger = $this->declarations($values, 'burger', $device);
|
||||
if ('' !== $burger) {
|
||||
$rules .= sprintf('#%s .menu-burger>summary{%s}', $id, $burger);
|
||||
}
|
||||
$hover = is_array($values['hover'] ?? null) ? $this->declarations($values['hover'], 'self', $device) : '';
|
||||
if ('' !== $hover) {
|
||||
$rules .= sprintf('#%s:hover,#%s:focus-within{%s}', $id, $id, $hover);
|
||||
|
||||
@@ -206,6 +206,14 @@ final class StyleSchema
|
||||
'transition' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Übergangsdauer (ms)', 'min' => 0, 'max' => 2000, 'step' => 25],
|
||||
// The slider controls are built by the script next to the slider, so their colours
|
||||
// travel as custom properties onto the slider and its controls (target "controls").
|
||||
// Der Menü-Knopf ist ein eigener Knopf im Menüblock, kein eigener Baustein: seine
|
||||
// Farben liegen deshalb auf dem Block und wirken auf das summary-Element darin.
|
||||
'burgerColor' => ['group' => 'menu', 'type' => 'color', 'css' => 'color', 'label' => 'Menü-Knopf: Schrift und Striche', 'target' => 'burger'],
|
||||
'burgerBackgroundColor' => ['group' => 'menu', 'type' => 'color', 'css' => 'background-color', 'label' => 'Menü-Knopf: Fläche', 'target' => 'burger'],
|
||||
'burgerBorderColor' => ['group' => 'menu', 'type' => 'color', 'css' => 'border-color', 'label' => 'Menü-Knopf: Rahmen', 'target' => 'burger'],
|
||||
'burgerBorderWidth' => ['group' => 'menu', 'type' => 'length', 'css' => 'border-width', 'label' => 'Menü-Knopf: Rahmenstärke', 'target' => 'burger'],
|
||||
'burgerBorderRadius' => ['group' => 'menu', 'type' => 'length', 'css' => 'border-radius', 'label' => 'Menü-Knopf: Ecken', 'target' => 'burger'],
|
||||
'burgerFontSize' => ['group' => 'menu', 'type' => 'length', 'css' => 'font-size', 'label' => 'Menü-Knopf: Schriftgröße', 'target' => 'burger'],
|
||||
'sliderDotColor' => ['group' => 'slider', 'type' => 'color', 'css' => '--slider-dot', 'label' => 'Punkte', 'target' => 'controls'],
|
||||
'sliderDotActiveColor' => ['group' => 'slider', 'type' => 'color', 'css' => '--slider-dot-active', 'label' => 'Aktiver Punkt', 'target' => 'controls'],
|
||||
'sliderControlColor' => ['group' => 'slider', 'type' => 'color', 'css' => '--slider-ink', 'label' => 'Pfeile und Zähler', 'target' => 'controls'],
|
||||
|
||||
@@ -82,7 +82,8 @@ final class PageController extends AbstractController
|
||||
$needsMotion = (bool) array_filter($documents, fn (array $doc): bool => $this->hasEntrance($doc));
|
||||
// Only the header changes with the scroll position; a scrolled value anywhere in it asks
|
||||
// for the small script that marks the page as scrolled.
|
||||
$needsScrolled = isset($areas['header']) && $this->hasScrolled($areas['header']);
|
||||
// Den Nach-oben-Knopf blendet dasselbe Skript ein; ohne es bleibt er dauerhaft sichtbar.
|
||||
$needsScrolled = (isset($areas['header']) && $this->hasScrolled($areas['header'])) || (bool) ($this->settings->load()['scrollTop'] ?? false);
|
||||
$styles = $this->theme->css($theme).$this->scrollTopStyles($this->settings->load(), $theme).$this->styles->compileDocument($page['document']).$this->code->compileDocument($page['document']);
|
||||
foreach ([...array_values($areas), ...array_values($sections)] as $extra) {
|
||||
$styles .= $this->styles->compileDocument($extra).$this->code->compileDocument($extra);
|
||||
|
||||
@@ -99,7 +99,9 @@
|
||||
aria-label="{{ {header: 'Hauptnavigation', secondary: 'Zweite Navigation', footer: 'Fußzeile', legal: 'Rechtliches'}[props.slot]|default('Navigation') }}">
|
||||
{% if menuLayout == 'burger' or collapse != 'never' %}
|
||||
<details class="menu-burger">
|
||||
<summary><span class="burger-lines" aria-hidden="true"></span><span class="burger-text">Menü</span></summary>
|
||||
{# Nur Symbol behält den Namen für Vorlesesoftware; sichtbar ist dann nur der Strich. #}
|
||||
{% set burger = props.burgerLabel|default('both') %}
|
||||
<summary>{% if burger != 'text' %}<span class="burger-lines" aria-hidden="true"></span>{% endif %}<span class="burger-text{{ burger == 'icon' ? ' visually-hidden' : '' }}">Menü</span></summary>
|
||||
<div class="menu-burger-body">{% include 'nav.html.twig' with {links: links} only %}</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
phone: 'M6 3h4l2 5-2.5 1.5a12 12 0 0 0 5 5L16 12l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 4 5a2 2 0 0 1 2-2Z',
|
||||
mail: 'M3 6h18v12H3Zm0 1 9 7 9-7',
|
||||
pin: 'M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z',
|
||||
check: 'm4 12.5 5.5 5.5L20 6'
|
||||
check: 'm4 12.5 5.5 5.5L20 6',
|
||||
people: 'M9.5 11.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM2.5 20v-1c0-3.3 3-5 7-5s7 1.7 7 5v1M16.5 5.3a3.2 3.2 0 0 1 0 6.4M18 14.4c2.1.7 3.5 2.1 3.5 4.1V20',
|
||||
music: 'M10 18V5.5L20 3v12.5M10 9.5 20 7M10 18a3 2.5 0 1 0-6 0 3 2.5 0 0 0 6 0ZM20 15.5a3 2.5 0 1 0-6 0 3 2.5 0 0 0 6 0Z',
|
||||
home: 'm3 11.5 9-7.5 9 7.5M5.5 10v10h13V10M10 20v-6.5h4V20'
|
||||
} %}
|
||||
<svg class="block-icon" viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="{{ paths[name] }}"/></svg>
|
||||
@@ -1887,6 +1887,29 @@ final class CmsTest extends TestCase
|
||||
self::assertStringContainsString('Bewertung: 5 von 5', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Jedes erlaubte Symbol braucht eine Zeichnung, und die Symbolfarbe muss am SVG ankommen.
|
||||
*
|
||||
* Ohne den ersten Teil rendert ein Name aus der Liste ein leeres `<path d="">`: unsichtbar,
|
||||
* ohne Fehler, erst auf der fertigen Seite bemerkbar. Der zweite Teil sichert die Regel, die
|
||||
* das eingebettete SVG die Farbe seiner Hülle erben lässt - es trägt dieselbe Klasse und
|
||||
* setzte sonst die Akzentfarbe erneut.
|
||||
*/
|
||||
public function testEveryAllowedIconHasADrawingAndTakesItsColourFromTheBlock(): void
|
||||
{
|
||||
$template = (string) file_get_contents(__DIR__.'/../templates/blocks/icon.html.twig');
|
||||
foreach (DocumentValidator::ICONS as $icon) {
|
||||
self::assertMatchesRegularExpression(
|
||||
'/
|
||||
\s{4}'.preg_quote($icon, '/').": '[^']{4,}',?
|
||||
/",
|
||||
$template,
|
||||
sprintf('Das Symbol „%s“ steht in der Liste, hat aber keine Zeichnung.', $icon),
|
||||
);
|
||||
}
|
||||
self::assertStringContainsString('.block-icon .block-icon{color:inherit}', (string) file_get_contents(__DIR__.'/../public/assets/site.css'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{array}>
|
||||
*/
|
||||
@@ -3342,6 +3365,61 @@ final class CmsTest extends TestCase
|
||||
self::assertStringContainsString("script-src 'none'", (string) $this->request('/')->headers->get('Content-Security-Policy'));
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string, list<string>, list<string>}> */
|
||||
public static function burgerLabels(): iterable
|
||||
{
|
||||
yield 'symbol and text' => ['both', ['<span class="burger-lines"', '<span class="burger-text">Menü'], []];
|
||||
yield 'only text' => ['text', ['<span class="burger-text">Menü'], ['burger-lines']];
|
||||
yield 'only the symbol, the name stays for screen readers' => ['icon', ['<span class="burger-lines"', '<span class="burger-text visually-hidden">Menü'], []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $enthalten
|
||||
* @param list<string> $fehlen
|
||||
*/
|
||||
#[DataProvider('burgerLabels')]
|
||||
public function testTheMenuButtonCanBeSymbolTextOrBoth(string $variante, array $enthalten, array $fehlen): void
|
||||
{
|
||||
$document = DemoContent::starter('Mit Menü');
|
||||
$document['schemaVersion'] = 16;
|
||||
$document['blocks'][0]['children'][] = ['id' => 'nav', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'burger', 'burgerLabel' => $variante]];
|
||||
$this->menus->save('header', 'Hauptnavigation', [['label' => 'Start', 'type' => 'page', 'level' => 1, 'pageId' => 'page-1', 'position' => 10]], 0);
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
// Nur der eigene Knopf, nicht der des Kopfbereichs aus der Startvorlage.
|
||||
$crawler = new \Symfony\Component\DomCrawler\Crawler($this->request('/')->getContent());
|
||||
$summary = $crawler->filter('#nav > details.menu-burger > summary')->html();
|
||||
foreach ($enthalten as $teil) {
|
||||
self::assertStringContainsString($teil, $summary);
|
||||
}
|
||||
foreach ($fehlen as $teil) {
|
||||
self::assertStringNotContainsString($teil, $summary);
|
||||
}
|
||||
}
|
||||
|
||||
public function testAnInventedMenuButtonVariantIsRefused(): void
|
||||
{
|
||||
$document = DemoContent::starter('Erfunden');
|
||||
$document['schemaVersion'] = 16;
|
||||
$document['blocks'][0]['children'][] = ['id' => 'nav', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'burger', 'burgerLabel' => 'gross']];
|
||||
$this->expectException(InvalidDocument::class);
|
||||
$this->pages->saveDraft('page-1', $document, 0);
|
||||
}
|
||||
|
||||
/** Die Farben des Menü-Knopfs liegen am Block und müssen am summary ankommen. */
|
||||
public function testTheMenuButtonTakesItsOwnColours(): void
|
||||
{
|
||||
$document = DemoContent::starter('Farbiger Knopf');
|
||||
$document['schemaVersion'] = 16;
|
||||
$document['blocks'][0]['children'][] = [
|
||||
'id' => 'nav', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'burger'],
|
||||
'style' => ['desktop' => ['burgerColor' => '#294655', 'burgerBackgroundColor' => '#ffffff', 'burgerBorderColor' => '#d8e0d1', 'burgerBorderRadius' => '3px', 'burgerFontSize' => '14px']],
|
||||
];
|
||||
$this->menus->save('header', 'Hauptnavigation', [['label' => 'Start', 'type' => 'page', 'level' => 1, 'pageId' => 'page-1', 'position' => 10]], 0);
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('#nav .menu-burger>summary{color:#294655;background-color:#ffffff;border-color:#d8e0d1;border-radius:3px;font-size:14px}', $html);
|
||||
}
|
||||
|
||||
public function testAMenuLayoutNeedsSchemaSixteen(): void
|
||||
{
|
||||
$document = DemoContent::starter('Zu alt');
|
||||
|
||||
Reference in new issue
Block a user