feat: premium toolbar with glassmorphism, auto-hide, edge snap, and smooth snap animation
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m52s

This commit is contained in:
openhands committed 2026-07-26 18:17:32 +02:00
1 parent ea71a574a4
commit 1e0dd183b1
1 file changed
+52 -5
+52 -5
View File
@@ -121,8 +121,43 @@ export function ClientView({
useEffect(() => { if (pos) posRef.current = pos; }, [pos]); 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<typeof setTimeout>;
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) => { const startDrag = useCallback((clientX: number, clientY: number) => {
if (!pos) return; if (!pos) return;
setSnapTween(false);
dragRef.current = { dragRef.current = {
startX: clientX, startX: clientX,
startY: clientY, startY: clientY,
@@ -165,10 +200,13 @@ export function ClientView({
} }
function onEnd() { function onEnd() {
setDragging(false); setDragging(false);
setPos(p => p); const snapped = snapPos(posRef.current.top, posRef.current.left);
setPos(snapped);
setSnapTween(true);
setTimeout(() => setSnapTween(false), 180);
try { try {
localStorage.setItem("client_toolbar_x", String(posRef.current.left)); localStorage.setItem("client_toolbar_x", String(snapped.left));
localStorage.setItem("client_toolbar_y", String(posRef.current.top)); localStorage.setItem("client_toolbar_y", String(snapped.top));
} catch {} } catch {}
} }
window.addEventListener("mousemove", onMove); window.addEventListener("mousemove", onMove);
@@ -188,11 +226,19 @@ export function ClientView({
{pos ? ( {pos ? (
<div <div
ref={toolbarRef} ref={toolbarRef}
className={`fixed z-[9999] flex items-center gap-1 sm:gap-2 select-none ${dragging ? "cursor-grabbing" : "cursor-grab"}`} onMouseEnter={() => setHidden(false)}
style={{ top: pos.top, left: pos.left }} 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} onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
> >
<div className="flex items-center gap-1 sm:gap-2">
<ToolbarBtn href="/" title="Home"> <ToolbarBtn href="/" title="Home">
<svg <svg
width="14" width="14"
@@ -313,6 +359,7 @@ export function ClientView({
> >
<LogOut size={14} /> <LogOut size={14} />
</ToolbarBtn> </ToolbarBtn>
</div>
</div> </div>
) : null} ) : null}