Files
livewire-material/resources/views/components/modal.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

116 lines
5.6 KiB
PHP

{{-- An M3 dialog, on the native `<dialog>` opened with `showModal()`.
Native because it gets the hard parts right on its own: the top layer above everything, the
rest of the page inert, focus moved in and handed back, Escape. The open state is the Livewire
property in `wire:model` (entangled live) a flag (`$confirmingDelete`) or an id
(`$deletingShareId`) and closing writes back whichever "closed" means for it, `false` or
`null`. Without `wire:model` it reads and writes `open` in the Alpine scope around it:
<div x-data="{ open: false }">
<x-button label="Delete" x-on:click="open = true" />
<x-modal title="Delete this share?"></x-modal>
</div>
`wire:ignore.self`, because `showModal()` sets the `open` attribute, which the server's HTML
does not have: without it the next Livewire render morphs the attribute away and the dialog
shuts under the person using it. The contents still morph.
M3's basic dialog (DialogTokens, androidx Compose Material 3, Apache-2.0): surface-container-
high, extra-large corner, elevation 3, a headline-small `title`, body-medium `subtitle` in
on-surface-variant, and the `actions` slot at the end. `icon` puts a secondary-coloured icon
above a centred title, as M3 draws a dialog with a hero icon. `fullscreen` makes a dialog that
holds a form take the whole screen on a compact window (below `medium`, 600px M3 uses
full-screen dialogs "only in compact breakpoints"), with a close button and the title in a top bar
clear of the notch. `persistent` ignores Escape and the scrim, for a dialog that must be
answered. It opens on the fast spatial spring and closes at once, as M3's do. --}}
@props([
'title' => null,
'subtitle' => null,
'icon' => null,
'separator' => false,
'persistent' => false,
'fullscreen' => false,
'boxClass' => null,
])
@php
$model = $attributes->wire('model')->value() ?: null;
$id = $attributes->get('id') ?? 'material-dialog-'.substr(md5($model.'|'.$title), 0, 10);
@endphp
<dialog
wire:ignore.self
@if ($model !== null)
x-data="{
open: @entangle($attributes->wire('model')).live,
close() { this.open = typeof this.open === 'boolean' ? false : null },
}"
@else
x-data="{ close() { this.open = false } }"
@endif
x-effect="open ? ($el.open || $el.showModal()) : ($el.open && $el.close())"
x-on:cancel.prevent="{{ $persistent ? '' : 'close()' }}"
x-on:close="if (open) close()"
@if (! $persistent) x-on:click.self="close()" @endif
@if (filled($title)) aria-labelledby="{{ $id }}-title" @endif
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id', 'class'])->merge(['id' => $id]) }}
@class([
'm-auto max-h-[calc(100dvh-3rem)] w-[calc(100vw-3rem)] max-w-[35rem] min-w-70 overflow-visible bg-transparent p-0 text-on-surface',
'backdrop:bg-scrim/32',
'opacity-100 scale-100 starting:opacity-0 starting:scale-95 transition-[opacity,scale] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast',
'max-medium:m-0 max-medium:h-dvh max-medium:max-h-none max-medium:w-full max-medium:max-w-none max-medium:min-w-0' => $fullscreen,
$attributes->get('class'),
])
>
<div @class([
'flex max-h-[inherit] flex-col overflow-y-auto rounded-corner-xl bg-surface-container-high p-6 shadow-elevation-3',
'max-medium:h-full max-medium:rounded-none max-medium:p-0 max-medium:pt-[var(--material-safe-top,env(safe-area-inset-top))]' => $fullscreen,
$boxClass,
])>
@if ($fullscreen)
<div class="flex h-16 shrink-0 items-center gap-1 px-1 medium:hidden">
<x-livewire-material::button icon="close" :tooltip="__('Close')" x-on:click="close()" />
@if (filled($title))
<span class="truncate type-title-lg">{{ $title }}</span>
@endif
</div>
@endif
<div @class(['min-h-0 flex-1', 'max-medium:overflow-y-auto max-medium:px-6 max-medium:pb-6' => $fullscreen])>
@if (filled($title) || filled($subtitle) || $icon)
{{-- A full-screen dialog's bar names it on a phone, so only the subtitle stays there. --}}
<div @class(['mb-4', 'max-medium:hidden' => $fullscreen && ! $icon && blank($subtitle), 'text-center' => $icon])>
@if ($icon)
<x-livewire-material::icon :name="$icon" class="mx-auto mb-4 size-6 text-secondary" />
@endif
@if (filled($title))
<h2 id="{{ $id }}-title" @class(['type-headline-sm', 'max-medium:hidden' => $fullscreen && ! $icon])>{{ $title }}</h2>
@endif
@if (filled($subtitle))
<p @class(['type-body-md text-on-surface-variant', 'mt-4' => filled($title) || $icon, 'max-medium:mt-0' => $fullscreen && ! $icon])>{{ $subtitle }}</p>
@endif
@if ($separator)
<x-livewire-material::divider class="mt-4" />
@endif
</div>
@endif
<div class="type-body-md text-on-surface-variant">{{ $slot }}</div>
</div>
@isset($actions)
<div @class([
'flex shrink-0 flex-wrap items-center justify-end gap-2 pt-6',
'max-medium:border-t max-medium:border-outline-variant max-medium:px-6 max-medium:py-4' => $fullscreen,
])>
{{ $actions }}
</div>
@endisset
</div>
</dialog>