feat: premium toolbar with glassmorphism, auto-hide, edge snap, and smooth snap animation
This commit is contained in:
1 parent
ea71a574a4
commit
1e0dd183b1
1 file changed
+52
-5
@@ -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}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user