Menú lateral (Default Layout)¶
Autor: Yadilton Torres
Selector: app-dashboard
Ubicación: SitioLogiGho/src/app/layout/default-layout
¿Qué hace?¶
Es la estructura que envuelve todas las pantallas internas de LogiGho: el menú lateral (sidebar), la barra superior (header), el contenido de la página (router-outlet), el botón de WhatsApp, la barra flotante y el footer.
Es la estructura que envuelve todas las pantallas internas de LogiGho: el menú lateral (sidebar), la barra superior (header), el contenido de la página (router-outlet), el botón de WhatsApp, la barra flotante y el footer.
El menú lateral se arma según los roles del usuario (colección module) y tiene:
- Apertura sin parpadeo: se abre solo si el mouse se queda encima un momento y tiene un margen antes de cerrarse.
- Clics que no se pierden: si el menú se mueve mientras se hace clic, la opción igual se abre.
- Ubicación visible: barrita azul en la sección abierta o actual, opción activa en morado y subgrupos con pastilla y línea de árbol.
- Buscador del menú con atajo Ctrl + B (Cmd + B en Mac) y resultados agrupados por sección.
- Uso con el teclado: con el menú abierto se recorre con las flechas, Enter abre submenús o va a la página y Escape cierra.
- Barrita del scroll: arrastrarla no cierra el menú aunque el mouse salga de la barra.
- Celular: el menú es un cajón completo que se cierra al elegir una opción o deslizando el dedo a la izquierda.
Roles y acceso¶
| Acceso | Descripción |
|---|---|
| Autenticado | Requiere login. Es el layout de todas las rutas bajo /app con AuthGuard |
| Opciones del menú | Solo los módulos de la colección module que corresponden a los roles del usuario (ver Rutas y navegación) |
Estructura de archivos¶
default-layout/
├── default-layout.component.ts → Arma el menú, valida acceso a módulos y hace la búsqueda
├── default-layout.component.html → Sidebar, buscador, header, contenido y footer
├── default-layout.component.scss → Solo importa los archivos de styles/ (10 líneas)
├── directives/
│ ├── sidebar-interaccion.directive.ts → Mouse, clics, Ctrl + B, flechas, scroll y cajón del celular
│ └── sidebar-interaccion.directive.spec.ts → 33 pruebas del comportamiento del menú
├── styles/
│ ├── _variables.scss → Tiempos, curvas de animación y corte de celular
│ ├── _menu.scss → Colores, opciones, iconos, barrita azul, activo, grupos, subgrupos y marca del teclado
├── default-layout.component.scss → Solo importa los archivos de estilos/ (10 líneas)
├── sidebar-interaccion.directive.ts → Mouse, clics, Ctrl + B y cajón del celular
├── sidebar-interaccion.directive.spec.ts → 15 pruebas del comportamiento del menú
├── estilos/
│ ├── _variables.scss → Tiempos, curvas de animación y corte de celular
│ ├── _menu.scss → Colores, opciones, iconos, barrita azul, activo, grupos y subgrupos
│ ├── _barra.scss → Ancho cerrada/abierta y apertura por capas (logo, buscador, textos, flechas)
│ ├── _buscador.scss → Campo, etiqueta Ctrl B, "Sin resultados" y resultados agrupados
│ ├── _movil.scss → Cajón del celular
│ ├── _animaciones.scss → Fundidos, cascadas, brillo del activo y "reducir movimiento"
│ └── _pagina.scss → Contenedor, cuerpo y botón de WhatsApp (ya existía)
└── default-header/ → Barra superior
└── default-header/ → Barra superior (ver su página)
Decoradores y propiedades clave¶
| Elemento | Nombre | Tipo | Descripción |
|---|---|---|---|
@ViewChild |
sidebarInteraccion |
SidebarInteraccionDirective |
Directiva del sidebar. El layout la usa para cerrar la barra cuando terminan de cargar los módulos |
| Propiedad | navItems |
INavData[] |
Menú completo, armado desde la colección module según los roles |
| Propiedad | visibleNavItems |
INavData[] |
Resultados del buscador: igual a navItems si no hay nada escrito |
| Propiedad | searchTerm |
string |
Texto escrito en el buscador del menú |
El <c-sidebar-nav> pinta navItems cuando la barra está cerrada (interaccion.mostrarMenuCompleto) y visibleNavItems cuando está abierta. Ver Barra cerrada con búsqueda.
| Propiedad | visibleNavItems | INavData[] | Lo que se pinta: igual a navItems, o los resultados si hay búsqueda |
| Propiedad | searchTerm | string | Texto escrito en el buscador del menú |
Servicios y endpoints¶
| Servicio | Método | Endpoint | Cuándo |
|---|---|---|---|
ConsumoGenericoService |
consultarGenerico() |
users (por email) |
Al iniciar: guarda tiendas_asignadas, roles_asignados y tiendas_id en sessionStorage |
ConsumoGenericoService |
consultarGenerico() |
module |
Al iniciar: módulos del menú según los roles |
ConsumoGenericoService |
consultarGenerico() |
metodoGenerico?coleccion=Tienda |
Revisa si las tiendas del usuario están inactivas para mostrar el aviso |
SidebarService (CoreUI) |
toggle() |
— | La directiva cierra el cajón del celular |
Directiva SidebarInteraccionDirective¶
Todo el comportamiento del menú con el mouse, el teclado y el dedo vive en esta directiva, no en el layout. Se pone en el <c-sidebar> con el atributo appSidebarInteraccion; el layout solo arma el menú y hace la búsqueda.
| Elemento | Nombre | Descripción |
|---|---|---|
@HostBinding |
class.abierta |
Pone la clase abierta que usa el SCSS para abrir la barra (en vez de :hover) |
@Output |
limpiarBusqueda |
Avisa al layout que borre lo escrito en el buscador (Escape o Ctrl + B para cerrar) |
@ContentChild |
menuSearchInput |
El campo del buscador, para ponerle el cursor con Ctrl + B |
| Propiedad | atajoTexto / atajoAria |
Texto del atajo según el sistema: Ctrl B o ⌘ B |
Abrir y cerrar con el mouse¶
| Constante | Valor | Para qué |
|---|---|---|
ESPERA_ABRIR_MS |
150 ms | La barra solo se abre si el mouse se queda encima este tiempo. Una pasada rápida no la mueve |
MARGEN_CERRAR_MS |
120 ms | Margen de perdón: si el mouse sale y vuelve antes, la barra sigue abierta |
DURACION_ENCOGER_MS |
450 ms | Si ya empezó a cerrarse, termina de cerrarse y después se vuelve a abrir. Debe ser igual a $sidebar-duracion de estilos/_variables.scss |
- Al presionar el mouse sobre la barra cerrada se abre al instante, sin la espera: el usuario ya decidió usar el menú.
- Si el usuario está escribiendo en el buscador, sacar el mouse no cierra la barra.
- Los submenús abiertos se cierran cuando la barra termina de encogerse, con el mismo clic de CoreUI. Así su estado interno queda al día y el siguiente clic siempre abre (se corrigió el "primer clic gastado").
Clic que no se pierde¶
El navegador solo cuenta un clic si se presiona y se suelta sobre el mismo elemento. Si el menú se mueve mientras el botón está presionado (baja el buscador o se cierra otro grupo), el clic se perdía.
Flujo:
1. mousedown: se anota la opción presionada y el punto donde se presionó.
2. click: si el clic del navegador sí llegó a esa opción, se marca como recibido.
3. mouseup: si el clic no llegó y el mouse se movió 10 px o menos (MOVIMIENTO_MAX_PX), se completa el clic sobre la opción anotada. Si se movió más, fue el usuario que se arrepintió y no se hace nada.
Atajo Ctrl + B (Cmd + B en Mac)¶
Flujo: 1. Solo en escritorio. En celular el atajo no hace nada. 2. No se activa escribiendo en un editor con formato, porque ahí Ctrl + B es negrita. 3. Se evita la acción del navegador para esa combinación (en Firefox abre el panel de marcadores). 4. Si la barra ya está abierta con el foco adentro, el atajo la cierra (igual que Escape). 5. Si no, abre la barra al instante y pone el cursor en el buscador con el texto seleccionado.
- Escape en el buscador borra lo escrito, cierra la barra y devuelve el foco a donde estaba antes del atajo.
- Abierta con el teclado, la barra se cierra cuando el foco sale de ella, salvo que el mouse siga encima.
Celular¶
| Constante | Valor | Para qué |
|---|---|---|
CORTE_MOVIL |
(max-width: 767.98px) |
Corte de celular, el mismo de CoreUI y de $corte-movil del SCSS. Ahí no aplica nada del mouse: el toque del dedo contaba como "mouse que entra" y obligaba a tocar dos veces |
DESLIZAR_MIN_PX |
60 px | Deslizar a la izquierda más que esto (y mucho más en horizontal que en vertical) cierra el cajón. Hacer scroll en el menú no lo cierra |
- Al navegar a otra pantalla el cajón se cierra solo.
- El cajón mide
min(300px, 85vw)y sale completo de una vez (estilos/_movil.scss).
Buscador del menú¶
Está en el layout (onSearch() y searchNavItems()).
- Sin texto muestra el menú completo.
- Con texto devuelve una lista plana de las opciones que coinciden por nombre, para no depender de abrir y cerrar grupos mientras se busca. Si coincide un grupo, entran todos sus submenús.
- La comparación ignora tildes y mayúsculas (
normalizeText()). - Los resultados van agrupados bajo un título con la ruta de donde vienen, por ejemplo "Mi Tienda › Pedidos Masivos" (clase
nav-title-busqueda). Debajo van sus opciones sin icono y con sangría (clasenav-item-busqueda). Las opciones sueltas del primer nivel conservan su icono. - Si no hay coincidencias se muestra Sin resultados para "…".
- La etiqueta
Ctrl Ba la derecha del campo recuerda el atajo; se oculta al escribir y en pantallas táctiles.
Diseño y animaciones¶
| Qué | Cómo se ve |
|---|---|
| Barra en escritorio | Cerrada mide 80 px (solo iconos) y abierta 300 px. Se abre en 0.45 s |
| Apertura por capas | Primero se ensancha mientras el logo se despliega con una máscara (cerrada solo se ve el icono), después entran el buscador y los textos en cascada y al final las flechas de los grupos |
| Sección actual | Barrita azul a la izquierda del principal abierto o que contiene la página actual; crece con un pequeño rebote |
| Opción activa | En morado, con un brillo suave |
| Subgrupos | Pastilla, puntito y línea tipo árbol |
| Barra cerrada | Rayitas separadoras entre principales |
| Traductor | translate="no" en el sidebar: el traductor del navegador no cambia los nombres del menú |
| Menos movimiento | Se respeta la opción "reducir movimiento" del sistema (prefers-reduced-motion) |
Los valores que se repiten están en estilos/_variables.scss:
| Variable | Valor | Uso |
|---|---|---|
$sidebar-duracion |
0.45s |
Tiempo de abrir y cerrar. Debe coincidir con DURACION_ENCOGER_MS de la directiva |
$curva-abrir |
cubic-bezier(0.22, 1, 0.36, 1) |
Arranca con impulso y frena suave |
$curva-cerrar |
cubic-bezier(0.4, 0, 0.2, 1) |
Movimiento parejo para cerrar |
$curva-rebote |
cubic-bezier(0.34, 1.56, 0.64, 1) |
Barrita azul, iconos y flechas |
$corte-movil / $corte-escritorio |
767.98px / 768px |
Corte entre celular y escritorio. Debe coincidir con CORTE_MOVIL de la directiva |
Flujo principal¶
Usuario entra a una ruta bajo /app
→ DefaultLayoutComponent
→ consulta users → guarda tiendas y roles en sessionStorage
(si /users no trae al usuario o falla → metodoGenerico?coleccion=Users)
→ revisarAccesos() → formulario Accesos en LOGI y luego la guía de Red social
→ consulta module → buildNavItems() según roles → navItems / visibleNavItems
→ validateModuleAccess() → si no tiene acceso a la ruta, va a login
→ cierra la barra (directiva)
Escritorio
Mouse encima 150 ms → clase "abierta" → apertura por capas
Mouse sale → espera 120 ms → se encoge en 0.45 s → cierra submenús
Ctrl + B → abre al instante → cursor en el buscador
Escribir → onSearch() → resultados agrupados por sección
Escape → borra, cierra y devuelve el foco
Celular
☰ del header → cajón completo
Elegir opción → navega y cierra el cajón
Deslizar a la izquierda → cierra el cajón
Carga del usuario¶
fetchUsers() consulta users?email=<correo> y guarda en sessionStorage tiendas_asignadas, roles_asignados y tiendas_id. Con eso arma datosBienvenida (nombre, correo, teléfono, documento, autorizaDatos y tieneTienda), verifica el estado de las tiendas, carga los módulos y llama a verificarPermisos().
Respaldo: si /users no trae al usuario o responde con error, se busca con metodoGenerico?coleccion=Users&email=<correo> (usuarioPorGenerico). Sin esto, la sesión se quedaba con los roles de la cuenta anterior y no se revisaban permisos ni el formulario.
Formulario Accesos en LOGI y guía de Red social¶
verificarPermisos() llama a revisarAccesos():
¿tiene UN solo rol y es "Red social"? ──no──► nada (ni formulario ni guía)
│ sí
¿sessionStorage accesos_logi_visto = true? ──sí──► guía
│ no
RegisterContact.fechaAccesos existe? ──sí──► marca visto ► guía
│ no (o la consulta falló)
formulario obligatorio ──al terminar──► cerrarAccesos() ► marca visto ► guía
- La marca
accesos_logi_vistosolo se pone cuando de verdad lo completó: mientras no lo llene, sale en cada recarga y en cada inicio de sesión. - La guía (
app-bienvenida-red-social) también sale solo con el rol único Red social, una vez por sesión, y nunca encima del formulario. abrirAccesos()lo abre desde el menú de la cuenta del header.
Ver Accesos en LOGI.
Pruebas¶
sidebar-interaccion.directive.spec.ts tiene 15 pruebas:
- Pasar el mouse rápido no abre la barra; dejarlo encima la abre a los 150 ms.
- Margen de perdón de 120 ms y "termina de cerrar antes de volver a abrir".
- Presionar el mouse con la barra cerrada la abre al instante.
- Clic que no se pierde: se completa si no llegó, no se repite si sí llegó y no se completa si el mouse se arrastró más de 10 px.
- Ctrl + B abre y enfoca el buscador; con el foco adentro la cierra; no hace nada en un editor con formato.
- Escape cierra, limpia y devuelve el foco.
- Celular: el toque no abre la barra ni Ctrl + B hace nada; deslizar a la izquierda cierra el cajón; scroll o toque corto no lo cierran.
npx ng test --include=src/app/layout/default-layout/sidebar-interaccion.directive.spec.ts --watch=false --browsers=ChromeHeadless
Historial de cambios¶
| Fecha | Autor | Cambio |
|---|---|---|
| 2026-11-11 | Yadilton Torres | Formulario Accesos en LOGI antes de la guía de Red social; respaldo de la carga del usuario por metodoGenerico; Panel comercial en el menú |
| 2026-10-09 | Yadilton Torres | Rediseño del menú lateral: apertura sin parpadeo con la clase abierta, clic que no se pierde, barrita azul y subgrupos, buscador del menú con Ctrl + B y resultados agrupados, cajón completo en celular. El comportamiento pasó a la directiva SidebarInteraccionDirective y los estilos a la carpeta estilos/ (7 archivos por tema). |
Observaciones¶
-
Panel comercial en el menú (TEMPORAL):
/app/comercial/panel-comercialestá agregado a mano en el componente porque aún no está en la colecciónmodule. Su guard deja entrar solo a COO, CEO y Desarrollador. Cuando se registre el módulo, se quita esa línea. -
Dos valores deben ir sincronizados entre el TS y el SCSS: la duración (
DURACION_ENCOGER_MS=$sidebar-duracion) y el corte de celular (CORTE_MOVIL=$corte-movil). Si se cambia uno, cambiar el otro. - El orden de los
@useendefault-layout.component.scssse respeta al armar el CSS. Hoy ninguna regla de un archivo pisa a otra de otro archivo; si se agrega una que lo haga, tenerlo en cuenta. - La apertura no usa
:hoverde CoreUI (barraunfoldable): en escritorio esas reglas quedan neutralizadas mientras la barra no tenga la claseabierta. sidebar-nav.scssestá vacío y no se usa.