Offer 98 icons in a dialog and style a button icon under Design

The icon set grows from 33 to 98 drawings in seven categories (arrows,
contact, commerce, files and media, people and security, house and trade,
general), including truck, cart, euro, camera, tools and solar. icons.js is
the single source; npm run build:icons writes the Twig drawings and the
validator list, and a test keeps them equal.

Choosing an icon no longer fills the panel with tiles: the panel shows the
current icon with "Change", which opens a modal with category tabs and named
tiles, keyboard navigable, closing with Escape. Button, icon and icon card
share it.

A new style group "Symbol" targets the icon inside a button per device and
on hover: colour, background, size, padding, radius, rotation, plus the gap
to the text. Schema v26 adds hover motion (slide, reveal) and an icon-only
button whose label stays as the accessible name and keeps the line height.
ADR 109.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-07 10:01:18 +02:00
1 parent 6af2e2aead
commit 08998de533
20 files changed
+658 -228

No files matched your search

+57
View File
@@ -315,6 +315,63 @@ final class CmsTest extends TestCase
yield 'Symbol ist kein Text' => ['icon', ['phone'], 25];
yield 'Stellung vor v25' => ['iconPosition', 'start', 24];
yield 'unbekannte Stellung' => ['iconPosition', 'middle', 25];
yield 'Bewegung vor v26' => ['iconMotion', 'slide', 25];
yield 'unbekannte Bewegung' => ['iconMotion', 'spin', 26];
yield 'Nur Symbol vor v26' => ['iconOnly', true, 25];
yield 'Nur Symbol ist kein Schalter' => ['iconOnly', 'ja', 26];
}
public function testAButtonWithoutAnIconCannotBeIconOnly(): void
{
$document = DemoContent::pages()['home'];
$document['schemaVersion'] = 26;
$document['blocks'][0]['children'][3]['props'] += ['icon' => 'none', 'iconOnly' => true];
$this->expectException(InvalidDocument::class);
(new DocumentValidator())->validate($document);
}
/**
* Schema v26 (ADR 109): Bewegung beim Überfahren und ein Button nur aus Symbol. Der Text
* bleibt als unsichtbare Beschriftung der Name des Links.
*/
public function testAButtonIconMovesOnHoverOrStandsAlone(): void
{
$document = DemoContent::pages()['home'];
$button = $document['blocks'][0]['children'][3];
$document['schemaVersion'] = 26;
$document['blocks'][0]['children'][3]['props'] += ['icon' => 'phone', 'iconMotion' => 'slide', 'iconOnly' => true];
$id = $this->pages->saveDraft('page-1', $document, 0);
$this->pages->publish('page-1', $id, 1);
$html = (string) $this->request('/')->getContent();
self::assertMatchesRegularExpression('/<a id="'.$button['id'].'" class="button cms-button[^"]* icon-motion-slide icon-only"[^>]*><span class="button-label">'.preg_quote($button['props']['text'], '/').'<\/span> <span class="button-icon"/', $html);
$document['blocks'][0]['children'][3]['props'] = ['text' => $button['props']['text'], 'href' => $button['props']['href'], 'icon' => 'download', 'iconPosition' => 'start', 'iconMotion' => 'reveal'];
$id = $this->pages->saveDraft('page-1', $document, 2);
$this->pages->publish('page-1', $id, 3);
$html = (string) $this->request('/')->getContent();
self::assertMatchesRegularExpression('/<a id="'.$button['id'].'" class="button cms-button[^"]* icon-start icon-motion-reveal"/', $html);
$css = (string) file_get_contents(__DIR__.'/../public/assets/site.css');
self::assertStringContainsString('.cms-button.icon-motion-reveal:is(:hover,:focus-visible) .button-icon{max-width:4em;opacity:1', $css);
self::assertStringContainsString('.cms-button .button-icon>svg{width:var(--icon-size,1.15em)', $css);
}
/**
* Farbe, Fläche, Größe und Drehung des Symbols liegen am Button und treffen seine
* Symbolhülle - je Gerät und beim Überfahren, auch bei Tastaturfokus.
*/
public function testButtonIconStylesTargetTheIconPerDeviceAndOnHover(): void
{
$document = ['schemaVersion' => 26, 'title' => 'T', 'description' => '', 'blocks' => [['id' => 'b1', 'type' => 'button', 'props' => ['text' => 'Anrufen', 'href' => '/', 'icon' => 'phone'], 'style' => [
'desktop' => ['iconColor' => '@accent', 'iconBackgroundColor' => '#ffffff', 'iconSize' => '20px', 'iconPadding' => '8px', 'iconRadius' => '999px', 'iconGap' => '12px', 'hover' => ['iconColor' => '#000000', 'iconRotate' => '45deg']],
'mobile' => ['iconSize' => '16px'],
]]]];
$css = (new StyleCompiler())->compileDocument($document);
self::assertStringContainsString('#b1 .button-icon{color:var(--k-accent);background-color:#ffffff;--icon-size:20px;padding:8px;border-radius:999px}', $css);
self::assertStringContainsString('#b1:hover .button-icon,#b1:focus-visible .button-icon{color:#000000;rotate:45deg}', $css);
self::assertStringContainsString('--icon-gap:12px', $css);
self::assertMatchesRegularExpression('/@media \(max-width:\d+px\)\{#b1 \.button-icon\{--icon-size:16px\}\}/', $css);
// Die Werte gelten nur der Symbolhülle, nicht dem Button selbst.
self::assertStringNotContainsString('#b1{color:var(--k-accent)', $css);
}
#[DataProvider('rejectedButtonIcons')]