refactor(ui): always show icon-only app switcher, drop auto-compact

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.
This commit is contained in:
Jason
2026-07-30 08:52:48 +08:00
parent 56fb46c093
commit f5f4281d06
3 changed files with 3 additions and 73 deletions
+1 -9
View File
@@ -42,7 +42,6 @@ import { openclawKeys, useOpenClawHealth } from "@/hooks/useOpenClaw";
import { hermesKeys, useOpenHermesWebUI } from "@/hooks/useHermes";
import { hermesApi } from "@/lib/api/hermes";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { useAutoCompact } from "@/hooks/useAutoCompact";
import { useUsageCacheBridge } from "@/hooks/useUsageCacheBridge";
import { useTauriEvent } from "@/hooks/useTauriEvent";
import { useLastValidValue } from "@/hooks/useLastValidValue";
@@ -247,9 +246,6 @@ function App() {
const effectiveEditingProvider = useLastValidValue(editingProvider);
const effectiveUsageProvider = useLastValidValue(usageProvider);
const toolbarRef = useRef<HTMLDivElement>(null);
const isToolbarCompact = useAutoCompact(toolbarRef);
useUsageCacheBridge();
const promptPanelRef = useRef<any>(null);
@@ -1277,10 +1273,7 @@ function App() {
<ProfileSwitcher activeApp={activeApp} />
</div>
)}
<div
ref={toolbarRef}
className="flex flex-1 min-w-0 overflow-x-hidden items-center py-4 pr-2"
>
<div className="flex flex-1 min-w-0 overflow-x-hidden items-center py-4 pr-2">
<div
className="flex shrink-0 items-center gap-1.5 ml-auto"
style={{ WebkitAppRegion: "no-drag" } as any}
@@ -1399,7 +1392,6 @@ function App() {
activeApp={activeApp}
onSwitch={setActiveApp}
visibleApps={visibleApps}
compact={isToolbarCompact}
/>
<div className="flex items-center gap-1 p-1 bg-muted rounded-xl">
+2 -12
View File
@@ -15,7 +15,6 @@ interface AppSwitcherProps {
activeApp: AppId;
onSwitch: (app: AppId) => void;
visibleApps?: VisibleApps;
compact?: boolean;
}
const ALL_APPS: AppId[] = [
@@ -34,7 +33,6 @@ export function AppSwitcher({
activeApp,
onSwitch,
visibleApps,
compact,
}: AppSwitcherProps) {
const handleSwitch = (app: AppId) => {
if (app === activeApp) return;
@@ -80,6 +78,8 @@ export function AppSwitcher({
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
@@ -115,16 +115,6 @@ export function AppSwitcher({
</span>
)}
</span>
<span
className={cn(
"transition-all duration-200 whitespace-nowrap overflow-hidden",
compact
? "max-w-0 opacity-0 ml-0"
: "max-w-[120px] opacity-100 ml-2",
)}
>
{appDisplayName[app]}
</span>
</button>
);
})}
-52
View File
@@ -1,52 +0,0 @@
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);
const lockUntilRef = useRef(0);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver(() => {
// During expand animation, ignore resize events to prevent flicker
if (Date.now() < lockUntilRef.current) return;
if (!compact) {
// Overflow detected → switch to compact
if (el.scrollWidth > el.clientWidth + 1) {
// Cache only at the overflow edge: when content fits,
// scrollWidth === clientWidth (DOM spec), so caching unconditionally
// would pollute normalWidthRef with the container width (e.g. after
// maximizing), making the expand threshold unreachable.
normalWidthRef.current = el.scrollWidth;
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) {
// Lock out resize events during the expand animation (200ms + 50ms margin)
lockUntilRef.current = Date.now() + 250;
setCompact(false);
}
}
});
ro.observe(el);
return () => ro.disconnect();
}, [compact]);
return compact;
}