Files
livewire-material/resources/views/components/group.blade.php
T
Andreas Reinhold / reiniandClaude Opus 5 247c596c3a Cut duplicated and speculative code across the package
An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:29:21 +02:00

110 lines
5.0 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. At `xs` and `sm` a
segment keeps a 48px target and a 48px minimum width, which M3 asks for by name and tells
you never to reduce. Corners move on the spatial spring and colours on the effects one.
`shape` is M3's "Default shape | Round, square" configuration: `square` squares the two ends
of the group to the corner its inner edges take (M3's square table, 4/8/8/16/20dp by size),
and the chosen segment still rounds fully, so selection reads the same either way.
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`, `shape`, `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="md-ink-warning"` for a hint that warns), because a caller's class outranks the
package's layer.
Drawn by resources/css/components/group.css, with the connected shapes of
resources/css/components/button-group.css. --}}
@props([
'label' => null,
'hint' => null,
'hintClass' => null,
'name' => null,
'options' => [],
'optionValue' => 'id',
'optionLabel' => 'name',
'optionIcon' => 'icon',
'size' => 'sm',
'variant' => 'tonal',
'shape' => 'round',
'multiple' => false,
'inline' => false,
])
@php
$model = $attributes->whereStartsWith('wire:model')->first();
$name ??= $model;
$field = \NoNameWeb\LivewireMaterial\Support\Field::class;
$errorKey = $field::key($model, $attributes->get('name'));
$messages = $field::messages($errors ?? null, $errorKey);
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
$variant = in_array($variant, ['tonal', 'filled', 'outlined'], true) ? $variant : 'tonal';
// M3's "Default shape | Round, square": a square group's ends take the corner its inner edges
// take; the chosen segment still rounds fully, which is the cue selection has always carried.
$shape = $shape === 'square' ? 'square' : 'round';
// A 20px glyph comes from M3's 20px cut, which the icon component picks from the size.
$iconSize = ['xs' => 20, 'sm' => 20, 'md' => 24, 'lg' => 32, 'xl' => 40][$size];
// Below `medium` a segment draws under 48px, so it needs the foundation's touch target too.
$segmentNeedsTouchTarget = in_array($size, ['xs', 'sm'], true);
$root = $attributes->only(['class', 'style', 'wire:key'])->merge([
'data-md-group' => true,
'data-md-size' => $size,
'data-md-variant' => $variant,
'data-md-shape' => $shape,
'data-md-multiple' => $multiple ? true : null,
'data-md-inline' => $inline ? true : null,
]);
@endphp
<fieldset {{ $root }}>
@if (filled($label))
<legend data-md-group-legend>{{ $label }}</legend>
@endif
<div data-md-button-group="connected" data-md-size="{{ $size }}" data-md-shape="{{ $shape }}">
@foreach ($options as $option)
<label data-md-group-segment class="md-state-layer @if ($segmentNeedsTouchTarget) md-touch-target @endif">
<input
class="md-visually-hidden"
{{ $attributes->whereStartsWith(['wire:model', 'x-model']) }}
type="{{ $multiple ? 'checkbox' : 'radio' }}"
name="{{ $multiple ? $name.'[]' : $name }}"
value="{{ data_get($option, $optionValue) }}"
@disabled(data_get($option, 'disabled'))
/>
@if (filled(data_get($option, $optionIcon)))
<x-livewire-material::icon :name="data_get($option, $optionIcon)" :size="$iconSize" />
@endif
<span data-md-group-label>{{ data_get($option, $optionLabel) }}</span>
</label>
@endforeach
</div>
@if ($messages !== [])
@foreach ($messages as $message)
<p data-md-group-error>{{ $message }}</p>
@endforeach
@elseif (filled($hint))
<p data-md-group-hint @if (filled($hintClass)) class="{{ $hintClass }}" @endif>{{ $hint }}</p>
@endif
</fieldset>