Let menu entries appear as buttons with icon, colours and phone placement
The menu editor gets "Als Button darstellen" per entry: shape, appearance and size from the button design, fill and ink with hover and palette, icon colour, icon from the dialog, motion, icon-only and where the button sits on the phone (in the opened menu, beside the burger, or there as an icon only). Colours go into the page stylesheet under a stable per-button id because the CSP forbids inline styles. ADR 110. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48ae2443cd
commit
13f6759df8
22 files changed
+623
-14
No files matched your search
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Content;
|
||||
|
||||
use App\Domain\InvalidMenu;
|
||||
|
||||
/**
|
||||
* Ein Menüeintrag, der als Button erscheint (ADR 110).
|
||||
*
|
||||
* Die Gestalt kommt aus demselben Wortschatz wie der Button-Baustein: Form, Darstellung und Größe
|
||||
* aus ButtonDesign, das Symbol aus dem Symbolsatz. Fehlt ein Wert, gilt der Website-Standard unter
|
||||
* Design → Buttons. Eigene Farben brauchen eine Kennung, weil die Sicherheitsrichtlinie keine
|
||||
* Inline-Stile erlaubt: sie werden als Regel für `.menu-button-<id>` ins Stylesheet der Seite
|
||||
* geschrieben.
|
||||
*
|
||||
* Gespeichert am Eintrag als `button`:
|
||||
* {id, shape?, appearance?, size?, fill?, ink?, hoverFill?, hoverInk?, iconColor?, mobile, iconMotion?, iconOnly?}
|
||||
*/
|
||||
final class MenuButton
|
||||
{
|
||||
/** Wo der Button auf dem Telefon steht, wenn das Menü zum Menü-Knopf zusammenklappt. */
|
||||
public const MOBILE = ['menu' => 'Im aufgeklappten Menü', 'bar' => 'Neben dem Menü-Knopf', 'bar-icon' => 'Neben dem Menü-Knopf, nur als Symbol'];
|
||||
public const MOTION = ['none', 'slide', 'reveal'];
|
||||
/** Formularfeld => gespeicherter Schlüssel. */
|
||||
public const COLORS = ['buttonFill' => 'fill', 'buttonInk' => 'ink', 'buttonHoverFill' => 'hoverFill', 'buttonHoverInk' => 'hoverInk', 'buttonIconColor' => 'iconColor'];
|
||||
private const DESIGN = ['buttonShape' => 'shape', 'buttonAppearance' => 'appearance', 'buttonSize' => 'size'];
|
||||
|
||||
/**
|
||||
* Aus den flachen Formularfeldern einer Zeile. Ohne Häkchen „Als Button“ kein Button.
|
||||
*/
|
||||
public static function fromForm(array $row): ?array
|
||||
{
|
||||
if ('1' !== (string) ($row['button'] ?? '')) {
|
||||
return null;
|
||||
}
|
||||
$id = (string) ($row['buttonId'] ?? '');
|
||||
$button = ['id' => 1 === preg_match('/^m[a-f0-9]{10}$/D', $id) ? $id : 'm'.bin2hex(random_bytes(5))];
|
||||
foreach ([...self::DESIGN, ...self::COLORS] as $field => $key) {
|
||||
$value = trim((string) ($row[$field] ?? ''));
|
||||
if ('' !== $value) {
|
||||
$button[$key] = $value;
|
||||
}
|
||||
}
|
||||
$button['mobile'] = (string) ($row['buttonMobile'] ?? 'menu');
|
||||
$motion = (string) ($row['iconMotion'] ?? 'none');
|
||||
if ('none' !== $motion) {
|
||||
$button['iconMotion'] = $motion;
|
||||
}
|
||||
if ('1' === (string) ($row['iconOnly'] ?? '')) {
|
||||
$button['iconOnly'] = true;
|
||||
}
|
||||
|
||||
return $button;
|
||||
}
|
||||
|
||||
public static function validate(mixed $button, array $item): void
|
||||
{
|
||||
if (!is_array($button) || 1 !== preg_match('/^m[a-f0-9]{10}$/D', (string) ($button['id'] ?? ''))) {
|
||||
throw new InvalidMenu('Ein Menü-Button braucht eine Kennung.');
|
||||
}
|
||||
$known = ['id', 'mobile', 'iconMotion', 'iconOnly', ...array_values(self::DESIGN), ...array_values(self::COLORS)];
|
||||
if ([] !== array_diff(array_keys($button), $known)) {
|
||||
throw new InvalidMenu('Unbekannte Einstellung am Menü-Button.');
|
||||
}
|
||||
foreach (self::DESIGN as $key) {
|
||||
if (isset($button[$key]) && !isset(ButtonDesign::OPTIONS[$key][$button[$key]])) {
|
||||
throw new InvalidMenu('Unbekannte Form, Darstellung oder Größe am Menü-Button.');
|
||||
}
|
||||
}
|
||||
foreach (self::COLORS as $key) {
|
||||
if (isset($button[$key]) && (!is_string($button[$key]) || 1 !== preg_match(StyleSchema::COLOR, $button[$key]))) {
|
||||
throw new InvalidMenu('Eine Farbe am Menü-Button ist ein Hexwert wie #b71c22 oder eine Farbe der Palette.');
|
||||
}
|
||||
}
|
||||
if (!isset(self::MOBILE[$button['mobile'] ?? ''])) {
|
||||
throw new InvalidMenu('Unbekannte Anzeige auf dem Telefon.');
|
||||
}
|
||||
// Auf dem Telefon nur als Symbol: ohne Symbol stünde dort ein leerer Knopf.
|
||||
if ('bar-icon' === $button['mobile'] && '' === ($item['icon'] ?? '')) {
|
||||
throw new InvalidMenu('Nur als Symbol neben dem Menü-Knopf braucht ein Symbol.');
|
||||
}
|
||||
if (isset($button['iconMotion']) && !in_array($button['iconMotion'], self::MOTION, true)) {
|
||||
throw new InvalidMenu('Unbekannte Bewegung des Symbols.');
|
||||
}
|
||||
if (isset($button['iconOnly']) && (true !== $button['iconOnly'] || '' === ($item['icon'] ?? ''))) {
|
||||
throw new InvalidMenu('Ein Menü-Button nur aus Symbol braucht ein Symbol.');
|
||||
}
|
||||
// Ein Knopf öffnet kein Untermenü: mit Unterpunkten wäre er Knopf und Aufklapper zugleich.
|
||||
if ([] !== ($item['children'] ?? [])) {
|
||||
throw new InvalidMenu(sprintf('„%s“ hat Unterpunkte und kann deshalb kein Button sein.', $item['label'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Farbregeln aller Menü-Buttons. Gesetzt werden die Variablen, aus denen die Button-Stile
|
||||
* ihre Flächen berechnen, plus die abgeleiteten Werte der gewählten oder der Standard-
|
||||
* Darstellung - sonst rechnete ein Button ohne eigene Darstellung weiter mit den Farben vom
|
||||
* Seitenanfang.
|
||||
*
|
||||
* @param array<string, array{items: array}> $menus
|
||||
*/
|
||||
public static function css(array $menus, array $themeButtons): string
|
||||
{
|
||||
$color = static fn (string $v): string => str_starts_with($v, '@') ? 'var(--k-'.substr($v, 1).')' : $v;
|
||||
$css = '';
|
||||
$walk = function (array $items) use (&$walk, &$css, $color, $themeButtons): void {
|
||||
foreach ($items as $item) {
|
||||
$walk($item['children'] ?? []);
|
||||
$button = $item['button'] ?? null;
|
||||
if (!is_array($button) || !isset($button['id'])) {
|
||||
continue;
|
||||
}
|
||||
$vars = [];
|
||||
foreach (['fill' => '--kb-fill', 'ink' => '--kb-ink', 'hoverFill' => '--kb-hover-fill', 'hoverInk' => '--kb-hover-ink'] as $key => $var) {
|
||||
if (isset($button[$key])) {
|
||||
$vars[] = $var.':'.$color($button[$key]);
|
||||
}
|
||||
}
|
||||
if ([] !== $vars) {
|
||||
foreach (ButtonDesign::appearance($button['appearance'] ?? $themeButtons['appearance'] ?? 'solid') as $key => $value) {
|
||||
$vars[] = '--kb-'.$key.':'.$value;
|
||||
}
|
||||
$css .= sprintf('.menu-button-%s{%s}', $button['id'], implode(';', $vars));
|
||||
}
|
||||
if (isset($button['iconColor'])) {
|
||||
$css .= sprintf('.menu-button-%s .button-icon{color:%s}', $button['id'], $color($button['iconColor']));
|
||||
}
|
||||
}
|
||||
};
|
||||
foreach ($menus as $menu) {
|
||||
$walk($menu['items'] ?? []);
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
}
|
||||
@@ -132,12 +132,18 @@ final class MenuRepository
|
||||
if ('' === trim($label)) {
|
||||
continue;
|
||||
}
|
||||
$links[] = ['label' => $label, 'href' => $href, 'current' => $current, 'children' => $children, ...(($item['newTab'] ?? false) ? ['newTab' => true] : []), ...(!empty($item['icon']) ? ['icon' => $item['icon'], 'iconPosition' => $item['iconPosition'] ?? 'before'] : [])];
|
||||
$links[] = ['label' => $label, 'href' => $href, 'current' => $current, 'children' => $children, ...(($item['newTab'] ?? false) ? ['newTab' => true] : []), ...(!empty($item['icon']) ? ['icon' => $item['icon'], 'iconPosition' => $item['iconPosition'] ?? 'before'] : []), ...(isset($item['button']) ? ['button' => $item['button']] : [])];
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/** Die Farben der Menü-Buttons aller Plätze, fürs Stylesheet der Seite (ADR 110). */
|
||||
public function buttonCss(array $themeButtons, string $locale = 'de'): string
|
||||
{
|
||||
return MenuButton::css($this->all($locale), $themeButtons);
|
||||
}
|
||||
|
||||
private function validate(array $items, int $depth = 1, int &$count = 0): void
|
||||
{
|
||||
if (!array_is_list($items)) {
|
||||
@@ -175,6 +181,9 @@ final class MenuRepository
|
||||
} else {
|
||||
throw new InvalidMenu('Unbekannte Zielart.');
|
||||
}
|
||||
if (array_key_exists('button', $item)) {
|
||||
MenuButton::validate($item['button'], $item);
|
||||
}
|
||||
$this->validate($item['children'] ?? [], $depth + 1, $count);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user