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>
90 lines
6.9 KiB
Twig
90 lines
6.9 KiB
Twig
{# The frame around every management screen. One sidebar, always the same, so each area is one
|
|
click away and the current one is visible. Screens fill in heading, lead, actions and content. #}
|
|
{% extends 'admin/base.html.twig' %}
|
|
{% block body_class %}admin-shell{% endblock %}
|
|
{# Solange jemand hier arbeitet, laeuft der Aufgabenplan - auch auf einer Website ohne Besucher.
|
|
Das Skript fragt im Minutentakt an; was faellig ist, entscheidet der Server. #}
|
|
{% block worker %}
|
|
<meta name="kant-tick" content="{{ path('admin_tasks_tick') }}">
|
|
<meta name="kant-tick-token" content="{{ csrf_token('tasks-tick') }}">
|
|
<script src="/assets/task-worker.js?v={{ kant_version() }}" defer></script>
|
|
{% endblock %}
|
|
{% block body %}
|
|
<header class="shell-nav">
|
|
<a class="brand" href="{{ path('admin_pages') }}"><span class="brand-icon" aria-hidden="true">k.</span>kant<span>sites</span></a>
|
|
{# Auf dem Telefon ein Klappmenue, am Schreibtisch die gewohnte Spalte. Ein details-Element
|
|
kann beides, ohne eine Zeile JavaScript: die Zusammenfassung wird auf grossen Bildschirmen
|
|
ausgeblendet und der Inhalt dauerhaft gezeigt. Zwoelf Eintraege in einer Wischleiste zu
|
|
verstecken war die schlechtere Loesung - man sieht nie, was man nicht trifft. #}
|
|
<details class="shell-menu">
|
|
<summary>
|
|
<svg class="shell-menu-icon" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
|
<span class="shell-menu-label">{{ {
|
|
'pages': 'Seiten', 'posts': 'Beiträge', 'partials': 'Bereiche', 'menus': 'Menüs', 'media': 'Mediathek',
|
|
'forms': 'Formulare', 'statistics': 'Statistik', 'theme': 'Design',
|
|
'settings': 'Einstellungen', 'system': 'System', 'tasks': 'Aufgaben', 'users': 'Benutzer',
|
|
'account': 'Mein Konto'
|
|
}[nav|default('')]|default('Verwaltung') }}</span>
|
|
</summary>
|
|
<nav aria-label="Verwaltung">
|
|
<p class="nav-label" id="nav-content">INHALT</p>
|
|
<ul class="nav-list" aria-labelledby="nav-content">
|
|
{% set entries = [
|
|
{id: 'pages', route: 'admin_pages', icon: 'pages', label: 'Seiten'},
|
|
{id: 'posts', route: 'admin_posts', icon: 'pages', label: 'Beiträge'},
|
|
{id: 'partials', route: 'admin_partials', icon: 'partials', label: 'Bereiche'},
|
|
{id: 'menus', route: 'admin_menus', icon: 'menus', label: 'Menüs'},
|
|
{id: 'media', route: 'admin_media', icon: 'media', label: 'Mediathek'},
|
|
{id: 'forms', route: 'admin_forms', icon: 'forms', label: 'Formulare'},
|
|
{id: 'statistics', route: 'admin_statistics', icon: 'statistics', label: 'Statistik'}
|
|
] %}
|
|
{% for entry in entries %}
|
|
<li><a class="nav-item" href="{{ path(entry.route) }}"{{ nav|default('') == entry.id ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: entry.icon} only %}{{ entry.label }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
<p class="nav-label" id="nav-site">WEBSITE</p>
|
|
<ul class="nav-list" aria-labelledby="nav-site">
|
|
<li><a class="nav-item" href="{{ path('admin_theme') }}"{{ nav|default('') == 'theme' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'theme'} only %}Design</a></li>
|
|
<li><a class="nav-item" href="{{ path('admin_settings') }}"{{ nav|default('') == 'settings' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'settings'} only %}Einstellungen</a></li>
|
|
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_system') }}"{{ nav|default('') == 'system' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'system'} only %}System</a></li>{% endif %}
|
|
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_tasks') }}"{{ nav|default('') == 'tasks' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'tasks'} only %}Aufgaben</a></li>{% endif %}
|
|
{% if is_granted('ROLE_ADMIN') %}<li><a class="nav-item" href="{{ path('admin_users') }}"{{ nav|default('') == 'users' ? ' aria-current="page"' : '' }}>{% include 'admin/icon.html.twig' with {name: 'users'} only %}Benutzer</a></li>{% endif %}
|
|
<li><a class="nav-item nav-out" href="/">{% include 'admin/icon.html.twig' with {name: 'external'} only %}Website ansehen</a></li>
|
|
</ul>
|
|
</nav>
|
|
</details>
|
|
<div class="shell-foot">
|
|
<p class="shell-version">{% if is_granted('ROLE_ADMIN') %}<a href="{{ path('admin_system') }}">Fassung {{ kant_version() }}</a>{% set updateVersion = kant_update_version() %}{% if updateVersion %}<a class="version-update" href="{{ path('admin_system', {pruefen: 1}) }}" aria-label="Update {{ updateVersion }} verfügbar. Aktualisierung öffnen.">Update {{ updateVersion }} verfügbar</a>{% endif %}{% else %}Fassung {{ kant_version() }}{% endif %}</p>
|
|
<a class="shell-user" href="{{ path('admin_account') }}" aria-label="Mein Konto: {{ app.user.userIdentifier }}"{{ nav|default('') == 'account' ? ' aria-current="page"' : '' }}><span class="shell-avatar" aria-hidden="true">{{ app.user.userIdentifier|first|upper }}</span><strong>{{ app.user.userIdentifier }}</strong></a>
|
|
<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="shell-main" tabindex="-1">
|
|
<div class="shell-top">
|
|
<div class="shell-heading">
|
|
<h1>{% block heading %}Verwaltung{% endblock %}</h1>
|
|
<p class="muted">{% block lead %}{% endblock %}</p>
|
|
</div>
|
|
<div class="shell-actions">{% block actions %}{% endblock %}</div>
|
|
</div>
|
|
<div class="shell-body stack">
|
|
{# Offene Handlungen, auf jedem Bildschirm gleich: fehlende Schriften, eine neue Fassung.
|
|
Sie stehen hier statt nur auf ihrer eigenen Seite - einen Hinweis findet nur, wer schon
|
|
weiss, wo er steht. Jeder traegt den Knopf, der ihn erledigt. #}
|
|
{% for hinweis in kant_admin_notices() %}{% if not hinweis.admin or is_granted('ROLE_ADMIN') %}
|
|
<p class="shell-notice" role="status">
|
|
<span>{{ hinweis.text }}</span>
|
|
<a class="secondary" href="{{ path(hinweis.route) }}">{{ hinweis.action }} →</a>
|
|
</p>
|
|
{% endif %}{% endfor %}
|
|
{% 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 %}
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
{% endblock %}
|