---
/**
* Dialog — a native modal that opens and closes with motion, zero JS.
*
* The trigger is a button with the invoker `command="show-modal"`; closing
* is a form with method="dialog", which every browser has. Motion is
* @starting-style in, transition-behavior: allow-discrete out, so display
* and the top layer wait for the exit to finish. `polyfill` adds an inline
* click handler (403 B raw) for browsers that predate invoker commands. `morph`
* renders one internal component with a measured script (lib/morph): the
* box grows out of its trigger on a same-document view transition and
* returns to it on close; the default stays at 0 B.
*/
import type { HTMLAttributes } from 'astro/types';
import DialogMorph from './internal/DialogMorph.astro';
interface Props extends HTMLAttributes<'dialog'> {
/** Element id; the trigger points at it. */
id: string;
/** Trigger label; or use the `trigger` slot. */
label?: string;
/** Close on backdrop click or Escape (closedby="any"). */
dismiss?: boolean;
/** Show the close button. */
close?: boolean;
/** Accessible name of the close button. */
closeLabel?: string;
/** Add the inline click handler for browsers without invoker commands. */
polyfill?: boolean;
/** Grow out of the trigger and back into it on a same-document view transition (adds the measured morph script; without view transitions or under reduced motion it opens and closes as usual). */
morph?: boolean;
}
const { id, label = 'Open', dismiss = true, close = true, closeLabel = 'Close', polyfill = false, morph = false, class: className, ...rest } = Astro.props;
const triggerAttrs = { command: 'show-modal', commandfor: id };
const dialogAttrs = dismiss ? { closedby: 'any' as const } : {};
---
{Astro.slots.has('trigger') ? <slot name="trigger" /> : <button type="button" class="ma-dialog__trigger" {...triggerAttrs}>{label}</button>}
<dialog id={id} class:list={['ma-dialog', className]} data-morph={morph ? '' : undefined} {...dialogAttrs} {...rest}>
<div class="ma-dialog__panel">
<slot />
{close && (
<form method="dialog" class="ma-dialog__close">
<button type="submit" aria-label={closeLabel}>
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M4 4l8 8M12 4l-8 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /></svg>
</button>
</form>
)}
</div>
</dialog>
{morph && <DialogMorph for={id} />}
{polyfill && <script is:inline>window.__maCmd||(window.__maCmd=1,document.addEventListener('click',function(e){if('command'in HTMLButtonElement.prototype||e.defaultPrevented)return;var b=e.target.closest('[commandfor]');if(!b)return;var d=document.getElementById(b.getAttribute('commandfor'));if(!d)return;var c=b.getAttribute('command');c==='show-modal'?d.showModal():c==='close'?d.close():c==='toggle-popover'?d.togglePopover():0}))</script>}
<style is:global>
@layer components {
:where(.ma-dialog) {
inset: 0;
margin: auto;
max-width: min(32rem, calc(100vw - 2rem));
border: 1px solid var(--ma-edge);
border-radius: 1rem;
padding: 0;
background: var(--ma-panel);
color: inherit;
box-shadow: 0 24px 64px -24px rgb(0 0 0 / 0.4);
opacity: 0;
scale: 0.96;
translate: 0 var(--ma-travel-hover, 4px);
transition:
opacity var(--ma-duration-fast) var(--ma-ease-out),
scale var(--ma-duration-fast) var(--ma-ease-out),
translate var(--ma-duration-fast) var(--ma-ease-out),
display var(--ma-duration-fast) allow-discrete,
overlay var(--ma-duration-fast) allow-discrete;
}
:where(.ma-dialog[open]) {
opacity: 1;
scale: 1;
translate: 0 0;
transition-duration: var(--ma-duration);
transition-timing-function: var(--ma-ease);
}
@starting-style {
:where(.ma-dialog[open]) {
opacity: 0;
scale: 0.96;
translate: 0 var(--ma-travel-enter, 24px);
}
}
:where(.ma-dialog)::backdrop {
background: var(--ma-scrim);
backdrop-filter: blur(6px);
opacity: 0;
transition:
opacity var(--ma-duration-fast) var(--ma-ease-out),
display var(--ma-duration-fast) allow-discrete,
overlay var(--ma-duration-fast) allow-discrete;
}
:where(.ma-dialog[open])::backdrop {
opacity: 1;
transition-duration: var(--ma-duration);
}
@starting-style {
:where(.ma-dialog[open])::backdrop {
opacity: 0;
}
}
:where(.ma-dialog__panel) {
position: relative;
padding: 1.5rem;
}
/* the close button's corner stays clear: a first line that runs the width of the panel ends before it */
:where(.ma-dialog__panel:has(> .ma-dialog__close)) {
padding-inline-end: 3.25rem;
}
:where(.ma-dialog__close) {
position: absolute;
inset-block-start: 0.75rem;
inset-inline-end: 0.75rem;
}
/* a plain button with no page reset under it: no UA border, background, padding or font */
:where(.ma-dialog__close button) {
display: grid;
place-items: center;
width: 2rem;
height: 2rem;
padding: 0;
border: 0;
border-radius: 999px;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
opacity: 0.6;
transition: opacity var(--ma-duration-fast) var(--ma-ease-out), background-color var(--ma-duration-fast) var(--ma-ease-out);
}
:where(.ma-dialog__close button:hover) {
opacity: 1;
background: var(--ma-edge);
}
:where(.ma-dialog__trigger) {
cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-dialog),
:where(.ma-dialog[open]),
:where(.ma-dialog)::backdrop {
scale: 1;
translate: 0 0;
transition-duration: var(--ma-duration-fast);
transition-timing-function: linear;
}
}
}
</style>