Files
livewire-material/resources/views/components/drawer.blade.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 b1642aed1c Key the views on M3's window size classes, not Tailwind's
Plan step 15. Every `sm:`/`md:`/`lg:`/`xl:` in the components, the showcase
(sections, layout, shell) and the pagination and error views becomes the M3
window size class the audits' breakpoint maps prescribe: the full-screen
dialog, the FAB, the snackbar's width, `radio inline`, the section picker and
the page numbers move from 640px to `medium` (600px, M3's compact boundary,
N-07 / C-07 / IN-28); `button responsive` moves from 1024px to `expanded`
(840px); `section-nav`'s grid-to-row switch from 1280px to `large` (1200px,
N-07); and `drawer pane` from 1280px to `expanded`, where M3 shows two panes
for list-detail (C-07). The pane's default width is now 360dp, M3's fixed
supporting pane at expanded.

Showcase example grids take two columns from `medium` and three or more from
`expanded`; the showcase's own theme picker swaps at `expanded`, the nearest
class boundary to the 768px it used (no M3 rule applies to it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 04:03:54 +02:00

145 lines
7.2 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 `expanded` (840px) it is a second pane beside the list
instead M3 shows two panes for list-detail from expanded upwards, and puts a supporting pane
on the side of the focus pane at expanded, 360dp wide
(docs/reference/m3/foundations-supplement.md § Canonical layout examples).
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 `medium` (a caller's `w-*`
would race the sheet's own — a compact window gets the full-bleed sheet).
As a pane (`pane`, from `expanded`) nothing is covered: the page renders the drawer after its
list in
an `expanded:flex expanded:items-start expanded: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 (`22.5rem`, M3's 360dp fixed pane). Escape closes
the sheet but leaves a pane open, since the page beside it is still in use; `pane-close-on-escape`
closes the pane on Escape too, for a pane that is a transient detail. 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' => '22.5rem',
'paneCloseOnEscape' => false,
])
@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('(width >= 52.5rem)');
this.wide = query.matches;
query.addEventListener('change', (event) => this.wide = event.matches);
},
@endif
}"
@if ($closeOnEscape) x-on:keydown.window.escape="{{ $paneCloseOnEscape ? 'if (open) close()' : 'if (open && ! wide) close()' }}" @endif
data-sheet="{{ $id }}"
@if ($pane)
x-bind:class="! open && 'expanded:hidden'"
class="expanded:sticky expanded:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1.25rem)] expanded:shrink-0 expanded: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', 'expanded: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-expanded:-translate-x-full expanded:opacity-0' : 'max-expanded:translate-x-full expanded: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-expanded:-translate-x-full expanded:opacity-0' : 'max-expanded:translate-x-full expanded: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 }}"
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->class([
'fixed top-[var(--material-safe-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 medium:rounded-s-corner-lg' => ! $start,
'start-0 medium:rounded-e-corner-lg' => $start,
'medium:w-(--sheet-width) medium:max-w-[calc(100vw-4rem)]',
'expanded:relative expanded:top-0 expanded:z-auto expanded:max-h-[calc(100dvh-2.5rem-var(--material-safe-top,env(safe-area-inset-top)))] expanded:w-(--pane-width) expanded:max-w-none expanded:rounded-corner-lg expanded:bg-surface-container expanded: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-livewire-material::button icon="close" :tooltip-left="__('Close')" x-on:click="close()" />
</span>
@endif
</div>
@if ($separator)
<x-livewire-material::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>