<x-group hint-class> adds classes to the hint, as the text fields' hint-class does, and a validation message still replaces the hint. <x-menu-item icon-class> adds classes to the leading icon, for an icon whose colour means something of its own, such as a sport's glyph. A colour class there has to win over the component's own colour, and which of two colour utilities wins depends on the order Tailwind emits them (text-error comes before text-on-surface-variant). So when either prop is given, the component's own colour is written with a :where() variant that carries no specificity, as the fields' hint colour sits in the components layer. A disabled item's icon stays disabled. Without the props the markup is unchanged. The skill now also lists the fields' hint-class, which it had left out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
110 lines
5.3 KiB
PHP
110 lines
5.3 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`, `hint-class`, `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.
|
|
|
|
`hint-class` adds classes to the hint, as on `<x-field>`: a colour there paints it
|
|
(`hint-class="text-warning"` for a hint that warns). The hint's own colour then carries no
|
|
specificity, as the field's does in the components layer, because which of two colour
|
|
utilities wins depends on the order Tailwind emits them. --}}
|
|
|
|
@props([
|
|
'label' => null,
|
|
'hint' => null,
|
|
'hintClass' => 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;
|
|
// A plain form's field is named, not bound: its errors are under its name (`files[]` → `files`, `a[b]` → `a.b`).
|
|
$errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null);
|
|
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
|
$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];
|
|
|
|
$hintClasses = filled($hintClass)
|
|
? \Illuminate\Support\Arr::toCssClasses(['mt-1 type-body-sm [:where(&)]:text-on-surface-variant', $hintClass])
|
|
: 'mt-1 type-body-sm text-on-surface-variant';
|
|
|
|
$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-livewire-material::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="{{ $hintClasses }}">{{ $hint }}</p>
|
|
@endif
|
|
</fieldset>
|