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>
137 lines
6.4 KiB
PHP
137 lines
6.4 KiB
PHP
<?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;
|
|
}
|
|
}
|