Files
KantSites/templates/admin/menus.html.twig
T
kantarciandClaude Opus 5 abf0bd2c6b 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>
2026-09-12 13:04:54 +02:00

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 %}