-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: Add gallery page with ten pre-built widget templates, template prop support, and accessibility #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: Add gallery page with ten pre-built widget templates, template prop support, and accessibility #2
Changes from all commits
fde963f
a060489
20254ae
ab14c6e
1a34d1a
b370f1d
58cf9eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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`} | ||
| > | ||
| <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} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot The WidgetRenderer is not support
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added Changes:
|
||
| /> | ||
|
Comment on lines
+85
to
+90
|
||
| </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
|
||
| </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> | ||
| ); | ||
| }; | ||
| 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" | ||
| } | ||
| } | ||
| ] | ||
| }; |
| 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 | ||
| } | ||
| } | ||
| ] | ||
| }; |
| 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 }, | ||
| ]; |
| 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 | ||
| } | ||
| } | ||
| ] | ||
| }; |

There was a problem hiding this comment.
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 anonKeyDownhandler to support keyboard navigation (Enter/Space keys) for users who cannot use a mouse.