mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
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:
75
src/components/molecules/TreeCard.tsx
Normal file
75
src/components/molecules/TreeCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
53
src/components/molecules/TreeListHeader.tsx
Normal file
53
src/components/molecules/TreeListHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
16
src/lib/json-ui/wrappers/LoadingFallbackWrapper.tsx
Normal file
16
src/lib/json-ui/wrappers/LoadingFallbackWrapper.tsx
Normal 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={{}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
27
src/lib/json-ui/wrappers/NavigationItemWrapper.tsx
Normal file
27
src/lib/json-ui/wrappers/NavigationItemWrapper.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
22
src/lib/json-ui/wrappers/PageHeaderContentWrapper.tsx
Normal file
22
src/lib/json-ui/wrappers/PageHeaderContentWrapper.tsx
Normal 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={{}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
33
src/lib/json-ui/wrappers/TreeCardWrapper.tsx
Normal file
33
src/lib/json-ui/wrappers/TreeCardWrapper.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
src/lib/json-ui/wrappers/definitions/loading-fallback.json
Normal file
32
src/lib/json-ui/wrappers/definitions/loading-fallback.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
37
src/lib/json-ui/wrappers/definitions/navigation-item.json
Normal file
37
src/lib/json-ui/wrappers/definitions/navigation-item.json
Normal 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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
63
src/lib/json-ui/wrappers/definitions/tree-card.json
Normal file
63
src/lib/json-ui/wrappers/definitions/tree-card.json
Normal 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`"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user