mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
feat: add Accordion component for UI and Zustand store for coordinator management
- Implemented Accordion component using Radix UI for collapsible sections. - Created Zustand store to manage coordinator execution state, command chains, logs, and interactive questions. - Added validation tests for CLI settings type definitions, ensuring type safety and correct behavior of helper functions.
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
// ========================================
|
||||
// Sidebar Component
|
||||
// ========================================
|
||||
// Collapsible navigation sidebar with route links
|
||||
// Collapsible navigation sidebar with 6-group accordion structure
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Home,
|
||||
@@ -12,8 +11,6 @@ import {
|
||||
Workflow,
|
||||
RefreshCw,
|
||||
AlertCircle,
|
||||
ListTodo,
|
||||
Search,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
Brain,
|
||||
@@ -28,9 +25,15 @@ import {
|
||||
Shield,
|
||||
History,
|
||||
Server,
|
||||
Layers,
|
||||
Wrench,
|
||||
Cog,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Accordion } from '@/components/ui/Accordion';
|
||||
import { NavGroup, type NavItem } from '@/components/shared/NavGroup';
|
||||
import { useAppStore } from '@/stores/appStore';
|
||||
|
||||
export interface SidebarProps {
|
||||
/** Whether sidebar is collapsed */
|
||||
@@ -43,34 +46,82 @@ export interface SidebarProps {
|
||||
onMobileClose?: () => void;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
badge?: number | string;
|
||||
badgeVariant?: 'default' | 'success' | 'warning' | 'info';
|
||||
// Navigation group definitions
|
||||
interface NavGroupDef {
|
||||
id: string;
|
||||
titleKey: string;
|
||||
icon?: React.ElementType;
|
||||
items: Array<{
|
||||
path: string;
|
||||
labelKey: string;
|
||||
icon: React.ElementType;
|
||||
badge?: number | string;
|
||||
badgeVariant?: 'default' | 'success' | 'warning' | 'info';
|
||||
}>;
|
||||
}
|
||||
|
||||
// Navigation item definitions (without labels for i18n)
|
||||
const navItemDefinitions: Omit<NavItem, 'label'>[] = [
|
||||
{ path: '/', icon: Home },
|
||||
{ path: '/sessions', icon: FolderKanban },
|
||||
{ path: '/lite-tasks', icon: Zap },
|
||||
{ path: '/project', icon: LayoutDashboard },
|
||||
{ path: '/history', icon: Clock },
|
||||
{ path: '/orchestrator', icon: Workflow },
|
||||
{ path: '/loops', icon: RefreshCw },
|
||||
{ path: '/issues', icon: AlertCircle },
|
||||
{ path: '/skills', icon: Sparkles },
|
||||
{ path: '/commands', icon: Terminal },
|
||||
{ path: '/memory', icon: Brain },
|
||||
{ path: '/prompts', icon: History },
|
||||
{ path: '/hooks', icon: GitFork },
|
||||
{ path: '/settings', icon: Settings },
|
||||
{ path: '/settings/rules', icon: Shield },
|
||||
{ path: '/settings/codexlens', icon: Sparkles },
|
||||
{ path: '/api-settings', icon: Server },
|
||||
{ path: '/help', icon: HelpCircle },
|
||||
// Define the 6 navigation groups with their items
|
||||
const navGroupDefinitions: NavGroupDef[] = [
|
||||
{
|
||||
id: 'overview',
|
||||
titleKey: 'navigation.groups.overview',
|
||||
icon: Layers,
|
||||
items: [
|
||||
{ path: '/', labelKey: 'navigation.main.home', icon: Home },
|
||||
{ path: '/project', labelKey: 'navigation.main.project', icon: LayoutDashboard },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'workflow',
|
||||
titleKey: 'navigation.groups.workflow',
|
||||
icon: Workflow,
|
||||
items: [
|
||||
{ path: '/sessions', labelKey: 'navigation.main.sessions', icon: FolderKanban },
|
||||
{ path: '/lite-tasks', labelKey: 'navigation.main.liteTasks', icon: Zap },
|
||||
{ path: '/orchestrator', labelKey: 'navigation.main.orchestrator', icon: Workflow },
|
||||
{ path: '/loops', labelKey: 'navigation.main.loops', icon: RefreshCw },
|
||||
{ path: '/history', labelKey: 'navigation.main.history', icon: Clock },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
titleKey: 'navigation.groups.knowledge',
|
||||
icon: Brain,
|
||||
items: [
|
||||
{ path: '/memory', labelKey: 'navigation.main.memory', icon: Brain },
|
||||
{ path: '/prompts', labelKey: 'navigation.main.prompts', icon: History },
|
||||
{ path: '/skills', labelKey: 'navigation.main.skills', icon: Sparkles },
|
||||
{ path: '/commands', labelKey: 'navigation.main.commands', icon: Terminal },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'issues',
|
||||
titleKey: 'navigation.groups.issues',
|
||||
icon: AlertCircle,
|
||||
items: [
|
||||
{ path: '/issues', labelKey: 'navigation.main.issues', icon: AlertCircle },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
titleKey: 'navigation.groups.tools',
|
||||
icon: Wrench,
|
||||
items: [
|
||||
{ path: '/hooks', labelKey: 'navigation.main.hooks', icon: GitFork },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'configuration',
|
||||
titleKey: 'navigation.groups.configuration',
|
||||
icon: Cog,
|
||||
items: [
|
||||
{ path: '/settings', labelKey: 'navigation.main.settings', icon: Settings },
|
||||
{ path: '/settings/rules', labelKey: 'navigation.main.rules', icon: Shield },
|
||||
{ path: '/settings/codexlens', labelKey: 'navigation.main.codexlens', icon: Sparkles },
|
||||
{ path: '/api-settings', labelKey: 'navigation.main.apiSettings', icon: Server },
|
||||
{ path: '/help', labelKey: 'navigation.main.help', icon: HelpCircle },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar({
|
||||
@@ -80,18 +131,17 @@ export function Sidebar({
|
||||
onMobileClose,
|
||||
}: SidebarProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const location = useLocation();
|
||||
const [internalCollapsed, setInternalCollapsed] = useState(collapsed);
|
||||
const { sidebarCollapsed, expandedNavGroups, setExpandedNavGroups } = useAppStore();
|
||||
|
||||
const isCollapsed = onCollapsedChange ? collapsed : internalCollapsed;
|
||||
const isCollapsed = onCollapsedChange ? collapsed : sidebarCollapsed;
|
||||
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
if (onCollapsedChange) {
|
||||
onCollapsedChange(!collapsed);
|
||||
} else {
|
||||
setInternalCollapsed(!internalCollapsed);
|
||||
useAppStore.getState().setSidebarCollapsed(!sidebarCollapsed);
|
||||
}
|
||||
}, [collapsed, internalCollapsed, onCollapsedChange]);
|
||||
}, [collapsed, sidebarCollapsed, onCollapsedChange]);
|
||||
|
||||
const handleNavClick = useCallback(() => {
|
||||
// Close mobile sidebar when navigating
|
||||
@@ -100,31 +150,18 @@ export function Sidebar({
|
||||
}
|
||||
}, [onMobileClose]);
|
||||
|
||||
// Build nav items with translated labels
|
||||
const navItems = useMemo(() => {
|
||||
const keyMap: Record<string, string> = {
|
||||
'/': 'main.home',
|
||||
'/sessions': 'main.sessions',
|
||||
'/lite-tasks': 'main.liteTasks',
|
||||
'/project': 'main.project',
|
||||
'/history': 'main.history',
|
||||
'/orchestrator': 'main.orchestrator',
|
||||
'/loops': 'main.loops',
|
||||
'/issues': 'main.issues',
|
||||
'/skills': 'main.skills',
|
||||
'/commands': 'main.commands',
|
||||
'/memory': 'main.memory',
|
||||
'/prompts': 'main.prompts',
|
||||
'/hooks': 'main.hooks',
|
||||
'/settings': 'main.settings',
|
||||
'/settings/rules': 'main.rules',
|
||||
'/settings/codexlens': 'main.codexlens',
|
||||
'/api-settings': 'main.apiSettings',
|
||||
'/help': 'main.help',
|
||||
};
|
||||
return navItemDefinitions.map((item) => ({
|
||||
...item,
|
||||
label: formatMessage({ id: `navigation.${keyMap[item.path]}` }),
|
||||
const handleAccordionChange = useCallback((value: string[]) => {
|
||||
setExpandedNavGroups(value);
|
||||
}, [setExpandedNavGroups]);
|
||||
|
||||
// Build nav groups with translated labels
|
||||
const navGroups = useMemo(() => {
|
||||
return navGroupDefinitions.map((group) => ({
|
||||
...group,
|
||||
items: group.items.map((item) => ({
|
||||
...item,
|
||||
label: formatMessage({ id: item.labelKey }),
|
||||
})) as NavItem[],
|
||||
}));
|
||||
}, [formatMessage]);
|
||||
|
||||
@@ -153,57 +190,42 @@ export function Sidebar({
|
||||
aria-label={formatMessage({ id: 'navigation.header.brand' })}
|
||||
>
|
||||
<nav className="flex-1 py-3 overflow-y-auto">
|
||||
<ul className="space-y-1 px-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
// Parse item path to extract base path and query params
|
||||
const [basePath, searchParams] = item.path.split('?');
|
||||
const isActive = location.pathname === basePath ||
|
||||
(basePath !== '/' && location.pathname.startsWith(basePath));
|
||||
// For query param items, also check if search matches
|
||||
const isQueryParamActive = searchParams &&
|
||||
location.search.includes(searchParams);
|
||||
|
||||
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 && !searchParams) || isQueryParamActive
|
||||
? '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>
|
||||
{isCollapsed ? (
|
||||
// Collapsed view: render flat list of icons
|
||||
<div className="space-y-4 px-2">
|
||||
{navGroups.map((group) => (
|
||||
<NavGroup
|
||||
key={group.id}
|
||||
groupId={group.id}
|
||||
titleKey={group.titleKey}
|
||||
icon={group.icon}
|
||||
items={group.items}
|
||||
collapsed={true}
|
||||
onNavClick={handleNavClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Expanded view: render accordion groups
|
||||
<Accordion
|
||||
type="multiple"
|
||||
value={expandedNavGroups}
|
||||
onValueChange={handleAccordionChange}
|
||||
className="space-y-1 px-2"
|
||||
>
|
||||
{navGroups.map((group) => (
|
||||
<NavGroup
|
||||
key={group.id}
|
||||
groupId={group.id}
|
||||
titleKey={group.titleKey}
|
||||
icon={group.icon}
|
||||
items={group.items}
|
||||
collapsed={false}
|
||||
onNavClick={handleNavClick}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Sidebar footer - collapse toggle */}
|
||||
|
||||
Reference in New Issue
Block a user