11"use client" ;
22
33import { useRef , useEffect , useState } from "react" ;
4+ import { THEME_CSS } from "@/components/generative-ui/widget-renderer" ;
5+
6+ const CHART_COLORS = [
7+ "#3b82f6" , "#8b5cf6" , "#ec4899" , "#f59e0b" ,
8+ "#10b981" , "#06b6d4" , "#f97316" ,
9+ ] ;
410
511interface TemplateCardProps {
612 id : string ;
713 name : string ;
814 description : string ;
915 html : string ;
16+ componentType ?: string ;
17+ componentData ?: Record < string , unknown > ;
1018 dataDescription : string ;
1119 version : number ;
1220 onApply : ( id : string ) => void ;
1321 onDelete : ( id : string ) => void ;
1422}
1523
24+ /** Mini bar chart preview rendered as inline SVG */
25+ function BarChartPreview ( { data } : { data : { label : string ; value : number } [ ] } ) {
26+ if ( ! data ?. length ) return null ;
27+ const max = Math . max ( ...data . map ( ( d ) => d . value ) ) ;
28+ const barWidth = Math . min ( 40 , Math . floor ( 280 / data . length ) - 8 ) ;
29+ const chartWidth = data . length * ( barWidth + 8 ) ;
30+ const chartHeight = 100 ;
31+
32+ return (
33+ < svg
34+ viewBox = { `0 0 ${ chartWidth } ${ chartHeight } ` }
35+ width = "100%"
36+ height = "100%"
37+ preserveAspectRatio = "xMidYMid meet"
38+ style = { { padding : 16 } }
39+ >
40+ { data . map ( ( d , i ) => {
41+ const h = max > 0 ? ( d . value / max ) * ( chartHeight - 20 ) : 0 ;
42+ return (
43+ < rect
44+ key = { i }
45+ x = { i * ( barWidth + 8 ) }
46+ y = { chartHeight - h - 10 }
47+ width = { barWidth }
48+ height = { h }
49+ rx = { 3 }
50+ fill = { CHART_COLORS [ i % CHART_COLORS . length ] }
51+ />
52+ ) ;
53+ } ) }
54+ </ svg >
55+ ) ;
56+ }
57+
58+ /** Mini pie chart preview rendered as inline SVG */
59+ function PieChartPreview ( { data } : { data : { label : string ; value : number } [ ] } ) {
60+ if ( ! data ?. length ) return null ;
61+ const total = data . reduce ( ( sum , d ) => sum + d . value , 0 ) ;
62+ if ( total === 0 ) return null ;
63+
64+ const cx = 60 , cy = 60 , r = 50 ;
65+ let cumulative = 0 ;
66+ const slices = data . map ( ( d , i ) => {
67+ const startAngle = ( cumulative / total ) * 2 * Math . PI - Math . PI / 2 ;
68+ cumulative += d . value ;
69+ const endAngle = ( cumulative / total ) * 2 * Math . PI - Math . PI / 2 ;
70+ const largeArc = d . value / total > 0.5 ? 1 : 0 ;
71+ const x1 = cx + r * Math . cos ( startAngle ) ;
72+ const y1 = cy + r * Math . sin ( startAngle ) ;
73+ const x2 = cx + r * Math . cos ( endAngle ) ;
74+ const y2 = cy + r * Math . sin ( endAngle ) ;
75+ return (
76+ < path
77+ key = { i }
78+ d = { `M${ cx } ,${ cy } L${ x1 } ,${ y1 } A${ r } ,${ r } 0 ${ largeArc } ,1 ${ x2 } ,${ y2 } Z` }
79+ fill = { CHART_COLORS [ i % CHART_COLORS . length ] }
80+ />
81+ ) ;
82+ } ) ;
83+
84+ return (
85+ < svg viewBox = "0 0 120 120" width = "100%" height = "100%" preserveAspectRatio = "xMidYMid meet" >
86+ { slices }
87+ </ svg >
88+ ) ;
89+ }
90+
1691export function TemplateCard ( {
1792 id,
1893 name,
1994 description,
2095 html,
96+ componentType,
97+ componentData,
2198 dataDescription,
2299 version,
23100 onApply,
@@ -26,19 +103,31 @@ export function TemplateCard({
26103 const iframeRef = useRef < HTMLIFrameElement > ( null ) ;
27104 const [ previewReady , setPreviewReady ] = useState ( false ) ;
28105
29- useEffect ( ( ) => {
30- if ( ! iframeRef . current || ! html ) return ;
31- const doc = `<!DOCTYPE html>
106+ const previewHtml = html ? `<!DOCTYPE html>
32107<html><head><meta charset="utf-8">
33108<style>
109+ ${ THEME_CSS }
34110* { box-sizing: border-box; margin: 0; }
35- body { font-family: system-ui, sans-serif; font-size: 16px; color: #1a1a1a; background: #fff; transform-origin: top left; overflow: hidden; }
36- @media (prefers-color-scheme: dark) { body { color: #e8e6de; background: #1a1a18; } }
37- </style></head><body><div id="content">${ html } </div></body></html>` ;
38- iframeRef . current . srcdoc = doc ;
39- const timer = setTimeout ( ( ) => setPreviewReady ( true ) , 500 ) ;
40- return ( ) => clearTimeout ( timer ) ;
41- } , [ html ] ) ;
111+ body {
112+ font-family: system-ui, -apple-system, sans-serif;
113+ font-size: 16px;
114+ line-height: 1.7;
115+ color: var(--color-text-primary);
116+ background: var(--color-background-primary);
117+ overflow: hidden;
118+ }
119+ </style></head><body><div id="content">${ html } </div></body></html>` : "" ;
120+
121+ useEffect ( ( ) => {
122+ if ( ! iframeRef . current || ! previewHtml ) return ;
123+ iframeRef . current . srcdoc = previewHtml ;
124+ } , [ previewHtml ] ) ;
125+
126+ // Determine chart data for mini preview
127+ const chartData = componentData ?. data as { label : string ; value : number } [ ] | undefined ;
128+ const isBarChart = componentType === "barChart" ;
129+ const isPieChart = componentType === "pieChart" ;
130+ const isChart = isBarChart || isPieChart ;
42131
43132 return (
44133 < div
@@ -53,18 +142,34 @@ body { font-family: system-ui, sans-serif; font-size: 16px; color: #1a1a1a; back
53142 className = "relative overflow-hidden"
54143 style = { { height : 140 , background : "var(--color-background-secondary, #f7f6f3)" } }
55144 >
56- < iframe
57- ref = { iframeRef }
58- sandbox = "allow-same-origin"
59- className = "border-0 w-[300%] h-[300%] origin-top-left"
60- style = { {
61- transform : "scale(0.333)" ,
62- pointerEvents : "none" ,
63- opacity : previewReady ? 1 : 0 ,
64- transition : "opacity 300ms" ,
65- } }
66- title = { `Preview: ${ name } ` }
67- />
145+ { isChart && chartData ? (
146+ < div className = "flex items-center justify-center h-full" >
147+ { isBarChart && < BarChartPreview data = { chartData } /> }
148+ { isPieChart && < PieChartPreview data = { chartData } /> }
149+ </ div >
150+ ) : html ? (
151+ < iframe
152+ ref = { iframeRef }
153+ sandbox = "allow-same-origin allow-scripts"
154+ onLoad = { ( ) => setPreviewReady ( true ) }
155+ className = "border-0 w-[300%] h-[300%] origin-top-left"
156+ style = { {
157+ transform : "scale(0.333)" ,
158+ pointerEvents : "none" ,
159+ opacity : previewReady ? 1 : 0 ,
160+ transition : "opacity 300ms" ,
161+ } }
162+ title = { `Preview: ${ name } ` }
163+ />
164+ ) : (
165+ < div className = "flex items-center justify-center h-full" >
166+ < svg width = "32" height = "32" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "1.5" strokeLinecap = "round" strokeLinejoin = "round" style = { { color : "var(--text-tertiary, #999)" , opacity : 0.5 } } >
167+ < rect x = "3" y = "3" width = "18" height = "18" rx = "2" />
168+ < path d = "M3 9h18" />
169+ < path d = "M9 21V9" />
170+ </ svg >
171+ </ div >
172+ ) }
68173 { /* Version badge */ }
69174 < span
70175 className = "absolute top-2 right-2 text-[10px] font-semibold px-1.5 py-0.5 rounded-full"
@@ -102,9 +207,7 @@ body { font-family: system-ui, sans-serif; font-size: 16px; color: #1a1a1a; back
102207 </ div >
103208
104209 { /* Actions */ }
105- < div
106- className = "flex gap-2 p-3 pt-0"
107- >
210+ < div className = "flex gap-2 p-3 pt-0" >
108211 < button
109212 onClick = { ( ) => onApply ( id ) }
110213 className = "flex-1 text-xs font-medium py-1.5 rounded-lg transition-all duration-150 hover:scale-[1.02] text-white"
0 commit comments