feat: implement FlowExecutor for executing flow definitions with DAG traversal and node execution

This commit is contained in:
catlog22
2026-01-30 16:59:18 +08:00
parent 0a7c1454d9
commit a5c3dff8d3
92 changed files with 23875 additions and 542 deletions

View 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;

View 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;

View 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;

View 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;

View 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';

View File

@@ -0,0 +1,238 @@
// ========================================
// IssueCard Component
// ========================================
// Card component for displaying issues with actions
import { useState } from 'react';
import {
AlertCircle,
AlertTriangle,
Info,
MoreVertical,
Edit,
Trash2,
ExternalLink,
CheckCircle,
Clock,
XCircle,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@/components/ui/Dropdown';
import type { Issue } from '@/lib/api';
// ========== Types ==========
export interface IssueCardProps {
issue: Issue;
onEdit?: (issue: Issue) => void;
onDelete?: (issue: Issue) => void;
onClick?: (issue: Issue) => void;
onStatusChange?: (issue: Issue, status: Issue['status']) => void;
className?: string;
compact?: boolean;
showActions?: boolean;
draggableProps?: Record<string, unknown>;
dragHandleProps?: Record<string, unknown>;
innerRef?: React.Ref<HTMLDivElement>;
}
// ========== Priority Helpers ==========
const priorityConfig: Record<Issue['priority'], { icon: React.ElementType; color: string; label: string }> = {
critical: { icon: AlertCircle, color: 'destructive', label: 'Critical' },
high: { icon: AlertTriangle, color: 'warning', label: 'High' },
medium: { icon: Info, color: 'info', label: 'Medium' },
low: { icon: Info, color: 'secondary', label: 'Low' },
};
const statusConfig: Record<Issue['status'], { icon: React.ElementType; color: string; label: string }> = {
open: { icon: AlertCircle, color: 'info', label: 'Open' },
in_progress: { icon: Clock, color: 'warning', label: 'In Progress' },
resolved: { icon: CheckCircle, color: 'success', label: 'Resolved' },
closed: { icon: XCircle, color: 'muted', label: 'Closed' },
completed: { icon: CheckCircle, color: 'success', label: 'Completed' },
};
// ========== Priority Badge ==========
export function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
const config = priorityConfig[priority];
const Icon = config.icon;
return (
<Badge variant={config.color as 'default' | 'secondary' | 'destructive' | 'outline'} className="gap-1">
<Icon className="w-3 h-3" />
{config.label}
</Badge>
);
}
// ========== Status Badge ==========
export function StatusBadge({ status }: { status: Issue['status'] }) {
const config = statusConfig[status];
const Icon = config.icon;
return (
<Badge variant="outline" className="gap-1">
<Icon className="w-3 h-3" />
{config.label}
</Badge>
);
}
// ========== Main IssueCard Component ==========
export function IssueCard({
issue,
onEdit,
onDelete,
onClick,
onStatusChange,
className,
compact = false,
showActions = true,
draggableProps,
dragHandleProps,
innerRef,
}: IssueCardProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleClick = () => {
if (!isMenuOpen) {
onClick?.(issue);
}
};
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation();
setIsMenuOpen(false);
onEdit?.(issue);
};
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
setIsMenuOpen(false);
onDelete?.(issue);
};
if (compact) {
return (
<div
ref={innerRef}
{...draggableProps}
{...dragHandleProps}
onClick={handleClick}
className={cn(
'p-3 bg-card border border-border rounded-lg cursor-pointer',
'hover:shadow-md hover:border-primary/50 transition-all',
className
)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{issue.title}</p>
<p className="text-xs text-muted-foreground mt-0.5">#{issue.id}</p>
</div>
<PriorityBadge priority={issue.priority} />
</div>
</div>
);
}
return (
<Card
ref={innerRef}
{...draggableProps}
onClick={handleClick}
className={cn(
'p-4 cursor-pointer hover:shadow-md hover:border-primary/50 transition-all',
className
)}
>
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0" {...dragHandleProps}>
<h3 className="text-sm font-medium text-foreground line-clamp-2">
{issue.title}
</h3>
<p className="text-xs text-muted-foreground mt-1">#{issue.id}</p>
</div>
{showActions && (
<Dropdown open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<DropdownTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownTrigger>
<DropdownContent align="end">
<DropdownItem onClick={handleEdit}>
<Edit className="w-4 h-4 mr-2" />
Edit
</DropdownItem>
<DropdownItem onClick={() => onStatusChange?.(issue, 'in_progress')}>
<Clock className="w-4 h-4 mr-2" />
Start Progress
</DropdownItem>
<DropdownItem onClick={() => onStatusChange?.(issue, 'resolved')}>
<CheckCircle className="w-4 h-4 mr-2" />
Mark Resolved
</DropdownItem>
<DropdownItem onClick={handleDelete} className="text-destructive">
<Trash2 className="w-4 h-4 mr-2" />
Delete
</DropdownItem>
</DropdownContent>
</Dropdown>
)}
</div>
{/* Context Preview */}
{issue.context && (
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
{issue.context}
</p>
)}
{/* Labels */}
{issue.labels && issue.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{issue.labels.slice(0, 3).map((label) => (
<Badge key={label} variant="outline" className="text-xs">
{label}
</Badge>
))}
{issue.labels.length > 3 && (
<Badge variant="outline" className="text-xs">
+{issue.labels.length - 3}
</Badge>
)}
</div>
)}
{/* Footer */}
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
<PriorityBadge priority={issue.priority} />
<StatusBadge status={issue.status} />
</div>
{/* Solutions Count */}
{issue.solutions && issue.solutions.length > 0 && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<ExternalLink className="w-3 h-3" />
{issue.solutions.length} solution{issue.solutions.length !== 1 ? 's' : ''}
</div>
)}
</Card>
);
}
export default IssueCard;

View File

@@ -0,0 +1,265 @@
// ========================================
// KanbanBoard Component
// ========================================
// Drag-and-drop kanban board for loops and tasks
import { useState, useCallback } from 'react';
import {
DragDropContext,
Droppable,
Draggable,
type DropResult,
type DraggableProvided,
type DroppableProvided,
} from '@hello-pangea/dnd';
import { cn } from '@/lib/utils';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
// ========== Types ==========
export interface KanbanItem {
id: string;
title?: string;
status: string;
[key: string]: unknown;
}
export interface KanbanColumn<T extends KanbanItem = KanbanItem> {
id: string;
title: string;
items: T[];
color?: string;
icon?: React.ReactNode;
}
export interface KanbanBoardProps<T extends KanbanItem = KanbanItem> {
columns: KanbanColumn<T>[];
onDragEnd?: (result: DropResult, sourceColumn: string, destColumn: string) => void;
onItemClick?: (item: T) => void;
renderItem?: (item: T, provided: DraggableProvided) => React.ReactNode;
className?: string;
columnClassName?: string;
itemClassName?: string;
emptyColumnMessage?: string;
isLoading?: boolean;
}
// ========== Default Item Renderer ==========
function DefaultItemRenderer<T extends KanbanItem>({
item,
provided,
onClick,
className,
}: {
item: T;
provided: DraggableProvided;
onClick?: () => void;
className?: string;
}) {
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
onClick={onClick}
className={cn(
'p-3 bg-card border border-border rounded-lg shadow-sm cursor-pointer',
'hover:shadow-md hover:border-primary/50 transition-all',
'focus:outline-none focus:ring-2 focus:ring-primary/50',
className
)}
>
<p className="text-sm font-medium text-foreground truncate">
{item.title || item.id}
</p>
</div>
);
}
// ========== Column Component ==========
function KanbanColumnComponent<T extends KanbanItem>({
column,
onItemClick,
renderItem,
itemClassName,
emptyMessage,
}: {
column: KanbanColumn<T>;
onItemClick?: (item: T) => void;
renderItem?: (item: T, provided: DraggableProvided) => React.ReactNode;
itemClassName?: string;
emptyMessage?: string;
}) {
return (
<Droppable droppableId={column.id}>
{(provided: DroppableProvided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={cn(
'min-h-[200px] p-2 space-y-2 rounded-lg transition-colors',
snapshot.isDraggingOver && 'bg-primary/5'
)}
>
{column.items.length === 0 ? (
<p className="text-center text-sm text-muted-foreground py-8">
{emptyMessage || 'No items'}
</p>
) : (
column.items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(dragProvided: DraggableProvided) =>
renderItem ? (
renderItem(item, dragProvided)
) : (
<DefaultItemRenderer
item={item}
provided={dragProvided}
onClick={() => onItemClick?.(item)}
className={itemClassName}
/>
)
}
</Draggable>
))
)}
{provided.placeholder}
</div>
)}
</Droppable>
);
}
// ========== Main Kanban Board Component ==========
export function KanbanBoard<T extends KanbanItem = KanbanItem>({
columns,
onDragEnd,
onItemClick,
renderItem,
className,
columnClassName,
itemClassName,
emptyColumnMessage,
isLoading = false,
}: KanbanBoardProps<T>) {
const handleDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) return;
const { source, destination } = result;
if (
source.droppableId === destination.droppableId &&
source.index === destination.index
) {
return;
}
onDragEnd?.(result, source.droppableId, destination.droppableId);
},
[onDragEnd]
);
if (isLoading) {
return (
<div className={cn('grid gap-4', className)} style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` }}>
{columns.map((column) => (
<Card key={column.id} className={cn('p-4', columnClassName)}>
<div className="h-6 w-24 bg-muted animate-pulse rounded mb-4" />
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
</Card>
))}
</div>
);
}
return (
<DragDropContext onDragEnd={handleDragEnd}>
<div
className={cn('grid gap-4', className)}
style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` }}
>
{columns.map((column) => (
<Card key={column.id} className={cn('p-4', columnClassName)}>
{/* Column Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
{column.icon}
<h3 className="font-medium text-foreground">{column.title}</h3>
</div>
<Badge
variant="secondary"
className={cn(column.color && `bg-${column.color}/10 text-${column.color}`)}
>
{column.items.length}
</Badge>
</div>
{/* Column Content */}
<KanbanColumnComponent
column={column}
onItemClick={onItemClick}
renderItem={renderItem}
itemClassName={itemClassName}
emptyMessage={emptyColumnMessage}
/>
</Card>
))}
</div>
</DragDropContext>
);
}
// ========== Loop-specific Kanban ==========
export interface LoopKanbanItem extends KanbanItem {
status: 'created' | 'running' | 'paused' | 'completed' | 'failed';
currentStep?: number;
totalSteps?: number;
prompt?: string;
tool?: string;
}
export function useLoopKanbanColumns(loopsByStatus: Record<string, LoopKanbanItem[]>): KanbanColumn<LoopKanbanItem>[] {
return [
{
id: 'created',
title: 'Pending',
items: loopsByStatus.created || [],
color: 'muted',
},
{
id: 'running',
title: 'Running',
items: loopsByStatus.running || [],
color: 'primary',
},
{
id: 'paused',
title: 'Paused',
items: loopsByStatus.paused || [],
color: 'warning',
},
{
id: 'completed',
title: 'Completed',
items: loopsByStatus.completed || [],
color: 'success',
},
{
id: 'failed',
title: 'Failed',
items: loopsByStatus.failed || [],
color: 'destructive',
},
];
}
export default KanbanBoard;

View File

@@ -0,0 +1,287 @@
// ========================================
// SessionCard Component
// ========================================
// Session card with status badge and action menu
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from '@/components/ui/Dropdown';
import {
Calendar,
ListChecks,
MoreVertical,
Eye,
Archive,
Trash2,
Play,
Pause,
} from 'lucide-react';
import type { SessionMetadata } from '@/types/store';
export interface SessionCardProps {
/** Session data */
session: SessionMetadata;
/** Called when view action is triggered */
onView?: (sessionId: string) => void;
/** Called when archive action is triggered */
onArchive?: (sessionId: string) => void;
/** Called when delete action is triggered */
onDelete?: (sessionId: string) => void;
/** Called when card is clicked */
onClick?: (sessionId: string) => void;
/** Optional className */
className?: string;
/** Show actions dropdown */
showActions?: boolean;
/** Disabled state for actions */
actionsDisabled?: boolean;
}
// Status badge configuration
const statusConfig: Record<
SessionMetadata['status'],
{ label: string; variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' }
> = {
planning: { label: 'Planning', variant: 'info' },
in_progress: { label: 'In Progress', variant: 'warning' },
completed: { label: 'Completed', variant: 'success' },
archived: { label: 'Archived', variant: 'secondary' },
paused: { label: 'Paused', variant: 'default' },
};
/**
* Format date to localized string
*/
function formatDate(dateString: string | undefined): string {
if (!dateString) return 'Unknown';
try {
const date = new Date(dateString);
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
} catch {
return 'Invalid date';
}
}
/**
* Calculate progress percentage from tasks
*/
function calculateProgress(tasks: SessionMetadata['tasks']): {
completed: number;
total: number;
percentage: number;
} {
if (!tasks || tasks.length === 0) {
return { completed: 0, total: 0, percentage: 0 };
}
const completed = tasks.filter((t) => t.status === 'completed').length;
const total = tasks.length;
const percentage = Math.round((completed / total) * 100);
return { completed, total, percentage };
}
/**
* SessionCard component for displaying session information
*
* @example
* ```tsx
* <SessionCard
* session={session}
* onView={(id) => navigate(`/sessions/${id}`)}
* onArchive={(id) => archiveSession(id)}
* onDelete={(id) => deleteSession(id)}
* />
* ```
*/
export function SessionCard({
session,
onView,
onArchive,
onDelete,
onClick,
className,
showActions = true,
actionsDisabled = false,
}: SessionCardProps) {
const { label: statusLabel, variant: statusVariant } = statusConfig[session.status] || {
label: 'Unknown',
variant: 'default' as const,
};
const progress = calculateProgress(session.tasks);
const isPlanning = session.status === 'planning';
const isArchived = session.status === 'archived' || session.location === 'archived';
const handleCardClick = (e: React.MouseEvent) => {
// Don't trigger if clicking on dropdown
if ((e.target as HTMLElement).closest('[data-radix-popper-content-wrapper]')) {
return;
}
onClick?.(session.session_id);
};
const handleAction = (
e: React.MouseEvent,
action: 'view' | 'archive' | 'delete'
) => {
e.stopPropagation();
switch (action) {
case 'view':
onView?.(session.session_id);
break;
case 'archive':
onArchive?.(session.session_id);
break;
case 'delete':
onDelete?.(session.session_id);
break;
}
};
return (
<Card
className={cn(
'group cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary/30',
isPlanning && 'border-info/30 bg-info/5',
className
)}
onClick={handleCardClick}
>
<CardContent className="p-4">
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-card-foreground truncate">
{session.title || session.session_id}
</h3>
{session.title && session.title !== session.session_id && (
<p className="text-xs text-muted-foreground truncate mt-0.5">
{session.session_id}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge variant={statusVariant}>{statusLabel}</Badge>
{showActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
disabled={actionsDisabled}
>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">Actions</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => handleAction(e, 'view')}>
<Eye className="mr-2 h-4 w-4" />
View Details
</DropdownMenuItem>
{!isArchived && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={(e) => handleAction(e, 'archive')}>
<Archive className="mr-2 h-4 w-4" />
Archive
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => handleAction(e, 'delete')}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
{/* Meta info */}
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{formatDate(session.created_at)}
</span>
<span className="flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
{progress.total} tasks
</span>
</div>
{/* Progress bar (only show if not planning and has tasks) */}
{progress.total > 0 && !isPlanning && (
<div className="mt-3">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">Progress</span>
<span className="text-card-foreground font-medium">
{progress.completed}/{progress.total} ({progress.percentage}%)
</span>
</div>
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${progress.percentage}%` }}
/>
</div>
</div>
)}
{/* Description (if exists) */}
{session.description && (
<p className="mt-2 text-xs text-muted-foreground line-clamp-2">
{session.description}
</p>
)}
</CardContent>
</Card>
);
}
/**
* Skeleton loader for SessionCard
*/
export function SessionCardSkeleton({ className }: { className?: string }) {
return (
<Card className={cn('animate-pulse', className)}>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="h-5 w-32 rounded bg-muted" />
<div className="mt-1 h-3 w-24 rounded bg-muted" />
</div>
<div className="h-5 w-16 rounded-full bg-muted" />
</div>
<div className="mt-3 flex gap-4">
<div className="h-4 w-20 rounded bg-muted" />
<div className="h-4 w-16 rounded bg-muted" />
</div>
<div className="mt-3">
<div className="h-1.5 w-full rounded-full bg-muted" />
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,257 @@
// ========================================
// SkillCard Component
// ========================================
// Card component for displaying skills with enable/disable toggle
import { useState } from 'react';
import {
Sparkles,
MoreVertical,
Info,
Settings,
Power,
PowerOff,
Tag,
User,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@/components/ui/Dropdown';
import type { Skill } from '@/lib/api';
// ========== Types ==========
export interface SkillCardProps {
skill: Skill;
onToggle?: (skill: Skill, enabled: boolean) => void;
onClick?: (skill: Skill) => void;
onConfigure?: (skill: Skill) => void;
className?: string;
compact?: boolean;
showActions?: boolean;
isToggling?: boolean;
}
// ========== Source Badge ==========
const sourceConfig: Record<NonNullable<Skill['source']>, { color: string; label: string }> = {
builtin: { color: 'default', label: 'Built-in' },
custom: { color: 'secondary', label: 'Custom' },
community: { color: 'outline', label: 'Community' },
};
export function SourceBadge({ source }: { source?: Skill['source'] }) {
const config = sourceConfig[source ?? 'builtin'];
return (
<Badge variant={config.color as 'default' | 'secondary' | 'destructive' | 'outline'}>
{config.label}
</Badge>
);
}
// ========== Main SkillCard Component ==========
export function SkillCard({
skill,
onToggle,
onClick,
onConfigure,
className,
compact = false,
showActions = true,
isToggling = false,
}: SkillCardProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleClick = () => {
if (!isMenuOpen) {
onClick?.(skill);
}
};
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
onToggle?.(skill, !skill.enabled);
};
const handleConfigure = (e: React.MouseEvent) => {
e.stopPropagation();
setIsMenuOpen(false);
onConfigure?.(skill);
};
if (compact) {
return (
<div
onClick={handleClick}
className={cn(
'p-3 bg-card border border-border rounded-lg cursor-pointer',
'hover:shadow-md hover:border-primary/50 transition-all',
!skill.enabled && 'opacity-60',
className
)}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<Sparkles className={cn('w-4 h-4 flex-shrink-0', skill.enabled ? 'text-primary' : 'text-muted-foreground')} />
<span className="text-sm font-medium text-foreground truncate">{skill.name}</span>
</div>
<Button
variant={skill.enabled ? 'default' : 'outline'}
size="sm"
className="h-7 px-2"
onClick={handleToggle}
disabled={isToggling}
>
{skill.enabled ? (
<>
<Power className="w-3 h-3 mr-1" />
On
</>
) : (
<>
<PowerOff className="w-3 h-3 mr-1" />
Off
</>
)}
</Button>
</div>
</div>
);
}
return (
<Card
onClick={handleClick}
className={cn(
'p-4 cursor-pointer hover:shadow-md hover:border-primary/50 transition-all',
!skill.enabled && 'opacity-75',
className
)}
>
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-3 min-w-0">
<div className={cn(
'p-2 rounded-lg flex-shrink-0',
skill.enabled ? 'bg-primary/10' : 'bg-muted'
)}>
<Sparkles className={cn('w-5 h-5', skill.enabled ? 'text-primary' : 'text-muted-foreground')} />
</div>
<div className="min-w-0">
<h3 className="text-sm font-medium text-foreground">{skill.name}</h3>
{skill.version && (
<p className="text-xs text-muted-foreground">v{skill.version}</p>
)}
</div>
</div>
{showActions && (
<Dropdown open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<DropdownTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownTrigger>
<DropdownContent align="end">
<DropdownItem onClick={() => onClick?.(skill)}>
<Info className="w-4 h-4 mr-2" />
View Details
</DropdownItem>
<DropdownItem onClick={handleConfigure}>
<Settings className="w-4 h-4 mr-2" />
Configure
</DropdownItem>
<DropdownItem onClick={handleToggle}>
{skill.enabled ? (
<>
<PowerOff className="w-4 h-4 mr-2" />
Disable
</>
) : (
<>
<Power className="w-4 h-4 mr-2" />
Enable
</>
)}
</DropdownItem>
</DropdownContent>
</Dropdown>
)}
</div>
{/* Description */}
<p className="text-sm text-muted-foreground mt-3 line-clamp-2">
{skill.description}
</p>
{/* Triggers */}
{skill.triggers && skill.triggers.length > 0 && (
<div className="mt-3">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<Tag className="w-3 h-3" />
Triggers
</div>
<div className="flex flex-wrap gap-1">
{skill.triggers.slice(0, 4).map((trigger) => (
<Badge key={trigger} variant="outline" className="text-xs">
{trigger}
</Badge>
))}
{skill.triggers.length > 4 && (
<Badge variant="outline" className="text-xs">
+{skill.triggers.length - 4}
</Badge>
)}
</div>
</div>
)}
{/* Footer */}
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border">
<div className="flex items-center gap-2">
<SourceBadge source={skill.source} />
{skill.category && (
<Badge variant="outline" className="text-xs">
{skill.category}
</Badge>
)}
</div>
<Button
variant={skill.enabled ? 'default' : 'outline'}
size="sm"
onClick={handleToggle}
disabled={isToggling}
>
{skill.enabled ? (
<>
<Power className="w-4 h-4 mr-1" />
Enabled
</>
) : (
<>
<PowerOff className="w-4 h-4 mr-1" />
Disabled
</>
)}
</Button>
</div>
{/* Author */}
{skill.author && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<User className="w-3 h-3" />
{skill.author}
</div>
)}
</Card>
);
}
export default SkillCard;

View File

@@ -0,0 +1,161 @@
// ========================================
// StatCard Component
// ========================================
// Reusable stat card for dashboard metrics
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/Card';
import { TrendingUp, TrendingDown, Minus, type LucideIcon } from 'lucide-react';
const statCardVariants = cva(
'transition-all duration-200 hover:shadow-md',
{
variants: {
variant: {
default: 'border-border',
primary: 'border-primary/30 bg-primary/5',
success: 'border-success/30 bg-success/5',
warning: 'border-warning/30 bg-warning/5',
danger: 'border-destructive/30 bg-destructive/5',
info: 'border-info/30 bg-info/5',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const iconContainerVariants = cva(
'flex h-10 w-10 items-center justify-center rounded-lg',
{
variants: {
variant: {
default: 'bg-muted text-muted-foreground',
primary: 'bg-primary/10 text-primary',
success: 'bg-success/10 text-success',
warning: 'bg-warning/10 text-warning',
danger: 'bg-destructive/10 text-destructive',
info: 'bg-info/10 text-info',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface StatCardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof statCardVariants> {
/** Card title */
title: string;
/** Stat value to display */
value: number | string;
/** Optional icon component */
icon?: LucideIcon;
/** Optional trend direction */
trend?: 'up' | 'down' | 'neutral';
/** Optional trend value (e.g., "+12%") */
trendValue?: string;
/** Loading state */
isLoading?: boolean;
/** Optional description */
description?: string;
}
/**
* StatCard component for displaying dashboard metrics
*
* @example
* ```tsx
* <StatCard
* title="Total Sessions"
* value={42}
* icon={FolderIcon}
* variant="primary"
* trend="up"
* trendValue="+5"
* />
* ```
*/
export function StatCard({
className,
variant,
title,
value,
icon: Icon,
trend,
trendValue,
isLoading = false,
description,
...props
}: StatCardProps) {
const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;
const trendColor =
trend === 'up'
? 'text-success'
: trend === 'down'
? 'text-destructive'
: 'text-muted-foreground';
return (
<Card className={cn(statCardVariants({ variant }), className)} {...props}>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-muted-foreground truncate">
{title}
</p>
<div className="mt-2 flex items-baseline gap-2">
{isLoading ? (
<div className="h-8 w-16 animate-pulse rounded bg-muted" />
) : (
<p className="text-2xl font-semibold text-card-foreground">
{typeof value === 'number' ? value.toLocaleString() : value}
</p>
)}
{trend && trendValue && !isLoading && (
<span className={cn('flex items-center text-xs font-medium', trendColor)}>
<TrendIcon className="mr-0.5 h-3 w-3" />
{trendValue}
</span>
)}
</div>
{description && (
<p className="mt-1 text-xs text-muted-foreground truncate">
{description}
</p>
)}
</div>
{Icon && (
<div className={cn(iconContainerVariants({ variant }))}>
<Icon className="h-5 w-5" />
</div>
)}
</div>
</CardContent>
</Card>
);
}
/**
* Skeleton loader for StatCard
*/
export function StatCardSkeleton({ className }: { className?: string }) {
return (
<Card className={cn('animate-pulse', className)}>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="h-4 w-20 rounded bg-muted" />
<div className="mt-3 h-8 w-16 rounded bg-muted" />
</div>
<div className="h-10 w-10 rounded-lg bg-muted" />
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground",
secondary:
"border-transparent bg-secondary text-secondary-foreground",
destructive:
"border-transparent bg-destructive text-destructive-foreground",
outline:
"text-foreground",
success:
"border-transparent bg-success text-white",
warning:
"border-transparent bg-warning text-white",
info:
"border-transparent bg-info text-white",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-border bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground",
link:
"text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,85 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,119 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-card-foreground",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -0,0 +1,197 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@@ -0,0 +1,27 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, error, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
error && "border-destructive focus-visible:ring-destructive",
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,156 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-card text-card-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -0,0 +1,52 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,128 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border-border bg-card text-card-foreground",
success: "border-success bg-success text-white",
warning: "border-warning bg-warning text-white",
error: "border-destructive bg-destructive text-destructive-foreground",
info: "border-info bg-info text-white",
},
},
defaultVariants: {
variant: "default",
},
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.success]:border-white/20 group-[.success]:hover:bg-white/20 group-[.warning]:border-white/20 group-[.warning]:hover:bg-white/20 group-[.error]:border-white/20 group-[.error]:hover:bg-white/20",
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.success]:text-white/50 group-[.success]:hover:text-white group-[.warning]:text-white/50 group-[.warning]:hover:text-white group-[.error]:text-white/50 group-[.error]:hover:text-white",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -0,0 +1,87 @@
// UI Component Library - Barrel Export
// All components follow shadcn/ui patterns with Radix UI primitives and Tailwind CSS
// Button
export { Button, buttonVariants } from "./Button";
export type { ButtonProps } from "./Button";
// Input
export { Input } from "./Input";
export type { InputProps } from "./Input";
// Select (Radix)
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
} from "./Select";
// Dialog (Radix)
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
} from "./Dialog";
// Dropdown (Radix)
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
} from "./Dropdown";
// Tabs (Radix)
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
// Card
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from "./Card";
// Badge
export { Badge, badgeVariants } from "./Badge";
export type { BadgeProps } from "./Badge";
// Toast (Radix)
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
} from "./Toast";