"use client"; import { PrototypeComponent } from "@prototype/components/prototypes/prototype-target"; import { Button } from "@prototype/components/ui/button"; import { getPrototypePortalContainer } from "@prototype/lib/tool-portal"; import { cn } from "@prototype/lib/utils"; import type { MobbinReference } from "@prototype/lib/prototypes/design-exploration-types"; import { ChevronLeft, ChevronRight, ExternalLink, X, ZoomIn } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; /** Above prototype preview + design brief dialog (z-50), below review chrome (1050). */ const MOBBIN_LIGHTBOX_Z_INDEX = 1040; const GALLERY_SECTION_TITLE_CLASS = "tool-chip tool-chip-pink"; const GALLERY_SECTION_DESC_CLASS = "tool-body max-w-2xl"; const GALLERY_CARD_TITLE_CLASS = "text-sm font-medium text-foreground"; const GALLERY_CARD_BODY_CLASS = "tool-body"; type PrototypeMobbinGalleryLayout = "default" | "sidebar"; type PrototypeMobbinGalleryProps = { componentIdPrefix: string; galleryId: string; references: MobbinReference[]; title?: string; description?: string; imagePathForReference?: (id: string) => string; layout?: PrototypeMobbinGalleryLayout; }; function resolveReferenceImageUrl( reference: MobbinReference, imagePathForReference?: (id: string) => string, ) { return imagePathForReference?.(reference.id) ?? reference.imageUrl; } function MobbinReferenceCard({ reference, imageUrl, componentId, onOpen, compact = false, }: { reference: MobbinReference; imageUrl: string; componentId: string; onOpen: () => void; compact?: boolean; }) { const cardTitleClass = compact ? "text-sm text-foreground font-medium" : GALLERY_CARD_TITLE_CLASS; const cardBodyClass = compact ? "text-sm text-muted-foreground leading-relaxed" : GALLERY_CARD_BODY_CLASS; return (

{reference.appName}

{reference.variantHint ? ( {reference.variantHint} ) : null}

{reference.relevance}

); } function MobbinReferenceLightbox({ references, imageUrls, activeIndex, onClose, onNavigate, galleryId, }: { references: MobbinReference[]; imageUrls: string[]; activeIndex: number; onClose: () => void; onNavigate: (index: number) => void; galleryId: string; }) { const reference = references[activeIndex]; const imageUrl = imageUrls[activeIndex]; const total = references.length; const hasPrev = activeIndex > 0; const hasNext = activeIndex < total - 1; const goPrev = useCallback(() => { if (hasPrev) onNavigate(activeIndex - 1); }, [activeIndex, hasPrev, onNavigate]); const goNext = useCallback(() => { if (hasNext) onNavigate(activeIndex + 1); }, [activeIndex, hasNext, onNavigate]); useEffect(() => { function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { onClose(); } else if (event.key === "ArrowLeft") { goPrev(); } else if (event.key === "ArrowRight") { goNext(); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [goNext, goPrev, onClose]); const [portalTarget, setPortalTarget] = useState(null); useEffect(() => { setPortalTarget(getPrototypePortalContainer() ?? null); }, []); useEffect(() => { const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previousOverflow; }; }, []); const lightbox = (
{hasPrev ? ( ) : null}
{/* eslint-disable-next-line @next/next/no-img-element -- Mobbin CDN URLs are ephemeral; local assets preferred when available. */} {`${reference.appName}
{hasNext ? ( ) : null}

{reference.appName}

{reference.variantHint ? ( {reference.variantHint} ) : null}

{reference.relevance}

View on Mobbin
); if (!portalTarget) return null; return createPortal(lightbox, portalTarget); } export function PrototypeMobbinGallery({ componentIdPrefix, galleryId, references, title = "Mobbin references", description = "Reference screens from B2B and developer dashboards.", imagePathForReference, layout = "default", }: PrototypeMobbinGalleryProps) { const isSidebar = layout === "sidebar"; const [activeIndex, setActiveIndex] = useState(null); const imageUrls = references.map((reference) => resolveReferenceImageUrl(reference, imagePathForReference), ); const sectionTitleClass = isSidebar ? "text-xs font-medium text-muted-foreground uppercase tracking-wide" : GALLERY_SECTION_TITLE_CLASS; const sectionDescClass = isSidebar ? "text-sm text-muted-foreground leading-relaxed" : GALLERY_SECTION_DESC_CLASS; return (

{title}

{!isSidebar &&

{description}

}
1 && "lg:grid-cols-2 lg:gap-x-8 lg:gap-y-12", )} > {references.map((reference, index) => ( setActiveIndex(index)} compact={isSidebar} /> ))}
{activeIndex !== null ? ( setActiveIndex(null)} onNavigate={setActiveIndex} galleryId={galleryId} /> ) : null}
); }