mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
258 lines
7.4 KiB
TypeScript
258 lines
7.4 KiB
TypeScript
// ========================================
|
|
// 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;
|