refactor: useLayoutEffect for no-flash position init, clean localStorage save, hide toolbar until ready
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 2m3s

This commit is contained in:
openhands committed 2026-07-26 18:11:12 +02:00
1 parent b77dbb65f6
commit ea71a574a4
1 file changed
+22 -19
+22 -19
View File
@@ -7,6 +7,7 @@ import {
type ReactNode, type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
@@ -99,10 +100,11 @@ export function ClientView({
const toolbarRef = useRef<HTMLDivElement>(null); const toolbarRef = useRef<HTMLDivElement>(null);
const dragRef = useRef({ startX: 0, startY: 0, startTop: 0, startLeft: 0 }); const dragRef = useRef({ startX: 0, startY: 0, startTop: 0, startLeft: 0 });
const [pos, setPos] = useState({ top: 8, left: -1 }); const posRef = useRef({ top: 8, left: 8 });
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
useEffect(() => { useLayoutEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const ux = params.get("toolbarX"), uy = params.get("toolbarY"); const ux = params.get("toolbarX"), uy = params.get("toolbarY");
if (ux && uy) { setPos({ top: Number(uy), left: Number(ux) }); return; } if (ux && uy) { setPos({ top: Number(uy), left: Number(ux) }); return; }
@@ -117,9 +119,10 @@ export function ClientView({
setPos({ top: 8, left: window.innerWidth - w - 8 }); setPos({ top: 8, left: window.innerWidth - w - 8 });
}, []); }, []);
useEffect(() => { if (pos) posRef.current = pos; }, [pos]);
const startDrag = useCallback((clientX: number, clientY: number) => { const startDrag = useCallback((clientX: number, clientY: number) => {
const el = toolbarRef.current; if (!pos) return;
if (!el) return;
dragRef.current = { dragRef.current = {
startX: clientX, startX: clientX,
startY: clientY, startY: clientY,
@@ -127,7 +130,7 @@ export function ClientView({
startLeft: pos.left, startLeft: pos.left,
}; };
setDragging(true); setDragging(true);
}, [pos.top, pos.left]); }, [pos]);
const handleMouseDown = useCallback((e: React.MouseEvent) => { const handleMouseDown = useCallback((e: React.MouseEvent) => {
if ((e.target as HTMLElement).closest("button, a")) return; if ((e.target as HTMLElement).closest("button, a")) return;
@@ -162,13 +165,11 @@ export function ClientView({
} }
function onEnd() { function onEnd() {
setDragging(false); setDragging(false);
setPos(p => { setPos(p => p);
try { try {
localStorage.setItem("client_toolbar_x", String(p.left)); localStorage.setItem("client_toolbar_x", String(posRef.current.left));
localStorage.setItem("client_toolbar_y", String(p.top)); localStorage.setItem("client_toolbar_y", String(posRef.current.top));
} catch {} } catch {}
return p;
});
} }
window.addEventListener("mousemove", onMove); window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onEnd); window.addEventListener("mouseup", onEnd);
@@ -184,13 +185,14 @@ export function ClientView({
return ( return (
<> <>
<div {pos ? (
ref={toolbarRef} <div
className={`fixed z-[9999] flex items-center gap-1 sm:gap-2 select-none ${dragging ? "cursor-grabbing" : "cursor-grab"}`} ref={toolbarRef}
style={{ top: pos.top, left: pos.left >= 0 ? pos.left : undefined, right: pos.left < 0 ? 8 : undefined }} className={`fixed z-[9999] flex items-center gap-1 sm:gap-2 select-none ${dragging ? "cursor-grabbing" : "cursor-grab"}`}
onMouseDown={handleMouseDown} style={{ top: pos.top, left: pos.left }}
onTouchStart={handleTouchStart} onMouseDown={handleMouseDown}
> onTouchStart={handleTouchStart}
>
<ToolbarBtn href="/" title="Home"> <ToolbarBtn href="/" title="Home">
<svg <svg
width="14" width="14"
@@ -312,6 +314,7 @@ export function ClientView({
<LogOut size={14} /> <LogOut size={14} />
</ToolbarBtn> </ToolbarBtn>
</div> </div>
) : null}
<div <div
className="relative w-screen h-screen overflow-hidden" className="relative w-screen h-screen overflow-hidden"