Freie Gestaltung statt Tokenlisten (Dokumentschema v10)
Der bisherige Editor bot drei Farben, keine Schriftwahl und elf Auswahllisten mit 44 festen Werten. ADR 008 hatte das mit Sicherheit begründet - der Schluss war falsch: die Alternative zu beliebigem CSS-Text ist nicht eine Auswahlliste, sondern ein typisiertes Designmodell. StyleSchema erklärt jede Eigenschaft einmal mit Typ, CSS-Entsprechung und Grenzen. Validator, CSS-Erzeugung und Editorpanel lesen dieselbe Definition, also kann der Editor nichts anbieten, was der Server ablehnt. Rund fünfzig Eigenschaften in sieben Gruppen, je Block, je Gerät und für den Hover-Zustand, mit Vererbung und einzelnem Zurücksetzen. Werte sind kurze Zeichenketten mit verankertem Muster, nie CSS-Text; zusammengesetzte Deklarationen baut der Compiler selbst. Der Server erzeugt daraus pro Seite ein blockgenaues Stylesheet, das über seinen eigenen SHA-256-Hash erlaubt wird statt über unsafe-inline. Theme unter /admin/theme: freie Farbpalette als @schlüssel, Schriften aus Systemschrift oder Google Fonts, Grundtypografie mit modularer Überschriftenskala. Google Fonts auf ausdrückliche Nutzerentscheidung; die Richtlinie öffnet die Hosts nur, wenn wirklich eine eingestellt ist. Alte Tokenwerte aus v2 bis v9 bleiben gültig und im Editor unter "Klassische Werte dieser Seite" bearbeitbar. Geprüft: 154 PHP-Tests / 563 Assertions unter PHP 8.2.12 und 8.4.22, 15 JavaScript-Modelltests, Lint/Format/Container. Im Browser wurde nur der Aufbau des Panels bestätigt, kein Gestaltungsvorgang von Hand ausgeführt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a40c2bb27e
commit
abf0bd2c6b
24 files changed
+1591
-21
No files matched your search
@@ -8,7 +8,7 @@
|
||||
<main id="admin-main" class="pages-main" tabindex="-1">
|
||||
<p class="eyebrow">FORMULARE</p><h1>Formulare</h1>
|
||||
<p class="muted">Felder, Empfänger und Einwilligungstext werden hier gepflegt. Auf einer Seite steht nur ein Formularblock mit der Adresse – so kann ein gespeicherter Seiteninhalt niemals ändern, wohin eine Anfrage geht.</p>
|
||||
<p class="admin-links"><a class="secondary" href="{{ path('admin_pages') }}">Seiten</a> <a class="secondary" href="{{ path('admin_menus') }}">Menüs</a> <a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a></p>
|
||||
<p class="admin-links"><a class="secondary" href="{{ path('admin_pages') }}">Seiten</a> <a class="secondary" href="{{ path('admin_menus') }}">Menüs</a> <a class="secondary" href="{{ path('admin_theme') }}">Design</a> <a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a></p>
|
||||
|
||||
{% for type in ['error', 'success'] %}{% for message in app.flashes(type) %}
|
||||
<p class="notice {{ type == 'error' ? 'error' : 'ok' }}" role="{{ type == 'error' ? 'alert' : 'status' }}">{{ message }}</p>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<main id="admin-main" class="pages-main" tabindex="-1">
|
||||
<p class="eyebrow">NAVIGATION</p><h1>Menüs</h1>
|
||||
<p class="muted">Ein Eintrag verweist auf eine Seite, auf ein Sprungziel innerhalb einer Seite oder auf eine externe Adresse. Die Reihenfolge ergibt sich aus der Position, die Verschachtelung aus der Ebene.</p>
|
||||
<p class="admin-links"><a class="secondary" href="{{ path('admin_pages') }}">Seiten</a> <a class="secondary" href="{{ path('admin_forms') }}">Formulare</a> <a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a></p>
|
||||
<p class="admin-links"><a class="secondary" href="{{ path('admin_pages') }}">Seiten</a> <a class="secondary" href="{{ path('admin_forms') }}">Formulare</a> <a class="secondary" href="{{ path('admin_theme') }}">Design</a> <a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a></p>
|
||||
|
||||
{% for type in ['error', 'success'] %}{% for message in app.flashes(type) %}
|
||||
<p class="notice {{ type == 'error' ? 'error' : 'ok' }}" role="{{ type == 'error' ? 'alert' : 'status' }}">{{ message }}</p>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<main id="admin-main" class="pages-main" tabindex="-1">
|
||||
<p class="eyebrow">DEINE WEBSITE</p><h1>Gute Inhalte beginnen hier.</h1>
|
||||
<p class="muted">Wähle eine Seite. Gestalte deinen Entwurf. Veröffentliche, wenn alles passt.</p>
|
||||
<p class="admin-links"><a class="secondary" href="{{ path('admin_media') }}">Medienverwaltung</a> <a class="secondary" href="{{ path('admin_menus') }}">Menüs</a> <a class="secondary" href="{{ path('admin_forms') }}">Formulare</a> <a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a> <a class="secondary" href="/beitraege">Beitragsübersicht ↗</a></p>
|
||||
<p class="admin-links"><a class="secondary" href="{{ path('admin_media') }}">Medienverwaltung</a> <a class="secondary" href="{{ path('admin_menus') }}">Menüs</a> <a class="secondary" href="{{ path('admin_forms') }}">Formulare</a> <a class="secondary" href="{{ path('admin_theme') }}">Design</a> <a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a> <a class="secondary" href="/beitraege">Beitragsübersicht ↗</a></p>
|
||||
|
||||
{% for type in ['error', 'success'] %}{% for message in app.flashes(type) %}
|
||||
<p class="notice {{ type == 'error' ? 'error' : 'ok' }}" role="{{ type == 'error' ? 'alert' : 'status' }}">{{ message }}</p>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<p class="muted">Aus diesen Angaben entstehen die strukturierten Daten der Startseite. Was hier steht, ist genau das, was Suchmaschinen lesen – es gibt keine zweite, unsichtbare Fassung.</p>
|
||||
<p class="admin-links">
|
||||
<a class="secondary" href="{{ path('admin_pages') }}">Seiten</a>
|
||||
<a class="secondary" href="{{ path('admin_menus') }}">Menüs</a>
|
||||
<a class="secondary" href="{{ path('admin_menus') }}">Menüs</a> <a class="secondary" href="{{ path('admin_theme') }}">Design</a>
|
||||
<a class="secondary" href="{{ path('admin_forms') }}">Formulare</a>
|
||||
<a class="secondary" href="/sitemap.xml">Sitemap ↗</a>
|
||||
<a class="secondary" href="/robots.txt">robots.txt ↗</a>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
{% extends 'admin/base.html.twig' %}
|
||||
{% block title %}Design{% endblock %}
|
||||
{% block body %}
|
||||
<header class="admin-header">
|
||||
<a class="brand" href="{{ path('admin_pages') }}"><span class="brand-icon" aria-hidden="true">k.</span>kant<span>cms</span></a>
|
||||
<div class="header-actions"><a href="/">Website ansehen ↗</a><span class="user-name">{{ app.user.userIdentifier }}</span><form action="{{ path('admin_logout') }}" method="post"><input type="hidden" name="_csrf_token" value="{{ csrf_token('logout') }}"><button class="subtle" type="submit">Abmelden</button></form></div>
|
||||
</header>
|
||||
<main id="admin-main" class="pages-main" tabindex="-1">
|
||||
<p class="eyebrow">DESIGN</p><h1>Farben, Schriften, Grundtypografie</h1>
|
||||
<p class="muted">Das ist die Grundlage für die ganze Website. Jeder Block erbt von hier und kann jeden Wert einzeln überschreiben – je Gerät und beim Überfahren.</p>
|
||||
<p class="admin-links">
|
||||
<a class="secondary" href="{{ path('admin_pages') }}">Seiten</a>
|
||||
<a class="secondary" href="{{ path('admin_menus') }}">Menüs</a>
|
||||
<a class="secondary" href="{{ path('admin_forms') }}">Formulare</a>
|
||||
<a class="secondary" href="{{ path('admin_settings') }}">Einstellungen</a>
|
||||
</p>
|
||||
|
||||
{% for type in ['error', 'success'] %}{% for message in app.flashes(type) %}
|
||||
<p class="notice {{ type == 'error' ? 'error' : 'ok' }}" role="{{ type == 'error' ? 'alert' : 'status' }}">{{ message }}</p>
|
||||
{% endfor %}{% endfor %}
|
||||
|
||||
<form action="{{ path('admin_theme_save') }}" method="post" class="menu-form theme-form">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('theme') }}">
|
||||
|
||||
<div class="section-heading"><h2>Farbpalette <span class="count">{{ theme.colors|length }}</span></h2><span class="muted">Beliebig viele eigene Farben</span></div>
|
||||
<table class="menu-table">
|
||||
<caption class="visually-hidden">Farben der Website. Einen Schlüssel zu leeren entfernt die Farbe.</caption>
|
||||
<thead><tr><th scope="col">Schlüssel</th><th scope="col">Name</th><th scope="col">Farbe</th><th scope="col">Wert</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in theme.colors|merge([{key: '', name: '', value: '#000000'}, {key: '', name: '', value: '#000000'}, {key: '', name: '', value: '#000000'}]) %}
|
||||
{% set n = loop.index0 %}
|
||||
<tr{% if row.key == '' %} class="menu-row-new"{% endif %}>
|
||||
<td><input name="colors[{{ n }}][key]" maxlength="32" value="{{ row.key }}" pattern="[a-z][a-z0-9-]*" aria-label="Schlüssel von Farbe {{ n + 1 }}" placeholder="akzent"></td>
|
||||
<td><input name="colors[{{ n }}][name]" maxlength="60" value="{{ row.name }}" aria-label="Name von Farbe {{ n + 1 }}" placeholder="Akzent"></td>
|
||||
<td><input type="color" name="colors[{{ n }}][value]" value="{{ row.value }}" aria-label="Farbwähler für Farbe {{ n + 1 }}" class="theme-swatch"></td>
|
||||
<td><code>{{ row.value }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="field-help">Der Schlüssel wird im Editor als <code>@schlüssel</code> verwendet und bleibt bei Farbänderungen gleich – eine Farbe ändern wirkt überall, wo sie benutzt wird.</p>
|
||||
|
||||
<div class="section-heading"><h2>Schriften <span class="count">{{ theme.fonts|length }}</span></h2><span class="muted">Systemschrift oder Google Fonts</span></div>
|
||||
<table class="menu-table">
|
||||
<caption class="visually-hidden">Schriften der Website. Einen Schlüssel zu leeren entfernt die Schrift.</caption>
|
||||
<thead><tr><th scope="col">Schlüssel</th><th scope="col">Name</th><th scope="col">Quelle</th><th scope="col">Schriftname</th><th scope="col">Ersatz</th><th scope="col">Schnitte</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in theme.fonts|merge([{key: '', name: '', family: '', source: 'google', fallback: 'sans-serif', weights: [400, 700]}]) %}
|
||||
{% set n = loop.index0 %}
|
||||
<tr{% if row.key == '' %} class="menu-row-new"{% endif %}>
|
||||
<td><input name="fonts[{{ n }}][key]" maxlength="40" value="{{ row.key }}" pattern="[a-z][a-z0-9-]*" aria-label="Schlüssel von Schrift {{ n + 1 }}" placeholder="heading"></td>
|
||||
<td><input name="fonts[{{ n }}][name]" maxlength="60" value="{{ row.name }}" aria-label="Name von Schrift {{ n + 1 }}" placeholder="Überschriften"></td>
|
||||
<td>
|
||||
<select name="fonts[{{ n }}][source]" aria-label="Quelle von Schrift {{ n + 1 }}">
|
||||
<option value="system"{{ row.source == 'system' ? ' selected' : '' }}>Systemschrift</option>
|
||||
<option value="google"{{ row.source == 'google' ? ' selected' : '' }}>Google Fonts</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input name="fonts[{{ n }}][family]" maxlength="48" value="{{ row.family }}" aria-label="Schriftname von Schrift {{ n + 1 }}" placeholder="Inter"></td>
|
||||
<td>
|
||||
<select name="fonts[{{ n }}][fallback]" aria-label="Ersatzschrift von Schrift {{ n + 1 }}">
|
||||
{% for value, label in fallbacks %}<option value="{{ value }}"{{ row.fallback == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td class="weights-cell">
|
||||
{% for weight in weights %}
|
||||
<label><input type="checkbox" name="fonts[{{ n }}][weights][]" value="{{ weight }}"{{ weight in row.weights ? ' checked' : '' }}> {{ weight }}</label>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="field-help">Bei Google Fonts den Namen genau wie dort schreiben, zum Beispiel <code>Inter</code> oder <code>Playfair Display</code>. Je Schnitt wird eine Datei geladen – nur ankreuzen, was wirklich gebraucht wird. <strong>Google Fonts bedeutet eine Anfrage an Google bei jedem Seitenaufruf.</strong></p>
|
||||
|
||||
<div class="section-heading"><h2>Grundtypografie</h2><span class="muted">Gilt überall, solange ein Block nichts anderes sagt</span></div>
|
||||
<div class="create-grid">
|
||||
<div>
|
||||
<label for="base-font">Schrift für Fließtext</label>
|
||||
<select id="base-font" name="base[fontFamily]">
|
||||
{% for font in theme.fonts %}<option value="{{ font.key }}"{{ theme.base.fontFamily == font.key ? ' selected' : '' }}>{{ font.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-heading-font">Schrift für Überschriften</label>
|
||||
<select id="base-heading-font" name="base[headingFontFamily]">
|
||||
{% for font in theme.fonts %}<option value="{{ font.key }}"{{ theme.base.headingFontFamily == font.key ? ' selected' : '' }}>{{ font.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-size">Grundgröße</label>
|
||||
<input id="base-size" name="base[fontSize]" value="{{ theme.base.fontSize }}" placeholder="18px">
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-line">Zeilenhöhe</label>
|
||||
<input id="base-line" name="base[lineHeight]" type="number" step="0.05" min="0.8" max="3" value="{{ theme.base.lineHeight }}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-scale">Überschriftenskala</label>
|
||||
<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-container">Inhaltsbreite</label>
|
||||
<input id="base-container" name="base[containerWidth]" value="{{ theme.base.containerWidth }}" placeholder="1240px">
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-color">Textfarbe</label>
|
||||
<input id="base-color" name="base[color]" value="{{ theme.base.color }}" placeholder="@ink">
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-bg">Hintergrundfarbe</label>
|
||||
<input id="base-bg" name="base[backgroundColor]" value="{{ theme.base.backgroundColor }}" placeholder="@paper">
|
||||
</div>
|
||||
<div>
|
||||
<label for="base-link">Linkfarbe</label>
|
||||
<input id="base-link" name="base[linkColor]" value="{{ theme.base.linkColor }}" placeholder="@accent">
|
||||
</div>
|
||||
</div>
|
||||
<p class="field-help">Farben entweder als <code>@schlüssel</code> aus der Palette oder als Wert wie <code>#1a2b3c</code>.</p>
|
||||
<button class="primary" type="submit">Design speichern</button>
|
||||
</form>
|
||||
<p class="footnote">Kant CMS · Visueller Editor, erster Prototyp</p>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -13,7 +13,7 @@
|
||||
<link rel="stylesheet" href="/assets/site.css">
|
||||
<link rel="stylesheet" href="/assets/design.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="kant">
|
||||
<a class="skip-link" href="#main">Zum Inhalt springen</a>
|
||||
<header class="site-header shell">
|
||||
<a class="brand" href="/" aria-label="Kant CMS – Startseite"><span class="brand-icon" aria-hidden="true">k.</span> kant<span class="brand-light">cms</span></a>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{% if socialImage|default(null) %}<meta property="og:image" content="{{ socialImage }}"><meta name="twitter:card" content="summary_large_image">{% endif %}
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/assets/site.css">
|
||||
{% if googleFonts|default(null) %}<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="{{ googleFonts }}">{% endif %}
|
||||
<link rel="alternate" type="application/rss+xml" title="Beiträge" href="/feed.xml">
|
||||
{% if page.document.schemaVersion >= 2 %}<link rel="stylesheet" href="/assets/design.css">{% endif %}
|
||||
{% if page.document.schemaVersion >= 3 %}<link rel="stylesheet" href="/assets/enhancements.css">{% endif %}
|
||||
@@ -21,8 +22,9 @@
|
||||
{# A data block, not a script: CSP script-src does not apply to application/ld+json. #}
|
||||
{% for block in structuredData|default([]) %}<script type="application/ld+json">{{ block|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE'))|raw }}</script>
|
||||
{% endfor %}
|
||||
{% if pageStyles|default('') %}<style>{{ pageStyles|raw }}</style>{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<body class="kant">
|
||||
<a class="skip-link" href="#main">Zum Inhalt springen</a>
|
||||
<header class="site-header shell">
|
||||
<a class="brand" href="/" aria-label="Kant CMS – Startseite"><span class="brand-icon" aria-hidden="true">k.</span> kant<span class="brand-light">cms</span></a>
|
||||
|
||||
Reference in new issue
Block a user