Fonts were still reachable from Google in two places. The editor and area previews were handed `googleFonts` and no `localFonts`, so the screen used most of the day fetched Lato from fonts.googleapis.com on every open; the public page fell back to the same link whenever the files were missing. Neither path remains: the template has no Google branch, `googleFontsUrl()` is gone, and the policies name no foreign font host - `font-src 'self'` publicly, `style-src 'self'; font-src 'self'` in administration. Without local files a site now shows the theme's fallback and says so. Preloading took the first two weights per family. The Kant-IT theme names Lato 400, 700 and 900 and sets its headings on 800/900, so the one weight that carried them loaded after the stylesheet and `font-display:swap` swapped the heading in plain sight. Every weight of the theme is preloaded now, latin subset, and the links sit before the stylesheet: measured cold, all three files finish at 168 ms against a first paint at 292 ms. Open tasks no longer hide on the screen that owns them. A strip above every management screen names what needs a button and carries it: a new version leads to System, missing fonts to Design. Neither asks the network - the version comes from the daily update-check task, the fonts from a look in the folder. System and update was unusable on a phone: `.stack` is a grid whose automatic column grows to its widest content, and one long database path pushed the page sideways. It gets `minmax(0, 1fr)`, the backup table stacks below 620px, and the prose is 45% shorter with the fine print folded away. The device chips drew the current device with an outline, which lies outside the box, ran under its neighbour and was clipped by the sidebar - it read as a cut-off red frame. Used devices are filled in `#9d4e0d` now (5.6:1), the current one carries a border inside the chip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
13 KiB
Twig
156 lines
13 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'theme' %}
|
||
{% 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() }}">{% endblock %}
|
||
{% block scripts %}<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 %}
|
||
{% block content %}
|
||
<form action="{{ path('admin_theme_save') }}" method="post" class="card card-body theme-form">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('theme') }}">
|
||
|
||
<div class="section-head"><h2>Farbpalette <span class="count">{{ theme.colors|length }}</span></h2><span class="muted">Beliebig viele eigene Farben</span></div>
|
||
<table class="table theme-table">
|
||
<caption class="visually-hidden">Farben der Website. Einen Schlüssel zu leeren entfernt die Farbe.</caption>
|
||
<thead><tr><th scope="col">Schlüssel</th><th scope="col">Name</th><th scope="col">Farbe</th><th scope="col">Wert</th></tr></thead>
|
||
<tbody>
|
||
{% for row in theme.colors|merge([{key: '', name: '', value: '#000000'}, {key: '', name: '', value: '#000000'}, {key: '', name: '', value: '#000000'}]) %}
|
||
{% set n = loop.index0 %}
|
||
<tr{% if row.key == '' %} class="table-new"{% endif %}>
|
||
<td><input name="colors[{{ n }}][key]" maxlength="32" value="{{ row.key }}" pattern="[a-z][a-z0-9-]*" aria-label="Schlüssel von Farbe {{ n + 1 }}" placeholder="akzent"></td>
|
||
<td><input name="colors[{{ n }}][name]" maxlength="60" value="{{ row.name }}" aria-label="Name von Farbe {{ n + 1 }}" placeholder="Akzent"></td>
|
||
<td><input type="color" name="colors[{{ n }}][value]" value="{{ row.value }}" aria-label="Farbwähler für Farbe {{ n + 1 }}" class="theme-swatch"></td>
|
||
<td><code>{{ row.value }}</code></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<p class="field-help">Der Schlüssel wird im Editor als <code>@schlüssel</code> verwendet und bleibt bei Farbänderungen gleich – eine Farbe ändern wirkt überall, wo sie benutzt wird.</p>
|
||
|
||
<div class="section-head"><h2>Schriften <span class="count">{{ theme.fonts|length }}</span></h2><span class="muted">Systemschrift oder Google Fonts</span></div>
|
||
<table class="table theme-table">
|
||
<caption class="visually-hidden">Schriften der Website. Einen Schlüssel zu leeren entfernt die Schrift.</caption>
|
||
<thead><tr><th scope="col">Schlüssel</th><th scope="col">Name</th><th scope="col">Quelle</th><th scope="col">Schriftname</th><th scope="col">Ersatz</th><th scope="col">Schnitte</th></tr></thead>
|
||
<tbody>
|
||
{% for row in theme.fonts|merge([{key: '', name: '', family: '', source: 'google', fallback: 'sans-serif', weights: [400, 700]}]) %}
|
||
{% set n = loop.index0 %}
|
||
<tr{% if row.key == '' %} class="table-new"{% endif %}>
|
||
<td><input name="fonts[{{ n }}][key]" maxlength="40" value="{{ row.key }}" pattern="[a-z][a-z0-9-]*" aria-label="Schlüssel von Schrift {{ n + 1 }}" placeholder="heading"></td>
|
||
<td><input name="fonts[{{ n }}][name]" maxlength="60" value="{{ row.name }}" aria-label="Name von Schrift {{ n + 1 }}" placeholder="Überschriften"></td>
|
||
<td>
|
||
<select name="fonts[{{ n }}][source]" aria-label="Quelle von Schrift {{ n + 1 }}">
|
||
<option value="system"{{ row.source == 'system' ? ' selected' : '' }}>Systemschrift</option>
|
||
<option value="google"{{ row.source == 'google' ? ' selected' : '' }}>Google Fonts</option>
|
||
</select>
|
||
</td>
|
||
<td><input name="fonts[{{ n }}][family]" maxlength="48" value="{{ row.family }}" aria-label="Schriftname von Schrift {{ n + 1 }}" placeholder="Inter"></td>
|
||
<td>
|
||
<select name="fonts[{{ n }}][fallback]" aria-label="Ersatzschrift von Schrift {{ n + 1 }}">
|
||
{% for value, label in fallbacks %}<option value="{{ value }}"{{ row.fallback == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td class="weights-cell">
|
||
{% for weight in weights %}
|
||
<label><input type="checkbox" name="fonts[{{ n }}][weights][]" value="{{ weight }}"{{ weight in row.weights ? ' checked' : '' }}> {{ weight }}</label>
|
||
{% endfor %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<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. <strong>Google Fonts bedeutet eine Anfrage an Google bei jedem Seitenaufruf.</strong></p>
|
||
|
||
<div class="section-head"><h2>Grundtypografie</h2><span class="muted">Gilt überall, solange ein Block nichts anderes sagt</span></div>
|
||
<div class="fields">
|
||
<div>
|
||
<label for="base-font">Schrift für Fließtext</label>
|
||
<select id="base-font" name="base[fontFamily]">
|
||
{% 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]">
|
||
{% 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 class="check-label" for="base-underline"><input id="base-underline" name="base[linkUnderline]" type="checkbox" value="1"{{ (theme.base.linkUnderline ?? true) ? ' checked' : '' }}> 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>
|
||
<label for="base-container">Inhaltsbreite</label>
|
||
<input id="base-container" name="base[containerWidth]" value="{{ theme.base.containerWidth }}" placeholder="1240px">
|
||
<p class="field-help">Die Breite des Inhalts auf allen Seiten. Abschnitte mit der Inhaltsbreite „Breit“ oder „Randlos“ halten sich nicht daran – das ist der Zweck dieser beiden.</p>
|
||
</div>
|
||
<div>
|
||
<label for="base-color">Textfarbe</label>
|
||
<input id="base-color" name="base[color]" value="{{ theme.base.color }}" placeholder="@ink" data-color data-palette="{{ theme.colors|json_encode|e('html_attr') }}">
|
||
</div>
|
||
<div>
|
||
<label for="base-bg">Hintergrundfarbe</label>
|
||
<input id="base-bg" name="base[backgroundColor]" value="{{ theme.base.backgroundColor }}" placeholder="@paper" data-color data-palette="{{ theme.colors|json_encode|e('html_attr') }}">
|
||
</div>
|
||
<div>
|
||
<label for="base-link">Linkfarbe</label>
|
||
<input id="base-link" name="base[linkColor]" value="{{ theme.base.linkColor }}" placeholder="@accent" data-color data-palette="{{ theme.colors|json_encode|e('html_attr') }}">
|
||
</div>
|
||
</div>
|
||
<p class="field-help">Farben entweder als <code>@schlüssel</code> aus der Palette oder als Wert wie <code>#1a2b3c</code>.</p>
|
||
<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]"><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>
|
||
<button class="primary" type="submit">Design speichern</button>
|
||
</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 %}
|