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 remaining molecule-level components
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { AppLogo } from '@/components/atoms'
|
||||
import { AppLogo, Stack, Heading, Text } from '@/components/atoms'
|
||||
|
||||
interface AppBrandingProps {
|
||||
title?: string
|
||||
@@ -10,14 +10,14 @@ export function AppBranding({
|
||||
subtitle = 'Low-Code Next.js App Builder'
|
||||
}: AppBrandingProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
||||
<Stack direction="horizontal" align="center" spacing="sm" className="flex-1 min-w-0">
|
||||
<AppLogo />
|
||||
<div className="flex flex-col min-w-[100px]">
|
||||
<h1 className="text-base sm:text-xl font-bold whitespace-nowrap">{title}</h1>
|
||||
<p className="text-xs text-muted-foreground hidden sm:block whitespace-nowrap">
|
||||
<Stack direction="vertical" spacing="none" className="min-w-[100px]">
|
||||
<Heading level={1} className="text-base sm:text-xl font-bold whitespace-nowrap">{title}</Heading>
|
||||
<Text variant="caption" className="hidden sm:block whitespace-nowrap">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CaretRight, House } from '@phosphor-icons/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNavigationHistory } from '@/hooks/use-navigation-history'
|
||||
import { getPageById } from '@/config/page-loader'
|
||||
import { Flex, IconWrapper } from '@/components/atoms'
|
||||
|
||||
export function Breadcrumb() {
|
||||
const { history } = useNavigationHistory()
|
||||
@@ -30,59 +31,65 @@ export function Breadcrumb() {
|
||||
const previousPages = history.slice(1, 4)
|
||||
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-1 text-sm overflow-x-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
history[0]?.path === '/' ? "text-foreground font-medium" : "text-muted-foreground"
|
||||
)}
|
||||
aria-label="Home"
|
||||
>
|
||||
<House className="shrink-0" size={16} weight="duotone" />
|
||||
</Link>
|
||||
<nav aria-label="Breadcrumb" className="overflow-x-auto">
|
||||
<Flex align="center" gap="xs" className="text-sm">
|
||||
<Link
|
||||
to="/"
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
history[0]?.path === '/' ? "text-foreground font-medium" : "text-muted-foreground"
|
||||
)}
|
||||
aria-label="Home"
|
||||
>
|
||||
<IconWrapper
|
||||
icon={<House size={16} weight="duotone" />}
|
||||
size="sm"
|
||||
variant={history[0]?.path === '/' ? 'default' : 'muted'}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{previousPages.length > 0 && (
|
||||
<>
|
||||
<CaretRight size={14} className="text-muted-foreground shrink-0" />
|
||||
<div className="flex items-center gap-1">
|
||||
{previousPages.map((item, index) => (
|
||||
<div key={item.path} className="flex items-center gap-1">
|
||||
<Link
|
||||
to={item.path}
|
||||
className={cn(
|
||||
"px-2 py-1 rounded-md transition-colors text-muted-foreground",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"max-w-[120px] truncate"
|
||||
{previousPages.length > 0 && (
|
||||
<>
|
||||
<CaretRight size={14} className="text-muted-foreground shrink-0" />
|
||||
<Flex align="center" gap="xs">
|
||||
{previousPages.map((item, index) => (
|
||||
<Flex key={item.path} align="center" gap="xs">
|
||||
<Link
|
||||
to={item.path}
|
||||
className={cn(
|
||||
"px-2 py-1 rounded-md transition-colors text-muted-foreground",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"max-w-[120px] truncate"
|
||||
)}
|
||||
title={getPageTitle(item.path)}
|
||||
>
|
||||
{getPageTitle(item.path)}
|
||||
</Link>
|
||||
{index < previousPages.length - 1 && (
|
||||
<CaretRight size={14} className="text-muted-foreground shrink-0" />
|
||||
)}
|
||||
title={getPageTitle(item.path)}
|
||||
>
|
||||
{getPageTitle(item.path)}
|
||||
</Link>
|
||||
{index < previousPages.length - 1 && (
|
||||
<CaretRight size={14} className="text-muted-foreground shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentPage.path !== '/' && (
|
||||
<>
|
||||
<CaretRight size={14} className="text-muted-foreground shrink-0" />
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 rounded-md font-medium text-foreground bg-accent/50",
|
||||
"max-w-[150px] truncate"
|
||||
)}
|
||||
title={getPageTitle(currentPage.path)}
|
||||
>
|
||||
{getPageTitle(currentPage.path)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{currentPage.path !== '/' && (
|
||||
<>
|
||||
<CaretRight size={14} className="text-muted-foreground shrink-0" />
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 rounded-md font-medium text-foreground bg-accent/50 text-sm",
|
||||
"max-w-[150px] truncate"
|
||||
)}
|
||||
title={getPageTitle(currentPage.path)}
|
||||
>
|
||||
{getPageTitle(currentPage.path)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentDefinition, getCategoryComponents } from '@/lib/component-definitions'
|
||||
import { ComponentPaletteItem } from '@/components/atoms/ComponentPaletteItem'
|
||||
import { PanelHeader } from '@/components/atoms'
|
||||
import { PanelHeader, Stack } from '@/components/atoms'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Package } from '@phosphor-icons/react'
|
||||
@@ -18,7 +18,7 @@ export function ComponentPalette({ onDragStart }: ComponentPaletteProps) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Stack direction="vertical" className="h-full">
|
||||
<div className="p-4">
|
||||
<PanelHeader
|
||||
title="Components"
|
||||
@@ -53,6 +53,6 @@ export function ComponentPalette({ onDragStart }: ComponentPaletteProps) {
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Card, Stack, Text, Heading, Skeleton, Flex, IconWrapper } from '@/components/atoms'
|
||||
|
||||
interface DataCardProps {
|
||||
title?: string
|
||||
@@ -27,46 +26,49 @@ export function DataCard({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-20 mb-2" />
|
||||
<Skeleton className="h-8 w-16 mb-1" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</CardContent>
|
||||
<div className="pt-6 px-6 pb-6">
|
||||
<Stack spacing="sm">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-8 w-16" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</Stack>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="pt-6 px-6 pb-6">
|
||||
<Flex justify="between" align="start" gap="md">
|
||||
<Stack spacing="xs" className="flex-1">
|
||||
{title && (
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">
|
||||
<Text variant="muted" className="font-medium">
|
||||
{title}
|
||||
</div>
|
||||
</Text>
|
||||
)}
|
||||
<div className="text-3xl font-bold">
|
||||
<Heading level={1} className="text-3xl font-bold">
|
||||
{value}
|
||||
</div>
|
||||
</Heading>
|
||||
{description && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
<Text variant="caption">
|
||||
{description}
|
||||
</div>
|
||||
</Text>
|
||||
)}
|
||||
{trend && (
|
||||
<div className={`text-xs mt-2 ${trend.positive ? 'text-green-500' : 'text-red-500'}`}>
|
||||
<Text
|
||||
variant="small"
|
||||
className={trend.positive ? 'text-green-500' : 'text-red-500'}
|
||||
>
|
||||
{trend.positive ? '↑' : '↓'} {trend.value} {trend.label}
|
||||
</div>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
{icon && (
|
||||
<div className="text-muted-foreground">
|
||||
{icon}
|
||||
</div>
|
||||
<IconWrapper icon={icon} size="lg" variant="muted" />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Flex>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, Badge, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||
import { DataSourceBadge } from '@/components/atoms/DataSourceBadge'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { Pencil, Trash, ArrowsDownUp } from '@phosphor-icons/react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface DataSourceCardProps {
|
||||
dataSource: DataSource
|
||||
@@ -25,21 +21,21 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
const renderTypeSpecificInfo = () => {
|
||||
if (dataSource.type === 'kv') {
|
||||
return (
|
||||
<div className="text-xs text-muted-foreground font-mono bg-muted/30 px-2 py-1 rounded">
|
||||
<Text variant="caption" className="font-mono bg-muted/30 px-2 py-1 rounded">
|
||||
Key: {dataSource.key || 'Not set'}
|
||||
</div>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (dataSource.type === 'computed') {
|
||||
const depCount = getDependencyCount()
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Flex align="center" gap="sm">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<ArrowsDownUp className="w-3 h-3 mr-1" />
|
||||
{depCount} {depCount === 1 ? 'dependency' : 'dependencies'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,48 +44,45 @@ export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur hover:bg-card/70 transition-colors">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="p-4">
|
||||
<Flex justify="between" align="start" gap="md">
|
||||
<Stack spacing="sm" className="flex-1 min-w-0">
|
||||
<Flex align="center" gap="sm">
|
||||
<DataSourceBadge type={dataSource.type} />
|
||||
<span className="font-mono text-sm font-medium truncate">
|
||||
<Text variant="small" className="font-mono font-medium truncate">
|
||||
{dataSource.id}
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{renderTypeSpecificInfo()}
|
||||
|
||||
{dependents.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-border/50">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<Text variant="caption">
|
||||
Used by {dependents.length} computed {dependents.length === 1 ? 'source' : 'sources'}
|
||||
</span>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
<Flex align="center" gap="xs">
|
||||
<IconButton
|
||||
icon={<Pencil className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(dataSource.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<Trash className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(dataSource.id)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
className="text-destructive hover:text-destructive"
|
||||
disabled={dependents.length > 0}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button, Flex } from '@/components/atoms'
|
||||
import { Info, Sparkle } from '@phosphor-icons/react'
|
||||
|
||||
interface EditorActionsProps {
|
||||
@@ -8,14 +8,14 @@ interface EditorActionsProps {
|
||||
|
||||
export function EditorActions({ onExplain, onImprove }: EditorActionsProps) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Flex gap="sm">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onExplain}
|
||||
className="h-7 text-xs"
|
||||
leftIcon={<Info size={14} />}
|
||||
>
|
||||
<Info size={14} className="mr-1" />
|
||||
Explain
|
||||
</Button>
|
||||
<Button
|
||||
@@ -23,10 +23,10 @@ export function EditorActions({ onExplain, onImprove }: EditorActionsProps) {
|
||||
variant="ghost"
|
||||
onClick={onImprove}
|
||||
className="h-7 text-xs"
|
||||
leftIcon={<Sparkle size={14} weight="duotone" />}
|
||||
>
|
||||
<Sparkle size={14} className="mr-1" weight="duotone" />
|
||||
Improve
|
||||
</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { EmptyStateIcon, Stack, Text } from '@/components/atoms'
|
||||
import { FileCode } from '@phosphor-icons/react'
|
||||
|
||||
export function EmptyEditorState() {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<FileCode size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>Select a file to edit</p>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Stack direction="vertical" align="center" spacing="md">
|
||||
<EmptyStateIcon icon={<FileCode size={48} />} />
|
||||
<Text variant="muted">Select a file to edit</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { FileCode, X } from '@phosphor-icons/react'
|
||||
import { Flex } from '@/components/atoms'
|
||||
|
||||
interface FileTabsProps {
|
||||
files: ProjectFile[]
|
||||
@@ -10,7 +11,7 @@ interface FileTabsProps {
|
||||
|
||||
export function FileTabs({ files, activeFileId, onFileSelect, onFileClose }: FileTabsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Flex align="center" gap="xs">
|
||||
{files.map((file) => (
|
||||
<button
|
||||
key={file.id}
|
||||
@@ -34,6 +35,6 @@ export function FileTabs({ files, activeFileId, onFileSelect, onFileClose }: Fil
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Badge, Flex, Text } from '@/components/atoms'
|
||||
|
||||
interface LabelWithBadgeProps {
|
||||
label: string
|
||||
@@ -12,13 +12,13 @@ export function LabelWithBadge({
|
||||
badgeVariant = 'secondary'
|
||||
}: LabelWithBadgeProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
<Flex align="center" gap="sm">
|
||||
<Text variant="small" className="font-medium">{label}</Text>
|
||||
{badge !== undefined && (
|
||||
<Badge variant={badgeVariant} className="text-xs">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Badge, Flex, Text, IconWrapper } from '@/components/atoms'
|
||||
|
||||
interface NavigationItemProps {
|
||||
icon: React.ReactNode
|
||||
@@ -24,12 +24,14 @@ export function NavigationItem({
|
||||
: 'hover:bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<span className={isActive ? 'text-primary-foreground' : 'text-muted-foreground'}>
|
||||
{icon}
|
||||
</span>
|
||||
<span className="flex-1 text-left text-sm font-medium">
|
||||
<IconWrapper
|
||||
icon={icon}
|
||||
size="md"
|
||||
variant={isActive ? 'default' : 'muted'}
|
||||
/>
|
||||
<Text className="flex-1 text-left font-medium" variant="small">
|
||||
{label}
|
||||
</span>
|
||||
</Text>
|
||||
{badge !== undefined && badge > 0 && (
|
||||
<Badge
|
||||
variant={isActive ? 'secondary' : 'destructive'}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
import { PropertyEditorField } from '@/components/atoms/PropertyEditorField'
|
||||
import { PanelHeader } from '@/components/atoms'
|
||||
import { PanelHeader, Badge, IconButton, Stack, Text, EmptyStateIcon } from '@/components/atoms'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Sliders, Trash, Code } from '@phosphor-icons/react'
|
||||
import { Sliders, Trash } from '@phosphor-icons/react'
|
||||
import { getComponentDef } from '@/lib/component-definitions'
|
||||
|
||||
interface PropertyEditorProps {
|
||||
@@ -17,10 +15,14 @@ interface PropertyEditorProps {
|
||||
export function PropertyEditor({ component, onUpdate, onDelete }: PropertyEditorProps) {
|
||||
if (!component) {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-muted-foreground p-8 text-center">
|
||||
<Sliders className="w-12 h-12 mb-4 opacity-50" />
|
||||
<p className="text-sm">No component selected</p>
|
||||
<p className="text-xs mt-1">Select a component to edit its properties</p>
|
||||
<div className="h-full flex flex-col items-center justify-center p-8">
|
||||
<Stack direction="vertical" align="center" spacing="md">
|
||||
<EmptyStateIcon icon={<Sliders className="w-12 h-12" />} />
|
||||
<Stack direction="vertical" align="center" spacing="xs">
|
||||
<Text variant="small">No component selected</Text>
|
||||
<Text variant="caption">Select a component to edit its properties</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -106,33 +108,32 @@ export function PropertyEditor({ component, onUpdate, onDelete }: PropertyEditor
|
||||
<PanelHeader
|
||||
title="Properties"
|
||||
subtitle={
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Stack direction="horizontal" align="center" spacing="sm" className="mt-1">
|
||||
<Badge variant="outline" className="text-xs font-mono">
|
||||
{def?.label || component.type}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">#{component.id}</span>
|
||||
</div>
|
||||
<Text variant="caption">#{component.id}</Text>
|
||||
</Stack>
|
||||
}
|
||||
icon={<Sliders size={20} weight="duotone" />}
|
||||
actions={
|
||||
<Button
|
||||
<IconButton
|
||||
icon={<Trash className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<Stack spacing="lg">
|
||||
<Stack spacing="md">
|
||||
<Text variant="caption" className="font-semibold uppercase tracking-wide">
|
||||
Component Properties
|
||||
</h3>
|
||||
</Text>
|
||||
{props.map((prop) => (
|
||||
<PropertyEditorField
|
||||
key={prop.name}
|
||||
@@ -144,14 +145,14 @@ export function PropertyEditor({ component, onUpdate, onDelete }: PropertyEditor
|
||||
onChange={handlePropChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<Stack spacing="md">
|
||||
<Text variant="caption" className="font-semibold uppercase tracking-wide">
|
||||
Common Properties
|
||||
</h3>
|
||||
</Text>
|
||||
{commonProps.map((prop) => (
|
||||
<PropertyEditorField
|
||||
key={prop.name}
|
||||
@@ -162,8 +163,8 @@ export function PropertyEditor({ component, onUpdate, onDelete }: PropertyEditor
|
||||
onChange={handlePropChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input, IconButton, Flex } from '@/components/atoms'
|
||||
import { MagnifyingGlass, X } from '@phosphor-icons/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SearchBarProps {
|
||||
value: string
|
||||
@@ -12,7 +10,7 @@ interface SearchBarProps {
|
||||
|
||||
export function SearchBar({ value, onChange, placeholder = 'Search...', className }: SearchBarProps) {
|
||||
return (
|
||||
<div className={cn('flex gap-2', className)}>
|
||||
<Flex gap="sm" className={className}>
|
||||
<div className="relative flex-1">
|
||||
<MagnifyingGlass
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
@@ -26,15 +24,13 @@ export function SearchBar({ value, onChange, placeholder = 'Search...', classNam
|
||||
/>
|
||||
</div>
|
||||
{value && (
|
||||
<Button
|
||||
<IconButton
|
||||
icon={<X size={16} />}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onChange('')}
|
||||
title="Clear search"
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ActionIcon } from '@/components/atoms'
|
||||
import { Card, Badge, ActionIcon, IconButton, Stack, Flex, Text, Heading } from '@/components/atoms'
|
||||
import { ComponentTree } from '@/types/project'
|
||||
|
||||
interface TreeCardProps {
|
||||
@@ -25,45 +22,54 @@ export function TreeCard({
|
||||
}: TreeCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={`cursor-pointer transition-all ${
|
||||
className={`cursor-pointer transition-all p-4 ${
|
||||
isSelected ? 'ring-2 ring-primary bg-accent' : 'hover:bg-accent/50'
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-sm truncate">{tree.name}</CardTitle>
|
||||
<Stack spacing="sm">
|
||||
<Flex justify="between" align="start" gap="sm">
|
||||
<Stack spacing="xs" className="flex-1 min-w-0">
|
||||
<Heading level={4} className="text-sm truncate">{tree.name}</Heading>
|
||||
{tree.description && (
|
||||
<CardDescription className="text-xs mt-1 line-clamp-2">
|
||||
<Text variant="caption" className="line-clamp-2">
|
||||
{tree.description}
|
||||
</CardDescription>
|
||||
</Text>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tree.rootNodes.length} components
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Flex>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Flex gap="xs" className="mt-1">
|
||||
<IconButton
|
||||
icon={<ActionIcon action="edit" size={14} />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
title="Edit tree"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<ActionIcon action="copy" size={14} />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onDuplicate}
|
||||
title="Duplicate tree"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<ActionIcon action="delete" size={14} />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
disabled={disableDelete}
|
||||
title="Delete tree"
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button size="sm" variant="ghost" onClick={onEdit} title="Edit tree">
|
||||
<ActionIcon action="edit" size={14} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDuplicate} title="Duplicate tree">
|
||||
<ActionIcon action="copy" size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onDelete}
|
||||
disabled={disableDelete}
|
||||
title="Delete tree"
|
||||
>
|
||||
<ActionIcon action="delete" size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TreeIcon, ActionIcon } from '@/components/atoms'
|
||||
import { Button, TreeIcon, ActionIcon, Flex, Heading, Stack, IconButton } from '@/components/atoms'
|
||||
|
||||
interface TreeListHeaderProps {
|
||||
onCreateNew: () => void
|
||||
@@ -15,25 +14,27 @@ export function TreeListHeader({
|
||||
hasSelectedTree = false,
|
||||
}: TreeListHeaderProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Stack spacing="sm">
|
||||
<Flex justify="between" align="center">
|
||||
<Flex align="center" gap="sm">
|
||||
<TreeIcon size={20} />
|
||||
Component Trees
|
||||
</h2>
|
||||
<Button size="sm" onClick={onCreateNew}>
|
||||
<ActionIcon action="add" size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<Heading level={2} className="text-lg font-semibold">Component Trees</Heading>
|
||||
</Flex>
|
||||
<IconButton
|
||||
icon={<ActionIcon action="add" size={16} />}
|
||||
size="sm"
|
||||
onClick={onCreateNew}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Flex gap="sm">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onImportJson}
|
||||
className="flex-1 text-xs"
|
||||
leftIcon={<ActionIcon action="upload" size={14} />}
|
||||
>
|
||||
<ActionIcon action="upload" size={14} className="mr-1.5" />
|
||||
Import JSON
|
||||
</Button>
|
||||
<Button
|
||||
@@ -42,11 +43,11 @@ export function TreeListHeader({
|
||||
onClick={onExportJson}
|
||||
disabled={!hasSelectedTree}
|
||||
className="flex-1 text-xs"
|
||||
leftIcon={<ActionIcon action="download" size={14} />}
|
||||
>
|
||||
<ActionIcon action="download" size={14} className="mr-1.5" />
|
||||
Export JSON
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user