repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Projectflow
stars
latest
clone command
git clone gitlawb://did:key:z6Mkfh4Y...QBEi/projectflowgit clone gitlawb://did:key:z6Mkfh4Y.../projectflowb3cded1async from playground1d ago| #1 | import { useEffect, useRef, type ReactNode } from "react"; |
| #2 | import { motion, AnimatePresence } from "framer-motion"; |
| #3 | import { X } from "lucide-react"; |
| #4 | import clsx from "clsx"; |
| #5 | |
| #6 | export function Modal({ |
| #7 | open, |
| #8 | onClose, |
| #9 | title, |
| #10 | children, |
| #11 | size = "md", |
| #12 | }: { |
| #13 | open: boolean; |
| #14 | onClose: () => void; |
| #15 | title: string; |
| #16 | children: ReactNode; |
| #17 | size?: "sm" | "md" | "lg" | "xl"; |
| #18 | }) { |
| #19 | const ref = useRef<HTMLDivElement>(null); |
| #20 | |
| #21 | useEffect(() => { |
| #22 | if (!open) return; |
| #23 | const handleKey = (e: KeyboardEvent) => { |
| #24 | if (e.key === "Escape") onClose(); |
| #25 | }; |
| #26 | window.addEventListener("keydown", handleKey); |
| #27 | return () => window.removeEventListener("keydown", handleKey); |
| #28 | }, [open, onClose]); |
| #29 | |
| #30 | useEffect(() => { |
| #31 | if (open) { |
| #32 | document.body.style.overflow = "hidden"; |
| #33 | } else { |
| #34 | document.body.style.overflow = ""; |
| #35 | } |
| #36 | return () => { |
| #37 | document.body.style.overflow = ""; |
| #38 | }; |
| #39 | }, [open]); |
| #40 | |
| #41 | const sizes = { |
| #42 | sm: "max-w-sm", |
| #43 | md: "max-w-lg", |
| #44 | lg: "max-w-2xl", |
| #45 | xl: "max-w-4xl", |
| #46 | }; |
| #47 | |
| #48 | return ( |
| #49 | <AnimatePresence> |
| #50 | {open && ( |
| #51 | <div className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"> |
| #52 | <motion.div |
| #53 | initial={{ opacity: 0 }} |
| #54 | animate={{ opacity: 1 }} |
| #55 | exit={{ opacity: 0 }} |
| #56 | transition={{ duration: 0.15 }} |
| #57 | className="absolute inset-0 bg-black/60 backdrop-blur-sm" |
| #58 | onClick={onClose} |
| #59 | /> |
| #60 | <motion.div |
| #61 | ref={ref} |
| #62 | initial={{ opacity: 0, scale: 0.95, y: -10 }} |
| #63 | animate={{ opacity: 1, scale: 1, y: 0 }} |
| #64 | exit={{ opacity: 0, scale: 0.95, y: -10 }} |
| #65 | transition={{ duration: 0.15 }} |
| #66 | className={clsx( |
| #67 | "relative w-full rounded-xl border border-border bg-surface-1 shadow-2xl shadow-black/50", |
| #68 | sizes[size] |
| #69 | )} |
| #70 | > |
| #71 | <div className="flex items-center justify-between px-5 py-4 border-b border-border"> |
| #72 | <h2 className="text-sm font-semibold text-zinc-100"> |
| #73 | {title} |
| #74 | </h2> |
| #75 | <button |
| #76 | onClick={onClose} |
| #77 | className="p-1 rounded-md text-zinc-400 hover:text-zinc-200 hover:bg-surface-3 transition-colors" |
| #78 | > |
| #79 | <X size={16} /> |
| #80 | </button> |
| #81 | </div> |
| #82 | <div className="p-5">{children}</div> |
| #83 | </motion.div> |
| #84 | </div> |
| #85 | )} |
| #86 | </AnimatePresence> |
| #87 | ); |
| #88 | } |
| #89 |