refactor(ui): simplify AppSwitcher styles and migrate to local SVG icons

- Replace complex gradient animations with clean, minimal tab design
- Migrate from @lobehub/icons CDN to local SVG assets for better reliability
- Fix clippy warning in error.rs (use inline format args)
- Improve code formatting in skill service and commands
- Reduce CSS complexity in AppSwitcher component (removed blur effects and gradients)
- Update BrandIcons to use imported local SVG files instead of dynamic image loading

This improves performance, reduces external dependencies, and provides a cleaner UI experience.
This commit is contained in:
YoVinchen
2025-11-22 01:20:21 +08:00
parent e7545f8cdf
commit de7f93d513
8 changed files with 64 additions and 123 deletions

View File

@@ -56,26 +56,20 @@ pub async fn install_skill(
if !skill.installed {
let repo = SkillRepo {
owner: skill
.repo_owner
.clone()
.ok_or_else(|| {
format_skill_error(
"MISSING_REPO_INFO",
&[("directory", &directory), ("field", "owner")],
None,
)
})?,
name: skill
.repo_name
.clone()
.ok_or_else(|| {
format_skill_error(
"MISSING_REPO_INFO",
&[("directory", &directory), ("field", "name")],
None,
)
})?,
owner: skill.repo_owner.clone().ok_or_else(|| {
format_skill_error(
"MISSING_REPO_INFO",
&[("directory", &directory), ("field", "owner")],
None,
)
})?,
name: skill.repo_name.clone().ok_or_else(|| {
format_skill_error(
"MISSING_REPO_INFO",
&[("directory", &directory), ("field", "name")],
None,
)
})?,
branch: skill
.repo_branch
.clone()

View File

@@ -116,6 +116,6 @@ pub fn format_skill_error(
serde_json::to_string(&error_obj).unwrap_or_else(|_| {
// 如果 JSON 序列化失败,返回简单格式
format!("ERROR:{}", code)
format!("ERROR:{code}")
})
}

View File

@@ -490,7 +490,9 @@ impl SkillService {
// 根据 skills_path 确定源目录路径
let source = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skills_path源路径为: temp_dir/skills_path/directory
temp_dir.join(skills_path.trim_matches('/')).join(&directory)
temp_dir
.join(skills_path.trim_matches('/'))
.join(&directory)
} else {
// 否则源路径为: temp_dir/directory
temp_dir.join(&directory)

View File

@@ -58,7 +58,7 @@ function App() {
const mcpPanelRef = useRef<any>(null);
const skillsPageRef = useRef<any>(null);
const addActionButtonClass =
"bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg shadow-primary/20 rounded-full w-8 h-8";
"bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8";
const { data, isLoading, refetch } = useProvidersQuery(activeApp);
const providers = useMemo(() => data?.providers ?? {}, [data]);
@@ -281,7 +281,7 @@ function App() {
return <AgentsPanel onOpenChange={() => setCurrentView("providers")} />;
default:
return (
<div className="mx-auto max-w-5xl space-y-4">
<div className="mx-auto max-w-4xl space-y-4">
<ProviderList
providers={providers}
currentProviderId={currentProviderId}
@@ -509,9 +509,8 @@ function App() {
</header>
<main
className={`flex-1 overflow-y-auto pb-12 px-6 animate-fade-in scroll-overlay ${
currentView === "providers" ? "pt-24" : "pt-20"
}`}
className={`flex-1 overflow-y-auto pb-12 px-6 animate-fade-in scroll-overlay ${currentView === "providers" ? "pt-24" : "pt-20"
}`}
style={{ overflowX: "hidden" }}
>
{renderContent()}
@@ -554,8 +553,8 @@ function App() {
message={
confirmDelete
? t("confirm.deleteProviderMessage", {
name: confirmDelete.name,
})
name: confirmDelete.name,
})
: ""
}
onConfirm={() => void handleConfirmDelete()}

View File

@@ -13,28 +13,22 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
};
return (
<div className="glass p-1.5 rounded-full flex items-center gap-1.5">
<div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 gap-1">
<button
type="button"
onClick={() => handleSwitch("claude")}
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${
className={`group inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "claude"
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(20,184,166,0.6)] ring-1 ring-white/10"
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100"
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
}`}
>
{activeApp === "claude" && (
<div className="absolute inset-0 bg-gradient-to-r from-teal-500 via-emerald-500 to-green-600 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
)}
{activeApp !== "claude" && (
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
)}
<ClaudeIcon
size={16}
className={
activeApp === "claude"
? "text-white"
: "text-muted-foreground group-hover:text-teal-500 transition-colors"
? "text-foreground"
: "text-gray-500 dark:text-gray-400 group-hover:text-foreground transition-colors"
}
/>
<span>Claude</span>
@@ -43,24 +37,18 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
<button
type="button"
onClick={() => handleSwitch("codex")}
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "codex"
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(59,130,246,0.8)] ring-1 ring-white/10"
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100"
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
}`}
>
{activeApp === "codex" && (
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 via-sky-500 to-cyan-500 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
)}
{activeApp !== "codex" && (
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
)}
<CodexIcon
size={16}
className={
activeApp === "codex"
? "text-white"
: "text-muted-foreground group-hover:text-blue-500 transition-colors"
? "text-foreground"
: "text-gray-500 dark:text-gray-400 group-hover:text-foreground transition-colors"
}
/>
<span>Codex</span>
@@ -69,24 +57,18 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
<button
type="button"
onClick={() => handleSwitch("gemini")}
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeApp === "gemini"
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(99,102,241,0.8)] ring-1 ring-white/10"
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100"
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
}`}
>
{activeApp === "gemini" && (
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500 via-violet-500 to-purple-600 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
)}
{activeApp !== "gemini" && (
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
)}
<GeminiIcon
size={16}
className={
activeApp === "gemini"
? "text-white"
: "text-muted-foreground group-hover:text-indigo-500 transition-colors"
? "text-foreground"
: "text-gray-500 dark:text-gray-400 group-hover:text-foreground transition-colors"
}
/>
<span>Gemini</span>

View File

@@ -1,84 +1,48 @@
import { useEffect, useState } from "react";
interface IconProps {
size?: number;
className?: string;
}
const LOBE_ICONS_VERSION = "latest"; // pin if needed, e.g. "1.4.0"
const LOBE_BASE = `https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@${LOBE_ICONS_VERSION}/icons`;
function IconImage({
urls,
alt,
size,
className,
}: {
urls: string[];
alt: string;
size: number;
className?: string;
}) {
const [index, setIndex] = useState(0);
useEffect(() => {
setIndex(0);
}, [urls.join("|")]);
const src = urls[index] ?? urls[urls.length - 1];
return (
<img
src={src}
width={size}
height={size}
className={className}
alt={alt}
loading="lazy"
referrerPolicy="no-referrer"
onError={() => {
if (index < urls.length - 1) {
setIndex((i) => i + 1);
}
}}
/>
);
}
// 导入本地 SVG 图标
import ClaudeSvg from "@/icons/extracted/claude.svg?url";
import OpenAISvg from "@/icons/extracted/openai.svg?url";
import GeminiSvg from "@/icons/extracted/gemini.svg?url";
export function ClaudeIcon({ size = 16, className = "" }: IconProps) {
return (
<IconImage
urls={[`${LOBE_BASE}/claude-color.svg`, `${LOBE_BASE}/claude.svg`]}
size={size}
<img
src={ClaudeSvg}
width={size}
height={size}
className={className}
alt="Claude"
loading="lazy"
/>
);
}
export function CodexIcon({ size = 16, className = "" }: IconProps) {
return (
<IconImage
urls={[
`${LOBE_BASE}/openai-color.svg`,
`${LOBE_BASE}/chatgpt-color.svg`,
`${LOBE_BASE}/openai.svg`,
`${LOBE_BASE}/chatgpt.svg`,
]}
size={size}
className={className}
<img
src={OpenAISvg}
width={size}
height={size}
className={`dark:brightness-0 dark:invert ${className}`}
alt="Codex"
loading="lazy"
/>
);
}
export function GeminiIcon({ size = 16, className = "" }: IconProps) {
return (
<IconImage
urls={[`${LOBE_BASE}/gemini-color.svg`, `${LOBE_BASE}/gemini.svg`]}
size={size}
<img
src={GeminiSvg}
width={size}
height={size}
className={className}
alt="Gemini"
loading="lazy"
/>
);
}

View File

@@ -120,7 +120,7 @@ export function ProviderCard({
? "border-primary/50 bg-primary/5 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
: "hover:scale-[1.01]",
dragHandleProps?.isDragging &&
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
)}
>
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
@@ -141,7 +141,7 @@ export function ProviderCard({
</button>
{/* 供应商图标 */}
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center border border-white/10 group-hover:scale-105 transition-transform duration-300">
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center border border-gray-200 dark:border-white/10 group-hover:scale-105 transition-transform duration-300">
<ProviderIcon
icon={provider.icon}
name={provider.name}
@@ -196,7 +196,7 @@ export function ProviderCard({
</div>
<div className="relative flex items-center ml-auto">
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[14rem] group-focus-within:-translate-x-[14rem] sm:group-hover:-translate-x-[16rem] sm:group-focus-within:-translate-x-[16rem]">
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[11rem] group-focus-within:-translate-x-[11rem] sm:group-hover:-translate-x-[13rem] sm:group-focus-within:-translate-x-[13rem]">
<UsageFooter
provider={provider}
providerId={provider.id}

View File

@@ -70,7 +70,7 @@ function getSuggestionI18nKey(suggestion: string): string {
export function formatSkillError(
errorString: string,
t: TFunction,
defaultTitle: string = "skills.installFailed"
defaultTitle: string = "skills.installFailed",
): { title: string; description: string } {
const parsedError = parseSkillError(errorString);