Skip to content

Commit 2838248

Browse files
devsart95claude
andcommitted
feat(mobile): barra de navegación inferior para pantallas < 860px
- 5 ítems con ícono SVG + label en JetBrains Mono uppercase - Active state con color accent + glow en ícono via IntersectionObserver - Glassmorphism: blur(20px) + border-top sutil - Safe-area-inset para notch/home-indicator iOS - padding-bottom en main/footer para no quedar tapado - Light mode compatible Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b8e954f commit 2838248

File tree

3 files changed

+143
-1
lines changed

3 files changed

+143
-1
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Skills } from './components/sections/Skills'
1414
import { Projects } from './components/sections/Projects'
1515
import { Connect } from './components/sections/Connect'
1616
import { Footer } from './components/Footer'
17+
import { MobileNav } from './components/MobileNav'
1718

1819
export default function App() {
1920
return (
@@ -39,6 +40,7 @@ export default function App() {
3940
<Connect />
4041
</main>
4142
<Footer />
43+
<MobileNav />
4244
</LangProvider>
4345
)
4446
}

src/components/MobileNav.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useState, useEffect } from 'react'
2+
import { useLang } from '../context/LangContext'
3+
4+
const NAV_ITEMS = [
5+
{
6+
labelKey: 'nav.about' as const,
7+
href: '#sobre',
8+
icon: (
9+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
10+
<circle cx="12" cy="8" r="4"/>
11+
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
12+
</svg>
13+
),
14+
},
15+
{
16+
labelKey: 'nav.stack' as const,
17+
href: '#stack',
18+
icon: (
19+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
20+
<polyline points="16 18 22 12 16 6"/>
21+
<polyline points="8 6 2 12 8 18"/>
22+
</svg>
23+
),
24+
},
25+
{
26+
labelKey: 'nav.skills' as const,
27+
href: '#skills',
28+
icon: (
29+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
30+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
31+
</svg>
32+
),
33+
},
34+
{
35+
labelKey: 'nav.projects' as const,
36+
href: '#proyectos',
37+
icon: (
38+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
39+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
40+
</svg>
41+
),
42+
},
43+
{
44+
labelKey: 'nav.contact' as const,
45+
href: '#conectar',
46+
icon: (
47+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
48+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
49+
<polyline points="22,6 12,13 2,6"/>
50+
</svg>
51+
),
52+
},
53+
]
54+
55+
const SECTION_IDS = ['sobre', 'stack', 'skills', 'proyectos', 'conectar']
56+
57+
export function MobileNav() {
58+
const { t } = useLang()
59+
const [active, setActive] = useState('')
60+
61+
useEffect(() => {
62+
const observers: IntersectionObserver[] = []
63+
64+
SECTION_IDS.forEach(id => {
65+
const el = document.getElementById(id)
66+
if (!el) return
67+
const obs = new IntersectionObserver(
68+
([entry]) => { if (entry.isIntersecting) setActive(id) },
69+
{ rootMargin: '-40% 0px -50% 0px' }
70+
)
71+
obs.observe(el)
72+
observers.push(obs)
73+
})
74+
75+
return () => observers.forEach(o => o.disconnect())
76+
}, [])
77+
78+
return (
79+
<nav className="mobile-nav" aria-label={t('nav.aria')}>
80+
{NAV_ITEMS.map(item => {
81+
const sectionId = item.href.replace('#', '')
82+
const isActive = active === sectionId
83+
return (
84+
<a
85+
key={item.href}
86+
href={item.href}
87+
className={`mn-item${isActive ? ' mn-active' : ''}`}
88+
>
89+
<span className="mn-icon">{item.icon}</span>
90+
<span className="mn-label">{t(item.labelKey)}</span>
91+
</a>
92+
)
93+
})}
94+
</nav>
95+
)
96+
}

src/index.css

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,48 @@ nav {
221221
0%,100% { opacity: .25; transform: scaleY(.6); }
222222
50% { opacity: .9; transform: scaleY(1); }
223223
}
224-
@media (max-width: 860px) { nav { display: none; } }
224+
@media (max-width: 860px) { nav:not(.mobile-nav) { display: none; } }
225+
226+
/* ── MOBILE NAV ───────────────────────────────────── */
227+
.mobile-nav {
228+
display: none;
229+
position: fixed; bottom: 0; left: 0; right: 0;
230+
z-index: 200;
231+
flex-direction: row;
232+
background: rgba(5, 10, 20, 0.82);
233+
backdrop-filter: blur(20px);
234+
-webkit-backdrop-filter: blur(20px);
235+
border-top: 1px solid rgba(56, 189, 248, 0.1);
236+
padding: 6px 0 max(6px, env(safe-area-inset-bottom));
237+
}
238+
@media (max-width: 860px) { .mobile-nav { display: flex; } }
239+
[data-theme="light"] .mobile-nav {
240+
background: rgba(241, 245, 249, 0.88);
241+
border-top: 1px solid rgba(2, 132, 199, 0.15);
242+
}
243+
.mn-item {
244+
flex: 1; display: flex; flex-direction: column;
245+
align-items: center; justify-content: center;
246+
gap: 3px; padding: 6px 4px;
247+
text-decoration: none; color: rgba(148, 163, 184, 0.6);
248+
transition: color 180ms;
249+
-webkit-tap-highlight-color: transparent;
250+
}
251+
.mn-item:active { opacity: 0.7; }
252+
.mn-active {
253+
color: var(--accent);
254+
}
255+
.mn-active .mn-icon svg {
256+
filter: drop-shadow(0 0 6px var(--accent));
257+
}
258+
.mn-icon { display: flex; align-items: center; justify-content: center; }
259+
.mn-label {
260+
font-family: 'JetBrains Mono', monospace;
261+
font-size: 9px; letter-spacing: 0.04em;
262+
text-transform: uppercase;
263+
}
264+
[data-theme="light"] .mn-item { color: rgba(15, 23, 42, 0.45); }
265+
[data-theme="light"] .mn-active { color: var(--accent); }
225266

226267
/* ── HERO ─────────────────────────────────────────── */
227268
#hero { display: flex; align-items: center; padding-top: 80px; }
@@ -1013,3 +1054,6 @@ footer p { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--m
10131054
.hud-row { grid-template-columns:100px 1fr; gap:12px; }
10141055
.lang-selector { top: 60px; right: 12px; }
10151056
}
1057+
@media (max-width: 860px) {
1058+
main, footer { padding-bottom: 72px; }
1059+
}

0 commit comments

Comments
 (0)