Draw the checkbox and the selection controls' row without Tailwind

<x-checkbox> renders data-md-checkbox, its box, tick and dash, and its
row, text and errors as data-md-* attributes; the 18px tick takes a size
prop and a label-less box md-touch-target (plan step 36). selection.css
moves into material.components as the row, text and state layer the
three controls share, and each control's drawing goes to its own file:
checkbox.css, radio.css, toggle.css, on the state tokens. The radio and
the switch take the renamed shared hooks now and lose their layout
classes in their own commits. field.js follows data-md-indeterminate.

Renaming the switch's data-handle also stops selection.css matching the
slider's handles, which it drew 24px above the track.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 14:34:00 +02:00
co-authored by Claude Opus 5
parent 3083fe28b2
commit f86afc0542
12 changed files with 510 additions and 365 deletions
+18 -25
View File
@@ -4,19 +4,20 @@
The label and its hint sit beside it and are its accessible name the whole row is the
`<label>`, so either can be pressed. `right` puts the box at the end of the row, for a setting
read left to right. `indeterminate` shows the mixed state of a "select all" whose items are
partly ticked: HTML has no attribute for it, so the input is marked `data-indeterminate` and
partly ticked: HTML has no attribute for it, so the input is marked `data-md-indeterminate` and
resources/js/field.js keeps the property in step, on load and after every morph. Errors are
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`
(resources/css/components/selection.css).
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`.
The drawing is resources/css/components/checkbox.css, on the row the three selection controls
share (resources/css/components/selection.css).
Without a label a "select all" in a table header, a row's tick — the row is only the 18px box,
so the box carries `touch-target` and catches presses over M3's 48px minimum.
so the box carries `md-touch-target` and catches presses over M3's 48px minimum.
Grouping is the caller's, not this component's: M3 asks that from `expanded` (840px) a set of
related checkboxes be gathered into a contained region rather than left as one long column
(docs/reference/m3/components-navigation-selection-inputs.md § Checkbox, Behaviour). Lay the
group out yourself `<div class="grid gap-4 expanded:grid-cols-2">` around the checkboxes, or a
`<x-card>` or side sheet holding them and give the group a heading that names what it asks. --}}
group out yourself a grid of one column that takes two from `expanded`, or an `<x-card>` or
side sheet holding them and give the group a heading that names what it asks. --}}
@props([
'label' => null,
@@ -33,42 +34,34 @@
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
@endphp
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
<label for="{{ $id }}" data-selection @if ($messages !== []) data-invalid @endif @class([
'flex gap-4',
'items-start' => filled($hint),
'items-center' => blank($hint),
'flex-row-reverse justify-between' => $right,
])>
<span data-checkbox @class([
'mt-[3px]' => filled($hint),
'touch-target' => blank($label) && blank($hint),
])>
<div data-md-checkbox {{ $attributes->only(['class', 'style', 'wire:key']) }}>
<label for="{{ $id }}" data-md-selection-row @if ($right) data-md-right @endif @if ($messages !== []) data-md-invalid @endif>
<span data-md-checkbox-box @if (blank($label) && blank($hint)) class="md-touch-target" @endif>
<input
{{ $attributes->except(['class', 'id', 'wire:key']) }}
{{ $attributes->except(['class', 'style', 'id', 'wire:key']) }}
id="{{ $id }}"
type="checkbox"
@if ($indeterminate) data-indeterminate @endif
@if ($indeterminate) data-md-indeterminate @endif
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
/>
<x-livewire-material::icon name="check" class="size-4.5" optical="20" data-check />
<x-livewire-material::icon name="remove" class="size-4.5" optical="20" data-mixed />
<x-livewire-material::icon name="check" size="18" data-md-checkbox-check />
<x-livewire-material::icon name="remove" size="18" data-md-checkbox-mixed />
</span>
@if (filled($label) || filled($hint))
<span class="min-w-0">
<span data-md-selection-text>
@if (filled($label))
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
<span data-md-selection-label>{{ $label }}</span>
@endif
@if (filled($hint))
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
<span data-md-selection-hint>{{ $hint }}</span>
@endif
</span>
@endif
</label>
@if ($messages !== [])
<div id="{{ $id }}-support" class="mt-1 type-body-sm text-error" role="alert">
<div id="{{ $id }}-support" data-md-selection-support role="alert">
@foreach ($messages as $message)
<p>{{ $message }}</p>
@endforeach