Fix 0 (skeleton carry-over): el guard de Etapa 1 era isLoading && !currentProcess — fallaba al navegar directo entre dos workspaces distintos, porque el proceso anterior seguía en el store mientras el nuevo cargaba. Cambié a if (isLoading) return <WorkspaceSkeleton /> sin condición adicional, exactamente como pedía el brief.
Fix 1 (fit-viewport): una línea — el botón llamaba canvas.zoom('fit-viewport') sin 'auto' (la carga inicial sí lo tenía). Agregado.
Fix 2 (controles de zoom): agregué ZoomIn/ZoomOut de Lucide (no Tabler — ya está en el stack del proyecto, son visualmente equivalentes) en la esquina inferior derecha, apilados verticalmente, mismo estilo que el botón de encuadre (32×32px, fondo blanco semitransparente). Factor 1.2x, límites 0.1x–4x tal como especificaba el brief.
Verificación:
Build limpio
Lint: 2 warnings any nuevos en BpmnCanvas.tsx, consistentes con las 9 que ya existían en ese mismo archivo (el propio snippet del brief los prescribe así; bpmn-js tipa débilmente viewer.get()) — no es una categoría nueva de problema, lo marco para que quede explícito
Vitest: 521/521 verdes
E2E smoke: 9/9 verdes
Falta tu validación visual — en particular probar con Slow 3G que aparece el skeleton (no el spinner), y confirmar que el encuadre centra el diagrama y los botones de zoom no tapan nada.
6.7 KiB
Sprint 5 — Etapa 2: Fix zoom BPMN + skeleton carry-over
Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind + Supabase. Etapa 1 completada con OK formal. Workspace carga correctamente. Dos ítems pendientes de esta etapa:
- Carry-over de Etapa 1:
WorkspaceSkeleton.tsxfue creado y está bien implementado, pero no aparece en la práctica — el spinner del router intercepta antes. Corregir la condición de render enWorkspacePage.tsx. - Fix + mejora del canvas BPMN: botón fit-viewport con bug (clip al borde superior) y ausencia de controles zoom-in/zoom-out.
Fix 0 — Skeleton carry-over (Etapa 1)
Diagnóstico
WorkspaceSkeleton.tsx existe en src/features/workspace/ y está bien implementado (layout completo con animate-pulse). El problema está en WorkspacePage.tsx: el skeleton no se renderiza cuando isLoading === true en el store, porque TanStack Router muestra su propio indicador de carga antes de que WorkspacePage monte y evalúe su estado interno.
Fix
Abrir WorkspacePage.tsx (o WorkspacePageWithBoundary si así quedó tras Etapa 1) y localizar la condición de loading. Debe quedar así:
// Al inicio del componente, después de leer el store:
const { isLoading, error, process } = useProcessStore()
if (isLoading) return <WorkspaceSkeleton />
if (error) return <WorkspaceError error={error} onRetry={...} onBack={...} />
if (!process) return <WorkspaceSkeleton /> // guard adicional para estado transitorio
El objetivo es que el skeleton aparezca mientras isLoading === true desde el componente, no depender del loader de TanStack Router para mostrar feedback visual al usuario.
Si el router-level loader también muestra algo, no hay conflicto — el router puede tener su indicador mínimo (barra de progreso en el top si la hay) y el componente muestra el skeleton completo una vez montado. No eliminar el router loader si existe, solo asegurarse de que el componente también maneje su propio estado de carga con el skeleton.
Verificación: con DevTools → Network → Slow 3G, navegar desde biblioteca a proceso. Debe aparecer el skeleton (bloques grises animados preservando el layout) durante la carga, no una rueda giratoria sobre fondo en blanco.
Fix 1 — Fit-viewport: botón sin argumento 'auto'
Diagnóstico
En BpmnCanvas.tsx, el botón de encuadre llama:
canvas.zoom('fit-viewport') // ← sin segundo argumento
El load inicial correcto usa:
canvas.zoom('fit-viewport', 'auto') // ← con 'auto', centra el diagrama
Sin 'auto', bpmn-js hace fit pero sin centrar — el diagrama queda pegado al borde superior izquierdo del viewport. Con 'auto', centra el diagrama en el viewport con el zoom justo para que entre.
Fix
En el handler del botón de encuadre (botón con ícono Maximize2 en BpmnCanvas.tsx), agregar el argumento 'auto':
// Antes:
canvas.zoom('fit-viewport')
// Después:
canvas.zoom('fit-viewport', 'auto')
Una línea. No tocar el resto del componente.
Fix 2 — Controles de zoom-in y zoom-out
Contexto del layout
BpmnCanvas.tsx tiene:
- Wrapper externo:
position: relative, width: 100%, height: 100% containerRef(donde monta bpmn-js):width: 100%, height: 100%- Botón fit-viewport existente:
position: absolute, top: 12, right: 12
Los botones nuevos se posicionan sobre el canvas usando position: absolute.
Implementación
Agregar dos botones: zoom-in (+) y zoom-out (−). Junto al botón fit existente, formando un grupo de 3 controles.
Posición: esquina inferior derecha del canvas (no esquina superior — para no interferir con el botón fit existente que ya está arriba). Usar bottom: 12, right: 12.
Lógica:
const handleZoomIn = () => {
if (!viewerRef.current) return
const canvas = viewerRef.current.get('canvas') as any
const currentZoom = canvas.zoom()
canvas.zoom(Math.min(currentZoom * 1.2, 4)) // máximo zoom: 4x
}
const handleZoomOut = () => {
if (!viewerRef.current) return
const canvas = viewerRef.current.get('canvas') as any
const currentZoom = canvas.zoom()
canvas.zoom(Math.max(currentZoom / 1.2, 0.1)) // mínimo zoom: 0.1x
}
Los límites min: 0.1 y max: 4 previenen que el usuario llegue a un estado de zoom irrecuperable.
Estilo de los botones — consistente con el botón fit existente:
- Tamaño: 28×28px o 32×32px (el que use el botón fit actual)
- Fondo: blanco semitransparente con borde sutil
- Ícono Tabler:
ti-zoom-inpara +,ti-zoom-outpara − - Separación entre botones: 4px (grupo compacto)
- Disposición: verticalmente apilados (+ arriba, − abajo) o en fila horizontal — lo que quede más compacto sin solapar el diagrama
No reemplazar el zoom por scroll/pinch — esos eventos de bpmn-js siguen funcionando igual. Estos botones son controles adicionales para usuarios que no tienen scroll o prefieren click explícito.
Extraer en subcomponente si mejora la legibilidad — si el bloque de JSX del canvas empieza a crecer, considerar un <BpmnZoomControls onZoomIn={...} onZoomOut={...} onFit={...} /> separado. Solo si la extracción es natural; no forzar si el componente ya es compacto.
Qué NO entra en esta etapa
- Cambios en la lógica de simulación o cálculos
- Cambios en paneles laterales (ActivityPanel, ResourcesPanel)
- Cambios en el reporte o PDF
- Nuevas features de producto
- Cambios en auth o Supabase
Orden de ejecución
- Fix 0 —
WorkspacePage.tsx: renderizarWorkspaceSkeletoncuandoisLoading === true - Fix 1 —
BpmnCanvas.tsx: agregar'auto'al botón fit-viewport - Fix 2 —
BpmnCanvas.tsx: agregar botones zoom-in y zoom-out en esquina inferior derecha npm run build— 0 errores TypeScriptnpm run lint— 0 warnings nuevosnpm run test— mantener 521+ tests Vitest verdesnpx playwright test tests/e2e/sprint-4-smoke.spec.ts— 9/9 verdes- Commit:
fix(sprint-5/etapa-2): skeleton workspace + fit-viewport center + zoom controls
Criterios de validación
- Con Slow 3G: navegar desde biblioteca a proceso muestra skeleton (no spinner genérico) durante la carga
- Botón fit-viewport centra el diagrama con padding visible en todos los bordes (no clip al borde superior)
- Botones zoom-in y zoom-out visibles en el canvas
- Zoom-in hace la imagen más grande (factor ~1.2x por click)
- Zoom-out hace la imagen más pequeña (factor ~1.2x por click)
- Zoom por scroll/pinch sigue funcionando sin interferencia
- Los controles no tapan el diagrama ni los paneles laterales
- Build limpio, lint sin warnings nuevos, 521+ tests Vitest verdes
- Validación visual del director antes del OK formal