diff --git a/web/src/lib/components/moderation/ReportDialog.svelte b/web/src/lib/components/moderation/ReportDialog.svelte index 063e7f52..73e225f3 100644 --- a/web/src/lib/components/moderation/ReportDialog.svelte +++ b/web/src/lib/components/moderation/ReportDialog.svelte @@ -49,8 +49,10 @@ clear(); }); + // resetting on close would swap the panel back to the form while it is still + // fading out, so a sent report waits for the next open to clear itself $effect(() => { - if (!open && sent) clear(); + if (open && sent) clear(); }); const submit = createAction(async () => { diff --git a/web/src/lib/components/ui/Modal.svelte b/web/src/lib/components/ui/Modal.svelte index 09f4a81e..c67bbc25 100644 --- a/web/src/lib/components/ui/Modal.svelte +++ b/web/src/lib/components/ui/Modal.svelte @@ -3,7 +3,7 @@ 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", + panel: "modal-panel m-auto w-[95%] 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", @@ -124,19 +124,85 @@ overflow: hidden; } + /* the panel rides in on a spring (stiffness 420, damping 36, mass 0.9) sampled + over its 300ms settle, while the fade finishes at 160ms — so it is fully + painted while still travelling its last couple of pixels. leaving is a plain + curve, shorter and closer in, because a dismissal should not linger. */ + .modal-panel { + --modal-spring: linear( + 0, + 0.043, + 0.142, + 0.263, + 0.387, + 0.503, + 0.606, + 0.693, + 0.765, + 0.823, + 0.868, + 0.904, + 0.931, + 0.951, + 0.966, + 0.977, + 0.984, + 0.99, + 0.994, + 0.996, + 1 + ); + --modal-leave: cubic-bezier(0.4, 0, 1, 1); + --modal-enter: cubic-bezier(0.23, 1, 0.32, 1); + + /* `display` belongs here and not to a `flex` utility: an author-level + `display: flex` outranks the UA's `dialog:not([open])`, so a closed panel + would keep an invisible click-eating box in the middle of the viewport */ + display: none; + opacity: 0; + scale: 0.98; + translate: 0 6px; + transition: + opacity 150ms var(--modal-leave), + scale 150ms var(--modal-leave), + translate 150ms var(--modal-leave), + overlay 150ms allow-discrete, + display 150ms allow-discrete; + } + + .modal-panel:modal { + display: flex; + opacity: 1; + scale: 1; + translate: 0 0; + transition: + opacity 160ms var(--modal-enter), + scale 300ms var(--modal-spring), + translate 300ms var(--modal-spring); + } + + @starting-style { + .modal-panel:modal { + opacity: 0; + scale: 0.96; + translate: 0 12px; + } + } + /* 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; + opacity 150ms var(--modal-leave), + overlay 150ms allow-discrete, + display 150ms allow-discrete; } .modal-panel:modal::backdrop { opacity: 1; + transition: opacity 200ms var(--modal-enter); } @starting-style { @@ -146,7 +212,10 @@ } @media (prefers-reduced-motion: reduce) { - .modal-panel::backdrop { + .modal-panel, + .modal-panel:modal, + .modal-panel::backdrop, + .modal-panel:modal::backdrop { transition: none; } }