Arrow icons, cascading own CSS, a preload for the hero image, and EB Solar

Built while setting up the redesign of ebsolar.de as the sixth local
installation (scripts/seed-ebsolar.php, reset-ebsolar.sh, ports 8096/8097).
The rebuild surfaced four things in the core:

- The icon set knows three arrows now: arrow-right, arrow-left,
  arrow-up-right. The button block and the post navigation render them as
  inline SVG inside the same aria-hidden span as before, so a template that
  hides the arrow with "& span{display:none}" keeps working. Until now the
  arrow was a text glyph that looked different in every font. Umut: "bei
  alle Projekte bzw bei allen möchte ich dieses emoji icon weg haben".

- Own CSS with the same nested selector twice kept only the last rule.
  "& .post-card{border:0}" followed by "& .post-card{transform:none}" lost
  the border - silently, which brought the core's card frame back on the
  project grid. The parser now joins the declarations in source order, as
  CSS does; the same for "&{…}" next to the element's own properties.

- The eager image over the fold was not preloaded. The background image of
  the first section has had a preload link for a while; a plain <img> was
  found only after the stylesheet was parsed, and that round trip went
  straight into LCP. PageController::heroImage() now writes the same src,
  srcset and sizes the block template writes, and page.html.twig emits the
  link - unless a background image is already preloaded.

- Measured on the way: an entrance effect on the LCP element costs about
  900 ms of render delay. The EB Solar hero image therefore has none.

Tests: 581 green, the icon test accepts quoted keys, two cases for the CSS
cascade, the preload asserted in the big-picture test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.1 committed 2026-09-17 12:56:57 +02:00
1 parent ab9e6a7ea0
commit d0019c80af
15 files changed
+1472 -12

No files matched your search

+19 -2
View File
@@ -193,17 +193,34 @@ final class CustomCode
if (1 !== preg_match('/^[a-zA-Z0-9 .:>+~_\[\]="\'-]*$/D', $suffix)) {
throw new InvalidDocument(sprintf('„&%s“ ist als verschachtelter Selektor nicht erlaubt.', $suffix));
}
$rules[$suffix] = $this->declarations($match[2][0]);
// Zusammenfuegen statt ersetzen: zweimal derselbe Selektor ist in CSS eine Kaskade,
// nicht ein Widerruf. Vorher gewann die letzte Regel, und alles, was nur in der
// ersten stand, verschwand ohne Meldung - gefunden am Projektraster von EB Solar,
// wo `& .post-card{border:0}` und `& .post-card{transform:none}` nebeneinander
// standen und der Rahmen des Kerns wieder auftauchte.
$rules[$suffix] = $this->verbinden($rules[$suffix] ?? '', $this->declarations($match[2][0]));
$rest = substr_replace($rest, '', $match[0][1], strlen($match[0][0]));
}
if (str_contains($rest, '{') || str_contains($rest, '}')) {
throw new InvalidDocument('Eigenes CSS besteht aus Eigenschaften für dieses Element und aus verschachtelten Regeln, die mit & beginnen. Andere Selektoren sind nicht vorgesehen.');
}
$rules[''] = $this->declarations($rest);
// Die Eigenschaften des Elements selbst stehen ueblicherweise vor den verschachtelten
// Regeln; eine Regel `&{…}` schreibt sie aber in denselben Schluessel.
$rules[''] = $this->verbinden($this->declarations($rest), $rules['']);
return $rules;
}
/** Zwei Deklarationslisten desselben Selektors, in der Reihenfolge, in der sie standen. */
private function verbinden(string $erste, string $zweite): string
{
if ('' === $erste || '' === $zweite) {
return '' === $erste ? $zweite : $erste;
}
return $erste.';'.$zweite;
}
/** @throws InvalidDocument */
private function declarations(string $block): string
{