mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 14:14:57 +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 AppDialogs from '@/components/app/AppDialogs'
|
||||||
import AppMainPanel from '@/components/app/AppMainPanel'
|
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 { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
|
||||||
import appStrings from '@/data/app-shortcuts.json'
|
import appStrings from '@/data/app-shortcuts.json'
|
||||||
import useAppNavigation from '@/hooks/use-app-navigation'
|
import useAppNavigation from '@/hooks/use-app-navigation'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
import { AppHeader } from '@/components/organisms'
|
import { AppHeader } from '@/lib/json-ui/json-components'
|
||||||
import { PWARegistry } from '@/lib/component-registry'
|
import { PWARegistry } from '@/lib/component-registry'
|
||||||
import { RouterProvider } from '@/router'
|
import { RouterProvider } from '@/router'
|
||||||
import type { FeatureToggles, Project } from '@/types/project'
|
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 { 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'
|
export { JSONUIShowcase } from '../JSONUIShowcase'
|
||||||
|
|||||||
Reference in New Issue
Block a user