This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-09 19:48:43 -03:00
1 parent baf749caa8
commit 3b6f8cf5e4
7 files changed
+168 -24

No files matched your search

+7 -5
View File
@@ -5,10 +5,12 @@ const linkClass = ({ isActive }: { isActive: boolean }) =>
export function Layout() {
return (
<div className="h-screen flex flex-col">
<header className="flex items-center gap-4 px-4 py-2 border-b border-white/10 bg-panel">
<h1 className="text-sm font-semibold tracking-wide text-white">🫀 Atlas 3D de Anatomia</h1>
<nav className="flex gap-1">
<div className="h-[100dvh] flex flex-col">
<header className="flex items-center gap-2 sm:gap-4 px-2 sm:px-4 py-2 border-b border-white/10 bg-panel overflow-x-auto">
<h1 className="text-sm font-semibold tracking-wide text-white whitespace-nowrap">
🫀 <span className="hidden sm:inline">Atlas 3D de Anatomia</span>
</h1>
<nav className="flex gap-1 shrink-0">
<NavLink to="/" end className={linkClass}>
Atlas
</NavLink>
@@ -23,7 +25,7 @@ export function Layout() {
href="https://github.com/LluisV/Z-Anatomy-Sample"
target="_blank"
rel="noreferrer"
className="ml-auto text-[11px] text-slate-500 hover:text-slate-300"
className="ml-auto hidden md:inline-block text-[11px] text-slate-500 hover:text-slate-300 shrink-0"
>
Modelos 3D: projeto Z-Anatomy (CC BY-SA 4.0)
</a>
+6 -2
View File
@@ -18,12 +18,16 @@ export function QuickRegionNav() {
const focusOnRegion = useAtlasStore((s) => s.focusOnRegion);
return (
<div className="absolute top-3 left-1/2 -translate-x-1/2 flex flex-wrap gap-1.5 justify-center bg-panel/90 border border-white/10 rounded-lg px-3 py-2 shadow-lg max-w-[90%]">
<div
className="absolute top-14 md:top-3 left-1/2 -translate-x-1/2 flex gap-1.5
overflow-x-auto md:flex-wrap md:justify-center bg-panel/90 border border-white/10
rounded-lg px-3 py-2 shadow-lg max-w-[92vw] md:max-w-[90%]"
>
{REGIONS.map((r) => (
<button
key={r.region}
onClick={() => focusOnRegion(r.region)}
className="text-xs px-2.5 py-1 rounded-full bg-white/5 hover:bg-accent hover:text-white text-slate-300 transition-colors"
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>
+1 -1
View File
@@ -77,7 +77,7 @@ export function SearchNav() {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar estrutura (ex: fêmur, nervo ciático)..."
className="w-full rounded bg-white/5 border border-white/10 px-3 py-2 text-sm placeholder:text-slate-500 focus:outline-none focus:ring-1 focus:ring-accent"
className="w-full rounded bg-white/5 border border-white/10 px-3 py-2 text-base sm:text-sm placeholder:text-slate-500 focus:outline-none focus:ring-1 focus:ring-accent"
/>
<div className="overflow-y-auto flex-1 min-h-0 pr-1">
+1
View File
@@ -209,6 +209,7 @@ export function Scene({ bySlug }: { bySlug: Map<string, Structure> }) {
return (
<Canvas
shadows
dpr={[1, 1.5]}
camera={{ position: [1.2, 1.4, 1.6], fov: 45, near: 0.01, far: 100000 }}
gl={{ localClippingEnabled: true }}
>
+66 -4
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { useSystemStructures } from "../hooks/useSystemStructures.js";
import { useAtlasStore } from "../store/useAtlasStore.js";
import { Scene } from "../components/viewer/Scene.js";
@@ -7,13 +8,39 @@ import { SearchNav } from "../components/SearchNav.js";
import { StructureInfoPanel } from "../components/StructureInfoPanel.js";
import { QuickRegionNav } from "../components/QuickRegionNav.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]);
return (
<div className="h-full grid grid-cols-[280px_1fr_320px]">
<aside className="border-r border-white/10 bg-panel p-3 flex flex-col min-h-0">
<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">
@@ -21,12 +48,47 @@ export default function AtlasPage() {
</div>
</aside>
<div className="relative">
{(leftOpen || rightOpen) && (
<div
className="fixed inset-0 bg-black/50 z-20 md:hidden"
onClick={() => {
setLeftOpen(false);
setRightOpen(false);
}}
/>
)}
<div className="relative h-full min-h-0">
<Scene bySlug={bySlug} />
<QuickRegionNav />
<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>
<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>
<aside className="border-l border-white/10 bg-panel p-4 overflow-y-auto">
<aside
className={`fixed md:static inset-y-0 right-0 z-30 w-[86vw] max-w-[320px] md:w-auto
border-l border-white/10 bg-panel p-4 overflow-y-auto
transition-transform duration-200 md:translate-x-0
${rightOpen ? "translate-x-0" : "translate-x-full"}`}
>
<button
onClick={() => setRightOpen(false)}
className="md:hidden text-slate-400 hover:text-white text-sm mb-2 px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<StructureInfoPanel bySlug={bySlug} />
</aside>
</div>
+53 -5
View File
@@ -81,10 +81,24 @@ export default function PlanesPage() {
transverse: false,
});
const [showAxes, setShowAxes] = useState(true);
const [leftOpen, setLeftOpen] = useState(false);
const [rightOpen, setRightOpen] = useState(false);
return (
<div className="h-full grid grid-cols-[280px_1fr_300px]">
<aside className="border-r border-white/10 bg-panel p-4 space-y-3">
<div className="h-full relative md:grid md:grid-cols-[280px_1fr_300px]">
<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-4 space-y-3 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 float-right text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Planos Anatômicos</h3>
{PLANES.map((p) => (
<label key={p.key} className="flex items-center gap-2 text-sm cursor-pointer">
@@ -105,8 +119,30 @@ export default function PlanesPage() {
</label>
</aside>
<div className="relative">
<Canvas camera={{ position: [1.8, 1.3, 2.2], fov: 45 }}>
{(leftOpen || rightOpen) && (
<div
className="fixed inset-0 bg-black/50 z-20 md:hidden"
onClick={() => {
setLeftOpen(false);
setRightOpen(false);
}}
/>
)}
<div className="relative h-full min-h-0">
<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"
>
☰ Planos
</button>
<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"
>
📖 Glossário
</button>
<Canvas dpr={[1, 1.5]} camera={{ position: [1.8, 1.3, 2.2], fov: 45 }}>
<color attach="background" args={["#0b1220"]} />
<ambientLight intensity={0.6} />
<directionalLight position={[2, 3, 2]} intensity={1} />
@@ -139,7 +175,19 @@ export default function PlanesPage() {
</Canvas>
</div>
<aside className="border-l border-white/10 bg-panel p-4 overflow-y-auto space-y-3">
<aside
className={`fixed md:static inset-y-0 right-0 z-30 w-[86vw] max-w-[320px] md:w-auto
border-l border-white/10 bg-panel p-4 overflow-y-auto space-y-3
transition-transform duration-200 md:translate-x-0
${rightOpen ? "translate-x-0" : "translate-x-full"}`}
>
<button
onClick={() => setRightOpen(false)}
className="md:hidden text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<h3 className="text-xs uppercase tracking-wide text-slate-400">Glossário de Direções</h3>
{GLOSSARY.map((g) => (
<div key={g.term}>
+34 -7
View File
@@ -14,6 +14,7 @@ export default function QuizPage() {
const [index, setIndex] = useState(0);
const [feedback, setFeedback] = useState<"correct" | "wrong" | null>(null);
const [score, setScore] = useState({ correct: 0, total: 0 });
const [layersOpen, setLayersOpen] = useState(false);
const selectedStructureId = useAtlasStore((s) => s.selectedStructureId);
const selectStructure = useAtlasStore((s) => s.selectStructure);
@@ -70,15 +71,41 @@ export default function QuizPage() {
}
return (
<div className="h-full grid grid-cols-[280px_1fr]">
<aside className="border-r border-white/10 bg-panel p-3 flex flex-col gap-4 min-h-0">
<div className="h-full relative md:grid md:grid-cols-[280px_1fr]">
<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 gap-4 min-h-0 overflow-y-auto
transition-transform duration-200 md:translate-x-0
${layersOpen ? "translate-x-0" : "-translate-x-full"}`}
>
<button
onClick={() => setLayersOpen(false)}
className="md:hidden self-end text-slate-400 hover:text-white text-sm px-2 py-1"
aria-label="Fechar painel"
>
✕ Fechar
</button>
<LayerToggle />
</aside>
<div className="relative">
{layersOpen && (
<div
className="fixed inset-0 bg-black/50 z-20 md:hidden"
onClick={() => setLayersOpen(false)}
/>
)}
<div className="relative h-full min-h-0">
<Scene bySlug={bySlug} />
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-panel/95 border border-white/10 rounded-lg px-5 py-3 text-center shadow-lg max-w-md">
<button
onClick={() => setLayersOpen(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>
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-panel/95 border border-white/10 rounded-lg px-3 py-2.5 sm:px-5 sm:py-3 text-center shadow-lg w-[80vw] sm:w-auto sm:max-w-md">
{!current ? (
<p className="text-sm text-slate-300">
Nenhuma estrutura pendente de revisão agora. 🎉
@@ -88,7 +115,7 @@ export default function QuizPage() {
<p className="text-xs text-slate-400 uppercase tracking-wide mb-1">
Onde fica esta estrutura?
</p>
<p className="text-lg font-semibold text-white">{current.namePt}</p>
<p className="text-base sm:text-lg font-semibold text-white">{current.namePt}</p>
<p className="text-xs text-slate-500 mb-2">{current.system.namePt}</p>
{feedback === "correct" && (
@@ -102,14 +129,14 @@ export default function QuizPage() {
{feedback ? (
<button
onClick={next}
className="text-sm px-3 py-1 rounded bg-accent text-white"
className="text-sm px-3 py-1.5 rounded bg-accent text-white"
>
Próxima
</button>
) : (
<button
onClick={giveUp}
className="text-sm px-3 py-1 rounded bg-white/10 text-slate-200"
className="text-sm px-3 py-1.5 rounded bg-white/10 text-slate-200"
>
Mostrar resposta
</button>