Files
AgentCompany/src/renderer/components/skills/SkillList.tsx
T
ZacharyZhang-NY 75022f82c5 refactor(renderer): route all Phosphor icon imports through ui/icons barrel
Create src/renderer/components/ui/icons.ts as the single re-export point
for every Phosphor icon the renderer touches, then rewrite the 95 files
that previously imported directly from "@phosphor-icons/react" to pull
from the relative-path-to-barrel instead. The TitleBar commit earlier
in this series already used ui/icons; this commit extends the pattern
to the rest of the renderer.

Benefits now wired in:
  - one location where bundle tree-shaking can be reasoned about,
  - typos like Briefcase vs BriefCase show up against a finite list,
  - re-theming or swapping icon sets is a one-file edit,
  - a lint rule that flags raw @phosphor-icons/react imports can be
    added later without mass-rewriting call sites.

The barrel also re-exports the `Icon` type so call sites that type
icon props against the Phosphor component type work unchanged.

Test count stays at 363 passing; typecheck stays at 156 errors.
2026-04-16 23:37:35 -04:00

136 lines
4.5 KiB
TypeScript

import { useState, useCallback } from "react";
import {
CaretDown,
CaretRight,
FolderSimple,
GithubLogo,
Globe,
Link,
PuzzlePiece,
} from "../ui/icons";
import type { SkillRecord } from "@shared/types";
import { SkillFileTree, buildTree } from "./SkillFileTree";
const sourceIconMap: Record<string, typeof GithubLogo> = {
github: GithubLogo,
local_path: FolderSimple,
url: Link,
skills_sh: Globe,
managed: PuzzlePiece,
};
export function SkillList({
skills,
selectedSkillId,
skillFilter,
onSelectSkill,
}: {
skills: SkillRecord[];
selectedSkillId: string | null;
skillFilter: string;
onSelectSkill: (id: string) => void;
}) {
const [expandedSkillId, setExpandedSkillId] = useState<string | null>(null);
const [expandedDirs, setExpandedDirs] = useState<Record<string, Set<string>>>({});
const [selectedPaths, setSelectedPaths] = useState<Record<string, string>>({});
const filteredSkills = skills.filter((skill) => {
const haystack = `${skill.name} ${skill.key} ${skill.slug}`.toLowerCase();
return haystack.includes(skillFilter.toLowerCase());
});
const handleToggleSkill = useCallback((skillId: string) => {
setExpandedSkillId((prev) => (prev === skillId ? null : skillId));
}, []);
const handleToggleDir = useCallback((skillId: string, path: string) => {
setExpandedDirs((prev) => {
const current = prev[skillId] ?? new Set<string>();
const next = new Set(current);
if (next.has(path)) {
next.delete(path);
} else {
next.add(path);
}
return { ...prev, [skillId]: next };
});
}, []);
const handleSelectPath = useCallback((skillId: string, path: string) => {
setSelectedPaths((prev) => ({ ...prev, [skillId]: path }));
}, []);
if (filteredSkills.length === 0) {
return (
<div className="px-4 py-6 text-[13px] text-muted-foreground">
No skills match this filter.
</div>
);
}
return (
<div>
{filteredSkills.map((skill) => {
const isExpanded = expandedSkillId === skill.id;
const isSelected = skill.id === selectedSkillId;
const tree = buildTree(skill.fileInventory);
const SourceIcon = sourceIconMap[skill.sourceType] ?? PuzzlePiece;
const dirState = expandedDirs[skill.id] ?? new Set<string>();
const filePath = selectedPaths[skill.id] ?? "SKILL.md";
return (
<div key={skill.id} className="border-b border-border">
<div
className={`group flex items-center gap-2 px-3 py-1.5 hover:bg-muted ${
isSelected ? "text-foreground" : "text-muted-foreground"
}`}
>
<SourceIcon
size={14}
className="shrink-0 opacity-75 transition-opacity group-hover:opacity-100"
aria-hidden="true"
/>
<button
type="button"
className="min-w-0 flex-1 text-left"
onClick={() => onSelectSkill(skill.id)}
>
<span className="text-[13px] font-medium leading-5 [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:3]">
{skill.name}
</span>
</button>
<button
type="button"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[4px] text-muted-foreground opacity-60 transition-opacity hover:bg-muted hover:opacity-100"
onClick={() => handleToggleSkill(skill.id)}
aria-label={isExpanded ? "Collapse file tree" : "Expand file tree"}
>
{isExpanded ? <CaretDown size={12} /> : <CaretRight size={12} />}
</button>
</div>
<div
className="overflow-hidden transition-[grid-template-rows] duration-200"
style={{
display: "grid",
gridTemplateRows: isExpanded ? "1fr" : "0fr",
}}
>
<div className="min-h-0">
{tree.length > 0 && (
<SkillFileTree
nodes={tree}
selectedPath={filePath}
expandedDirs={dirState}
onToggleDir={(path) => handleToggleDir(skill.id, path)}
onSelectPath={(path) => handleSelectPath(skill.id, path)}
/>
)}
</div>
</div>
</div>
);
})}
</div>
);
}