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