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
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:32:15 +02:00
co-authored by Claude Fable 5.1
parent ce04101f89
commit 927e439064
4 changed files with 75 additions and 4 deletions
+37 -3
View File
@@ -5,15 +5,27 @@
`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) or `image` (a 56px thumbnail), 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
`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,
@@ -33,6 +45,7 @@
'icon' => null,
'avatar' => null,
'image' => null,
'video' => null,
'trailing' => null,
'iconRight' => null,
'link' => null,
@@ -53,6 +66,15 @@
@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);
@@ -82,6 +104,18 @@
>
@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>
@@ -60,6 +60,24 @@
<x-list-item title="Thirty days" :selected="false" />
</x-list>
BLADE,
'A list with leading video' => <<<'BLADE'
<x-list dividers label="Clips" class="w-full max-w-md">
<x-list-item title="Sunrise over the lake" description="2:14 · 48 MB">
<x-slot:video>
<div class="grid place-items-center bg-linear-to-br from-primary-container to-tertiary-container text-on-primary-container">
<x-icon name="play_circle" class="size-6" />
</div>
</x-slot:video>
</x-list-item>
<x-list-item overline="Draft" title="Walking the old town" description="Uploaded yesterday · 7:48">
<x-slot:video>
<div class="grid place-items-center bg-linear-to-br from-secondary-container to-primary-container text-on-secondary-container">
<x-icon name="play_circle" class="size-6" />
</div>
</x-slot:video>
</x-list-item>
</x-list>
BLADE,
'Dividers and collapse' => <<<'BLADE'
<div class="w-full space-y-4">
<x-collapse title="How long do links last?" icon="schedule" open>