feat(toolbar): add smooth transition animation for AppSwitcher compact toggle

Replace conditional rendering with always-rendered span driven by CSS
max-width + opacity animation. Add time-lock in useAutoCompact to prevent
ResizeObserver flicker during expand animation.
This commit is contained in:
Jason
2026-02-21 20:54:03 +08:00
parent 5763b9094b
commit 6b4ba64bbd
2 changed files with 21 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
import type { AppId } from "@/lib/api";
import type { VisibleApps } from "@/types";
import { ProviderIcon } from "@/components/ProviderIcon";
import { cn } from "@/lib/utils";
interface AppSwitcherProps {
activeApp: AppId;
@@ -52,22 +53,28 @@ export function AppSwitcher({
key={app}
type="button"
onClick={() => handleSwitch(app)}
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
className={cn(
"group inline-flex items-center px-3 h-8 rounded-md text-sm font-medium transition-all duration-200",
activeApp === app
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
}`}
: "text-muted-foreground hover:text-foreground hover:bg-background/50",
)}
>
<ProviderIcon
icon={appIconName[app]}
name={appDisplayName[app]}
size={iconSize}
/>
{!compact && (
<span className="transition-all duration-200 whitespace-nowrap">
{appDisplayName[app]}
</span>
)}
<span
className={cn(
"transition-all duration-200 whitespace-nowrap overflow-hidden",
compact
? "max-w-0 opacity-0 ml-0"
: "max-w-[80px] opacity-100 ml-2",
)}
>
{appDisplayName[app]}
</span>
</button>
))}
</div>

View File

@@ -14,12 +14,16 @@ export function useAutoCompact(
): 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) {
// Cache the total content width in normal mode
normalWidthRef.current = el.scrollWidth;
@@ -31,6 +35,8 @@ export function useAutoCompact(
// 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);
}
}