import { Check, CircleAlert, CircleCheck, CircleX, ExternalLink, X, Zap } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import type { Skill, SkillFormat, SkillStatus } from '@/types'
const FORMAT_META: Record<
SkillFormat,
{ label: string; variant: 'default' | 'secondary' | 'outline'; description: string }
> = {
oxios: { label: 'Oxios', variant: 'default', description: 'Oxios native skill' },
openclaw: { label: 'OpenClaw', variant: 'secondary', description: 'ClawHub marketplace skill' },
claude_code: { label: 'Claude', variant: 'outline', description: 'Claude Code skill' },
agent_skills: { label: 'Standard', variant: 'outline', description: 'Agent Skills standard' },
}
const STATUS_DISPLAY: Record<
SkillStatus,
{ icon: React.ReactNode; label: string; variant: 'success' | 'warning' | 'destructive' }
> = {
ready: { icon: <CircleCheck className="h-3 w-3" />, label: 'ready', variant: 'success' },
needs_setup: {
icon: <CircleAlert className="h-3 w-3" />,
label: 'needs-setup',
variant: 'warning',
},
disabled: { icon: <CircleX className="h-3 w-3" />, label: 'disabled', variant: 'destructive' },
}
// ─── Skill Detail side panel ────────────────────────────────
export function SkillDetail({ skill, onClose }: { skill: Skill; onClose: () => void }) {
const { t } = useTranslation()
const sd = STATUS_DISPLAY[skill.status]
const fm = FORMAT_META[skill.format]
const hasMissing =
skill.missing.bins.length > 0 ||
skill.missing.anyBins.length > 0 ||
skill.missing.env.length > 0 ||
skill.missing.config.length > 0
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<Zap className="h-5 w-5 shrink-0" />
<div className="min-w-0">
<h2 className="font-semibold text-lg leading-tight truncate">{skill.name}</h2>
<Badge variant={fm.variant} className="text-xs mt-1">
{fm.label}
</Badge>
</div>
</div>
<Button variant="ghost" size="icon" className="shrink-0 h-7 w-7" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
{/* Status */}
<div className="flex items-center gap-2">
<Badge variant={sd.variant} className="text-xs gap-1">
sd.icon {sd.label}
</Badge>
{skill.version && (
<span className="text-xs font-mono text-muted-foreground">v{skill.version}</span>
)}
<Badge variant="outline" className="text-xs">
{skill.source}
</Badge>
</div>
{/* Description */}
{skill.description && <p className="text-sm text-muted-foreground">{skill.description}</p>}
{skill.author && (
<p className="text-xs text-muted-foreground">
{t('skills.by')} {skill.author}
</p>
)}
<Separator />
{/* Requirements */}
{(skill.requirements.bins.length > 0 ||
skill.requirements.anyBins.length > 0 ||
skill.requirements.env.length > 0 ||
skill.requirements.config.length > 0) && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{t('skills.requires')}
</p>
<div className="space-y-1.5 pl-1">
{skill.requirements.bins.length > 0 && (
<ReqList items={skill.requirements.bins} missing={skill.missing.bins} />
)}
{skill.requirements.anyBins.length > 0 && (
<ReqList items={skill.requirements.anyBins} missing={skill.missing.anyBins} />
)}
{skill.requirements.env.length > 0 && (
<ReqList items={skill.requirements.env} missing={skill.missing.env} />
)}
{skill.requirements.config.length > 0 && (
<ReqList items={skill.requirements.config} missing={skill.missing.config} />
)}
</div>
</div>
)}
{/* Missing warning */}
{hasMissing && skill.status === 'needs_setup' && (
<div className="rounded-md bg-warning/10 border border-warning/20 px-3 py-2">
<p className="text-xs text-warning">
{t('skills.missingWarning', {
missing: [
...skill.missing.bins.map((b) => `bin:${b}`),
...skill.missing.env.map((e) => `env:${e}`),
...skill.missing.config.map((c) => `config:${c}`),
...skill.missing.anyBins.map((b) => `any_bin:${b}`),
].join(', '),
})}
</p>
</div>
)}
{/* Install specs */}
{skill.install.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{t('skills.install')}
</p>
<div className="space-y-1 pl-1">
{skill.install.map((sp, i) => (
<div
key={`${sp.kind}-${i}`}
className="flex items-center gap-2 text-sm text-muted-foreground"
>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">{sp.kind}</span>
<span>{sp.label ?? sp.bins.join(', ')}</span>
</div>
))}
</div>
</div>
)}
{/* Config checks */}
{skill.config_checks.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Config
</p>
<div className="space-y-1 pl-1">
{skill.config_checks.map((cc, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
{cc.satisfied ? (
<Check className="h-3 w-3 text-success" />
) : (
<X className="h-3 w-3 text-error" />
)}
<span className={cn('font-mono', !cc.satisfied && 'text-error')}>{cc.path}</span>
</div>
))}
</div>
</div>
)}
{/* OS / Flags */}
<div className="flex flex-wrap gap-1.5 text-xs">
{skill.os.length > 0 &&
skill.os.map((o) => (
<Badge key={o} variant="outline" className="text-xs">
{o}
</Badge>
))}
{skill.always && (
<Badge variant="secondary" className="text-xs">
{t('skills.always')}
</Badge>
)}
{skill.bundled && (
<Badge variant="outline" className="text-xs">
bundled
</Badge>
)}
</div>
{/* Homepage link */}
{skill.homepage && (
<a
href={skill.homepage}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
>
<ExternalLink className="h-3 w-3" /> {skill.homepage}
</a>
)}
{/* File path */}
<p className="text-xs text-muted-foreground/60 font-mono truncate" title={skill.file_path}>
{skill.file_path}
</p>
</div>
)
}
// ─── Helpers ─────────────────────────────────────────────────
function ReqList({ items, missing }: { items: string[]; missing: string[] }) {
return (
<div className="flex flex-wrap gap-x-3 gap-y-0.5">
{items.map((item) => {
const m = missing.includes(item)
return (
<span
key={item}
className={cn('flex items-center gap-1 text-xs', m ? 'text-error' : 'text-success')}
>
{m ? <X className="h-3 w-3" /> : <Check className="h-3 w-3" />}
{item}
</span>
)
})}
</div>
)
}