Offer M3's standard side sheet beside the content
Plan step 23, containment.md § Missing ("Side sheets: a genuine standard
variant"). `<x-drawer standard>` is M3's other side-sheet variant: from
`expanded` it is co-planar with the content — no scrim, no focus trap,
nothing inert, 0dp elevation, `surface` rather than surface-container-low,
no corner, and an outline-variant rule down its inner edge in place of the
scrim — sitting in the page flow at the window's full height. Below
`expanded` it is the modal sheet, since M3 caps a side sheet at 400dp and
a medium window has too little left beside one. Capped at that 400dp
whatever `width` says, and it always draws the close affordance M3
requires, because nothing else closes it there.
`pane` is untouched and the header and SKILL.md say how the two differ:
pane is the list-detail companion, standard is supplementary content.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b1f64771f3
commit
07759cd664
@@ -490,6 +490,8 @@ Props: `title`, `subtitle`, `icon` (centred hero icon), `separator` (a divider u
|
||||
|
||||
An M3 side sheet, bound like `<x-modal>`; `close()` in scope. Props: `title`, `subtitle`, `separator`, `side` (`end` default, `start`), `width` (`25rem`), `with-close-button` (**default true** — M3 requires a close affordance; `:with-close-button="false"` is ignored when Escape or the scrim is off, and on a pane), `close-on-escape` (default true), `without-backdrop-close`, `actions` slot (**left**-aligned in a 72dp row, which is what the side-sheet spec says; a dialog's are trailing-aligned). `pane` (with `pane-width`, `22.5rem` — M3's 360dp fixed pane) turns it into a second pane from `expanded`, where M3 shows two panes: render it after the list inside `<div class="expanded:flex expanded:items-start expanded:gap-6">`. Escape leaves a pane open unless `pane-close-on-escape`. Its body is a size container — lay out inside with `@md:` (a *container* query), never a window class.
|
||||
|
||||
`standard` is M3's other side-sheet variant, and it is not the same thing as `pane`. A **pane** is the list-detail companion: it shows what the list beside it has selected, 360dp wide on `surface-container` with a large corner. A **standard** sheet is supplementary content beside the primary content — filters, details, a list of actions: co-planar from `expanded`, flat on `surface` with 0dp elevation and no corner, the window's full height, an outline-variant rule down its inner edge instead of a scrim, nothing inert and no focus trap. Below `expanded` it is the modal sheet. Capped at M3's 400dp whatever `width` says, and it always draws the close button. Render it in the same `expanded:flex` row as a pane.
|
||||
|
||||
### `<x-bottom-sheet>`
|
||||
|
||||
An M3 bottom sheet, bound like `<x-modal>`: modal by default (scrim, inert page, drag the handle down or press Escape to close), `standard` for one that is part of the page. Props: `title`, `height` (`50dvh` — M3 caps a modal sheet's initial position at half the screen; whatever you pass is held under a ceiling of the screen less M3's 72dp top margin), `actions` slot.
|
||||
|
||||
@@ -15,6 +15,25 @@
|
||||
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 **standard** side sheet (`standard`, from `expanded`) it is M3's other variant: co-planar
|
||||
with the content rather than over it — no scrim, no focus trap, nothing inert, 0dp elevation
|
||||
(material-components-android's `SideSheet.md`: "standard side sheet elevation = 0dp,
|
||||
coplanar"), `surface` rather than surface-container-low, no corner, and an outline-variant
|
||||
divider down its inner edge in place of the scrim. It sits in the page flow beside the
|
||||
content, spans the window's height and scrolls on its own. M3 calls the standard sheet
|
||||
"supplementary surfaces mainly for medium to expanded breakpoints" and the modal one
|
||||
"preferred at compact breakpoints"; the library switches at `expanded` (840px) rather than at
|
||||
medium, because M3 also caps a side sheet at 400dp and a 600px window has too little left
|
||||
beside one — below `expanded` a `standard` sheet is the modal sheet
|
||||
(docs/reference/m3/components-actions-communication-containment.md § Side sheets).
|
||||
|
||||
`standard` and `pane` are not the same thing and neither replaces the other: a **pane** is the
|
||||
second pane of a list-detail layout — it shows what the list beside it has selected, is
|
||||
`22.5rem` wide (M3's 360dp fixed pane), sticks under the top of the viewport and sits on
|
||||
`surface-container` with a large corner. A **standard** sheet is supplementary content beside
|
||||
the primary content — filters, details, a list of actions — full height, flat on `surface`,
|
||||
divided from the content by a rule. Pass one or the other; `pane` wins if both are given.
|
||||
|
||||
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
|
||||
@@ -29,8 +48,8 @@
|
||||
sheet's open/close flow or tell whether it is transient or permanent
|
||||
(docs/reference/m3/components-actions-communication-containment.md § Side sheets →
|
||||
Accessibility) — so `with-close-button` is on by default, and `:with-close-button="false"` is
|
||||
ignored where nothing else closes the sheet: Escape off, the scrim off, or a pane, which has
|
||||
neither.
|
||||
ignored where nothing else closes the sheet: Escape off, the scrim off, or a pane or a
|
||||
standard sheet, which have neither from `expanded`.
|
||||
|
||||
maryUI's API, kept: `title`, `subtitle`, `separator`, `with-close-button`, `close-on-escape`,
|
||||
`without-backdrop-close`, `right` (ignored; use `side`), and an `actions` slot. --}}
|
||||
@@ -48,6 +67,7 @@
|
||||
'pane' => false,
|
||||
'paneWidth' => '22.5rem',
|
||||
'paneCloseOnEscape' => false,
|
||||
'standard' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
@@ -55,9 +75,21 @@
|
||||
$id = $attributes->get('id') ?? 'material-sheet-'.substr(md5($model.'|'.$title), 0, 10);
|
||||
$start = $side === 'start';
|
||||
|
||||
// One or the other: a pane is the list-detail companion, a standard sheet is supplementary
|
||||
// content beside the primary content.
|
||||
$standard = $standard && ! $pane;
|
||||
|
||||
// Both stop being modal from `expanded`, so both watch the window for that width.
|
||||
$wide = $pane || $standard;
|
||||
|
||||
// The side-sheet specs table caps the sheet at 400dp, which is where `width` already starts;
|
||||
// a wider one is the modal sheet's to take, not the co-planar standard sheet's.
|
||||
$sheetWidth = $standard ? "min({$width}, 25rem)" : $width;
|
||||
|
||||
// M3 requires a close affordance; the prop can only ever add one, never take away the last
|
||||
// way out of the sheet.
|
||||
$closeButton = $withCloseButton || ! $closeOnEscape || $withoutBackdropClose || ($pane && ! $paneCloseOnEscape);
|
||||
// way out of the sheet. A standard sheet keeps no scrim and no trap from `expanded`, and
|
||||
// Escape leaves it open there, so it always draws one.
|
||||
$closeButton = $withCloseButton || ! $closeOnEscape || $withoutBackdropClose || $standard || ($pane && ! $paneCloseOnEscape);
|
||||
@endphp
|
||||
|
||||
<div
|
||||
@@ -65,7 +97,7 @@
|
||||
@if ($model !== null) open: @entangle($attributes->wire('model')).live, @endif
|
||||
wide: false,
|
||||
close() { this.open = typeof this.open === 'boolean' ? false : null; },
|
||||
@if ($pane)
|
||||
@if ($wide)
|
||||
init() {
|
||||
const query = window.matchMedia('(width >= 52.5rem)');
|
||||
this.wide = query.matches;
|
||||
@@ -79,6 +111,12 @@
|
||||
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
|
||||
@elseif ($standard)
|
||||
{{-- A standard sheet spans the window's height beside the content and takes no room while
|
||||
closed; sticky, so it stays put as the page scrolls past it. --}}
|
||||
x-bind:class="! open && 'expanded:hidden'"
|
||||
class="expanded:sticky expanded:top-0 expanded:h-dvh expanded:shrink-0 expanded:self-start"
|
||||
data-standard
|
||||
@endif
|
||||
>
|
||||
<div
|
||||
@@ -86,7 +124,7 @@
|
||||
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])
|
||||
@class(['fixed inset-0 z-40 bg-scrim/32', 'expanded:hidden' => $wide])
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
@@ -95,24 +133,30 @@
|
||||
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-start="{{ $wide ? ($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') }}"
|
||||
x-transition:leave-end="{{ $wide ? ($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 }}"
|
||||
style="--sheet-width: {{ $sheetWidth }}; --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,
|
||||
// M3's standard side sheet from `expanded`: co-planar on `surface`, square, 0dp
|
||||
// elevation, the window's full height, with an outline-variant rule down the edge it
|
||||
// meets the content on — the divider its anatomy lists, in place of the scrim.
|
||||
'expanded:relative expanded:top-0 expanded:z-auto expanded:h-full expanded:rounded-corner-none expanded:bg-surface expanded:shadow-none' => $standard,
|
||||
'expanded:border-s expanded:border-outline-variant' => $standard && ! $start,
|
||||
'expanded:border-e expanded:border-outline-variant' => $standard && $start,
|
||||
$attributes->get('class'),
|
||||
]) }}
|
||||
>
|
||||
|
||||
@@ -140,6 +140,27 @@
|
||||
</x-modal>
|
||||
</div>
|
||||
BLADE,
|
||||
'A standard side sheet: co-planar from expanded, modal below' => <<<'BLADE'
|
||||
<div x-data="{ open: true }" class="w-full">
|
||||
<div class="expanded:flex expanded:items-start expanded:gap-6">
|
||||
<div class="min-w-0 flex-1 space-y-4">
|
||||
<x-button label="Show or hide the filters" variant="tonal" x-on:click="open = ! open" />
|
||||
<p class="type-body-md text-on-surface-variant">
|
||||
From <code>expanded</code> (840px) the sheet beside this text is co-planar: no scrim, no focus trap, flat on the surface, divided from this column by a rule. Narrow the window and the same sheet becomes the modal one.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-drawer standard title="Filters" subtitle="Narrow the list down">
|
||||
<x-list selectable label="Expiry">
|
||||
<x-list-item title="One hour" :selected="false" />
|
||||
<x-list-item title="Three days" :selected="true" />
|
||||
<x-list-item title="Thirty days" :selected="false" />
|
||||
</x-list>
|
||||
<x-slot:actions><x-button label="Apply" variant="filled" /></x-slot:actions>
|
||||
</x-drawer>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Sheets' => <<<'BLADE'
|
||||
<div x-data="{ open: false }">
|
||||
<x-button label="Side sheet" variant="tonal" x-on:click="open = true" />
|
||||
|
||||
Reference in New Issue
Block a user