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
138 lines
6.3 KiB
PHP
138 lines
6.3 KiB
PHP
{{-- An M3 side sheet: detail or controls that slide in from the edge over a scrim; on a phone it is
|
|
the whole screen. With `pane`, from `xl` it is a list-detail pane beside the list instead.
|
|
|
|
The open state is the Livewire property in `wire:model` (entangled live, so a detail held in the
|
|
URL follows a close) — a flag or an id — and closing writes back `false` or `null`. Without
|
|
`wire:model` it reads and writes `open` in the Alpine scope around it. `close()` is in scope for
|
|
anything inside the sheet, so it can draw its own close button.
|
|
|
|
As a sheet it is modal: the page inert and still (`x-trap.inert.noscroll`), and it enters on
|
|
emphasized decelerate rather than a spring — a sheet anchored to the edge that overshot would
|
|
open a gap. M3's modal side sheet: surface-container-low, a large corner on its inner edge,
|
|
elevation 1; `side` `end` (the default) or `start`; `width` from `sm` (a caller's `w-*` would
|
|
race the sheet's own).
|
|
|
|
As a pane (`pane`, from `xl`) nothing is covered: the page renders the drawer after its list in
|
|
an `xl:flex xl:items-start xl:gap-6` row, the drawer sticks under the top of the viewport, the
|
|
list stays usable and another row swaps what it shows — no scrim, no trap, no inert page. While
|
|
closed it takes no room. `pane-width` sizes the pane (the sheet's width by default). The body is
|
|
a size container, so its contents lay out by the room the sheet or pane actually has (`@md:`),
|
|
never by the viewport.
|
|
|
|
maryUI's API, kept: `title`, `subtitle`, `separator`, `with-close-button`, `close-on-escape`,
|
|
`without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot. --}}
|
|
|
|
@props([
|
|
'title' => null,
|
|
'subtitle' => null,
|
|
'separator' => false,
|
|
'side' => 'end',
|
|
'right' => true,
|
|
'withCloseButton' => false,
|
|
'closeOnEscape' => true,
|
|
'withoutBackdropClose' => false,
|
|
'width' => '25rem',
|
|
'pane' => false,
|
|
'paneWidth' => null,
|
|
])
|
|
|
|
@php
|
|
$model = $attributes->wire('model')->value() ?: null;
|
|
$id = $attributes->get('id') ?? 'material-sheet-'.substr(md5($model.'|'.$title), 0, 10);
|
|
$start = $side === 'start';
|
|
@endphp
|
|
|
|
<div
|
|
x-data="{
|
|
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
|
wide: false,
|
|
close() { this.open = typeof this.open === 'boolean' ? false : null; },
|
|
@if ($pane)
|
|
init() {
|
|
const query = window.matchMedia('(min-width: 80rem)');
|
|
this.wide = query.matches;
|
|
query.addEventListener('change', (event) => this.wide = event.matches);
|
|
},
|
|
@endif
|
|
}"
|
|
@if ($closeOnEscape) x-on:keydown.window.escape="if (open && ! wide) close()" @endif
|
|
data-sheet="{{ $id }}"
|
|
@if ($pane)
|
|
x-bind:class="! open && 'xl:hidden'"
|
|
class="xl:sticky xl:top-[calc(env(safe-area-inset-top)+1.25rem)] xl:shrink-0 xl:self-start"
|
|
data-pane
|
|
@endif
|
|
>
|
|
<div
|
|
x-cloak
|
|
x-show="open"
|
|
x-transition.opacity.duration.200ms
|
|
@if (! $withoutBackdropClose) x-on:click="close()" @endif
|
|
@class(['fixed inset-0 z-40 bg-scrim/32', 'xl:hidden' => $pane])
|
|
aria-hidden="true"
|
|
></div>
|
|
|
|
<aside
|
|
x-cloak
|
|
x-show="open"
|
|
x-trap.inert.noscroll="open && ! wide"
|
|
x-transition:enter="transition-[translate,opacity] duration-(--md-sys-motion-spatial-default-duration) ease-emphasized-decelerate"
|
|
x-transition:enter-start="{{ $pane ? ($start ? 'max-xl:-translate-x-full xl:opacity-0' : 'max-xl:translate-x-full xl:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
|
|
x-transition:enter-end="translate-x-0 opacity-100"
|
|
x-transition:leave="transition-[translate,opacity] duration-(--md-sys-motion-effects-default-duration) ease-emphasized-accelerate"
|
|
x-transition:leave-start="translate-x-0 opacity-100"
|
|
x-transition:leave-end="{{ $pane ? ($start ? 'max-xl:-translate-x-full xl:opacity-0' : 'max-xl:translate-x-full xl:opacity-0') : ($start ? '-translate-x-full' : 'translate-x-full') }}"
|
|
id="{{ $id }}"
|
|
x-bind:role="wide ? 'region' : 'dialog'"
|
|
x-bind:aria-modal="wide ? null : 'true'"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
|
|
style="--sheet-width: {{ $width }}; --pane-width: {{ $paneWidth ?? $width }}"
|
|
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([
|
|
'fixed top-[env(safe-area-inset-top)] bottom-0 z-50 flex w-full flex-col overflow-y-auto bg-surface-container-low p-6 text-on-surface shadow-elevation-1',
|
|
'end-0 sm:rounded-s-corner-lg' => ! $start,
|
|
'start-0 sm:rounded-e-corner-lg' => $start,
|
|
'sm:w-(--sheet-width) sm:max-w-[calc(100vw-4rem)]',
|
|
'xl:relative xl:top-0 xl:z-auto xl:max-h-[calc(100dvh-2.5rem-env(safe-area-inset-top))] xl:w-(--pane-width) xl:max-w-none xl:rounded-corner-lg xl:bg-surface-container xl:shadow-none' => $pane,
|
|
$attributes->get('class'),
|
|
]) }}
|
|
>
|
|
@if (filled($title) || $withCloseButton)
|
|
<div class="mb-4">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
@if (filled($title))
|
|
<h2 id="{{ $id }}-title" class="type-title-lg">{{ $title }}</h2>
|
|
@endif
|
|
|
|
@if (filled($subtitle))
|
|
<p class="mt-1 type-body-md text-on-surface-variant">{{ $subtitle }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($withCloseButton)
|
|
<span class="-me-3 -mt-2 inline-flex shrink-0">
|
|
<x-button icon="close" :tooltip-left="__('Close')" x-on:click="close()" />
|
|
</span>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($separator)
|
|
<x-divider class="mt-4" />
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<div class="@container flex-1">
|
|
{{ $slot }}
|
|
</div>
|
|
|
|
@isset($actions)
|
|
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 pt-6">
|
|
{{ $actions }}
|
|
</div>
|
|
@endisset
|
|
</aside>
|
|
</div>
|