Responsividad correcta para el wizard

This commit is contained in:
2025-12-31 14:32:55 -06:00
parent f535eea085
commit 6a2a4c0f05

View File

@@ -2,6 +2,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'
import * as Icons from 'lucide-react' import * as Icons from 'lucide-react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { CircularProgress } from '@/components/CircularProgress'
import { defineStepper } from '@/components/stepper' import { defineStepper } from '@/components/stepper'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -19,6 +20,12 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
export const Route = createFileRoute('/planes/_lista/nuevo')({ export const Route = createFileRoute('/planes/_lista/nuevo')({
component: NuevoPlanModal, component: NuevoPlanModal,
@@ -251,7 +258,12 @@ function NuevoPlanModal() {
1. h-[90vh]: Altura fija del 90% de la ventana. 1. h-[90vh]: Altura fija del 90% de la ventana.
2. flex-col gap-0 p-0: Quitamos espacios automáticos para control manual. 2. flex-col gap-0 p-0: Quitamos espacios automáticos para control manual.
*/} */}
<DialogContent className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"> <DialogContent
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
onInteractOutside={(e) => {
e.preventDefault()
}}
>
{role !== 'JEFE_CARRERA' ? ( {role !== 'JEFE_CARRERA' ? (
// --- VISTA SIN PERMISOS --- // --- VISTA SIN PERMISOS ---
<> <>
@@ -283,21 +295,74 @@ function NuevoPlanModal() {
initialStep={Wizard.utils.getFirst().id} initialStep={Wizard.utils.getFirst().id}
className="flex h-full flex-col" className="flex h-full flex-col"
> >
{({ methods }) => ( {({ methods }) => {
// --- LÓGICA PARA MÓVIL (Cálculos) ---
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
const totalSteps = Wizard.steps.length
const nextStep = Wizard.steps[currentIndex]
return (
<> <>
{/* 1. HEADER (FIJO - Top Bun) {/* =========================================================
flex-none: Asegura que no se aplaste ni haga scroll. HEADER RESPONSIVO
*/} ========================================================= */}
<div className="z-10 flex-none border-b bg-white"> <div className="z-10 flex-none border-b bg-white">
<DialogHeader className="p-6 pb-4"> <div className="flex items-center justify-between p-6 pb-4">
<DialogTitle>Nuevo plan de estudios</DialogTitle> <DialogHeader className="p-0">
{' '}
{/* Quitamos padding interno porque ya lo tiene el div padre */}
<DialogTitle /* className="hidden sm:block" */>
Nuevo plan de estudios
</DialogTitle>
</DialogHeader> </DialogHeader>
{/* Navegación dentro del bloque fijo */} {/* BOTÓN DE CERRAR (X) */}
<div className="px-6 pb-6"> <button
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2"> onClick={handleClose}
{/* Nota: En móvil esto se verá apretado. Considera overflow-x-auto si tienes muchos pasos */} className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"
>
<Icons.X className="h-4 w-4" />
<span className="sr-only">Cerrar</span>
</button>
</div>
<div className="px-6 pb-6">
{/* OPCIÓN A: MÓVIL (< 640px)
- Usa block sm:hidden
- Muestra el CircularProgress y Texto Grande
*/}
<div className="block sm:hidden">
<div className="flex items-center gap-5">
<CircularProgress
current={currentIndex}
total={totalSteps}
/>
<div className="flex flex-col justify-center">
<h2 className="text-lg font-bold text-slate-900">
{/* Usamos el Helper del Tooltip aquí también */}
<StepWithTooltip
title={methods.current.title}
desc={methods.current.description}
/>
</h2>
{nextStep ? (
<p className="text-sm text-slate-400">
Siguiente: {nextStep.title}
</p>
) : (
<p className="text-sm font-medium text-green-500">
¡Último paso!
</p>
)}
</div>
</div>
</div>
{/* OPCIÓN B: DESKTOP (>= 640px)
- Usa hidden sm:block
- Muestra la navegación original horizontal
*/}
<div className="hidden sm:block">
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
{Wizard.steps.map((step) => ( {Wizard.steps.map((step) => (
<Wizard.Stepper.Step <Wizard.Stepper.Step
key={step.id} key={step.id}
@@ -305,13 +370,18 @@ function NuevoPlanModal() {
className="whitespace-nowrap" className="whitespace-nowrap"
> >
<Wizard.Stepper.Title> <Wizard.Stepper.Title>
{step.title} {/* Envolvemos el título en nuestro componente Tooltip */}
<StepWithTooltip
title={step.title}
desc={step.description}
/>
</Wizard.Stepper.Title> </Wizard.Stepper.Title>
</Wizard.Stepper.Step> </Wizard.Stepper.Step>
))} ))}
</Wizard.Stepper.Navigation> </Wizard.Stepper.Navigation>
</div> </div>
</div> </div>
</div>
{/* 2. CONTENIDO (SCROLLEABLE - Meat) {/* 2. CONTENIDO (SCROLLEABLE - Meat)
flex-1: Ocupa todo el espacio restante. flex-1: Ocupa todo el espacio restante.
@@ -383,18 +453,24 @@ function NuevoPlanModal() {
onClick={() => methods.next()} onClick={() => methods.next()}
disabled={ disabled={
wizard.isLoading || wizard.isLoading ||
(Wizard.utils.getIndex(methods.current.id) === 0 && (Wizard.utils.getIndex(methods.current.id) ===
0 &&
!canContinueDesdeModo) || !canContinueDesdeModo) ||
(Wizard.utils.getIndex(methods.current.id) === 1 && (Wizard.utils.getIndex(methods.current.id) ===
1 &&
!canContinueDesdeBasicos) || !canContinueDesdeBasicos) ||
(Wizard.utils.getIndex(methods.current.id) === 2 && (Wizard.utils.getIndex(methods.current.id) ===
2 &&
!canContinueDesdeDetalles) !canContinueDesdeDetalles)
} }
> >
Siguiente Siguiente
</Button> </Button>
) : ( ) : (
<Button onClick={crearPlan} disabled={wizard.isLoading}> <Button
onClick={crearPlan}
disabled={wizard.isLoading}
>
Crear plan Crear plan
</Button> </Button>
)} )}
@@ -402,7 +478,8 @@ function NuevoPlanModal() {
</Wizard.Stepper.Controls> </Wizard.Stepper.Controls>
</div> </div>
</> </>
)} )
}}
</Wizard.Stepper.Provider> </Wizard.Stepper.Provider>
)} )}
</DialogContent> </DialogContent>
@@ -410,6 +487,33 @@ function NuevoPlanModal() {
) )
} }
function StepWithTooltip({ title, desc }: { title: string; desc: string }) {
const [isOpen, setIsOpen] = useState(false)
return (
<TooltipProvider delayDuration={0}>
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
<TooltipTrigger asChild>
<span
className="cursor-help decoration-dotted underline-offset-4 hover:underline"
onClick={(e) => {
e.stopPropagation() // Evita que el clic dispare la navegación del stepper si está dentro de un botón
setIsOpen((prev) => !prev)
}}
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
{title}
</span>
</TooltipTrigger>
<TooltipContent className="max-w-[200px] text-xs">
<p>{desc}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
// Paso 1: selección de modo // Paso 1: selección de modo
function PasoModo({ function PasoModo({
wizard, wizard,
@@ -477,7 +581,7 @@ function PasoModo({
<CardDescription>Desde un plan existente o archivos.</CardDescription> <CardDescription>Desde un plan existente o archivos.</CardDescription>
</CardHeader> </CardHeader>
{wizard.modoCreacion === 'CLONADO' && ( {wizard.modoCreacion === 'CLONADO' && (
<CardContent className="flex gap-3"> <CardContent className="flex flex-wrap gap-3">
<Button <Button
variant={isSubSelected('INTERNO') ? 'default' : 'secondary'} variant={isSubSelected('INTERNO') ? 'default' : 'secondary'}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
@@ -515,7 +619,7 @@ function PasoBasicos({
}) { }) {
return ( return (
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2"> <div className="grid gap-1 sm:col-span-2">
<Label htmlFor="nombrePlan">Nombre del plan</Label> <Label htmlFor="nombrePlan">Nombre del plan</Label>
<Input <Input
id="nombrePlan" id="nombrePlan"
@@ -530,7 +634,7 @@ function PasoBasicos({
/> />
</div> </div>
<div> <div className="grid gap-1">
<Label htmlFor="facultad">Facultad</Label> <Label htmlFor="facultad">Facultad</Label>
<select <select
id="facultad" id="facultad"
@@ -556,7 +660,7 @@ function PasoBasicos({
</select> </select>
</div> </div>
<div> <div className="grid gap-1">
<Label htmlFor="carrera">Carrera</Label> <Label htmlFor="carrera">Carrera</Label>
<select <select
id="carrera" id="carrera"
@@ -578,7 +682,7 @@ function PasoBasicos({
</select> </select>
</div> </div>
<div> <div className="grid gap-1">
<Label htmlFor="nivel">Nivel</Label> <Label htmlFor="nivel">Nivel</Label>
<select <select
id="nivel" id="nivel"
@@ -600,7 +704,7 @@ function PasoBasicos({
</select> </select>
</div> </div>
<div> <div className="grid gap-1">
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label> <Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
<select <select
id="tipoCiclo" id="tipoCiclo"
@@ -624,7 +728,7 @@ function PasoBasicos({
</select> </select>
</div> </div>
<div> <div className="grid gap-1">
<Label htmlFor="numCiclos">Número de ciclos</Label> <Label htmlFor="numCiclos">Número de ciclos</Label>
<Input <Input
id="numCiclos" id="numCiclos"