Files
KantSites/src/Content/StyleSchema.php
T
kantarciandClaude Opus 5 7251b82f09 Designer: 93 Eigenschaften, neun Einblendungen, drei neue Bausteine
Umut fragte nach Zeilen, nach Rahmen und Rundungen - und ob der Designer so
aufgestellt ist wie Flatsome. Der Vergleich steht in
docs/DESIGNER-VERGLEICH.md, mit der Einschraenkung, dass Flatsome hier nicht
installierbar war: das ist Wissen, keine Messung.

Zwei echte Fehler kamen dabei heraus, beide vorher unbemerkt.

Eine Rahmenstaerke je Seite wurde zu border-width-top. Das ist kein CSS, der
Browser verwirft die Zeile stillschweigend - Rahmenstaerken je Seite hatten
nie eine Wirkung, und es gab keine Meldung, die darauf hingewiesen haette. Die
Schreibweise steht jetzt je Eigenschaft im Schema, weil padding-top dem Namen
folgt und border-top-width nicht.

Und eine Rundung auf einem Bildblock landete auf dem figure, waehrend das Bild
darin eckig blieb. Eigenschaften koennen jetzt auf das Bild im Block zielen
(target 'img' im Schema, eine zweite Regel im Compiler) - deshalb tut
object-fit ueberhaupt etwas, denn auf einem figure tut es nichts.

Von 50 auf 93 Eigenschaften: Bild (Ausschnitt, Bildmitte, Seitenverhaeltnis,
Hoehe, Ecken, Rahmen), Positionierung mit z-index, getrennte Zeilen- und
Spaltenabstaende, Flex je Element, Innen- und Textschatten, acht Bildfilter
als eine filter-Deklaration in fester Reihenfolge, Mischmodus, Zusatzrahmen,
Mauszeiger. Neun Einblendungen mit Verzoegerung, sieben Hover-Effekte, alle
innerhalb von prefers-reduced-motion.

Drei neue Bausteine (Dokumentschema v15): Symbol als eigenes Element,
Fortschritt mit role=meter und dem Wert im Markup statt nur in der
Balkenbreite, und ein Slider als Scrollbereich mit Einrasten - ohne
JavaScript, ohne Autoplay, mit Finger, Trackpad, Mausrad und Tastatur.

Zeilen waren nicht unauffindbar, sondern unbrauchbar leer: eine Zeile ohne
Spalten sieht aus wie nichts. Jetzt legt "Zeile, 2/3/4 Spalten" beides auf
einen Klick an, und im Werkzeugkasten steht, wie der Aufbau gemeint ist.

315 PHP-Tests / 1309 Assertions, 16 JS-Modelltests, axe auf dreizehn
Bildschirmen ohne Verstoesse. 16 Datensaetze rechnen jede Eigenschaftsform
gegen die erwartete Deklaration nach - die Pruefung, die die beiden Fehler
oben gefunden haette, wenn es sie vorher gegeben haette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 20:37:49 +02:00

232 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Content;
/**
* The single source of truth for free block styling.
*
* Every property is declared once here with its type, its CSS counterpart and its limits. The
* validator, the CSS generator and the editor panel all read this definition, so the three can
* never drift apart: what the editor offers is exactly what the server accepts and renders.
*
* Values are stored as short strings ("18px", "#1a2b3c", "flex"), never as CSS text. Each type has
* an anchored pattern or a fixed list, so no declaration can escape its property.
*/
final class StyleSchema
{
public const DEVICES = ['desktop' => 'Desktop', 'tablet' => 'Tablet', 'mobile' => 'Mobil'];
/** A colour is a hex value or "@name", a reference into the site palette. */
public const COLOR = '/^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|@[a-z][a-z0-9-]{0,31})$/D';
/** A length is a number with a unit, plus the two keywords that behave like one. */
public const LENGTH = '/^(0|-?\d{1,5}(\.\d{1,3})?(px|rem|em|%|vw|vh|ch))$/D';
public const LENGTH_AUTO = '/^(auto|none|0|-?\d{1,5}(\.\d{1,3})?(px|rem|em|%|vw|vh|ch))$/D';
/**
* group – where the property appears in the editor
* type – length | length-auto | color | number | choice | text
* css – the CSS property it becomes; sides/corners expand to four
* label – German label for the editor
* target – 'img' applies the declaration to the images inside the block instead of the block
* itself. object-fit on a figure does nothing; it belongs on the image.
* sideCss – how one side is spelled. "padding-top" follows the property name, but a border
* width is "border-top-width" and not "border-width-top" - written out, because
* guessing it wrong produces a declaration the browser silently drops.
*
* @return array<string, array{group: string, type: string, css: string|array, label: string, values?: array<string, string>, min?: float, max?: float, step?: float, sides?: bool, hover?: bool}>
*/
public static function properties(): array
{
return [
// ---- Typografie -------------------------------------------------------------
'fontFamily' => ['group' => 'text', 'type' => 'font', 'css' => 'font-family', 'label' => 'Schriftart'],
'fontSize' => ['group' => 'text', 'type' => 'length', 'css' => 'font-size', 'label' => 'Schriftgröße'],
'fontWeight' => ['group' => 'text', 'type' => 'choice', 'css' => 'font-weight', 'label' => 'Schriftstärke', 'values' => [
'100' => '100 · Thin', '200' => '200 · Extra Light', '300' => '300 · Light', '400' => '400 · Regular',
'500' => '500 · Medium', '600' => '600 · Semibold', '700' => '700 · Bold', '800' => '800 · Extra Bold', '900' => '900 · Black',
]],
'fontStyle' => ['group' => 'text', 'type' => 'choice', 'css' => 'font-style', 'label' => 'Schriftlage', 'values' => ['normal' => 'Normal', 'italic' => 'Kursiv']],
'lineHeight' => ['group' => 'text', 'type' => 'number', 'css' => 'line-height', 'label' => 'Zeilenhöhe', 'min' => 0.7, 'max' => 4, 'step' => 0.05],
'letterSpacing' => ['group' => 'text', 'type' => 'length', 'css' => 'letter-spacing', 'label' => 'Buchstabenabstand'],
'wordSpacing' => ['group' => 'text', 'type' => 'length', 'css' => 'word-spacing', 'label' => 'Wortabstand'],
'textAlign' => ['group' => 'text', 'type' => 'choice', 'css' => 'text-align', 'label' => 'Ausrichtung', 'values' => [
'left' => 'Links', 'center' => 'Mittig', 'right' => 'Rechts', 'justify' => 'Blocksatz',
]],
'textTransform' => ['group' => 'text', 'type' => 'choice', 'css' => 'text-transform', 'label' => 'Schreibweise', 'values' => [
'none' => 'Wie eingegeben', 'uppercase' => 'GROSSBUCHSTABEN', 'lowercase' => 'kleinbuchstaben', 'capitalize' => 'Erster Buchstabe Groß',
]],
'textDecoration' => ['group' => 'text', 'type' => 'choice', 'css' => 'text-decoration-line', 'label' => 'Textlinie', 'values' => [
'none' => 'Keine', 'underline' => 'Unterstrichen', 'line-through' => 'Durchgestrichen',
]],
'color' => ['group' => 'text', 'type' => 'color', 'css' => 'color', 'label' => 'Textfarbe', 'hover' => true],
// ---- Hintergrund ------------------------------------------------------------
'backgroundColor' => ['group' => 'background', 'type' => 'color', 'css' => 'background-color', 'label' => 'Hintergrundfarbe', 'hover' => true],
'gradientFrom' => ['group' => 'background', 'type' => 'color', 'css' => null, 'label' => 'Verlauf von'],
'gradientTo' => ['group' => 'background', 'type' => 'color', 'css' => null, 'label' => 'Verlauf bis'],
'gradientAngle' => ['group' => 'background', 'type' => 'number', 'css' => null, 'label' => 'Verlaufswinkel', 'min' => 0, 'max' => 360, 'step' => 1],
'backgroundImage' => ['group' => 'background', 'type' => 'media', 'css' => null, 'label' => 'Hintergrundbild'],
'backgroundSize' => ['group' => 'background', 'type' => 'choice', 'css' => 'background-size', 'label' => 'Bildgröße', 'values' => [
'cover' => 'Füllend', 'contain' => 'Vollständig sichtbar', 'auto' => 'Originalgröße',
]],
'backgroundPosition' => ['group' => 'background', 'type' => 'choice', 'css' => 'background-position', 'label' => 'Bildausschnitt', 'values' => [
'center' => 'Mittig', 'top' => 'Oben', 'bottom' => 'Unten', 'left' => 'Links', 'right' => 'Rechts',
'top left' => 'Oben links', 'top right' => 'Oben rechts', 'bottom left' => 'Unten links', 'bottom right' => 'Unten rechts',
]],
'backgroundRepeat' => ['group' => 'background', 'type' => 'choice', 'css' => 'background-repeat', 'label' => 'Wiederholung', 'values' => [
'no-repeat' => 'Nicht wiederholen', 'repeat' => 'Kacheln', 'repeat-x' => 'Waagerecht kacheln', 'repeat-y' => 'Senkrecht kacheln',
]],
'backgroundAttachment' => ['group' => 'background', 'type' => 'choice', 'css' => 'background-attachment', 'label' => 'Scrollverhalten', 'values' => [
'scroll' => 'Mitscrollen', 'fixed' => 'Feststehend (Parallax)',
]],
'overlayColor' => ['group' => 'background', 'type' => 'color', 'css' => null, 'label' => 'Überlagerung'],
'overlayOpacity' => ['group' => 'background', 'type' => 'number', 'css' => null, 'label' => 'Deckkraft der Überlagerung', 'min' => 0, 'max' => 1, 'step' => 0.05],
// ---- Abstand ----------------------------------------------------------------
'padding' => ['group' => 'spacing', 'type' => 'length', 'css' => 'padding', 'label' => 'Innenabstand', 'sides' => true, 'sideCss' => 'padding-%s'],
'margin' => ['group' => 'spacing', 'type' => 'length-auto', 'css' => 'margin', 'label' => 'Außenabstand', 'sides' => true, 'sideCss' => 'margin-%s'],
// ---- Größe ------------------------------------------------------------------
'width' => ['group' => 'size', 'type' => 'length-auto', 'css' => 'width', 'label' => 'Breite'],
'maxWidth' => ['group' => 'size', 'type' => 'length-auto', 'css' => 'max-width', 'label' => 'Maximale Breite'],
'minWidth' => ['group' => 'size', 'type' => 'length-auto', 'css' => 'min-width', 'label' => 'Mindestbreite'],
'height' => ['group' => 'size', 'type' => 'length-auto', 'css' => 'height', 'label' => 'Höhe'],
'minHeight' => ['group' => 'size', 'type' => 'length-auto', 'css' => 'min-height', 'label' => 'Mindesthöhe'],
'maxHeight' => ['group' => 'size', 'type' => 'length-auto', 'css' => 'max-height', 'label' => 'Maximale Höhe'],
'overflow' => ['group' => 'size', 'type' => 'choice', 'css' => 'overflow', 'label' => 'Überlauf', 'values' => [
'visible' => 'Sichtbar', 'hidden' => 'Abschneiden', 'auto' => 'Scrollen bei Bedarf',
]],
// ---- Rahmen -----------------------------------------------------------------
'borderWidth' => ['group' => 'border', 'type' => 'length', 'css' => 'border-width', 'label' => 'Rahmenstärke', 'sides' => true, 'sideCss' => 'border-%s-width'],
'borderStyle' => ['group' => 'border', 'type' => 'choice', 'css' => 'border-style', 'label' => 'Rahmenart', 'values' => [
'solid' => 'Durchgezogen', 'dashed' => 'Gestrichelt', 'dotted' => 'Gepunktet', 'double' => 'Doppelt', 'none' => 'Keiner',
]],
'borderColor' => ['group' => 'border', 'type' => 'color', 'css' => 'border-color', 'label' => 'Rahmenfarbe', 'hover' => true],
'borderRadius' => ['group' => 'border', 'type' => 'length', 'css' => 'border-radius', 'label' => 'Ecken', 'corners' => true],
'outlineWidth' => ['group' => 'border', 'type' => 'length', 'css' => 'outline-width', 'label' => 'Zusatzrahmen Stärke'],
'outlineColor' => ['group' => 'border', 'type' => 'color', 'css' => 'outline-color', 'label' => 'Zusatzrahmen Farbe', 'hover' => true],
'outlineOffset' => ['group' => 'border', 'type' => 'length', 'css' => 'outline-offset', 'label' => 'Zusatzrahmen Abstand'],
'outlineStyle' => ['group' => 'border', 'type' => 'choice', 'css' => 'outline-style', 'label' => 'Zusatzrahmen Art', 'values' => [
'solid' => 'Durchgezogen', 'dashed' => 'Gestrichelt', 'dotted' => 'Gepunktet', 'double' => 'Doppelt', 'none' => 'Keiner',
]],
// ---- Bild: wirkt auf die Bilder im Block, nicht auf den Block ----------------
'objectFit' => ['group' => 'image', 'type' => 'choice', 'css' => 'object-fit', 'label' => 'Bildausschnitt', 'target' => 'img', 'values' => [
'cover' => 'Füllen und beschneiden', 'contain' => 'Vollständig zeigen', 'fill' => 'Verzerren', 'none' => 'Originalgröße', 'scale-down' => 'Nur verkleinern',
]],
'objectPosition' => ['group' => 'image', 'type' => 'choice', 'css' => 'object-position', 'label' => 'Bildmitte', 'target' => 'img', 'values' => [
'center' => 'Mitte', 'top' => 'Oben', 'bottom' => 'Unten', 'left' => 'Links', 'right' => 'Rechts',
'top left' => 'Oben links', 'top right' => 'Oben rechts', 'bottom left' => 'Unten links', 'bottom right' => 'Unten rechts',
]],
'imageRatio' => ['group' => 'image', 'type' => 'choice', 'css' => 'aspect-ratio', 'label' => 'Seitenverhältnis', 'target' => 'img', 'values' => [
'auto' => 'Wie das Bild', '1/1' => 'Quadrat 1:1', '4/3' => 'Klassisch 4:3', '3/2' => 'Foto 3:2', '16/9' => 'Breitbild 16:9',
'21/9' => 'Panorama 21:9', '3/4' => 'Hochkant 3:4', '2/3' => 'Hochkant 2:3', '9/16' => 'Hochkant 9:16',
]],
'imageHeight' => ['group' => 'image', 'type' => 'length-auto', 'css' => 'height', 'label' => 'Bildhöhe', 'target' => 'img'],
'imageRadius' => ['group' => 'image', 'type' => 'length', 'css' => 'border-radius', 'label' => 'Bildecken', 'target' => 'img', 'corners' => true],
'imageBorderWidth' => ['group' => 'image', 'type' => 'length', 'css' => 'border-width', 'label' => 'Bildrahmen Stärke', 'target' => 'img', 'sides' => true, 'sideCss' => 'border-%s-width'],
'imageBorderColor' => ['group' => 'image', 'type' => 'color', 'css' => 'border-color', 'label' => 'Bildrahmen Farbe', 'target' => 'img'],
'imageBorderStyle' => ['group' => 'image', 'type' => 'choice', 'css' => 'border-style', 'label' => 'Bildrahmen Art', 'target' => 'img', 'values' => [
'solid' => 'Durchgezogen', 'dashed' => 'Gestrichelt', 'dotted' => 'Gepunktet', 'double' => 'Doppelt', 'none' => 'Keiner',
]],
// ---- Layout -----------------------------------------------------------------
'display' => ['group' => 'layout', 'type' => 'choice', 'css' => 'display', 'label' => 'Anzeige', 'values' => [
'block' => 'Block', 'flex' => 'Flex', 'inline-flex' => 'Inline-Flex', 'grid' => 'Raster', 'inline-block' => 'Inline-Block', 'none' => 'Ausgeblendet',
]],
'flexDirection' => ['group' => 'layout', 'type' => 'choice', 'css' => 'flex-direction', 'label' => 'Flexrichtung', 'values' => [
'row' => 'Nebeneinander', 'row-reverse' => 'Nebeneinander, umgekehrt', 'column' => 'Untereinander', 'column-reverse' => 'Untereinander, umgekehrt',
]],
'flexWrap' => ['group' => 'layout', 'type' => 'choice', 'css' => 'flex-wrap', 'label' => 'Umbruch', 'values' => ['wrap' => 'Umbrechen', 'nowrap' => 'Nicht umbrechen']],
'justifyContent' => ['group' => 'layout', 'type' => 'choice', 'css' => 'justify-content', 'label' => 'Verteilung', 'values' => [
'flex-start' => 'Anfang', 'center' => 'Mitte', 'flex-end' => 'Ende', 'space-between' => 'Außen verteilt', 'space-around' => 'Gleichmäßig', 'space-evenly' => 'Exakt gleich',
]],
'alignItems' => ['group' => 'layout', 'type' => 'choice', 'css' => 'align-items', 'label' => 'Ausrichtung quer', 'values' => [
'stretch' => 'Dehnen', 'flex-start' => 'Anfang', 'center' => 'Mitte', 'flex-end' => 'Ende', 'baseline' => 'Grundlinie',
]],
'gridColumns' => ['group' => 'layout', 'type' => 'number', 'css' => null, 'label' => 'Spalten', 'min' => 1, 'max' => 12, 'step' => 1],
'gap' => ['group' => 'layout', 'type' => 'length', 'css' => 'gap', 'label' => 'Abstand zwischen Elementen'],
'order' => ['group' => 'layout', 'type' => 'number', 'css' => 'order', 'label' => 'Reihenfolge', 'min' => -20, 'max' => 20, 'step' => 1],
'rowGap' => ['group' => 'layout', 'type' => 'length', 'css' => 'row-gap', 'label' => 'Abstand zwischen Zeilen'],
'columnGap' => ['group' => 'layout', 'type' => 'length', 'css' => 'column-gap', 'label' => 'Abstand zwischen Spalten'],
'alignSelf' => ['group' => 'layout', 'type' => 'choice', 'css' => 'align-self', 'label' => 'Eigene Ausrichtung quer', 'values' => [
'auto' => 'Wie die Gruppe', 'flex-start' => 'Anfang', 'center' => 'Mitte', 'flex-end' => 'Ende', 'stretch' => 'Strecken', 'baseline' => 'Grundlinie',
]],
'flexGrow' => ['group' => 'layout', 'type' => 'number', 'css' => 'flex-grow', 'label' => 'Wachsen', 'min' => 0, 'max' => 12, 'step' => 1],
'flexShrink' => ['group' => 'layout', 'type' => 'number', 'css' => 'flex-shrink', 'label' => 'Schrumpfen', 'min' => 0, 'max' => 12, 'step' => 1],
'flexBasis' => ['group' => 'layout', 'type' => 'length-auto', 'css' => 'flex-basis', 'label' => 'Grundbreite'],
'position' => ['group' => 'layout', 'type' => 'choice', 'css' => 'position', 'label' => 'Positionierung', 'values' => [
'static' => 'Im Fluss', 'relative' => 'Relativ verschoben', 'absolute' => 'Freigestellt', 'sticky' => 'Klebt beim Scrollen',
]],
'top' => ['group' => 'layout', 'type' => 'length-auto', 'css' => 'top', 'label' => 'Abstand von oben'],
'right' => ['group' => 'layout', 'type' => 'length-auto', 'css' => 'right', 'label' => 'Abstand von rechts'],
'bottom' => ['group' => 'layout', 'type' => 'length-auto', 'css' => 'bottom', 'label' => 'Abstand von unten'],
'left' => ['group' => 'layout', 'type' => 'length-auto', 'css' => 'left', 'label' => 'Abstand von links'],
'zIndex' => ['group' => 'layout', 'type' => 'number', 'css' => 'z-index', 'label' => 'Ebene (vorn/hinten)', 'min' => -10, 'max' => 100, 'step' => 1],
// ---- Wirkung ----------------------------------------------------------------
'opacity' => ['group' => 'effect', 'type' => 'number', 'css' => 'opacity', 'label' => 'Deckkraft', 'min' => 0, 'max' => 1, 'step' => 0.05, 'hover' => true],
'shadowX' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Schatten waagerecht'],
'shadowY' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Schatten senkrecht'],
'shadowBlur' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Schatten weich'],
'shadowSpread' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Schatten größer'],
'shadowColor' => ['group' => 'effect', 'type' => 'color', 'css' => null, 'label' => 'Schattenfarbe', 'hover' => true],
'translateX' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Verschieben waagerecht', 'hover' => true],
'translateY' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Verschieben senkrecht', 'hover' => true],
'scale' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Skalierung', 'min' => 0.1, 'max' => 3, 'step' => 0.01, 'hover' => true],
'rotate' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Drehung', 'min' => -360, 'max' => 360, 'step' => 1, 'hover' => true],
'shadowInset' => ['group' => 'effect', 'type' => 'choice', 'css' => null, 'label' => 'Schatten innen', 'values' => ['aussen' => 'Außen', 'innen' => 'Innen']],
'textShadowX' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Textschatten waagerecht'],
'textShadowY' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Textschatten senkrecht'],
'textShadowBlur' => ['group' => 'effect', 'type' => 'length', 'css' => null, 'label' => 'Textschatten weich'],
'textShadowColor' => ['group' => 'effect', 'type' => 'color', 'css' => null, 'label' => 'Textschattenfarbe'],
'blur' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Weichzeichnen (px)', 'min' => 0, 'max' => 40, 'step' => 0.5, 'hover' => true],
'brightness' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Helligkeit', 'min' => 0, 'max' => 3, 'step' => 0.05, 'hover' => true],
'contrast' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Kontrast', 'min' => 0, 'max' => 3, 'step' => 0.05, 'hover' => true],
'saturate' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Farbsättigung', 'min' => 0, 'max' => 3, 'step' => 0.05, 'hover' => true],
'grayscale' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Graustufen', 'min' => 0, 'max' => 1, 'step' => 0.05, 'hover' => true],
'sepia' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Sepia', 'min' => 0, 'max' => 1, 'step' => 0.05, 'hover' => true],
'hueRotate' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Farbdrehung (Grad)', 'min' => -180, 'max' => 180, 'step' => 1, 'hover' => true],
'invert' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Umkehren', 'min' => 0, 'max' => 1, 'step' => 0.05],
'mixBlendMode' => ['group' => 'effect', 'type' => 'choice', 'css' => 'mix-blend-mode', 'label' => 'Mischmodus', 'values' => [
'normal' => 'Normal', 'multiply' => 'Multiplizieren', 'screen' => 'Negativ multiplizieren', 'overlay' => 'Überlagern',
'darken' => 'Abdunkeln', 'lighten' => 'Aufhellen', 'color-dodge' => 'Farbig abwedeln', 'difference' => 'Differenz',
'exclusion' => 'Ausschluss', 'hue' => 'Farbton', 'saturation' => 'Sättigung', 'color' => 'Farbe', 'luminosity' => 'Leuchtkraft',
]],
'cursor' => ['group' => 'effect', 'type' => 'choice', 'css' => 'cursor', 'label' => 'Mauszeiger', 'values' => [
'auto' => 'Automatisch', 'pointer' => 'Hand', 'default' => 'Pfeil', 'not-allowed' => 'Gesperrt', 'grab' => 'Greifen', 'zoom-in' => 'Lupe',
]],
'transition' => ['group' => 'effect', 'type' => 'number', 'css' => null, 'label' => 'Übergangsdauer (ms)', 'min' => 0, 'max' => 2000, 'step' => 25],
];
}
/** Properties that are written onto the images inside a block instead of the block itself. */
public static function imageTargets(): array
{
return array_keys(array_filter(self::properties(), static fn (array $p): bool => 'img' === ($p['target'] ?? null)));
}
/** Properties that may also be given a hover value. */
public static function hoverable(): array
{
return array_keys(array_filter(self::properties(), static fn (array $p): bool => $p['hover'] ?? false));
}
public static function sides(): array
{
return ['top' => 'Oben', 'right' => 'Rechts', 'bottom' => 'Unten', 'left' => 'Links'];
}
/** Corners round a box; they are not sides, and their CSS property is spelled differently. */
public static function corners(): array
{
return ['topLeft' => 'Oben links', 'topRight' => 'Oben rechts', 'bottomRight' => 'Unten rechts', 'bottomLeft' => 'Unten links'];
}
/** "topLeft" becomes the CSS fragment "top-left". */
public static function cornerCss(string $corner): string
{
return strtolower(preg_replace('/([A-Z])/', '-$1', $corner) ?? $corner);
}
}