Browse Source
- TemplateDetailModal: full details view with description, workflow preview, metadata, and "Use this workflow" button - TemplateCard: simplified - removed preview icon and tags row, card click now opens detail modal - TemplateExplorerView: wired up new flow where card opens detail modal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
3 changed files with 244 additions and 70 deletions
@ -0,0 +1,224 @@ |
|||
"use client"; |
|||
|
|||
import { useEffect, useCallback } from "react"; |
|||
import { Node, Edge } from "@xyflow/react"; |
|||
import { TemplateCategory } from "@/types/quickstart"; |
|||
import { WorkflowPreview } from "./WorkflowPreview"; |
|||
|
|||
interface TemplateDetailModalProps { |
|||
isOpen: boolean; |
|||
onClose: () => void; |
|||
onUseWorkflow: () => void; |
|||
isLoading: boolean; |
|||
template: { |
|||
id: string; |
|||
name: string; |
|||
description: string; |
|||
category: TemplateCategory; |
|||
tags: string[]; |
|||
}; |
|||
nodeCount: number; |
|||
workflow: { nodes: Node[]; edges: Edge[] }; |
|||
} |
|||
|
|||
const CATEGORY_LABELS: Record<TemplateCategory, string> = { |
|||
product: "Product", |
|||
style: "Style", |
|||
composition: "Composition", |
|||
community: "Community", |
|||
}; |
|||
|
|||
const CATEGORY_COLORS: Record<TemplateCategory, string> = { |
|||
product: "bg-blue-500/20 text-blue-300 border-blue-500/30", |
|||
style: "bg-purple-500/20 text-purple-300 border-purple-500/30", |
|||
composition: "bg-green-500/20 text-green-300 border-green-500/30", |
|||
community: "bg-amber-500/20 text-amber-300 border-amber-500/30", |
|||
}; |
|||
|
|||
export function TemplateDetailModal({ |
|||
isOpen, |
|||
onClose, |
|||
onUseWorkflow, |
|||
isLoading, |
|||
template, |
|||
nodeCount, |
|||
workflow, |
|||
}: TemplateDetailModalProps) { |
|||
// Handle escape key to close
|
|||
const handleKeyDown = useCallback( |
|||
(e: KeyboardEvent) => { |
|||
if (e.key === "Escape" && !isLoading) { |
|||
onClose(); |
|||
} |
|||
}, |
|||
[onClose, isLoading] |
|||
); |
|||
|
|||
useEffect(() => { |
|||
if (isOpen) { |
|||
document.addEventListener("keydown", handleKeyDown); |
|||
return () => document.removeEventListener("keydown", handleKeyDown); |
|||
} |
|||
}, [isOpen, handleKeyDown]); |
|||
|
|||
if (!isOpen) return null; |
|||
|
|||
return ( |
|||
<div |
|||
className="fixed inset-0 z-50 flex items-center justify-center p-4" |
|||
onClick={isLoading ? undefined : onClose} |
|||
> |
|||
{/* Backdrop */} |
|||
<div className="absolute inset-0 bg-black/70" /> |
|||
|
|||
{/* Modal */} |
|||
<div |
|||
className="relative bg-neutral-900 border border-neutral-700 rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden" |
|||
onClick={(e) => e.stopPropagation()} |
|||
> |
|||
{/* Header */} |
|||
<div className="flex items-start justify-between px-6 py-5 border-b border-neutral-700"> |
|||
<div className="flex-1 pr-4"> |
|||
<div className="flex items-center gap-3 mb-2"> |
|||
<h2 className="text-xl font-semibold text-neutral-100"> |
|||
{template.name} |
|||
</h2> |
|||
<span |
|||
className={` |
|||
inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border |
|||
${CATEGORY_COLORS[template.category]} |
|||
`}
|
|||
> |
|||
{CATEGORY_LABELS[template.category]} |
|||
</span> |
|||
</div> |
|||
<p className="text-sm text-neutral-400 leading-relaxed"> |
|||
{template.description} |
|||
</p> |
|||
</div> |
|||
<button |
|||
onClick={onClose} |
|||
disabled={isLoading} |
|||
className="p-1.5 rounded-md text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 transition-colors disabled:opacity-50" |
|||
> |
|||
<svg |
|||
className="w-5 h-5" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
strokeWidth={2} |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
d="M6 18L18 6M6 6l12 12" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
</div> |
|||
|
|||
{/* Workflow Preview */} |
|||
<div className="flex-1 min-h-0 p-6 bg-neutral-950/50"> |
|||
<div className="w-full h-[50vh] bg-neutral-900 rounded-lg border border-neutral-800 overflow-hidden"> |
|||
<WorkflowPreview workflow={workflow} /> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Footer */} |
|||
<div className="px-6 py-4 border-t border-neutral-700 bg-neutral-900"> |
|||
<div className="flex items-center justify-between"> |
|||
{/* Metadata */} |
|||
<div className="flex items-center gap-4"> |
|||
<div className="flex items-center gap-2 text-sm text-neutral-400"> |
|||
<svg |
|||
className="w-4 h-4" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
strokeWidth={1.5} |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" |
|||
/> |
|||
</svg> |
|||
<span>{nodeCount} nodes</span> |
|||
</div> |
|||
|
|||
{/* Tags */} |
|||
<div className="flex items-center gap-1.5"> |
|||
{template.tags.map((tag) => ( |
|||
<span |
|||
key={tag} |
|||
className="px-2 py-0.5 rounded text-xs bg-neutral-800 text-neutral-500" |
|||
> |
|||
{tag} |
|||
</span> |
|||
))} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Actions */} |
|||
<div className="flex items-center gap-3"> |
|||
<button |
|||
onClick={onClose} |
|||
disabled={isLoading} |
|||
className="px-4 py-2 text-sm font-medium text-neutral-400 hover:text-neutral-200 transition-colors disabled:opacity-50" |
|||
> |
|||
Cancel |
|||
</button> |
|||
<button |
|||
onClick={onUseWorkflow} |
|||
disabled={isLoading} |
|||
className="px-5 py-2 text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" |
|||
> |
|||
{isLoading ? ( |
|||
<> |
|||
<svg |
|||
className="w-4 h-4 animate-spin" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
> |
|||
<circle |
|||
className="opacity-25" |
|||
cx="12" |
|||
cy="12" |
|||
r="10" |
|||
stroke="currentColor" |
|||
strokeWidth="4" |
|||
/> |
|||
<path |
|||
className="opacity-75" |
|||
fill="currentColor" |
|||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
|||
/> |
|||
</svg> |
|||
Loading... |
|||
</> |
|||
) : ( |
|||
<> |
|||
Use this workflow |
|||
<svg |
|||
className="w-4 h-4" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
strokeWidth={2} |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" |
|||
/> |
|||
</svg> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
Loading…
Reference in new issue