frontend changes

This commit is contained in:
Chandini 2025-09-11 09:34:34 +05:30
parent d57e20e9d8
commit f3b998f6b8
20 changed files with 2530 additions and 792 deletions

View File

@ -1,7 +1,24 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ transpilePackages: ['@tldraw/tldraw'],
webpack: (config, { isServer }) => {
// Fix tldraw duplication issues
config.resolve.alias = {
...config.resolve.alias,
'@tldraw/utils': require.resolve('@tldraw/utils'),
'@tldraw/state': require.resolve('@tldraw/state'),
'@tldraw/state-react': require.resolve('@tldraw/state-react'),
'@tldraw/store': require.resolve('@tldraw/store'),
'@tldraw/validate': require.resolve('@tldraw/validate'),
'@tldraw/tlschema': require.resolve('@tldraw/tlschema'),
'@tldraw/editor': require.resolve('@tldraw/editor'),
'tldraw': require.resolve('tldraw'),
'@tldraw/tldraw': require.resolve('@tldraw/tldraw'),
};
return config;
},
}; };
export default nextConfig; export default nextConfig;

91
package-lock.json generated
View File

@ -15,9 +15,12 @@
"@next/font": "^14.2.15", "@next/font": "^14.2.15",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
@ -30,14 +33,18 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"next": "15.4.6", "next": "15.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.9.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-resizable-panels": "^3.0.5", "react-resizable-panels": "^3.0.5",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"svg-path-parser": "^1.1.0", "svg-path-parser": "^1.1.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
@ -90,6 +97,12 @@
"anthropic-ai-sdk": "bin/cli" "anthropic-ai-sdk": "bin/cli"
} }
}, },
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@dnd-kit/accessibility": { "node_modules/@dnd-kit/accessibility": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -4854,6 +4867,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -4967,6 +4996,34 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
"integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
"license": "MIT",
"dependencies": {
"embla-carousel": "8.6.0",
"embla-carousel-reactive-utils": "8.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@ -8008,6 +8065,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-day-picker": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.9.0.tgz",
"integrity": "sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@ -9204,6 +9282,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/w3c-keyname": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@ -16,9 +16,12 @@
"@next/font": "^14.2.15", "@next/font": "^14.2.15",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
@ -31,14 +34,18 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"next": "15.4.6", "next": "15.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.9.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-resizable-panels": "^3.0.5", "react-resizable-panels": "^3.0.5",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"svg-path-parser": "^1.1.0", "svg-path-parser": "^1.1.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {

View File

@ -120,3 +120,62 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
/* Added custom styles for drag-and-drop editor */
.drag-overlay {
@apply opacity-50 rotate-3 scale-105;
}
.drop-zone-active {
@apply ring-2 ring-accent bg-accent/5;
}
.canvas-grid {
background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
}
.resizer {
@apply bg-border hover:bg-accent transition-colors;
}
.resizer:hover {
@apply bg-accent;
}
/* Enhanced scrolling for panels */
[data-slot="scroll-area-viewport"] {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
[data-slot="scroll-area-viewport"]::-webkit-scrollbar {
width: 6px;
}
[data-slot="scroll-area-viewport"]::-webkit-scrollbar-track {
background: transparent;
}
[data-slot="scroll-area-viewport"]::-webkit-scrollbar-thumb {
background-color: hsl(var(--border));
border-radius: 3px;
}
[data-slot="scroll-area-viewport"]::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--accent));
}
/* Smooth scrolling for panels */
.component-panel-scroll {
scroll-behavior: smooth;
overflow-y: auto;
overflow-x: hidden;
}
.properties-panel-scroll {
scroll-behavior: smooth;
overflow-y: auto;
overflow-x: hidden;
}

View File

@ -1,143 +1,82 @@
"use client" "use client"
import { useRef, useState } from "react" import { useDroppable } from "@dnd-kit/core"
import { useEffect, useRef, useState } from "react"
import { useEditorStore } from "@/lib/store" import { useEditorStore } from "@/lib/store"
import { ComponentRenderer } from "./component-renderer" import { ComponentRenderer } from "@/components/component-renderer"
import { cn } from "@/lib/utils" import { MousePointer } from "lucide-react"
export function Canvas() { export function Canvas() {
const canvasRef = useRef<HTMLDivElement>(null) const { components, selectComponent, moveComponent } = useEditorStore()
const [isDragging, setIsDragging] = useState(false) const containerRef = useRef<HTMLDivElement | null>(null)
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null) const [dragState, setDragState] = useState<{
const [selectedComponents, setSelectedComponents] = useState<Set<string>>(new Set()) id: string
offsetX: number
offsetY: number
} | null>(null)
const { const { setNodeRef, isOver } = useDroppable({
components, id: "canvas",
selectedComponent, })
selectComponent,
moveComponent,
updateComponent
} = useEditorStore()
const handleMouseDown = (e: React.MouseEvent) => {
if (e.target === canvasRef.current) {
// Clicked on empty canvas, deselect all
selectComponent(null)
setSelectedComponents(new Set())
}
}
const handleComponentClick = (componentId: string, e: React.MouseEvent) => {
e.stopPropagation()
const component = components.find(c => c.id === componentId)
if (component) {
selectComponent(component)
setSelectedComponents(new Set([componentId]))
}
}
const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => {
e.stopPropagation()
const component = components.find(c => c.id === componentId)
if (component) {
selectComponent(component)
setSelectedComponents(new Set([componentId]))
// Start dragging
setIsDragging(true)
setDragStart({ x: e.clientX, y: e.clientY })
}
}
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging && dragStart && selectedComponent) {
const deltaX = e.clientX - dragStart.x
const deltaY = e.clientY - dragStart.y
const newPosition = {
x: Math.max(0, selectedComponent.position.x + deltaX),
y: Math.max(0, selectedComponent.position.y + deltaY)
}
moveComponent(selectedComponent.id, newPosition)
setDragStart({ x: e.clientX, y: e.clientY })
}
}
const handleMouseUp = () => {
setIsDragging(false)
setDragStart(null)
}
return ( return (
<div <div className="h-full flex flex-col">
ref={canvasRef} {/* Canvas Header */}
className={cn( <div className="p-4 border-b border-border bg-card">
"relative w-full h-full bg-white overflow-hidden", <h2 className="text-lg font-semibold text-card-foreground">Canvas</h2>
"canvas-grid" <p className="text-sm text-muted-foreground">Drag components here to build your interface</p>
)}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{/* Canvas Grid Background */}
<div className="absolute inset-0 opacity-20">
<div
className="w-full h-full"
style={{
backgroundImage: `
linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px)
`,
backgroundSize: '20px 20px'
}}
/>
</div> </div>
{/* Components */} {/* Canvas Area */}
{components.map((component) => ( <div
<div ref={(node) => {
key={component.id} setNodeRef(node)
className={cn( containerRef.current = node
"absolute cursor-move select-none", }}
selectedComponents.has(component.id) && "ring-2 ring-blue-500 ring-opacity-50" className={`flex-1 relative canvas-grid overflow-auto ${isOver ? "drop-zone-active" : ""}`}
)} onClick={() => selectComponent(null)}
style={{ onMouseMove={(e) => {
left: component.position.x, if (!dragState || !containerRef.current) return
top: component.position.y, const rect = containerRef.current.getBoundingClientRect()
width: component.size?.width || 100, const x = e.clientX - rect.left - dragState.offsetX + containerRef.current.scrollLeft
height: component.size?.height || 100, const y = e.clientY - rect.top - dragState.offsetY + containerRef.current.scrollTop
}} moveComponent(dragState.id, { x: Math.max(0, x), y: Math.max(0, y) })
onClick={(e) => handleComponentClick(component.id, e)} }}
onMouseDown={(e) => handleComponentMouseDown(component.id, e)} onMouseUp={() => setDragState(null)}
> onMouseLeave={() => setDragState(null)}
<ComponentRenderer >
component={component} {components.length === 0 ? (
isSelected={selectedComponents.has(component.id)} <div className="absolute inset-0 flex items-center justify-center">
/> <div className="text-center">
</div> <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-muted flex items-center justify-center">
))} <MousePointer className="w-8 h-8 text-muted-foreground" />
</div>
{/* Drop Zone Indicator */} <h3 className="text-lg font-medium text-foreground mb-2">Start Building</h3>
{isDragging && ( <p className="text-muted-foreground max-w-sm">
<div className="absolute inset-0 pointer-events-none"> Drag components from the sidebar to start building your interface
<div className="w-full h-full border-2 border-dashed border-blue-400 bg-blue-50 bg-opacity-20" /> </p>
</div> </div>
)}
{/* Empty State */}
{components.length === 0 && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="text-6xl mb-4">🎨</div>
<h3 className="text-lg font-medium mb-2">No components yet</h3>
<p className="text-sm">Drag components from the palette to get started</p>
</div> </div>
</div> ) : (
)} components.map((component) => (
<ComponentRenderer
key={component.id}
component={component}
onClick={(e) => {
e.stopPropagation()
selectComponent(component)
}}
onMouseDown={(e) => {
const target = e.currentTarget as HTMLDivElement
const rect = target.getBoundingClientRect()
const offsetX = e.clientX - rect.left
const offsetY = e.clientY - rect.top
setDragState({ id: component.id, offsetX, offsetY })
}}
/>
))
)}
</div>
</div> </div>
) )
} }

View File

@ -1,213 +1,132 @@
"use client" "use client"
import { useDraggable } from "@dnd-kit/core"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useState } from "react" import { useState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Textarea } from "./ui/textarea"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Checkbox } from "./ui/checkbox"
import { Switch } from "./ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { RadioGroup, RadioGroupItem } from "./ui/radio-group"
import { Progress } from "./ui/progress"
import { Avatar, AvatarFallback } from "./ui/avatar"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { Badge } from "./ui/badge"
import { cn } from "@/lib/utils"
import { import {
MousePointer, Table,
Square,
Circle,
Type,
Image,
Layout,
BarChart3,
Settings,
User,
Calendar, Calendar,
Mail, Circle,
Phone, Square,
Globe, Type,
Search, MousePointer,
Filter, CreditCard,
Download,
Upload,
Edit,
Trash2,
Plus,
Minus,
Check,
X,
ArrowRight,
ArrowLeft,
ArrowUp,
ArrowDown,
ChevronDown, ChevronDown,
ChevronUp, Tags as Tabs,
ChevronLeft, BarChart3,
ChevronRight, ToggleLeft,
Menu, User,
MoreHorizontal, Medal as Modal,
Star, Search,
Heart,
Share,
Bookmark,
Flag,
AlertCircle,
Info,
CheckCircle,
XCircle,
AlertTriangle,
HelpCircle
} from "lucide-react" } from "lucide-react"
const componentCategories = [ const COMPONENT_TYPES = [
{ { id: "data-table", name: "Data Table", icon: Table, category: "Data" },
name: "Basic", { id: "contextmenu", name: "Context Menu", icon: MousePointer, category: "Overlay" },
icon: Square, { id: "menubar", name: "Menubar", icon: Tabs, category: "Layout" },
components: [ { id: "carousel", name: "Carousel", icon: BarChart3, category: "Display" },
{ type: "button", name: "Button", icon: MousePointer }, { id: "drawer", name: "Drawer", icon: Modal, category: "Overlay" },
{ type: "input", name: "Input", icon: Type }, { id: "table", name: "Table", icon: Table, category: "Data" },
{ type: "textarea", name: "Textarea", icon: Type }, { id: "datepicker", name: "Date Picker", icon: Calendar, category: "Input" },
{ type: "card", name: "Card", icon: Square }, { id: "radiogroup", name: "Radio Group", icon: Circle, category: "Input" },
] { id: "checkbox", name: "Checkbox", icon: Square, category: "Input" },
}, { id: "input", name: "Input", icon: Type, category: "Input" },
{ { id: "textarea", name: "Textarea", icon: Type, category: "Input" },
name: "Form", { id: "button", name: "Button", icon: MousePointer, category: "Action" },
icon: Settings, { id: "card", name: "Card", icon: CreditCard, category: "Layout" },
components: [ { id: "select", name: "Select", icon: ChevronDown, category: "Input" },
{ type: "checkbox", name: "Checkbox", icon: Check }, { id: "tabs", name: "Tabs", icon: Tabs, category: "Layout" },
{ type: "switch", name: "Switch", icon: Settings }, { id: "progress", name: "Progress", icon: BarChart3, category: "Display" },
{ type: "select", name: "Select", icon: ChevronDown }, { id: "switch", name: "Switch", icon: ToggleLeft, category: "Input" },
{ type: "radiogroup", name: "Radio Group", icon: Circle }, { id: "avatar", name: "Avatar", icon: User, category: "Display" },
] { id: "dialog", name: "Dialog", icon: Modal, category: "Overlay" },
},
{
name: "Data",
icon: BarChart3,
components: [
{ type: "table", name: "Table", icon: Layout },
{ type: "progress", name: "Progress", icon: BarChart3 },
{ type: "tabs", name: "Tabs", icon: Layout },
]
},
{
name: "Media",
icon: Image,
components: [
{ type: "avatar", name: "Avatar", icon: User },
]
}
] ]
export function ComponentPalette() { function DraggableComponent({ component }: { component: (typeof COMPONENT_TYPES)[0] }) {
const [selectedCategory, setSelectedCategory] = useState("Basic") const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
const [searchQuery, setSearchQuery] = useState("") id: component.id,
})
const filteredComponents = componentCategories const style = transform
.find(cat => cat.name === selectedCategory) ? {
?.components.filter(comp => transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
comp.name.toLowerCase().includes(searchQuery.toLowerCase()) }
) || [] : undefined
return (
<div className="h-full flex flex-col bg-white">
{/* Header */}
<div className="p-4 border-b">
<h2 className="text-lg font-semibold text-gray-900">Components</h2>
<p className="text-sm text-gray-500">Drag components to canvas</p>
</div>
{/* Search */}
<div className="p-4 border-b">
<Input
placeholder="Search components..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
</div>
{/* Categories */}
<div className="p-4 border-b">
<div className="flex flex-wrap gap-2">
{componentCategories.map((category) => (
<Button
key={category.name}
variant={selectedCategory === category.name ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category.name)}
className="flex items-center gap-2"
>
<category.icon className="h-4 w-4" />
{category.name}
</Button>
))}
</div>
</div>
{/* Components List */}
<div className="flex-1 overflow-y-auto p-4">
<div className="grid grid-cols-2 gap-3">
{filteredComponents.map((component) => (
<ComponentPreview
key={component.type}
type={component.type}
name={component.name}
icon={component.icon}
/>
))}
</div>
{filteredComponents.length === 0 && (
<div className="text-center text-gray-500 py-8">
<Search className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No components found</p>
</div>
)}
</div>
</div>
)
}
interface ComponentPreviewProps {
type: string
name: string
icon: React.ComponentType<{ className?: string }>
}
function ComponentPreview({ type, name, icon: Icon }: ComponentPreviewProps) {
const [isDragging, setIsDragging] = useState(false)
const handleDragStart = (e: React.DragEvent) => {
setIsDragging(true)
e.dataTransfer.setData('application/json', JSON.stringify({ type }))
e.dataTransfer.effectAllowed = 'copy'
}
const handleDragEnd = () => {
setIsDragging(false)
}
return ( return (
<div <div
draggable ref={setNodeRef}
onDragStart={handleDragStart} style={style}
onDragEnd={handleDragEnd} {...listeners}
className={cn( {...attributes}
"p-3 border border-gray-200 rounded-lg cursor-grab hover:border-blue-300 hover:shadow-sm transition-all", className={`p-3 bg-card border border-border rounded-md cursor-grab hover:bg-accent/10 transition-colors ${
isDragging && "opacity-50 scale-95" isDragging ? "opacity-50" : ""
)} }`}
> >
<div className="flex flex-col items-center space-y-2"> <div className="flex items-center gap-2">
<div className="p-2 bg-gray-100 rounded-lg"> <component.icon className="w-4 h-4 text-muted-foreground" />
<Icon className="h-4 w-4 text-gray-600" /> <span className="text-sm font-medium text-card-foreground">{component.name}</span>
</div>
<span className="text-xs font-medium text-gray-700 text-center">
{name}
</span>
</div> </div>
</div> </div>
) )
} }
export function ComponentPalette() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedCategory, setSelectedCategory] = useState<string>("All")
const categories = ["All", ...Array.from(new Set(COMPONENT_TYPES.map((c) => c.category)))]
const filteredComponents = COMPONENT_TYPES.filter((component) => {
const matchesSearch = component.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesCategory = selectedCategory === "All" || component.category === selectedCategory
return matchesSearch && matchesCategory
})
return (
<div className="h-full flex flex-col overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-sidebar-border">
<h2 className="text-lg font-semibold text-sidebar-foreground mb-3">Components</h2>
{/* Search */}
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search components..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 bg-input border-border"
/>
</div>
{/* Category Filter */}
<div className="flex flex-wrap gap-1">
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-2 py-1 text-xs rounded-md transition-colors ${
selectedCategory === category
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "bg-sidebar hover:bg-sidebar-accent/20 text-sidebar-foreground"
}`}
>
{category}
</button>
))}
</div>
</div>
{/* Component List */}
<ScrollArea className="flex-1 component-panel-scroll">
<div className="p-4 space-y-2">
{filteredComponents.map((component) => (
<DraggableComponent key={component.id} component={component} />
))}
</div>
</ScrollArea>
</div>
)
}

View File

@ -1,135 +1,254 @@
"use client" "use client"
import { ComponentInstance } from "@/lib/store" import type React from "react"
import { useState } from "react"
import { type ComponentInstance, useEditorStore } from "@/lib/store"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Switch } from "@/components/ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Progress } from "@/components/ui/progress"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Calendar } from "@/components/ui/calendar"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarTrigger,
} from "@/components/ui/menubar"
import {
Carousel as UiCarousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Plus, Minus, User } from "lucide-react"
import { WireframeRenderer } from "./wireframe-renderer" import { WireframeRenderer } from "./wireframe-renderer"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Textarea } from "./ui/textarea"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Checkbox } from "./ui/checkbox"
import { Switch } from "./ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { RadioGroup, RadioGroupItem } from "./ui/radio-group"
import { Progress } from "./ui/progress"
import { Avatar, AvatarFallback } from "./ui/avatar"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { cn } from "@/lib/utils"
interface ComponentRendererProps { interface ComponentRendererProps {
component: ComponentInstance component: ComponentInstance
isSelected?: boolean onClick?: (e: React.MouseEvent) => void
onClick?: () => void
onMouseDown?: (e: React.MouseEvent) => void onMouseDown?: (e: React.MouseEvent) => void
} }
export function ComponentRenderer({ export function ComponentRenderer({ component, onClick, onMouseDown }: ComponentRendererProps) {
component, const { selectedComponent, updateComponent } = useEditorStore()
isSelected = false, const isSelected = selectedComponent?.id === component.id
onClick,
onMouseDown
}: ComponentRendererProps) {
const { type, props } = component
// Handle wireframe components const [checkboxState, setCheckboxState] = useState(component.props.checked || false)
if (type.startsWith('wireframe-')) { const [switchState, setSwitchState] = useState(component.props.checked || false)
return ( const [radioValue, setRadioValue] = useState(component.props.value || "")
<WireframeRenderer const [progressValue, setProgressValue] = useState(component.props.value || 50)
component={component} const [selectedDate, setSelectedDate] = useState<Date | undefined>(
onClick={onClick} component.props.selectedDate ? new Date(component.props.selectedDate) : undefined,
onMouseDown={onMouseDown} )
/> const [tableData, setTableData] = useState(
) component.props.rows || [
} ["John", "john@example.com"],
["Jane", "jane@example.com"],
["kenil", "kenil@example.com"],
["kavya", "kavya@example.com"],
["raj", "raj@example.com"]
],
)
const [editingCell, setEditingCell] = useState<{ row: number; col: number } | null>(null)
const [editValue, setEditValue] = useState("")
const [cardTitle, setCardTitle] = useState(component.props.title || "Card Title")
const [cardDescription, setCardDescription] = useState(component.props.description || "Card description")
const [cardContent, setCardContent] = useState(component.props.content || "Card content goes here")
const [editingCardField, setEditingCardField] = useState<string | null>(null)
const [dataTableSortAsc, setDataTableSortAsc] = useState(true)
const [dataTablePage, setDataTablePage] = useState(1)
const [dataTableSelected, setDataTableSelected] = useState<number[]>([])
const [tableSortAsc, setTableSortAsc] = useState(true)
const [tablePage, setTablePage] = useState(1)
const [tableSelected, setTableSelected] = useState<number[]>([])
// Handle regular UI components
const renderComponent = () => { const renderComponent = () => {
switch (type) { switch (component.type) {
case 'button': case "button":
return ( return (
<Button <Button
variant={props.variant || 'default'} variant={component.props.variant || "default"}
size={props.size || 'default'} onClick={(e) => {
className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")} e.stopPropagation()
onClick={onClick} window.alert(`${component.props.children || "Button"} was clicked`)
onMouseDown={onMouseDown} }}
> >
{props.children || 'Button'} {component.props.children || "Button"}
</Button> </Button>
) )
case 'input': case "input":
return ( return <Input placeholder={component.props.placeholder || "Enter text..."} className="w-full max-w-sm" />
<Input
placeholder={props.placeholder || 'Enter text...'}
className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}
onClick={onClick}
onMouseDown={onMouseDown}
readOnly
/>
)
case 'textarea': case "textarea":
return ( return <Textarea placeholder={component.props.placeholder || "Enter text..."} className="w-full max-w-sm" />
<Textarea
placeholder={props.placeholder || 'Enter text...'}
className={cn("w-full h-full resize-none", isSelected && "ring-2 ring-blue-500")}
onClick={onClick}
onMouseDown={onMouseDown}
readOnly
/>
)
case 'card': case "card":
return ( return (
<Card className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}> <Card className="w-full max-w-sm">
<CardHeader> <CardHeader>
<CardTitle className="text-sm">{props.title || 'Card Title'}</CardTitle> <CardTitle>
{editingCardField === "title" ? (
<Input
value={cardTitle}
onChange={(e) => setCardTitle(e.target.value)}
onBlur={() => {
setEditingCardField(null)
updateComponent(component.id, { props: { title: cardTitle } })
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setEditingCardField(null)
updateComponent(component.id, { props: { title: cardTitle } })
}
}}
className="text-lg font-semibold"
autoFocus
/>
) : (
<span
onClick={(e) => {
e.stopPropagation()
setEditingCardField("title")
}}
className="cursor-text hover:bg-muted/50 px-1 rounded"
>
{cardTitle}
</span>
)}
</CardTitle>
<CardDescription>
{editingCardField === "description" ? (
<Input
value={cardDescription}
onChange={(e) => setCardDescription(e.target.value)}
onBlur={() => {
setEditingCardField(null)
updateComponent(component.id, { props: { description: cardDescription } })
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setEditingCardField(null)
updateComponent(component.id, { props: { description: cardDescription } })
}
}}
autoFocus
/>
) : (
<span
onClick={(e) => {
e.stopPropagation()
setEditingCardField("description")
}}
className="cursor-text hover:bg-muted/50 px-1 rounded"
>
{cardDescription}
</span>
)}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-xs text-gray-600">{props.description || 'Card description'}</p> {editingCardField === "content" ? (
<Textarea
value={cardContent}
onChange={(e) => setCardContent(e.target.value)}
onBlur={() => {
setEditingCardField(null)
updateComponent(component.id, { props: { content: cardContent } })
}}
autoFocus
/>
) : (
<p
onClick={(e) => {
e.stopPropagation()
setEditingCardField("content")
}}
className="cursor-text hover:bg-muted/50 px-1 rounded"
>
{cardContent}
</p>
)}
</CardContent> </CardContent>
</Card> </Card>
) )
case 'checkbox': case "checkbox":
return ( return (
<div className={cn("flex items-center space-x-2 w-full h-full", isSelected && "ring-2 ring-blue-500")}> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id={component.id} id={component.id}
checked={props.checked || false} checked={checkboxState}
onClick={onClick} onCheckedChange={(checked) => {
onMouseDown={onMouseDown} setCheckboxState(!!checked)
updateComponent(component.id, { props: { checked: !!checked } })
}}
/> />
<label htmlFor={component.id} className="text-sm"> <Label htmlFor={component.id}>{component.props.label || "Checkbox"}</Label>
{props.label || 'Checkbox'}
</label>
</div> </div>
) )
case 'switch': case "switch":
return ( return (
<div className={cn("flex items-center space-x-2 w-full h-full", isSelected && "ring-2 ring-blue-500")}> <div className="flex items-center space-x-2">
<Switch <Switch
checked={props.checked || false} id={component.id}
onClick={onClick} checked={switchState}
onMouseDown={onMouseDown} onCheckedChange={(checked) => {
setSwitchState(checked)
updateComponent(component.id, { props: { checked } })
}}
/> />
<label className="text-sm"> <Label htmlFor={component.id}>{component.props.label || "Switch"}</Label>
{props.label || 'Switch'}
</label>
</div> </div>
) )
case 'select': case "select":
return ( return (
<Select> <Select>
<SelectTrigger className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}> <SelectTrigger className="w-full max-w-sm">
<SelectValue placeholder={props.placeholder || 'Select option...'} /> <SelectValue placeholder={component.props.placeholder || "Select option..."} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(props.options || ['Option 1', 'Option 2']).map((option: string, index: number) => ( {(component.props.options || ["Option 1", "Option 2"]).map((option: string, index: number) => (
<SelectItem key={index} value={option}> <SelectItem key={index} value={option.toLowerCase()}>
{option} {option}
</SelectItem> </SelectItem>
))} ))}
@ -137,116 +256,539 @@ export function ComponentRenderer({
</Select> </Select>
) )
case 'radiogroup': case "radiogroup":
return ( return (
<RadioGroup <RadioGroup
value={props.value || 'Option 1'} value={radioValue}
className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")} onValueChange={(value) => {
onClick={onClick} setRadioValue(value)
onMouseDown={onMouseDown} updateComponent(component.id, { props: { value } })
}}
> >
{(props.options || ['Option 1', 'Option 2']).map((option: string, index: number) => ( {(component.props.options || ["Option 1", "Option 2"]).map((option: string, index: number) => (
<div key={index} className="flex items-center space-x-2"> <div key={index} className="flex items-center space-x-2">
<RadioGroupItem value={option} id={`${component.id}-${index}`} /> <RadioGroupItem value={option} id={`${component.id}-${index}`} />
<label htmlFor={`${component.id}-${index}`} className="text-sm"> <Label htmlFor={`${component.id}-${index}`}>{option}</Label>
{option}
</label>
</div> </div>
))} ))}
</RadioGroup> </RadioGroup>
) )
case 'progress': case "progress":
return ( return (
<div className={cn("w-full h-full flex items-center", isSelected && "ring-2 ring-blue-500")}> <div className="w-full max-w-sm space-y-2">
<Progress <Progress value={progressValue} className="w-full" />
value={props.value || 50} <div className="flex items-center justify-between">
max={props.max || 100} <Button
className="w-full" size="sm"
onClick={onClick} variant="outline"
onMouseDown={onMouseDown} onClick={(e) => {
/> e.stopPropagation()
const newValue = Math.max(0, progressValue - 10)
setProgressValue(newValue)
updateComponent(component.id, { props: { value: newValue } })
}}
>
<Minus className="h-3 w-3" />
</Button>
<p className="text-sm text-muted-foreground">{progressValue}%</p>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
const newValue = Math.min(100, progressValue + 10)
setProgressValue(newValue)
updateComponent(component.id, { props: { value: newValue } })
}}
>
<Plus className="h-3 w-3" />
</Button>
</div>
</div> </div>
) )
case 'avatar': case "avatar":
return ( return (
<div className={cn("w-full h-full flex items-center justify-center", isSelected && "ring-2 ring-blue-500")}> <Avatar>
<Avatar onClick={onClick} onMouseDown={onMouseDown}> {component.props.src && <AvatarImage src={component.props.src || "/placeholder.svg"} alt="Avatar" />}
<AvatarFallback>{props.fallback || 'AB'}</AvatarFallback> <AvatarFallback>
</Avatar> {component.props.fallback ? component.props.fallback : <User className="h-4 w-4" />}
</div> </AvatarFallback>
</Avatar>
) )
case 'table': case "table": {
const headers: string[] = component.props.headers || ["Name", "Email"]
const pageSize: number = component.props.pageSize || 5
const sortIndex: number = component.props.sortIndex ?? 0
const sorted = [...tableData].sort((a, b) => {
const va = a[sortIndex]?.toString().toLowerCase() ?? ""
const vb = b[sortIndex]?.toString().toLowerCase() ?? ""
const cmp = va.localeCompare(vb)
return tableSortAsc ? cmp : -cmp
})
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize))
const page = Math.min(tablePage, totalPages)
const start = (page - 1) * pageSize
const pageRows = sorted.slice(start, start + pageSize)
const toggleRow = (idx: number) => {
setTableSelected((sel) => (sel.includes(idx) ? sel.filter((i) => i !== idx) : [...sel, idx]))
}
return ( return (
<div className={cn("w-full h-full overflow-auto", isSelected && "ring-2 ring-blue-500")}> <div className="w-full max-w-md space-y-2">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{(props.headers || ['Name', 'Email', 'Role']).map((header: string, index: number) => ( <TableHead className="w-[40px]">Sel</TableHead>
<TableHead key={index} className="text-xs">{header}</TableHead> {headers.map((header: string, index: number) => (
<TableHead key={index}>
<button
className="underline"
onClick={(e) => {
e.stopPropagation()
if (sortIndex === index) setTableSortAsc(!tableSortAsc)
updateComponent(component.id, { props: { sortIndex: index } })
}}
>
{header}
</button>
</TableHead>
))}
<TableHead className="w-[50px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pageRows.map((row: string[], rowIndex: number) => (
<TableRow key={start + rowIndex} className={tableSelected.includes(start + rowIndex) ? "bg-accent/20" : ""}>
<TableCell>
<Checkbox
id={`${component.id}-t-sel-${start + rowIndex}`}
checked={tableSelected.includes(start + rowIndex)}
onCheckedChange={() => toggleRow(start + rowIndex)}
/>
</TableCell>
{row.map((cell: string, cellIndex: number) => (
<TableCell key={cellIndex}>
{editingCell?.row === start + rowIndex && editingCell?.col === cellIndex ? (
<Input
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={() => {
const newData = [...sorted]
newData[start + rowIndex][cellIndex] = editValue
// map back to original order not required for display; persist merged
const merged = [...tableData]
merged[start + rowIndex] = newData[start + rowIndex]
setTableData(merged)
updateComponent(component.id, { props: { rows: merged } })
setEditingCell(null)
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const newData = [...sorted]
newData[start + rowIndex][cellIndex] = editValue
const merged = [...tableData]
merged[start + rowIndex] = newData[start + rowIndex]
setTableData(merged)
updateComponent(component.id, { props: { rows: merged } })
setEditingCell(null)
}
}}
autoFocus
className="h-8"
/>
) : (
<span
onClick={(e) => {
e.stopPropagation()
setEditingCell({ row: start + rowIndex, col: cellIndex })
setEditValue(cell)
}}
className="cursor-text hover:bg-muted/50 px-1 rounded block min-h-[20px]"
>
{cell}
</span>
)}
</TableCell>
))}
<TableCell>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
const merged = tableData.filter((_: string[], index: number) => index !== start + rowIndex)
setTableData(merged)
updateComponent(component.id, { props: { rows: merged } })
}}
>
<Minus className="h-3 w-3" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex items-center justify-between">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
setTablePage(Math.max(1, page - 1))
}}
>
Previous
</Button>
<span className="text-xs text-muted-foreground">Page {page} / {totalPages}</span>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
setTablePage(Math.min(totalPages, page + 1))
}}
>
Next
</Button>
</div>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
const newRow = Array(tableData[0]?.length || headers.length || 2).fill("New")
const newData = [...tableData, newRow]
setTableData(newData)
updateComponent(component.id, { props: { rows: newData } })
}}
className="w-full"
>
<Plus className="h-3 w-3 mr-1" />
Add Row
</Button>
</div>
)
}
case "data-table": {
const headers: string[] = component.props.headers || ["Name", "Email"]
const pageSize: number = component.props.pageSize || 5
const sortIndex: number = component.props.sortIndex ?? 0
const sorted = [...tableData].sort((a, b) => {
const va = a[sortIndex]?.toString().toLowerCase() ?? ""
const vb = b[sortIndex]?.toString().toLowerCase() ?? ""
const cmp = va.localeCompare(vb)
return dataTableSortAsc ? cmp : -cmp
})
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize))
const page = Math.min(dataTablePage, totalPages)
const start = (page - 1) * pageSize
const pageRows = sorted.slice(start, start + pageSize)
const toggleRow = (idx: number) => {
setDataTableSelected((sel) =>
sel.includes(idx) ? sel.filter((i) => i !== idx) : [...sel, idx],
)
}
return (
<div className="w-full max-w-md space-y-2">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[40px]">Sel</TableHead>
{headers.map((header: string, index: number) => (
<TableHead key={index}>
<button
className="underline"
onClick={(e) => {
e.stopPropagation()
if (sortIndex === index) setDataTableSortAsc(!dataTableSortAsc)
updateComponent(component.id, { props: { sortIndex: index } })
}}
>
{header}
</button>
</TableHead>
))} ))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{(props.rows || [ {pageRows.map((row: string[], rowIndex: number) => (
['John Doe', 'john@example.com', 'Admin'], <TableRow key={start + rowIndex} className={dataTableSelected.includes(start + rowIndex) ? "bg-accent/20" : ""}>
['Jane Smith', 'jane@example.com', 'User'] <TableCell>
]).map((row: string[], rowIndex: number) => ( <Checkbox
<TableRow key={rowIndex}> id={`${component.id}-sel-${start + rowIndex}`}
checked={dataTableSelected.includes(start + rowIndex)}
onCheckedChange={() => toggleRow(start + rowIndex)}
/>
</TableCell>
{row.map((cell: string, cellIndex: number) => ( {row.map((cell: string, cellIndex: number) => (
<TableCell key={cellIndex} className="text-xs">{cell}</TableCell> <TableCell key={cellIndex}>
{editingCell?.row === start + rowIndex && editingCell?.col === cellIndex ? (
<Input
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={() => {
const merged = [...tableData]
merged[start + rowIndex][cellIndex] = editValue
setTableData(merged)
updateComponent(component.id, { props: { rows: merged } })
setEditingCell(null)
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const merged = [...tableData]
merged[start + rowIndex][cellIndex] = editValue
setTableData(merged)
updateComponent(component.id, { props: { rows: merged } })
setEditingCell(null)
}
}}
autoFocus
className="h-8"
/>
) : (
<span
onClick={(e) => {
e.stopPropagation()
setEditingCell({ row: start + rowIndex, col: cellIndex })
setEditValue(cell)
}}
className="cursor-text hover:bg-muted/50 px-1 rounded block min-h-[20px]"
>
{cell}
</span>
)}
</TableCell>
))} ))}
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
const newRow = Array(headers.length || 2).fill("New")
const newData = [...tableData, newRow]
setTableData(newData)
updateComponent(component.id, { props: { rows: newData } })
}}
className="w-full"
>
<Plus className="h-3 w-3 mr-1" />
Add Row
</Button>
<div className="flex items-center justify-between">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
setDataTablePage(Math.max(1, page - 1))
}}
>
Prev
</Button>
<span className="text-xs text-muted-foreground">
Page {page} / {totalPages}
</span>
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation()
setDataTablePage(Math.min(totalPages, page + 1))
}}
>
Next
</Button>
</div>
</div>
)
}
case "contextmenu":
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="w-[220px] h-[120px] bg-muted/30 flex items-center justify-center rounded">
Right-click me
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuLabel>Quick Actions</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Copy</ContextMenuItem>
<ContextMenuItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Paste</ContextMenuItem>
<ContextMenuItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
case "menubar":
return (
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>New</MenubarItem>
<MenubarItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Open</MenubarItem>
<MenubarItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Save</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Cut</MenubarItem>
<MenubarItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Copy</MenubarItem>
<MenubarItem onClick={(e: React.MouseEvent) => e.stopPropagation()}>Paste</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
case "carousel":
return (
<div className="w-[260px]">
<UiCarousel className="relative">
<CarouselContent>
{(["One", "Two", "Three"] as string[]).map((label, idx) => (
<CarouselItem key={idx} className="p-4">
<Card className="h-[120px] flex items-center justify-center">
<CardContent className="p-0">
<span className="text-sm text-muted-foreground">{label}</span>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</UiCarousel>
</div> </div>
) )
case 'tabs': case "drawer":
return ( return (
<Tabs defaultValue="0" className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}> <Drawer>
<TabsList className="grid w-full grid-cols-2"> <DrawerTrigger asChild>
{(props.tabs || [ <Button variant="outline">Open Drawer</Button>
{ label: 'Tab 1', content: 'Content 1' }, </DrawerTrigger>
{ label: 'Tab 2', content: 'Content 2' } <DrawerContent>
]).map((tab: any, index: number) => ( <DrawerHeader>
<TabsTrigger key={index} value={index.toString()}> <DrawerTitle>{component.props.title || "Drawer Title"}</DrawerTitle>
<DrawerDescription>
{component.props.description || "This is a drawer. You can close it below."}
</DrawerDescription>
</DrawerHeader>
<div className="p-4">
<DrawerClose asChild>
<Button variant="secondary">Close</Button>
</DrawerClose>
</div>
</DrawerContent>
</Drawer>
)
case "tabs":
return (
<Tabs defaultValue="tab-0" className="w-full max-w-sm">
<TabsList>
{(component.props.tabs || [{ label: "Tab 1" }, { label: "Tab 2" }]).map((tab: any, index: number) => (
<TabsTrigger key={index} value={`tab-${index}`}>
{tab.label} {tab.label}
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
{(props.tabs || [ {(component.props.tabs || [{ content: "Content 1" }, { content: "Content 2" }]).map(
{ label: 'Tab 1', content: 'Content 1' }, (tab: any, index: number) => (
{ label: 'Tab 2', content: 'Content 2' } <TabsContent key={index} value={`tab-${index}`}>
]).map((tab: any, index: number) => ( {tab.content || `Content ${index + 1}`}
<TabsContent key={index} value={index.toString()} className="mt-2"> </TabsContent>
<div className="text-sm">{tab.content}</div> ),
</TabsContent> )}
))}
</Tabs> </Tabs>
) )
default: case "datepicker":
return (
<div
className="w-[360px]"
onMouseDown={(e) => e.stopPropagation()} // prevent dragging out of the component
onClick={(e) => e.stopPropagation()} // prevent clicking out of the component
>
<Calendar
mode="single"
selected={selectedDate}
onSelect={(date: Date | undefined) => {
setSelectedDate(date)
updateComponent(component.id, {
props: {
...component.props,
selectedDate: date?.toISOString(),
},
})
}}
className="rounded-md border w-[360px] p-4 [--cell-size:2.4rem]"
/>
</div>
)
case "dialog":
return ( return (
<div <Dialog>
className={cn( <DialogTrigger asChild>
"w-full h-full border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center text-gray-500 text-sm", <Button variant="outline">Open Dialog</Button>
isSelected && "ring-2 ring-blue-500" </DialogTrigger>
)} <DialogContent>
<DialogHeader>
<DialogTitle>{component.props.title || "Dialog Title"}</DialogTitle>
<DialogDescription>{component.props.description || "Dialog description"}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
)
// Wireframe components
case "wireframe-svg":
case "wireframe-rect":
case "wireframe-circle":
case "wireframe-text":
case "wireframe-line":
case "wireframe-path":
case "wireframe-group":
return (
<WireframeRenderer
component={component}
onClick={onClick} onClick={onClick}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
> />
{type}
</div>
) )
default:
return <div className="p-4 border border-dashed border-border rounded">Unknown Component</div>
} }
} }
return ( return (
<div className="w-full h-full"> <div
className={`absolute cursor-pointer transition-all ${
isSelected ? "ring-2 ring-accent ring-offset-2" : "hover:ring-1 hover:ring-border"
}`}
style={{
left: component.position.x,
top: component.position.y,
}}
onClick={onClick}
onMouseDown={onMouseDown}
>
{renderComponent()} {renderComponent()}
</div> </div>
) )

View File

@ -46,6 +46,13 @@ export function DualCanvasEditor({
// Initialize wireframe integration // Initialize wireframe integration
useWireframeIntegration() useWireframeIntegration()
// Auto-show wireframes when components are added
useEffect(() => {
if (components.length > 0 && components.some(comp => comp.type.startsWith('wireframe-'))) {
setShowWireframes(true)
}
}, [components, setShowWireframes])
const handleDeviceChange = (device: 'desktop' | 'tablet' | 'mobile') => { const handleDeviceChange = (device: 'desktop' | 'tablet' | 'mobile') => {
if (onDeviceChange) { if (onDeviceChange) {
onDeviceChange(device) onDeviceChange(device)
@ -143,6 +150,29 @@ export function DualCanvasEditor({
</Tabs> </Tabs>
{canvasMode === 'components' && ( {canvasMode === 'components' && (
<> <>
<Button
variant="outline"
size="sm"
onClick={() => {
// Switch to wireframe mode to generate wireframe
setCanvasMode('wireframe')
// Trigger wireframe generation after a short delay
setTimeout(() => {
window.dispatchEvent(new CustomEvent("tldraw:generate", {
detail: {
prompt: "Dashboard with header, left sidebar, 3 stats cards, a line chart and a data table, plus footer.",
device: selectedDevice
}
}))
// Switch back to components mode after generation
setTimeout(() => setCanvasMode('components'), 2000)
}, 100)
}}
className="flex items-center gap-2"
>
<PenTool className="h-4 w-4" />
Generate Wireframe
</Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -169,7 +199,7 @@ export function DualCanvasEditor({
</div> </div>
</header> </header>
<div className="h-[calc(100vh-100px)] w-full"> <div className="h-[calc(100%-80px)] w-full">
{canvasMode === 'wireframe' ? ( {canvasMode === 'wireframe' ? (
<div className="h-full w-full flex"> <div className="h-full w-full flex">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">

View File

@ -1407,7 +1407,7 @@ function AIMockupStep({
</div> </div>
{/* Dual Canvas Editor */} {/* Dual Canvas Editor */}
<div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden" style={{ height: '700px' }}> <div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden h-[80vh] min-h-[600px]">
<DualCanvasEditor <DualCanvasEditor
className="h-full w-full" className="h-full w-full"
onWireframeGenerated={handleWireframeGenerated} onWireframeGenerated={handleWireframeGenerated}

View File

@ -1,323 +1,264 @@
"use client" "use client"
import { useState, useEffect } from "react"
import { useEditorStore } from "@/lib/store" import { useEditorStore } from "@/lib/store"
import { Button } from "./ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "./ui/input" import { Input } from "@/components/ui/input"
import { Label } from "./ui/label" import { Label } from "@/components/ui/label"
import { Textarea } from "./ui/textarea" import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Switch } from "@/components/ui/switch"
import { Switch } from "./ui/switch" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Slider } from "./ui/slider" import { Textarea } from "@/components/ui/textarea"
import { ColorPicker } from "./ui/color-picker" import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "./ui/separator" import { Trash2, X } from "lucide-react"
import { Badge } from "./ui/badge"
import { cn } from "@/lib/utils"
import { Trash2, Copy, Move, RotateCcw } from "lucide-react"
export function PropertiesPanel() { export function PropertiesPanel() {
const { selectedComponent, updateComponent, removeComponent } = useEditorStore() const { selectedComponent, updateComponent, removeComponent, selectComponent } = useEditorStore()
const [localProps, setLocalProps] = useState<Record<string, any>>({})
useEffect(() => { if (!selectedComponent) {
if (selectedComponent) { return null
setLocalProps(selectedComponent.props) }
}
}, [selectedComponent])
const handlePropChange = (key: string, value: any) => { const handlePropChange = (key: string, value: any) => {
setLocalProps(prev => ({ ...prev, [key]: value })) updateComponent(selectedComponent.id, {
} props: { ...selectedComponent.props, [key]: value },
})
const handleSave = () => {
if (selectedComponent) {
updateComponent(selectedComponent.id, { props: localProps })
}
}
const handleReset = () => {
if (selectedComponent) {
setLocalProps(selectedComponent.props)
}
} }
const handleDelete = () => { const handleDelete = () => {
if (selectedComponent) { removeComponent(selectedComponent.id)
removeComponent(selectedComponent.id) selectComponent(null)
}
} }
if (!selectedComponent) { const renderPropertyEditor = () => {
return ( switch (selectedComponent.type) {
<div className="h-full flex items-center justify-center text-gray-500"> case "button":
<div className="text-center"> return (
<div className="text-4xl mb-2">🎯</div> <div className="space-y-4">
<p className="text-sm">Select a component to edit properties</p> <div>
</div> <Label htmlFor="button-text">Button Text</Label>
</div> <Input
) id="button-text"
} value={selectedComponent.props.children || ""}
onChange={(e) => handlePropChange("children", e.target.value)}
const renderPropertyEditor = (key: string, value: any) => { placeholder="Button text"
const type = typeof value />
switch (type) {
case 'string':
if (key.includes('color') || key.includes('Color')) {
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<div className="flex items-center space-x-2">
<Input
id={key}
value={value}
onChange={(e) => handlePropChange(key, e.target.value)}
className="text-xs"
/>
<div
className="w-6 h-6 rounded border"
style={{ backgroundColor: value }}
/>
</div>
</div> </div>
) <div>
} <Label htmlFor="button-variant">Variant</Label>
<Select
value={selectedComponent.props.variant || "default"}
onValueChange={(value) => handlePropChange("variant", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="destructive">Destructive</SelectItem>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="secondary">Secondary</SelectItem>
<SelectItem value="ghost">Ghost</SelectItem>
<SelectItem value="link">Link</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)
if (key.includes('text') || key.includes('placeholder') || key.includes('label')) { case "input":
return ( case "textarea":
<div key={key} className="space-y-2"> return (
<Label htmlFor={key} className="text-xs font-medium"> <div className="space-y-4">
{key.charAt(0).toUpperCase() + key.slice(1)} <div>
</Label> <Label htmlFor="placeholder">Placeholder</Label>
<Input
id="placeholder"
value={selectedComponent.props.placeholder || ""}
onChange={(e) => handlePropChange("placeholder", e.target.value)}
placeholder="Enter placeholder text"
/>
</div>
</div>
)
case "card":
return (
<div className="space-y-4">
<div>
<Label htmlFor="card-title">Title</Label>
<Input
id="card-title"
value={selectedComponent.props.title || ""}
onChange={(e) => handlePropChange("title", e.target.value)}
placeholder="Card title"
/>
</div>
<div>
<Label htmlFor="card-description">Description</Label>
<Textarea <Textarea
id={key} id="card-description"
value={value} value={selectedComponent.props.description || ""}
onChange={(e) => handlePropChange(key, e.target.value)} onChange={(e) => handlePropChange("description", e.target.value)}
className="text-xs min-h-[60px]" placeholder="Card description"
/> />
</div> </div>
)
}
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Input
id={key}
value={value}
onChange={(e) => handlePropChange(key, e.target.value)}
className="text-xs"
/>
</div> </div>
) )
case 'number': case "checkbox":
if (key.includes('size') || key.includes('width') || key.includes('height')) { case "switch":
return ( return (
<div key={key} className="space-y-2"> <div className="space-y-4">
<Label htmlFor={key} className="text-xs font-medium"> <div>
{key.charAt(0).toUpperCase() + key.slice(1)}: {value} <Label htmlFor="label">Label</Label>
</Label> <Input
<Slider id="label"
value={[value]} value={selectedComponent.props.label || ""}
onValueChange={([newValue]) => handlePropChange(key, newValue)} onChange={(e) => handlePropChange("label", e.target.value)}
min={0} placeholder="Label text"
max={key.includes('size') ? 100 : 500}
step={1}
className="w-full"
/> />
</div> </div>
) <div className="flex items-center space-x-2">
} <Switch
id="checked"
return ( checked={selectedComponent.props.checked || false}
<div key={key} className="space-y-2"> onCheckedChange={(checked) => handlePropChange("checked", checked)}
<Label htmlFor={key} className="text-xs font-medium"> />
{key.charAt(0).toUpperCase() + key.slice(1)} <Label htmlFor="checked">Checked</Label>
</Label>
<Input
id={key}
type="number"
value={value}
onChange={(e) => handlePropChange(key, Number(e.target.value))}
className="text-xs"
/>
</div>
)
case 'boolean':
return (
<div key={key} className="flex items-center justify-between">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Switch
id={key}
checked={value}
onCheckedChange={(checked) => handlePropChange(key, checked)}
/>
</div>
)
case 'object':
if (Array.isArray(value)) {
return (
<div key={key} className="space-y-2">
<Label className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)} ({value.length} items)
</Label>
<div className="space-y-1">
{value.map((item, index) => (
<div key={index} className="flex items-center space-x-2">
<Input
value={item}
onChange={(e) => {
const newArray = [...value]
newArray[index] = e.target.value
handlePropChange(key, newArray)
}}
className="text-xs"
/>
<Button
size="sm"
variant="outline"
onClick={() => {
const newArray = value.filter((_, i) => i !== index)
handlePropChange(key, newArray)
}}
className="h-6 w-6 p-0"
>
×
</Button>
</div>
))}
<Button
size="sm"
variant="outline"
onClick={() => {
const newArray = [...value, '']
handlePropChange(key, newArray)
}}
className="w-full text-xs"
>
Add Item
</Button>
</div>
</div> </div>
) </div>
} )
case "progress":
return ( return (
<div key={key} className="space-y-2"> <div className="space-y-4">
<Label className="text-xs font-medium"> <div>
{key.charAt(0).toUpperCase() + key.slice(1)} (Object) <Label htmlFor="progress-value">Value</Label>
</Label> <Input
<div className="text-xs text-gray-500 p-2 bg-gray-100 rounded"> id="progress-value"
{JSON.stringify(value, null, 2)} type="number"
min="0"
max="100"
value={selectedComponent.props.value || 50}
onChange={(e) => handlePropChange("value", Number.parseInt(e.target.value))}
/>
</div>
</div>
)
case "avatar":
return (
<div className="space-y-4">
<div>
<Label htmlFor="fallback">Fallback Text</Label>
<Input
id="fallback"
value={selectedComponent.props.fallback || ""}
onChange={(e) => handlePropChange("fallback", e.target.value)}
placeholder="AB"
maxLength={2}
/>
</div> </div>
</div> </div>
) )
default: default:
return ( return <div className="text-sm text-muted-foreground">No properties available for this component type.</div>
<div key={key} className="space-y-2">
<Label className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<div className="text-xs text-gray-500 p-2 bg-gray-100 rounded">
{String(value)}
</div>
</div>
)
} }
} }
return ( return (
<div className="h-full flex flex-col bg-white"> <div className="h-full flex flex-col overflow-hidden">
{/* Header */} {/* Header */}
<div className="p-4 border-b"> <div className="p-4 border-b border-sidebar-border flex items-center justify-between">
<div className="flex items-center justify-between"> <h2 className="text-lg font-semibold text-sidebar-foreground">Properties</h2>
<h3 className="text-lg font-semibold text-gray-900">Properties</h3> <Button variant="ghost" size="sm" onClick={() => selectComponent(null)} className="h-8 w-8 p-0">
<Badge variant="outline" className="text-xs"> <X className="h-4 w-4" />
{selectedComponent.type}
</Badge>
</div>
</div>
{/* Component Info */}
<div className="p-4 border-b bg-gray-50">
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-gray-500">ID:</span>
<span className="font-mono">{selectedComponent.id}</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-gray-500">Position:</span>
<span className="font-mono">
{Math.round(selectedComponent.position.x)}, {Math.round(selectedComponent.position.y)}
</span>
</div>
{selectedComponent.size && (
<div className="flex justify-between text-xs">
<span className="text-gray-500">Size:</span>
<span className="font-mono">
{Math.round(selectedComponent.size.width)} × {Math.round(selectedComponent.size.height)}
</span>
</div>
)}
</div>
</div>
{/* Properties */}
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
{Object.entries(localProps).map(([key, value]) =>
renderPropertyEditor(key, value)
)}
</div>
{Object.keys(localProps).length === 0 && (
<div className="text-center text-gray-500 py-8">
<div className="text-4xl mb-2"></div>
<p className="text-sm">No properties available</p>
</div>
)}
</div>
{/* Actions */}
<div className="p-4 border-t space-y-2">
<div className="flex space-x-2">
<Button
onClick={handleSave}
size="sm"
className="flex-1"
>
Save
</Button>
<Button
onClick={handleReset}
variant="outline"
size="sm"
className="flex-1"
>
Reset
</Button>
</div>
<Button
onClick={handleDelete}
variant="destructive"
size="sm"
className="w-full"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Component
</Button> </Button>
</div> </div>
<ScrollArea className="flex-1 properties-panel-scroll">
<div className="p-4 space-y-6">
{/* Component Info */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Component Info</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div>
<Label className="text-xs text-muted-foreground">Type</Label>
<p className="text-sm font-medium capitalize">{selectedComponent.type}</p>
</div>
<div>
<Label className="text-xs text-muted-foreground">ID</Label>
<p className="text-sm font-mono text-muted-foreground">{selectedComponent.id}</p>
</div>
</CardContent>
</Card>
{/* Properties */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Properties</CardTitle>
</CardHeader>
<CardContent>{renderPropertyEditor()}</CardContent>
</Card>
{/* Position */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Position</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-2">
<div>
<Label htmlFor="pos-x" className="text-xs">
X
</Label>
<Input
id="pos-x"
type="number"
value={selectedComponent.position.x}
onChange={(e) =>
updateComponent(selectedComponent.id, {
position: { ...selectedComponent.position, x: Number.parseInt(e.target.value) || 0 },
})
}
/>
</div>
<div>
<Label htmlFor="pos-y" className="text-xs">
Y
</Label>
<Input
id="pos-y"
type="number"
value={selectedComponent.position.y}
onChange={(e) =>
updateComponent(selectedComponent.id, {
position: { ...selectedComponent.position, y: Number.parseInt(e.target.value) || 0 },
})
}
/>
</div>
</div>
</CardContent>
</Card>
{/* Actions */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Actions</CardTitle>
</CardHeader>
<CardContent>
<Button variant="destructive" size="sm" onClick={handleDelete} className="w-full">
<Trash2 className="w-4 h-4 mr-2" />
Delete Component
</Button>
</CardContent>
</Card>
</div>
</ScrollArea>
</div> </div>
) )
} }

View File

@ -0,0 +1,213 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn(
"bg-popover absolute inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@ -0,0 +1,262 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -0,0 +1,115 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,256 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return <MenubarPrimitive.RadioGroup {...props} />
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-menubar-content-transform-origin]",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@ -1157,6 +1157,18 @@ export default function WireframeCanvas({
if (containsSVG) { if (containsSVG) {
await parseSVGAndRender(editor, responseText) await parseSVGAndRender(editor, responseText)
// Dispatch wireframe generation event for Components mode integration
window.dispatchEvent(new CustomEvent('wireframe:generated', {
detail: {
svgData: responseText,
deviceType: targetDevice,
prompt: prompt
}
}))
// Show success message
console.log('Wireframe generated successfully and integrated into Components mode')
// NEW: also convert SVG to component instances for the Components tab // NEW: also convert SVG to component instances for the Components tab
try { try {
const parsed = wireframeConverter.parseSVGToWireframe(responseText, targetDevice as 'desktop' | 'tablet' | 'mobile') const parsed = wireframeConverter.parseSVGToWireframe(responseText, targetDevice as 'desktop' | 'tablet' | 'mobile')
@ -1183,6 +1195,19 @@ export default function WireframeCanvas({
const jsonResponse = JSON.parse(responseText) const jsonResponse = JSON.parse(responseText)
if (jsonResponse.svg) { if (jsonResponse.svg) {
await parseSVGAndRender(editor, jsonResponse.svg) await parseSVGAndRender(editor, jsonResponse.svg)
// Dispatch wireframe generation event for Components mode integration
window.dispatchEvent(new CustomEvent('wireframe:generated', {
detail: {
svgData: jsonResponse.svg,
deviceType: targetDevice,
prompt: prompt
}
}))
// Show success message
console.log('Wireframe generated successfully and integrated into Components mode')
try { try {
const parsed = wireframeConverter.parseSVGToWireframe(jsonResponse.svg, targetDevice as 'desktop' | 'tablet' | 'mobile') const parsed = wireframeConverter.parseSVGToWireframe(jsonResponse.svg, targetDevice as 'desktop' | 'tablet' | 'mobile')
const componentInstances = wireframeConverter.convertToComponents(parsed) const componentInstances = wireframeConverter.convertToComponents(parsed)
@ -1198,6 +1223,16 @@ export default function WireframeCanvas({
}) })
} else if (jsonResponse.data && jsonResponse.data.svg) { } else if (jsonResponse.data && jsonResponse.data.svg) {
await parseSVGAndRender(editor, jsonResponse.data.svg) await parseSVGAndRender(editor, jsonResponse.data.svg)
// Dispatch wireframe generation event for Components mode integration
window.dispatchEvent(new CustomEvent('wireframe:generated', {
detail: {
svgData: jsonResponse.data.svg,
deviceType: targetDevice,
prompt: prompt
}
}))
try { try {
const parsed = wireframeConverter.parseSVGToWireframe(jsonResponse.data.svg, targetDevice as 'desktop' | 'tablet' | 'mobile') const parsed = wireframeConverter.parseSVGToWireframe(jsonResponse.data.svg, targetDevice as 'desktop' | 'tablet' | 'mobile')
const componentInstances = wireframeConverter.convertToComponents(parsed) const componentInstances = wireframeConverter.convertToComponents(parsed)
@ -1284,7 +1319,10 @@ export default function WireframeCanvas({
// Auto-save effect // Auto-save effect
useEffect(() => { useEffect(() => {
if (!autoSaveEnabled || !editorRef.current) return if (!autoSaveEnabled || !editorRef.current || !isAuthenticated) {
console.log('Auto-save disabled, no editor, or user not authenticated')
return
}
const autoSaveInterval = setInterval(() => { const autoSaveInterval = setInterval(() => {
if (editorRef.current) { if (editorRef.current) {
@ -1294,7 +1332,7 @@ export default function WireframeCanvas({
}, 30000) // Auto-save every 30 seconds }, 30000) // Auto-save every 30 seconds
return () => clearInterval(autoSaveInterval) return () => clearInterval(autoSaveInterval)
}, [autoSaveEnabled, selectedDevice]) // Include selectedDevice in dependencies }, [autoSaveEnabled, selectedDevice, isAuthenticated]) // Include selectedDevice and isAuthenticated in dependencies
// Load wireframe on mount if user is authenticated // Load wireframe on mount if user is authenticated
useEffect(() => { useEffect(() => {
@ -1316,6 +1354,8 @@ export default function WireframeCanvas({
const mostRecent = data.wireframes[0] // Assuming they're sorted by created_at desc const mostRecent = data.wireframes[0] // Assuming they're sorted by created_at desc
await loadWireframe(mostRecent.id) await loadWireframe(mostRecent.id)
} }
} else {
console.log('No recent wireframes found or error loading:', response.status)
} }
} catch (error) { } catch (error) {
console.log('No recent wireframes to load or error loading:', error) console.log('No recent wireframes to load or error loading:', error)
@ -1323,6 +1363,8 @@ export default function WireframeCanvas({
} }
loadRecentWireframe() loadRecentWireframe()
} else {
console.log('User not authenticated, skipping wireframe loading')
} }
}, [isAuthenticated, user?.id]) }, [isAuthenticated, user?.id])
@ -1373,10 +1415,11 @@ export default function WireframeCanvas({
Logged in as: {user?.username || 'User'} Logged in as: {user?.username || 'User'}
</div> </div>
<label className="flex items-center gap-2 text-xs text-gray-600"> <label className={`flex items-center gap-2 text-xs ${isAuthenticated ? 'text-gray-600' : 'text-gray-400'}`}>
<input <input
type="checkbox" type="checkbox"
checked={autoSaveEnabled} checked={autoSaveEnabled && isAuthenticated}
disabled={!isAuthenticated}
onChange={(e) => setAutoSaveEnabled(e.target.checked)} onChange={(e) => setAutoSaveEnabled(e.target.checked)}
className="w-3 h-3" className="w-3 h-3"
/> />
@ -1385,8 +1428,13 @@ export default function WireframeCanvas({
<button <button
onClick={() => editorRef.current && saveWireframe(editorRef.current, false)} onClick={() => editorRef.current && saveWireframe(editorRef.current, false)}
className="bg-blue-500 hover:bg-blue-600 text-white text-xs px-2 py-1 rounded transition-colors" disabled={!isAuthenticated}
title="Save wireframe (Ctrl+S)" className={`text-xs px-2 py-1 rounded transition-colors ${
isAuthenticated
? 'bg-blue-500 hover:bg-blue-600 text-white'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
title={isAuthenticated ? "Save wireframe (Ctrl+S)" : "Please sign in to save wireframes"}
> >
Save Save
</button> </button>

View File

@ -5,7 +5,7 @@ import { cn } from "@/lib/utils"
interface WireframeRendererProps { interface WireframeRendererProps {
component: ComponentInstance component: ComponentInstance
onClick?: () => void onClick?: (e: React.MouseEvent) => void
onMouseDown?: (e: React.MouseEvent) => void onMouseDown?: (e: React.MouseEvent) => void
} }
@ -22,9 +22,9 @@ export function WireframeRenderer({
y: element.y || 0, y: element.y || 0,
width: element.width || 100, width: element.width || 100,
height: element.height || 50, height: element.height || 50,
fill: element.fill || 'none', fill: element.fill === 'none' ? '#ffffff' : (element.fill || '#ffffff'),
stroke: element.stroke || '#000000', stroke: '#000000',
strokeWidth: element.strokeWidth || 1 strokeWidth: 3
} }
switch (element.type) { switch (element.type) {
@ -45,9 +45,9 @@ export function WireframeRenderer({
cx={element.cx || element.x + element.width / 2} cx={element.cx || element.x + element.width / 2}
cy={element.cy || element.y + element.height / 2} cy={element.cy || element.y + element.height / 2}
r={element.r || Math.min(element.width, element.height) / 2} r={element.r || Math.min(element.width, element.height) / 2}
fill={element.fill || 'none'} fill="#ffffff"
stroke={element.stroke || '#000000'} stroke="#000000"
strokeWidth={element.strokeWidth || 1} strokeWidth={3}
/> />
) )
@ -57,10 +57,12 @@ export function WireframeRenderer({
key={index} key={index}
x={element.x || 0} x={element.x || 0}
y={element.y || 0} y={element.y || 0}
fill={element.fill || '#000000'} fill="#000000"
fontSize={element.fontSize || 16} fontSize={element.fontSize || 16}
fontFamily={element.fontFamily || 'sans-serif'} fontFamily={element.fontFamily || 'Arial, sans-serif'}
fontWeight={element.fontWeight || 'normal'} fontWeight="bold"
textAnchor="middle"
style={{ color: '#000000' }}
> >
{element.text || ''} {element.text || ''}
</text> </text>
@ -74,8 +76,8 @@ export function WireframeRenderer({
y1={element.y || 0} y1={element.y || 0}
x2={(element.x || 0) + element.width} x2={(element.x || 0) + element.width}
y2={(element.y || 0) + element.height} y2={(element.y || 0) + element.height}
stroke={element.stroke || '#000000'} stroke="#000000"
strokeWidth={element.strokeWidth || 1} strokeWidth={3}
/> />
) )
@ -84,8 +86,10 @@ export function WireframeRenderer({
<rect <rect
key={index} key={index}
{...elementProps} {...elementProps}
fill="none" fill="#ffffff"
stroke="#000000"
strokeDasharray="5,5" strokeDasharray="5,5"
strokeWidth={3}
/> />
) )
@ -103,8 +107,10 @@ export function WireframeRenderer({
<rect <rect
key={index} key={index}
{...elementProps} {...elementProps}
fill="none" fill="#ffffff"
stroke="#000000"
strokeDasharray="3,3" strokeDasharray="3,3"
strokeWidth={3}
/> />
) )
} }
@ -125,13 +131,17 @@ export function WireframeRenderer({
<div className="p-2 bg-gray-100 border-b text-xs font-medium text-gray-600"> <div className="p-2 bg-gray-100 border-b text-xs font-medium text-gray-600">
Wireframe ({deviceType}) - {elements?.length || 0} elements Wireframe ({deviceType}) - {elements?.length || 0} elements
</div> </div>
<div className="p-4"> <div className="p-4 bg-white">
<svg <svg
width={width} width={width}
height={height} height={height}
viewBox={`0 0 ${width} ${height}`} viewBox={`0 0 ${width} ${height}`}
className="max-w-full max-h-full" className="max-w-full max-h-full"
style={{ border: '1px solid #e5e7eb' }} style={{
border: '3px solid #000000',
backgroundColor: '#ffffff',
borderRadius: '8px'
}}
> >
{elements?.map((element: any, index: number) => {elements?.map((element: any, index: number) =>
renderWireframeElement(element, index) renderWireframeElement(element, index)
@ -165,12 +175,17 @@ export function WireframeRenderer({
<div className="p-1 bg-gray-100 border-b text-xs font-medium text-gray-600"> <div className="p-1 bg-gray-100 border-b text-xs font-medium text-gray-600">
{type.replace('wireframe-', '')} {type.replace('wireframe-', '')}
</div> </div>
<div className="p-2"> <div className="p-2 bg-white">
<svg <svg
width={elementProps.width} width={elementProps.width}
height={elementProps.height} height={elementProps.height}
viewBox={`0 0 ${elementProps.width} ${elementProps.height}`} viewBox={`0 0 ${elementProps.width} ${elementProps.height}`}
className="max-w-full max-h-full" className="max-w-full max-h-full"
style={{
border: '2px solid #000000',
backgroundColor: '#ffffff',
borderRadius: '4px'
}}
> >
{renderWireframeElement({ type: type.replace('wireframe-', ''), ...elementProps }, 0)} {renderWireframeElement({ type: type.replace('wireframe-', ''), ...elementProps }, 0)}
</svg> </svg>

View File

@ -27,12 +27,11 @@ interface EditorState {
showWireframes: boolean showWireframes: boolean
wireframeRenderMode: 'svg' | 'editable' wireframeRenderMode: 'svg' | 'editable'
addComponent: (component: ComponentInstance) => void addComponent: (component: ComponentInstance) => void
addComponents: (components: ComponentInstance[]) => void
setComponents: (components: ComponentInstance[]) => void
updateComponent: (id: string, updates: Partial<ComponentInstance>) => void updateComponent: (id: string, updates: Partial<ComponentInstance>) => void
removeComponent: (id: string) => void removeComponent: (id: string) => void
selectComponent: (component: ComponentInstance | null) => void selectComponent: (component: ComponentInstance | null) => void
moveComponent: (id: string, position: { x: number; y: number }) => void moveComponent: (id: string, position: { x: number; y: number }) => void
setComponents: (components: ComponentInstance[]) => void
clearAll: () => void clearAll: () => void
addWireframe: (wireframe: WireframeData) => void addWireframe: (wireframe: WireframeData) => void
updateWireframe: (id: string, updates: Partial<WireframeData>) => void updateWireframe: (id: string, updates: Partial<WireframeData>) => void
@ -59,13 +58,6 @@ export const useEditorStore = create<EditorState>()(
components: [...state.components, component], components: [...state.components, component],
})), })),
addComponents: (components) =>
set((state) => ({
components: [...state.components, ...components],
})),
setComponents: (components) => set({ components }),
updateComponent: (id, updates) => updateComponent: (id, updates) =>
set((state) => ({ set((state) => ({
components: state.components.map((comp) => components: state.components.map((comp) =>
@ -102,6 +94,8 @@ export const useEditorStore = create<EditorState>()(
components: state.components.map((comp) => (comp.id === id ? { ...comp, position } : comp)), components: state.components.map((comp) => (comp.id === id ? { ...comp, position } : comp)),
})), })),
setComponents: (components) => set({ components }),
clearAll: () => clearAll: () =>
set({ set({
components: [], components: [],

View File

@ -0,0 +1,90 @@
export interface ParsedWireframe {
id: string
elements: WireframeElement[]
metadata: {
deviceType: 'desktop' | 'tablet' | 'mobile'
createdAt: Date
updatedAt: Date
}
}
export interface WireframeElement {
id: string
type: 'rect' | 'circle' | 'text' | 'line' | 'path' | 'group'
x: number
y: number
width?: number
height?: number
content?: string
style?: {
fill?: string
stroke?: string
strokeWidth?: number
fontSize?: number
fontFamily?: string
opacity?: number
}
children?: WireframeElement[]
}
export function parseWireframeData(data: any): ParsedWireframe {
// This is a placeholder implementation
// In a real application, this would parse the actual wireframe data
return {
id: data.id || `wireframe-${Date.now()}`,
elements: data.elements || [],
metadata: {
deviceType: data.deviceType || 'desktop',
createdAt: new Date(),
updatedAt: new Date(),
},
}
}
// Wireframe converter service
export const wireframeConverter = {
parseSVGToWireframe: (svgData: string, deviceType: 'desktop' | 'tablet' | 'mobile'): ParsedWireframe => {
// Placeholder implementation
return {
id: `wireframe-${Date.now()}`,
elements: [],
metadata: {
deviceType,
createdAt: new Date(),
updatedAt: new Date(),
},
}
},
convertToComponents: (wireframe: ParsedWireframe) => {
// Convert wireframe elements to individual components
return wireframe.elements.map(element => ({
id: `${element.type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type: `wireframe-${element.type}`,
props: {
...element,
svgData: element
},
position: {
x: element.x,
y: element.y
}
}))
},
convertToSVGComponent: (wireframe: ParsedWireframe) => {
// Convert entire wireframe to a single SVG component
return {
id: `wireframe-svg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type: 'wireframe-svg',
props: {
svgData: wireframe,
deviceType: wireframe.metadata.deviceType
},
position: {
x: 0,
y: 0
}
}
}
}

View File

@ -2,7 +2,7 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useEditorStore } from './store' import { useEditorStore } from './store'
import { wireframeConverter, ParsedWireframe } from './wireframe-converter.tsx' import { wireframeConverter, ParsedWireframe } from './wireframe-converter'
/** /**
* Hook to integrate wireframe data from the backend with the component canvas * Hook to integrate wireframe data from the backend with the component canvas