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>
68 lines
5.7 KiB
Twig
68 lines
5.7 KiB
Twig
{% extends 'admin/base.html.twig' %}
|
|
{% block title %}Menüs{% 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">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_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>
|
|
{% endfor %}{% endfor %}
|
|
|
|
{% for slot, slotName in slots %}
|
|
{% set menu = menus[slot]|default(null) %}
|
|
<section class="menu-section">
|
|
<div class="section-heading"><h2>{{ slotName }} <span class="count">{{ rows[slot]|length }}</span></h2><span class="muted">Platz <code>{{ slot }}</code></span></div>
|
|
<form action="{{ path('admin_menu_save', {slot: slot}) }}" method="post" class="menu-form">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('menus') }}">
|
|
<input type="hidden" name="expectedVersion" value="{{ menu ? menu.version : 0 }}">
|
|
<label for="name-{{ slot }}">Name des Menüs</label>
|
|
<input id="name-{{ slot }}" name="name" maxlength="120" required value="{{ menu ? menu.name : slotName }}">
|
|
|
|
<table class="menu-table">
|
|
<caption class="visually-hidden">Einträge im Menü {{ slotName }}. Eine Beschriftung zu leeren entfernt den Eintrag.</caption>
|
|
<thead><tr><th scope="col">Position</th><th scope="col">Ebene</th><th scope="col">Beschriftung</th><th scope="col">Ziel</th><th scope="col">Seite</th><th scope="col">Sprungziel</th><th scope="col">Adresse</th></tr></thead>
|
|
<tbody>
|
|
{% for index, row in rows[slot]|merge([{label: '', type: 'page', level: 1, pageId: '', anchor: '', url: ''}, {label: '', type: 'page', level: 1, pageId: '', anchor: '', url: ''}, {label: '', type: 'page', level: 1, pageId: '', anchor: '', url: ''}]) %}
|
|
{% set n = loop.index0 %}
|
|
<tr{% if row.label == '' %} class="menu-row-new"{% endif %}>
|
|
<td><input type="number" name="items[{{ n }}][position]" value="{{ (n + 1) * 10 }}" min="0" max="9999" step="1" aria-label="Position von Eintrag {{ n + 1 }}"></td>
|
|
<td>
|
|
<select name="items[{{ n }}][level]" aria-label="Ebene von Eintrag {{ n + 1 }}">
|
|
<option value="1"{{ row.level == 1 ? ' selected' : '' }}>1 — oben</option>
|
|
<option value="2"{{ row.level == 2 ? ' selected' : '' }}>2 — Untereintrag</option>
|
|
</select>
|
|
</td>
|
|
<td><input name="items[{{ n }}][label]" maxlength="80" value="{{ row.label }}" aria-label="Beschriftung von Eintrag {{ n + 1 }}" placeholder="{{ row.label == '' ? 'Neuer Eintrag' : '' }}"></td>
|
|
<td>
|
|
<select name="items[{{ n }}][type]" aria-label="Zielart von Eintrag {{ n + 1 }}">
|
|
<option value="page"{{ row.type == 'page' ? ' selected' : '' }}>Seite</option>
|
|
<option value="url"{{ row.type == 'url' ? ' selected' : '' }}>Adresse</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<select name="items[{{ n }}][pageId]" aria-label="Seite von Eintrag {{ n + 1 }}">
|
|
<option value="">— Seite wählen —</option>
|
|
{% for page in pages %}<option value="{{ page.id }}"{{ row.pageId == page.id ? ' selected' : '' }}>{{ page.title }} (/{{ page.path == 'home' ? '' : page.path }})</option>{% endfor %}
|
|
</select>
|
|
</td>
|
|
<td><input name="items[{{ n }}][anchor]" maxlength="65" value="{{ row.anchor }}" aria-label="Sprungziel von Eintrag {{ n + 1 }}" placeholder="kontakt"></td>
|
|
<td><input name="items[{{ n }}][url]" maxlength="2048" value="{{ row.url }}" aria-label="Adresse von Eintrag {{ n + 1 }}" placeholder="https://…"></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
<p class="field-help">Ein Sprungziel gilt zusammen mit der gewählten Seite und führt zu <code>/seite#sprungziel</code>. Als Adresse sind HTTPS-Adressen, lokale Pfade sowie <code>mailto:</code> und <code>tel:</code> erlaubt. Die letzten drei Zeilen sind für neue Einträge; beim Speichern werden sie neu durchnummeriert.</p>
|
|
<button class="primary" type="submit">{{ slotName }} speichern</button>
|
|
</form>
|
|
</section>
|
|
{% endfor %}
|
|
<p class="footnote">Kant CMS · Visueller Editor, erster Prototyp</p>
|
|
</main>
|
|
{% endblock %}
|