Add cards, lists, dialogs, sheets and the rest of M3's containment
tests / lint (push) Failing after 2m6s
tests / feature (8.4) (push) Successful in 1m3s
tests / feature (8.5) (push) Successful in 1m7s
tests / browser (safari, webkit) (push) Successful in 3m14s
tests / browser (chrome, chromium) (push) Successful in 2m9s
tests / browser (firefox, firefox) (push) Successful in 2m29s

<x-card> (filled, elevated, outlined), <x-list> and <x-list-item>
(plain or M3 Expressive's segmented list), <x-divider>, <x-collapse> on
<details>, <x-modal> on the native <dialog>, <x-drawer> as a side sheet or
list-detail pane, and <x-bottom-sheet> with drag to dismiss. Dialogs and
sheets bind to a Livewire flag or id and write back false or null on close,
or use the surrounding Alpine scope. Rows open from anywhere on them through
data-list-row and data-list-open. DesignGuard now also reports Blade
directives written inside a component tag, where they do not compile.

Browser test helpers wait for a complete document with Alpine and Livewire
running: in Firefox, networkidle alone could return before a repeated visit
had loaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 07:28:05 +02:00
co-authored by Claude Opus 5
parent e57063b0f4
commit fef20a9178
30 changed files with 1490 additions and 16 deletions
@@ -0,0 +1,75 @@
{{-- An M3 bottom sheet: secondary content or actions anchored to the bottom of the screen.
Modal by default: over a scrim, the page inert and still, dismissed by the scrim, Escape, or a
downward drag on its handle (or anywhere on the sheet when its content is scrolled to the top).
`standard` makes it part of the page instead no scrim, nothing inert, and it stays until closed.
The open state works as for `<x-drawer>`: `wire:model` (a flag or an id, written back `false` or
`null` on close) or `open` in the surrounding Alpine scope; `close()` is in scope inside it.
SheetBottomTokens (androidx Compose Material 3, Apache-2.0): surface-container-low, extra-large
top corners, elevation 1, a 32×4px drag handle in on-surface-variant; 640px wide at most, centred
on a wide screen; it rises on emphasized decelerate. `title` and `actions` as on a dialog.
`height` caps it (default: 90% of the screen); content scrolls inside. --}}
@props([
'title' => null,
'standard' => false,
'height' => '90dvh',
])
@php
$model = $attributes->wire('model')->value() ?: null;
$id = $attributes->get('id') ?? 'material-bottom-sheet-'.substr(md5($model.'|'.$title), 0, 10);
@endphp
<div
x-data="{
...materialBottomSheet({{ $standard ? 'true' : 'false' }}),
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
}"
x-on:keydown.window.escape="if (open && ! standard) close()"
>
@unless ($standard)
<div x-cloak x-show="open" x-transition.opacity.duration.200ms x-on:click="close()" class="fixed inset-0 z-40 bg-scrim/32" aria-hidden="true"></div>
@endunless
<section
x-cloak
x-show="open"
x-ref="sheet"
@unless ($standard) x-trap.inert.noscroll="open" @endunless
x-transition:enter="transition-[translate] duration-(--md-sys-motion-spatial-default-duration) ease-emphasized-decelerate"
x-transition:enter-start="translate-y-full"
x-transition:enter-end="translate-y-0"
x-transition:leave="transition-[translate] duration-(--md-sys-motion-effects-default-duration) ease-emphasized-accelerate"
x-transition:leave-start="translate-y-0"
x-transition:leave-end="translate-y-full"
x-bind:style="dragged ? { translate: `0 ${dragged}px`, transition: 'none' } : {}"
x-on:pointerdown="dragStart($event)"
id="{{ $id }}"
role="dialog"
@unless ($standard) aria-modal="true" @endunless
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
style="--sheet-max-height: {{ $height }}"
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([
'fixed inset-x-0 bottom-0 z-50 mx-auto flex max-h-(--sheet-max-height) w-full max-w-160 touch-pan-y flex-col rounded-t-corner-xl bg-surface-container-low pb-[env(safe-area-inset-bottom)] text-on-surface shadow-elevation-1',
$attributes->get('class'),
]) }}
>
<div class="flex shrink-0 cursor-grab justify-center py-4 active:cursor-grabbing" data-drag-handle>
<button type="button" class="h-1 w-8 rounded-corner-full bg-on-surface-variant/40 outline-offset-4 focus-visible:outline-3 focus-visible:outline-secondary" aria-label="{{ __('Close') }}" x-on:click="close()"></button>
</div>
<div x-ref="body" class="min-h-0 flex-1 overflow-y-auto px-6 pb-6">
@if (filled($title))
<h2 id="{{ $id }}-title" class="mb-4 type-title-lg">{{ $title }}</h2>
@endif
{{ $slot }}
</div>
@isset($actions)
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pb-6">{{ $actions }}</div>
@endisset
</section>
</div>