mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-09 11:16:37 +08:00
222 lines
4.7 KiB
TypeScript
222 lines
4.7 KiB
TypeScript
import * as React from "react";
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent as BaseDialogContent,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface DisclosureOptions {
|
|
isOpen?: boolean;
|
|
}
|
|
|
|
type OpenChangeHandler = (open?: boolean) => void;
|
|
|
|
export function useDisclosure(options: DisclosureOptions = {}) {
|
|
const [isOpen, setIsOpen] = React.useState(Boolean(options.isOpen));
|
|
|
|
const onOpen = React.useCallback(() => {
|
|
setIsOpen(true);
|
|
}, []);
|
|
|
|
const onClose = React.useCallback(() => {
|
|
setIsOpen(false);
|
|
}, []);
|
|
|
|
const onOpenChange = React.useCallback<OpenChangeHandler>((open) => {
|
|
setIsOpen((prev) => (typeof open === "boolean" ? open : !prev));
|
|
}, []);
|
|
|
|
return {
|
|
isOpen,
|
|
onClose,
|
|
onOpen,
|
|
onOpenChange,
|
|
};
|
|
}
|
|
|
|
interface ModalContextValue {
|
|
classNames?: Record<string, string>;
|
|
onClose: () => void;
|
|
scrollBehavior?: "inside" | "outside";
|
|
size?: ModalSize;
|
|
}
|
|
|
|
const ModalContext = React.createContext<ModalContextValue | null>(null);
|
|
|
|
function useModalContext() {
|
|
return React.useContext(ModalContext);
|
|
}
|
|
|
|
type ModalSize = "sm" | "md" | "lg" | "xl" | "2xl" | "4xl" | "full";
|
|
|
|
function mapSize(size: ModalSize | undefined) {
|
|
if (size === "sm") {
|
|
return "max-w-md";
|
|
}
|
|
if (size === "lg") {
|
|
return "max-w-2xl";
|
|
}
|
|
if (size === "xl") {
|
|
return "max-w-3xl";
|
|
}
|
|
if (size === "2xl") {
|
|
return "max-w-5xl";
|
|
}
|
|
if (size === "4xl") {
|
|
return "max-w-6xl";
|
|
}
|
|
if (size === "full") {
|
|
return "w-[95vw] max-w-[95vw] h-[95vh] max-h-[95vh]";
|
|
}
|
|
|
|
return "max-w-lg";
|
|
}
|
|
|
|
export interface ModalProps {
|
|
backdrop?: "blur" | "opaque" | "transparent";
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
classNames?: Record<string, string>;
|
|
isDismissable?: boolean;
|
|
isOpen?: boolean;
|
|
onClose?: () => void;
|
|
onOpenChange?: (open: boolean) => void;
|
|
placement?: "center" | "top" | "bottom";
|
|
scrollBehavior?: "inside" | "outside";
|
|
size?: ModalSize;
|
|
}
|
|
|
|
export function Modal({
|
|
children,
|
|
classNames,
|
|
isOpen = false,
|
|
onClose,
|
|
onOpenChange,
|
|
scrollBehavior,
|
|
size,
|
|
}: ModalProps) {
|
|
const handleOpenChange = (open: boolean) => {
|
|
onOpenChange?.(open);
|
|
if (!open) {
|
|
onClose?.();
|
|
}
|
|
};
|
|
|
|
const contextValue = {
|
|
classNames,
|
|
onClose: () => {
|
|
handleOpenChange(false);
|
|
},
|
|
scrollBehavior,
|
|
size,
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
|
<ModalContext.Provider value={contextValue}>
|
|
{children}
|
|
</ModalContext.Provider>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
interface ModalContentProps
|
|
extends Omit<React.ComponentProps<"div">, "children"> {
|
|
children: React.ReactNode | ((onClose: () => void) => React.ReactNode);
|
|
scrollBehavior?: "inside" | "outside";
|
|
size?: ModalSize;
|
|
}
|
|
|
|
export function ModalContent({
|
|
children,
|
|
className,
|
|
scrollBehavior,
|
|
size,
|
|
...props
|
|
}: ModalContentProps) {
|
|
const context = useModalContext();
|
|
const resolvedScrollBehavior = scrollBehavior ?? context?.scrollBehavior;
|
|
const resolvedSize = size ?? context?.size;
|
|
const renderedChildren =
|
|
typeof children === "function"
|
|
? children(() => context?.onClose())
|
|
: children;
|
|
|
|
return (
|
|
<BaseDialogContent
|
|
className={cn(
|
|
mapSize(resolvedSize),
|
|
context?.classNames?.base,
|
|
resolvedScrollBehavior === "outside"
|
|
? "max-h-[90vh] overflow-y-auto scrollbar-hide"
|
|
: "",
|
|
resolvedScrollBehavior === "inside"
|
|
? "max-h-[90vh] flex flex-col overflow-hidden [&_[data-slot=modal-body]]:min-h-0 [&_[data-slot=modal-body]]:flex-1 [&_[data-slot=modal-body]]:overflow-y-auto [&_[data-slot=modal-body]]:scrollbar-hide"
|
|
: "",
|
|
className,
|
|
)}
|
|
showCloseButton={false}
|
|
{...props}
|
|
>
|
|
{renderedChildren}
|
|
</BaseDialogContent>
|
|
);
|
|
}
|
|
|
|
export function ModalHeader({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
const context = useModalContext();
|
|
|
|
return (
|
|
<DialogTitle asChild>
|
|
<div
|
|
className={cn(
|
|
"text-lg font-semibold",
|
|
context?.classNames?.header,
|
|
className,
|
|
)}
|
|
data-slot="modal-header"
|
|
{...props}
|
|
/>
|
|
</DialogTitle>
|
|
);
|
|
}
|
|
|
|
export function ModalBody({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
const context = useModalContext();
|
|
|
|
return (
|
|
<div
|
|
className={cn("space-y-4", context?.classNames?.body, className)}
|
|
data-slot="modal-body"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function ModalFooter({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
const context = useModalContext();
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"mt-4 flex flex-wrap justify-end gap-2",
|
|
context?.classNames?.footer,
|
|
className,
|
|
)}
|
|
data-slot="modal-footer"
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|