From 2fdf771d4b6a94745d3bd4f6cbdf39a4aa2d448b Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 16 Jan 2026 23:07:10 +1300 Subject: [PATCH] feat(26-01): create detail modal flow for template selection - 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 --- src/components/quickstart/TemplateCard.tsx | 64 +---- .../quickstart/TemplateDetailModal.tsx | 224 ++++++++++++++++++ .../quickstart/TemplateExplorerView.tsx | 26 +- 3 files changed, 244 insertions(+), 70 deletions(-) create mode 100644 src/components/quickstart/TemplateDetailModal.tsx diff --git a/src/components/quickstart/TemplateCard.tsx b/src/components/quickstart/TemplateCard.tsx index 788d1b4d..7b0c79fe 100644 --- a/src/components/quickstart/TemplateCard.tsx +++ b/src/components/quickstart/TemplateCard.tsx @@ -1,6 +1,5 @@ "use client"; -import { Node, Edge } from "@xyflow/react"; import { TemplateCategory } from "@/types/quickstart"; interface TemplateCardProps { @@ -14,11 +13,9 @@ interface TemplateCardProps { }; nodeCount: number; previewImage?: string; - workflow?: { nodes: Node[]; edges: Edge[] }; - isLoading: boolean; + isLoading?: boolean; onClick: () => void; - onPreviewClick?: () => void; - disabled: boolean; + disabled?: boolean; } const CATEGORY_LABELS: Record = { @@ -39,16 +36,14 @@ export function TemplateCard({ template, nodeCount, previewImage, - workflow, - isLoading, + isLoading = false, onClick, - onPreviewClick, - disabled, + disabled = false, }: TemplateCardProps) { return ( ); } diff --git a/src/components/quickstart/TemplateDetailModal.tsx b/src/components/quickstart/TemplateDetailModal.tsx new file mode 100644 index 00000000..d16f0049 --- /dev/null +++ b/src/components/quickstart/TemplateDetailModal.tsx @@ -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 = { + product: "Product", + style: "Style", + composition: "Composition", + community: "Community", +}; + +const CATEGORY_COLORS: Record = { + 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 ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
e.stopPropagation()} + > + {/* Header */} +
+
+
+

+ {template.name} +

+ + {CATEGORY_LABELS[template.category]} + +
+

+ {template.description} +

+
+ +
+ + {/* Workflow Preview */} +
+
+ +
+
+ + {/* Footer */} +
+
+ {/* Metadata */} +
+
+ + + + {nodeCount} nodes +
+ + {/* Tags */} +
+ {template.tags.map((tag) => ( + + {tag} + + ))} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+
+ ); +} diff --git a/src/components/quickstart/TemplateExplorerView.tsx b/src/components/quickstart/TemplateExplorerView.tsx index a56e3f1b..deed8740 100644 --- a/src/components/quickstart/TemplateExplorerView.tsx +++ b/src/components/quickstart/TemplateExplorerView.tsx @@ -5,7 +5,7 @@ import { WorkflowFile } from "@/store/workflowStore"; import { getAllPresets, PRESET_TEMPLATES, getTemplateContent } from "@/lib/quickstart/templates"; import { QuickstartBackButton } from "./QuickstartBackButton"; import { TemplateCard } from "./TemplateCard"; -import { WorkflowPreviewModal } from "./WorkflowPreviewModal"; +import { TemplateDetailModal } from "./TemplateDetailModal"; import { CommunityWorkflowMeta, TemplateCategory, TemplateMetadata } from "@/types/quickstart"; interface TemplateExplorerViewProps { @@ -31,7 +31,7 @@ export function TemplateExplorerView({ const [isLoadingList, setIsLoadingList] = useState(true); const [loadingWorkflowId, setLoadingWorkflowId] = useState(null); const [error, setError] = useState(null); - const [previewTemplateId, setPreviewTemplateId] = useState(null); + const [selectedTemplateId, setSelectedTemplateId] = useState(null); // Filter state const [searchQuery, setSearchQuery] = useState(""); @@ -420,10 +420,7 @@ export function TemplateExplorerView({ template={preset} nodeCount={presetMetadata[preset.id]?.nodeCount ?? 0} previewImage={previewImages[preset.id]} - workflow={presetWorkflows[preset.id]} - isLoading={loadingWorkflowId === preset.id} - onClick={() => handlePresetSelect(preset.id)} - onPreviewClick={() => setPreviewTemplateId(preset.id)} + onClick={() => setSelectedTemplateId(preset.id)} disabled={isLoading} /> ))} @@ -597,13 +594,18 @@ export function TemplateExplorerView({
- {/* Workflow Preview Modal */} - {previewTemplateId && presetWorkflows[previewTemplateId] && ( - setPreviewTemplateId(null)} - templateName={presets.find((p) => p.id === previewTemplateId)?.name || "Template"} - workflow={presetWorkflows[previewTemplateId]} + onClose={() => setSelectedTemplateId(null)} + onUseWorkflow={() => { + handlePresetSelect(selectedTemplateId); + }} + isLoading={loadingWorkflowId === selectedTemplateId} + template={presets.find((p) => p.id === selectedTemplateId)!} + nodeCount={presetMetadata[selectedTemplateId]?.nodeCount ?? 0} + workflow={presetWorkflows[selectedTemplateId]} /> )}