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 */}
+
+
+ );
};
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;
}
}