mirror of
https://github.com/katanemo/plano.git
synced 2026-05-06 22:32:42 +02:00
feat: redesign archgw -> plano + website in Next.js (#613)
* feat: redesign archgw -> plano + website * feat(www): refactor landing page sections, add new diagrams and UI improvements * feat(www): sections enhanced for clarify & diagrams added * feat(www): improvements to mobile design, layout of diagrams * feat(www): clean + typecheck * feat(www): feedback loop changes * feat(www): fix type error * fix lib/utils error * feat(www): ran biome formatting * feat(www): graphic changes * feat(www): web analytics * fea(www): changes * feat(www): introduce monorepo This change brings Turborepo monorepo to independently handle the marketing website, the docs website and any other future use cases for mutli-platform support. They are using internal @katanemo package handlers for the design system and logic. * fix(www): transpiler failure * fix(www): tsconfig issue * fix(www): next.config issue * feat(docs): hold off on docs * Delete next.config.ts * feat(www): content fix * feat(www): introduce blog * feat(www): content changes * Update package-lock.json * feat: update text * Update IntroSection.tsx * feat: Turbopack issue * fix * Update IntroSection.tsx * feat: updated Research page * refactor(www): text clarity, padding adj. * format(www) * fix: add missing lib/ files to git - fixes Vercel GitHub deployment - Updated .gitignore to properly exclude Python lib/ but include Next.js lib/ directories - Added packages/ui/src/lib/utils.ts (cn utility function) - Added apps/www/src/lib/sanity.ts (Sanity client configuration) - Fixes module resolution errors in Vercel GitHub deployments (case-sensitive filesystem) * Update .gitignore * style(www): favicon + metadata * fix(www): links * fix(www): add analytics * fix(www): add * fix(www): fix links + image * fix(www): fix links + image * fix(www): fix links * fix(www): remove from tools testing.md
This commit is contained in:
parent
48bbc7cce7
commit
0c3efdbef2
119 changed files with 27142 additions and 266 deletions
109
packages/ui/src/components/Footer.tsx
Normal file
109
packages/ui/src/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
const footerLinks = {
|
||||
product: [
|
||||
{ label: "Research", href: "/research" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Documentation", href: "https://docs.planoai.dev", external: true },
|
||||
{ label: "Hugging Face", href: "https://huggingface.co/katanemo", external: true },
|
||||
],
|
||||
resources: [
|
||||
{ label: "GitHub", href: "https://github.com/katanemo/arch", external: true },
|
||||
{ label: "Discord", href: "https://discord.gg/pGZf2gcwEc", external: true },
|
||||
{ label: "Get Started", href: "https://docs.planoai.dev/get_started/installation", external: true },
|
||||
],
|
||||
};
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer
|
||||
className="relative overflow-hidden pt-20 px-6 lg:px-[102px] pb-48"
|
||||
style={{ background: "linear-gradient(to top right, #ffffff, #dcdfff)" }}
|
||||
>
|
||||
<div className="max-w-[81rem] mx-auto relative z-10">
|
||||
{/* Main Grid Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
|
||||
{/* Left Column - Tagline and Copyright */}
|
||||
<div className="flex flex-col">
|
||||
<p className="ext-base sm:text-lg md:text-xl lg:text-[22px] font-sans font-normal tracking-[-1.0px] sm:tracking-[-1.22px]! text-black mb-6 sm:mb-8">
|
||||
Plano is a powerful agent delivery infrastructure platform that is framework-friendly,
|
||||
and empowers developers and teams to seamlessly build, deliver, and scale agentic
|
||||
applications.
|
||||
</p>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="mt-auto">
|
||||
<p className="font-sans text-sm sm:text-base text-black/63 tracking-[-0.6px] sm:tracking-[-0.8px]!">
|
||||
© Katanemo Labs, Inc. 2025 / Plano by Katanemo Labs, Inc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Navigation Links */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
|
||||
{/* Product Links */}
|
||||
<div>
|
||||
<h3 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
|
||||
Product
|
||||
</h3>
|
||||
<nav className="space-y-3 sm:space-y-4">
|
||||
{footerLinks.product.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.external ? "_blank" : undefined}
|
||||
rel={link.external ? "noopener noreferrer" : undefined}
|
||||
className="block font-sans font-normal text-sm sm:text-base lg:text-lg text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Resources Links */}
|
||||
<div>
|
||||
<h3 className="font-sans font-normal text-xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
|
||||
Resources
|
||||
</h3>
|
||||
<nav className="space-y-3 sm:space-y-4">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.external ? "_blank" : undefined}
|
||||
rel={link.external ? "noopener noreferrer" : undefined}
|
||||
className="block font-sans font-normal text-sm sm:text-base lg:text-lg text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Half-Cut Plano Logo Background */}
|
||||
<div className="absolute bottom-0 left-0 right-0 overflow-hidden pointer-events-none">
|
||||
<div className="max-w-[81rem] mx-auto px-6 lg:px-[1px]">
|
||||
<div className="relative w-full flex justify-start">
|
||||
<Image
|
||||
src="/LogoOutline.svg"
|
||||
alt="Plano Logo"
|
||||
width={1800}
|
||||
height={200}
|
||||
className="w-150 h-auto opacity-30 select-none"
|
||||
style={{
|
||||
transform: "translateY(0%)", // Push logo down more while showing top part
|
||||
transformOrigin: "center bottom",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
17
packages/ui/src/components/Logo.tsx
Normal file
17
packages/ui/src/components/Logo.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{/* LogoMarkSquare SVG */}
|
||||
<Image
|
||||
src="/Logomark.svg"
|
||||
alt="Plano Logo"
|
||||
width={90}
|
||||
height={20}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
289
packages/ui/src/components/Navbar.tsx
Normal file
289
packages/ui/src/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Logo } from "./Logo";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "../lib/utils";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, Menu } from "lucide-react";
|
||||
|
||||
const navItems = [
|
||||
{ href: "https://docs.planoai.dev/get_started/quickstart", label: "start locally" },
|
||||
{ href: "https://docs.planoai.dev", label: "docs" },
|
||||
{ href: "/research", label: "research" },
|
||||
{ href: "/blog", label: "blog" },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isDarkBackground, setIsDarkBackground] = useState(false);
|
||||
|
||||
// Detect background color behind dropdown menu
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) {
|
||||
setIsDarkBackground(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const detectBackground = () => {
|
||||
// Small delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
const nav = document.querySelector("nav");
|
||||
if (!nav) return;
|
||||
|
||||
const navRect = nav.getBoundingClientRect();
|
||||
const dropdownBottom = navRect.bottom;
|
||||
const checkY = dropdownBottom + 20; // Just below the dropdown
|
||||
|
||||
// First, try to find section elements directly
|
||||
const main = document.querySelector("main");
|
||||
if (main) {
|
||||
const sections = main.querySelectorAll("section");
|
||||
let foundDarkSection = false;
|
||||
|
||||
sections.forEach((section) => {
|
||||
const rect = section.getBoundingClientRect();
|
||||
// Check if this section is visible below the navbar
|
||||
if (rect.top <= checkY && rect.bottom > checkY) {
|
||||
// Check for dark background classes
|
||||
const classList = Array.from(section.classList);
|
||||
const hasDarkBg = classList.some(
|
||||
(cls) =>
|
||||
cls.includes("bg-[#1a1a1a]") ||
|
||||
cls.includes("bg-black") ||
|
||||
cls.includes("bg-gray-900") ||
|
||||
cls.includes("bg-neutral-900") ||
|
||||
cls.includes("dark"),
|
||||
);
|
||||
|
||||
if (hasDarkBg) {
|
||||
foundDarkSection = true;
|
||||
setIsDarkBackground(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check computed background
|
||||
const computed = window.getComputedStyle(section);
|
||||
const bg = computed.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
const rgbMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const r = parseInt(rgbMatch[1]);
|
||||
const g = parseInt(rgbMatch[2]);
|
||||
const b = parseInt(rgbMatch[3]);
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
setIsDarkBackground(luminance < 0.5);
|
||||
foundDarkSection = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (foundDarkSection) return;
|
||||
}
|
||||
|
||||
// Fallback: Check element at point
|
||||
const centerX = window.innerWidth / 2;
|
||||
const elementBelow = document.elementFromPoint(centerX, checkY);
|
||||
|
||||
if (elementBelow) {
|
||||
let current: HTMLElement | null = elementBelow as HTMLElement;
|
||||
let backgroundColor = "";
|
||||
|
||||
// Walk up the DOM tree
|
||||
let levels = 0;
|
||||
while (
|
||||
current &&
|
||||
!backgroundColor &&
|
||||
current !== document.body &&
|
||||
levels < 15
|
||||
) {
|
||||
const computed = window.getComputedStyle(current);
|
||||
const bg = computed.backgroundColor;
|
||||
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
const rgbaMatch = bg.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
|
||||
);
|
||||
if (rgbaMatch) {
|
||||
const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
||||
if (alpha > 0.1) {
|
||||
backgroundColor = bg;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
backgroundColor = bg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
levels++;
|
||||
}
|
||||
|
||||
if (!backgroundColor) {
|
||||
const bodyBg = window.getComputedStyle(
|
||||
document.body,
|
||||
).backgroundColor;
|
||||
backgroundColor = bodyBg;
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
const rgbMatch = backgroundColor.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)/,
|
||||
);
|
||||
if (rgbMatch) {
|
||||
const r = parseInt(rgbMatch[1]);
|
||||
const g = parseInt(rgbMatch[2]);
|
||||
const b = parseInt(rgbMatch[3]);
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
setIsDarkBackground(luminance < 0.5);
|
||||
} else {
|
||||
const darkColors = [
|
||||
"black",
|
||||
"#000",
|
||||
"#000000",
|
||||
"rgb(0,0,0)",
|
||||
"rgba(0,0,0",
|
||||
"#1a1a1a",
|
||||
];
|
||||
const isDark = darkColors.some((color) =>
|
||||
backgroundColor.toLowerCase().includes(color.toLowerCase()),
|
||||
);
|
||||
setIsDarkBackground(isDark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Detect on open and on scroll
|
||||
detectBackground();
|
||||
const scrollHandler = () => detectBackground();
|
||||
const resizeHandler = () => detectBackground();
|
||||
|
||||
window.addEventListener("scroll", scrollHandler, { passive: true });
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", scrollHandler);
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
// Close menu when route changes
|
||||
const handleLinkClick = () => {
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="relative z-50 bg-gradient-to-b from-transparent to-white/5 backdrop-blur border-b border-neutral-200/5">
|
||||
<div className="max-w-[85rem] mx-auto px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center">
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links and CTA - Far Right */}
|
||||
<div className="hidden md:flex items-center justify-end gap-8">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"text-lg font-medium text-[var(--muted)]",
|
||||
"hover:text-[var(--primary)] transition-colors",
|
||||
"font-mono tracking-tighter",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
}}
|
||||
className="p-2 rounded-md text-[var(--muted)] hover:text-[var(--primary)] transition-colors"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isMenuOpen}
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isMenuOpen ? (
|
||||
<motion.div
|
||||
key="close"
|
||||
initial={{ opacity: 0, rotate: -90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="menu"
|
||||
initial={{ opacity: 0, rotate: 90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: -90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Dropdown Menu - Outside constrained container for full width */}
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="md:hidden overflow-hidden bg-[#7580DF]/70"
|
||||
>
|
||||
<div className="max-w-[85rem] mx-auto px-6 lg:px-8 py-3">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{navItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.href}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: index * 0.05,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={handleLinkClick}
|
||||
className={cn(
|
||||
"block px-0 py-1.5 border-b border-dashed transition-colors font-mono tracking-tighter",
|
||||
"text-sm font-medium",
|
||||
"text-white",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
65
packages/ui/src/components/ui/button.tsx
Normal file
65
packages/ui/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[7px] font-mono font-medium tracking-[-0.989px] transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-[#7780d9] border-[#4141b2] border-[1.562px] border-solid text-white hover:bg-[#7780d9]/90 text-base leading-[1.102]",
|
||||
primary:
|
||||
"bg-[#7780d9] border-[#4141b2] border-[1.562px] border-solid text-white hover:bg-[#7780d9]/90 text-base leading-[1.102]",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-[#edefff] border-[#d1d1d1] border-[1.562px] border-solid text-[#494949] hover:bg-[#edefff]/90 text-base leading-[1.102]",
|
||||
secondaryDark:
|
||||
"bg-neutral-600 border-[#d1d1d1]/20 border-[1.562px] border-solid text-white hover:bg-[#1a1a1a]/90 text-base leading-[1.102]",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-5 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-[7px] px-5 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
143
packages/ui/src/components/ui/dialog.tsx
Normal file
143
packages/ui/src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="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"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
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 leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue