Let a button carry a chosen icon before or after its text, and pick icons from tiles

Schema v25: a button takes an optional icon (any name from the shared set or
"none") and an icon position (start or end). Without a choice it keeps the
diagonal arrow behind the text, still with the button-arrow class, so stored
pages and their own CSS look exactly as before.

Thirteen everyday icons join the set (arrow down, chevron, calendar, message,
send, download, document, play, search, heart, info, plus, person). Button,
icon and icon card choose from a tile picker with a drawing per icon, grouped,
keyboard navigable, instead of a select with English keys. icons.js holds the
paths for the editor; a test keeps them equal to the Twig drawings and the
validator list. ADR 108.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-07 09:39:34 +02:00
1 parent e1753fa7e4
commit 88737a95a9
17 files changed
+463 -15

No files matched your search

+12 -2
View File
@@ -16,7 +16,9 @@ final class DocumentValidator
/** Blocks introduced with schema v6 for the sections a marketing page actually needs. */
public const V6_TYPES = ['iconcard', 'stat', 'quote', 'logos', 'cta', 'contact', 'divider', 'spacer', 'accordion', 'accordionitem'];
/** Icons ship as inline SVG from this fixed list; no icon font and no external request. */
public const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check', 'people', 'music', 'home', 'arrow-right', 'arrow-left', 'arrow-up-right'];
public const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check', 'people', 'music', 'home', 'arrow-right', 'arrow-left', 'arrow-up-right', 'arrow-down', 'chevron-right', 'calendar', 'chat', 'send', 'download', 'document', 'play', 'search', 'heart', 'info', 'plus', 'user'];
/** Das Symbol eines Buttons, wenn keins gewählt ist: der Pfeil, den jeder Button vor v25 hatte. */
public const BUTTON_DEFAULT_ICON = 'arrow-up-right';
public const SPACER_SIZES = ['small', 'medium', 'large', 'huge'];
/** Schema v7: tabs, the one pattern that needs a script to become a real widget. */
public const V7_TYPES = ['tabs', 'tabitem'];
@@ -40,7 +42,7 @@ final class DocumentValidator
public const V23_TYPES = ['social'];
public const V24_TYPES = ['html'];
/** Die aktuelle Fassung des Dokumentschemas - dieselbe Zahl wie SCHEMA_VERSION in editor-model.js. */
public const SCHEMA_VERSION = 24;
public const SCHEMA_VERSION = 25;
/**
* @param bool $requireHeading A page needs exactly one main heading. A header, a footer or a
@@ -237,6 +239,14 @@ final class DocumentValidator
}
if ('button' === $type) {
$this->link($props['href'] ?? null);
// Schema v25: das Symbol am Button ist wählbar. Fehlt es, bleibt es beim Pfeil wie
// bisher - ältere Dokumente sehen also unverändert aus. "none" heißt ohne Symbol.
if (array_key_exists('icon', $props)) {
$this->require($schemaVersion >= 25 && ('none' === $props['icon'] || in_array($props['icon'], self::ICONS, true)), 'Unbekanntes Symbol am Button oder Dokumentschema vor v25.');
}
if (array_key_exists('iconPosition', $props)) {
$this->require($schemaVersion >= 25 && in_array($props['iconPosition'], ['start', 'end'], true), 'Das Symbol steht vor oder nach dem Text und braucht Schema v25.');
}
}
if ('iconcard' === $type) {
$this->require(in_array($props['icon'] ?? null, self::ICONS, true), 'Unknown icon.');