# 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:

1. **Carry-over de Etapa 1:** `WorkspaceSkeleton.tsx` fue creado y está bien implementado, pero no aparece en la práctica — el spinner del router intercepta antes. Corregir la condición de render en `WorkspacePage.tsx`.
2. **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í:

```tsx
// 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:

```ts
canvas.zoom('fit-viewport')   // ← sin segundo argumento
```

El load inicial correcto usa:

```ts
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'`:

```ts
// 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:**
```ts
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-in` para +, `ti-zoom-out` para −
- 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

1. Fix 0 — `WorkspacePage.tsx`: renderizar `WorkspaceSkeleton` cuando `isLoading === true`
2. Fix 1 — `BpmnCanvas.tsx`: agregar `'auto'` al botón fit-viewport
3. Fix 2 — `BpmnCanvas.tsx`: agregar botones zoom-in y zoom-out en esquina inferior derecha
4. `npm run build` — 0 errores TypeScript
5. `npm run lint` — 0 warnings nuevos
6. `npm run test` — mantener 521+ tests Vitest verdes
7. `npx playwright test tests/e2e/sprint-4-smoke.spec.ts` — 9/9 verdes
8. 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
