fix: version 2.4.0 -- menu lateral como drawer en celular

En pantallas angostas (<=768px) el sidebar arranca oculto y se abre
como panel superpuesto desde el boton de hamburguesa, con fondo
oscuro detras y cierre automatico al navegar o tocar afuera. El
escritorio no se toca: sigue igual que siempre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-22 13:20:30 -05:00
parent ea0feb7e3a
commit 9b20f85ae0
7 changed files with 61 additions and 10 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog - project.linkedcorp.net # Changelog - project.linkedcorp.net
## [2.4.0] - 2026-09-22
### Fixed
- Menú lateral en celular: antes quedaba siempre visible fijo, ocupando gran parte de una
pantalla angosta. Ahora arranca oculto, se abre como un panel superpuesto (con fondo oscuro
detrás) desde el botón de hamburguesa, y se cierra solo al elegir una opción o tocar afuera.
El escritorio no cambia — sigue con el sidebar fijo y el modo compacto de siempre.
## [2.3.0] - 2026-09-22 ## [2.3.0] - 2026-09-22
### Added ### Added
- Al iniciar sesión, si sos superadmin y hay una actualización pendiente, aparece un aviso con - Al iniciar sesión, si sos superadmin y hay una actualización pendiente, aparece un aviso con
+1 -1
View File
@@ -1 +1 @@
2.3.0 2.4.0
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "project-linkedcorp-backend", "name": "project-linkedcorp-backend",
"version": "2.3.0", "version": "2.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/server.js", "main": "src/server.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "project-linkedcorp-frontend", "name": "project-linkedcorp-frontend",
"private": true, "private": true,
"version": "2.3.0", "version": "2.4.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+17 -2
View File
@@ -16,8 +16,11 @@ import { getUpdateStatus } from '../services/updater.js';
const EMPTY_PASSWORD_FORM = { currentPassword: '', newPassword: '', confirmPassword: '' }; const EMPTY_PASSWORD_FORM = { currentPassword: '', newPassword: '', confirmPassword: '' };
const MOBILE_BREAKPOINT = 768;
export default function AppShell() { export default function AppShell() {
const [navMode, setNavMode] = useState('expanded'); const [navMode, setNavMode] = useState('expanded');
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { t, theme, setTheme, lang, setLang, pushToast, openDialog } = useUi(); const { t, theme, setTheme, lang, setLang, pushToast, openDialog } = useUi();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -54,8 +57,19 @@ export default function AppShell() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// En celular el boton de hamburguesa abre/cierra el drawer superpuesto (mobileNavOpen); en
// escritorio, sigue alternando el riel expandido/compacto de siempre -- son dos estados
// independientes para no tocar el comportamiento de escritorio ya existente.
function toggleNav() { function toggleNav() {
setNavMode((m) => (m === 'expanded' ? 'compact' : 'expanded')); if (window.innerWidth <= MOBILE_BREAKPOINT) {
setMobileNavOpen((v) => !v);
} else {
setNavMode((m) => (m === 'expanded' ? 'compact' : 'expanded'));
}
}
function closeMobileNav() {
setMobileNavOpen(false);
} }
async function handleNotificationClick(notification) { async function handleNotificationClick(notification) {
@@ -92,7 +106,8 @@ export default function AppShell() {
return ( return (
<div className="app-shell"> <div className="app-shell">
<NavigationView mode={navMode} /> <NavigationView mode={navMode} mobileOpen={mobileNavOpen} onNavigate={closeMobileNav} />
<div className={`nav-backdrop ${mobileNavOpen ? 'visible' : ''}`} onClick={closeMobileNav} />
<div className="content-column"> <div className="content-column">
<div className="topbar"> <div className="topbar">
<div className="topbar-left"> <div className="topbar-left">
+7 -5
View File
@@ -17,15 +17,17 @@ const ITEMS = [
{ to: '/configuracion', icon: Settings, labelKey: 'nav_config', superadminOnly: true }, { to: '/configuracion', icon: Settings, labelKey: 'nav_config', superadminOnly: true },
]; ];
export function NavigationView({ mode }) { export function NavigationView({ mode, mobileOpen, onNavigate }) {
const { t, theme } = useUi(); const { t, theme } = useUi();
const { user } = useAuth(); const { user } = useAuth();
const branding = useBranding(); const branding = useBranding();
const compact = mode === 'compact'; // En celular el drawer siempre se ve completo (con etiquetas), sin importar el modo
// compacto/expandido del riel de escritorio -- son dos cosas independientes.
const compact = mode === 'compact' && !mobileOpen;
const items = ITEMS.filter((item) => !item.superadminOnly || user?.roleCode === 'superadmin'); const items = ITEMS.filter((item) => !item.superadminOnly || user?.roleCode === 'superadmin');
const brandName = branding.name || t('appName'); const brandName = branding.name || t('appName');
return ( return (
<nav className={`nav-view ${mode}`}> <nav className={`nav-view ${mode} ${mobileOpen ? 'mobile-open' : ''}`}>
{branding.logoUrl && ( {branding.logoUrl && (
<div className="nav-header"> <div className="nav-header">
<img <img
@@ -37,9 +39,9 @@ export function NavigationView({ mode }) {
)} )}
<div className="nav-items"> <div className="nav-items">
{items.map((item) => ( {items.map((item) => (
<NavLink key={item.to} to={item.to} end={item.end} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}> <NavLink key={item.to} to={item.to} end={item.end} onClick={onNavigate} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
<item.icon /> <item.icon />
{mode !== 'compact' && <span>{t(item.labelKey)}</span>} {!compact && <span>{t(item.labelKey)}</span>}
</NavLink> </NavLink>
))} ))}
</div> </div>
+27
View File
@@ -73,6 +73,33 @@ button { font-family: inherit; }
.nav-logo-icon-header { width: 72px; height: 72px; flex: none; object-fit: contain; } .nav-logo-icon-header { width: 72px; height: 72px; flex: none; object-fit: contain; }
.nav-logo-full-header { height: 64px; width: auto; max-width: 100%; object-fit: contain; } .nav-logo-full-header { height: 64px; width: auto; max-width: 100%; object-fit: contain; }
/* --- Menu lateral en celular: todo lo de acá abajo es exclusivo de pantallas angostas, no
toca nada del layout de escritorio (que sigue con el sidebar fijo de siempre). --- */
.nav-backdrop { display: none; }
@media (max-width: 768px) {
.nav-view, .nav-view.compact, .nav-view.expanded {
position: fixed;
top: 0; left: 0; bottom: 0;
width: 260px;
z-index: 60;
transform: translateX(-100%);
transition: transform .2s ease;
box-shadow: 4px 0 20px rgb(0 0 0 / .4);
}
.nav-view.mobile-open { transform: translateX(0); }
.nav-backdrop {
display: block;
position: fixed; inset: 0;
background: rgb(0 0 0 / .5);
z-index: 55;
opacity: 0; pointer-events: none;
transition: opacity .2s ease;
}
.nav-backdrop.visible { opacity: 1; pointer-events: auto; }
}
.content-column { flex: 1; display: flex; flex-direction: column; min-width: 0; } .content-column { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar { .topbar {
height: 84px; flex: none; display: flex; align-items: center; justify-content: space-between; height: 84px; flex: none; display: flex; align-items: center; justify-content: space-between;