Generated by Spark: Integrate atomic components into more organism-level components throughout the app

This commit is contained in:
2026-01-17 16:23:59 +00:00
committed by GitHub
parent ba78f094ff
commit d9ecf25ca5
8 changed files with 331 additions and 308 deletions

View File

@@ -3,10 +3,7 @@ import { CodeError } from '@/types/errors'
import { ProjectFile } from '@/types/project'
import { ErrorRepairService } from '@/lib/error-repair-service'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import {
Warning,
X,
@@ -23,6 +20,19 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
Badge,
ActionButton,
Stack,
Flex,
Heading,
Text,
EmptyState,
IconText,
Code,
StatusIcon,
PanelHeader
} from '@/components/atoms'
interface ErrorPanelProps {
files: ProjectFile[]
@@ -210,14 +220,13 @@ export function ErrorPanel({ files, onFileChange, onFileSelect }: ErrorPanelProp
return (
<div className="h-full flex flex-col bg-background">
<div className="border-b border-border bg-card px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Wrench size={20} weight="duotone" className="text-accent" />
<h2 className="text-lg font-semibold">Error Detection & Repair</h2>
</div>
<Flex align="center" justify="between">
<Flex align="center" gap="md">
<IconText icon={<Wrench size={20} weight="duotone" className="text-accent" />}>
<Heading level={3}>Error Detection & Repair</Heading>
</IconText>
{errors.length > 0 && (
<div className="flex gap-2">
<Flex gap="sm">
{errorCount > 0 && (
<Badge variant="destructive">
{errorCount} {errorCount === 1 ? 'Error' : 'Errors'}
@@ -228,85 +237,81 @@ export function ErrorPanel({ files, onFileChange, onFileSelect }: ErrorPanelProp
{warningCount} {warningCount === 1 ? 'Warning' : 'Warnings'}
</Badge>
)}
</div>
</Flex>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
</Flex>
<Flex gap="sm">
<ActionButton
icon={<Lightning size={16} />}
label={isScanning ? 'Scanning...' : 'Scan'}
onClick={scanForErrors}
disabled={isScanning || isRepairing}
>
<Lightning size={16} className="mr-2" />
{isScanning ? 'Scanning...' : 'Scan'}
</Button>
<Button
variant="outline"
/>
<ActionButton
icon={<Wrench size={16} />}
label={isRepairing ? 'Repairing...' : 'Repair All'}
onClick={repairAllErrors}
disabled={errors.length === 0 || isRepairing || isScanning}
>
<Wrench size={16} className="mr-2" />
{isRepairing ? 'Repairing...' : 'Repair All'}
</Button>
</div>
</div>
variant="default"
/>
</Flex>
</Flex>
</div>
<ScrollArea className="flex-1">
<div className="p-6">
{errors.length === 0 && !isScanning && (
<Card className="p-8 text-center">
<CheckCircle size={48} weight="duotone" className="text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No Issues Found</h3>
<p className="text-sm text-muted-foreground">
All files are looking good! Click "Scan" to check again.
</p>
</Card>
<EmptyState
icon={<CheckCircle size={48} weight="duotone" className="text-green-500" />}
title="No Issues Found"
description="All files are looking good! Click 'Scan' to check again."
/>
)}
{isScanning && (
<Card className="p-8 text-center">
<Lightning size={48} weight="duotone" className="text-accent mx-auto mb-4 animate-pulse" />
<h3 className="text-lg font-semibold mb-2">Scanning Files...</h3>
<p className="text-sm text-muted-foreground">
Analyzing your code for errors and issues
</p>
</Card>
<EmptyState
icon={<Lightning size={48} weight="duotone" className="text-accent animate-pulse" />}
title="Scanning Files..."
description="Analyzing your code for errors and issues"
/>
)}
{errors.length > 0 && (
<div className="space-y-4">
<Stack direction="vertical" spacing="md">
{Object.entries(errorsByFile).map(([fileId, fileErrors]) => {
const file = files.find(f => f.id === fileId)
if (!file) return null
return (
<Card key={fileId} className="overflow-hidden">
<div className="bg-muted px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<FileCode size={18} weight="duotone" />
<span className="font-medium">{file.name}</span>
<Badge variant="outline">
{fileErrors.length} {fileErrors.length === 1 ? 'issue' : 'issues'}
</Badge>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => onFileSelect(fileId)}
>
<ArrowRight size={14} className="mr-1" />
Open
</Button>
<Button
size="sm"
onClick={() => repairFileWithContext(fileId)}
disabled={isRepairing}
>
<Wrench size={14} className="mr-1" />
Repair
</Button>
</div>
<div className="bg-muted px-4 py-3">
<Flex align="center" justify="between">
<Flex align="center" gap="sm">
<FileCode size={18} weight="duotone" />
<Text className="font-medium">{file.name}</Text>
<Badge variant="outline" size="sm">
{fileErrors.length} {fileErrors.length === 1 ? 'issue' : 'issues'}
</Badge>
</Flex>
<Flex gap="sm">
<ActionButton
icon={<ArrowRight size={14} />}
label="Open"
onClick={() => onFileSelect(fileId)}
variant="outline"
size="sm"
/>
<ActionButton
icon={<Wrench size={14} />}
label="Repair"
onClick={() => repairFileWithContext(fileId)}
disabled={isRepairing}
variant="default"
size="sm"
/>
</Flex>
</Flex>
</div>
<div className="p-4 space-y-2">
@@ -317,48 +322,46 @@ export function ErrorPanel({ files, onFileChange, onFileSelect }: ErrorPanelProp
{getSeverityIcon(error.severity)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Badge variant={getSeverityColor(error.severity) as any} className="text-xs">
<Flex align="center" gap="sm" className="mb-1">
<Badge variant={getSeverityColor(error.severity) as any} size="sm">
{error.type}
</Badge>
{error.line && (
<span className="text-xs text-muted-foreground">
<Text variant="caption">
Line {error.line}
</span>
</Text>
)}
{error.isFixed && (
<Badge variant="outline" className="text-green-500 border-green-500">
<Badge variant="outline" size="sm" className="text-green-500 border-green-500">
<CheckCircle size={12} className="mr-1" />
Fixed
</Badge>
)}
</div>
<p className="text-sm mb-2">{error.message}</p>
</Flex>
<Text variant="body" className="mb-2">{error.message}</Text>
{error.code && (
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-auto p-0 text-xs text-accent hover:text-accent/80"
<button
className="text-xs text-accent hover:text-accent/80 underline"
>
{expandedErrors.has(error.id) ? 'Hide' : 'Show'} code
</Button>
</button>
</CollapsibleTrigger>
)}
</div>
<Button
size="sm"
variant="outline"
<ActionButton
icon={<Wrench size={14} />}
label=""
onClick={() => repairSingleError(error)}
disabled={isRepairing || error.isFixed}
>
<Wrench size={14} />
</Button>
variant="outline"
size="sm"
/>
</div>
{error.code && (
<CollapsibleContent>
<div className="ml-8 mt-2 p-3 bg-muted rounded text-xs font-mono">
{error.code}
<div className="ml-8 mt-2 p-3 bg-muted rounded">
<Code className="text-xs">{error.code}</Code>
</div>
</CollapsibleContent>
)}
@@ -368,7 +371,7 @@ export function ErrorPanel({ files, onFileChange, onFileSelect }: ErrorPanelProp
</Card>
)
})}
</div>
</Stack>
)}
</div>
</ScrollArea>

View File

@@ -1,5 +1,4 @@
import { useNavigate } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import {
House,
@@ -11,6 +10,18 @@ import {
Lightning,
ChartBar
} from '@phosphor-icons/react'
import {
EmptyState,
ActionButton,
Stack,
Heading,
Text,
Kbd,
IconWrapper,
GlowCard,
ResponsiveGrid,
Container
} from '@/components/atoms'
export function NotFoundPage() {
const navigate = useNavigate()
@@ -24,92 +35,89 @@ export function NotFoundPage() {
return (
<div className="h-full w-full flex items-center justify-center bg-gradient-to-br from-background via-background to-muted/20 p-6">
<div className="max-w-3xl w-full space-y-8">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-accent/20 backdrop-blur-sm mb-4">
<Compass className="text-primary" size={64} weight="duotone" />
<Container size="lg">
<Stack direction="vertical" spacing="xl" className="items-center">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-accent/20 backdrop-blur-sm mb-4">
<Compass className="text-primary" size={64} weight="duotone" />
</div>
<Heading level={1} className="text-6xl">404</Heading>
<Heading level={2}>Page Not Found</Heading>
<Text variant="muted" className="max-w-md mx-auto">
The page you're looking for doesn't exist or may have been moved.
</Text>
</div>
<h1 className="text-6xl font-bold text-foreground">404</h1>
<h2 className="text-2xl font-semibold text-foreground">Page Not Found</h2>
<p className="text-muted-foreground max-w-md mx-auto">
The page you're looking for doesn't exist or may have been moved.
</p>
</div>
<div className="flex flex-wrap gap-4 justify-center">
<Button
onClick={() => navigate(-1)}
variant="outline"
size="lg"
className="gap-2"
>
<ArrowLeft />
Go Back
</Button>
<Button
onClick={() => navigate('/')}
size="lg"
className="gap-2"
>
<House />
Home
</Button>
<Button
onClick={() => {
const searchBtn = document.querySelector('[data-search-trigger]') as HTMLElement
searchBtn?.click()
}}
variant="secondary"
size="lg"
className="gap-2"
>
<MagnifyingGlass />
Search
</Button>
</div>
<Stack direction="horizontal" spacing="md" className="flex-wrap justify-center">
<ActionButton
icon={<ArrowLeft size={20} />}
label="Go Back"
onClick={() => navigate(-1)}
variant="outline"
size="lg"
/>
<ActionButton
icon={<House size={20} />}
label="Home"
onClick={() => navigate('/')}
variant="default"
size="lg"
/>
<ActionButton
icon={<MagnifyingGlass size={20} />}
label="Search"
onClick={() => {
const searchBtn = document.querySelector('[data-search-trigger]') as HTMLElement
searchBtn?.click()
}}
variant="outline"
size="lg"
/>
</Stack>
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground text-center">
Quick Links
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{quickLinks.map((link) => {
const Icon = link.icon
return (
<Card
key={link.path}
className="p-4 hover:bg-accent/50 transition-colors cursor-pointer group"
onClick={() => navigate(link.path)}
>
<div className="flex flex-col items-center text-center gap-3">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon className="text-primary" size={24} weight="duotone" />
</div>
<div>
<h4 className="font-medium text-sm">{link.label}</h4>
<p className="text-xs text-muted-foreground mt-1">
{link.description}
</p>
</div>
</div>
</Card>
)
})}
<Stack direction="vertical" spacing="md" className="w-full">
<Text variant="muted" className="text-center font-medium">
Quick Links
</Text>
<ResponsiveGrid columns={4} gap="md">
{quickLinks.map((link) => {
const Icon = link.icon
return (
<GlowCard
key={link.path}
onClick={() => navigate(link.path)}
className="cursor-pointer"
>
<Stack direction="vertical" spacing="md" className="items-center text-center">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
<Icon className="text-primary" size={24} weight="duotone" />
</div>
<Stack direction="vertical" spacing="xs">
<Heading level={4}>{link.label}</Heading>
<Text variant="muted" className="text-xs">
{link.description}
</Text>
</Stack>
</Stack>
</GlowCard>
)
})}
</ResponsiveGrid>
</Stack>
<div className="text-center">
<Text variant="caption">
Need help? Check the keyboard shortcuts with{' '}
<Kbd>Ctrl</Kbd>
{' + '}
<Kbd>/</Kbd>
</Text>
</div>
</div>
<div className="text-center">
<p className="text-xs text-muted-foreground">
Need help? Check the keyboard shortcuts with{' '}
<kbd className="px-2 py-1 rounded bg-muted text-xs font-mono">Ctrl</kbd>
{' + '}
<kbd className="px-2 py-1 rounded bg-muted text-xs font-mono">/</kbd>
</p>
</div>
</div>
</Stack>
</Container>
</div>
)
}

View File

@@ -10,6 +10,7 @@ export interface EmptyStateProps {
label: string
onClick: () => void
}
children?: ReactNode
className?: string
}
@@ -18,6 +19,7 @@ export function EmptyState({
title,
description,
action,
children,
className,
}: EmptyStateProps) {
return (
@@ -43,6 +45,7 @@ export function EmptyState({
{action.label}
</Button>
)}
{children}
</div>
)
}

View File

@@ -1,6 +1,11 @@
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { DataSourceCard } from '@/components/molecules/DataSourceCard'
import { DataSourceEditorDialog } from '@/components/molecules/DataSourceEditorDialog'
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
@@ -8,11 +13,14 @@ import { DataSource, DataSourceType } from '@/types/json-ui'
import { Plus, Database, Function, FileText } from '@phosphor-icons/react'
import { toast } from 'sonner'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
EmptyState,
ActionButton,
Heading,
Text,
IconText,
Stack,
Section
} from '@/components/atoms'
interface DataSourceManagerProps {
dataSources: DataSource[]
@@ -76,18 +84,22 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Data Sources</CardTitle>
<CardDescription>
<Stack direction="vertical" spacing="xs">
<Heading level={2}>Data Sources</Heading>
<Text variant="muted">
Manage KV storage, computed values, and static data
</CardDescription>
</div>
</Text>
</Stack>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<Plus className="w-4 h-4 mr-2" />
Add Data Source
</Button>
<div>
<ActionButton
icon={<Plus size={16} />}
label="Add Data Source"
variant="default"
onClick={() => {}}
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleAddDataSource('kv')}>
@@ -108,24 +120,22 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
</CardHeader>
<CardContent>
{localSources.length === 0 ? (
<div className="text-center py-12">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
<Database className="w-8 h-8 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-2">No data sources yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Create your first data source to start binding data to components
</p>
</div>
<EmptyState
icon={<Database size={48} weight="duotone" />}
title="No data sources yet"
description="Create your first data source to start binding data to components"
/>
) : (
<div className="space-y-6">
<Stack direction="vertical" spacing="xl">
{groupedSources.kv.length > 0 && (
<div>
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Database className="w-4 h-4" />
<Section>
<IconText
icon={<Database size={16} />}
className="text-sm font-semibold mb-3"
>
KV Store ({groupedSources.kv.length})
</h3>
<div className="space-y-2">
</IconText>
<Stack direction="vertical" spacing="sm">
{groupedSources.kv.map(ds => (
<DataSourceCard
key={ds.id}
@@ -135,17 +145,19 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
onDelete={handleDeleteSource}
/>
))}
</div>
</div>
</Stack>
</Section>
)}
{groupedSources.static.length > 0 && (
<div>
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<FileText className="w-4 h-4" />
<Section>
<IconText
icon={<FileText size={16} />}
className="text-sm font-semibold mb-3"
>
Static Data ({groupedSources.static.length})
</h3>
<div className="space-y-2">
</IconText>
<Stack direction="vertical" spacing="sm">
{groupedSources.static.map(ds => (
<DataSourceCard
key={ds.id}
@@ -155,17 +167,19 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
onDelete={handleDeleteSource}
/>
))}
</div>
</div>
</Stack>
</Section>
)}
{groupedSources.computed.length > 0 && (
<div>
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Function className="w-4 h-4" />
<Section>
<IconText
icon={<Function size={16} />}
className="text-sm font-semibold mb-3"
>
Computed Values ({groupedSources.computed.length})
</h3>
<div className="space-y-2">
</IconText>
<Stack direction="vertical" spacing="sm">
{groupedSources.computed.map(ds => (
<DataSourceCard
key={ds.id}
@@ -175,10 +189,10 @@ export function DataSourceManager({ dataSources, onChange }: DataSourceManagerPr
onDelete={handleDeleteSource}
/>
))}
</div>
</div>
</Stack>
</Section>
)}
</div>
</Stack>
)}
</CardContent>
</Card>

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button'
import { Plus, Folder } from '@phosphor-icons/react'
import { EmptyState, ActionButton, Stack } from '@/components/atoms'
interface EmptyCanvasStateProps {
onAddFirstComponent?: () => void
@@ -8,32 +8,31 @@ interface EmptyCanvasStateProps {
export function EmptyCanvasState({ onAddFirstComponent, onImportSchema }: EmptyCanvasStateProps) {
return (
<div className="h-full flex flex-col items-center justify-center p-8 text-center bg-muted/20">
<div className="mb-6 relative">
<div className="w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center">
<Folder className="w-12 h-12 text-primary" weight="duotone" />
</div>
</div>
<h3 className="text-xl font-semibold mb-2">Empty Canvas</h3>
<p className="text-sm text-muted-foreground max-w-md mb-6">
Start building your UI by dragging components from the left panel, or import an existing schema.
</p>
<div className="flex items-center gap-3">
{onImportSchema && (
<Button variant="outline" onClick={onImportSchema}>
<Folder className="w-4 h-4 mr-2" />
Import Schema
</Button>
)}
{onAddFirstComponent && (
<Button onClick={onAddFirstComponent}>
<Plus className="w-4 h-4 mr-2" />
Add Component
</Button>
)}
</div>
<div className="h-full flex flex-col items-center justify-center p-8 bg-muted/20">
<EmptyState
icon={<Folder size={64} weight="duotone" />}
title="Empty Canvas"
description="Start building your UI by dragging components from the left panel, or import an existing schema."
>
<Stack direction="horizontal" spacing="md" className="mt-4">
{onImportSchema && (
<ActionButton
icon={<Folder size={16} />}
label="Import Schema"
onClick={onImportSchema}
variant="outline"
/>
)}
{onAddFirstComponent && (
<ActionButton
icon={<Plus size={16} />}
label="Add Component"
onClick={onAddFirstComponent}
variant="default"
/>
)}
</Stack>
</EmptyState>
</div>
)
}

View File

@@ -1,8 +1,8 @@
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Code, Eye } from '@phosphor-icons/react'
import { UIComponent } from '@/types/json-ui'
import { PanelHeader, Text, Code as CodeAtom, Stack, IconText } from '@/components/atoms'
interface SchemaCodeViewerProps {
components: UIComponent[]
@@ -14,14 +14,7 @@ export function SchemaCodeViewer({ components, schema }: SchemaCodeViewerProps)
return (
<div className="h-full flex flex-col bg-card">
<div className="border-b border-border px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Code className="w-5 h-5 text-primary" />
<h3 className="font-semibold">Schema Output</h3>
</div>
</div>
</div>
<PanelHeader title="Schema Output" icon={<Code size={20} weight="duotone" />} />
<Tabs defaultValue="json" className="flex-1 flex flex-col">
<TabsList className="w-full justify-start px-4 pt-2">
@@ -39,9 +32,9 @@ export function SchemaCodeViewer({ components, schema }: SchemaCodeViewerProps)
<TabsContent value="preview" className="flex-1 m-0 mt-2">
<div className="p-4">
<p className="text-sm text-muted-foreground">
<Text variant="muted">
Live preview coming soon
</p>
</Text>
</div>
</TabsContent>
</Tabs>

View File

@@ -1,5 +1,5 @@
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { Badge, Chip, Text, Flex } from '@/components/atoms'
interface SchemaEditorStatusBarProps {
componentCount: number
@@ -16,31 +16,35 @@ export function SchemaEditorStatusBar({
}: SchemaEditorStatusBarProps) {
return (
<div className={cn(
"border-t border-border px-4 py-2 bg-card flex items-center justify-between text-xs text-muted-foreground",
"border-t border-border px-4 py-2 bg-card flex items-center justify-between",
className
)}>
<div className="flex items-center gap-4">
<span>
<Flex align="center" gap="lg">
<Text variant="caption">
<span className="font-medium text-foreground">{componentCount}</span> component{componentCount !== 1 ? 's' : ''}
</span>
</Text>
{selectedComponentType && (
<div className="flex items-center gap-2">
<span>Selected:</span>
<Badge variant="secondary" className="text-xs font-mono">
<Flex align="center" gap="sm">
<Text variant="caption">Selected:</Text>
<Chip
variant="default"
size="sm"
className="font-mono"
>
{selectedComponentType}
</Badge>
</div>
</Chip>
</Flex>
)}
</div>
</Flex>
<div className="flex items-center gap-2">
<Flex align="center" gap="sm">
{hasUnsavedChanges && (
<Badge variant="outline" className="text-xs">
<Badge variant="outline" size="sm">
Unsaved changes
</Badge>
)}
</div>
</Flex>
</div>
)
}

View File

@@ -1,5 +1,3 @@
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import {
Download,
Upload,
@@ -7,6 +5,9 @@ import {
Trash,
Copy,
} from '@phosphor-icons/react'
import { Heading, TextGradient, Text, Separator, Stack } from '@/components/atoms'
import { ActionBar } from '@/components/molecules'
import { ActionButton } from '@/components/atoms'
interface SchemaEditorToolbarProps {
onImport: () => void
@@ -26,58 +27,56 @@ export function SchemaEditorToolbar({
return (
<div className="border-b border-border px-6 py-3 bg-card">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
<Stack direction="vertical" spacing="xs">
<TextGradient
from="primary"
to="accent"
className="text-2xl font-bold"
>
Schema Editor
</h1>
<p className="text-sm text-muted-foreground mt-1">
</TextGradient>
<Text variant="muted">
Build JSON UI schemas with drag-and-drop
</p>
</div>
</Text>
</Stack>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
<ActionButton
icon={<Upload size={16} />}
label="Import"
onClick={onImport}
>
<Upload className="w-4 h-4 mr-2" />
Import
</Button>
<Button
variant="outline"
size="sm"
/>
<ActionButton
icon={<Copy size={16} />}
label="Copy JSON"
onClick={onCopy}
>
<Copy className="w-4 h-4 mr-2" />
Copy JSON
</Button>
<Button
variant="outline"
size="sm"
/>
<ActionButton
icon={<Download size={16} />}
label="Export"
onClick={onExport}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
variant="outline"
size="sm"
/>
<Separator orientation="vertical" className="h-6" />
<Button
variant="outline"
size="sm"
<ActionButton
icon={<Play size={16} />}
label="Preview"
onClick={onPreview}
>
<Play className="w-4 h-4 mr-2" />
Preview
</Button>
<Button
variant="outline"
size="sm"
/>
<ActionButton
icon={<Trash size={16} />}
label="Clear"
onClick={onClear}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash className="w-4 h-4 mr-2" />
Clear
</Button>
variant="destructive"
size="sm"
/>
</div>
</div>
</div>