From bfafb8999ee0fa303d906db1c350c54a9eb8cecd Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 26 Mar 2026 12:54:12 -0600 Subject: [PATCH 1/5] Cambios a la card de plan de estudio --- src/components/planes/PlanEstudiosCard.tsx | 65 ++++++++++------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/src/components/planes/PlanEstudiosCard.tsx b/src/components/planes/PlanEstudiosCard.tsx index 17c1799..3cd31ef 100644 --- a/src/components/planes/PlanEstudiosCard.tsx +++ b/src/components/planes/PlanEstudiosCard.tsx @@ -4,20 +4,17 @@ import type { LucideIcon } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card' -import { cn } from '@/lib/utils' // Asegúrate de tener tu utilidad cn +import { cn } from '@/lib/utils' interface PlanEstudiosCardProps { - /** El componente del ícono importado de lucide-react (ej. BookOpen) */ Icono: LucideIcon nombrePrograma: string nivel: string - ciclos: string | number // Acepta "8" o "8 semestres" + ciclos: string | number facultad: string estado: string - /** Código hex o variable CSS (ej. "#ef4444" o "var(--primary)") */ claseColorEstado?: string colorFacultad: string - /** Opcional: para manejar el click en la tarjeta */ onClick?: () => void } @@ -36,45 +33,39 @@ export default function PlanEstudiosCard({ - - {/* Ícono con el color de la facultad */} -
- -
+
+ + {/* Círculo del ícono con el color de la facultad */} +
+ +
- {/* Título del Programa */} -

- {nombrePrograma} -

-
+ {/* Título del Programa */} +

+ {nombrePrograma} +

+ - -

- {nivel} • {ciclos} -

-

{facultad}

-
+ +

+ {nivel} • {ciclos} +

+

{facultad}

+
+
- - + + {estado} - {/* - {estado} - */} {/* Flecha animada */}
Date: Thu, 26 Mar 2026 14:11:09 -0600 Subject: [PATCH 2/5] Espaciado ahora es consistente --- .../asignaturas/detalle/AsignaturaDetailPage.tsx | 2 +- .../asignaturas/detalle/BibliographyItem.tsx | 2 +- .../asignaturas/detalle/ContenidoTematico.tsx | 2 +- .../wizard/PasoFuenteClonadoInterno.tsx | 2 +- src/components/wizard/WizardLayout.tsx | 6 +++--- src/routes/dashboard.tsx | 2 +- src/routes/planes/$planId/_detalle.tsx | 2 +- src/routes/planes/$planId/_detalle/asignaturas.tsx | 2 +- src/routes/planes/$planId/_detalle/documento.tsx | 2 +- src/routes/planes/$planId/_detalle/flujo.tsx | 2 +- src/routes/planes/$planId/_detalle/historial.tsx | 2 +- src/routes/planes/$planId/_detalle/iaplan.tsx | 6 +++--- src/routes/planes/$planId/_detalle/index.tsx | 2 +- .../$planId/asignaturas/$asignaturaId/route.tsx | 8 ++++---- src/routes/planes/_lista.tsx | 14 ++++++++++++-- 15 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx index cf97662..7cacce6 100644 --- a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx +++ b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx @@ -232,7 +232,7 @@ function DatosGenerales({ if (isLoading) return

Cargando información...

return ( -
+
{/* Encabezado de la Sección */}
diff --git a/src/components/asignaturas/detalle/BibliographyItem.tsx b/src/components/asignaturas/detalle/BibliographyItem.tsx index c566c3a..a6c4b37 100644 --- a/src/components/asignaturas/detalle/BibliographyItem.tsx +++ b/src/components/asignaturas/detalle/BibliographyItem.tsx @@ -125,7 +125,7 @@ export function BibliographyItem() { return
Cargando bibliografía...
return ( -
+

diff --git a/src/components/asignaturas/detalle/ContenidoTematico.tsx b/src/components/asignaturas/detalle/ContenidoTematico.tsx index 70a5165..806d7af 100644 --- a/src/components/asignaturas/detalle/ContenidoTematico.tsx +++ b/src/components/asignaturas/detalle/ContenidoTematico.tsx @@ -662,7 +662,7 @@ export function ContenidoTematico() { } return ( -
+

diff --git a/src/components/asignaturas/wizard/PasoFuenteClonadoInterno.tsx b/src/components/asignaturas/wizard/PasoFuenteClonadoInterno.tsx index 17d629f..4a1ae4b 100644 --- a/src/components/asignaturas/wizard/PasoFuenteClonadoInterno.tsx +++ b/src/components/asignaturas/wizard/PasoFuenteClonadoInterno.tsx @@ -163,7 +163,7 @@ export function PasoFuenteClonadoInterno({ return (
- + Fuente diff --git a/src/components/wizard/WizardLayout.tsx b/src/components/wizard/WizardLayout.tsx index 96fb1a5..77b2594 100644 --- a/src/components/wizard/WizardLayout.tsx +++ b/src/components/wizard/WizardLayout.tsx @@ -25,7 +25,7 @@ export function WizardLayout({ }} >
- + {title}
-
+
{children}
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 2dc7df5..862ba12 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -30,7 +30,7 @@ function RouteComponent() { 4. px-4 md:px-6: Padding RESPONSIVO interno (seguro para móviles y desktop). 5. py-6: Padding vertical (opcional, para separarse del header). */} -
+
-
+
{/* 2. Header del Plan */} {isLoading ? ( /* ===== SKELETON ===== */ diff --git a/src/routes/planes/$planId/_detalle/asignaturas.tsx b/src/routes/planes/$planId/_detalle/asignaturas.tsx index 8de9d88..cd0ef8a 100644 --- a/src/routes/planes/$planId/_detalle/asignaturas.tsx +++ b/src/routes/planes/$planId/_detalle/asignaturas.tsx @@ -129,7 +129,7 @@ function AsignaturasPage() { } return ( -
+
{/* Header */}
diff --git a/src/routes/planes/$planId/_detalle/documento.tsx b/src/routes/planes/$planId/_detalle/documento.tsx index 2c2bdbf..2e1cd47 100644 --- a/src/routes/planes/$planId/_detalle/documento.tsx +++ b/src/routes/planes/$planId/_detalle/documento.tsx @@ -102,7 +102,7 @@ function RouteComponent() { } } return ( -
+
{/* HEADER DE ACCIONES */}
diff --git a/src/routes/planes/$planId/_detalle/flujo.tsx b/src/routes/planes/$planId/_detalle/flujo.tsx index eb3ba17..61d6272 100644 --- a/src/routes/planes/$planId/_detalle/flujo.tsx +++ b/src/routes/planes/$planId/_detalle/flujo.tsx @@ -18,7 +18,7 @@ function RouteComponent() { console.log(rawData) return ( -
+
{/* Header Informativo (Opcional, si no viene del layout padre) */}
diff --git a/src/routes/planes/$planId/_detalle/historial.tsx b/src/routes/planes/$planId/_detalle/historial.tsx index 51c3b30..345b8c6 100644 --- a/src/routes/planes/$planId/_detalle/historial.tsx +++ b/src/routes/planes/$planId/_detalle/historial.tsx @@ -125,7 +125,7 @@ function RouteComponent() { ) return ( -
+

diff --git a/src/routes/planes/$planId/_detalle/iaplan.tsx b/src/routes/planes/$planId/_detalle/iaplan.tsx index 2329252..ffaee91 100644 --- a/src/routes/planes/$planId/_detalle/iaplan.tsx +++ b/src/routes/planes/$planId/_detalle/iaplan.tsx @@ -581,7 +581,7 @@ function RouteComponent() { } return ( -
+
{/* --- HEADER MÓVIL (Solo visible en < md) --- */}
-- 2.52.0 From 085e6fa0e94f7af187d7aa0ffc707d38efc3988b Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 26 Mar 2026 19:06:54 -0600 Subject: [PATCH 3/5] =?UTF-8?q?mejoras=20de=20dise=C3=B1o=20de=20mapa=20cu?= =?UTF-8?q?rricular=20parte=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ancho de ciclos fijo - alineacion correcta de CR, HD y HI - tamaño de nombre de asignatura pasó de md a sm --- src/routes/planes/$planId/_detalle/mapa.tsx | 41 +++++++++------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index 0738284..77502a1 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -248,7 +248,7 @@ function AsignaturaCardItem({ {/* titulo */}

-
-
- - - CR - -
+
+ {/* */} + + CR + +
{asignatura.creditos}
-
-
- - - HD - -
+
+ + HD + +
{asignatura.hd}
-
-
- - - HI - -
+
+ + HI + +
{asignatura.hi}
@@ -728,7 +723,7 @@ function MapaCurricularPage() {
-- 2.52.0 From ddb83782a661d706e375a65baa4ef8230b004d14 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 26 Mar 2026 20:11:58 -0600 Subject: [PATCH 4/5] =?UTF-8?q?mejoras=20de=20dise=C3=B1o=20de=20mapa=20cu?= =?UTF-8?q?rricular=20parte=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - color picker añadido por linea - bote de basura para borrar linea alineado abajo a la derecha - se quitó degradado y linea de color superior de las tarjetas de asignaturas --- src/routes/planes/$planId/_detalle/mapa.tsx | 89 ++++++++++++++------- src/types/plan.ts | 7 +- src/types/supabase.ts | 3 + 3 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index 77502a1..cf6e3ad 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -59,7 +59,7 @@ const mapLineasToLineaCurricular = ( id: linea.id, nombre: linea.nombre, orden: linea.orden ?? 0, - color: palette[index % palette.length], + color: linea.color ?? palette[index % palette.length], })) } @@ -185,7 +185,7 @@ function AsignaturaCardItem({ onDragStart={(e) => onDragStart(e, asignatura.id)} onClick={onClick} className={[ - 'group relative h-50 w-40 shrink-0 overflow-hidden rounded-[22px] border text-left', + 'group bg-background relative h-50 w-40 shrink-0 overflow-hidden rounded-[22px] border text-left', 'transition-all duration-300 ease-out', 'focus-visible:ring-ring/30 focus-visible:ring-2 focus-visible:outline-none', 'cursor-grab active:cursor-grabbing', @@ -194,26 +194,10 @@ function AsignaturaCardItem({ : 'hover:-translate-y-1 hover:shadow-lg', ].join(' ')} style={{ - borderColor: hexToRgba(lineaColor, 0.18), - background: ` - radial-gradient(circle at top right, ${hexToRgba(lineaColor, 0.22)} 0%, transparent 34%), - linear-gradient(180deg, ${hexToRgba(lineaColor, 0.12)} 0%, ${hexToRgba(lineaColor, 0.04)} 42%, var(--card) 100%) - `, + borderColor: lineaColor, }} title={asignatura.nombre} > - {/* franja */} -
- - {/* glow decorativo */} -
-
{/* top */}
@@ -248,7 +232,7 @@ function AsignaturaCardItem({ {/* titulo */}

-
+
{/* */} CR @@ -272,7 +256,7 @@ function AsignaturaCardItem({
-
+
HD @@ -282,7 +266,7 @@ function AsignaturaCardItem({
-
+
HI @@ -382,7 +366,7 @@ function MapaCurricularPage() { id: nueva.id, nombre: nueva.nombre, orden: nueva.orden, - color: '#1976d2', + color: (nueva as any).color ?? '#1976d2', } setLineas((prev) => [...prev, mapeada]) setNombreNuevaLinea('') @@ -390,6 +374,24 @@ function MapaCurricularPage() { }, ) } + + const cambiarColorLinea = (lineaId: string, nuevoColor: string) => { + setLineas((prev) => + prev.map((l) => (l.id === lineaId ? { ...l, color: nuevoColor } : l)), + ) + + updateLineaApi( + { + lineaId, + patch: { color: nuevoColor }, + }, + { + onError: (err) => { + console.error('Error al actualizar color de linea:', err) + }, + }, + ) + } const guardarEdicionLinea = (id: string, nuevoNombre: string) => { const nombreAFijar = nuevoNombre.trim() @@ -721,7 +723,7 @@ function MapaCurricularPage() {
-
+ +
+
+
+ + cambiarColorLinea(linea.id, e.target.value) + } + className="absolute inset-0 h-full w-full cursor-pointer opacity-0" + /> + +
+ +
+
+
-
+
{subjectsLoading ? (
Cargando asignaturas… diff --git a/src/data/api/plans.api.ts b/src/data/api/plans.api.ts index f9daed5..93dfcef 100644 --- a/src/data/api/plans.api.ts +++ b/src/data/api/plans.api.ts @@ -221,7 +221,9 @@ export async function plan_lineas_list( const supabase = supabaseBrowser() const { data, error } = await supabase .from('lineas_plan') - .select('id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en') + .select( + 'id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en,color', + ) .eq('plan_estudio_id', planId) .order('orden', { ascending: true }) diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index 9b4ab51..e745812 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -499,6 +499,7 @@ export async function lineas_insert(linea: { plan_estudio_id: string orden: number area?: string + color?: string | null }) { const supabase = supabaseBrowser() const { data, error } = await supabase @@ -514,7 +515,12 @@ export async function lineas_insert(linea: { // Actualizar una línea existente export async function lineas_update( lineaId: string, - patch: { nombre?: string; orden?: number; area?: string }, + patch: { + nombre?: string + orden?: number + area?: string + color?: string | null + }, ) { const supabase = supabaseBrowser() const { data, error } = await supabase diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 862ba12..7f70a9e 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -30,7 +30,7 @@ function RouteComponent() { 4. px-4 md:px-6: Padding RESPONSIVO interno (seguro para móviles y desktop). 5. py-6: Padding vertical (opcional, para separarse del header). */} -
+
-
+
{/* 2. Header del Plan */} {isLoading ? ( /* ===== SKELETON ===== */ diff --git a/src/routes/planes/$planId/_detalle/mapa.tsx b/src/routes/planes/$planId/_detalle/mapa.tsx index cf6e3ad..e3622b7 100644 --- a/src/routes/planes/$planId/_detalle/mapa.tsx +++ b/src/routes/planes/$planId/_detalle/mapa.tsx @@ -2,10 +2,10 @@ import { createFileRoute } from '@tanstack/react-router' import { Plus, AlertTriangle, Trash2, Download } from 'lucide-react' import * as Icons from 'lucide-react' -import { useMemo, useState, useEffect, Fragment } from 'react' +import { useMemo, useState, useEffect, Fragment, useRef } from 'react' import type { TipoAsignatura } from '@/data' -import type { Asignatura, LineaCurricular } from '@/types/plan' +import type { Asignatura } from '@/types/plan' import type { TablesUpdate } from '@/types/supabase' import { Badge } from '@/components/ui/badge' @@ -17,6 +17,8 @@ import { DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Select, SelectContent, @@ -24,6 +26,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { Separator } from '@/components/ui/separator' import { Tooltip, TooltipContent, @@ -39,6 +42,8 @@ import { useUpdateAsignatura, useUpdateLinea, } from '@/data' +import { cn } from '@/lib/utils' +import { generarColorContrastante } from '@/utils/colors' // --- Mapeadores (Fuera del componente para mayor limpieza) --- const palette = [ @@ -52,9 +57,16 @@ const palette = [ '#C026D3', // fucsia ] +type LineaCurricularUI = { + id: string + nombre: string + orden: number + color: string +} + const mapLineasToLineaCurricular = ( lineasApi: Array = [], -): Array => { +): Array => { return lineasApi.map((linea, index) => ({ id: linea.id, nombre: linea.nombre, @@ -276,11 +288,6 @@ function AsignaturaCardItem({
- - {/* drag affordance */} -
- -
@@ -311,20 +318,26 @@ function MapaCurricularPage() { const { data } = usePlan(planId) const [totalCiclos, setTotalCiclos] = useState(0) const [editingLineaId, setEditingLineaId] = useState(null) - const { mutate: createLinea } = useCreateLinea() + const { mutate: createLinea, isPending: isCreatingLinea } = useCreateLinea() const { mutate: updateLineaApi } = useUpdateLinea() const { mutate: deleteLineaApi } = useDeleteLinea() const { data: asignaturaApi, isLoading: loadingAsig } = usePlanAsignaturas(planId) const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId) const [asignaturas, setAsignaturas] = useState>([]) - const [lineas, setLineas] = useState>([]) + const [lineas, setLineas] = useState>([]) const [draggedAsignatura, setDraggedAsignatura] = useState( null, ) const [isEditModalOpen, setIsEditModalOpen] = useState(false) - const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado + const [isAddLineaDialogOpen, setIsAddLineaDialogOpen] = useState(false) + const [selectedLineaOption, setSelectedLineaOption] = useState< + 'matematicas' | 'area_comun' | 'custom' | '' + >('') + const [customLineaNombre, setCustomLineaNombre] = useState('') + const [ultimoHue, setUltimoHue] = useState(null) const { mutate: updateAsignatura } = useUpdateAsignatura() + const inputRef = useRef(null) useEffect(() => { if (data?.numero_ciclos) { @@ -332,7 +345,13 @@ function MapaCurricularPage() { } }, [data]) - const manejarAgregarLinea = (nombre: string) => { + useEffect(() => { + if (selectedLineaOption === 'custom' && inputRef.current) { + inputRef.current.focus() + } + }, [selectedLineaOption]) + + const manejarAgregarLinea = (nombre: string, color: string, hue: number) => { const nombreNormalizado = nombre.trim() if (!nombreNormalizado) return const nombreBusqueda = nombreNormalizado @@ -353,12 +372,14 @@ function MapaCurricularPage() { return } const maxOrden = lineas.reduce((max, l) => Math.max(max, l.orden || 0), 0) + createLinea( { nombre: nombreNormalizado, plan_estudio_id: planId, orden: maxOrden + 1, area: 'sin asignar', + color, }, { onSuccess: (nueva) => { @@ -366,15 +387,43 @@ function MapaCurricularPage() { id: nueva.id, nombre: nueva.nombre, orden: nueva.orden, - color: (nueva as any).color ?? '#1976d2', + color: nueva.color ?? color, } setLineas((prev) => [...prev, mapeada]) - setNombreNuevaLinea('') + setUltimoHue(hue) + setIsAddLineaDialogOpen(false) + setSelectedLineaOption('') + setCustomLineaNombre('') + }, + onError: (err) => { + console.error('Error al crear linea:', err) }, }, ) } + const canAddLinea = + selectedLineaOption === 'matematicas' || + selectedLineaOption === 'area_comun' || + (selectedLineaOption === 'custom' && customLineaNombre.trim().length > 0) + + const handleAgregarLinea = () => { + if (!canAddLinea || isCreatingLinea) return + + const nombreSeleccionado = + selectedLineaOption === 'matematicas' + ? 'Matemáticas' + : selectedLineaOption === 'area_comun' + ? 'Área Común' + : customLineaNombre.trim() + + if (!nombreSeleccionado) return + + const { hex, hue } = generarColorContrastante(ultimoHue) + + manejarAgregarLinea(nombreSeleccionado, hex, hue) + } + const cambiarColorLinea = (lineaId: string, nuevoColor: string) => { setLineas((prev) => prev.map((l) => (l.id === lineaId ? { ...l, color: nuevoColor } : l)), @@ -421,14 +470,6 @@ function MapaCurricularPage() { }, ) } - const tieneAreaComun = useMemo(() => { - return lineas.some( - (l) => - l.nombre.toLowerCase() === 'área común' || - l.nombre.toLowerCase() === 'area comun', - ) - }, [lineas]) - useEffect(() => { if (asignaturaApi) setAsignaturas(mapAsignaturasToAsignaturas(asignaturaApi)) @@ -662,65 +703,37 @@ function MapaCurricularPage() { )}
-
- - {!tieneAreaComun && ( - + -
-
-
- -

- Crea una línea personalizada sin abrir menús adicionales. -

-
- -
- setNombreNuevaLinea(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && nombreNuevaLinea.trim()) { - manejarAgregarLinea(nombreNuevaLinea) - } - }} - placeholder="Ej: Optativas" - className="h-9" - /> - -
-
+ {/* Barra Totales */} +
+ + + +
- {/* Barra Totales */} -
- - - - -
-
+
+ +
+
+
@@ -936,7 +963,7 @@ function MapaCurricularPage() { ) })} -
+
{stats.cr} Cr
{stats.hd + stats.hi} Hrs
@@ -944,7 +971,7 @@ function MapaCurricularPage() {
{/* Asignaturas Sin Asignar */} -
+
@@ -1025,6 +1052,150 @@ function MapaCurricularPage() {
{/* Modal de Edición */} + { + setIsAddLineaDialogOpen(open) + if (!open) { + setSelectedLineaOption(undefined) + setCustomLineaNombre('') + } + }} + > + + + + Agregar línea curricular + + + + + setSelectedLineaOption(val as typeof selectedLineaOption) + } + className="grid grid-cols-[1fr_auto_1fr] gap-8 py-4" + > + {/* Columna Izquierda: Predefinidas */} +
+
+ Catálogo Institucional +
+ + {/* Tarjeta: Matemáticas */} +
+ +
+ +

+ Línea base para ciencias exactas. +

+
+
+ + {/* Tarjeta: Área Común */} +
+ +
+ +

+ Materias compartidas entre programas. +

+
+
+
+ + {/* Separador */} +
+ +
+ + {/* Columna Derecha: Personalizada */} +
+
+ Línea personalizada +
+ + {/* Tarjeta: Custom */} +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() // Evita que la página haga scroll con el espacio + setSelectedLineaOption('custom') + inputRef.current?.focus() + } + }} + onClick={() => { + setSelectedLineaOption('custom') + inputRef.current?.focus() + }} + className={`focus-visible:ring-primary relative flex w-full cursor-pointer items-start gap-3 rounded-md border p-4 shadow-sm transition-all outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${ + selectedLineaOption === 'custom' + ? 'border-primary/50 bg-primary/5' + : 'border-input hover:bg-muted/50' + }`} + > + {/* Omitimos after:absolute para no tapar el input */} + +
+ + + setCustomLineaNombre(e.target.value.slice(0, 200)) + } + placeholder="Escribe el nombre aquí" + maxLength={200} + disabled={selectedLineaOption !== 'custom'} + className="bg-background h-9 w-full" + /> +
+
+
+
+ +
+ +
+
+
+
-
+
-
+
{/* CAMBIOS CLAVE: 1. overflow-x-auto: Permite scroll horizontal. 2. scrollbar-hide: (Opcional) para que no se vea la barra fea. @@ -260,7 +260,7 @@ function AsignaturaLayout() {
-
+
diff --git a/src/routes/planes/_lista.tsx b/src/routes/planes/_lista.tsx index 28830a7..547c325 100644 --- a/src/routes/planes/_lista.tsx +++ b/src/routes/planes/_lista.tsx @@ -114,7 +114,7 @@ function RouteComponent() { return (
-
+
{/* Header y Botón Nuevo */}
@@ -193,7 +193,7 @@ function RouteComponent() { onClick={resetFilters} disabled={isClearDisabled} className={`ring-offset-background bg-secondary text-secondary-foreground hover:bg-secondary/90 inline-flex h-9 items-center justify-center gap-2 rounded-md px-4 text-sm font-medium shadow-md transition-colors ${ - isClearDisabled ? 'opacity-50 cursor-not-allowed' : '' + isClearDisabled ? 'cursor-not-allowed opacity-50' : '' }`} > Limpiar diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 0000000..0e5e059 --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,36 @@ +// Convierte HSL a Hexadecimal +function hslToHex(h: number, s: number, l: number): string { + l /= 100 + const a = (s * Math.min(l, 1 - l)) / 100 + const f = (n: number) => { + const k = (n + h / 30) % 12 + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1) + return Math.round(255 * color) + .toString(16) + .padStart(2, '0') + } + return `#${f(0)}${f(8)}${f(4)}` +} + +/** + * Genera un color contrastante. + * @param prevHue El tono (0-360) del color anterior. Null si es el primero. + * @returns Objeto con el hex y el nuevo hue para guardar como referencia. + */ +export function generarColorContrastante(prevHue: number | null = null) { + let newHue: number + + if (prevHue === null) { + // Primer color: completamente al azar + newHue = Math.floor(Math.random() * 360) + } else { + // Siguientes: Salto aleatorio entre 130° y 230° para forzar contraste + const salto = 130 + Math.floor(Math.random() * 100) + newHue = (prevHue + salto) % 360 + } + + // Mantenemos saturación al 70% y luminosidad al 50% para colores vivos pero no fosforescentes + const hex = hslToHex(newHue, 70, 50) + + return { hex, hue: newHue } +} -- 2.52.0