@@ -14,7 +14,6 @@ import { Skeleton } from '@/components/ui/skeleton';
1414import {
1515 Eye ,
1616 Filter ,
17- Package ,
1817 RefreshCw ,
1918 Search ,
2019 Star ,
@@ -29,7 +28,6 @@ import {
2928 Database ,
3029 Link ,
3130 TestTube2 ,
32- FileText ,
3331 MoreHorizontal ,
3432 AlertTriangle ,
3533 Layers ,
@@ -53,6 +51,13 @@ import { track, Events } from '@/features/analytics/events';
5351import { UseTemplateModal } from '@/features/templates/UseTemplateModal' ;
5452import { WorkflowPreview } from '@/features/templates/WorkflowPreview' ;
5553import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from '@/components/ui/tooltip' ;
54+ import {
55+ Dialog ,
56+ DialogContent ,
57+ DialogHeader ,
58+ DialogTitle ,
59+ DialogDescription ,
60+ } from '@/components/ui/dialog' ;
5661import { cn } from '@/lib/utils' ;
5762
5863// ---------------------------------------------------------------------------
@@ -344,24 +349,26 @@ function PreviewSection({ graph }: { graph?: Record<string, unknown>; category?:
344349interface TemplateCardProps {
345350 template : Template ;
346351 onUse : ( template : Template ) => void ;
352+ onPreview : ( template : Template ) => void ;
347353 canUse : boolean ;
348354}
349355
350- function TemplateCard ( { template, onUse, canUse } : TemplateCardProps ) {
356+ function TemplateCard ( { template, onUse, onPreview , canUse } : TemplateCardProps ) {
351357 const catStyle = getCategoryStyle ( template . category ) ;
352358 const CategoryIcon = catStyle . icon ;
353359
354360 return (
355361 < div
356362 className = { cn (
357- 'group flex flex-col rounded-2xl' ,
363+ 'group flex flex-col rounded-2xl cursor-pointer ' ,
358364 'bg-white dark:bg-zinc-900' ,
359365 'border border-gray-100 dark:border-white/5' ,
360366 'shadow-sm' ,
361367 'transition-all duration-300 ease-out' ,
362368 'hover:shadow-lg hover:-translate-y-1' ,
363369 'dark:hover:border-white/10' ,
364370 ) }
371+ onClick = { ( ) => onPreview ( template ) }
365372 >
366373 { /* Content wrapper with padding */ }
367374 < div className = "flex flex-col flex-1 p-5 md:p-6 gap-6" >
@@ -492,7 +499,10 @@ function TemplateCard({ template, onUse, canUse }: TemplateCardProps) {
492499 'flex-1 h-11 rounded-xl font-medium gap-2' ,
493500 'active:scale-[0.98] transition-all duration-200' ,
494501 ) }
495- onClick = { ( ) => onUse ( template ) }
502+ onClick = { ( e ) => {
503+ e . stopPropagation ( ) ;
504+ onUse ( template ) ;
505+ } }
496506 disabled = { ! canUse }
497507 >
498508 Use Template
@@ -516,6 +526,7 @@ function TemplateCard({ template, onUse, canUse }: TemplateCardProps) {
516526 href = { `https://github.com/${ template . repository } /blob/${ template . branch || 'main' } /${ template . path } ` }
517527 target = "_blank"
518528 rel = "noopener noreferrer"
529+ onClick = { ( e ) => e . stopPropagation ( ) }
519530 >
520531 < Eye className = "h-4 w-4" />
521532 </ a >
@@ -594,6 +605,121 @@ function EmptyState({ hasFilters }: { hasFilters: boolean }) {
594605 ) ;
595606}
596607
608+ // ---------------------------------------------------------------------------
609+ // Template detail modal
610+ // ---------------------------------------------------------------------------
611+
612+ interface TemplateDetailModalProps {
613+ template : Template | null ;
614+ open : boolean ;
615+ onOpenChange : ( open : boolean ) => void ;
616+ onUse : ( template : Template ) => void ;
617+ canUse : boolean ;
618+ }
619+
620+ function TemplateDetailModal ( {
621+ template,
622+ open,
623+ onOpenChange,
624+ onUse,
625+ canUse,
626+ } : TemplateDetailModalProps ) {
627+ if ( ! template ) return null ;
628+
629+ const catStyle = getCategoryStyle ( template . category ) ;
630+ const CategoryIcon = catStyle . icon ;
631+ const hasGraph =
632+ template . graph &&
633+ ( template . graph as any ) ?. nodes &&
634+ Array . isArray ( ( template . graph as any ) . nodes ) &&
635+ ( template . graph as any ) . nodes . length > 0 ;
636+
637+ return (
638+ < Dialog open = { open } onOpenChange = { onOpenChange } >
639+ < DialogContent className = "max-w-3xl max-h-[85vh] overflow-y-auto p-0" >
640+ { /* Graph preview */ }
641+ < div
642+ className = "relative w-full h-72 sm:h-80 overflow-hidden rounded-t-lg"
643+ style = { {
644+ background : 'linear-gradient(180deg, #F8FAFF 0%, #F1F5FF 100%)' ,
645+ } }
646+ >
647+ < div
648+ className = "absolute inset-0 hidden dark:block"
649+ style = { {
650+ background : 'linear-gradient(180deg, #111827 0%, #0B1220 100%)' ,
651+ } }
652+ />
653+ < div
654+ className = "absolute inset-0 hidden dark:block pointer-events-none"
655+ style = { {
656+ background :
657+ 'radial-gradient(circle at 50% 0%, rgba(99,102,241,0.08), transparent 60%)' ,
658+ } }
659+ />
660+ < div
661+ className = "absolute inset-0 opacity-[0.03] pointer-events-none"
662+ style = { {
663+ backgroundImage :
664+ 'radial-gradient(circle, hsl(var(--foreground)) 0.5px, transparent 0.5px)' ,
665+ backgroundSize : '12px 12px' ,
666+ } }
667+ />
668+ { hasGraph ? (
669+ < div className = "absolute inset-0 flex items-center justify-center p-4" >
670+ < WorkflowPreview graph = { template . graph ! } className = "w-full h-full" />
671+ </ div >
672+ ) : (
673+ < div className = "absolute inset-0 flex flex-col items-center justify-center gap-2 text-muted-foreground/30" >
674+ < Workflow className = "h-12 w-12" />
675+ < span className = "text-xs font-medium" > No preview</ span >
676+ </ div >
677+ ) }
678+ </ div >
679+
680+ { /* Content */ }
681+ < div className = "px-6 pb-6 space-y-4" >
682+ < DialogHeader >
683+ < div className = "flex items-center gap-2 mb-2" >
684+ < Badge
685+ variant = "outline"
686+ className = { cn (
687+ 'text-xs font-medium gap-1 rounded-full px-3 py-1 border' ,
688+ catStyle . badge ,
689+ ) }
690+ >
691+ < CategoryIcon className = "h-3 w-3" />
692+ { template . category || 'Automation' }
693+ </ Badge >
694+ { template . author && (
695+ < span className = "text-xs text-muted-foreground" > by { template . author } </ span >
696+ ) }
697+ </ div >
698+ < DialogTitle className = "text-2xl font-semibold" >
699+ { toTitleCase ( template . name ) }
700+ </ DialogTitle >
701+ { template . description && (
702+ < DialogDescription className = "text-sm mt-2" > { template . description } </ DialogDescription >
703+ ) }
704+ </ DialogHeader >
705+
706+ < Button
707+ className = { cn (
708+ 'w-full h-11 rounded-xl font-medium gap-2' ,
709+ 'active:scale-[0.98] transition-all duration-200' ,
710+ ) }
711+ onClick = { ( ) => onUse ( template ) }
712+ disabled = { ! canUse }
713+ >
714+ Use Template
715+ < ArrowRight className = "h-4 w-4" />
716+ </ Button >
717+ </ div >
718+ </ DialogContent >
719+ </ Dialog >
720+ ) ;
721+ }
722+
597723// ---------------------------------------------------------------------------
598724// Main page
599725// ---------------------------------------------------------------------------
@@ -625,6 +751,7 @@ export function TemplateLibraryPage() {
625751
626752 const [ selectedTemplate , setSelectedTemplate ] = useState < Template | null > ( null ) ;
627753 const [ isUseModalOpen , setIsUseModalOpen ] = useState ( false ) ;
754+ const [ previewTemplate , setPreviewTemplate ] = useState < Template | null > ( null ) ;
628755
629756 const handleSync = ( ) => {
630757 syncMutation . mutate ( ) ;
@@ -673,49 +800,9 @@ export function TemplateLibraryPage() {
673800 return (
674801 < div className = "flex-1 bg-background" >
675802 < div className = "container mx-auto py-6 md:py-8 px-3 md:px-6 max-w-7xl" >
676- { /* Header */ }
677- < div className = "mb-8" >
678- < div className = "flex items-start justify-between gap-4" >
679- < div >
680- < div className = "flex items-center gap-3 mb-1.5" >
681- < div className = "h-9 w-9 rounded-lg bg-primary/10 flex items-center justify-center" >
682- < Package className = "h-5 w-5 text-primary" />
683- </ div >
684- < div >
685- < h1 className = "text-2xl md:text-3xl font-bold tracking-tight" >
686- Template Library
687- </ h1 >
688- </ div >
689- </ div >
690- < p className = "text-muted-foreground text-sm ml-12" >
691- Browse and use pre-built workflow templates to accelerate your automation
692- </ p >
693- </ div >
694-
695- < div className = "flex items-center gap-2 flex-shrink-0" >
696- { ! isLoading && templates . length > 0 && (
697- < Badge variant = "secondary" className = "text-xs font-medium gap-1 hidden sm:flex" >
698- < FileText className = "h-3 w-3" />
699- { templates . length } template{ templates . length !== 1 ? 's' : '' }
700- </ Badge >
701- ) }
702- < Button
703- variant = "outline"
704- size = "sm"
705- onClick = { handleSync }
706- disabled = { isSyncing || ! canManageWorkflows }
707- className = "gap-2 h-8"
708- >
709- < RefreshCw className = { cn ( 'h-3.5 w-3.5' , isSyncing && 'animate-spin' ) } />
710- < span className = "hidden sm:inline" > Sync</ span >
711- </ Button >
712- </ div >
713- </ div >
714- </ div >
715-
716803 { /* Filters */ }
717804 < div className = "mb-6 space-y-3" >
718- { /* Search + Category */ }
805+ { /* Search + Category + Sync */ }
719806 < div className = "flex gap-3" >
720807 < div className = "relative flex-1" >
721808 < Search className = "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
@@ -751,6 +838,17 @@ export function TemplateLibraryPage() {
751838 } ) }
752839 </ SelectContent >
753840 </ Select >
841+
842+ < Button
843+ variant = "outline"
844+ size = "sm"
845+ onClick = { handleSync }
846+ disabled = { isSyncing || ! canManageWorkflows }
847+ className = "gap-2 h-9"
848+ >
849+ < RefreshCw className = { cn ( 'h-3.5 w-3.5' , isSyncing && 'animate-spin' ) } />
850+ < span className = "hidden sm:inline" > Sync</ span >
851+ </ Button >
754852 </ div >
755853
756854 { /* Tags + Clear */ }
@@ -819,13 +917,28 @@ export function TemplateLibraryPage() {
819917 key = { template . id }
820918 template = { template }
821919 onUse = { handleUseTemplate }
920+ onPreview = { setPreviewTemplate }
822921 canUse = { canManageWorkflows }
823922 />
824923 ) ) }
825924 </ div >
826925 ) }
827926 </ div >
828927
928+ { /* Template Detail Modal */ }
929+ < TemplateDetailModal
930+ template = { previewTemplate }
931+ open = { ! ! previewTemplate }
932+ onOpenChange = { ( open ) => {
933+ if ( ! open ) setPreviewTemplate ( null ) ;
934+ } }
935+ onUse = { ( template ) => {
936+ setPreviewTemplate ( null ) ;
937+ handleUseTemplate ( template ) ;
938+ } }
939+ canUse = { canManageWorkflows }
940+ />
941+
829942 { /* Use Template Modal */ }
830943 { selectedTemplate && (
831944 < UseTemplateModal
0 commit comments