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() {
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user