Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145<script module lang="ts"> import { tv, type VariantProps } from "tailwind-variants";
export const modal = tv({ slots: { panel: "modal-panel m-auto flex w-[95%] popover-transition flex-col gap-4 overscroll-none rounded-lg border border-border-default bg-background-default p-4 text-foreground-default shadow-lg", title: "flex flex-row items-center gap-2 px-0.5 typography-paragraph-large font-medium", titleIcon: "size-4.5 text-foreground-muted", description: "text-foreground-muted", header: "flex flex-col gap-2", footer: "flex gap-2" }, variants: { size: { regular: { panel: "md:w-96" }, wide: { panel: "md:w-lg" } } }, defaultVariants: { size: "regular" } });
export type ModalVariants = VariantProps<typeof modal>;</script>
<script lang="ts"> import type { Component, Snippet } from "svelte"; import type { SvelteHTMLElements } from "svelte/elements"; import { popoverExit } from "$lib/popoverExit";
interface Props { open?: boolean; title?: string; titleIcon?: Component<SvelteHTMLElements["svg"]>; description?: string; size?: ModalVariants["size"]; class?: string; children: Snippet; footer?: Snippet; }
let { open = $bindable(false), title, description, size = "regular", class: className, children, footer, titleIcon }: Props = $props();
const classes = $derived(modal({ size }));
let panel = $state<HTMLDialogElement>(); const titleId = $props.id();
$effect(() => { if (!panel) return; if (open && !panel.open) { panel.showModal(); panel.focus(); } else if (!open && panel.open) panel.close(); });
const TitleIcon = $derived(titleIcon);
// safari has no `closedby` yet, so dismissing on an outside click is on us there. // a click on the backdrop is reported against the dialog itself, so the pointer // is what tells the two apart const onClick = (event: MouseEvent) => { if (!panel || event.target !== panel) return; const box = panel.getBoundingClientRect(); const inside = event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom; if (!inside) panel.close(); };</script>
<dialog bind:this={panel} closedby="any" onclose={() => (open = false)} onclick={onClick} aria-labelledby={title ? titleId : undefined} tabindex={-1} class={classes.panel({ class: className })} {@attach popoverExit()}> {#if title || description} <div class={classes.header()}> {#if title} <p id={titleId} class={classes.title()}> <TitleIcon class={classes.titleIcon()} /> {title} </p> {/if} {#if description}<p class={classes.description()}>{description}</p>{/if} </div> {/if}
{@render children()}
{#if footer} <div class={classes.footer()}>{@render footer()}</div> {/if}</dialog>
<style> :global(html:has(.modal-panel:modal)) { overflow: hidden; }
/* the panel fades, so the backdrop has to fade with it — a `backdrop:` utility can only paint it, and it would pop in under a panel that is still fading */ .modal-panel::backdrop { background-color: rgb(0 0 0 / 0.4); opacity: 0; transition: opacity 120ms ease-out, overlay 120ms allow-discrete, display 120ms allow-discrete; }
.modal-panel:modal::backdrop { opacity: 1; }
@starting-style { .modal-panel:modal::backdrop { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .modal-panel::backdrop { transition: none; } }</style>