fix: chat UI fixes

This commit is contained in:
gosangam
2026-08-29 04:12:30 +05:30
parent 290ceb048f
commit 028a8d7e1c
8 changed files with 369 additions and 166 deletions
+125
View File
@@ -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,
);
}