mobile
This commit is contained in:
1 parent
baf749caa8
commit
3b6f8cf5e4
7 files changed
+168
-24
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user