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>
106 lines
7.7 KiB
Twig
106 lines
7.7 KiB
Twig
{% extends 'admin/base.html.twig' %}
|
||
{% block title %}Formulare{% 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">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_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 form in forms|merge([null]) %}
|
||
{% set new = form is null %}
|
||
{% set rows = new ? [] : form.fields %}
|
||
<section class="menu-section">
|
||
<div class="section-heading">
|
||
<h2>{{ new ? 'Neues Formular' : form.name }}{% if not new %} <span class="count">{{ form.fields|length }} Felder</span>{% endif %}</h2>
|
||
{% if not new %}<span class="muted">Block-Adresse <code>{{ form.slug }}</code></span>{% endif %}
|
||
</div>
|
||
<form action="{{ path('admin_form_save', {slug: new ? 'neu' : form.slug}) }}" method="post" class="menu-form">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('forms') }}">
|
||
<input type="hidden" name="expectedVersion" value="{{ new ? 0 : form.version }}">
|
||
<div class="create-grid">
|
||
<div>
|
||
<label for="name-{{ loop.index }}">Name</label>
|
||
<input id="name-{{ loop.index }}" name="name" maxlength="120" required value="{{ new ? '' : form.name }}">
|
||
</div>
|
||
{% if new %}
|
||
<div>
|
||
<label for="slug-new">Adresse</label>
|
||
<input id="slug-new" name="slug" maxlength="80" pattern="[a-z0-9]+(-[a-z0-9]+)*" required placeholder="kontakt">
|
||
<p class="field-help">Wird im Formularblock eingetragen.</p>
|
||
</div>
|
||
{% endif %}
|
||
<div>
|
||
<label for="recipient-{{ loop.index }}">Empfänger</label>
|
||
<input id="recipient-{{ loop.index }}" name="recipient" type="email" required value="{{ new ? '' : form.settings.recipient }}">
|
||
</div>
|
||
<div>
|
||
<label for="subject-{{ loop.index }}">Betreff</label>
|
||
<input id="subject-{{ loop.index }}" name="subject" maxlength="160" value="{{ new ? '' : form.settings.subject }}">
|
||
</div>
|
||
<div>
|
||
<label for="reply-{{ loop.index }}">Antwortadresse aus Feld</label>
|
||
<input id="reply-{{ loop.index }}" name="replyToField" maxlength="32" value="{{ new ? '' : form.settings.replyToField }}" placeholder="email">
|
||
<p class="field-help">Technischer Name eines E-Mail-Feldes. Dann lässt sich direkt antworten.</p>
|
||
</div>
|
||
<div>
|
||
<label for="success-{{ loop.index }}">Meldung nach dem Absenden</label>
|
||
<input id="success-{{ loop.index }}" name="success" maxlength="300" value="{{ new ? '' : form.settings.success }}">
|
||
</div>
|
||
</div>
|
||
|
||
<table class="menu-table">
|
||
<caption class="visually-hidden">Felder des Formulars. Eine Beschriftung zu leeren entfernt das Feld.</caption>
|
||
<thead><tr><th scope="col">Technischer Name</th><th scope="col">Typ</th><th scope="col">Beschriftung</th><th scope="col">Pflicht</th><th scope="col">Hilfetext</th><th scope="col">Optionen</th></tr></thead>
|
||
<tbody>
|
||
{% for row in rows|merge([{name: '', type: 'text', label: '', required: false, help: '', options: []}, {name: '', type: 'text', label: '', required: false, help: '', options: []}]) %}
|
||
{% set n = loop.index0 %}
|
||
<tr{% if row.label == '' %} class="menu-row-new"{% endif %}>
|
||
<td><input name="fields[{{ n }}][name]" maxlength="32" value="{{ row.name }}" pattern="[a-z][a-z0-9_]*" aria-label="Technischer Name von Feld {{ n + 1 }}" placeholder="vorname"></td>
|
||
<td>
|
||
<select name="fields[{{ n }}][type]" aria-label="Typ von Feld {{ n + 1 }}">
|
||
{% for value, label in types %}<option value="{{ value }}"{{ row.type == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td><input name="fields[{{ n }}][label]" maxlength="120" value="{{ row.label }}" aria-label="Beschriftung von Feld {{ n + 1 }}"></td>
|
||
<td class="check-cell"><input type="checkbox" name="fields[{{ n }}][required]" value="1"{{ row.required ? ' checked' : '' }} aria-label="Feld {{ n + 1 }} ist ein Pflichtfeld"></td>
|
||
<td><input name="fields[{{ n }}][help]" maxlength="200" value="{{ row.help }}" aria-label="Hilfetext von Feld {{ n + 1 }}"></td>
|
||
<td><input name="fields[{{ n }}][options]" maxlength="500" value="{{ row.options|join(', ') }}" aria-label="Optionen von Feld {{ n + 1 }}" placeholder="nur bei Auswahl"></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<p class="field-help">Ein Einwilligungsfeld ist immer ein Pflichtfeld; der Zeitpunkt der Einwilligung wird mitgespeichert. Optionen einer Auswahl durch Komma trennen. Die letzten beiden Zeilen sind für neue Felder.</p>
|
||
<button class="primary" type="submit">{{ new ? 'Formular anlegen' : 'Formular speichern' }}</button>
|
||
</form>
|
||
</section>
|
||
{% endfor %}
|
||
|
||
<div class="section-heading"><h2>Einsendungen <span class="count">{{ submissions|length }}</span></h2><span class="muted">Die neuesten 50.</span></div>
|
||
{% if submissions is empty %}
|
||
<div class="empty-state"><h3>Noch keine Einsendungen.</h3><p>Hier erscheinen eingegangene Nachrichten, auch wenn der E-Mail-Versand fehlschlägt.</p></div>
|
||
{% else %}
|
||
<div class="page-list">
|
||
{% for entry in submissions %}
|
||
<article class="page-card submission-card">
|
||
<div class="page-info">
|
||
<h3>{{ entry.form }}</h3>
|
||
<p>{{ entry.createdAt|date('d.m.Y H:i') }} UTC{% if entry.consentAt %} · Einwilligung {{ entry.consentAt|date('d.m.Y H:i') }}{% endif %}</p>
|
||
<dl class="submission-data">{% for key, value in entry.data %}<dt>{{ key }}</dt><dd>{{ value }}</dd>{% endfor %}</dl>
|
||
</div>
|
||
<span class="badge {{ entry.delivered ? 'published' : '' }}">{{ entry.delivered ? 'Versendet' : 'Nicht versendet' }}</span>
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
<p class="footnote">Kant CMS · Visueller Editor, erster Prototyp</p>
|
||
</main>
|
||
{% endblock %}
|