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:
@@ -56,20 +56,14 @@ pub async fn install_skill(
|
||||
|
||||
if !skill.installed {
|
||||
let repo = SkillRepo {
|
||||
owner: skill
|
||||
.repo_owner
|
||||
.clone()
|
||||
.ok_or_else(|| {
|
||||
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(|| {
|
||||
name: skill.repo_name.clone().ok_or_else(|| {
|
||||
format_skill_error(
|
||||
"MISSING_REPO_INFO",
|
||||
&[("directory", &directory), ("field", "name")],
|
||||
|
||||
@@ -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}")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,8 +509,7 @@ 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" }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user