Files
anatomia/frontend/src/components/QuickRegionNav.tsx
T

133 lines
4.8 KiB
TypeScript

import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
import { useAtlasStore, type BodyRegion } from "../store/useAtlasStore.js";
const REGIONS: { region: BodyRegion; label: string }[] = [
{ region: "cabeca", label: "Cabeça" },
{ region: "pescoco", label: "Pescoço" },
{ region: "torax", label: "Tórax" },
{ region: "abdome", label: "Abdome" },
{ region: "pelve", label: "Pelve" },
{ region: "membros_superiores", label: "Membros Sup." },
{ region: "membros_inferiores", label: "Membros Inf." },
{ region: "corpo_inteiro", label: "Corpo Inteiro" },
];
const STORAGE_KEY = "atlas.quickRegionNav.pos";
function loadStoredPos(): { x: number; y: number } | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
// Navegação rápida por região — em vez de precisar acertar o clique numa
// estrutura minúscula pra girar/aproximar a câmera manualmente, o usuário
// pede direto pra "ir" numa região do corpo. Arrastável pela alcinha (⠿),
// pois a barra pode cair em cima de uma parte do modelo que o usuário
// queira ver — a posição escolhida fica salva entre visitas.
export function QuickRegionNav() {
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
const [pos, setPos] = useState<{ x: number; y: number } | null>(() => loadStoredPos());
const elRef = useRef<HTMLDivElement>(null);
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
function clamp(x: number, y: number) {
const el = elRef.current;
const parent = el?.offsetParent as HTMLElement | null;
if (!el || !parent) return { x, y };
const maxX = Math.max(0, parent.clientWidth - el.offsetWidth);
const maxY = Math.max(0, parent.clientHeight - el.offsetHeight);
return { x: Math.min(Math.max(0, x), maxX), y: Math.min(Math.max(0, y), maxY) };
}
function onGripPointerDown(e: ReactPointerEvent) {
const el = elRef.current;
const parent = el?.offsetParent as HTMLElement | null;
if (!el || !parent) return;
// Antes do primeiro arrasto a barra fica centralizada via CSS transform
// (translate-x-1/2) — offsetLeft/offsetTop ignoram transform, então
// calculamos a posição visual real a partir do bounding rect pra não
// dar aquele "pulo" no primeiro clique.
const elRect = el.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
dragRef.current = {
startX: e.clientX,
startY: e.clientY,
origX: elRect.left - parentRect.left,
origY: elRect.top - parentRect.top,
};
(e.target as HTMLElement).setPointerCapture(e.pointerId);
}
function onGripPointerMove(e: ReactPointerEvent) {
if (!dragRef.current) return;
const dx = e.clientX - dragRef.current.startX;
const dy = e.clientY - dragRef.current.startY;
setPos(clamp(dragRef.current.origX + dx, dragRef.current.origY + dy));
}
function onGripPointerUp() {
dragRef.current = null;
setPos((current) => {
if (current) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
} catch {
// ignore
}
}
return current;
});
}
// Se a janela mudar de tamanho (ex: girar o celular), reaplica o clamp
// pra barra não ficar presa fora da área visível.
useEffect(() => {
if (!pos) return;
function onResize() {
setPos((current) => (current ? clamp(current.x, current.y) : current));
}
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div
ref={elRef}
style={pos ? { position: "absolute", left: pos.x, top: pos.y } : undefined}
className={
"flex items-center gap-1.5 bg-panel/90 border border-white/10 rounded-lg pl-1.5 pr-3 py-2 shadow-lg " +
(pos
? "max-w-[92vw]"
: "absolute top-14 md:top-3 left-1/2 -translate-x-1/2 max-w-[92vw] md:max-w-[90%]")
}
>
<div
onPointerDown={onGripPointerDown}
onPointerMove={onGripPointerMove}
onPointerUp={onGripPointerUp}
className="shrink-0 cursor-move select-none text-slate-500 hover:text-slate-300 px-1 touch-none"
title="Arrastar"
aria-label="Arrastar menu de regiões"
>
⠿
</div>
<div className="flex gap-1.5 overflow-x-auto md:flex-wrap">
{REGIONS.map((r) => (
<button
key={r.region}
onClick={() => focusOnRegion(r.region)}
className="shrink-0 text-xs px-3 py-1.5 rounded-full bg-white/5 hover:bg-accent hover:text-white text-slate-300 transition-colors"
>
{r.label}
</button>
))}
</div>
</div>
);
}