Add M3 text fields, selects and selection controls
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m4s
tests / lint (push) Successful in 59s
tests / browser (chrome, chromium) (push) Successful in 2m54s
tests / browser (firefox, firefox) (push) Successful in 3m3s
tests / browser (safari, webkit) (push) Successful in 4m13s

Outlined and filled text fields (input, password, auto-growing textarea,
native select with the customizable select menu, file), checkbox with an
indeterminate state, radio buttons, the M3 switch and form, ported from
ReStride and extended. The first half of Phase 6.

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 07:53:14 +02:00
co-authored by Claude Opus 5
parent ad724662ca
commit 7f92f1cb29
24 changed files with 2296 additions and 1 deletions
@@ -0,0 +1,79 @@
{{-- A text field: M3's outlined or filled one (resources/views/components/field.blade.php).
`label`, `hint`, leading `icon`, trailing `icon-right`, `prefix` and `suffix` (text beside the
value, such as a currency or a unit), `variant`, `size` for unlabelled controls in toolbars,
and `mono` for a field that holds code. `clearable` adds a trailing button that empties the
field once it holds something; `copyable` one that copies its value and says so in a snackbar
(a share link, a token). Every other attribute reaches the `<input>`: `type`, `min`, `step`,
`readonly`, `wire:model` and the rest. Errors are read from the bag under the `wire:model` name.
The placeholder is a single space when none is given, because the label can only tell an
empty field from a filled one through `:placeholder-shown`. With a label, a real placeholder
shows only while the field has focus — until then the label stands where it would be. --}}
@props([
'label' => null,
'hint' => null,
'hintClass' => null,
'icon' => null,
'iconRight' => null,
'prefix' => null,
'suffix' => null,
'clearable' => false,
'copyable' => false,
'size' => 'md',
'variant' => null,
'mono' => false,
])
@php
$model = $attributes->whereStartsWith('wire:model')->first();
$placeholder = filled($attributes->get('placeholder')) ? $attributes->get('placeholder') : ' ';
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|'.$placeholder), 0, 12);
$messages = $model !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($model)) : [];
@endphp
<x-field :$id :$label :$hint :hint-class="$hintClass" :$messages :$icon :$prefix :$suffix :$size :$variant :$mono :class="$attributes->get('class')" :data-readonly="$attributes->get('readonly') ? '' : null">
<input
{{ $attributes->except(['class', 'id', 'placeholder'])->merge(['type' => 'text']) }}
id="{{ $id }}"
placeholder="{{ $placeholder }}"
@if ($messages !== []) aria-invalid="true" @endif
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
class="field-control"
/>
@if ($clearable || $copyable || $iconRight)
<x-slot:trailing>
@if ($clearable)
<button
type="button"
x-data
x-on:click="const control = $el.closest('.field').querySelector('.field-control'); control.value = ''; control.dispatchEvent(new Event('input', { bubbles: true })); control.dispatchEvent(new Event('change', { bubbles: true })); control.focus();"
class="field-trailing field-clear field-button"
aria-label="{{ __('Clear') }}"
data-field-clear
>
<x-icon name="close" class="size-(--field-icon)" />
</button>
@endif
@if ($copyable)
<button
type="button"
x-data
x-on:click="navigator.clipboard.writeText($el.closest('.field').querySelector('.field-control').value).then(() => window.materialToast(@js(__('Copied to the clipboard')), { type: 'success' }))"
class="field-trailing field-button"
aria-label="{{ __('Copy') }}"
data-field-copy
>
<x-icon name="content_copy" class="size-(--field-icon)" />
</button>
@endif
@if ($iconRight)
<x-icon :name="$iconRight" class="field-trailing size-(--field-icon)" />
@endif
</x-slot:trailing>
@endif
</x-field>