Back up unsaved editor work locally, structure the settings, fix archive accessibility

- The editor keeps unsaved changes in the browser and offers them
  after a crash, a closed tab or an expired session. No timed save on
  the server: every save is a full revision.
- The save status stays visible in narrow windows.
- Settings get a sticky section bar, two folded groups and a save bar
  that stays in view.
- The admin post list fetches terms and images once, not per post.
- Archive post titles are second-level headings, the archive head
  keeps full text contrast, the skip link sits above a sticky header,
  a linked image without alternative text names its link.
- The editor check starts through the dev router like the others.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.2 committed 2026-11-10 13:01:23 +01:00
1 parent 5e46c98825
commit b08862b139
17 files changed
+182 -26

No files matched your search

+4
View File
@@ -103,6 +103,10 @@ final class BacklogTest extends TestCase
self::assertStringContainsString('<h1', $html);
self::assertStringContainsString('Beiträge', $html);
self::assertStringContainsString('Aus der Fußsektion', $html, 'Der Fuß samt verknüpfter Sektion steht unter dem Archiv.');
// Auf die Hauptüberschrift folgt im Archiv die zweite Ebene, nicht die dritte - sonst
// springt die Gliederung, und Lighthouse zieht dafür Punkte bei der Barrierefreiheit ab.
self::assertMatchesRegularExpression('#<div class="post-body">\s*<h2><a href="/[a-z0-9-]+">#', $html);
self::assertStringNotContainsString('<h3><a', $html);
self::assertStringContainsString('#fuss-hinweis p{letter-spacing:0.3em}', $html, 'Eigenes CSS der Sektion kommt mit.');
self::assertStringContainsString('scroll-top', $html, 'Der Nach-oben-Knopf ist da wie auf jeder Seite.');
self::assertStringContainsString('<link rel="canonical" href="https://cms.test/beitraege">', $html);
+8
View File
@@ -1035,6 +1035,14 @@ final class CmsTest extends TestCase
self::assertStringContainsString('<a class="image-link" href="/">', $html);
self::assertStringContainsString('alt="Firmenlogo"', $html);
// Ein dekoratives Logo hat keinen Alternativtext - der Link bekäme sonst keinen Namen.
$document['blocks'][array_key_last($document['blocks'])]['props'] = ['src' => '/media/'.str_repeat('a', 32).'/480', 'alt' => '', 'decorative' => true, 'width' => 480, 'height' => 120, 'href' => '/'];
$document['blocks'][] = ['id' => 'siegel', 'type' => 'image', 'props' => ['src' => '/media/'.str_repeat('b', 32).'/480', 'alt' => '', 'decorative' => true, 'width' => 480, 'height' => 120, 'href' => '/kontakt']];
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 2), 3);
$html = $this->request('/')->getContent();
self::assertStringContainsString('<a class="image-link" href="/" aria-label="Startseite">', $html);
self::assertStringContainsString('<a class="image-link" href="/kontakt" aria-label="Link: /kontakt">', $html);
// Datum, Autor und Kategorie über einem Beitrag lassen sich abschalten - ein Beitrag mit
// eigenem Kopfbild trägt den Titel schon, und die Zeile darüber steht dann im Weg.
$this->publishedPost('projekt', 'Ein Projekt', '2026-02-01', ['Projekte'], []);