import { useEffect, useState } from 'react'; import { useT } from '../i18n'; import { fetchPromptTemplate } from '../providers/registry'; import type { PromptTemplateDetail, PromptTemplateSummary, } from '../types'; import { Icon } from './Icon'; interface Props { summary: PromptTemplateSummary; onClose: () => void; } // Modal preview for a curated prompt template. The summary payload from // /api/prompt-templates carries enough to render the header (title, // description, category, tags, attribution) and the preview asset; the // prompt body is fetched lazily so the gallery list stays cheap. export function PromptTemplatePreviewModal({ summary, onClose }: Props) { const t = useT(); const [detail, setDetail] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); // Immersive fullscreen preview state. Layered ABOVE the modal so the // user can dive into the asset without losing the prompt context they // came from — closing the lightbox restores the modal underneath. const [lightboxOpen, setLightboxOpen] = useState(false); useEffect(() => { let cancelled = false; setDetail(null); setError(null); setCopied(false); setLightboxOpen(false); void fetchPromptTemplate(summary.surface, summary.id).then((d) => { if (cancelled) return; if (!d) { setError(t('promptTemplates.fetchError')); return; } setDetail(d); }); return () => { cancelled = true; }; }, [summary.id, summary.surface, t]); // Close on Escape — when the lightbox is open, ESC closes only the // lightbox (preserving the modal beneath); otherwise it closes the // modal itself. Mirrors the design-system preview modal's pattern so // the two gallery views feel consistent. useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key !== 'Escape') return; if (lightboxOpen) { setLightboxOpen(false); return; } onClose(); } document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [onClose, lightboxOpen]); function handleCopy() { if (!detail) return; void navigator.clipboard.writeText(detail.prompt).then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 2000); }); } const sourceLabel = summary.source.author ? `${summary.source.author} · ${summary.source.repo}` : summary.source.repo; const hasAsset = !!(summary.previewVideoUrl || summary.previewImageUrl); const fullscreenLabel = t('promptTemplates.openFullscreen'); const closeFullscreenLabel = t('promptTemplates.closeFullscreen'); return ( <>
{ if (e.target === e.currentTarget) onClose(); }} >

{summary.title}

{summary.summary}

{summary.category} {(summary.tags ?? []).map((tag) => ( {tag} ))} {summary.model ? ( {t('promptTemplates.modelHint', { model: summary.model })} ) : null} {summary.aspect ? ( {summary.aspect} ) : null}
{hasAsset ? (
{summary.previewVideoUrl ? (
) : null}
{t('promptTemplates.promptLabel')}
              {detail
                ? detail.prompt
                : error
                  ? error
                  : t('common.loading')}
            
{lightboxOpen && hasAsset ? ( // Immersive lightbox — full viewport, dark backdrop, centered // media. Rendered as a sibling of the modal backdrop so its // backdrop click is independent (clicking the lightbox backdrop // closes only the lightbox, not the modal beneath).
{ if (e.target === e.currentTarget) setLightboxOpen(false); }} > {summary.previewVideoUrl ? (
) : null} ); }