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:
catlog22
2026-02-03 10:02:40 +08:00
parent bcb4af3ba0
commit 5483a72e9f
82 changed files with 6156 additions and 7605 deletions

View File

@@ -0,0 +1,143 @@
// ========================================
// NavGroup Component
// ========================================
// Collapsible navigation group using Radix Accordion
import { NavLink, useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { cn } from '@/lib/utils';
import {
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/components/ui/Accordion';
export interface NavItem {
path: string;
label: string;
icon: React.ElementType;
badge?: number | string;
badgeVariant?: 'default' | 'success' | 'warning' | 'info';
}
export interface NavGroupProps {
/** Unique identifier for the group */
groupId: string;
/** Title i18n key */
titleKey: string;
/** Optional icon for group header */
icon?: React.ElementType;
/** Navigation items in this group */
items: NavItem[];
/** Whether sidebar is collapsed */
collapsed?: boolean;
/** Callback when nav item is clicked */
onNavClick?: () => void;
}
export function NavGroup({
groupId,
titleKey,
icon: Icon,
items,
collapsed = false,
onNavClick,
}: NavGroupProps) {
const { formatMessage } = useIntl();
const location = useLocation();
const title = formatMessage({ id: titleKey });
// If collapsed, render items without accordion
if (collapsed) {
return (
<div className="space-y-1">
{items.map((item) => {
const ItemIcon = item.icon;
const [basePath] = item.path.split('?');
const isActive =
location.pathname === basePath ||
(basePath !== '/' && location.pathname.startsWith(basePath));
return (
<NavLink
key={item.path}
to={item.path}
onClick={onNavClick}
className={cn(
'flex items-center justify-center gap-3 px-2 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'
)}
title={item.label}
>
<ItemIcon className="w-5 h-5 flex-shrink-0" />
</NavLink>
);
})}
</div>
);
}
return (
<AccordionItem value={groupId} className="border-none">
<AccordionTrigger className="px-3 py-2 hover:no-underline hover:bg-hover/50 rounded-md text-muted-foreground hover:text-foreground">
<div className="flex items-center gap-2 text-sm font-semibold">
{Icon && <Icon className="w-4 h-4" />}
<span>{title}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-1">
<ul className="space-y-1">
{items.map((item) => {
const ItemIcon = item.icon;
const [basePath, searchParams] = item.path.split('?');
const isActive =
location.pathname === basePath ||
(basePath !== '/' && location.pathname.startsWith(basePath));
const isQueryParamActive =
searchParams && location.search.includes(searchParams);
return (
<li key={item.path}>
<NavLink
to={item.path}
onClick={onNavClick}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors pl-6',
'hover:bg-hover hover:text-foreground',
(isActive && !searchParams) || isQueryParamActive
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground'
)}
>
<ItemIcon className="w-4 h-4 flex-shrink-0" />
<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>
</AccordionContent>
</AccordionItem>
);
}
export default NavGroup;