Pick the site icon from the library and crop it, find legal pages by search, list newest first

- The site icon is no longer a select at the end of the company data. It has
  its own section: a button opens the media library (upload included), then
  a square is dragged, resized by its corner, a slider or +/-, and previewed
  at 180 and 32 px. The crop is stored as fractions (faviconCrop) and the
  PNGs are cut from it; faviconVersion changes the link so browsers pick up
  a new cut. Icons without a crop keep their old link.
- openMedia() takes needsAlt: false, since the icon never appears as
  content. The dialog styles moved from editor.css to media-dialog.css so
  the dialog works outside the editor.
- Privacy page and imprint are chosen with the content search, like the 404
  page. The picker can store the path instead of the id (valueKey/data-value).
- The page and post lists in the administration sort newest first; the
  subpage indentation is gone there, as it would point at the wrong parent.

Tests: 581 PHPUnit, 35 node, test:editor green (the consent step now picks
the privacy page by search). test:admin --admin-only fails only at the known
nested-interactive menu summaries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-18 07:43:05 +02:00
1 parent 6632836580
commit 106f84dfd5
19 files changed
+300 -69

No files matched your search

+19 -6
View File
@@ -222,11 +222,16 @@ final class MediaLibrary
* Erzeugt aus einem Bild der Mediathek die Symboldateien der Website.
*
* Ein Favicon ist quadratisch, winzig und wird von den Browsern bis heute am zuverlässigsten
* als PNG gelesen - deshalb hier PNG statt der WebP-Varianten und ein mittiger quadratischer
* als PNG gelesen - deshalb hier PNG statt der WebP-Varianten und ein quadratischer
* Ausschnitt statt eines gestauchten Bildes. Erzeugt wird einmal beim Speichern der
* Einstellung, nicht bei jedem Aufruf.
*
* $crop ist der im Zuschnitt gewählte Ausschnitt als [links, oben, Kante]: links und oben als
* Anteil von Breite und Höhe, die Kante als Anteil der kürzeren Seite. Ohne Angabe die Mitte.
*
* @param array{0: float, 1: float, 2: float}|null $crop
*/
public function renderFavicon(string $mediaId): void
public function renderFavicon(string $mediaId, ?array $crop = null): void
{
// Ueber die Varianten, die dieses Bild tatsaechlich hat, nicht ueber die vier Regelbreiten:
// ein 192 px breites Zeichen hat genau eine Variante mit 192 - und die Suche nach 480
@@ -244,10 +249,18 @@ final class MediaLibrary
throw new \RuntimeException('Das Bild für das Symbol konnte nicht gelesen werden.');
}
try {
// Mittiger quadratischer Ausschnitt: ein breites Logo würde sonst zur Briefmarke.
$kante = min(imagesx($source), imagesy($source));
$links = (int) ((imagesx($source) - $kante) / 2);
$oben = (int) ((imagesy($source) - $kante) / 2);
// Quadratischer Ausschnitt: ein breites Logo würde sonst zur Briefmarke. Die Anteile
// gelten für jede Variante gleich; gerechnet wird in der größten, die es gibt.
$breite = imagesx($source);
$hoehe = imagesy($source);
$kante = min($breite, $hoehe);
$links = (int) (($breite - $kante) / 2);
$oben = (int) (($hoehe - $kante) / 2);
if (null !== $crop) {
$kante = max(1, min($kante, (int) round($crop[2] * min($breite, $hoehe))));
$links = max(0, min($breite - $kante, (int) round($crop[0] * $breite)));
$oben = max(0, min($hoehe - $kante, (int) round($crop[1] * $hoehe)));
}
foreach (self::FAVICON_SIZES as $size) {
$target = imagecreatetruecolor($size, $size);
try {