mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: implement FlowExecutor for executing flow definitions with DAG traversal and node execution
This commit is contained in:
111
ccw/frontend/src/components/layout/AppShell.tsx
Normal file
111
ccw/frontend/src/components/layout/AppShell.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// ========================================
|
||||
// AppShell Component
|
||||
// ========================================
|
||||
// Root layout component combining Header, Sidebar, and MainContent
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Header } from './Header';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { MainContent } from './MainContent';
|
||||
|
||||
export interface AppShellProps {
|
||||
/** Initial sidebar collapsed state */
|
||||
defaultCollapsed?: boolean;
|
||||
/** Current project path to display in header */
|
||||
projectPath?: string;
|
||||
/** Callback for refresh action */
|
||||
onRefresh?: () => void;
|
||||
/** Whether refresh is in progress */
|
||||
isRefreshing?: boolean;
|
||||
/** Children to render in main content area */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Local storage key for sidebar state
|
||||
const SIDEBAR_COLLAPSED_KEY = 'ccw-sidebar-collapsed';
|
||||
|
||||
export function AppShell({
|
||||
defaultCollapsed = false,
|
||||
projectPath = '',
|
||||
onRefresh,
|
||||
isRefreshing = false,
|
||||
children,
|
||||
}: AppShellProps) {
|
||||
// Sidebar collapse state (persisted)
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY);
|
||||
return stored ? JSON.parse(stored) : defaultCollapsed;
|
||||
}
|
||||
return defaultCollapsed;
|
||||
});
|
||||
|
||||
// Mobile sidebar open state
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
// Persist sidebar state
|
||||
useEffect(() => {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, JSON.stringify(sidebarCollapsed));
|
||||
}, [sidebarCollapsed]);
|
||||
|
||||
// Close mobile sidebar on route change or resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = useCallback(() => {
|
||||
setMobileOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleMobileClose = useCallback(() => {
|
||||
setMobileOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleCollapsedChange = useCallback((collapsed: boolean) => {
|
||||
setSidebarCollapsed(collapsed);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-background">
|
||||
{/* Header - fixed at top */}
|
||||
<Header
|
||||
onMenuClick={handleMenuClick}
|
||||
projectPath={projectPath}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
|
||||
{/* Main layout - sidebar + content */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
mobileOpen={mobileOpen}
|
||||
onMobileClose={handleMobileClose}
|
||||
/>
|
||||
|
||||
{/* Main content area */}
|
||||
<MainContent
|
||||
className={cn(
|
||||
'transition-all duration-300',
|
||||
// Adjust padding on mobile when sidebar is hidden
|
||||
'md:ml-0'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</MainContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppShell;
|
||||
164
ccw/frontend/src/components/layout/Header.tsx
Normal file
164
ccw/frontend/src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
// ========================================
|
||||
// Header Component
|
||||
// ========================================
|
||||
// Top navigation bar with theme toggle and user menu
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Workflow,
|
||||
Menu,
|
||||
Moon,
|
||||
Sun,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
User,
|
||||
LogOut,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTheme } from '@/hooks';
|
||||
|
||||
export interface HeaderProps {
|
||||
/** Callback to toggle mobile sidebar */
|
||||
onMenuClick?: () => void;
|
||||
/** Current project path */
|
||||
projectPath?: string;
|
||||
/** Callback for refresh action */
|
||||
onRefresh?: () => void;
|
||||
/** Whether refresh is in progress */
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
|
||||
export function Header({
|
||||
onMenuClick,
|
||||
projectPath = '',
|
||||
onRefresh,
|
||||
isRefreshing = false,
|
||||
}: HeaderProps) {
|
||||
const { isDark, toggleTheme } = useTheme();
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
if (onRefresh && !isRefreshing) {
|
||||
onRefresh();
|
||||
}
|
||||
}, [onRefresh, isRefreshing]);
|
||||
|
||||
// Get display path (truncate if too long)
|
||||
const displayPath = projectPath.length > 40
|
||||
? '...' + projectPath.slice(-37)
|
||||
: projectPath || 'No project selected';
|
||||
|
||||
return (
|
||||
<header
|
||||
className="flex items-center justify-between px-4 md:px-5 h-14 bg-card border-b border-border sticky top-0 z-50 shadow-sm"
|
||||
role="banner"
|
||||
>
|
||||
{/* Left side - Menu button (mobile) and Logo */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Mobile menu toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={onMenuClick}
|
||||
aria-label="Toggle navigation menu"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</Button>
|
||||
|
||||
{/* Logo / Brand */}
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold text-primary hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Workflow className="w-6 h-6" />
|
||||
<span className="hidden sm:inline">Claude Code Workflow</span>
|
||||
<span className="sm:hidden">CCW</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right side - Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Project path indicator */}
|
||||
{projectPath && (
|
||||
<div className="hidden lg:flex items-center gap-2 px-3 py-1.5 bg-muted rounded-md text-sm text-muted-foreground max-w-[300px]">
|
||||
<span className="truncate" title={projectPath}>
|
||||
{displayPath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Refresh button */}
|
||||
{onRefresh && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
aria-label="Refresh workspace"
|
||||
title="Refresh workspace"
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn('w-5 h-5', isRefreshing && 'animate-spin')}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Theme toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{isDark ? (
|
||||
<Sun className="w-5 h-5" />
|
||||
) : (
|
||||
<Moon className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* User menu dropdown - simplified version */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full"
|
||||
aria-label="User menu"
|
||||
title="User menu"
|
||||
>
|
||||
<User className="w-5 h-5" />
|
||||
</Button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
<div className="absolute right-0 top-full mt-1 w-48 bg-card border border-border rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
<div className="py-1">
|
||||
<Link
|
||||
to="/settings"
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-foreground hover:bg-hover transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
<hr className="my-1 border-border" />
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-muted-foreground hover:bg-hover hover:text-foreground transition-colors w-full text-left"
|
||||
onClick={() => {
|
||||
// Placeholder for logout action
|
||||
console.log('Logout clicked');
|
||||
}}
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>Exit Dashboard</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
31
ccw/frontend/src/components/layout/MainContent.tsx
Normal file
31
ccw/frontend/src/components/layout/MainContent.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
// ========================================
|
||||
// MainContent Component
|
||||
// ========================================
|
||||
// Main content area with scrollable container and Outlet for routes
|
||||
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface MainContentProps {
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
/** Children to render instead of Outlet */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MainContent({ className, children }: MainContentProps) {
|
||||
return (
|
||||
<main
|
||||
className={cn(
|
||||
'flex-1 overflow-y-auto min-w-0',
|
||||
'p-4 md:p-6',
|
||||
className
|
||||
)}
|
||||
role="main"
|
||||
>
|
||||
{children ?? <Outlet />}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainContent;
|
||||
184
ccw/frontend/src/components/layout/Sidebar.tsx
Normal file
184
ccw/frontend/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
// ========================================
|
||||
// Sidebar Component
|
||||
// ========================================
|
||||
// Collapsible navigation sidebar with route links
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Home,
|
||||
FolderKanban,
|
||||
Workflow,
|
||||
RefreshCw,
|
||||
AlertCircle,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
Brain,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
export interface SidebarProps {
|
||||
/** Whether sidebar is collapsed */
|
||||
collapsed?: boolean;
|
||||
/** Callback when collapse state changes */
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
/** Whether sidebar is open on mobile */
|
||||
mobileOpen?: boolean;
|
||||
/** Callback to close mobile sidebar */
|
||||
onMobileClose?: () => void;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
badge?: number | string;
|
||||
badgeVariant?: 'default' | 'success' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ path: '/', label: 'Home', icon: Home },
|
||||
{ path: '/sessions', label: 'Sessions', icon: FolderKanban },
|
||||
{ path: '/orchestrator', label: 'Orchestrator', icon: Workflow },
|
||||
{ path: '/loops', label: 'Loop Monitor', icon: RefreshCw },
|
||||
{ path: '/issues', label: 'Issues', icon: AlertCircle },
|
||||
{ path: '/skills', label: 'Skills', icon: Sparkles },
|
||||
{ path: '/commands', label: 'Commands', icon: Terminal },
|
||||
{ path: '/memory', label: 'Memory', icon: Brain },
|
||||
{ path: '/settings', label: 'Settings', icon: Settings },
|
||||
{ path: '/help', label: 'Help', icon: HelpCircle },
|
||||
];
|
||||
|
||||
export function Sidebar({
|
||||
collapsed = false,
|
||||
onCollapsedChange,
|
||||
mobileOpen = false,
|
||||
onMobileClose,
|
||||
}: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const [internalCollapsed, setInternalCollapsed] = useState(collapsed);
|
||||
|
||||
const isCollapsed = onCollapsedChange ? collapsed : internalCollapsed;
|
||||
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
if (onCollapsedChange) {
|
||||
onCollapsedChange(!collapsed);
|
||||
} else {
|
||||
setInternalCollapsed(!internalCollapsed);
|
||||
}
|
||||
}, [collapsed, internalCollapsed, onCollapsedChange]);
|
||||
|
||||
const handleNavClick = useCallback(() => {
|
||||
// Close mobile sidebar when navigating
|
||||
if (onMobileClose) {
|
||||
onMobileClose();
|
||||
}
|
||||
}, [onMobileClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile overlay */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
||||
onClick={onMobileClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
'bg-sidebar-background border-r border-border flex flex-col transition-all duration-300',
|
||||
// Desktop styles
|
||||
'hidden md:flex sticky top-14 h-[calc(100vh-56px)]',
|
||||
isCollapsed ? 'w-16' : 'w-64',
|
||||
// Mobile styles
|
||||
'md:translate-x-0',
|
||||
mobileOpen && 'fixed left-0 top-14 flex translate-x-0 z-50 h-[calc(100vh-56px)] w-64 shadow-lg'
|
||||
)}
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<nav className="flex-1 py-3 overflow-y-auto">
|
||||
<ul className="space-y-1 px-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = location.pathname === item.path ||
|
||||
(item.path !== '/' && location.pathname.startsWith(item.path));
|
||||
|
||||
return (
|
||||
<li key={item.path}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
onClick={handleNavClick}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors',
|
||||
'hover:bg-hover hover:text-foreground',
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-muted-foreground',
|
||||
isCollapsed && 'justify-center px-2'
|
||||
)}
|
||||
title={isCollapsed ? item.label : undefined}
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<span className="flex-1">{item.label}</span>
|
||||
{item.badge !== undefined && (
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 text-xs font-semibold rounded-full',
|
||||
item.badgeVariant === 'success' && 'bg-success-light text-success',
|
||||
item.badgeVariant === 'warning' && 'bg-warning-light text-warning',
|
||||
item.badgeVariant === 'info' && 'bg-info-light text-info',
|
||||
(!item.badgeVariant || item.badgeVariant === 'default') &&
|
||||
'bg-muted text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Sidebar footer - collapse toggle */}
|
||||
<div className="p-3 border-t border-border hidden md:block">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleToggleCollapse}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 text-muted-foreground hover:text-foreground',
|
||||
isCollapsed && 'justify-center'
|
||||
)}
|
||||
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<PanelLeftOpen className="w-4 h-4" />
|
||||
) : (
|
||||
<>
|
||||
<PanelLeftClose className="w-4 h-4" />
|
||||
<span>Collapse</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
16
ccw/frontend/src/components/layout/index.ts
Normal file
16
ccw/frontend/src/components/layout/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// ========================================
|
||||
// Layout Components Barrel Export
|
||||
// ========================================
|
||||
// Re-export all layout components for convenient imports
|
||||
|
||||
export { AppShell } from './AppShell';
|
||||
export type { AppShellProps } from './AppShell';
|
||||
|
||||
export { Header } from './Header';
|
||||
export type { HeaderProps } from './Header';
|
||||
|
||||
export { Sidebar } from './Sidebar';
|
||||
export type { SidebarProps } from './Sidebar';
|
||||
|
||||
export { MainContent } from './MainContent';
|
||||
export type { MainContentProps } from './MainContent';
|
||||
Reference in New Issue
Block a user