Autor: Adalberto González¶
Fecha creacion: 2026-06-27 Estado: produccion Tipo: componente
Componente: TourGuiadoComponent¶
Selector: app-tour-guiado
Ubicación: src/app/components/tour-guiado/tour-guiado.component.ts
Acceso: Público (reutilizable en cualquier módulo autenticado)
¿Qué hace?¶
Muestra un tour interactivo paso a paso sobre cualquier vista de la aplicación. Oscurece toda la pantalla excepto el elemento que se quiere explicar, y muestra una tarjeta flotante con el título y la descripción de ese paso.
No depende de ninguna librería externa. Cada módulo que lo quiera usar solo necesita decirle qué elementos destacar y qué explicar en cada uno.
Decoradores¶
| Decorador | Nombre | Tipo | Descripción |
|---|---|---|---|
@Input |
isOpen |
boolean |
Abre o cierra el tour. Al abrirse siempre empieza desde el primer paso. |
@Input |
steps |
TourStep[] |
Array de pasos definido directamente en el componente que usa el tour. |
@Output |
closed |
EventEmitter<void> |
Se dispara cuando el usuario cierra el tour, para que el padre lo sepa. |
TourStep¶
| Campo | Tipo | Descripción |
|---|---|---|
elementId |
string |
id del elemento HTML a destacar en este paso |
title |
string |
Título corto de la tarjeta |
description |
string |
Explicación — acepta HTML simple (<b>, <br>) |
side? |
'top' \| 'bottom' \| 'left' \| 'right' |
Dónde aparece la tarjeta respecto al elemento. Default 'bottom' |
onBeforeStep? |
() => void |
Acción antes de mostrar el paso (ej. abrir un panel que contiene el elemento) |
onLeaveStep? |
() => void |
Acción al salir del paso (ej. cerrar lo que abrió onBeforeStep) |
Propiedades clave¶
| Propiedad | Tipo | Descripción |
|---|---|---|
stepsActivos |
TourStep[] |
Los pasos que realmente se muestran. Se calculan una sola vez, al abrir el tour: se filtran los elementId cuyo elemento no existe en el DOM en ese instante (ver Observaciones — implica que el elemento debe existir ya al momento de abrir, no puede depender de que un paso anterior lo cree). |
stepIndex |
number |
Paso actual, índice sobre stepsActivos |
pos |
PopoverPos \| null |
Posición calculada del spotlight y la tarjeta. null mientras se recalcula |
Atajos de teclado¶
| Tecla | Qué hace |
|---|---|
→ |
Ir al siguiente paso |
← |
Volver al paso anterior |
Escape |
Cerrar el tour |
Cómo usarlo en un módulo nuevo¶
- Pon
id="algo"en los elementos del HTML que quieras destacar. Deben existir en el DOM al momento de abrir el tour — si dependen de un*ngIf(loading, un modal, etc.), deshabilita el botón que abre el tour hasta que esa condición se cumpla. - Define un array
readonly tourSteps: TourStep[]directamente en el.tsdel componente que va a mostrar el tour (no hace falta un servicio aparte). - Agrega
TourGuiadoComponental arrayimportsdel componente raíz del módulo. - En el HTML del módulo:
<app-tour-guiado
[isOpen]="tourAbierto"
[steps]="tourSteps"
(closed)="tourAbierto = false">
</app-tour-guiado>
Módulos que lo usan¶
17 archivos en total, entre componentes raíz de vista y modales internos:
| Módulo | Ubicación |
|---|---|
| Cambio Entrega Inter | views/logistica/cambio-entrega-inter/ |
| Resumen Inventario | views/logistica/resumen-inventario/ (+ 3 modales internos) |
| Gestión Bodegas | views/logistica/gestion-bodegas/ (+ 3 modales internos) |
| Relación Despacho | views/logistica/relacion-despacho/ (+ carga masiva) |
| Guías Devoluciones | views/logistica/guias-devoluciones/ |
| Dashboard Sin Despacho | views/logistica/dashboard-sin-despacho/ |
| Pancake Estadística | views/tienda/pancake-estadistica/ |
| Gestión Páginas | views/tienda/gestion-paginas/ |
| Gestión Cuentas | views/tienda/gestion-cuentas/ |
Flujo principal¶
El padre pone isOpen = true
-> stepsActivos = steps.filter(elemento existe en el DOM AHORA)
-> si no hay ninguno, cierra inmediatamente (closed.emit())
-> stepIndex = 0
-> irAElemento(): scrollIntoView + sondeo del rect del elemento hasta
que deja de moverse (requestAnimationFrame, no un delay fijo)
-> calcPos(): calcula spotlight y tarjeta, cdr.markForCheck()
Al avanzar/retroceder (siguiente()/anterior())
-> cancela cualquier sondeo de posición anterior todavía en curso
-> repite scroll + sondeo + cálculo para el nuevo elemento
Al cerrar (X, Finalizar o Escape)
-> cancela el sondeo pendiente, limpia pos
-> closed.emit()
Si un paso tiene onBeforeStep, se ejecuta y se espera 350 ms extra antes de hacer scroll (tiempo para que un modal/panel que abre termine su animación de entrada).
Historial de cambios¶
| Fecha | Autor | Cambio |
|---|---|---|
| 2026-06-27 | Adalberto González | Creación del componente reutilizable, con filtrado automático de pasos por existencia en el DOM. |
| 2026-08-20 | Iker Acevedo | Fix de posicionamiento: el sondeo de "¿terminó el scroll?" vigilaba window.scrollY/el contenedor con scroll, en vez del elemento destino — si la página terminaba de moverse un instante antes de que el layout del elemento se asentara del todo, la posición calculada quedaba "mirando" el paso anterior. Ahora se sondea el getBoundingClientRect() del elemento destino directamente. Fix de condición de carrera: ningún sondeo de posición se cancelaba al navegar a otro paso o cerrar el tour — un sondeo viejo que terminaba tarde pisaba la posición/el estado del paso vigente (causaba "a veces avanza y a veces no", "si me devuelvo se daña", "si cierro a la mitad se desconfigura"). Se agregó cancelación real (cancelAnimationFrame) más un guard por índice de paso. Fix de compatibilidad con OnPush: el cálculo de posición corre dentro de un requestAnimationFrame, un callback que Angular no propaga automáticamente a través de un componente ancestro con ChangeDetectionStrategy.OnPush — el resultado se calculaba bien pero no se pintaba hasta el siguiente evento real, mostrando siempre la posición del paso anterior. Se inyectó ChangeDetectorRef y se agregó markForCheck() después de cada cálculo asíncrono. Responsive: en pantallas ≤480px la tarjeta se ancla fija abajo tipo bottom-sheet con scroll interno, en vez de la posición calculada (que asume una altura fija de 160px para no salirse de la ventana — en mobile, con textos largos, esa altura estimada quedaba corta y el pie con los botones "Siguiente"/"Cerrar" terminaba fuera de la pantalla, sin scroll posible). |
Observaciones¶
- El filtrado de pasos es una sola pasada, al abrir. Si un
elementIdvive dentro de un*ngIfque depende de datos cargando asíncronamente (ej. una tabla gateada porloading()), y el usuario abre el tour antes de que termine de cargar, ese paso se descarta silenciosamente y el conteo total de pasos baja — no se vuelve a evaluar más tarde. Mitigación usada encambio-entrega-inter: deshabilitar el botón que abre el tour mientrasloading()estrue. - Por el mismo motivo, no uses
onBeforeSteppara revelar un elemento que solo existe dentro de un modal*ngIf(el elemento no está en el DOM todavía en el momento del filtrado inicial, así que ese paso nunca entra astepsActivos). Si necesitas explicar contenido de un modal, apunta el paso al botón que lo abre y describe el contenido en el texto — no lo abras de verdad desde el tour. - Se construyó desde cero en lugar de usar driver.js porque el sidebar de CoreUI creaba un conflicto de capas que impedía que el overlay de driver.js se viera correctamente.
- La animación de deslizamiento entre pasos es puro CSS, sin JavaScript de animaciones.