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,51 @@
{{-- An M3 plain tooltip: a short label for a control, in the inverse surface.
Two ways in. Inside a component that already names its own anchor `<x-button tooltip="…">`
passes `anchor` it is a bare popover. Standalone, it wraps its trigger:
<x-tooltip text="Copy link"><button ></button></x-tooltip>
It shows after a short hover on a pointer that can hover, at once on keyboard focus, and
hides on leave, blur, press and Escape. A phone has no hover, so a control there carries its
words. The bubble is a `popover="manual"` in the top layer never clipped by an
`overflow-hidden` parent, never widening a scroll container placed by CSS anchor
positioning on `side` (`top`, `bottom`, `left`, `right`), flipping when there is no room.
It is aria-hidden: an icon button takes the same words as its aria-label, and a labelled
control would otherwise read them twice. --}}
@props([
'text',
'side' => 'top',
'anchor' => null,
])
@php
$side = in_array($side, ['top', 'bottom', 'left', 'right'], true) ? $side : 'top';
$standalone = $anchor === null;
$anchor ??= '--material-tooltip-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
@endphp
@if ($standalone)
<span class="inline-flex" style="anchor-name: {{ $anchor }}">
{{ $slot }}
@endif
<span
popover="manual"
aria-hidden="true"
x-data="materialTooltip"
style="position-anchor: {{ $anchor }}"
@class([
'pointer-events-none m-0 max-w-50 overflow-visible border-0 rounded-corner-xs bg-inverse-surface px-2 py-1 text-center whitespace-normal type-body-sm text-inverse-on-surface [inset:auto]',
'opacity-0 transition-[opacity,display,overlay] transition-discrete duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast open:opacity-100 starting:open:opacity-0',
'my-1 [position-area:top] [position-try-fallbacks:flip-block]' => $side === 'top',
'my-1 [position-area:bottom] [position-try-fallbacks:flip-block]' => $side === 'bottom',
'mx-1 [position-area:left] [position-try-fallbacks:flip-inline]' => $side === 'left',
'mx-1 [position-area:right] [position-try-fallbacks:flip-inline]' => $side === 'right',
])
>{{ $text }}</span>
@if ($standalone)
</span>
@endif