mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 11:01:36 +08:00
f5f4281d06
The toolbar app switcher used a ResizeObserver-based overflow detection (useAutoCompact) to collapse app labels when space ran out. With the number of managed apps growing, the labels are collapsed in practice anyway, so remove the mechanism and render icons only. Buttons now carry title/aria-label so app names remain discoverable via tooltip and accessible to screen readers.
124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
import type { AppId } from "@/lib/api";
|
|
import type { VisibleApps } from "@/types";
|
|
import { ProviderIcon } from "@/components/ProviderIcon";
|
|
import { cn } from "@/lib/utils";
|
|
import { Monitor, Terminal } from "lucide-react";
|
|
|
|
const APP_BADGE_ICON: Partial<
|
|
Record<AppId, { icon: typeof Terminal; offsetY?: number }>
|
|
> = {
|
|
claude: { icon: Terminal },
|
|
"claude-desktop": { icon: Monitor, offsetY: 0.5 },
|
|
};
|
|
|
|
interface AppSwitcherProps {
|
|
activeApp: AppId;
|
|
onSwitch: (app: AppId) => void;
|
|
visibleApps?: VisibleApps;
|
|
}
|
|
|
|
const ALL_APPS: AppId[] = [
|
|
"claude",
|
|
"claude-desktop",
|
|
"codex",
|
|
"gemini",
|
|
"grokbuild",
|
|
"opencode",
|
|
"openclaw",
|
|
"hermes",
|
|
];
|
|
const STORAGE_KEY = "cc-switch-last-app";
|
|
|
|
export function AppSwitcher({
|
|
activeApp,
|
|
onSwitch,
|
|
visibleApps,
|
|
}: AppSwitcherProps) {
|
|
const handleSwitch = (app: AppId) => {
|
|
if (app === activeApp) return;
|
|
localStorage.setItem(STORAGE_KEY, app);
|
|
onSwitch(app);
|
|
};
|
|
const iconSize = 20;
|
|
const appIconName: Record<AppId, string> = {
|
|
claude: "claude",
|
|
"claude-desktop": "claude",
|
|
codex: "openai",
|
|
gemini: "gemini",
|
|
grokbuild: "grok",
|
|
opencode: "opencode",
|
|
openclaw: "openclaw",
|
|
hermes: "hermes",
|
|
};
|
|
const appDisplayName: Record<AppId, string> = {
|
|
claude: "Claude Code",
|
|
"claude-desktop": "Claude Desktop",
|
|
codex: "Codex",
|
|
gemini: "Gemini",
|
|
grokbuild: "Grok Build",
|
|
opencode: "OpenCode",
|
|
openclaw: "OpenClaw",
|
|
hermes: "Hermes",
|
|
};
|
|
|
|
// Filter apps based on visibility settings (default all visible)
|
|
const appsToShow = ALL_APPS.filter((app) => {
|
|
if (!visibleApps) return true;
|
|
return visibleApps[app];
|
|
});
|
|
|
|
return (
|
|
<div className="inline-flex bg-muted rounded-xl p-1 gap-1">
|
|
{appsToShow.map((app) => {
|
|
const badgeConfig = APP_BADGE_ICON[app];
|
|
const BadgeIcon = badgeConfig?.icon;
|
|
const isActive = activeApp === app;
|
|
return (
|
|
<button
|
|
key={app}
|
|
type="button"
|
|
onClick={() => handleSwitch(app)}
|
|
title={appDisplayName[app]}
|
|
aria-label={appDisplayName[app]}
|
|
className={cn(
|
|
"group inline-flex items-center px-3 h-8 rounded-md text-sm font-medium transition-all duration-200",
|
|
isActive
|
|
? "bg-background text-foreground shadow-sm"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-background/50",
|
|
)}
|
|
>
|
|
<span className="relative inline-flex shrink-0">
|
|
<ProviderIcon
|
|
icon={appIconName[app]}
|
|
name={appDisplayName[app]}
|
|
size={iconSize}
|
|
/>
|
|
{BadgeIcon && (
|
|
<span
|
|
className={cn(
|
|
"absolute -bottom-0.5 -right-0.5 flex items-center justify-center rounded-[3px] border h-[11px] w-[11px]",
|
|
isActive
|
|
? "bg-background border-border text-foreground"
|
|
: "bg-muted border-background text-muted-foreground group-hover:bg-background group-hover:text-foreground",
|
|
)}
|
|
aria-hidden="true"
|
|
>
|
|
<BadgeIcon
|
|
className="h-[8px] w-[8px]"
|
|
strokeWidth={2.5}
|
|
style={
|
|
badgeConfig?.offsetY
|
|
? { transform: `translateY(${badgeConfig.offsetY}px)` }
|
|
: undefined
|
|
}
|
|
/>
|
|
</span>
|
|
)}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|