From 1e0dd183b1852360cd224d4599bfb369d696e058 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 26 Jul 2026 18:17:32 +0200 Subject: [PATCH] feat: premium toolbar with glassmorphism, auto-hide, edge snap, and smooth snap animation --- src/app/client/client-view.tsx | 57 +++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/app/client/client-view.tsx b/src/app/client/client-view.tsx index acf679c2..9f38137e 100644 --- a/src/app/client/client-view.tsx +++ b/src/app/client/client-view.tsx @@ -121,8 +121,43 @@ export function ClientView({ useEffect(() => { if (pos) posRef.current = pos; }, [pos]); + const [hidden, setHidden] = useState(false); + const [snapTween, setSnapTween] = useState(false); + const SNAP = 40; + + /* auto-hide: show on mouse activity, fade after 2s idle */ + useEffect(() => { + let t: ReturnType; + function poke() { + setHidden(false); + clearTimeout(t); + if (!dragging) t = setTimeout(() => setHidden(true), 2000); + } + window.addEventListener("mousemove", poke); + window.addEventListener("touchstart", poke); + poke(); + return () => { + window.removeEventListener("mousemove", poke); + window.removeEventListener("touchstart", poke); + clearTimeout(t); + }; + }, [dragging]); + + function snapPos(top: number, left: number) { + const el = toolbarRef.current; + if (!el) return { top, left }; + const w = el.offsetWidth, h = el.offsetHeight; + const vw = window.innerWidth, vh = window.innerHeight; + if (top < SNAP) top = 0; + else if (top + h > vh - SNAP) top = vh - h; + if (left < SNAP) left = 0; + else if (left + w > vw - SNAP) left = vw - w; + return { top, left }; + } + const startDrag = useCallback((clientX: number, clientY: number) => { if (!pos) return; + setSnapTween(false); dragRef.current = { startX: clientX, startY: clientY, @@ -165,10 +200,13 @@ export function ClientView({ } function onEnd() { setDragging(false); - setPos(p => p); + const snapped = snapPos(posRef.current.top, posRef.current.left); + setPos(snapped); + setSnapTween(true); + setTimeout(() => setSnapTween(false), 180); try { - localStorage.setItem("client_toolbar_x", String(posRef.current.left)); - localStorage.setItem("client_toolbar_y", String(posRef.current.top)); + localStorage.setItem("client_toolbar_x", String(snapped.left)); + localStorage.setItem("client_toolbar_y", String(snapped.top)); } catch {} } window.addEventListener("mousemove", onMove); @@ -188,11 +226,19 @@ export function ClientView({ {pos ? (
setHidden(false)} + className={`fixed z-[9999] backdrop-blur-xl bg-black/30 rounded-xl border border-white/10 shadow-2xl px-1.5 py-1 select-none ${dragging ? "cursor-grabbing" : "cursor-grab"} ${hidden && !dragging ? "opacity-0 pointer-events-none" : "opacity-100"}`} + style={{ + top: pos.top, + left: pos.left, + transitionProperty: snapTween ? "top, left, opacity" : "opacity", + transitionDuration: snapTween ? "0.18s, 0.18s, 0.3s" : "0.3s", + transitionTimingFunction: snapTween ? "ease-out, ease-out, ease" : "ease", + }} onMouseDown={handleMouseDown} onTouchStart={handleTouchStart} > +
+
) : null}