Skip to content

Commit 4f42ca5

Browse files
authored
feat(nav): horizontally scrollable mobile bottom nav (#143)
Replace the fixed 4/5-column grid (which depended on the charger extension being on) with a scrollable row: min-width tabs, hidden scrollbar and auto-center on the active tab (keynest #284 pattern).
1 parent 5fabe7e commit 4f42ca5

1 file changed

Lines changed: 23 additions & 8 deletions

File tree

‎app/src/components/shell/AppLayout.tsx‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState, type MouseEvent, type ReactNode } from 'react';
1+
import { useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react';
22
import { Link, NavLink, useLocation, useNavigate } from 'react-router';
33
import { flushSync } from 'react-dom';
44
import { motion } from 'framer-motion';
@@ -403,7 +403,16 @@ export default function AppLayout({ children }: { children: ReactNode }) {
403403

404404
const titleKey = TITLE_KEYS.find(([re]) => re.test(pathname))?.[1] ?? 'nav.dashboard';
405405
const count = useInverterCount();
406-
const { navItems, chargerOn } = useNavItems();
406+
const { navItems } = useNavItems();
407+
/* Bottom-nav deslizable: centra el tab activo en la fila scrolleable. */
408+
const bottomNavRef = useRef<HTMLDivElement>(null);
409+
useEffect(() => {
410+
const el = bottomNavRef.current;
411+
const activeEl = el?.querySelector('[data-active="true"]');
412+
if (el && activeEl) {
413+
activeEl.scrollIntoView({ inline: 'center', block: 'nearest' });
414+
}
415+
}, [pathname]);
407416
const resolvedTitleKey = titleKey === 'nav.inversores' ? invLabelKey(count) : titleKey;
408417
const lgMargin = collapsed ? 'lg:ml-16' : 'lg:ml-[232px]';
409418

@@ -466,13 +475,21 @@ export default function AppLayout({ children }: { children: ReactNode }) {
466475
className="[view-transition-name:helios-nav] fixed bottom-0 left-0 right-0 z-50 border-t border-app bg-surface/85 pb-safe backdrop-blur-[16px] md:hidden"
467476
aria-label={t('nav.dashboard')}
468477
>
469-
{/* Sin Ajustes: en móvil el acceso es el avatar del header (→ /ajustes) */}
470-
<div className={cn('grid h-16', chargerOn ? 'grid-cols-5' : 'grid-cols-4')}>
478+
{/* Fila deslizable (keynest #284 / deltos #290 / netpulse #1312):
479+
min-width por tab, scrollbar oculta y auto-scroll al activo. */}
480+
<div ref={bottomNavRef} className="flex h-16 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
471481
{navItems.map(({ to, labelKey, icon: Icon }) => {
472482
const active = isActive(pathname, to);
473483
const label = t(labelKey === 'nav.inversores' ? invLabelKey(count) : labelKey);
474484
return (
475-
<NavLink key={to} to={to} onClick={handleMobileNav(to)} className="relative flex flex-col items-center justify-center gap-1" aria-label={label}>
485+
<NavLink
486+
key={to}
487+
to={to}
488+
onClick={handleMobileNav(to)}
489+
data-active={active ? 'true' : undefined}
490+
className="relative flex min-w-[64px] flex-1 flex-col items-center justify-center gap-1"
491+
aria-label={label}
492+
>
476493
<motion.span
477494
animate={active ? { scale: [1, 1.15, 1] } : { scale: 1 }}
478495
transition={{ duration: 0.25, type: 'spring', stiffness: 500, damping: 20 }}
@@ -486,9 +503,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
486503
);
487504
})}
488505
</div>
489-
</nav>
490-
491-
<HeliosToaster />
506+
</nav> <HeliosToaster />
492507
</div>
493508
);
494509
}

0 commit comments

Comments
 (0)