Add buttons, menus and the rest of M3 Expressive's actions
tests / browser (firefox, firefox) (push) Successful in 1m54s
tests / browser (safari, webkit) (push) Successful in 2m17s
tests / lint (push) Successful in 59s
tests / feature (8.4) (push) Successful in 1m7s
tests / feature (8.5) (push) Successful in 1m0s
tests / browser (chrome, chromium) (push) Successful in 1m49s

<x-button> (label buttons, icon buttons and toggles in five sizes, with
filled, tonal, outlined, elevated and text variants in any colour role),
<x-tooltip>, <x-menu> with items, groups and separators, <x-button-group>,
<x-group> as a connected button group, <x-split-button>, <x-fab>,
<x-fab-menu> and <x-loading>. Sizes, colours and shapes come from
androidx Compose Material 3's tokens; the loading indicator ports its
Morph into SVG + SMIL.

Menus follow WAI-ARIA's menu button pattern on popovers placed by CSS
anchor positioning. Browser tests run in Chromium, Firefox and WebKit.
The showcase fetches the icon names on demand: inlined, they tripped
Pest's test server into HTTP 431s under Firefox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 06:09:28 +02:00
co-authored by Claude Opus 5
parent b48e879254
commit cd64f4f371
46 changed files with 2968 additions and 36 deletions
@@ -0,0 +1,71 @@
{{-- An M3 Expressive split button: an action, and a menu of its alternatives.
<x-split-button label="Download all" icon="download" wire:click="downloadZip" menu-label="Download options">
<x-menu-item label="Download as ZIP" wire:click="downloadZip" />
<x-menu-item label="Download files one by one" wire:click="downloadEach" />
</x-split-button>
Attributes (wire:click, spinner…) go to the leading button; the slot is the menu. `variant` is
`filled` (the default), `tonal`, `outlined` or `elevated` a text split button does not exist
in M3 with `color` and `size` as on `<x-button>`. The halves sit 2px apart; the trailing
one rounds fully and turns its chevron over while the menu is open. `menu-label` names the
trailing button and the menu for screen readers.
Padding from SplitButton*Tokens (androidx Compose Material 3, Apache-2.0): the leading button
keeps less room on its inner side at the two smallest sizes, and the trailing button is 48px
wide there. The corners are resources/css/components/groups.css. --}}
@props([
'label' => null,
'icon' => null,
'variant' => 'filled',
'color' => 'primary',
'size' => 'sm',
'disabled' => false,
'spinner' => null,
'menuLabel' => null,
'position' => 'bottom-end',
])
@php
$variant = in_array($variant, ['filled', 'tonal', 'outlined', 'elevated'], true) ? $variant : 'filled';
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
$menuLabel ??= __('More options');
$leadingPadding = ['xs' => 'pe-2.5', 'sm' => 'pe-3', 'md' => '', 'lg' => '', 'xl' => ''][$size];
$trailingWidth = ['xs' => 'w-12', 'sm' => 'w-12', 'md' => '', 'lg' => '', 'xl' => ''][$size];
@endphp
<div data-button-group="split" data-size="{{ $size }}" {{ $attributes->only('class')->class('inline-flex items-center gap-0.5') }}>
<x-button
{{ $attributes->except('class') }}
:label="$label"
:icon="$icon"
:variant="$variant"
:color="$color"
:size="$size"
:disabled="$disabled"
:spinner="$spinner"
corners=""
data-split="leading"
:class="$leadingPadding"
/>
<x-menu :position="$position" :label="$menuLabel">
<x-slot:trigger>
<x-button
icon="keyboard_arrow_down"
:aria-label="$menuLabel"
:variant="$variant"
:color="$color"
:size="$size"
:disabled="$disabled"
corners=""
data-split="trailing"
:class="$trailingWidth"
/>
</x-slot:trigger>
{{ $slot }}
</x-menu>
</div>