Plan step 36 (containment group, last of its four components): moves into resources/css/components/list.css (the container: display, the 2px segmented gap) and list-item.css (everything else — anatomy, states, the segmented item's own background/corner, and the row interaction pattern list.css used to carry). One commit for both: list-item.css keys heavily on list's own data-md-list="segmented" attribute and the two files are designed together. Kept, now on data-md-* hooks: selectable/selection listbox semantics (role="listbox"/"option", aria-selected, a trailing check as the second cue, C-03), a disabled item's link dropped entirely rather than left focusable (C-02), the segmented row's own state layer no longer racing its background through an unlayered escape hatch (C-08 doesn't recur — nothing here is unlayered), leading video/icon/avatar slots, three-line top alignment and 12px vertical padding (C-15), 16px leading/trailing gap (C-14), dividers inset 16px (C-16), the segmented list's surface fill (C-21) and its hover/focus/pressed/selected corner morph (C-22). The segmented icon size (20px) is now the `size` prop `<x-icon>` already takes from the parent's @aware(['segmented']), replacing the CSS override C-26's fix suggested. data-md-lines="1|2|3" replaces the old line-count classes; data-md-list-item-* hooks replace every other class list. Hooks renamed data-list -> data-md-list, data-list-item -> data-md-list-item, data-dividers -> data-md-dividers, data-selected -> data-md-selected, the generic data-list-row/-open row pattern's last consumer (list-item) moved, so list-rows.js drops the transitional dual-hook fallback the card commit added. table.css now @imports list-item.css, completing what its own comment already claimed (a row answers a pointer as a list row does) under the new per-component-import architecture. Imported from the Containment block of components.css; leaves tailwind.css, which now carries only dialog.css for this group. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
154 lines
7.0 KiB
PHP
154 lines
7.0 KiB
PHP
{{-- 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 items (`data-md-lines`, 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-md-list-row` and put `data-md-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 ? 20 : 24;
|
||
@endphp
|
||
|
||
<div
|
||
role="{{ $option ? 'option' : 'listitem' }}"
|
||
@if ($option) aria-selected="{{ $selected ? 'true' : 'false' }}" @elseif ($selected) aria-current="true" @endif
|
||
data-md-list-item
|
||
data-md-lines="{{ $lines + 1 }}"
|
||
@if ($isLink) data-md-list-row @endif
|
||
@if ($selected) data-md-selected @endif
|
||
@if ($disabled) aria-disabled="true" @endif
|
||
{{ $attributes }}
|
||
>
|
||
@isset($leading)
|
||
<div data-md-list-item-leading>{{ $leading }}</div>
|
||
@elseif ($hasVideo)
|
||
<div data-md-list-item-video data-md-size="{{ $videoLarge ? 'lg' : 'sm' }}">
|
||
@if ($video instanceof \Illuminate\View\ComponentSlot)
|
||
{{ $video }}
|
||
@else
|
||
<img src="{{ $video }}" alt="" />
|
||
@endif
|
||
</div>
|
||
@elseif ($avatar)
|
||
@if ($initials)
|
||
<span data-md-list-item-avatar aria-hidden="true">{{ $avatar }}</span>
|
||
@else
|
||
<img src="{{ $avatar }}" alt="" data-md-list-item-avatar />
|
||
@endif
|
||
@elseif ($image)
|
||
<img src="{{ $image }}" alt="" data-md-list-item-image />
|
||
@elseif ($icon)
|
||
<x-livewire-material::icon :name="$icon" :size="$iconSize" data-md-list-item-icon />
|
||
@endif
|
||
|
||
<div data-md-list-item-content>
|
||
@if ($overline)
|
||
<p data-md-list-item-overline>{{ $overline }}</p>
|
||
@endif
|
||
|
||
@if ($isLink)
|
||
<a
|
||
href="{{ $link }}"
|
||
data-md-list-open
|
||
data-md-list-item-title
|
||
@if ($external) target="_blank" rel="noopener" @elseif (! $noWireNavigate) wire:navigate @endif
|
||
>{{ $title ?? $slot }}</a>
|
||
@else
|
||
<p data-md-list-item-title>{{ $title ?? $slot }}</p>
|
||
@endif
|
||
|
||
@if ($description)
|
||
<p data-md-list-item-description>{{ $description }}</p>
|
||
@endif
|
||
</div>
|
||
|
||
@isset($end)
|
||
<div data-md-list-item-end>{{ $end }}</div>
|
||
@endisset
|
||
|
||
@if ($trailing)
|
||
<span data-md-list-item-trailing>{{ $trailing }}</span>
|
||
@endif
|
||
|
||
@if ($iconRight)
|
||
<x-livewire-material::icon :name="$iconRight" :size="$iconSize" data-md-list-item-icon />
|
||
@elseif ($check)
|
||
<x-livewire-material::icon name="check" :size="$iconSize" />
|
||
@endif
|
||
</div>
|