mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +08:00
127fa5bf9d
- Standardize container max-width across all panels: * Replace max-w-4xl and max-w-5xl with unified max-w-[60rem] * Apply to SettingsPage, UnifiedMcpPanel, SkillsPage, and FullScreenPanel * Ensures consistent reading width and visual balance on wide screens - Optimize padding hierarchy and structure: * Move px-6 from parent elements to content containers * FullScreenPanel: Add max-w-[60rem] wrapper to header, content, and footer * Add border separators (border-t/border-b) to header and footer sections * Consolidate nested padding in MCP, Skills, and Prompts panels * Remove redundant padding layers for cleaner CSS - Simplify component styling: * MCP list items: Replace card-based layout with modern group-based design * Remove unnecessary wrapper divs and flatten DOM structure * Update card hover effects with smooth transitions * Simplify icon selection dialog (remove description text in BasicFormFields) - Benefits: * Consistent 60rem max-width provides optimal readability * Unified spacing rules reduce maintenance complexity * Cleaner component hierarchy improves performance * Better responsive behavior across different screen sizes * More cohesive visual design language throughout the app This creates a maintainable, scalable design system foundation.
220 lines
7.1 KiB
TypeScript
220 lines
7.1 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Trash2, ExternalLink, Plus } from "lucide-react";
|
|
import { settingsApi } from "@/lib/api";
|
|
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
|
import type { Skill, SkillRepo } from "@/lib/api/skills";
|
|
|
|
interface RepoManagerPanelProps {
|
|
repos: SkillRepo[];
|
|
skills: Skill[];
|
|
onAdd: (repo: SkillRepo) => Promise<void>;
|
|
onRemove: (owner: string, name: string) => Promise<void>;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function RepoManagerPanel({
|
|
repos,
|
|
skills,
|
|
onAdd,
|
|
onRemove,
|
|
onClose,
|
|
}: RepoManagerPanelProps) {
|
|
const { t } = useTranslation();
|
|
const [repoUrl, setRepoUrl] = useState("");
|
|
const [branch, setBranch] = useState("");
|
|
const [skillsPath, setSkillsPath] = useState("");
|
|
const [error, setError] = useState("");
|
|
|
|
const getSkillCount = (repo: SkillRepo) =>
|
|
skills.filter(
|
|
(skill) =>
|
|
skill.repoOwner === repo.owner &&
|
|
skill.repoName === repo.name &&
|
|
(skill.repoBranch || "main") === (repo.branch || "main"),
|
|
).length;
|
|
|
|
const parseRepoUrl = (
|
|
url: string,
|
|
): { owner: string; name: string } | null => {
|
|
let cleaned = url.trim();
|
|
cleaned = cleaned.replace(/^https?:\/\/github\.com\//, "");
|
|
cleaned = cleaned.replace(/\.git$/, "");
|
|
|
|
const parts = cleaned.split("/");
|
|
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
return { owner: parts[0], name: parts[1] };
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const handleAdd = async () => {
|
|
setError("");
|
|
|
|
const parsed = parseRepoUrl(repoUrl);
|
|
if (!parsed) {
|
|
setError(t("skills.repo.invalidUrl"));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await onAdd({
|
|
owner: parsed.owner,
|
|
name: parsed.name,
|
|
branch: branch || "main",
|
|
enabled: true,
|
|
skillsPath: skillsPath.trim() || undefined,
|
|
});
|
|
|
|
setRepoUrl("");
|
|
setBranch("");
|
|
setSkillsPath("");
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
|
|
}
|
|
};
|
|
|
|
const handleOpenRepo = async (owner: string, name: string) => {
|
|
try {
|
|
await settingsApi.openExternal(`https://github.com/${owner}/${name}`);
|
|
} catch (error) {
|
|
console.error("Failed to open URL:", error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<FullScreenPanel
|
|
isOpen={true}
|
|
title={t("skills.repo.title")}
|
|
onClose={onClose}
|
|
>
|
|
{/* 添加仓库表单 */}
|
|
<div className="space-y-4 glass rounded-xl p-6 border border-white/10">
|
|
<h3 className="text-base font-semibold text-foreground">
|
|
添加技能仓库
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="repo-url" className="text-foreground">
|
|
{t("skills.repo.url")}
|
|
</Label>
|
|
<Input
|
|
id="repo-url"
|
|
placeholder={t("skills.repo.urlPlaceholder")}
|
|
value={repoUrl}
|
|
onChange={(e) => setRepoUrl(e.target.value)}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="branch" className="text-foreground">
|
|
{t("skills.repo.branch")}
|
|
</Label>
|
|
<Input
|
|
id="branch"
|
|
placeholder={t("skills.repo.branchPlaceholder")}
|
|
value={branch}
|
|
onChange={(e) => setBranch(e.target.value)}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="skills-path" className="text-foreground">
|
|
{t("skills.repo.path")}
|
|
</Label>
|
|
<Input
|
|
id="skills-path"
|
|
placeholder={t("skills.repo.pathPlaceholder")}
|
|
value={skillsPath}
|
|
onChange={(e) => setSkillsPath(e.target.value)}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{error && (
|
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
)}
|
|
<Button
|
|
onClick={handleAdd}
|
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
|
type="button"
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
{t("skills.repo.add")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 仓库列表 */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-base font-semibold text-foreground">
|
|
{t("skills.repo.list")}
|
|
</h3>
|
|
{repos.length === 0 ? (
|
|
<div className="text-center py-12 glass rounded-xl border border-white/10">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("skills.repo.empty")}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{repos.map((repo) => (
|
|
<div
|
|
key={`${repo.owner}/${repo.name}`}
|
|
className="flex items-center justify-between rounded-xl border border-white/10 bg-gray-900/40 px-4 py-3"
|
|
>
|
|
<div>
|
|
<div className="text-sm font-medium text-foreground">
|
|
{repo.owner}/{repo.name}
|
|
</div>
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
{t("skills.repo.branch")}: {repo.branch || "main"}
|
|
{repo.skillsPath && (
|
|
<>
|
|
<span className="mx-2">•</span>
|
|
{t("skills.repo.path")}: {repo.skillsPath}
|
|
</>
|
|
)}
|
|
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
|
|
{t("skills.repo.skillCount", {
|
|
count: getSkillCount(repo),
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
onClick={() => handleOpenRepo(repo.owner, repo.name)}
|
|
title={t("common.view", { defaultValue: "查看" })}
|
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
onClick={() => onRemove(repo.owner, repo.name)}
|
|
title={t("common.delete")}
|
|
className="hover:text-red-500 hover:bg-red-100 dark:hover:text-red-400 dark:hover:bg-red-500/10"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</FullScreenPanel>
|
|
);
|
|
}
|