Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 174 additions & 1 deletion apps/widget-builder/src/pages/gallery/WidgetGallery.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>Widget Gallery Page</div>;
const navigate = useNavigate();
const { createWidget } = useWidgets();
const [selectedWidget, setSelectedWidget] = useState<number | null>(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 (
<div className="h-screen flex flex-col bg-background">
{/* Header */}
<div className="h-14 border-b flex items-center justify-between px-6">
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
<h1 className="text-lg font-semibold">Widget Gallery</h1>
</div>
<p className="text-sm text-muted-foreground">
{galleryWidgets.length} pre-built widgets ready to use
</p>
</div>

{/* Gallery Grid */}
<ScrollArea className="flex-1">
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{galleryWidgets.map((item, index) => (
<div
key={index}
role="button"
tabIndex={0}
className="group relative cursor-pointer rounded-lg border bg-card p-6 hover:border-primary hover:shadow-lg transition-all focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
onClick={() => handleWidgetClick(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
aria-label={`View ${item.widget.name} details`}
>
Comment on lines +71 to +79
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clickable gallery item div lacks keyboard accessibility. Add role=\"button\", tabIndex={0}, and an onKeyDown handler to support keyboard navigation (Enter/Space keys) for users who cannot use a mouse.

Copilot uses AI. Check for mistakes.
<div className="flex flex-col gap-4">
{/* Widget Preview */}
<div className="flex items-center justify-center min-h-[200px] bg-muted/30 rounded-md p-4">
<div className="w-full max-w-sm">
<ErrorBoundary>
<WidgetRenderer
schema={undefined}
components={components}
data={item.widget.states?.[0]?.data ?? {}}
template={item.widget.template}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The WidgetRenderer is not support template not. You can add this support, parse the schema from template when the schema prop not provided

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added template prop support to WidgetRenderer in commit ab14c6e. When schema is not provided but template is, the renderer now automatically parses the template using parseJSXTemplate and renders the widget.

Changes:

  • Added template?: string to WidgetRendererProps
  • Parse template to schema when schema is undefined
  • Gallery widgets now render correctly using the template prop

Dialog with template support

/>
Comment on lines +85 to +90
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WidgetRenderer parses the template on every render. Consider memoizing the parsed schema with useMemo to avoid redundant parsing operations, especially since the template is static for each gallery item.

Copilot uses AI. Check for mistakes.
</ErrorBoundary>
</div>
</div>

{/* Widget Info */}
<div className="space-y-1">
<h3 className="font-semibold text-base">{item.widget.name}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{item.widget.description}
</p>
</div>

{/* Hover Overlay */}
<div className="absolute inset-0 bg-primary/5 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center">
<Button variant="secondary" size="sm">
<Code2 className="h-4 w-4 mr-2" />
View Code
</Button>
</div>
</div>
</div>
))}
</div>
</div>
</ScrollArea>

{/* Widget Details Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-3xl max-h-[90vh]">
<DialogHeader>
<DialogTitle>{selectedWidgetData?.widget.name}</DialogTitle>
<DialogDescription>{selectedWidgetData?.widget.description}</DialogDescription>
</DialogHeader>

<ScrollArea className="max-h-[60vh]">
<div className="space-y-4">
{/* Widget Preview */}
<div className="p-6 bg-muted/30 rounded-lg flex items-center justify-center">
<div className="w-full max-w-sm">
{selectedWidgetData && (
<ErrorBoundary>
<WidgetRenderer
schema={undefined}
components={components}
data={selectedWidgetData.widget.states?.[0]?.data ?? {}}
template={selectedWidgetData.widget.template}
/>
Comment on lines +132 to +137
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WidgetRenderer parses the template on every render in the dialog. Consider memoizing the parsed schema with useMemo to avoid redundant parsing operations when the dialog is opened.

Copilot uses AI. Check for mistakes.
</ErrorBoundary>
)}
</div>
</div>

{/* JSX Template Code */}
<div className="space-y-2">
<h4 className="font-semibold text-sm">JSX Template</h4>
<div className="rounded-lg overflow-hidden">
<SyntaxHighlighter
language="jsx"
style={vscDarkPlus}
customStyle={{
margin: 0,
borderRadius: "0.5rem",
fontSize: "0.875rem",
}}
>
{selectedWidgetData?.widget.template || ""}
</SyntaxHighlighter>
</div>
</div>
</div>
</ScrollArea>

<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleUseWidget}>
<Sparkles className="h-4 w-4 mr-2" />
Use This Widget
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
70 changes: 70 additions & 0 deletions apps/widget-builder/src/pages/gallery/widgets/calendar-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Widget } from "@deer-flow/widget";

export const calendarEvent: Omit<Widget, "id"> = {
name: "Calendar Event",
description: "An event card showing meeting details and participants",
template: `<Card size="md" padding="md">
<Row align="center" gap={2}>
<Box background="blue-500" padding="md" radius="md">
<Col align="center" gap={0}>
<Text color="white" size="xs" weight="medium">{data.month}</Text>
<Title level="h2" color="white">{data.day}</Title>
</Col>
</Box>

<Col gap={1} minWidth="auto">
<Title level="h4">{data.title}</Title>
<Row align="center" gap={2}>
<Text color="muted" size="sm">🕐 {data.time}</Text>
<Text color="muted" size="sm">📍 {data.location}</Text>
</Row>
</Col>
</Row>

<Divider margin="md" />

<Col gap={2}>
<Text size="sm" color="muted">{data.description}</Text>

<Row align="center" gap={2}>
<Caption>Participants:</Caption>
<Row gap={1}>
{data.participants.map((participant, i) => (
<Image
key={i}
src={participant.avatar}
size={28}
radius="full"
/>
))}
</Row>
<Text size="sm" color="muted">+{data.moreParticipants}</Text>
</Row>

<Row gap={2}>
<Button size="sm" variant="default">Accept</Button>
<Button size="sm" variant="outline">Maybe</Button>
<Button size="sm" variant="ghost">Decline</Button>
</Row>
</Col>
</Card>`,
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"
}
}
]
};
64 changes: 64 additions & 0 deletions apps/widget-builder/src/pages/gallery/widgets/email-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Widget } from "@deer-flow/widget";

export const emailPreview: Omit<Widget, "id"> = {
name: "Email Preview",
description: "An email preview card with sender info and quick actions",
template: `<Card size="md" padding="md">
<Row align="center" gap={2}>
<Image
src={data.sender.avatar}
size={48}
radius="full"
/>
<Col gap={0} minWidth="auto">
<Row align="center" gap={2}>
<Text weight="medium">{data.sender.name}</Text>
{data.unread && <Badge variant="default" size="sm">New</Badge>}
</Row>
<Text color="muted" size="sm">{data.sender.email}</Text>
</Col>
<Spacer />
<Caption>{data.time}</Caption>
</Row>

<Box padding="sm">
<Title level="h4">{data.subject}</Title>
</Box>

<Text color="muted" size="sm" truncate>
{data.preview}
</Text>

{data.hasAttachment && (
<Row align="center" gap={1} margin="sm">
<Text size="sm">📎</Text>
<Text size="sm" color="muted">{data.attachmentCount} attachment{data.attachmentCount > 1 ? "s" : ""}</Text>
</Row>
)}

<Divider margin="sm" />

<Row gap={2} justify="end">
<Button size="sm" variant="ghost">Archive</Button>
<Button size="sm" variant="outline">Reply</Button>
</Row>
</Card>`,
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
}
}
]
};
30 changes: 30 additions & 0 deletions apps/widget-builder/src/pages/gallery/widgets/index.ts
Original file line number Diff line number Diff line change
@@ -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<Widget, "id">;
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 },
];
50 changes: 50 additions & 0 deletions apps/widget-builder/src/pages/gallery/widgets/music-player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Widget } from "@deer-flow/widget";

export const musicPlayer: Omit<Widget, "id"> = {
name: "Music Player",
description: "A compact music player widget with controls",
template: `<Card size="md" padding="lg" background="gradient-to-br from-purple-500 to-pink-500">
<Row align="center" gap={3}>
<Image
src={data.albumArt}
size={80}
radius="md"
fit="cover"
/>
<Col gap={1} minWidth="auto">
<Title level="h4" color="white">{data.title}</Title>
<Text color="white" size="sm">{data.artist}</Text>
<Caption color="white">{data.album}</Caption>
</Col>
</Row>

<Box margin="md">
<Progress value={data.progress} />
<Row justify="between" margin="xs">
<Caption color="white">{data.currentTime}</Caption>
<Caption color="white">{data.duration}</Caption>
</Row>
</Box>

<Row gap={2} justify="center" align="center">
<Button size="sm" variant="outline">⏮</Button>
<Button size="lg">{data.playing ? "⏸" : "▶"}</Button>
<Button size="sm" variant="outline">⏭</Button>
</Row>
</Card>`,
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
}
}
]
};
Loading