Refine popup and controls

This commit is contained in:
Anmoldeep Singh
2026-04-18 11:59:22 +05:30
parent c55f888520
commit be3717e845
22 changed files with 171 additions and 80 deletions

92
AGENTS.md Normal file
View File

@@ -0,0 +1,92 @@
# AGENTS.md
## Project Overview
**mapcn** is a free, open-source library of ready-to-use, customizable map components for React. Built on [MapLibre GL](https://maplibre.org/), styled with [Tailwind CSS](https://tailwindcss.com/), works seamlessly with [shadcn/ui](https://ui.shadcn.com/).
The repo serves dual purpose: the core map component registry (`src/registry/`) and the documentation site at [mapcn.dev](https://mapcn.dev).
## Tech Stack
- **Framework**: Next.js 16 (App Router, React 19)
- **Language**: TypeScript (strict)
- **Styling**: Tailwind CSS v4, shadcn/ui, Radix UI
- **Map Engine**: MapLibre GL JS v5
- **Fonts**: Geist (sans) + Geist Mono
- **Code Highlighting**: Shiki (dual light/dark theme)
- **Registry**: shadcn CLI for building distributable components
## Project Structure
```
src/
├── app/
│ ├── layout.tsx # Root layout (fonts, metadata, providers)
│ ├── (main)/ # Main layout group (header + content)
│ │ ├── layout.tsx # Header wrapper
│ │ ├── (home)/ # Landing page
│ │ │ └── _components/ # Homepage examples grid
│ │ ├── docs/ # Documentation pages
│ │ │ └── _components/ # Docs layout, sidebar, TOC, code blocks
│ │ └── blocks/ # Full-page block demos
│ └── (view)/ # Standalone block viewer
├── components/ # Shared app components (header, footer, logo, nav)
│ └── ui/ # shadcn/ui primitives
├── registry/ # Core map components (the library itself)
│ ├── map.tsx # Main map component + all sub-components
│ └── blocks/ # Full-page block examples
├── lib/ # Utilities, navigation config, helpers
└── styles/
└── globals.css # Theme tokens, animations, base styles
```
## Key Conventions
### Components
- Use functional components with named exports
- Prefer React Server Components; use `"use client"` only when Web APIs are needed
- Structure files: exported component → subcomponents → helpers → static content → types
- Use `cn()` from `@/lib/utils` for conditional class merging
### Styling
- Tailwind utility classes only — no CSS modules, no styled-components
- Mobile-first responsive design
- Theme tokens defined as CSS custom properties in `globals.css` (oklch color space)
- No hardcoded colors — always use semantic tokens (`foreground`, `muted-foreground`, `border`, etc.)
### Registry (`src/registry/`)
- `map.tsx` is the single-file component library containing Map, Marker, Popup, Route, Controls, etc.
- Blocks live in `src/registry/blocks/` — each block is a self-contained page component
- Registry is built via `npm run registry:build` using the shadcn CLI
### Naming
- Directories: lowercase with dashes (`docs-sidebar`, `block-preview`)
- Components: PascalCase exports (`DocsLayout`, `PageHeader`)
- Files: kebab-case (`command-search.tsx`, `page-header.tsx`)
### Navigation
- All site navigation is centralized in `src/lib/site-navigation.ts`
- Docs sidebar nav uses `docsNavigation`, command search uses `siteNavigation`
## Design System
- Color palette is monochrome (grayscale oklch)
- Radius: `0.625rem` base with computed variants
- Animations: `fade-up`, `fade-in`, `scale-in` with staggered delays (100ms intervals)
- Header: sticky, backdrop-blur, gradient bottom border
- Footer: gradient top border matching header
## Commands
```bash
npm run dev # Start dev server
npm run build # Production build
npm run lint # ESLint
npm run registry:build # Build distributable registry to public/r/
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -46,7 +46,6 @@ export function DeliveryExample() {
return ( return (
<ExampleCard <ExampleCard
label="Delivery"
className="aspect-square sm:col-span-2 sm:aspect-video lg:aspect-auto" className="aspect-square sm:col-span-2 sm:aspect-video lg:aspect-auto"
stagger={9} stagger={9}
> >

View File

@@ -117,7 +117,7 @@ const statusConfig: Record<
export function EVChargingExample() { export function EVChargingExample() {
return ( return (
<ExampleCard label="EV Charging" className="aspect-square" stagger={7}> <ExampleCard className="aspect-square" stagger={7}>
<Map center={[-122.434, 37.776]} zoom={11}> <Map center={[-122.434, 37.776]} zoom={11}>
{stations.map((station) => { {stations.map((station) => {
const config = statusConfig[station.status]; const config = statusConfig[station.status];

View File

@@ -4,14 +4,12 @@ import { CSSProperties } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface ExampleCardProps { interface ExampleCardProps {
label?: string;
className?: string; className?: string;
stagger?: number; stagger?: number;
children: React.ReactNode; children: React.ReactNode;
} }
export function ExampleCard({ export function ExampleCard({
label,
className, className,
stagger = 5, stagger = 5,
children, children,
@@ -28,11 +26,6 @@ export function ExampleCard({
} as CSSProperties } as CSSProperties
} }
> >
{label && (
<div className="text-muted-foreground bg-background/90 absolute top-2 left-2 z-10 rounded px-2 py-1 text-[10px] tracking-wider uppercase backdrop-blur-sm">
{label}
</div>
)}
{children} {children}
</div> </div>
); );

View File

@@ -23,10 +23,10 @@ export function FlyToExample() {
const mapRef = useRef<MapRef>(null); const mapRef = useRef<MapRef>(null);
return ( return (
<ExampleCard label="Fly To" className="aspect-square" stagger={6}> <ExampleCard className="aspect-square" stagger={6}>
<Map <Map
center={destination.startCenter} center={destination.startCenter}
zoom={0.5} zoom={0.6}
ref={mapRef} ref={mapRef}
projection={{ type: "globe" }} projection={{ type: "globe" }}
> >

View File

@@ -6,7 +6,7 @@ import { ExampleCard } from "./example-card";
export function TrendingExample() { export function TrendingExample() {
return ( return (
<ExampleCard label="Trending" className="aspect-square" stagger={8}> <ExampleCard className="aspect-square" stagger={8}>
<Map center={[-73.99, 40.735]} zoom={10}> <Map center={[-73.99, 40.735]} zoom={10}>
<MapMarker longitude={-73.9857} latitude={40.7484}> <MapMarker longitude={-73.9857} latitude={40.7484}>
<MarkerContent> <MarkerContent>

View File

@@ -58,7 +58,7 @@ export function DocsToc({ items, className }: DocsTocProps) {
return ( return (
<nav aria-label="On this page" className={cn("flex flex-col", className)}> <nav aria-label="On this page" className={cn("flex flex-col", className)}>
<p className="text-sidebar-foreground/70 mb-3 text-[13px] font-medium tracking-tight"> <p className="text-sidebar-foreground/70 mb-3 text-xs font-medium">
On This Page On This Page
</p> </p>
@@ -72,7 +72,7 @@ export function DocsToc({ items, className }: DocsTocProps) {
className={cn( className={cn(
"py-1 text-sm no-underline transition-colors", "py-1 text-sm no-underline transition-colors",
isActive isActive
? "text-foreground" ? "text-foreground font-medium"
: "text-muted-foreground hover:text-foreground", : "text-muted-foreground hover:text-foreground",
)} )}
> >

View File

@@ -98,8 +98,8 @@ export function DocsLayout({
)} )}
</div> </div>
<aside className="hidden w-42 shrink-0 xl:block"> <aside className="hidden w-44 shrink-0 xl:block">
<nav className="sticky top-24 max-h-[calc(100svh-7rem)] overflow-y-auto pr-1"> <nav className="sticky top-14 max-h-[calc(100svh-3.5rem)] overflow-y-auto pt-10 pb-10 [scrollbar-gutter:stable]">
{toc.length > 0 && <DocsToc items={toc} />} {toc.length > 0 && <DocsToc items={toc} />}
</nav> </nav>
</aside> </aside>
@@ -122,7 +122,7 @@ export function DocsSection({ title, children }: DocsSectionProps) {
{title} {title}
</h2> </h2>
)} )}
<div className="text-primary [&_strong]:text-foreground [&_em]:text-muted-foreground space-y-4 text-base leading-7 [&_li]:leading-7 [&_ol]:list-decimal [&_ol]:space-y-2 [&_ol]:pl-5 [&_p]:leading-7 [&_strong]:font-medium [&_ul]:list-disc [&_ul]:space-y-2 [&_ul]:pl-5"> <div className="text-primary [&_strong]:text-foreground [&_em]:text-muted-foreground space-y-4 text-base leading-7 [&_strong]:font-medium [&>ol]:list-decimal [&>ol]:space-y-2 [&>ol]:pl-5 [&>ol>li]:leading-7 [&>p]:leading-7 [&>ul]:list-disc [&>ul]:space-y-2 [&>ul]:pl-5 [&>ul>li]:leading-7">
{children} {children}
</div> </div>
</section> </section>
@@ -173,7 +173,7 @@ export function DocsCode({
return ( return (
<code <code
className={cn( className={cn(
"bg-muted relative rounded-md px-2 py-1 font-mono text-sm", "bg-muted relative rounded-md px-1.5 py-0.5 font-mono text-sm",
className, className,
)} )}
> >

View File

@@ -41,14 +41,20 @@ export default function ClusterExample() {
closeOnClick={false} closeOnClick={false}
focusAfterOpen={false} focusAfterOpen={false}
closeButton closeButton
className="w-34"
> >
<div className="space-y-1 p-1"> <div className="text-[13px]">
<p className="text-sm"> <p className="text-muted-foreground">
Magnitude: {selectedPoint.properties.mag} Magnitude:{" "}
<span className="text-foreground font-medium">
{selectedPoint.properties.mag}
</span>
</p> </p>
<p className="text-sm"> <p className="text-muted-foreground">
Tsunami:{" "} Tsunami:{" "}
{selectedPoint.properties?.tsunami === 1 ? "Yes" : "No"} <span className="text-foreground font-medium">
{selectedPoint.properties?.tsunami === 1 ? "Yes" : "No"}
</span>
</p> </p>
</div> </div>
</MapPopup> </MapPopup>

View File

@@ -17,7 +17,7 @@ export function DraggableMarkerExample() {
draggable draggable
longitude={draggableMarker.lng} longitude={draggableMarker.lng}
latitude={draggableMarker.lat} latitude={draggableMarker.lat}
onDragEnd={(lngLat) => { onDrag={(lngLat) => {
setDraggableMarker({ lng: lngLat.lng, lat: lngLat.lat }); setDraggableMarker({ lng: lngLat.lng, lat: lngLat.lat });
}} }}
> >
@@ -32,7 +32,7 @@ export function DraggableMarkerExample() {
<MarkerPopup> <MarkerPopup>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-foreground font-medium">Coordinates</p> <p className="text-foreground font-medium">Coordinates</p>
<p className="text-muted-foreground text-xs"> <p className="text-muted-foreground text-xs tabular-nums">
{draggableMarker.lat.toFixed(4)},{" "} {draggableMarker.lat.toFixed(4)},{" "}
{draggableMarker.lng.toFixed(4)} {draggableMarker.lng.toFixed(4)}
</p> </p>

View File

@@ -132,7 +132,7 @@ function MarkersLayer() {
offset={10} offset={10}
closeButton closeButton
> >
<div className="min-w-[140px]"> <div className="min-w-24">
<p className="font-medium">{selectedPoint.name}</p> <p className="font-medium">{selectedPoint.name}</p>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{selectedPoint.category} {selectedPoint.category}

View File

@@ -3,9 +3,9 @@ import { Map, MapControls } from "@/registry/map";
export function MapControlsExample() { export function MapControlsExample() {
return ( return (
<div className="h-[420px] w-full"> <div className="h-[420px] w-full">
<Map center={[2.3522, 48.8566]} zoom={11}> <Map center={[2.3522, 48.8566]} zoom={8.5}>
<MapControls <MapControls
position="bottom-right" position="top-right"
showZoom showZoom
showCompass showCompass
showLocate showLocate

View File

@@ -58,10 +58,10 @@ export function PopupExample() {
{places.map((place) => ( {places.map((place) => (
<MapMarker key={place.id} longitude={place.lng} latitude={place.lat}> <MapMarker key={place.id} longitude={place.lng} latitude={place.lat}>
<MarkerContent> <MarkerContent>
<div className="size-5 rounded-full bg-rose-500 border-2 border-white shadow-lg cursor-pointer hover:scale-110 transition-transform" /> <div className="size-5 cursor-pointer rounded-full border-2 border-white bg-rose-500 shadow-lg transition-transform hover:scale-110" />
<MarkerLabel position="bottom">{place.label}</MarkerLabel> <MarkerLabel position="bottom">{place.label}</MarkerLabel>
</MarkerContent> </MarkerContent>
<MarkerPopup className="p-0 w-62"> <MarkerPopup className="w-62 p-0">
<div className="relative h-32 overflow-hidden rounded-t-md"> <div className="relative h-32 overflow-hidden rounded-t-md">
<Image <Image
fill fill
@@ -72,10 +72,10 @@ export function PopupExample() {
</div> </div>
<div className="space-y-2 p-3"> <div className="space-y-2 p-3">
<div> <div>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <p className="text-muted-foreground pb-0.5 text-[11px] font-medium tracking-wide uppercase">
{place.category} {place.category}
</span> </p>
<h3 className="font-semibold text-foreground leading-tight"> <h3 className="text-foreground leading-tight font-semibold">
{place.name} {place.name}
</h3> </h3>
</div> </div>
@@ -88,16 +88,16 @@ export function PopupExample() {
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-1.5 text-sm text-muted-foreground"> <div className="text-muted-foreground flex items-center gap-1.5 text-sm">
<Clock className="size-3.5" /> <Clock className="size-3.5" />
<span>{place.hours}</span> <span>{place.hours}</span>
</div> </div>
<div className="flex gap-2 pt-1"> <div className="flex gap-2 pt-1">
<Button size="sm" className="flex-1 h-8"> <Button size="sm" className="flex-1">
<Navigation className="size-3.5 mr-1.5" /> <Navigation className="size-3.5" />
Directions Directions
</Button> </Button>
<Button size="sm" variant="outline" className="h-8"> <Button size="icon-sm" variant="outline">
<ExternalLink className="size-3.5" /> <ExternalLink className="size-3.5" />
</Button> </Button>
</div> </div>

View File

@@ -18,7 +18,6 @@ export function StandalonePopupExample() {
closeButton closeButton
focusAfterOpen={false} focusAfterOpen={false}
closeOnClick={false} closeOnClick={false}
className="w-62"
> >
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-foreground font-semibold">New York City</h3> <h3 className="text-foreground font-semibold">New York City</h3>

View File

@@ -166,13 +166,15 @@ export default function IntroductionPage() {
{features.map((feature) => ( {features.map((feature) => (
<div <div
key={feature.title} key={feature.title}
className="bg-card space-y-2 rounded-lg border p-4" className="bg-card space-y-3 rounded-lg border p-4"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-primary/10 flex size-8 items-center justify-center rounded-md"> <div className="bg-muted flex size-8 items-center justify-center rounded-md">
<feature.icon className="text-primary size-4" /> <feature.icon className="text-foreground size-4" />
</div> </div>
<h3 className="text-foreground font-medium">{feature.title}</h3> <h3 className="text-foreground text-base font-medium">
{feature.title}
</h3>
</div> </div>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{feature.description} {feature.description}

View File

@@ -1,5 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { Logo } from "./logo"; import { Logo } from "./logo";
import { cn } from "@/lib/utils";
const footerLinks = { const footerLinks = {
product: [ product: [
@@ -30,9 +31,9 @@ const footerLinks = {
], ],
}; };
export function Footer() { export function Footer({ className }: { className?: string }) {
return ( return (
<footer className="mt-24 border-t md:mt-32"> <footer className={cn("mt-30 border-t", className)}>
<div className="container py-12 md:py-16"> <div className="container py-12 md:py-16">
<div className="grid grid-cols-2 gap-8 md:grid-cols-4"> <div className="grid grid-cols-2 gap-8 md:grid-cols-4">
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">

View File

@@ -19,7 +19,7 @@ export function Header({ className }: { className?: string }) {
<MobileNav /> <MobileNav />
<Logo className="hidden shrink-0 lg:flex" /> <Logo className="hidden shrink-0 lg:flex" />
<Separator <Separator
className="ml-2.5 hidden h-4! w-px lg:block" className="ml-2.5 hidden h-4! lg:block"
orientation="vertical" orientation="vertical"
/> />
<MainNav className="hidden lg:flex" /> <MainNav className="hidden lg:flex" />

View File

@@ -1,8 +1,8 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -39,7 +39,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( 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", "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}
@@ -61,7 +61,7 @@ function DialogContent({
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( 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 outline-none sm:max-w-lg", "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 bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
className className
)} )}
{...props} {...props}
@@ -70,7 +70,7 @@ function DialogContent({
{showCloseButton && ( {showCloseButton && (
<DialogPrimitive.Close <DialogPrimitive.Close
data-slot="dialog-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" className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
> >
<XIcon /> <XIcon />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
@@ -138,7 +138,7 @@ function DialogDescription({
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
) )

View File

@@ -106,7 +106,7 @@ export default function Page() {
return ( return (
<div className="p-8"> <div className="p-8">
<div className="bg-sidebar mx-auto grid max-w-7xl rounded-lg border md:h-[600px] md:grid-cols-[1.05fr_1fr]"> <div className="bg-sidebar mx-auto grid max-w-7xl rounded-xl border md:h-[600px] md:grid-cols-[1.05fr_1fr]">
<div className="flex flex-col p-5 md:p-6"> <div className="flex flex-col p-5 md:p-6">
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-2xl font-semibold tracking-tight"> <h3 className="text-2xl font-semibold tracking-tight">

View File

@@ -518,6 +518,19 @@ function DefaultMarkerIcon() {
); );
} }
function PopupCloseButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
aria-label="Close popup"
className="focus-visible:ring-ring hover:bg-muted text-foreground absolute top-0.5 right-0.5 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm transition-colors focus:outline-none focus-visible:ring-2"
>
<X className="size-3.5" />
</button>
);
}
type MarkerPopupProps = { type MarkerPopupProps = {
/** Popup content */ /** Popup content */
children: ReactNode; children: ReactNode;
@@ -580,21 +593,12 @@ function MarkerPopup({
return createPortal( return createPortal(
<div <div
className={cn( className={cn(
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 relative rounded-md border p-3 shadow-md", "bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md",
"animate-in fade-in-0 zoom-in-95 duration-200 ease-out",
className, className,
)} )}
> >
{closeButton && ( {closeButton && <PopupCloseButton onClick={handleClose} />}
<button
type="button"
onClick={handleClose}
className="ring-offset-background focus:ring-ring absolute top-1 right-1 z-10 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
aria-label="Close popup"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
)}
{children} {children}
</div>, </div>,
container, container,
@@ -666,7 +670,8 @@ function MarkerTooltip({
return createPortal( return createPortal(
<div <div
className={cn( className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 rounded-md px-2 py-1 text-xs shadow-md", "bg-foreground text-background pointer-events-none rounded-md px-2 py-1 text-xs text-balance shadow-md",
"animate-in fade-in-0 zoom-in-95 duration-200 ease-out",
className, className,
)} )}
> >
@@ -758,8 +763,11 @@ function ControlButton({
aria-label={label} aria-label={label}
type="button" type="button"
className={cn( className={cn(
"hover:bg-accent dark:hover:bg-accent/40 flex size-8 items-center justify-center transition-colors", "flex size-8 items-center justify-center transition-all",
disabled && "pointer-events-none cursor-not-allowed opacity-50", "first:rounded-t-md last:rounded-b-md",
"hover:bg-accent dark:hover:bg-accent/40",
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset",
"disabled:pointer-events-none disabled:opacity-50",
)} )}
disabled={disabled} disabled={disabled}
> >
@@ -1006,21 +1014,12 @@ function MapPopup({
return createPortal( return createPortal(
<div <div
className={cn( className={cn(
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 relative rounded-md border p-3 shadow-md", "bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md",
"animate-in fade-in-0 zoom-in-95 duration-200 ease-out",
className, className,
)} )}
> >
{closeButton && ( {closeButton && <PopupCloseButton onClick={handleClose} />}
<button
type="button"
onClick={handleClose}
className="ring-offset-background focus:ring-ring absolute top-1 right-1 z-10 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
aria-label="Close popup"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
)}
{children} {children}
</div>, </div>,
container, container,