Generated by Spark: Integrate atomic components into remaining molecule-level components

This commit is contained in:
2026-01-17 16:46:53 +00:00
committed by GitHub
parent c9a149df48
commit 11a340cea1
14 changed files with 235 additions and 225 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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'}

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}