feat: Implement JSON components with wrappers for LoadingFallback, NavigationItem, PageHeaderContent

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-18 22:07:45 +00:00
parent a386551f23
commit a65a994ec4
12 changed files with 401 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
import { Card, Badge, ActionIcon, IconButton, Stack, Flex, Text, Heading } from '@/components/atoms'
import { ComponentTree } from '@/types/project'
interface TreeCardProps {
tree: ComponentTree
isSelected: boolean
onSelect: () => void
onEdit: () => void
onDuplicate: () => void
onDelete: () => void
disableDelete?: boolean
}
export function TreeCard({
tree,
isSelected,
onSelect,
onEdit,
onDuplicate,
onDelete,
disableDelete = false,
}: TreeCardProps) {
return (
<Card
className={`cursor-pointer transition-all p-4 ${
isSelected ? 'ring-2 ring-primary bg-accent' : 'hover:bg-accent/50'
}`}
onClick={onSelect}
>
<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 && (
<Text variant="caption" className="line-clamp-2">
{tree.description}
</Text>
)}
<div>
<Badge variant="outline" className="text-xs">
{tree.rootNodes.length} components
</Badge>
</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>
</Stack>
</Card>
)
}

View File

@@ -0,0 +1,53 @@
import { Button, TreeIcon, ActionIcon, Flex, Heading, Stack, IconButton } from '@/components/atoms'
interface TreeListHeaderProps {
onCreateNew: () => void
onImportJson: () => void
onExportJson: () => void
hasSelectedTree?: boolean
}
export function TreeListHeader({
onCreateNew,
onImportJson,
onExportJson,
hasSelectedTree = false,
}: TreeListHeaderProps) {
return (
<Stack spacing="sm">
<Flex justify="between" align="center">
<Flex align="center" gap="sm">
<TreeIcon size={20} />
<Heading level={2} className="text-lg font-semibold">Component Trees</Heading>
</Flex>
<IconButton
icon={<ActionIcon action="add" size={16} />}
size="sm"
onClick={onCreateNew}
/>
</Flex>
<Flex gap="sm">
<Button
size="sm"
variant="outline"
onClick={onImportJson}
className="flex-1 text-xs"
leftIcon={<ActionIcon action="upload" size={14} />}
>
Import JSON
</Button>
<Button
size="sm"
variant="outline"
onClick={onExportJson}
disabled={!hasSelectedTree}
className="flex-1 text-xs"
leftIcon={<ActionIcon action="download" size={14} />}
>
Export JSON
</Button>
</Flex>
</Stack>
)
}

View File

@@ -18,3 +18,9 @@ export { SearchInput } from './SearchInput'
export { BindingEditor } from './BindingEditor'
export { DataSourceEditorDialog } from './DataSourceEditorDialog'
export { ComponentBindingDialog } from './ComponentBindingDialog'
export { TreeCard } from './TreeCard'
export { TreeListHeader } from './TreeListHeader'
export { LoadingFallbackWrapper as LoadingFallback } from '@/lib/json-ui/wrappers'
export { NavigationItemWrapper as NavigationItem } from '@/lib/json-ui/wrappers'
export { PageHeaderContentWrapper as PageHeaderContent } from '@/lib/json-ui/wrappers'
export { preloadMonacoEditor } from './LazyMonacoEditor'

View File

@@ -0,0 +1,16 @@
import { ComponentRenderer } from '@/lib/json-ui/component-renderer'
import loadingFallbackDef from './definitions/loading-fallback.json'
interface LoadingFallbackWrapperProps {
message?: string
}
export function LoadingFallbackWrapper({ message = 'Loading...' }: LoadingFallbackWrapperProps) {
return (
<ComponentRenderer
component={loadingFallbackDef}
data={{ message }}
context={{}}
/>
)
}

View File

@@ -0,0 +1,27 @@
import { ComponentRenderer } from '@/lib/json-ui/component-renderer'
import navigationItemDef from './definitions/navigation-item.json'
interface NavigationItemWrapperProps {
icon: React.ReactNode
label: string
isActive: boolean
badge?: number
onClick: () => void
}
export function NavigationItemWrapper(props: NavigationItemWrapperProps) {
return (
<div onClick={props.onClick}>
<ComponentRenderer
component={navigationItemDef}
data={{
icon: props.icon,
label: props.label,
isActive: props.isActive,
badge: props.badge
}}
context={{}}
/>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { ComponentRenderer } from '@/lib/json-ui/component-renderer'
import pageHeaderContentDef from './definitions/page-header-content.json'
interface PageHeaderContentWrapperProps {
title: string
icon: React.ReactNode
description?: string
}
export function PageHeaderContentWrapper(props: PageHeaderContentWrapperProps) {
return (
<ComponentRenderer
component={pageHeaderContentDef}
data={{
title: props.title,
icon: props.icon,
description: props.description
}}
context={{}}
/>
)
}

View File

@@ -0,0 +1,33 @@
import { ComponentRenderer } from '@/lib/json-ui/component-renderer'
import treeCardDefinition from './definitions/tree-card.json'
import { ComponentTree } from '@/types/project'
interface TreeCardWrapperProps {
tree: ComponentTree
isSelected: boolean
onSelect: () => void
onEdit: () => void
onDuplicate: () => void
onDelete: () => void
disableDelete?: boolean
}
export function TreeCardWrapper(props: TreeCardWrapperProps) {
return (
<div onClick={props.onSelect}>
<ComponentRenderer
component={treeCardDefinition}
data={{
tree: props.tree,
isSelected: props.isSelected
}}
context={{}}
/>
<div className="flex gap-xs mt-2" onClick={(e) => e.stopPropagation()}>
<button onClick={props.onEdit} className="px-2 py-1 text-xs hover:bg-muted rounded">Edit</button>
<button onClick={props.onDuplicate} className="px-2 py-1 text-xs hover:bg-muted rounded">Duplicate</button>
<button onClick={props.onDelete} disabled={props.disableDelete} className="px-2 py-1 text-xs hover:bg-muted rounded disabled:opacity-50">Delete</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,32 @@
{
"id": "loading-container",
"type": "div",
"props": {
"className": "flex items-center justify-center h-full w-full"
},
"children": [
{
"id": "loading-content",
"type": "div",
"props": {
"className": "flex flex-col items-center gap-3"
},
"children": [
{
"id": "loading-spinner",
"type": "LoadingSpinner"
},
{
"id": "loading-message",
"type": "p",
"props": {
"className": "text-sm text-muted-foreground"
},
"bindings": {
"children": "message"
}
}
]
}
]
}

View File

@@ -0,0 +1,37 @@
{
"id": "nav-item-button",
"type": "button",
"bindings": {
"className": {
"source": "isActive",
"transform": "data ? 'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors bg-primary text-primary-foreground' : 'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors hover:bg-muted text-foreground'"
}
},
"children": [
{
"id": "nav-item-icon",
"type": "IconWrapper",
"bindings": {
"icon": "icon",
"variant": { "source": "isActive", "transform": "data ? 'default' : 'muted'" }
},
"props": { "size": "md" }
},
{
"id": "nav-item-label",
"type": "Text",
"props": { "className": "flex-1 text-left font-medium", "variant": "small" },
"bindings": { "children": "label" }
},
{
"id": "nav-item-badge",
"type": "Badge",
"bindings": {
"variant": { "source": "isActive", "transform": "data ? 'secondary' : 'destructive'" },
"children": "badge"
},
"props": { "className": "ml-auto" },
"conditional": { "if": "badge !== undefined && badge > 0" }
}
]
}

View File

@@ -0,0 +1,33 @@
{
"id": "page-header-container",
"type": "div",
"props": { "className": "flex items-center gap-3" },
"children": [
{
"id": "page-header-icon",
"type": "TabIcon",
"bindings": { "icon": "icon" },
"props": { "variant": "gradient" }
},
{
"id": "page-header-content",
"type": "div",
"props": { "className": "min-w-0" },
"children": [
{
"id": "page-header-title",
"type": "h2",
"props": { "className": "text-lg sm:text-xl font-bold truncate" },
"bindings": { "children": "title" }
},
{
"id": "page-header-description",
"type": "p",
"props": { "className": "text-xs sm:text-sm text-muted-foreground hidden sm:block" },
"bindings": { "children": "description" },
"conditional": { "if": "description" }
}
]
}
]
}

View File

@@ -0,0 +1,63 @@
{
"id": "tree-card-container",
"type": "Card",
"bindings": {
"className": {
"source": "isSelected",
"transform": "data ? 'cursor-pointer transition-all p-4 ring-2 ring-primary bg-accent' : 'cursor-pointer transition-all p-4 hover:bg-accent/50'"
}
},
"children": [
{
"id": "tree-card-content",
"type": "Stack",
"props": { "spacing": "sm" },
"children": [
{
"id": "tree-card-header",
"type": "Flex",
"props": { "justify": "between", "align": "start", "gap": "sm" },
"children": [
{
"id": "tree-card-info",
"type": "Stack",
"props": { "spacing": "xs", "className": "flex-1 min-w-0" },
"children": [
{
"id": "tree-card-name",
"type": "Heading",
"props": { "level": 4, "className": "text-sm truncate" },
"bindings": { "children": "tree.name" }
},
{
"id": "tree-card-description",
"type": "Text",
"props": { "variant": "caption", "className": "line-clamp-2" },
"bindings": { "children": "tree.description" },
"conditional": { "if": "tree.description" }
},
{
"id": "tree-card-badge-container",
"type": "div",
"children": [
{
"id": "tree-card-badge",
"type": "Badge",
"props": { "variant": "outline", "className": "text-xs" },
"bindings": {
"children": {
"source": "tree.rootNodes.length",
"transform": "`${data} components`"
}
}
}
]
}
]
}
]
}
]
}
]
}

View File

@@ -8,3 +8,7 @@ export { GitHubBuildStatusWrapper } from './GitHubBuildStatusWrapper'
export { ComponentBindingDialogWrapper } from './ComponentBindingDialogWrapper'
export { DataSourceEditorDialogWrapper } from './DataSourceEditorDialogWrapper'
export { ComponentTreeWrapper } from './ComponentTreeWrapper'
export { LoadingFallbackWrapper } from './LoadingFallbackWrapper'
export { NavigationItemWrapper } from './NavigationItemWrapper'
export { PageHeaderContentWrapper } from './PageHeaderContentWrapper'
export { LoadingFallbackWrapper } from './LoadingFallbackWrapper'