Sprint 5 — Etapa 2: resumen
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
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.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { useEffect, useRef, useCallback, type CSSProperties } from 'react'
|
||||
import BpmnViewer from 'bpmn-js/lib/Viewer'
|
||||
import { Maximize2 } from 'lucide-react'
|
||||
import { Maximize2, ZoomIn, ZoomOut } from 'lucide-react'
|
||||
|
||||
const ZOOM_STEP = 1.2
|
||||
const ZOOM_MIN = 0.1
|
||||
const ZOOM_MAX = 4
|
||||
|
||||
export type BpmnElementCategory = 'activity' | 'gateway' | 'other'
|
||||
|
||||
@@ -158,34 +162,56 @@ export function BpmnCanvas({
|
||||
}
|
||||
}, [selectedElementId])
|
||||
|
||||
function handleZoomIn() {
|
||||
const canvas = viewerRef.current?.get('canvas') as any
|
||||
if (!canvas) return
|
||||
canvas.zoom(Math.min(canvas.zoom() * ZOOM_STEP, ZOOM_MAX))
|
||||
}
|
||||
|
||||
function handleZoomOut() {
|
||||
const canvas = viewerRef.current?.get('canvas') as any
|
||||
if (!canvas) return
|
||||
canvas.zoom(Math.max(canvas.zoom() / ZOOM_STEP, ZOOM_MIN))
|
||||
}
|
||||
|
||||
const controlButtonStyle: CSSProperties = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
background: 'hsl(var(--background))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
|
||||
|
||||
{/* Encuadrar — esquina superior derecha */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const canvas = viewerRef.current?.get('canvas') as any
|
||||
canvas?.zoom('fit-viewport')
|
||||
canvas?.zoom('fit-viewport', 'auto')
|
||||
}}
|
||||
title="Encuadrar diagrama"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
background: 'hsl(var(--background))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
style={{ position: 'absolute', top: 12, right: 12, zIndex: 10, ...controlButtonStyle }}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Zoom in/out — esquina inferior derecha, no interfiere con el botón de encuadre */}
|
||||
<div style={{ position: 'absolute', bottom: 12, right: 12, zIndex: 10, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<button onClick={handleZoomIn} title="Acercar" style={controlButtonStyle}>
|
||||
<ZoomIn className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
<button onClick={handleZoomOut} title="Alejar" style={controlButtonStyle}>
|
||||
<ZoomOut className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -151,7 +151,9 @@ export function WorkspacePage() {
|
||||
|
||||
|
||||
|
||||
if (isLoading && !currentProcess) {
|
||||
// isLoading manda siempre, incluso si currentProcess todavía tiene el proceso
|
||||
// anterior en el store (navegación directa entre dos workspaces distintos).
|
||||
if (isLoading) {
|
||||
return <WorkspaceSkeleton />
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user