mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-11 17:27:54 +02:00
246 lines
6.8 KiB
TypeScript
246 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence, type HTMLMotionProps, motion } from "motion/react";
|
|
import { Dialog as DialogPrimitive } from "radix-ui";
|
|
import type * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { RxCross2 } from "react-icons/rx";
|
|
|
|
import { useControlledState } from "@/hooks/use-controlled-state";
|
|
import { getStrictContext } from "@/lib/get-strict-context";
|
|
import { cn } from "@/lib/utils";
|
|
import { WindowDragArea } from "../window-drag-area";
|
|
|
|
type DialogContextType = {
|
|
isOpen: boolean;
|
|
setIsOpen: DialogProps["onOpenChange"];
|
|
};
|
|
|
|
const [DialogProvider, useDialog] =
|
|
getStrictContext<DialogContextType>("DialogContext");
|
|
|
|
type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>;
|
|
|
|
function Dialog(props: DialogProps) {
|
|
const [isOpen, setIsOpen] = useControlledState({
|
|
value: props?.open,
|
|
defaultValue: props?.defaultOpen,
|
|
onChange: props?.onOpenChange,
|
|
});
|
|
|
|
return (
|
|
<DialogProvider value={{ isOpen, setIsOpen }}>
|
|
<DialogPrimitive.Root
|
|
data-slot="dialog"
|
|
{...props}
|
|
onOpenChange={setIsOpen}
|
|
/>
|
|
</DialogProvider>
|
|
);
|
|
}
|
|
|
|
type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Trigger>;
|
|
|
|
function DialogTrigger(props: DialogTriggerProps) {
|
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
}
|
|
|
|
type DialogPortalProps = Omit<
|
|
React.ComponentProps<typeof DialogPrimitive.Portal>,
|
|
"forceMount"
|
|
>;
|
|
|
|
function DialogPortal(props: DialogPortalProps) {
|
|
const { isOpen } = useDialog();
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<DialogPrimitive.Portal
|
|
data-slot="dialog-portal"
|
|
forceMount
|
|
{...props}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|
|
|
|
type DialogOverlayProps = Omit<
|
|
React.ComponentProps<typeof DialogPrimitive.Overlay>,
|
|
"forceMount" | "asChild"
|
|
> &
|
|
HTMLMotionProps<"div">;
|
|
|
|
function DialogOverlay({
|
|
className,
|
|
transition = { duration: 0.2, ease: "easeInOut" },
|
|
...props
|
|
}: DialogOverlayProps) {
|
|
return (
|
|
<DialogPrimitive.Overlay data-slot="dialog-overlay" asChild forceMount>
|
|
<motion.div
|
|
key="dialog-overlay"
|
|
initial={{ opacity: 0, filter: "blur(4px)" }}
|
|
animate={{ opacity: 1, filter: "blur(0px)" }}
|
|
exit={{ opacity: 0, filter: "blur(4px)" }}
|
|
transition={transition}
|
|
className={cn("fixed inset-0 z-9999 bg-background/50", className)}
|
|
{...props}
|
|
>
|
|
<WindowDragArea />
|
|
</motion.div>
|
|
</DialogPrimitive.Overlay>
|
|
);
|
|
}
|
|
|
|
type DialogFlipDirection = "top" | "bottom" | "left" | "right";
|
|
|
|
type DialogContentProps = Omit<
|
|
React.ComponentProps<typeof DialogPrimitive.Content>,
|
|
"forceMount" | "asChild"
|
|
> &
|
|
HTMLMotionProps<"div"> & {
|
|
from?: DialogFlipDirection;
|
|
};
|
|
|
|
function DialogContent({
|
|
className,
|
|
children,
|
|
from = "top",
|
|
onOpenAutoFocus,
|
|
onCloseAutoFocus,
|
|
onEscapeKeyDown,
|
|
onPointerDownOutside,
|
|
onInteractOutside,
|
|
transition = { type: "spring", stiffness: 150, damping: 25 },
|
|
...props
|
|
}: DialogContentProps) {
|
|
const { t } = useTranslation();
|
|
const initialRotation =
|
|
from === "bottom" || from === "left" ? "20deg" : "-20deg";
|
|
const isVertical = from === "top" || from === "bottom";
|
|
const rotateAxis = isVertical ? "rotateX" : "rotateY";
|
|
|
|
return (
|
|
<DialogPortal data-slot="dialog-portal">
|
|
<DialogOverlay />
|
|
<DialogPrimitive.Content
|
|
asChild
|
|
forceMount
|
|
onOpenAutoFocus={onOpenAutoFocus}
|
|
onCloseAutoFocus={onCloseAutoFocus}
|
|
onEscapeKeyDown={onEscapeKeyDown}
|
|
onPointerDownOutside={onPointerDownOutside}
|
|
onInteractOutside={(event) => {
|
|
const target = event.target as HTMLElement | null;
|
|
if (target?.closest('[data-window-drag-area="true"]')) {
|
|
event.preventDefault();
|
|
}
|
|
onInteractOutside?.(event);
|
|
}}
|
|
>
|
|
<motion.div
|
|
key="dialog-content"
|
|
data-slot="dialog-content"
|
|
initial={{
|
|
opacity: 0,
|
|
filter: "blur(4px)",
|
|
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
|
}}
|
|
animate={{
|
|
opacity: 1,
|
|
filter: "blur(0px)",
|
|
transform: `perspective(500px) ${rotateAxis}(0deg) scale(1)`,
|
|
}}
|
|
exit={{
|
|
opacity: 0,
|
|
filter: "blur(4px)",
|
|
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
|
}}
|
|
transition={transition}
|
|
className={cn(
|
|
"bg-background fixed top-[50%] left-[50%] z-10000 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<DialogPrimitive.Close className="cursor-pointer ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
|
<RxCross2 />
|
|
<span className="sr-only">{t("common.buttons.close")}</span>
|
|
</DialogPrimitive.Close>
|
|
</motion.div>
|
|
</DialogPrimitive.Content>
|
|
</DialogPortal>
|
|
);
|
|
}
|
|
|
|
type DialogCloseProps = React.ComponentProps<typeof DialogPrimitive.Close>;
|
|
|
|
function DialogClose(props: DialogCloseProps) {
|
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
}
|
|
|
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-header"
|
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-footer"
|
|
className={cn(
|
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DialogTitle({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
return (
|
|
<DialogPrimitive.Title
|
|
data-slot="dialog-title"
|
|
className={cn("text-lg font-semibold leading-none", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DialogDescription({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
return (
|
|
<DialogPrimitive.Description
|
|
data-slot="dialog-description"
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogOverlay,
|
|
DialogPortal,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
};
|