mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 19:45:34 +08:00
feat(toolbar): auto-compact AppSwitcher based on available width
Replace hardcoded app count threshold with ResizeObserver-based detection. Uses a two-layer layout (overflow-hidden outer + shrink-0 inner) to avoid the compact/normal oscillation problem.
This commit is contained in:
+240
-234
@@ -35,6 +35,7 @@ import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env";
|
|||||||
import { useProviderActions } from "@/hooks/useProviderActions";
|
import { useProviderActions } from "@/hooks/useProviderActions";
|
||||||
import { openclawKeys } from "@/hooks/useOpenClaw";
|
import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||||
|
import { useAutoCompact } from "@/hooks/useAutoCompact";
|
||||||
import { useLastValidValue } from "@/hooks/useLastValidValue";
|
import { useLastValidValue } from "@/hooks/useLastValidValue";
|
||||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||||
import { isTextEditableTarget } from "@/utils/domUtils";
|
import { isTextEditableTarget } from "@/utils/domUtils";
|
||||||
@@ -200,6 +201,9 @@ function App() {
|
|||||||
const effectiveEditingProvider = useLastValidValue(editingProvider);
|
const effectiveEditingProvider = useLastValidValue(editingProvider);
|
||||||
const effectiveUsageProvider = useLastValidValue(usageProvider);
|
const effectiveUsageProvider = useLastValidValue(usageProvider);
|
||||||
|
|
||||||
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isToolbarCompact = useAutoCompact(toolbarRef);
|
||||||
|
|
||||||
const promptPanelRef = useRef<any>(null);
|
const promptPanelRef = useRef<any>(null);
|
||||||
const mcpPanelRef = useRef<any>(null);
|
const mcpPanelRef = useRef<any>(null);
|
||||||
const skillsPageRef = useRef<any>(null);
|
const skillsPageRef = useRef<any>(null);
|
||||||
@@ -961,250 +965,252 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1.5 h-[32px]"
|
ref={toolbarRef}
|
||||||
style={{ WebkitAppRegion: "no-drag" } as any}
|
className="flex flex-1 min-w-0 overflow-hidden justify-end items-center h-[32px]"
|
||||||
>
|
>
|
||||||
{currentView === "prompts" && (
|
<div
|
||||||
<Button
|
className="flex shrink-0 items-center gap-1.5 h-full"
|
||||||
variant="ghost"
|
style={{ WebkitAppRegion: "no-drag" } as any}
|
||||||
size="sm"
|
>
|
||||||
onClick={() => promptPanelRef.current?.openAdd()}
|
{currentView === "prompts" && (
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
{t("prompts.add")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{currentView === "mcp" && (
|
|
||||||
<>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => mcpPanelRef.current?.openImport()}
|
onClick={() => promptPanelRef.current?.openAdd()}
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
{t("mcp.importExisting")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => mcpPanelRef.current?.openAdd()}
|
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
{t("mcp.addMcp")}
|
{t("prompts.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
)}
|
||||||
)}
|
{currentView === "mcp" && (
|
||||||
{currentView === "skills" && (
|
<>
|
||||||
<>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => mcpPanelRef.current?.openImport()}
|
||||||
onClick={() =>
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
unifiedSkillsPanelRef.current?.openInstallFromZip()
|
>
|
||||||
}
|
<Download className="w-4 h-4 mr-2" />
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
{t("mcp.importExisting")}
|
||||||
>
|
</Button>
|
||||||
<FolderArchive className="w-4 h-4 mr-2" />
|
<Button
|
||||||
{t("skills.installFromZip.button")}
|
variant="ghost"
|
||||||
</Button>
|
size="sm"
|
||||||
<Button
|
onClick={() => mcpPanelRef.current?.openAdd()}
|
||||||
variant="ghost"
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
size="sm"
|
>
|
||||||
onClick={() => unifiedSkillsPanelRef.current?.openImport()}
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
{t("mcp.addMcp")}
|
||||||
>
|
</Button>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
</>
|
||||||
{t("skills.import")}
|
)}
|
||||||
</Button>
|
{currentView === "skills" && (
|
||||||
<Button
|
<>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={() => setCurrentView("skillsDiscovery")}
|
size="sm"
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
onClick={() =>
|
||||||
>
|
unifiedSkillsPanelRef.current?.openInstallFromZip()
|
||||||
<Search className="w-4 h-4 mr-2" />
|
}
|
||||||
{t("skills.discover")}
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
</Button>
|
>
|
||||||
</>
|
<FolderArchive className="w-4 h-4 mr-2" />
|
||||||
)}
|
{t("skills.installFromZip.button")}
|
||||||
{currentView === "skillsDiscovery" && (
|
</Button>
|
||||||
<>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => unifiedSkillsPanelRef.current?.openImport()}
|
||||||
onClick={() => skillsPageRef.current?.refresh()}
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
>
|
||||||
>
|
<Download className="w-4 h-4 mr-2" />
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
{t("skills.import")}
|
||||||
{t("skills.refresh")}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => setCurrentView("skillsDiscovery")}
|
||||||
onClick={() => skillsPageRef.current?.openRepoManager()}
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
>
|
||||||
>
|
<Search className="w-4 h-4 mr-2" />
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
{t("skills.discover")}
|
||||||
{t("skills.repoManager")}
|
</Button>
|
||||||
</Button>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
{currentView === "skillsDiscovery" && (
|
||||||
{currentView === "providers" && (
|
<>
|
||||||
<>
|
<Button
|
||||||
{activeApp !== "opencode" &&
|
variant="ghost"
|
||||||
activeApp !== "openclaw" &&
|
size="sm"
|
||||||
settingsData?.enableLocalProxy && (
|
onClick={() => skillsPageRef.current?.refresh()}
|
||||||
<>
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
<ProxyToggle activeApp={activeApp} />
|
>
|
||||||
<div
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
className={cn(
|
{t("skills.refresh")}
|
||||||
"transition-all duration-300 ease-in-out overflow-hidden",
|
</Button>
|
||||||
isCurrentAppTakeoverActive
|
<Button
|
||||||
? "opacity-100 max-w-[100px] scale-100"
|
variant="ghost"
|
||||||
: "opacity-0 max-w-0 scale-75 pointer-events-none",
|
size="sm"
|
||||||
)}
|
onClick={() => skillsPageRef.current?.openRepoManager()}
|
||||||
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
{t("skills.repoManager")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{currentView === "providers" && (
|
||||||
|
<>
|
||||||
|
{activeApp !== "opencode" &&
|
||||||
|
activeApp !== "openclaw" &&
|
||||||
|
settingsData?.enableLocalProxy && (
|
||||||
|
<>
|
||||||
|
<ProxyToggle activeApp={activeApp} />
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-300 ease-in-out overflow-hidden",
|
||||||
|
isCurrentAppTakeoverActive
|
||||||
|
? "opacity-100 max-w-[100px] scale-100"
|
||||||
|
: "opacity-0 max-w-0 scale-75 pointer-events-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FailoverToggle activeApp={activeApp} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AppSwitcher
|
||||||
|
activeApp={activeApp}
|
||||||
|
onSwitch={setActiveApp}
|
||||||
|
visibleApps={visibleApps}
|
||||||
|
compact={isToolbarCompact}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 p-1 bg-muted rounded-xl">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeApp === "openclaw" ? "openclaw" : "default"}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
>
|
>
|
||||||
<FailoverToggle activeApp={activeApp} />
|
{activeApp === "openclaw" ? (
|
||||||
</div>
|
<>
|
||||||
</>
|
<Button
|
||||||
)}
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("workspace")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("workspace.manage")}
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("openclawEnv")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("openclaw.env.title")}
|
||||||
|
>
|
||||||
|
<KeyRound className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("openclawTools")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("openclaw.tools.title")}
|
||||||
|
>
|
||||||
|
<Shield className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("openclawAgents")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("openclaw.agents.title")}
|
||||||
|
>
|
||||||
|
<Cpu className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("sessions")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("sessionManager.title")}
|
||||||
|
>
|
||||||
|
<History className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("skills")}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5",
|
||||||
|
"transition-all duration-200 ease-in-out overflow-hidden",
|
||||||
|
hasSkillsSupport
|
||||||
|
? "opacity-100 w-8 scale-100 px-2"
|
||||||
|
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1",
|
||||||
|
)}
|
||||||
|
title={t("skills.manage")}
|
||||||
|
>
|
||||||
|
<Wrench className="flex-shrink-0 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("prompts")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("prompts.manage")}
|
||||||
|
>
|
||||||
|
<Book className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("sessions")}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5",
|
||||||
|
"transition-all duration-200 ease-in-out overflow-hidden",
|
||||||
|
hasSessionSupport
|
||||||
|
? "opacity-100 w-8 scale-100 px-2"
|
||||||
|
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1",
|
||||||
|
)}
|
||||||
|
title={t("sessionManager.title")}
|
||||||
|
>
|
||||||
|
<History className="flex-shrink-0 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("mcp")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("mcp.title")}
|
||||||
|
>
|
||||||
|
<McpIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AppSwitcher
|
<Button
|
||||||
activeApp={activeApp}
|
onClick={() => setIsAddOpen(true)}
|
||||||
onSwitch={setActiveApp}
|
size="icon"
|
||||||
visibleApps={visibleApps}
|
className={`ml-2 ${addActionButtonClass}`}
|
||||||
compact={
|
>
|
||||||
isCurrentAppTakeoverActive &&
|
<Plus className="w-5 h-5" />
|
||||||
Object.values(visibleApps).filter(Boolean).length >= 4
|
</Button>
|
||||||
}
|
</>
|
||||||
/>
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-1 p-1 bg-muted rounded-xl">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
|
||||||
key={activeApp === "openclaw" ? "openclaw" : "default"}
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
{activeApp === "openclaw" ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("workspace")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("workspace.manage")}
|
|
||||||
>
|
|
||||||
<FolderOpen className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("openclawEnv")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("openclaw.env.title")}
|
|
||||||
>
|
|
||||||
<KeyRound className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("openclawTools")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("openclaw.tools.title")}
|
|
||||||
>
|
|
||||||
<Shield className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("openclawAgents")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("openclaw.agents.title")}
|
|
||||||
>
|
|
||||||
<Cpu className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("sessions")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("sessionManager.title")}
|
|
||||||
>
|
|
||||||
<History className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("skills")}
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5",
|
|
||||||
"transition-all duration-200 ease-in-out overflow-hidden",
|
|
||||||
hasSkillsSupport
|
|
||||||
? "opacity-100 w-8 scale-100 px-2"
|
|
||||||
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1",
|
|
||||||
)}
|
|
||||||
title={t("skills.manage")}
|
|
||||||
>
|
|
||||||
<Wrench className="flex-shrink-0 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("prompts")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("prompts.manage")}
|
|
||||||
>
|
|
||||||
<Book className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("sessions")}
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5",
|
|
||||||
"transition-all duration-200 ease-in-out overflow-hidden",
|
|
||||||
hasSessionSupport
|
|
||||||
? "opacity-100 w-8 scale-100 px-2"
|
|
||||||
: "opacity-0 w-0 scale-75 pointer-events-none px-0 -ml-1",
|
|
||||||
)}
|
|
||||||
title={t("sessionManager.title")}
|
|
||||||
>
|
|
||||||
<History className="flex-shrink-0 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("mcp")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title={t("mcp.title")}
|
|
||||||
>
|
|
||||||
<McpIcon size={16} />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsAddOpen(true)}
|
|
||||||
size="icon"
|
|
||||||
className={`ml-2 ${addActionButtonClass}`}
|
|
||||||
>
|
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ export function AppSwitcher({
|
|||||||
name={appDisplayName[app]}
|
name={appDisplayName[app]}
|
||||||
size={iconSize}
|
size={iconSize}
|
||||||
/>
|
/>
|
||||||
{!compact && <span>{appDisplayName[app]}</span>}
|
{!compact && (
|
||||||
|
<span className="transition-all duration-200 whitespace-nowrap">
|
||||||
|
{appDisplayName[app]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useRef, useState, type RefObject } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether the container's children overflow the available width
|
||||||
|
* and returns a `compact` flag for the AppSwitcher.
|
||||||
|
*
|
||||||
|
* Uses ResizeObserver on a flex-constrained container. The container
|
||||||
|
* must have `flex-1 min-w-0 overflow-hidden` so its width is determined
|
||||||
|
* by the parent layout, not its own content — avoiding the oscillation
|
||||||
|
* problem when toggling compact mode.
|
||||||
|
*/
|
||||||
|
export function useAutoCompact(
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>,
|
||||||
|
): boolean {
|
||||||
|
const [compact, setCompact] = useState(false);
|
||||||
|
const normalWidthRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(() => {
|
||||||
|
if (!compact) {
|
||||||
|
// Cache the total content width in normal mode
|
||||||
|
normalWidthRef.current = el.scrollWidth;
|
||||||
|
// Overflow detected → switch to compact
|
||||||
|
if (el.scrollWidth > el.clientWidth + 1) {
|
||||||
|
setCompact(true);
|
||||||
|
}
|
||||||
|
} else if (normalWidthRef.current > 0) {
|
||||||
|
// In compact mode: only recover to normal if
|
||||||
|
// available space >= what normal mode needed
|
||||||
|
if (el.clientWidth >= normalWidthRef.current) {
|
||||||
|
setCompact(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, [compact]);
|
||||||
|
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user