Files
KantSites/templates/admin/theme.html.twig
T
kantarciandClaude Fable 5.2 ead47cc924 Keep old pages readable, detect stale settings writes, make backups honest
- Own CSS is parsed on write only. A page whose stored CSS no longer
  passes a stricter rule answers 200 and opens in the editor; that one
  block loses its CSS. A document read several times in one request is
  validated once.
- Settings and theme refuse a write from an outdated form.
- A backup fails when a media file does not arrive completely and
  leaves no half directory behind; rotation counts intact backups only.
- The archive update takes its backup behind the maintenance page.
- A package import puts all media in place before removing the old
  ones, reports honestly when that fails and keeps a maintenance state
  that was already on.
- Restoring replaces the SQLite file in one move and points at pending
  migrations.
- Six irreversible actions ask first, in a dialog of the admin's own.
- Ctrl+S in the editor takes the field being typed in along; Delete
  no longer removes a block while a dialog is open.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 11:41:05 +01:00

257 lines
20 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'admin/shell.html.twig' %}
{% set nav = 'theme' %}
{# Die lokalen Schriftdateien, sofern es sie gibt: dann zeigt die Vorschau jede Google-Schrift so,
wie die Website sie ausliefert. Von Google selbst wird hier nichts geladen. #}
{% block styles %}<link rel="stylesheet" href="/assets/buttons.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/button-controls.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/color-control.css?v={{ kant_version() }}">{% if fontsStylesheet %}<link rel="stylesheet" href="/assets/fonts/fonts.css?v={{ kant_version() }}">{% endif %}{% endblock %}
{% block scripts %}<script type="module" src="/assets/theme-panel.js?v={{ kant_version() }}"></script><script type="module" src="/assets/theme-buttons.js?v={{ kant_version() }}"></script>{% endblock %}
{% block title %}Design{% endblock %}
{% block heading %}Farben, Schriften, Grundtypografie{% endblock %}
{% block lead %}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.{% endblock %}
{# Eine Farbe der Palette als Karte: der Farbwähler ist die Fläche selbst. #}
{% macro color(n, row) %}
<li class="swatch-card{{ row.key == '' ? ' swatch-card-new' : '' }}" data-color-card>
<label class="swatch-well" for="color-{{ n }}-value">
<input type="color" id="color-{{ n }}-value" name="colors[{{ n }}][value]" value="{{ row.value }}" class="swatch-input" aria-label="Farbwert von Farbe {{ n }}" data-color-value>
<code class="swatch-hex" data-color-hex>{{ row.value }}</code>
</label>
<div class="swatch-fields">
<div>
<label for="color-{{ n }}-name">Name</label>
<input id="color-{{ n }}-name" name="colors[{{ n }}][name]" maxlength="60" value="{{ row.name }}" placeholder="Akzent" data-color-name>
</div>
<div>
<label for="color-{{ n }}-key">Schlüssel</label>
<input id="color-{{ n }}-key" name="colors[{{ n }}][key]" maxlength="32" value="{{ row.key }}" pattern="[a-z][a-z0-9\-]*" placeholder="akzent" data-color-key>
</div>
</div>
<button type="button" class="field-remove" data-remove-color hidden aria-label="Farbe entfernen" title="Farbe entfernen">{% include 'admin/icon.html.twig' with {name: 'remove'} only %}</button>
</li>
{% endmacro %}
{# Eine Schrift als Karte: oben die Probe in der Schrift selbst, darunter die Einstellungen. #}
{% macro font(n, row, fallbacks, weights) %}
<li class="font-card{{ row.key == '' ? ' font-card-new' : '' }}" data-font-card>
<div class="font-sample" data-font-sample>
<span class="font-sample-big" aria-hidden="true">Aa</span>
<span class="font-sample-line">
<span class="font-sample-text">Zwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich.</span>
<span class="font-sample-weights" data-font-weights>{% for weight in weights %}<span class="font-weight-{{ weight }}"{{ weight in row.weights ? '' : ' hidden' }}>{{ weight }}</span>{% endfor %}</span>
</span>
</div>
<div class="font-fields">
<div>
<label for="font-{{ n }}-name">Name</label>
<input id="font-{{ n }}-name" name="fonts[{{ n }}][name]" maxlength="60" value="{{ row.name }}" placeholder="Überschriften" data-font-name>
</div>
<div>
<label for="font-{{ n }}-key">Schlüssel</label>
<input id="font-{{ n }}-key" name="fonts[{{ n }}][key]" maxlength="40" value="{{ row.key }}" pattern="[a-z][a-z0-9\-]*" placeholder="heading" data-font-key>
</div>
<div>
<label for="font-{{ n }}-source">Quelle</label>
<select id="font-{{ n }}-source" name="fonts[{{ n }}][source]" data-font-source>
<option value="system"{{ row.source == 'system' ? ' selected' : '' }}>Systemschrift</option>
<option value="google"{{ row.source == 'google' ? ' selected' : '' }}>Google Fonts</option>
</select>
</div>
<div>
<label for="font-{{ n }}-family">Schriftname</label>
<input id="font-{{ n }}-family" name="fonts[{{ n }}][family]" maxlength="48" value="{{ row.family }}" placeholder="Inter" data-font-family>
</div>
<div>
<label for="font-{{ n }}-fallback">Ersatzschrift</label>
<select id="font-{{ n }}-fallback" name="fonts[{{ n }}][fallback]" data-font-fallback>
{% for value, label in fallbacks %}<option value="{{ value }}"{{ row.fallback == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
</select>
</div>
<fieldset class="font-weights">
<legend>Schnitte</legend>
{% for weight in weights %}
<label class="chip"><input type="checkbox" name="fonts[{{ n }}][weights][]" value="{{ weight }}"{{ weight in row.weights ? ' checked' : '' }} data-font-weight> {{ weight }}</label>
{% endfor %}
</fieldset>
</div>
<button type="button" class="field-remove" data-remove-font hidden aria-label="Schrift entfernen" title="Schrift entfernen">{% include 'admin/icon.html.twig' with {name: 'remove'} only %}</button>
</li>
{% endmacro %}
{% block content %}
{% import _self as karte %}
{% set emptyColor = {key: '', name: '', value: '#4a7c59'} %}
{% set emptyFont = {key: '', name: '', family: '', source: 'google', fallback: 'sans-serif', weights: [400, 700]} %}
<form action="{{ path('admin_theme_save') }}" method="post" class="theme-form stack" data-theme-editor data-palettes="{{ palettes|json_encode|e('html_attr') }}" data-color-names="{{ colorNames|json_encode|e('html_attr') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('theme') }}">
<input type="hidden" name="expected" value="{{ themeStamp }}">
<section class="card">
<div class="card-head">
<h2>Farbpalette <span class="count" data-color-count>{{ theme.colors|length }}</span></h2>
<span class="muted">Jede Farbe hat einen Schlüssel – im Editor als <code>@schlüssel</code>, und eine Änderung hier wirkt überall</span>
</div>
<div class="card-body">
<div class="palette-presets" role="group" aria-label="Fertige Paletten" hidden data-palette-presets>
<span class="palette-presets-label">Fertige Palette übernehmen</span>
<div class="palette-preset-row">
{% for preset in palettes %}
<button type="button" class="palette-preset" data-palette-preset="{{ loop.index0 }}" aria-label="Palette {{ preset.name }} übernehmen">
<svg viewBox="0 0 50 14" aria-hidden="true" focusable="false">{% for key, value in preset.colors %}<rect x="{{ loop.index0 * 10 }}" y="0" width="10" height="14" fill="{{ value }}"></rect>{% endfor %}</svg>
<span>{{ preset.name }}</span>
</button>
{% endfor %}
</div>
<p class="field-help" data-palette-status role="status">Ersetzt die fünf Grundfarben; eigene Farben bleiben. Gilt erst nach dem Speichern.</p>
</div>
<ul class="swatch-list" data-color-list>
{% for row in theme.colors|merge([emptyColor, emptyColor]) %}{{ karte.color(loop.index0, row) }}{% endfor %}
</ul>
<template data-color-template>{{ karte.color('__n__', emptyColor) }}</template>
<div class="list-actions">
<button type="button" class="secondary" data-add-color hidden>{% include 'admin/icon.html.twig' with {name: 'plus'} only %}Farbe hinzufügen</button>
<p class="field-help">Eine Farbe ohne Schlüssel wird nicht gespeichert – so lässt sich eine Farbe auch ohne Skript entfernen. Die beiden leeren Karten sind für neue Farben.</p>
</div>
</div>
</section>
<section class="card">
<div class="card-head">
<h2>Schriften <span class="count" data-font-count>{{ theme.fonts|length }}</span></h2>
<span class="muted">Systemschrift oder Google Fonts – ausgeliefert vom eigenen Server</span>
</div>
<div class="card-body">
<ul class="font-list" data-font-list>
{% for row in theme.fonts|merge([emptyFont]) %}{{ karte.font(loop.index0, row, fallbacks, weights) }}{% endfor %}
</ul>
<template data-font-template>{{ karte.font('__n__', emptyFont, fallbacks, weights) }}</template>
<div class="list-actions">
<button type="button" class="secondary" data-add-font hidden>{% include 'admin/icon.html.twig' with {name: 'plus'} only %}Schrift hinzufügen</button>
<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. {% if fontsStylesheet %}Die Probe nutzt die Schriftdateien auf diesem Server; eine neue Schrift erscheint darin erst, wenn sie unten geholt wurde.{% else %}Die Probe zeigt eine Google-Schrift erst, wenn sie unten auf diesen Server geholt wurde.{% endif %}</p>
</div>
</div>
</section>
<section class="card">
<div class="card-head">
<h2>Grundtypografie</h2>
<span class="muted">Gilt überall, solange ein Block nichts anderes sagt</span>
</div>
<div class="card-body type-layout">
<div class="type-fields">
<div class="fields">
<div>
<label for="base-font">Schrift für Fließtext</label>
<select id="base-font" name="base[fontFamily]" data-font-select>
{% 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]" data-font-select>
{% 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-image-radius">Bildecken</label>
<input id="base-image-radius" name="base[imageRadius]" value="{{ theme.base.imageRadius }}" placeholder="12px">
<p class="field-help">Gilt für jedes Bild auf der Website – auch in Galerien, Logoleisten und Beitragslisten. <code>0px</code> macht sie eckig. Ein einzelnes Bild lässt sich im Editor unter <strong>Design → Bild → Bildecken</strong> abweichend einstellen.</p>
</div>
<div>
<label for="base-container">Inhaltsbreite</label>
<input id="base-container" name="base[containerWidth]" value="{{ theme.base.containerWidth }}" placeholder="1240px">
<p class="field-help">Abschnitte mit der Inhaltsbreite „Breit“ oder „Randlos“ halten sich nicht daran – das ist der Zweck dieser beiden.</p>
</div>
</div>
<div class="fields type-colors">
<div>
<label for="base-color">Textfarbe</label>
<input id="base-color" name="base[color]" value="{{ theme.base.color }}" placeholder="@ink" data-base-color>
</div>
<div>
<label for="base-bg">Seitenfarbe</label>
<input id="base-bg" name="base[backgroundColor]" value="{{ theme.base.backgroundColor }}" placeholder="@paper" data-base-color>
</div>
<div>
<label for="base-link">Linkfarbe</label>
<input id="base-link" name="base[linkColor]" value="{{ theme.base.linkColor }}" placeholder="@accent" data-base-color>
</div>
</div>
<p class="field-help">Aus der Palette wählen oder eine eigene Farbe eintragen. Eine Palettenfarbe folgt späteren Änderungen an der Palette, eine eigene bleibt, wie sie ist.</p>
<div class="field-wide">
<label class="check-label" for="base-underline"><input id="base-underline" name="base[linkUnderline]" type="checkbox" value="1"{{ (theme.base.linkUnderline ?? true) ? ' checked' : '' }} data-base-underline> Links im Fließtext unterstreichen</label>
<p class="field-help">Aus sieht ruhiger aus, aber dann erkennt man einen Link im Text nur an der Farbe – für Menschen mit Farbsehschwäche also gar nicht. Beim Überfahren erscheint der Strich in beiden Fällen. Menüs, Karten und Schaltflächen sind davon nicht betroffen.</p>
</div>
</div>
{# Die Probe: so liest sich die Website mit diesen Werten. Ohne Skript in der Schrift
der Verwaltung, mit Skript in Schrift, Größe, Zeilenhöhe und Farben von hier. #}
<aside class="type-preview-column" aria-label="Vorschau der Grundtypografie">
<h3 class="form-part">Vorschau</h3>
<div class="type-preview" data-type-preview>
<p class="type-preview-h1" data-type-h1>Überschrift der Seite</p>
<p class="type-preview-h2" data-type-h2>Ein Abschnitt darunter</p>
<p class="type-preview-p" data-type-p>Fließtext in der Grundgröße, mit <span class="type-preview-link" data-type-link>einem Link im Text</span> und genug Zeilen, um die Zeilenhöhe zu sehen. Gute Typografie fällt nicht auf – sie liest sich einfach leicht.</p>
</div>
<p class="field-help">Überschriften folgen der Skala: die erste Ebene ist vier Stufen über der Grundgröße, die zweite drei.</p>
</aside>
</div>
</section>
<fieldset class="button-defaults" id="button-defaults" data-schema="{{ {colors: theme.colors, fonts: theme.fonts, buttonPresets: buttonPresets}|json_encode|e('html_attr') }}">
<legend>Standardbutton</legend>
<p class="field-help">Die Grundlage für alle Button-Elemente. Eigene Varianten und Gestaltung im Editor haben Vorrang.</p>
<div class="button-defaults-layout">
<div class="fields">
{% for key, options in buttonOptions %}<div><label for="buttons-{{ key }}">{{ {shape: 'Form', appearance: 'Darstellung', size: 'Button-Größe'}[key] }}</label><select id="buttons-{{ key }}" name="buttons[{{ key }}]">{% for value, label in options %}<option value="{{ value }}"{{ theme.buttons[key] == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}</select></div>{% endfor %}
<div><label for="buttons-fontFamily">Button-Schrift</label><select id="buttons-fontFamily" name="buttons[fontFamily]" data-font-select><option value="inherit">Schrift der Umgebung übernehmen</option>{% for font in theme.fonts %}<option value="{{ font.key }}"{{ theme.buttons.fontFamily == font.key ? ' selected' : '' }}>{{ font.name }}</option>{% endfor %}</select></div>
<div><label for="buttons-fontSize">Button-Schriftgröße (px)</label><input id="buttons-fontSize" type="number" min="10" max="72" step="0.5" name="buttons[fontSize]" value="{{ theme.buttons.fontSize }}"></div>
<div><label for="buttons-fontWeight">Button-Schriftstärke</label><select id="buttons-fontWeight" name="buttons[fontWeight]">{% for weight in weights %}<option value="{{ weight }}"{{ theme.buttons.fontWeight == weight ? ' selected' : '' }}>{{ weight }}</option>{% endfor %}</select></div>
<div><label for="buttons-letterSpacing">Buchstabenabstand (px)</label><input id="buttons-letterSpacing" type="number" min="-1" max="8" step="0.1" name="buttons[letterSpacing]" value="{{ theme.buttons.letterSpacing }}"></div>
<div><label for="buttons-textTransform">Schreibweise</label><select id="buttons-textTransform" name="buttons[textTransform]"><option value="none"{{ theme.buttons.textTransform == 'none' ? ' selected' : '' }}>Wie eingegeben</option><option value="uppercase"{{ theme.buttons.textTransform == 'uppercase' ? ' selected' : '' }}>GROSSBUCHSTABEN</option></select></div>
{% for key, label in {backgroundColor: 'Button-Farbe', color: 'Button-Textfarbe', hoverBackgroundColor: 'Button-Farbe beim Überfahren', hoverColor: 'Textfarbe beim Überfahren'} %}<div><label for="buttons-{{ key }}">{{ label }}</label><input id="buttons-{{ key }}" name="buttons[{{ key }}]" value="{{ theme.buttons[key] }}" data-button-color></div>{% endfor %}
</div>
<div class="button-preview-column"><h3>Live-Vorschau</h3><div class="button-state-previews" aria-label="Standardbutton-Vorschau"></div><div class="button-preset-grid" role="group" aria-label="Standardbutton-Vorlagen">{% for preset in buttonPresets %}<button type="button" class="button-preset" data-button-preset aria-label="Standardbutton {{ preset.name }}"><span class="button-preset-label">{{ preset.name }}</span></button>{% endfor %}</div></div>
</div>
</fieldset>
<div class="theme-save">
<button class="primary" type="submit">Design speichern</button>
<span class="muted">Gilt sofort auf allen Seiten.</span>
</div>
</form>
<div class="card">
<div class="card-head">
<h2>Schriften ausliefern</h2>
<span class="muted">{{ fontsMissing ? fontsMissing|length ~ ' fehlt noch' : 'immer vom eigenen Server' }}</span>
</div>
<div class="card-body">
{% if fontsMissing is empty and not fontsLocal %}
<p class="muted">Dieses Design nutzt nur Systemschriften – es lädt ohnehin nichts von Google.</p>
{% else %}
{% if fontsLocal %}
<p class="muted"><strong>Die Schriften liegen auf diesem Server.</strong> Besucher laden nichts von Google, und die Seite baut sich schneller auf.</p>
{% else %}
<p class="muted"><strong>Die Schriftdateien fehlen auf diesem Server.</strong> Bis sie hier liegen, zeigt die Website die eingestellte Ersatzschrift – von Google wird nichts geladen, weder hier noch beim Besucher.</p>
{% endif %}
{% if fontsMissing %}<p class="field-help">Noch nicht hier: {{ fontsMissing|join(', ') }}.</p>{% endif %}
<form method="post" action="{{ path('admin_theme_fonts') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('theme') }}">
<button class="{{ fontsLocal ? 'secondary' : 'primary' }}" type="submit">{{ fontsLocal ? 'Schriften erneut holen' : 'Schriften auf diesen Server holen' }}</button>
</form>
{% endif %}
</div>
</div>
<p class="footnote">KantSites · Arbeitsstand</p>
{% endblock %}