mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
feat: Delete remaining duplicate organism TSX files (Task 11)
Delete 7 organisms that have JSON equivalents and are no longer needed: - AppHeader (routes to @/lib/json-ui/json-components) - EmptyCanvasState (routes to JSON) - NavigationMenu (routes to JSON with useNavigationMenu hook) - PageHeader (routes to JSON) - SchemaCodeViewer (routes to JSON) - ToolbarActions (routes to JSON) - TreeListPanel (routes to JSON) Updated imports in 2 files to use JSON versions: - AppMainPanel: AppHeader from json-components - AppLayout: NavigationMenu from json-components Remaining organisms (6): - SchemaEditorLayout, SchemaEditorCanvas, SchemaEditorPropertiesPanel - SchemaEditorSidebar, SchemaEditorStatusBar, SchemaEditorToolbar (These don't have JSON equivalents yet) Build: passing ✓ Component types: 343 ✓ Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
|
||||
|
||||
import AppDialogs from '@/components/app/AppDialogs'
|
||||
import AppMainPanel from '@/components/app/AppMainPanel'
|
||||
import { NavigationMenu } from '@/components/organisms/NavigationMenu'
|
||||
import { NavigationMenu } from '@/lib/json-ui/json-components'
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
|
||||
import appStrings from '@/data/app-shortcuts.json'
|
||||
import useAppNavigation from '@/hooks/use-app-navigation'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense } from 'react'
|
||||
|
||||
import { AppHeader } from '@/components/organisms'
|
||||
import { AppHeader } from '@/lib/json-ui/json-components'
|
||||
import { PWARegistry } from '@/lib/component-registry'
|
||||
import { RouterProvider } from '@/router'
|
||||
import type { FeatureToggles, Project } from '@/types/project'
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { AppBranding, Breadcrumb, SaveIndicator } from '@/components/molecules'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { ToolbarActions } from '@/components/organisms/ToolbarActions'
|
||||
import { ProjectManager } from '@/components/ProjectManager'
|
||||
import { FeatureToggles, Project } from '@/types/project'
|
||||
import { Flex, Stack, Separator, Container } from '@/components/atoms'
|
||||
|
||||
interface AppHeaderProps {
|
||||
activeTab: string
|
||||
onTabChange: (tab: string) => void
|
||||
featureToggles: FeatureToggles
|
||||
errorCount: number
|
||||
lastSaved: number | null
|
||||
currentProject: Project
|
||||
onProjectLoad: (project: Project) => void
|
||||
onSearch: () => void
|
||||
onShowShortcuts: () => void
|
||||
onGenerateAI: () => void
|
||||
onExport: () => void
|
||||
onPreview?: () => void
|
||||
onShowErrors: () => void
|
||||
}
|
||||
|
||||
export function AppHeader({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
featureToggles,
|
||||
errorCount,
|
||||
lastSaved,
|
||||
currentProject,
|
||||
onProjectLoad,
|
||||
onSearch,
|
||||
onShowShortcuts,
|
||||
onGenerateAI,
|
||||
onExport,
|
||||
onPreview,
|
||||
onShowErrors,
|
||||
}: AppHeaderProps) {
|
||||
return (
|
||||
<header className="border-b border-border bg-card">
|
||||
<Stack direction="vertical" spacing="none">
|
||||
<div className="px-4 sm:px-6 py-3 sm:py-4">
|
||||
<Flex justify="between" align="center" gap="sm">
|
||||
<Flex align="center" gap="sm" className="flex-1 min-w-0">
|
||||
<SidebarTrigger />
|
||||
<AppBranding />
|
||||
<SaveIndicator lastSaved={lastSaved} />
|
||||
</Flex>
|
||||
<Flex gap="xs" shrink className="shrink-0">
|
||||
<ProjectManager
|
||||
currentProject={currentProject}
|
||||
onProjectLoad={onProjectLoad}
|
||||
/>
|
||||
<ToolbarActions
|
||||
onSearch={onSearch}
|
||||
onShowShortcuts={onShowShortcuts}
|
||||
onGenerateAI={onGenerateAI}
|
||||
onExport={onExport}
|
||||
onPreview={onPreview}
|
||||
onShowErrors={onShowErrors}
|
||||
errorCount={errorCount}
|
||||
showErrorButton={featureToggles.errorRepair && errorCount > 0}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
<Separator className="opacity-50" />
|
||||
<div className="px-4 sm:px-6 py-2">
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
</Stack>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Plus, Folder } from '@phosphor-icons/react'
|
||||
import { EmptyState, ActionButton, Stack } from '@/components/atoms'
|
||||
|
||||
interface EmptyCanvasStateProps {
|
||||
onAddFirstComponent?: () => void
|
||||
onImportSchema?: () => void
|
||||
}
|
||||
|
||||
export function EmptyCanvasState({ onAddFirstComponent, onImportSchema }: EmptyCanvasStateProps) {
|
||||
return (
|
||||
<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,262 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sidebar, SidebarContent, SidebarHeader } from '@/components/ui/sidebar'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'
|
||||
import { CaretDoubleDown, CaretDoubleUp, CaretDown } from '@phosphor-icons/react'
|
||||
import { CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { Badge, Flex, Text, IconWrapper } from '@/components/atoms'
|
||||
import { navigationGroups, NavigationItemData } from '@/lib/navigation-config'
|
||||
import { FeatureToggles } from '@/types/project'
|
||||
import { useRoutePreload } from '@/hooks/use-route-preload'
|
||||
import navigationMenuCopy from '@/data/navigation-menu.json'
|
||||
|
||||
interface NavigationMenuProps {
|
||||
activeTab: string
|
||||
onTabChange: (tab: string) => void
|
||||
featureToggles: FeatureToggles
|
||||
errorCount?: number
|
||||
}
|
||||
|
||||
interface NavigationMenuControlsProps {
|
||||
onExpandAll: () => void
|
||||
onCollapseAll: () => void
|
||||
}
|
||||
|
||||
interface NavigationMenuGroupListProps {
|
||||
activeTab: string
|
||||
expandedGroups: Set<string>
|
||||
featureToggles: FeatureToggles
|
||||
errorCount: number
|
||||
onToggleGroup: (groupId: string) => void
|
||||
onItemClick: (value: string) => void
|
||||
onItemHover: (value: string) => void
|
||||
onItemLeave: (value: string) => void
|
||||
}
|
||||
|
||||
function NavigationMenuControls({
|
||||
onExpandAll,
|
||||
onCollapseAll,
|
||||
}: NavigationMenuControlsProps) {
|
||||
return (
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onExpandAll}
|
||||
className="flex-1"
|
||||
>
|
||||
<CaretDoubleDown size={16} className="mr-2" />
|
||||
{navigationMenuCopy.labels.expandAll}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCollapseAll}
|
||||
className="flex-1"
|
||||
>
|
||||
<CaretDoubleUp size={16} className="mr-2" />
|
||||
{navigationMenuCopy.labels.collapseAll}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuHeader({
|
||||
onExpandAll,
|
||||
onCollapseAll,
|
||||
}: NavigationMenuControlsProps) {
|
||||
return (
|
||||
<SidebarHeader className="px-4 py-4 border-b">
|
||||
<h2 className="text-lg font-semibold">{navigationMenuCopy.labels.title}</h2>
|
||||
<NavigationMenuControls
|
||||
onExpandAll={onExpandAll}
|
||||
onCollapseAll={onCollapseAll}
|
||||
/>
|
||||
</SidebarHeader>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuGroupList({
|
||||
activeTab,
|
||||
expandedGroups,
|
||||
featureToggles,
|
||||
errorCount,
|
||||
onToggleGroup,
|
||||
onItemClick,
|
||||
onItemHover,
|
||||
onItemLeave,
|
||||
}: NavigationMenuGroupListProps) {
|
||||
const isItemVisible = (item: NavigationItemData) => {
|
||||
if (!item.featureKey) return true
|
||||
return featureToggles[item.featureKey]
|
||||
}
|
||||
|
||||
const getVisibleItemsCount = (groupId: string) => {
|
||||
const group = navigationGroups.find((g) => g.id === groupId)
|
||||
if (!group) return 0
|
||||
return group.items.filter(isItemVisible).length
|
||||
}
|
||||
|
||||
const getItemBadge = (item: NavigationItemData) => {
|
||||
if (item.id === 'errors') return errorCount
|
||||
return item.badge
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 py-4">
|
||||
{navigationGroups.map((group) => {
|
||||
const visibleItemsCount = getVisibleItemsCount(group.id)
|
||||
if (visibleItemsCount === 0) return null
|
||||
|
||||
const isExpanded = expandedGroups.has(group.id)
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={group.id}
|
||||
open={isExpanded}
|
||||
onOpenChange={() => onToggleGroup(group.id)}
|
||||
>
|
||||
{/* NavigationGroupHeader - inlined */}
|
||||
<CollapsibleTrigger className="w-full flex items-center gap-2 px-2 py-2 rounded-lg hover:bg-muted transition-colors group">
|
||||
<CaretDown
|
||||
size={16}
|
||||
weight="bold"
|
||||
className={`text-muted-foreground transition-transform ${
|
||||
isExpanded ? 'rotate-0' : '-rotate-90'
|
||||
}`}
|
||||
/>
|
||||
<h3 className="flex-1 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{group.label}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">{visibleItemsCount}</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-1">
|
||||
<div className="space-y-1 pl-2">
|
||||
{group.items.map((item) => {
|
||||
if (!isItemVisible(item)) return null
|
||||
|
||||
const isActive = activeTab === item.value
|
||||
const badge = getItemBadge(item)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onMouseEnter={() => onItemHover(item.value)}
|
||||
onMouseLeave={() => onItemLeave(item.value)}
|
||||
>
|
||||
{/* NavigationItem - inlined */}
|
||||
<button
|
||||
onClick={() => onItemClick(item.value)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted text-foreground'
|
||||
}`}
|
||||
>
|
||||
<IconWrapper
|
||||
icon={item.icon}
|
||||
size="md"
|
||||
variant={isActive ? 'default' : 'muted'}
|
||||
/>
|
||||
<Text className="flex-1 text-left font-medium" variant="small">
|
||||
{item.label}
|
||||
</Text>
|
||||
{badge !== undefined && badge > 0 && (
|
||||
<Badge
|
||||
variant={isActive ? 'secondary' : 'destructive'}
|
||||
className="ml-auto"
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavigationMenu({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
featureToggles,
|
||||
errorCount = 0,
|
||||
}: NavigationMenuProps) {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
|
||||
new Set(['overview', 'development', 'automation', 'design', 'backend', 'testing', 'tools'])
|
||||
)
|
||||
|
||||
const { preloadRoute, cancelPreload } = useRoutePreload({ delay: 100 })
|
||||
|
||||
const handleItemClick = (value: string) => {
|
||||
onTabChange(value)
|
||||
}
|
||||
|
||||
const handleItemHover = (value: string) => {
|
||||
console.log(`[NAV] 🖱️ Hover detected on: ${value}`)
|
||||
preloadRoute(value)
|
||||
}
|
||||
|
||||
const handleItemLeave = (value: string) => {
|
||||
console.log(`[NAV] 👋 Hover left: ${value}`)
|
||||
cancelPreload(value)
|
||||
}
|
||||
|
||||
const toggleGroup = (groupId: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(groupId)) {
|
||||
newSet.delete(groupId)
|
||||
} else {
|
||||
newSet.add(groupId)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const handleExpandAll = () => {
|
||||
const allGroupIds = navigationGroups
|
||||
.filter((group) =>
|
||||
group.items.some((item) => {
|
||||
if (!item.featureKey) return true
|
||||
return featureToggles[item.featureKey]
|
||||
})
|
||||
)
|
||||
.map((group) => group.id)
|
||||
setExpandedGroups(new Set(allGroupIds))
|
||||
}
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
setExpandedGroups(new Set())
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar side="left" collapsible="offcanvas">
|
||||
<NavigationMenuHeader
|
||||
onExpandAll={handleExpandAll}
|
||||
onCollapseAll={handleCollapseAll}
|
||||
/>
|
||||
<SidebarContent>
|
||||
<ScrollArea className="h-full px-4">
|
||||
<NavigationMenuGroupList
|
||||
activeTab={activeTab}
|
||||
expandedGroups={expandedGroups}
|
||||
featureToggles={featureToggles}
|
||||
errorCount={errorCount}
|
||||
onToggleGroup={toggleGroup}
|
||||
onItemClick={handleItemClick}
|
||||
onItemHover={handleItemHover}
|
||||
onItemLeave={handleItemLeave}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Stack, Container } from '@/components/atoms'
|
||||
import { TabIcon } from '@/lib/json-ui/json-components'
|
||||
import { tabInfo } from '@/lib/navigation-config'
|
||||
|
||||
interface PageHeaderProps {
|
||||
activeTab: string
|
||||
}
|
||||
|
||||
export function PageHeader({ activeTab }: PageHeaderProps) {
|
||||
const info = tabInfo[activeTab]
|
||||
|
||||
if (!info) return null
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="vertical"
|
||||
spacing="none"
|
||||
className="border-b border-border bg-card px-4 sm:px-6 py-3 sm:py-4"
|
||||
>
|
||||
{/* PageHeaderContent - inlined */}
|
||||
<div className="flex items-center gap-3">
|
||||
<TabIcon icon={info.icon} variant="gradient" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg sm:text-xl font-bold truncate">{info.title}</h2>
|
||||
{info.description && (
|
||||
<p className="text-xs sm:text-sm text-muted-foreground hidden sm:block">
|
||||
{info.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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[]
|
||||
schema: any
|
||||
}
|
||||
|
||||
export function SchemaCodeViewer({ components, schema }: SchemaCodeViewerProps) {
|
||||
const jsonString = JSON.stringify(schema, null, 2)
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-card">
|
||||
<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">
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
<TabsTrigger value="preview">Preview</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="json" className="flex-1 m-0 mt-2">
|
||||
<ScrollArea className="h-full">
|
||||
<pre className="p-4 text-xs font-mono text-foreground">
|
||||
{jsonString}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="preview" className="flex-1 m-0 mt-2">
|
||||
<div className="p-4">
|
||||
<Text variant="muted">
|
||||
Live preview coming soon
|
||||
</Text>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { ToolbarButton } from '@/components/molecules'
|
||||
import { ErrorBadge, Flex, Tooltip, Badge } from '@/components/atoms'
|
||||
import {
|
||||
MagnifyingGlass,
|
||||
Keyboard,
|
||||
Sparkle,
|
||||
Download,
|
||||
Wrench,
|
||||
Eye,
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
interface ToolbarActionsProps {
|
||||
onSearch: () => void
|
||||
onShowShortcuts: () => void
|
||||
onGenerateAI: () => void
|
||||
onExport: () => void
|
||||
onPreview?: () => void
|
||||
onShowErrors?: () => void
|
||||
errorCount?: number
|
||||
showErrorButton?: boolean
|
||||
}
|
||||
|
||||
export function ToolbarActions({
|
||||
onSearch,
|
||||
onShowShortcuts,
|
||||
onGenerateAI,
|
||||
onExport,
|
||||
onPreview,
|
||||
onShowErrors,
|
||||
errorCount = 0,
|
||||
showErrorButton = false,
|
||||
}: ToolbarActionsProps) {
|
||||
return (
|
||||
<Flex gap="xs" shrink className="shrink-0">
|
||||
<ToolbarButton
|
||||
icon={<MagnifyingGlass size={18} />}
|
||||
label="Search (Ctrl+K)"
|
||||
onClick={onSearch}
|
||||
data-search-trigger
|
||||
/>
|
||||
|
||||
{showErrorButton && errorCount > 0 && onShowErrors && (
|
||||
<div className="relative">
|
||||
<ToolbarButton
|
||||
icon={<Wrench size={18} />}
|
||||
label={`${errorCount} ${errorCount === 1 ? 'Error' : 'Errors'}`}
|
||||
onClick={onShowErrors}
|
||||
variant="outline"
|
||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
/>
|
||||
<ErrorBadge count={errorCount} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onPreview && (
|
||||
<ToolbarButton
|
||||
icon={<Eye size={18} />}
|
||||
label="Preview (Ctrl+P)"
|
||||
onClick={onPreview}
|
||||
variant="outline"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Keyboard size={18} />}
|
||||
label="Keyboard Shortcuts (Ctrl+/)"
|
||||
onClick={onShowShortcuts}
|
||||
variant="ghost"
|
||||
className="hidden sm:flex"
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Sparkle size={18} weight="duotone" />}
|
||||
label="AI Generate (Ctrl+Shift+G)"
|
||||
onClick={onGenerateAI}
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Download size={18} />}
|
||||
label="Export Project (Ctrl+E)"
|
||||
onClick={onExport}
|
||||
variant="default"
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { EmptyState, Stack, Container, Card, Badge, ActionIcon, IconButton, Flex, Text, Heading, Button, TreeIcon } from '@/components/atoms'
|
||||
import { ComponentTree } from '@/types/project'
|
||||
import { FolderOpen } from '@phosphor-icons/react'
|
||||
|
||||
interface TreeListPanelProps {
|
||||
trees: ComponentTree[]
|
||||
selectedTreeId: string | null
|
||||
onTreeSelect: (treeId: string) => void
|
||||
onTreeEdit: (tree: ComponentTree) => void
|
||||
onTreeDuplicate: (tree: ComponentTree) => void
|
||||
onTreeDelete: (treeId: string) => void
|
||||
onCreateNew: () => void
|
||||
onImportJson: () => void
|
||||
onExportJson: () => void
|
||||
}
|
||||
|
||||
export function TreeListPanel({
|
||||
trees,
|
||||
selectedTreeId,
|
||||
onTreeSelect,
|
||||
onTreeEdit,
|
||||
onTreeDuplicate,
|
||||
onTreeDelete,
|
||||
onCreateNew,
|
||||
onImportJson,
|
||||
onExportJson,
|
||||
}: TreeListPanelProps) {
|
||||
return (
|
||||
<div className="w-80 border-r border-border bg-card p-4 flex flex-col gap-4">
|
||||
{/* TreeListHeader - inlined */}
|
||||
<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={!selectedTreeId}
|
||||
className="flex-1 text-xs"
|
||||
leftIcon={<ActionIcon action="download" size={14} />}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
|
||||
{trees.length === 0 ? (
|
||||
<Stack
|
||||
direction="vertical"
|
||||
align="center"
|
||||
justify="center"
|
||||
className="flex-1"
|
||||
>
|
||||
<EmptyState
|
||||
icon={<FolderOpen size={48} weight="duotone" />}
|
||||
title="No component trees yet"
|
||||
description="Create your first tree to get started"
|
||||
action={{
|
||||
label: 'Create First Tree',
|
||||
onClick: onCreateNew
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<ScrollArea className="flex-1">
|
||||
<Stack direction="vertical" spacing="sm">
|
||||
{trees.map((tree) => {
|
||||
const isSelected = selectedTreeId === tree.id
|
||||
const disableDelete = trees.length === 1
|
||||
|
||||
return (
|
||||
// TreeCard - inlined
|
||||
<Card
|
||||
key={tree.id}
|
||||
className={`cursor-pointer transition-all p-4 ${
|
||||
isSelected ? 'ring-2 ring-primary bg-accent' : 'hover:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => onTreeSelect(tree.id)}
|
||||
>
|
||||
<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={() => onTreeEdit(tree)}
|
||||
title="Edit tree"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<ActionIcon action="duplicate" size={14} />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onTreeDuplicate(tree)}
|
||||
title="Duplicate tree"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<ActionIcon action="delete" size={14} />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onTreeDelete(tree.id)}
|
||||
disabled={disableDelete}
|
||||
title={disableDelete ? "Can't delete last tree" : "Delete tree"}
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { NavigationMenu } from './NavigationMenu'
|
||||
export { AppHeader } from './AppHeader'
|
||||
export { TreeListPanel } from './TreeListPanel'
|
||||
export { SchemaEditorLayout } from './SchemaEditorLayout'
|
||||
export { SchemaCodeViewer } from './SchemaCodeViewer'
|
||||
export { SchemaEditorCanvas } from './SchemaEditorCanvas'
|
||||
export { SchemaEditorPropertiesPanel } from './SchemaEditorPropertiesPanel'
|
||||
export { SchemaEditorSidebar } from './SchemaEditorSidebar'
|
||||
export { SchemaEditorStatusBar } from './SchemaEditorStatusBar'
|
||||
export { SchemaEditorToolbar } from './SchemaEditorToolbar'
|
||||
export { JSONUIShowcase } from '../JSONUIShowcase'
|
||||
|
||||
Reference in New Issue
Block a user