Add chips, choices and search
tests / lint (push) Successful in 1m4s
tests / feature (8.4) (push) Successful in 1m8s
tests / feature (8.5) (push) Successful in 1m8s
tests / browser (chrome, chromium) (push) Successful in 3m6s
tests / browser (firefox, firefox) (push) Successful in 3m45s
tests / browser (safari, webkit) (push) Successful in 5m0s
tests / lint (push) Successful in 1m4s
tests / feature (8.4) (push) Successful in 1m8s
tests / feature (8.5) (push) Successful in 1m8s
tests / browser (chrome, chromium) (push) Successful in 3m6s
tests / browser (firefox, firefox) (push) Successful in 3m45s
tests / browser (safari, webkit) (push) Successful in 5m0s
M3's assist, filter, input and suggestion chips with chip sets; choices as filter chips or a searchable combobox whose list is an anchored popover; and the search bar that opens into a docked or full-screen search view. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f92f1cb29
commit
0fee2a0952
@@ -0,0 +1,65 @@
|
||||
{{-- A set of M3 chips: a row that wraps, 8px apart, named for screen readers as a group.
|
||||
|
||||
<x-chip-set label="File types" hint="Show only these" error-field="kinds">
|
||||
<x-chip type="filter" label="Photos" value="photos" wire:model.live="kinds" />
|
||||
<x-chip type="filter" label="Documents" value="documents" wire:model.live="kinds" />
|
||||
</x-chip-set>
|
||||
|
||||
`label` is shown above the row and names the group; without it, pass `aria-label`. `hint`
|
||||
sits under it, and a validation message for `error-field` (the property the chips bind, and
|
||||
its items: `kinds` and `kinds.*`) replaces the hint.
|
||||
|
||||
`scroll` keeps the chips on one line that scrolls sideways, as M3 lays chips out on a narrow
|
||||
screen: the edge it can still scroll towards fades (resources/js/chips.js), and a chip reached
|
||||
with Tab scrolls clear of the fade. Removing a focused input chip moves focus within the set. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'errorField' => null,
|
||||
'scroll' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$key = \Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
||||
$messages = filled($errorField) && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($errorField), $errors->get($errorField.'.*')])))
|
||||
: [];
|
||||
$describedBy = $messages !== [] || filled($hint) ? "material-chip-set-{$key}-hint" : null;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
role="group"
|
||||
@if (filled($label)) aria-labelledby="material-chip-set-{{ $key }}-label" @endif
|
||||
@if ($describedBy) aria-describedby="{{ $describedBy }}" @endif
|
||||
{{ $attributes->class('min-w-0') }}
|
||||
>
|
||||
@if (filled($label))
|
||||
<p id="material-chip-set-{{ $key }}-label" class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</p>
|
||||
@endif
|
||||
|
||||
@if ($scroll)
|
||||
<div
|
||||
data-chip-set
|
||||
x-data="materialChipSet"
|
||||
wire:ignore.self
|
||||
class="-mx-1.5 -my-2 flex scroll-px-6 gap-2 overflow-x-auto px-1.5 py-2 [scrollbar-width:none] [--chip-fade-end:0px] [--chip-fade-start:0px] data-scroll-end:[--chip-fade-end:1.5rem] data-scroll-start:[--chip-fade-start:1.5rem] [mask-image:linear-gradient(to_right,transparent,#000_var(--chip-fade-start),#000_calc(100%_-_var(--chip-fade-end)),transparent)] rtl:[mask-image:linear-gradient(to_left,transparent,#000_var(--chip-fade-start),#000_calc(100%_-_var(--chip-fade-end)),transparent)]"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@else
|
||||
<div data-chip-set class="flex flex-wrap gap-2">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($messages !== [])
|
||||
<div id="{{ $describedBy }}">
|
||||
@foreach ($messages as $message)
|
||||
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif (filled($hint))
|
||||
<p id="{{ $describedBy }}" class="mt-1 type-body-sm text-on-surface-variant">{{ $hint }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,298 @@
|
||||
{{-- An M3 chip — assist, filter, input or suggestion — in one component, picked by `type`.
|
||||
|
||||
<x-chip label="Add to calendar" icon="event" wire:click="addToCalendar" />
|
||||
<x-chip type="filter" label="Photos" value="photos" wire:model.live="kinds" />
|
||||
<x-chip type="input" label="anna@example.com" avatar="AM" removable wire:remove="removeRecipient(3)" wire:key="recipient-3" />
|
||||
<x-chip type="suggestion" label="Sounds good" wire:click="reply('Sounds good')" />
|
||||
|
||||
`assist` (the default) is an action: a button, or an anchor with `link` (`wire:navigate` unless
|
||||
`external` or `no-wire-navigate`; `disabled` works on a link too, as `aria-disabled`). Its icons
|
||||
are primary. `suggestion` is the same in on-surface-variant, for a suggested reply or query.
|
||||
|
||||
`filter` is a toggle. With `wire:model`, `x-model` or `name` it is a native checkbox under the
|
||||
chip, so Livewire and Alpine bind it as they bind any checkbox — to a boolean, or to an array
|
||||
with `value` for a multi-select set — and a form submits it; `selected` is then only the
|
||||
initial state without a model. Without any of them it is a toggle button whose `selected`
|
||||
(`aria-pressed`) the caller owns, flipped by its own `wire:click`. Selected, it takes
|
||||
secondary-container and a check that grows in at the start; with an `icon`, the check takes the
|
||||
icon's place.
|
||||
|
||||
`input` is something a person entered: `avatar` (an image URL, or initials) or `icon` at the
|
||||
start, `selected`, and `removable` for a trailing remove button named "Remove <label>". Removing
|
||||
calls `wire:remove` (a Livewire action) or `remove` (an Alpine expression); with neither, the
|
||||
chip takes itself off the page, hidden `name`/`value` input and all. Backspace or Delete on a
|
||||
focused chip removes it too, and focus moves to the previous (Backspace) or next (Delete) chip.
|
||||
The chip is a button only when it has something of its own to do (`wire:click`, `x-on:click`,
|
||||
`link`, `selected`); otherwise the remove button is its one stop. Give a removable chip in a
|
||||
Livewire loop a `wire:key`.
|
||||
|
||||
`elevated` draws assist, filter and suggestion chips on surface-container-low at elevation 1
|
||||
instead of the outline; input chips are flat only, as in M3. `tooltip` attaches a plain tooltip.
|
||||
|
||||
Attributes: on a filter checkbox and an input chip, `class`, `style` and `wire:key` stay on the
|
||||
chip and every other attribute goes to the control inside (the checkbox, or the chip's button).
|
||||
|
||||
Values from androidx Compose Material 3 (Chip.kt, AssistChipTokens, FilterChipTokens,
|
||||
InputChipTokens, SuggestionChipTokens at androidx/androidx 27cf9a7, Apache-2.0): 32px tall,
|
||||
small corner, label-large, 18px icons, 24px avatar; 16px padding beside a label, 8px beside an
|
||||
icon, 8px between (an input chip: 12px, 8px, 4px beside an avatar). The check grows in on the
|
||||
fast spatial spring and fades in on the slow effects one; it shrinks on default effects and
|
||||
fades on fast effects, as AnimatingChipContent does. --}}
|
||||
|
||||
@props([
|
||||
'type' => 'assist',
|
||||
'label' => null,
|
||||
'icon' => null,
|
||||
'iconRight' => null,
|
||||
'avatar' => null,
|
||||
'elevated' => false,
|
||||
'selected' => null,
|
||||
'removable' => false,
|
||||
'remove' => null,
|
||||
'link' => null,
|
||||
'external' => false,
|
||||
'noWireNavigate' => false,
|
||||
'disabled' => false,
|
||||
'name' => null,
|
||||
'value' => null,
|
||||
'tooltip' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$kind = in_array($type, ['assist', 'filter', 'input', 'suggestion'], true) ? $type : 'assist';
|
||||
$text = $label ?? trim(strip_tags((string) $slot));
|
||||
$isLink = filled($link) && $kind !== 'filter';
|
||||
$inert = $disabled && $isLink;
|
||||
$raised = $elevated && $kind !== 'input';
|
||||
$initials = filled($avatar) && ! str_contains((string) $avatar, '/') && ! str_contains((string) $avatar, '.');
|
||||
$anchor = $tooltip !== null ? '--material-chip-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)) : null;
|
||||
|
||||
// A filter chip is a native checkbox whenever something binds or submits it.
|
||||
$checkbox = $kind === 'filter' && ($attributes->whereStartsWith(['wire:model', 'x-model'])->isNotEmpty() || filled($name));
|
||||
$wireModel = $attributes->whereStartsWith('wire:model')->first();
|
||||
$checked = (bool) $selected;
|
||||
|
||||
// Rendered as the bound property already says, so nothing flips (and animates) once Livewire starts.
|
||||
if ($checkbox && $wireModel !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||
$bound = data_get($component, $wireModel);
|
||||
$checked = is_array($bound)
|
||||
? in_array((string) $value, array_map(fn ($item): string => is_scalar($item) ? (string) $item : '', $bound), true)
|
||||
: (bool) $bound;
|
||||
}
|
||||
|
||||
$wireRemove = $attributes->get('wire:remove');
|
||||
$attributes = $attributes->except(['wire:remove']);
|
||||
|
||||
$outer = ['class', 'style', 'wire:key'];
|
||||
|
||||
// Compose draws the 1px border inside the chip; a CSS border takes room, so the padding
|
||||
// beside it is 1px short of the token (7px for 8, 15px for 16).
|
||||
|
||||
$base = 'group/chip inline-flex h-8 max-w-full shrink-0 select-none items-center whitespace-nowrap rounded-corner-sm border type-label-lg transition-[background-color,border-color,color,box-shadow] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast';
|
||||
// Below 48px the touch target reaches past the chip, as M3 requires.
|
||||
$target = 'after:absolute after:inset-x-0 after:top-1/2 after:h-12 after:-translate-y-1/2';
|
||||
|
||||
// Every colour class written out whole, so Tailwind compiles it. Selected is a state the browser
|
||||
// owns on a filter chip (a checked checkbox, or aria-pressed), so it is written as a variant.
|
||||
$colours = match (true) {
|
||||
$kind === 'filter' && $raised && $disabled => 'border-transparent bg-on-surface/12 text-on-surface/38',
|
||||
$kind === 'filter' && $raised => 'border-transparent bg-surface-container-low text-on-surface-variant shadow-elevation-1 hover:shadow-elevation-2 has-checked:bg-secondary-container has-checked:text-on-secondary-container aria-pressed:bg-secondary-container aria-pressed:text-on-secondary-container',
|
||||
$kind === 'filter' && $disabled => 'border-on-surface/12 text-on-surface/38 has-checked:border-transparent has-checked:bg-on-surface/12 aria-pressed:border-transparent aria-pressed:bg-on-surface/12',
|
||||
$kind === 'filter' => 'border-outline-variant text-on-surface-variant focus-visible:border-on-surface-variant has-focus-visible:border-on-surface-variant has-checked:border-transparent has-checked:bg-secondary-container has-checked:text-on-secondary-container hover:has-checked:shadow-elevation-1 aria-pressed:border-transparent aria-pressed:bg-secondary-container aria-pressed:text-on-secondary-container hover:aria-pressed:shadow-elevation-1',
|
||||
$kind === 'input' && $disabled => $selected ? 'border-transparent bg-on-surface/12 text-on-surface/38' : 'border-on-surface/12 text-on-surface/38',
|
||||
$kind === 'input' => $selected ? 'border-transparent bg-secondary-container text-on-secondary-container' : 'border-outline-variant text-on-surface-variant has-[[data-chip-action]:focus-visible]:border-on-surface-variant',
|
||||
$raised && $disabled => 'border-transparent bg-on-surface/12 text-on-surface/38',
|
||||
$raised => $kind === 'suggestion'
|
||||
? 'border-transparent bg-surface-container-low text-on-surface-variant shadow-elevation-1 hover:shadow-elevation-2'
|
||||
: 'border-transparent bg-surface-container-low text-on-surface shadow-elevation-1 hover:shadow-elevation-2',
|
||||
$disabled => 'border-on-surface/12 text-on-surface/38',
|
||||
default => $kind === 'suggestion'
|
||||
? 'border-outline-variant text-on-surface-variant focus-visible:border-on-surface-variant'
|
||||
: 'border-outline-variant text-on-surface focus-visible:border-on-surface',
|
||||
};
|
||||
|
||||
// Leading icons: primary on assist, suggestion and filter chips; on-surface-variant on an input
|
||||
// chip, primary while it is hovered, focused or pressed, and primary on a selected one.
|
||||
$clicks = $attributes->whereStartsWith(['wire:click', 'x-on:click', '@click'])->isNotEmpty();
|
||||
$leadingInk = match (true) {
|
||||
$disabled => null,
|
||||
$kind !== 'input', (bool) $selected => 'text-primary',
|
||||
$isLink || $clicks => 'transition-colors duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-hover/chip:text-primary group-active/chip:text-primary group-has-focus-visible/chip:text-primary',
|
||||
default => null,
|
||||
};
|
||||
$trailingInk = in_array($kind, ['assist', 'suggestion'], true) && ! $disabled ? 'text-primary' : null;
|
||||
|
||||
$check = 'size-4.5 opacity-0 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-100 group-has-checked/chip:duration-(--md-sys-motion-effects-slow-duration) group-has-checked/chip:ease-effects-slow group-aria-pressed/chip:opacity-100 group-aria-pressed/chip:duration-(--md-sys-motion-effects-slow-duration) group-aria-pressed/chip:ease-effects-slow';
|
||||
|
||||
if ($kind === 'input') {
|
||||
$actionTag = match (true) {
|
||||
$isLink => 'a',
|
||||
$clicks || $selected !== null => 'button',
|
||||
default => 'span',
|
||||
};
|
||||
$interactive = $actionTag !== 'span';
|
||||
$removeExpression = implode('; ', array_filter([
|
||||
'removeChip($event)',
|
||||
filled($remove) ? $remove : null,
|
||||
blank($remove) && blank($wireRemove) ? '$root.remove()' : null,
|
||||
]));
|
||||
|
||||
$container = $attributes->only($outer)
|
||||
->class([
|
||||
$base,
|
||||
'relative isolate',
|
||||
$colours,
|
||||
'has-[[data-chip-action]:focus-visible]:outline-3 has-[[data-chip-action]:focus-visible]:outline-offset-2 has-[[data-chip-action]:focus-visible]:outline-secondary',
|
||||
])
|
||||
->merge(array_filter(['style' => $anchor ? "anchor-name: {$anchor}" : null]));
|
||||
|
||||
$action = $attributes->except($outer)
|
||||
->class([
|
||||
'flex h-full min-w-0 items-center outline-none',
|
||||
match (true) {
|
||||
filled($avatar) => 'ps-0.75',
|
||||
filled($icon) => 'ps-1.75',
|
||||
default => 'ps-2.75',
|
||||
},
|
||||
$removable ? 'pe-2' : 'pe-2.75',
|
||||
'cursor-pointer before:absolute before:-inset-px before:-z-10 before:rounded-corner-sm before:bg-current before:opacity-0 before:transition-opacity before:duration-(--md-sys-motion-effects-fast-duration) before:ease-effects-fast hover:before:opacity-8 focus-visible:before:opacity-10 active:before:opacity-10' => $interactive && ! $disabled,
|
||||
$target => $interactive,
|
||||
'cursor-not-allowed' => $interactive && $disabled,
|
||||
'pointer-events-none' => $inert,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'data-chip-action' => $interactive ? true : null,
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||
'aria-disabled' => $inert ? 'true' : null,
|
||||
'tabindex' => $inert ? '-1' : null,
|
||||
'type' => $actionTag === 'button' ? 'button' : null,
|
||||
'disabled' => $actionTag === 'button' && $disabled ? true : null,
|
||||
'aria-pressed' => $actionTag === 'button' && $selected !== null ? ($selected ? 'true' : 'false') : null,
|
||||
], fn ($attribute): bool => $attribute !== null));
|
||||
} else {
|
||||
$tag = match (true) {
|
||||
$checkbox => 'label',
|
||||
$isLink => 'a',
|
||||
default => 'button',
|
||||
};
|
||||
$control = $checkbox
|
||||
? $attributes->except($outer)->merge(array_filter([
|
||||
'type' => 'checkbox',
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'checked' => $checked ? true : null,
|
||||
'disabled' => $disabled ? true : null,
|
||||
], fn ($attribute): bool => $attribute !== null))->class('peer sr-only')
|
||||
: null;
|
||||
|
||||
$container = ($checkbox ? $attributes->only($outer) : $attributes)
|
||||
->class([
|
||||
$base,
|
||||
'state-layer relative',
|
||||
'focus-ring' => ! $checkbox,
|
||||
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary has-focus-visible:before:opacity-10' => $checkbox,
|
||||
$kind === 'filter' || filled($icon) ? 'ps-1.75' : 'ps-3.75',
|
||||
$iconRight ? 'pe-1.75' : 'pe-3.75',
|
||||
$colours,
|
||||
$target,
|
||||
'cursor-pointer' => ! $disabled,
|
||||
'cursor-not-allowed' => $disabled && ! $isLink,
|
||||
'pointer-events-none' => $inert,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'data-chip' => $kind,
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
|
||||
'aria-disabled' => $inert ? 'true' : null,
|
||||
'tabindex' => $inert ? '-1' : null,
|
||||
'type' => $tag === 'button' ? 'button' : null,
|
||||
'disabled' => $tag === 'button' && $disabled ? true : null,
|
||||
'name' => $tag === 'button' ? $name : null,
|
||||
'value' => $tag === 'button' ? $value : null,
|
||||
'aria-pressed' => $kind === 'filter' && ! $checkbox ? ($selected ? 'true' : 'false') : null,
|
||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||
], fn ($attribute): bool => $attribute !== null));
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($kind === 'input')
|
||||
<span data-chip="input" @if ($removable) x-data="materialChip" x-on:keydown="removeOnKey($event)" @endif {{ $container }}>
|
||||
<{{ $actionTag }} {{ $action }}>
|
||||
@if ($avatar)
|
||||
@if ($initials)
|
||||
<span aria-hidden="true" @class(['me-2 grid size-6 shrink-0 place-items-center rounded-corner-full bg-primary-container type-label-sm text-on-primary-container', 'opacity-38' => $disabled])>{{ $avatar }}</span>
|
||||
@else
|
||||
<img src="{{ $avatar }}" alt="" @class(['me-2 size-6 shrink-0 rounded-corner-full object-cover', 'opacity-38' => $disabled]) />
|
||||
@endif
|
||||
@elseif ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
@endif
|
||||
|
||||
<span data-chip-label class="truncate">{{ $label ?? $slot }}</span>
|
||||
</{{ $actionTag }}>
|
||||
|
||||
@if ($removable)
|
||||
<button
|
||||
type="button"
|
||||
@if (filled($text)) data-chip-remove aria-label="{{ __('Remove :label', ['label' => $text]) }}" @else data-chip-remove="{{ __('Remove :label') }}" @endif
|
||||
@if ($wireRemove) wire:click="{{ $wireRemove }}" @endif
|
||||
x-on:click="{{ $removeExpression }}"
|
||||
@disabled($disabled)
|
||||
@class([
|
||||
'relative me-1.75 grid size-4.5 shrink-0 place-items-center rounded-corner-full',
|
||||
'cursor-pointer focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-secondary' => ! $disabled,
|
||||
'before:absolute before:-inset-0.75 before:rounded-corner-full before:bg-current before:opacity-0 before:transition-opacity before:duration-(--md-sys-motion-effects-fast-duration) before:ease-effects-fast hover:before:opacity-8 focus-visible:before:opacity-10 active:before:opacity-10' => ! $disabled,
|
||||
'after:absolute after:-inset-x-2 after:-inset-y-3.75',
|
||||
'cursor-not-allowed' => $disabled,
|
||||
])
|
||||
>
|
||||
<x-icon name="close" class="size-4.5" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if (filled($name))
|
||||
<input type="hidden" name="{{ $name }}" value="{{ $value ?? $text }}" />
|
||||
@endif
|
||||
|
||||
@if ($tooltip !== null)
|
||||
<x-tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
@endif
|
||||
</span>
|
||||
@else
|
||||
<{{ $tag }} {{ $container }}>
|
||||
@if ($checkbox)
|
||||
<input {{ $control }} />
|
||||
@endif
|
||||
|
||||
@if ($kind === 'filter')
|
||||
{{-- The check grows in from nothing; beside an icon it takes the icon's place. --}}
|
||||
<span @class([
|
||||
'me-2 grid shrink-0 overflow-hidden *:col-start-1 *:row-start-1',
|
||||
'size-4.5' => filled($icon),
|
||||
'h-4.5 w-0 transition-[width] duration-(--md-sys-motion-effects-default-duration) ease-effects-default group-has-checked/chip:w-4.5 group-has-checked/chip:duration-(--md-sys-motion-spatial-fast-duration) group-has-checked/chip:ease-spatial-fast group-aria-pressed/chip:w-4.5 group-aria-pressed/chip:duration-(--md-sys-motion-spatial-fast-duration) group-aria-pressed/chip:ease-spatial-fast' => blank($icon),
|
||||
])>
|
||||
@if ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['size-4.5 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-0 group-aria-pressed/chip:opacity-0', $leadingInk])" />
|
||||
@endif
|
||||
<x-icon name="check" data-chip-check :class="$check" />
|
||||
</span>
|
||||
@elseif ($icon)
|
||||
<x-icon :name="$icon" :class="\Illuminate\Support\Arr::toCssClasses(['me-2 size-4.5', $leadingInk])" />
|
||||
@endif
|
||||
|
||||
<span class="truncate">{{ $label ?? $slot }}</span>
|
||||
|
||||
@if ($iconRight)
|
||||
<x-icon :name="$iconRight" :class="\Illuminate\Support\Arr::toCssClasses(['ms-2 size-4.5', $trailingInk])" />
|
||||
@endif
|
||||
|
||||
@if ($tooltip !== null)
|
||||
<x-tooltip :text="$tooltip" :anchor="$anchor" />
|
||||
@endif
|
||||
</{{ $tag }}>
|
||||
@endif
|
||||
@@ -0,0 +1,218 @@
|
||||
{{-- Choosing from a list: M3's filter chips, or a searchable field with a menu.
|
||||
|
||||
Two shapes for two jobs, both on `options` (`['id' => …, 'name' => …]`, read through
|
||||
`option-value` and `option-label`), `label`, `hint` and `single`:
|
||||
|
||||
- **Chips**, the default. Every option is on screen and a press turns it on or off — "the days
|
||||
you are free" is seven filter chips, not a dropdown opened seven times. `single` makes them
|
||||
choice chips, one on at a time.
|
||||
- **`searchable`**, for a list too long to lay out — four hundred time zones. A text field
|
||||
that filters as you type, with the matches in M3's menu under it: the arrow keys move,
|
||||
Enter chooses, Escape puts the field back. One value only. The list is a popover in the top
|
||||
layer, placed by CSS anchor positioning, so a card's clipping never cuts it off. `icon`,
|
||||
`variant` and `placeholder` are the field's.
|
||||
|
||||
The selection lives in Alpine, entangled with the `wire:model` property (live with
|
||||
`wire:model.live`), rather than in native checkboxes, which would send every value back as a
|
||||
string: a list of integers stays a list of integers. Without `wire:model` it works on its own
|
||||
and binds with `x-model`. Errors for the property and its items replace the hint. When the
|
||||
options change on the server, give the component a `wire:key` that changes with them: the
|
||||
options are baked into its Alpine state. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
'hint' => null,
|
||||
'icon' => null,
|
||||
'options' => [],
|
||||
'optionValue' => 'id',
|
||||
'optionLabel' => 'name',
|
||||
'single' => false,
|
||||
'searchable' => false,
|
||||
'variant' => null,
|
||||
'placeholder' => null,
|
||||
'value' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
$messages = $model !== null && isset($errors)
|
||||
? array_values(array_unique(\Illuminate\Support\Arr::flatten([$errors->get($model), $errors->get($model.'.*')])))
|
||||
: [];
|
||||
$choices = collect($options)->map(fn ($option): array => [
|
||||
'value' => data_get($option, $optionValue),
|
||||
'label' => (string) data_get($option, $optionLabel),
|
||||
'disabled' => (bool) data_get($option, 'disabled', false),
|
||||
])->values()->all();
|
||||
$single = $single || $searchable;
|
||||
|
||||
// Rendered as the bound property already says, so nothing flips once Alpine starts.
|
||||
$current = $value;
|
||||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null) {
|
||||
$current = data_get($component, $model);
|
||||
}
|
||||
$isSelected = fn ($candidate): bool => $single
|
||||
? $current !== null && (string) $current === (string) $candidate
|
||||
: in_array((string) $candidate, array_map('strval', array_filter((array) $current, 'is_scalar')), true);
|
||||
|
||||
$id = $attributes->get('id') ?? 'field-'.substr(md5($model.'|'.$label.'|choices'), 0, 12);
|
||||
$anchor = '--material-choices-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
|
||||
@endphp
|
||||
|
||||
@if ($searchable)
|
||||
<div
|
||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
||||
x-data="{
|
||||
@if ($model !== null) value: @entangle($attributes->wire('model')), @else value: @js($current), @endif
|
||||
options: @js($choices),
|
||||
query: '',
|
||||
open: false,
|
||||
active: 0,
|
||||
selecting: false,
|
||||
get selectedLabel() {
|
||||
return this.options.find((option) => option.value === this.value)?.label ?? '';
|
||||
},
|
||||
get filtered() {
|
||||
const query = this.query.trim().toLowerCase();
|
||||
|
||||
return query === '' || this.query === this.selectedLabel
|
||||
? this.options
|
||||
: this.options.filter((option) => option.label.toLowerCase().includes(query));
|
||||
},
|
||||
init() {
|
||||
this.query = this.selectedLabel;
|
||||
this.$watch('value', () => { if (! this.open) this.query = this.selectedLabel; });
|
||||
this.$watch('open', (open) => {
|
||||
const list = this.$refs.list;
|
||||
if (open && ! list.matches(':popover-open')) list.showPopover();
|
||||
if (! open && list.matches(':popover-open')) list.hidePopover();
|
||||
});
|
||||
},
|
||||
show() {
|
||||
this.open = true;
|
||||
this.active = Math.max(0, this.filtered.findIndex((option) => option.value === this.value));
|
||||
this.$nextTick(() => this.reveal());
|
||||
},
|
||||
close() {
|
||||
this.open = false;
|
||||
this.query = this.selectedLabel;
|
||||
},
|
||||
move(step) {
|
||||
if (! this.open) return this.show();
|
||||
if (this.filtered.length === 0) return;
|
||||
this.active = (this.active + step + this.filtered.length) % this.filtered.length;
|
||||
this.$nextTick(() => this.reveal());
|
||||
},
|
||||
choose(option) {
|
||||
if (! option || option.disabled) return;
|
||||
this.value = option.value;
|
||||
this.query = option.label;
|
||||
this.open = false;
|
||||
},
|
||||
reveal() {
|
||||
this.$refs.list.querySelector('[data-active]')?.scrollIntoView({ block: 'nearest' });
|
||||
},
|
||||
}"
|
||||
@if ($model === null) x-modelable="value" @endif
|
||||
>
|
||||
<div style="anchor-name: {{ $anchor }}">
|
||||
<x-field :$id :$label :$hint :$messages :$icon :$variant>
|
||||
<input
|
||||
id="{{ $id }}"
|
||||
type="text"
|
||||
role="combobox"
|
||||
autocomplete="off"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="{{ $id }}-list"
|
||||
aria-expanded="false"
|
||||
x-bind:aria-expanded="open.toString()"
|
||||
x-bind:aria-activedescendant="open && filtered[active] ? '{{ $id }}-option-' + active : null"
|
||||
@if ($messages !== []) aria-invalid="true" @endif
|
||||
@if ($messages !== [] || filled($hint)) aria-describedby="{{ $id }}-support" @endif
|
||||
placeholder="{{ filled($placeholder) ? $placeholder : ' ' }}"
|
||||
class="field-control"
|
||||
x-model="query"
|
||||
x-on:focus="show(); $nextTick(() => $el.select())"
|
||||
x-on:pointerdown="selecting = document.activeElement !== $el"
|
||||
x-on:click="if (selecting) { $el.select(); selecting = false; } if (! open) show();"
|
||||
x-on:input="open = true; active = 0"
|
||||
x-on:keydown.arrow-down.prevent="move(1)"
|
||||
x-on:keydown.arrow-up.prevent="move(-1)"
|
||||
x-on:keydown.enter.prevent="choose(filtered[active])"
|
||||
x-on:keydown.escape="close()"
|
||||
x-on:keydown.tab="close()"
|
||||
x-on:blur="close()"
|
||||
/>
|
||||
|
||||
<x-slot:trailing>
|
||||
<x-icon name="arrow_drop_down" class="field-trailing field-arrow size-(--field-icon)" />
|
||||
</x-slot:trailing>
|
||||
</x-field>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
id="{{ $id }}-list"
|
||||
role="listbox"
|
||||
popover="manual"
|
||||
x-ref="list"
|
||||
@if (filled($label)) aria-label="{{ $label }}" @endif
|
||||
style="position-anchor: {{ $anchor }}"
|
||||
class="field-menu"
|
||||
>
|
||||
<template x-for="(option, index) in filtered" :key="option.value">
|
||||
<li
|
||||
role="option"
|
||||
x-bind:id="'{{ $id }}-option-' + index"
|
||||
x-bind:aria-selected="(option.value === value).toString()"
|
||||
x-bind:aria-disabled="option.disabled ? 'true' : null"
|
||||
x-bind:data-active="index === active ? '' : null"
|
||||
x-on:mousedown.prevent="choose(option)"
|
||||
x-on:mousemove="active = index"
|
||||
class="field-option"
|
||||
>
|
||||
<span x-text="option.label"></span>
|
||||
<x-icon name="check" class="field-check size-6" />
|
||||
</li>
|
||||
</template>
|
||||
<li x-show="filtered.length === 0" class="px-4 py-3 type-body-md text-on-surface-variant">{{ __('Nothing matches') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
{{ $attributes->only(['class', 'wire:key', 'x-model']) }}
|
||||
x-data="{
|
||||
@if ($model !== null) selection: @entangle($attributes->wire('model')), @else selection: @js($current ?? ($single ? null : [])), @endif
|
||||
options: @js(array_column($choices, 'value')),
|
||||
selected(index) {
|
||||
const value = this.options[index];
|
||||
|
||||
return @js($single) ? this.selection === value : (this.selection ?? []).includes(value);
|
||||
},
|
||||
toggle(index) {
|
||||
const value = this.options[index];
|
||||
|
||||
if (@js($single)) {
|
||||
this.selection = value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.selection ?? [];
|
||||
this.selection = current.includes(value) ? current.filter((each) => each !== value) : [...current, value];
|
||||
},
|
||||
}"
|
||||
@if ($model === null) x-modelable="selection" @endif
|
||||
>
|
||||
<x-chip-set :$label :$hint :error-field="$model">
|
||||
@foreach ($choices as $index => $choice)
|
||||
<x-chip
|
||||
type="filter"
|
||||
:label="$choice['label']"
|
||||
:selected="$isSelected($choice['value'])"
|
||||
:disabled="$choice['disabled']"
|
||||
x-bind:aria-pressed="selected({{ $index }}).toString()"
|
||||
x-on:click="toggle({{ $index }})"
|
||||
/>
|
||||
@endforeach
|
||||
</x-chip-set>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,91 @@
|
||||
{{-- M3's search: a search bar that opens into a search view with the results.
|
||||
|
||||
Bind the input like any other (`wire:model.live.debounce.300ms="query"`) and render the
|
||||
results in the slot, from a Livewire property or computed property that follows the query;
|
||||
`empty` is shown instead when the slot renders nothing (say, "No shares match"). Results are
|
||||
usually `<x-list-item>`s with a `link`, or buttons: choosing one closes the view.
|
||||
|
||||
Docked under the bar from `sm`, full screen below it with a back arrow (resources/css/
|
||||
components/search.css); `docked` keeps it docked at every width. `placeholder` ("Search"),
|
||||
`label` (the input's name when it differs from the placeholder), leading `icon` (`search`), and
|
||||
a `trailing` slot for an avatar or icon buttons in the bar. Every other attribute reaches the
|
||||
`<input type="search">`. --}}
|
||||
|
||||
@props([
|
||||
'placeholder' => null,
|
||||
'label' => null,
|
||||
'icon' => 'search',
|
||||
'docked' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
$placeholder ??= __('Search');
|
||||
$id = $attributes->get('id') ?? 'material-search-'.substr(md5($model.'|'.$placeholder), 0, 10);
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="materialSearch({{ $docked ? 'true' : 'false' }})"
|
||||
x-on:keydown.escape="if (open) { $event.stopPropagation(); close(true); }"
|
||||
x-on:focusout="leave($event)"
|
||||
x-on:pointerdown.outside="close()"
|
||||
x-trap.noscroll="fullScreen"
|
||||
x-bind:data-open="open ? '' : null"
|
||||
x-bind:data-full-screen="fullScreen ? '' : null"
|
||||
data-search
|
||||
{{ $attributes->only(['class', 'wire:key'])->class(['relative']) }}
|
||||
>
|
||||
<div data-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
|
||||
<span data-search-leading x-show="! fullScreen">
|
||||
<x-icon :name="$icon" />
|
||||
</span>
|
||||
|
||||
<button type="button" data-search-leading data-search-back x-show="fullScreen" x-cloak x-on:click="close()" aria-label="{{ __('Back') }}">
|
||||
<x-icon name="arrow_back" />
|
||||
</button>
|
||||
|
||||
<input
|
||||
{{ $attributes->except(['class', 'wire:key', 'id', 'placeholder', 'type']) }}
|
||||
x-ref="input"
|
||||
id="{{ $id }}"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
enterkeyhint="search"
|
||||
placeholder="{{ $placeholder }}"
|
||||
aria-label="{{ $label ?? $placeholder }}"
|
||||
aria-controls="{{ $id }}-view"
|
||||
aria-expanded="false"
|
||||
x-bind:aria-expanded="open.toString()"
|
||||
x-on:focus="focused()"
|
||||
x-on:click="show()"
|
||||
x-on:input="show()"
|
||||
x-on:keydown.arrow-down.prevent="show(); $nextTick(() => step(1))"
|
||||
data-search-input
|
||||
/>
|
||||
|
||||
<button type="button" data-search-clear x-on:click="clear()" aria-label="{{ __('Clear') }}">
|
||||
<x-icon name="close" />
|
||||
</button>
|
||||
|
||||
@isset($trailing)
|
||||
<span data-search-trailing>{{ $trailing }}</span>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-ref="view"
|
||||
id="{{ $id }}-view"
|
||||
data-search-view
|
||||
x-cloak
|
||||
x-show="open"
|
||||
x-on:keydown.arrow-down.prevent="step(1)"
|
||||
x-on:keydown.arrow-up.prevent="step(-1)"
|
||||
x-on:click="choose($event)"
|
||||
>
|
||||
@if ($slot->hasActualContent())
|
||||
<div data-search-results>{{ $slot }}</div>
|
||||
@elseif (isset($empty))
|
||||
<div data-search-results><p class="px-4 py-3 type-body-md text-on-surface-variant">{{ $empty }}</p></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,5 +19,6 @@
|
||||
@include('livewire-material::showcase.sections.containment')
|
||||
@include('livewire-material::showcase.sections.carousel')
|
||||
@include('livewire-material::showcase.sections.fields')
|
||||
@include('livewire-material::showcase.sections.chips')
|
||||
</main>
|
||||
@endsection
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<a href="{{ route('livewire-material.showcase') }}" class="shrink-0 type-title-lg max-sm:hidden">Livewire Material</a>
|
||||
|
||||
<nav class="-my-2 flex min-w-0 flex-1 gap-x-4 overflow-x-auto py-2 whitespace-nowrap type-label-lg text-on-surface-variant [scrollbar-width:none]" aria-label="Sections">
|
||||
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields'] as $anchor => $section)
|
||||
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips'] as $anchor => $section)
|
||||
<a href="#{{ $anchor }}" class="rounded-corner-xs hover:text-on-surface focus-ring">{{ $section }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
@php
|
||||
$examples = [
|
||||
'Assist chips' => <<<'BLADE'
|
||||
<div x-data class="flex flex-wrap items-center gap-2">
|
||||
<x-chip label="Add to calendar" icon="event" x-on:click="materialToast('Added to your calendar')" />
|
||||
<x-chip label="Directions" icon="directions" link="#chips" />
|
||||
<x-chip label="Share" icon="share" elevated tooltip="Share this file" />
|
||||
<x-chip label="Material 3 guidelines" icon-right="open_in_new" link="https://m3.material.io/components/chips" external />
|
||||
<x-chip label="Unavailable" icon="block" disabled />
|
||||
<x-chip label="Unavailable" icon="block" elevated disabled />
|
||||
</div>
|
||||
BLADE,
|
||||
'Filter chips' => <<<'BLADE'
|
||||
<div x-data="{ kinds: ['photos'] }" class="w-full space-y-6">
|
||||
<x-chip-set label="File types" hint="Show only these kinds of file">
|
||||
<x-chip type="filter" label="Photos" value="photos" x-model="kinds" />
|
||||
<x-chip type="filter" label="Documents" value="documents" x-model="kinds" />
|
||||
<x-chip type="filter" label="Archives" value="archives" x-model="kinds" />
|
||||
<x-chip type="filter" label="Video" value="video" x-model="kinds" disabled />
|
||||
</x-chip-set>
|
||||
|
||||
<x-chip-set label="Elevated, with icons">
|
||||
<x-chip type="filter" label="Starred" icon="star" name="starred" :selected="true" elevated />
|
||||
<x-chip type="filter" label="Shared with me" icon="group" name="shared" elevated />
|
||||
<x-chip type="filter" label="Expiring" name="expiring" icon-right="arrow_drop_down" />
|
||||
<x-chip type="filter" label="Archived" name="archived" :selected="true" disabled />
|
||||
</x-chip-set>
|
||||
</div>
|
||||
BLADE,
|
||||
'Input chips' => <<<'BLADE'
|
||||
<div x-data="{ recipients: ['anna@example.com', 'ben@example.com', 'chiara@example.com'] }" class="w-full space-y-6">
|
||||
<x-chip-set label="Recipients" hint="Backspace or Delete removes the focused chip">
|
||||
<template x-for="recipient in recipients" :key="recipient">
|
||||
<x-chip type="input" icon="mail" removable remove="recipients = recipients.filter((other) => other !== recipient)"><span x-text="recipient"></span></x-chip>
|
||||
</template>
|
||||
</x-chip-set>
|
||||
|
||||
<x-chip-set label="People">
|
||||
<x-chip type="input" label="Anna Müller" avatar="AM" removable />
|
||||
<x-chip type="input" label="Ben Keller" avatar="BK" :selected="true" removable />
|
||||
<x-chip type="input" label="contract.pdf" icon="picture_as_pdf" x-on:click="materialToast('contract.pdf')" />
|
||||
<x-chip type="input" label="Chiara Rossi" avatar="CR" removable disabled />
|
||||
</x-chip-set>
|
||||
</div>
|
||||
BLADE,
|
||||
'Suggestion chips' => <<<'BLADE'
|
||||
<x-chip-set label="Suggested replies">
|
||||
<x-chip type="suggestion" label="Sounds good" />
|
||||
<x-chip type="suggestion" label="Can we talk tomorrow?" />
|
||||
<x-chip type="suggestion" label="Send the files" icon="attach_file" elevated />
|
||||
<x-chip type="suggestion" label="Not now" disabled />
|
||||
</x-chip-set>
|
||||
BLADE,
|
||||
'A set that scrolls' => <<<'BLADE'
|
||||
<div class="w-full max-w-sm">
|
||||
<x-chip-set label="Sort and filter" scroll>
|
||||
<x-chip type="filter" label="Newest" name="sort[]" value="newest" :selected="true" />
|
||||
<x-chip type="filter" label="Largest" name="sort[]" value="largest" />
|
||||
<x-chip type="filter" label="Expiring soon" name="sort[]" value="expiring" />
|
||||
<x-chip type="filter" label="Password protected" name="sort[]" value="protected" />
|
||||
<x-chip type="filter" label="Downloaded" name="sort[]" value="downloaded" />
|
||||
<x-chip type="filter" label="Never opened" name="sort[]" value="unopened" />
|
||||
</x-chip-set>
|
||||
</div>
|
||||
BLADE,
|
||||
];
|
||||
@endphp
|
||||
|
||||
<section id="chips" class="scroll-mt-24 space-y-6">
|
||||
<h2 class="type-headline-md">Chips</h2>
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-chip></code> — assist, filter, input and suggestion — and <code><x-chip-set></code>.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
<x-showcase::example :$title :$code />
|
||||
@endforeach
|
||||
</section>
|
||||
@@ -98,6 +98,39 @@
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Choices' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2">
|
||||
<div class="grid content-start gap-6">
|
||||
<x-choices label="Days you are free" hint="Filter chips: any number" :value="[1, 3]" :options="[['id' => 1, 'name' => 'Mon'], ['id' => 2, 'name' => 'Tue'], ['id' => 3, 'name' => 'Wed'], ['id' => 4, 'name' => 'Thu'], ['id' => 5, 'name' => 'Fri'], ['id' => 6, 'name' => 'Sat', 'disabled' => true]]" />
|
||||
<x-choices label="Expires after" single :value="24" :options="[['id' => 1, 'name' => '1 hour'], ['id' => 24, 'name' => '1 day'], ['id' => 168, 'name' => '7 days']]" />
|
||||
</div>
|
||||
|
||||
<div class="grid content-start gap-4">
|
||||
<x-choices label="Time zone" searchable icon="public" value="Europe/Zurich" :options="collect(timezone_identifiers_list())->map(fn ($zone) => ['id' => $zone, 'name' => str_replace('_', ' ', $zone)])->all()" />
|
||||
<x-choices label="Language" searchable variant="filled" placeholder="Type a language" :options="[['id' => 'de', 'name' => 'Deutsch'], ['id' => 'en', 'name' => 'English'], ['id' => 'fr', 'name' => 'Français'], ['id' => 'it', 'name' => 'Italiano'], ['id' => 'rm', 'name' => 'Rumantsch', 'disabled' => true]]" />
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Search' => <<<'BLADE'
|
||||
<div class="grid w-full gap-6 md:grid-cols-2">
|
||||
<div x-data="{ query: '' }">
|
||||
<x-search x-model="query" placeholder="Search shares">
|
||||
<x-list>
|
||||
<x-list-item title="holiday-photos.zip" description="248 MB · 3 days left" icon="folder_zip" link="#fields" x-show="'holiday-photos.zip'.includes(query.toLowerCase())" />
|
||||
<x-list-item title="contract.pdf" description="1.2 MB · 1 hour left" icon="picture_as_pdf" link="#fields" x-show="'contract.pdf'.includes(query.toLowerCase())" />
|
||||
<x-list-item title="design-review.mp4" description="3.4 GB · 7 days left" icon="movie" link="#fields" x-show="'design-review.mp4'.includes(query.toLowerCase())" />
|
||||
</x-list>
|
||||
<x-slot:trailing>
|
||||
<span class="grid size-8 place-items-center rounded-corner-full bg-primary-container type-label-lg text-on-primary-container">AR</span>
|
||||
</x-slot:trailing>
|
||||
</x-search>
|
||||
</div>
|
||||
|
||||
<x-search placeholder="Search in settings" docked>
|
||||
<x-slot:empty>Type to search your settings.</x-slot:empty>
|
||||
</x-search>
|
||||
</div>
|
||||
BLADE,
|
||||
'A form' => <<<'BLADE'
|
||||
<x-card variant="outlined" class="w-full max-w-xl">
|
||||
<x-form x-on:submit.prevent="materialToast('Share created', { type: 'success' })" separator>
|
||||
@@ -121,7 +154,7 @@
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-input></code>, <code><x-password></code>, <code><x-textarea></code>, <code><x-select></code>, <code><x-file></code>
|
||||
(all on <code><x-field></code>, outlined or filled), <code><x-checkbox></code>, <code><x-radio></code>, <code><x-toggle></code> and <code><x-form></code>.
|
||||
(all on <code><x-field></code>, outlined or filled), <code><x-checkbox></code>, <code><x-radio></code>, <code><x-toggle></code>, <code><x-choices></code>, <code><x-search></code> and <code><x-form></code>.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
|
||||
Reference in New Issue
Block a user