mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: Integrate atomic components into more organism-level components throughout the app
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user