fix: chat UI fixes
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The shell the chat dialogs sit in: a bottom drawer on mobile, a centred
|
||||
* modal on desktop.
|
||||
*
|
||||
* Extracted because "Add members" and "New group" are the same shape — title,
|
||||
* a fixed toolbar of inputs, a scrolling list of people, a confirm button —
|
||||
* and each would otherwise carry its own copy of the drawer/modal split.
|
||||
*
|
||||
* Follows the pattern already used by ReportDialog and ShareModal: the same
|
||||
* `useMediaQuery("(max-width: 767px)")` breakpoint, vaul's Drawer below it,
|
||||
* and a portalled modal above.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { X } from "lucide-react";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
|
||||
export function ResponsiveDialog({
|
||||
title,
|
||||
description,
|
||||
onClose,
|
||||
toolbar,
|
||||
footer,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
onClose: () => void;
|
||||
/** Fixed block under the title — search fields, selected chips. */
|
||||
toolbar?: ReactNode;
|
||||
/** Pinned action row at the bottom. */
|
||||
footer?: ReactNode;
|
||||
/** The scrolling region between toolbar and footer. */
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
|
||||
// Desktop only: Escape to dismiss, and lock the page behind the modal. The
|
||||
// drawer does both itself, and doubling up on the scroll lock fights vaul's
|
||||
// own body handling on iOS.
|
||||
useEffect(() => {
|
||||
if (isMobile) return;
|
||||
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
document.addEventListener("keydown", onKey);
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = prev;
|
||||
};
|
||||
}, [isMobile, onClose]);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open onOpenChange={(o) => !o && onClose()}>
|
||||
<DrawerContent className="bg-card">
|
||||
<DrawerHeader className="text-left">
|
||||
<DrawerTitle>{title}</DrawerTitle>
|
||||
<DrawerDescription>{description}</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{toolbar && <div className="border-b border-border px-4 pb-3">{toolbar}</div>}
|
||||
{/* Capped rather than flex-1: a drawer sizes to its content, so an
|
||||
unbounded list would grow the sheet past the screen. */}
|
||||
<div className="max-h-[45vh] overflow-y-auto overscroll-contain">
|
||||
{children}
|
||||
</div>
|
||||
{footer && <div className="border-t border-border p-4">{footer}</div>}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof document === "undefined") return null;
|
||||
return createPortal(
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-100 bg-foreground/40 backdrop-blur-sm animate-in fade-in-0"
|
||||
onClick={onClose}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="fixed left-1/2 top-1/2 z-101 w-full max-w-md -translate-x-1/2 -translate-y-1/2 px-4"
|
||||
>
|
||||
<div className="flex max-h-[80vh] flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-xl">
|
||||
<div className="flex items-start gap-3 border-b border-border px-5 py-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="font-[family-name:var(--font-title)] text-lg font-semibold text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-0.5 text-sm leading-relaxed text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{toolbar && <div className="border-b border-border p-3">{toolbar}</div>}
|
||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
||||
{footer && <div className="border-t border-border p-3">{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user