Forms are an editor now: one card per field with add and remove, the technical name derived from the label, options only for a choice, and a new "Aussehen" section with a button label and four colours (field border and background, button and button text) that travel to the page as one rule per form inside the hashed style block. A live preview draws the form with the site's default button and names the text contrast. Statistics open with six figures (views against the same span before, average per day, busiest day, pages, referred views, missing addresses), a chart with grid lines and a title per bar, a weekday chart and ranked lists with share bars. The design screen shows the palette as colour cards with seven ready-made palettes, picks the base colours from the palette, previews every font in itself and the typography as a sample. The sidebar ends in an account card with role and a logout icon. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
251 lines
19 KiB
Twig
251 lines
19 KiB
Twig
{% 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') }}">
|
||
|
||
<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-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 %}
|