113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { useSystemStructures } from "../hooks/useSystemStructures.js";
|
||
import { useAtlasStore } from "../store/useAtlasStore.js";
|
||
import { Scene } from "../components/viewer/Scene.js";
|
||
import { LayerToggle } from "../components/LayerToggle.js";
|
||
import { ClippingControls } from "../components/ClippingControls.js";
|
||
import { SearchNav } from "../components/SearchNav.js";
|
||
import { StructureInfoPanel } from "../components/StructureInfoPanel.js";
|
||
import { QuickRegionNav } from "../components/QuickRegionNav.js";
|
||
import { SkinDiagram } from "../components/SkinDiagram.js";
|
||
import { trackVisit } from "../lib/analytics.js";
|
||
|
||
// Em telas pequenas não há espaço para os 3 painéis lado a lado — os
|
||
// laterais viram gavetas (drawers) que deslizam por cima do visualizador
|
||
// 3D, que fica sempre em tela cheia (é o que importa pra estudar).
|
||
export default function AtlasPage() {
|
||
const visibleSystems = useAtlasStore((s) => s.visibleSystems);
|
||
const { bySlug } = useSystemStructures(visibleSystems);
|
||
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
|
||
|
||
const [leftOpen, setLeftOpen] = useState(false);
|
||
const [rightOpen, setRightOpen] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (selectedStructureId) {
|
||
setRightOpen(true);
|
||
setLeftOpen(false);
|
||
}
|
||
}, [selectedStructureId]);
|
||
|
||
// Registra qual(is) sistema(s) do atlas o visitante ligou — é o que
|
||
// alimenta o ranking de "sistemas mais acessados" no painel de
|
||
// estatísticas. Só manda algo se o visitante aceitou o aviso de cookies.
|
||
useEffect(() => {
|
||
for (const slug of visibleSystems) trackVisit("/", slug);
|
||
}, [visibleSystems]);
|
||
|
||
return (
|
||
<div className="h-full relative md:grid md:grid-cols-[280px_1fr_320px]">
|
||
<aside
|
||
className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[300px] md:w-auto
|
||
border-r border-white/10 bg-panel p-3 flex flex-col min-h-0 overflow-y-auto
|
||
transition-transform duration-200 md:translate-x-0
|
||
${leftOpen ? "translate-x-0" : "-translate-x-full"}`}
|
||
>
|
||
<button
|
||
onClick={() => setLeftOpen(false)}
|
||
className="md:hidden self-end text-slate-400 hover:text-white text-sm mb-2 px-2 py-1"
|
||
aria-label="Fechar painel"
|
||
>
|
||
✕ Fechar
|
||
</button>
|
||
<LayerToggle />
|
||
<ClippingControls />
|
||
<div className="flex-1 min-h-0 mt-4">
|
||
<SearchNav />
|
||
</div>
|
||
</aside>
|
||
|
||
{/* No mobile o painel de detalhes é uma bottom sheet (ver <aside> mais
|
||
abaixo) — o fundo escurecido só faz sentido pro drawer de camadas,
|
||
que ainda cobre a tela toda; a sheet já deixa o modelo 3D visível
|
||
por cima dela, então não precisa (e não deveria) escurecer a cena. */}
|
||
{leftOpen && (
|
||
<div
|
||
className="fixed inset-0 bg-black/50 z-20 md:hidden"
|
||
onClick={() => setLeftOpen(false)}
|
||
/>
|
||
)}
|
||
|
||
<div className="relative h-full min-h-0">
|
||
<Scene bySlug={bySlug} />
|
||
<QuickRegionNav />
|
||
<SkinDiagram bySlug={bySlug} />
|
||
|
||
<button
|
||
onClick={() => setLeftOpen(true)}
|
||
className="md:hidden absolute top-3 left-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
|
||
>
|
||
☰ Camadas
|
||
</button>
|
||
{!rightOpen && (
|
||
<button
|
||
onClick={() => setRightOpen(true)}
|
||
className="md:hidden absolute top-3 right-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
|
||
>
|
||
ℹ️ Detalhes
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Desktop: gaveta lateral fixa, igual à de camadas. Mobile: bottom
|
||
sheet que sobe de baixo pra cima e some — a parte anatômica
|
||
selecionada (destacada no modelo 3D, atrás dela) continua visível
|
||
por cima, em vez de ser tampada pela tela inteira do painel. */}
|
||
<aside
|
||
className={`fixed md:static inset-x-0 bottom-0 md:inset-auto z-30
|
||
max-h-[65vh] md:max-h-none md:h-full w-full md:w-auto
|
||
border-t md:border-t-0 md:border-l border-white/10 bg-panel
|
||
rounded-t-2xl md:rounded-none p-4 pt-2 md:pt-4 overflow-y-auto
|
||
shadow-[0_-8px_24px_rgba(0,0,0,0.4)] md:shadow-none
|
||
transition-transform duration-200 md:!translate-y-0
|
||
${rightOpen ? "translate-y-0" : "translate-y-full"}`}
|
||
>
|
||
{/* Alça de arrastar — só decorativa, mas é o sinal visual padrão de
|
||
bottom sheet ("puxe/deslize pra baixo pra fechar"). */}
|
||
<div className="md:hidden mx-auto mb-2 h-1.5 w-10 rounded-full bg-white/15" />
|
||
<StructureInfoPanel bySlug={bySlug} onClose={() => setRightOpen(false)} />
|
||
</aside>
|
||
</div>
|
||
);
|
||
}
|