From 26a9230a488fa2a20099d9a908681b2288a0837e Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:04:19 +0200 Subject: [PATCH] Give a list item M3's gaps, alignment and expressive icon size Plan step 19, containment.md C-14, C-15 and C-26. The leading and trailing gap was 12px where ListItem.kt's LeadingContentEndPadding and TrailingContentStartPadding are 16dp; a three-line item was middle-aligned and padded 10px where M3 top-aligns an 88dp item and Compose pads it 12; and a segmented list's icons stayed at the baseline 24dp instead of ListTokens' expressive 20dp, which the item now reads from its list with `@aware` and draws from the 20 optical cut. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 2 +- .../views/components/list-item.blade.php | 29 ++++++++++++++----- tests/Feature/Components/ListTest.php | 23 ++++++++++++--- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index f591786c..6f1b901a 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -431,7 +431,7 @@ A card or list item that opens something is a **row**: `data-list-row` on it and ### ``, `` -``: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). ``: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content. +``: `label`, `dividers`, `segmented` (M3 Expressive: separate tiles 2px apart). ``: `title` (or slot), `overline`, `description`, leading `icon` / `avatar` (image URL or initials) / `image` / `leading` slot, trailing `trailing` text / `icon-right` / `end` slot, `link` (the whole item becomes a row that opens it), `selected`, `disabled`. One-, two- and three-line heights follow from the content, and a three-line item top-aligns as M3 asks. Its icons are 24px, 20px in a `segmented` list. A list a person chooses from is `` (or `selection="single"` / `selection="multi"`): M3 maps those to a **list box** of **options**, so the container becomes `role="listbox"` (`aria-multiselectable` when multi) and each item an `option` announcing `aria-selected`. A selected option also draws a trailing check — M3 never allows colour as the only cue — which `icon-right` or a leading checkbox replaces. A plain list stays `role="list"`, where `selected` is `aria-current`. `disabled` renders no link and announces `aria-disabled`. diff --git a/resources/views/components/list-item.blade.php b/resources/views/components/list-item.blade.php index 04c720f4..f6ee5596 100644 --- a/resources/views/components/list-item.blade.php +++ b/resources/views/components/list-item.blade.php @@ -8,7 +8,11 @@ `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 - heights (56, 72, 88px) follow from what is given (ListTokens, androidx Compose Material 3, Apache-2.0). + 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). `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 @@ -38,10 +42,12 @@ 'disabled' => false, ]) -{{-- Whether the `` around it is one a person chooses from, and how many it takes. --}} +{{-- The `` 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 @@ -50,6 +56,11 @@ $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
class([ - 'relative flex items-center gap-3 px-4 text-on-surface', + '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.5' => $lines === 1, - 'min-h-22 py-2.5' => $lines === 2, + 'min-h-18 py-2' => $lines === 1, + 'min-h-22 py-3' => $lines === 2, 'pointer-events-none text-on-surface/38' => $disabled, ]) }} > @@ -78,7 +91,7 @@ @elseif ($image) @elseif ($icon) - + @endif
@@ -111,8 +124,8 @@ @endif @if ($iconRight) - + @elseif ($check) - + @endif
diff --git a/tests/Feature/Components/ListTest.php b/tests/Feature/Components/ListTest.php index 5e1c650c..4349175b 100644 --- a/tests/Feature/Components/ListTest.php +++ b/tests/Feature/Components/ListTest.php @@ -18,10 +18,25 @@ it('draws M3 Expressive\'s segmented list', function () { ->toContain('gap-0.5'); }); -it('grows an item from one to three lines', function () { - expect((string) $this->blade(''))->toContain('min-h-14') - ->and((string) $this->blade(''))->toContain('min-h-18')->toContain('248 MB') - ->and((string) $this->blade(''))->toContain('min-h-22')->toContain('Protected'); +it('grows an item from one to three lines, top-aligning the tallest', function () { + expect((string) $this->blade(''))->toContain('items-center')->toContain('min-h-14 py-2') + ->and((string) $this->blade(''))->toContain('items-center')->toContain('min-h-18 py-2')->toContain('248 MB') + ->and((string) $this->blade('')) + ->toContain('items-start') + ->toContain('min-h-22 py-3') + ->toContain('Protected') + ->not->toContain('items-center'); +}); + +it('leaves 16px between the item and what leads or trails it', function () { + expect((string) $this->blade(''))->toContain('flex gap-4 px-4'); +}); + +it('draws the expressive icon size in a segmented list', function () { + expect((string) $this->blade('')) + ->toContain('size-5') + ->not->toContain('size-6') + ->and((string) $this->blade(''))->toContain('size-6'); }); it('leads with an icon, an avatar or an image, and trails with text or an icon', function () {