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
98 lines
4.5 KiB
PHP
98 lines
4.5 KiB
PHP
{{-- A choice of a few options as an M3 Expressive connected button group — the successor of the
|
|
segmented button.
|
|
|
|
<x-group label="Theme" wire:model.live="theme" :options="[
|
|
['id' => 'light', 'name' => 'Light', 'icon' => 'light_mode'],
|
|
['id' => 'dark', 'name' => 'Dark', 'icon' => 'dark_mode'],
|
|
]" />
|
|
|
|
Native radios under the segments (checkboxes with `multiple`), so `wire:model` and `x-model`
|
|
bind as on any input, the arrow keys move the choice, and a screen reader announces a group.
|
|
The chosen segment rounds fully and takes the selected colour; `variant` is `tonal` (the
|
|
default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless
|
|
`inline`. An option with `'disabled' => true` greys its own segment.
|
|
|
|
ReStride's props, kept: `label`, `hint`, `name` (needed with `x-model`, which names no
|
|
property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`, `variant`,
|
|
`multiple`, `inline`. A validation message for the bound property replaces the hint. --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'hint' => null,
|
|
'name' => null,
|
|
'options' => [],
|
|
'optionValue' => 'id',
|
|
'optionLabel' => 'name',
|
|
'optionIcon' => 'icon',
|
|
'size' => 'sm',
|
|
'variant' => 'tonal',
|
|
'multiple' => false,
|
|
'inline' => false,
|
|
])
|
|
|
|
@php
|
|
$model = $attributes->whereStartsWith('wire:model')->first();
|
|
$name ??= $model;
|
|
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
|
|
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
|
|
|
$segment = [
|
|
'xs' => 'h-8 gap-2 px-3 type-label-lg',
|
|
'sm' => 'h-10 gap-2 px-4 type-label-lg',
|
|
'md' => 'h-14 gap-2 px-6 type-title-md',
|
|
'lg' => 'h-24 gap-3 px-12 type-headline-sm',
|
|
'xl' => 'h-34 gap-4 px-16 type-headline-lg',
|
|
][$size];
|
|
|
|
$iconSize = ['xs' => 'size-5', 'sm' => 'size-5', 'md' => 'size-6', 'lg' => 'size-8', 'xl' => 'size-10'][$size];
|
|
|
|
$colours = match ($variant) {
|
|
'filled' => 'bg-surface-container text-on-surface-variant has-checked:bg-primary has-checked:text-on-primary',
|
|
'outlined' => 'border border-outline-variant text-on-surface-variant has-checked:border-transparent has-checked:bg-inverse-surface has-checked:text-inverse-on-surface',
|
|
default => 'bg-secondary-container text-on-secondary-container has-checked:bg-secondary has-checked:text-on-secondary',
|
|
};
|
|
@endphp
|
|
|
|
<fieldset {{ $attributes->only(['class', 'wire:key'])->class('min-w-0') }}>
|
|
@if (filled($label))
|
|
<legend class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</legend>
|
|
@endif
|
|
|
|
<div data-button-group="connected" data-size="{{ $size }}" @class(['flex gap-0.5', 'w-full' => ! $inline, 'w-fit' => $inline])>
|
|
@foreach ($options as $option)
|
|
<label @class([
|
|
'state-layer relative flex cursor-pointer select-none items-center justify-center whitespace-nowrap',
|
|
'transition-[border-radius,background-color,color] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
|
|
'min-w-0 flex-1' => ! $inline,
|
|
$segment,
|
|
$colours,
|
|
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary',
|
|
'has-disabled:cursor-not-allowed has-disabled:bg-on-surface/10 has-disabled:text-on-surface/38',
|
|
])>
|
|
<input
|
|
{{ $attributes->whereStartsWith(['wire:model', 'x-model']) }}
|
|
type="{{ $multiple ? 'checkbox' : 'radio' }}"
|
|
name="{{ $multiple ? $name.'[]' : $name }}"
|
|
value="{{ data_get($option, $optionValue) }}"
|
|
@disabled(data_get($option, 'disabled'))
|
|
class="peer sr-only"
|
|
/>
|
|
|
|
@if (filled(data_get($option, $optionIcon)))
|
|
<x-icon :name="data_get($option, $optionIcon)" :class="$iconSize" />
|
|
@endif
|
|
|
|
<span class="truncate">{{ data_get($option, $optionLabel) }}</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($messages !== [])
|
|
@foreach ($messages as $message)
|
|
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
|
|
@endforeach
|
|
@elseif (filled($hint))
|
|
<p class="mt-1 type-body-sm text-on-surface-variant">{{ $hint }}</p>
|
|
@endif
|
|
</fieldset>
|