Add M3's uncontained multi-aspect-ratio carousel

Plan step 23, containment.md § Missing ("Carousel: the uncontained
multi-aspect-ratio layout"). `<x-carousel layout="multi-aspect">` is the
layout M3 added in November 2025: each `<x-carousel-item aspect="…">`
keeps its own ratio at the row's fixed height, held inside M3's 9:16 to
16:9 range (a square by default), with 16dp leading padding, 8dp gaps,
the extra-large corner and uncontained (default) scrolling.

The keyline machinery does not fit it: an Arrangement counts items of
one size each, and every snap position and mask follows from that size.
So the layout is a plain flex row, unmasked, and carousel.js only
measures each item's resting position off the DOM, which keeps the
previous/next buttons, the arrow keys, Home/End and bring-into-view
working. The header and SKILL.md say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 07:14:29 +02:00
co-authored by Claude Opus 5
parent 944431668d
commit bb9473b0b2
6 changed files with 163 additions and 19 deletions
+32
View File
@@ -156,3 +156,35 @@ it('lays a label over an item on a scrim, and passes attributes to the item', fu
->and((string) $this->blade('<x-carousel-item>A</x-carousel-item>'))
->not->toContain('data-material-carousel-label');
});
it('lets a multi-aspect carousel size each item by its own ratio, between 9:16 and 16:9', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-carousel layout="multi-aspect" label="Clips" height="180">
<x-carousel-item aspect="16/9">A</x-carousel-item>
<x-carousel-item aspect="9:16">B</x-carousel-item>
<x-carousel-item>C</x-carousel-item>
<x-carousel-item aspect="4">D</x-carousel-item>
<x-carousel-item aspect="0.1">E</x-carousel-item>
</x-carousel>
BLADE);
expect($html)
->toContain('data-material-carousel="multi-aspect"')
->toContain('--material-carousel-pad: 16px')
->toContain('--material-carousel-height: 180px')
->toContain('ps-(--material-carousel-pad)')
->toContain('data-padding="16" data-padding-end="0"')
// M3's own range holds whatever the item asks for: 16/9 at the top, 9/16 at the bottom.
->toContain('aspect-ratio: 1.7778')
->toContain('aspect-ratio: 0.5625')
->toContain('aspect-ratio: 1')
->toContain('w-auto rounded-corner-xl')
// Uncontained scrolling, and nothing masks an item whose width is its own.
->not->toContain('snap-mandatory')
->not->toContain('--material-carousel-slot')
->not->toContain('clip-path')
->not->toContain('x-ref="probe"')
->and(substr_count($html, 'aspect-ratio: 1.7778'))->toBe(2)
->and((string) $this->blade('<x-carousel><x-carousel-item aspect="16/9">A</x-carousel-item></x-carousel>'))
->not->toContain('aspect-ratio');
});