Files
livewire-material/resources/views/components/list-item.blade.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 927e439064 Lead a list item with M3's video slot
Plan step 23, containment.md § Missing ("Lists: leading video slot").
`<x-list-item video="…">` (or `<x-slot:video>`) draws M3's leading media
in its landscape sizes: 100×56px in a two-line item, 114×64px in a
three-line one (ListTokens' LeadingVideoSmall 56×100dp and
LeadingVideoLarge 64×114dp), with the small corner the leading image
takes. The tallest element sets an item's height, so a video lifts the
item to at least the 72px two-line row — 56 + 2×8 is 72 exactly, and
64 + 2×12 is 88.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:32:15 +02:00

166 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{-- One M3 list item: a headline with what leads it, what supports it and what trails it.
<x-list-item title="holiday-photos.zip" description="248 MB · expires in 3 days" icon="folder_zip" trailing="3 files" />
<x-list-item title="Settings" icon="settings" icon-right="chevron_right" link="/settings" />
`title` (or the slot) is the body-large headline; `overline` sits above it (label-small),
`description` under it (body-medium, up to two lines). The leading element is one of `icon`,
`avatar` (an image URL, or initials in primary-container), `image` (a 56px thumbnail) or
`video`, or a `leading` slot (a checkbox, a switch). The trailing element is `trailing` text
(label-small), `icon-right`, or an `end` slot for controls (a menu, a switch). One-, two- and three-line
heights (56, 72, 88px) follow from what is given, 16px between the item and what leads or
trails it, and a three-line item top-aligns rather than centring M3 aligns an item middle
"by default, top-aligned if the item is 88dp+ or has 3+ lines of text". Its icons are 24px,
or 20px in a `segmented` list, which is M3 Expressive's (ListTokens, androidx Compose
Material 3, Apache-2.0).
`video` is M3's leading media in its landscape size a poster URL, or a `<x-slot:video>`
holding a `<video>` or a thumbnail with a play badge. ListTokens gives it two: 100×56px in a
two-line item and 114×64px in a three-line one (`LeadingVideoSmall` 56dp × 100dp,
`LeadingVideoLarge` 64dp × 114dp height first, both 16:9), so a video always lifts the item
to at least the 72px two-line height, where 56 + 2×8 comes to 72 exactly, and to 88 with the
large one, where 64 + 2×12 comes to 88. It takes the small corner the leading image takes
(`ItemLeadingImageShape` = CornerSmall; the site publishes no separate video shape) and sits
in the leading slot, 16px in from the edge like every other leading element. M3's slot model
asks that the leading slot stay narrower than the content slot, so give an item with a large
video the room its text needs
(docs/reference/m3/components-actions-communication-containment.md § Lists → Anatomy, Specs).
`link` makes the whole item the link. Otherwise, to make it open something while its trailing
controls keep their own presses, give it `data-list-row` and put `data-list-open` on the one
control that opens — see resources/js/list-rows.js. `selected` (true) is M3's selected item,
in secondary-container; `disabled` greys it to 38%, drops its link and announces it
`aria-disabled` M3's states model treats disabled as not interactive, so a disabled item
answers neither the pointer nor the keyboard.
It takes its role from the `<x-list>` around it: a `listitem` in a plain list, where
`selected` is `aria-current`, and an `option` announcing `aria-selected` in a `selectable`
one. M3 asks for two cues on a selected item, never colour alone, so a selected option also
draws a trailing check unless `icon-right` says otherwise. --}}
@props([
'title' => null,
'overline' => null,
'description' => null,
'icon' => null,
'avatar' => null,
'image' => null,
'video' => null,
'trailing' => null,
'iconRight' => null,
'link' => null,
'external' => false,
'noWireNavigate' => false,
'selected' => false,
'disabled' => false,
])
{{-- The `<x-list>` around it: whether it is one a person chooses from and how many it takes, and
whether it is the expressive (segmented) one, whose icons are a size smaller. --}}
@aware([
'selectable' => false,
'selection' => null,
'segmented' => false,
])
@php
$isLink = filled($link) && ! $disabled;
$lines = (filled($overline) ? 1 : 0) + (filled($description) ? 1 : 0);
// `video` is either a poster URL or a `<x-slot:video>`; a slot is an object, which `filled()`
// would call full even when it holds nothing.
$hasVideo = $video instanceof \Illuminate\View\ComponentSlot ? $video->isNotEmpty() : filled($video);
// The tallest element sets the item's height: the small video needs the 72px two-line row,
// and the large one goes with the 88px three-line item M3 draws it in.
$videoLarge = $hasVideo && $lines === 2;
$lines = $hasVideo ? max($lines, 1) : $lines;
$initials = filled($avatar) && ! str_contains((string) $avatar, '/') && ! str_contains((string) $avatar, '.');
$option = $selectable || $selection !== null;
$check = $option && $selected && blank($iconRight);
// ListTokens.kt:153,156,332,335 — ItemLeading/TrailingIconExpressiveSize = 20dp against the
// baseline 24dp. At 20px the symbol is drawn from the 20 optical cut, not the 24 scaled down.
$iconSize = $segmented ? 'size-5' : 'size-6';
$optical = $segmented ? '20' : '24';
@endphp
<div
role="{{ $option ? 'option' : 'listitem' }}"
@if ($option) aria-selected="{{ $selected ? 'true' : 'false' }}" @elseif ($selected) aria-current="true" @endif
data-list-item
@if ($isLink) data-list-row @endif
@if ($selected) data-selected @endif
@if ($disabled) aria-disabled="true" @endif
{{ $attributes->class([
'relative flex gap-4 px-4 text-on-surface',
'items-center' => $lines !== 2,
'items-start' => $lines === 2,
'min-h-14 py-2' => $lines === 0,
'min-h-18 py-2' => $lines === 1,
'min-h-22 py-3' => $lines === 2,
'pointer-events-none text-on-surface/38' => $disabled,
]) }}
>
@isset($leading)
<div class="flex shrink-0 items-center">{{ $leading }}</div>
@elseif ($hasVideo)
<div @class([
'shrink-0 overflow-hidden rounded-corner-sm bg-surface-container-highest *:size-full *:object-cover',
'h-16 w-28.5' => $videoLarge,
'h-14 w-25' => ! $videoLarge,
])>
@if ($video instanceof \Illuminate\View\ComponentSlot)
{{ $video }}
@else
<img src="{{ $video }}" alt="" />
@endif
</div>
@elseif ($avatar)
@if ($initials)
<span class="grid size-10 shrink-0 place-items-center rounded-corner-full bg-primary-container type-title-md text-on-primary-container" aria-hidden="true">{{ $avatar }}</span>
@else
<img src="{{ $avatar }}" alt="" class="size-10 shrink-0 rounded-corner-full object-cover" />
@endif
@elseif ($image)
<img src="{{ $image }}" alt="" class="size-14 shrink-0 rounded-corner-sm object-cover" />
@elseif ($icon)
<x-livewire-material::icon :name="$icon" :optical="$optical" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'text-on-surface-variant' => ! $selected && ! $disabled])" />
@endif
<div class="min-w-0 flex-1">
@if ($overline)
<p @class(['type-label-sm', 'text-on-surface-variant' => ! $selected])>{{ $overline }}</p>
@endif
@if ($isLink)
<a
href="{{ $link }}"
data-list-open
@if ($external) target="_blank" rel="noopener" @elseif (! $noWireNavigate) wire:navigate @endif
class="block truncate type-body-lg outline-none"
>{{ $title ?? $slot }}</a>
@else
<p class="truncate type-body-lg">{{ $title ?? $slot }}</p>
@endif
@if ($description)
<p @class(['line-clamp-2 type-body-md', 'text-on-surface-variant' => ! $selected])>{{ $description }}</p>
@endif
</div>
@isset($end)
<div class="flex shrink-0 items-center gap-1">{{ $end }}</div>
@endisset
@if ($trailing)
<span @class(['shrink-0 type-label-sm', 'text-on-surface-variant' => ! $selected])>{{ $trailing }}</span>
@endif
@if ($iconRight)
<x-livewire-material::icon :name="$iconRight" :optical="$optical" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'text-on-surface-variant' => ! $selected && ! $disabled])" />
@elseif ($check)
<x-livewire-material::icon name="check" :optical="$optical" :class="$iconSize" />
@endif
</div>