diff --git a/apps/widget-builder/src/pages/gallery/WidgetGallery.tsx b/apps/widget-builder/src/pages/gallery/WidgetGallery.tsx index 9a0463e..961b1bd 100644 --- a/apps/widget-builder/src/pages/gallery/WidgetGallery.tsx +++ b/apps/widget-builder/src/pages/gallery/WidgetGallery.tsx @@ -1,3 +1,176 @@ +import { WidgetRenderer } from "@deer-flow/widget-renderer"; +import { Code2, Sparkles } from "lucide-react"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { components } from "@/components/widget-components"; +import { useWidgets } from "@/hooks/use-widgets"; + +import { galleryWidgets } from "./widgets"; + export const WidgetGallery = () => { - return
Widget Gallery Page
; + const navigate = useNavigate(); + const { createWidget } = useWidgets(); + const [selectedWidget, setSelectedWidget] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const handleWidgetClick = (index: number) => { + setSelectedWidget(index); + setIsDialogOpen(true); + }; + + const handleUseWidget = () => { + if (selectedWidget === null) return; + + const widget = galleryWidgets[selectedWidget].widget; + const newWidget = createWidget(widget); + setIsDialogOpen(false); + navigate(`/editor/${newWidget.id}`); + }; + + const handleKeyDown = (event: React.KeyboardEvent, index: number) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleWidgetClick(index); + } + }; + + const selectedWidgetData = selectedWidget !== null ? galleryWidgets[selectedWidget] : null; + + return ( +
+ {/* Header */} +
+
+ +

Widget Gallery

+
+

+ {galleryWidgets.length} pre-built widgets ready to use +

+
+ + {/* Gallery Grid */} + +
+
+ {galleryWidgets.map((item, index) => ( +
handleWidgetClick(index)} + onKeyDown={(e) => handleKeyDown(e, index)} + aria-label={`View ${item.widget.name} details`} + > +
+ {/* Widget Preview */} +
+
+ + + +
+
+ + {/* Widget Info */} +
+

{item.widget.name}

+

+ {item.widget.description} +

+
+ + {/* Hover Overlay */} +
+ +
+
+
+ ))} +
+
+
+ + {/* Widget Details Dialog */} + + + + {selectedWidgetData?.widget.name} + {selectedWidgetData?.widget.description} + + + +
+ {/* Widget Preview */} +
+
+ {selectedWidgetData && ( + + + + )} +
+
+ + {/* JSX Template Code */} +
+

JSX Template

+
+ + {selectedWidgetData?.widget.template || ""} + +
+
+
+
+ + + + + +
+
+
+ ); }; diff --git a/apps/widget-builder/src/pages/gallery/widgets/calendar-event.ts b/apps/widget-builder/src/pages/gallery/widgets/calendar-event.ts new file mode 100644 index 0000000..e426a26 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/calendar-event.ts @@ -0,0 +1,70 @@ +import { Widget } from "@deer-flow/widget"; + +export const calendarEvent: Omit = { + name: "Calendar Event", + description: "An event card showing meeting details and participants", + template: ` + + + + {data.month} + {data.day} + + + + + {data.title} + + 🕐 {data.time} + 📍 {data.location} + + + + + + + + {data.description} + + + Participants: + + {data.participants.map((participant, i) => ( + + ))} + + +{data.moreParticipants} + + + + + + + + +`, + states: [ + { + name: "Default", + data: { + month: "NOV", + day: "15", + title: "Team Sync Meeting", + time: "2:00 PM - 3:00 PM", + location: "Conference Room A", + description: "Weekly team sync to discuss project progress and upcoming milestones", + participants: [ + { avatar: "https://picsum.photos/150/150?random=6" }, + { avatar: "https://picsum.photos/150/150?random=7" }, + { avatar: "https://picsum.photos/150/150?random=8" } + ], + moreParticipants: "5" + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/email-preview.ts b/apps/widget-builder/src/pages/gallery/widgets/email-preview.ts new file mode 100644 index 0000000..c9978d6 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/email-preview.ts @@ -0,0 +1,64 @@ +import { Widget } from "@deer-flow/widget"; + +export const emailPreview: Omit = { + name: "Email Preview", + description: "An email preview card with sender info and quick actions", + template: ` + + + + + {data.sender.name} + {data.unread && New} + + {data.sender.email} + + + {data.time} + + + + {data.subject} + + + + {data.preview} + + + {data.hasAttachment && ( + + 📎 + {data.attachmentCount} attachment{data.attachmentCount > 1 ? "s" : ""} + + )} + + + + + + + +`, + states: [ + { + name: "Default", + data: { + sender: { + name: "Emma Wilson", + email: "emma@company.com", + avatar: "https://picsum.photos/150/150?random=9" + }, + subject: "Q4 Marketing Strategy Review", + preview: "Hi team, I've prepared the Q4 marketing strategy document. Please review the attached presentation and share your feedback before Friday's meeting...", + time: "10:30 AM", + unread: true, + hasAttachment: true, + attachmentCount: 2 + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/index.ts b/apps/widget-builder/src/pages/gallery/widgets/index.ts new file mode 100644 index 0000000..874fcb2 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/index.ts @@ -0,0 +1,30 @@ +import { Widget } from "@deer-flow/widget"; + +import { calendarEvent } from "./calendar-event"; +import { emailPreview } from "./email-preview"; +import { musicPlayer } from "./music-player"; +import { notificationCard } from "./notification-card"; +import { productCard } from "./product-card"; +import { socialMediaPost } from "./social-media-post"; +import { statsDashboard } from "./stats-dashboard"; +import { taskListItem } from "./task-list-item"; +import { userProfileCard } from "./user-profile-card"; +import { weatherWidget } from "./weather-widget"; + +export interface GalleryWidget { + widget: Omit; + thumbnail?: string; +} + +export const galleryWidgets: GalleryWidget[] = [ + { widget: userProfileCard }, + { widget: taskListItem }, + { widget: notificationCard }, + { widget: weatherWidget }, + { widget: statsDashboard }, + { widget: productCard }, + { widget: musicPlayer }, + { widget: socialMediaPost }, + { widget: calendarEvent }, + { widget: emailPreview }, +]; diff --git a/apps/widget-builder/src/pages/gallery/widgets/music-player.ts b/apps/widget-builder/src/pages/gallery/widgets/music-player.ts new file mode 100644 index 0000000..1cb2eee --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/music-player.ts @@ -0,0 +1,50 @@ +import { Widget } from "@deer-flow/widget"; + +export const musicPlayer: Omit = { + name: "Music Player", + description: "A compact music player widget with controls", + template: ` + + + + {data.title} + {data.artist} + {data.album} + + + + + + + {data.currentTime} + {data.duration} + + + + + + + + +`, + states: [ + { + name: "Default", + data: { + albumArt: "https://picsum.photos/400/400?random=20", + title: "Midnight Dreams", + artist: "Luna Rivers", + album: "Neon Nights", + currentTime: "2:34", + duration: "4:12", + progress: 62, + playing: true + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/notification-card.ts b/apps/widget-builder/src/pages/gallery/widgets/notification-card.ts new file mode 100644 index 0000000..bd62340 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/notification-card.ts @@ -0,0 +1,37 @@ +import { Widget } from "@deer-flow/widget"; + +export const notificationCard: Omit = { + name: "Notification Card", + description: "A notification card showing activity updates with timestamp", + template: ` + + + {data.icon} + + + + + {data.title} + + {data.type} + + + {data.message} + {data.timestamp} + + +`, + states: [ + { + name: "Default", + data: { + icon: "✓", + title: "Deployment Successful", + message: "Your application has been deployed to production", + type: "success", + timestamp: "2 minutes ago" + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/product-card.ts b/apps/widget-builder/src/pages/gallery/widgets/product-card.ts new file mode 100644 index 0000000..c1df075 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/product-card.ts @@ -0,0 +1,51 @@ +import { Widget } from "@deer-flow/widget"; + +export const productCard: Omit = { + name: "Product Card", + description: "An e-commerce product card with image, details, and actions", + template: ` + + + + + {data.category} + + + {data.rating} + + + + + {data.name} + {data.description} + + + + + {data.price} + {data.oldPrice && {data.oldPrice}} + + + + +`, + states: [ + { + name: "Default", + data: { + image: "https://picsum.photos/400/400?random=10", + name: "Wireless Headphones", + description: "Premium noise-cancelling headphones with 30h battery life", + category: "Audio", + price: "$299", + oldPrice: "$399", + rating: "4.8" + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/social-media-post.ts b/apps/widget-builder/src/pages/gallery/widgets/social-media-post.ts new file mode 100644 index 0000000..b47e6da --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/social-media-post.ts @@ -0,0 +1,66 @@ +import { Widget } from "@deer-flow/widget"; + +export const socialMediaPost: Omit = { + name: "Social Media Post", + description: "A social media post card with engagement metrics", + template: ` + + + + {data.author.name} + {data.timestamp} + + + + + + + {data.content} + + + {data.image && } + + + + + + ❤️ + {data.likes} + + + 💬 + {data.comments} + + + ↗️ + {data.shares} + + +`, + states: [ + { + name: "Default", + data: { + author: { + name: "Alex Johnson", + avatar: "https://picsum.photos/150/150?random=5" + }, + timestamp: "2 hours ago", + content: "Just launched our new product! Excited to share this journey with you all. 🚀", + image: "https://picsum.photos/600/400?random=30", + likes: "234", + comments: "45", + shares: "12" + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/stats-dashboard.ts b/apps/widget-builder/src/pages/gallery/widgets/stats-dashboard.ts new file mode 100644 index 0000000..74746f3 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/stats-dashboard.ts @@ -0,0 +1,47 @@ +import { Widget } from "@deer-flow/widget"; + +export const statsDashboard: Omit = { + name: "Stats Dashboard Card", + description: "A statistics dashboard card with metrics and trend indicators", + template: ` + + + Total Revenue + {data.value} + + + {data.trend === "up" ? "↑" : "↓"} {data.change} + + + + + + + + This Month + {data.thisMonth} + +{data.monthGrowth}% from last month + + + + Goal + {data.goal} + + + +`, + states: [ + { + name: "Default", + data: { + value: "$45,231", + trend: "up", + change: "12.5%", + thisMonth: "$12,450", + monthGrowth: "18", + goal: "$15,000", + progress: 83 + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/task-list-item.ts b/apps/widget-builder/src/pages/gallery/widgets/task-list-item.ts new file mode 100644 index 0000000..71dd2cd --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/task-list-item.ts @@ -0,0 +1,57 @@ +import { Widget } from "@deer-flow/widget"; + +export const taskListItem: Omit = { + name: "Task List Item", + description: "An elegant task list item with priority badge and due date", + template: ` + + + + {data.priority} + + + + + {data.title} + {data.description} + + + + + + Due Date + {data.dueDate} + + + + + + + + {data.assignee.name} + + + {data.progress}% + +`, + states: [ + { + name: "Default", + data: { + title: "Redesign Landing Page", + description: "Update the homepage with new branding and animations", + priority: "high", + dueDate: "Nov 12", + progress: 65, + assignee: { + name: "Mike Chen", + avatar: "https://picsum.photos/150/150?random=3" + } + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/user-profile-card.ts b/apps/widget-builder/src/pages/gallery/widgets/user-profile-card.ts new file mode 100644 index 0000000..7eb13f2 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/user-profile-card.ts @@ -0,0 +1,61 @@ +import { Widget } from "@deer-flow/widget"; + +export const userProfileCard: Omit = { + name: "User Profile Card", + description: "A beautiful user profile card with avatar, name, and social stats", + template: ` + + + + {data.name} + {data.role} + {data.location} + + + + + + + + {data.stats.followers} + Followers + + + {data.stats.following} + Following + + + {data.stats.posts} + Posts + + + + + + + + + +`, + states: [ + { + name: "Default", + data: { + avatar: "https://picsum.photos/150/150?random=1", + name: "Sarah Anderson", + role: "Product Designer", + location: "San Francisco, CA", + stats: { + followers: "2.4K", + following: "486", + posts: "127" + } + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/gallery/widgets/weather-widget.ts b/apps/widget-builder/src/pages/gallery/widgets/weather-widget.ts new file mode 100644 index 0000000..14cefc3 --- /dev/null +++ b/apps/widget-builder/src/pages/gallery/widgets/weather-widget.ts @@ -0,0 +1,56 @@ +import { Widget } from "@deer-flow/widget"; + +export const weatherWidget: Omit = { + name: "Weather Widget", + description: "Current weather conditions with forecast", + template: ` + + + + {data.location} + {data.date} + + {data.icon} + + + + {data.temperature} + °C + + + {data.condition} + + + + + + Humidity + {data.humidity}% + + + Wind + {data.wind} km/h + + + Feels Like + {data.feelsLike}° + + + +`, + states: [ + { + name: "Default", + data: { + location: "San Francisco", + date: "Monday, Nov 7", + icon: "☀️", + temperature: "22", + condition: "Sunny", + humidity: "65", + wind: "12", + feelsLike: "24" + } + } + ] +}; diff --git a/apps/widget-builder/src/pages/layout/AppSidebar.tsx b/apps/widget-builder/src/pages/layout/AppSidebar.tsx index 42e9ea2..6591d37 100644 --- a/apps/widget-builder/src/pages/layout/AppSidebar.tsx +++ b/apps/widget-builder/src/pages/layout/AppSidebar.tsx @@ -1,4 +1,4 @@ -import { LayoutTemplate, PlusCircleIcon, Package } from "lucide-react"; +import { LayoutTemplate, PlusCircleIcon, Package, Sparkles } from "lucide-react"; import { Link } from "react-router-dom"; import { @@ -51,6 +51,14 @@ export function AppSidebar() { + + + + + Gallery + + + diff --git a/packages/widget-renderer/src/WidgetRenderer.tsx b/packages/widget-renderer/src/WidgetRenderer.tsx index 3429a3f..ae77fe4 100644 --- a/packages/widget-renderer/src/WidgetRenderer.tsx +++ b/packages/widget-renderer/src/WidgetRenderer.tsx @@ -1,11 +1,12 @@ -import { JSXElementSchema, executeExpression } from "@deer-flow/widget"; -import React from "react"; +import { JSXElementSchema, executeExpression, parseJSXTemplate } from "@deer-flow/widget"; +import React, { useRef, useEffect } from "react"; import { ComponentType } from "react"; export type WidgetRendererProps = { schema?: JSXElementSchema; components: Record>; data?: Record; + template?: string; }; // Helper to resolve expression values from data context @@ -22,42 +23,67 @@ function resolveValue(value: JSXElementSchema["value"], data: Record(null); + const lastTemplateRef = useRef(undefined); + + // Parse template only when it changes + useEffect(() => { + if (template && template !== lastTemplateRef.current) { + const parseResult = parseJSXTemplate(template); + if (parseResult.success && parseResult.schema) { + parsedSchemaRef.current = parseResult.schema; + } else { + console.warn("Failed to parse template:", parseResult.errors); + parsedSchemaRef.current = null; + } + lastTemplateRef.current = template; + } else if (!template && lastTemplateRef.current !== undefined) { + // Template was removed + parsedSchemaRef.current = null; + lastTemplateRef.current = undefined; + } + }, [template]); + + // Determine effective schema + const effectiveSchema = schema || parsedSchemaRef.current; + + if (!effectiveSchema) { return null; } - switch (schema.type) { + switch (effectiveSchema.type) { case "text": { - const content = resolveValue(schema.value, data ?? {}); + const content = resolveValue(effectiveSchema.value, data ?? {}); return <>{content}; } case "expression": { - const content = resolveValue(schema.value, data ?? {}); + const content = resolveValue(effectiveSchema.value, data ?? {}); return <>{content}; } case "element": { - if (!schema.name) { - console.warn("Element schema missing 'name' property:", schema); + if (!effectiveSchema.name) { + console.warn("Element schema missing 'name' property:", effectiveSchema); return null; } // Resolve component from the map, or fall back to a string for native HTML elements - const Component = components[schema.name] || schema.name; + const Component = components[effectiveSchema.name] || effectiveSchema.name; const props: Record = {}; - if (schema.props) { - for (const key in schema.props) { - if (Object.prototype.hasOwnProperty.call(schema.props, key)) { + if (effectiveSchema.props) { + for (const key in effectiveSchema.props) { + if (Object.prototype.hasOwnProperty.call(effectiveSchema.props, key)) { // Resolve prop values, which might be expressions - props[key] = resolveValue(schema.props[key] as JSXElementSchema["value"], data ?? {}); + props[key] = resolveValue(effectiveSchema.props[key] as JSXElementSchema["value"], data ?? {}); } } } // Recursively render children - const children = schema.children - ? schema.children.map((child, index) => ( + const children = effectiveSchema.children + ? effectiveSchema.children.map((child, index) => ( )) : undefined; @@ -66,7 +92,7 @@ export function WidgetRenderer({ schema, components, data }: WidgetRendererProps } default: - console.warn("Unknown schema node type:", schema.type); + console.warn("Unknown schema node type:", effectiveSchema.type); return null; } }