feat: update dialog titles to use font-mono style for consistency across components

This commit is contained in:
2025-09-01 09:02:11 -06:00
parent 5a113ca603
commit 5181306b93
17 changed files with 274 additions and 114 deletions

View File

@@ -58,7 +58,7 @@ export function CarreraDetailDialog({
<Dialog open={!!carrera} onOpenChange={(o) => !o && onOpenChange(null)}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{carrera?.nombre}</DialogTitle>
<DialogTitle className="font-mono" >{carrera?.nombre}</DialogTitle>
<DialogDescription>
{carrera?.facultades?.nombre ?? "—"} · {carrera?.semestres} semestres{" "}
{typeof carrera?.activo === "boolean" && (

View File

@@ -75,7 +75,7 @@ export function CarreraFormDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{mode === "create" ? "Nueva carrera" : "Editar carrera"}</DialogTitle>
<DialogTitle className="font-mono" >{mode === "create" ? "Nueva carrera" : "Editar carrera"}</DialogTitle>
<DialogDescription>
{mode === "create" ? "Crea una nueva carrera en la base de datos." : "Actualiza los datos de la carrera."}
</DialogDescription>

View File

@@ -68,7 +68,7 @@ export function CriterioFormDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Nuevo criterio</DialogTitle>
<DialogTitle className="font-mono" >Nuevo criterio</DialogTitle>
<DialogDescription>Agrega un criterio para esta carrera.</DialogDescription>
</DialogHeader>

View File

@@ -33,7 +33,7 @@ export function useDeleteCarreraDialog(carreraId: string, onDeleted?: () => void
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>¿Eliminar carrera?</DialogTitle>
<DialogTitle className="font-mono" >¿Eliminar carrera?</DialogTitle>
<DialogDescription>
Esta acción no se puede deshacer. ¿Seguro que quieres eliminar esta carrera?
</DialogDescription>

View File

@@ -84,7 +84,7 @@ export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdd
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="w-[min(92vw,760px)]">
<DialogHeader>
<DialogTitle>Nueva asignatura</DialogTitle>
<DialogTitle className="font-mono" >Nueva asignatura</DialogTitle>
<DialogDescription>Elige cómo crearla: manual o generada por IA.</DialogDescription>
</DialogHeader>

View File

@@ -31,7 +31,7 @@ export function AdjustAIButton({ plan }: { plan: PlanFull }) {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Ajustar con IA</DialogTitle>
<DialogTitle className="font-mono" >Ajustar con IA</DialogTitle>
<DialogDescription>Describe cómo quieres modificar el plan actual.</DialogDescription>
</DialogHeader>
<Textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Ej.: Enfatiza ciberseguridad y proyectos prácticos…" className="min-h-[120px]" />

View File

@@ -57,7 +57,7 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[min(92vw,760px)]">
<DialogHeader>
<DialogTitle>Nuevo plan de estudios (IA)</DialogTitle>
<DialogTitle className="font-mono" >Nuevo plan de estudios (IA)</DialogTitle>
</DialogHeader>
<div className="grid gap-4 md:grid-cols-2">
<div className="md:col-span-2 space-y-1">

View File

@@ -54,7 +54,7 @@ export function EditPlanButton({ plan }: { plan: PlanFull }) {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Editar plan</DialogTitle>
<DialogTitle className="font-mono" >Editar plan</DialogTitle>
<DialogDescription>Actualiza datos básicos.</DialogDescription>
</DialogHeader>
<div className="grid gap-3">

View File

@@ -196,7 +196,7 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
<Dialog open={!!editing} onOpenChange={(o) => { if (!o) setEditing(null) }}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editing ? `Editar: ${sections.find((x) => x.key === editing.key)?.title}` : ""}</DialogTitle>
<DialogTitle className="font-mono" >{editing ? `Editar: ${sections.find((x) => x.key === editing.key)?.title}` : ""}</DialogTitle>
</DialogHeader>
<Textarea value={draft} onChange={(e) => setDraft(e.target.value)} className={`min-h-[260px] ${editing?.key === "prompt" ? "font-mono" : ""}`} placeholder="Escribe aquí…" />
<DialogFooter>

View File

@@ -45,7 +45,7 @@ function CommandDialog({
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogTitle className="font-mono" >{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent

View File

@@ -206,7 +206,7 @@ function DetailDialog({ row, onClose }: { row: RefRow | null; onClose: () => voi
<Dialog open={!!row} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle>
<DialogTitle className="font-mono" >{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle>
<DialogDescription>
{row?.descripcion || "Sin descripción"}
</DialogDescription>
@@ -333,7 +333,7 @@ function UploadDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Nuevo archivo de referencia</DialogTitle>
<DialogTitle className="font-mono" >Nuevo archivo de referencia</DialogTitle>
<DialogDescription>
Sube un documento y escribe instrucciones para su procesamiento. Se guardará en la base y se marcará como
<em> procesado </em> cuando termine el flujo.

View File

@@ -1,11 +1,18 @@
// routes/_authenticated/asignatura/$asignaturaId.tsx
import { createFileRoute, Link, useRouter } from "@tanstack/react-router"
import * as Icons from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { supabase } from "@/auth/supabase"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue
} from "@/components/ui/select"
import confetti from "canvas-confetti"
import {
@@ -16,6 +23,8 @@ import {
} from "@/components/ui/dialog"
import { Textarea } from "@/components/ui/textarea"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { ScrollArea } from "@/components/ui/scroll-area"
/* ================== Tipos ================== */
type Asignatura = {
@@ -456,7 +465,7 @@ function EditAsignaturaButton({ asignatura, onUpdate }: {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Editar asignatura</DialogTitle>
<DialogTitle className="font-mono" >Editar asignatura</DialogTitle>
<DialogDescription>Actualiza campos básicos.</DialogDescription>
</DialogHeader>
@@ -468,7 +477,28 @@ function EditAsignaturaButton({ asignatura, onUpdate }: {
<Input value={form.clave ?? ""} onChange={e => setForm(s => ({ ...s, clave: e.target.value }))} />
</Field>
<Field label="Tipo">
<Input value={form.tipo ?? ""} onChange={e => setForm(s => ({ ...s, tipo: e.target.value }))} />
<Select value={form.tipo ?? ""} onValueChange={v => setForm(s => ({ ...s, tipo: v }))}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecciona tipo…" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Obligatoria">
<span className={typeStyle("Obligatoria").chip}>Obligatoria</span>
</SelectItem>
<SelectItem value="Optativa">
<span className={typeStyle("Optativa").chip}>Optativa</span>
</SelectItem>
<SelectItem value="Taller">
<span className={typeStyle("Taller").chip}>Taller</span>
</SelectItem>
<SelectItem value="Laboratorio">
<span className={typeStyle("Laboratorio").chip}>Laboratorio</span>
</SelectItem>
<SelectItem value="Otro">
<span className={typeStyle("Otro").chip}>Otro</span>
</SelectItem>
</SelectContent>
</Select>
</Field>
<Field label="Semestre">
<Input value={String(form.semestre ?? "")} onChange={e => setForm(s => ({ ...s, semestre: Number(e.target.value) || null }))} />
@@ -537,7 +567,7 @@ function MejorarAIButton({ asignaturaId, onApply }: {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Mejorar asignatura con IA</DialogTitle>
<DialogTitle className="font-mono" >Mejorar asignatura con IA</DialogTitle>
<DialogDescription>
Describe el ajuste que deseas (p. ej. refuerza contenidos prácticos y añade bibliografía reciente).
</DialogDescription>
@@ -589,7 +619,9 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
}
function EditContenidosButton({
type UnitDraft = { title: string; temas: string[] }
export function EditContenidosButton({
asignaturaId,
value,
onSaved,
@@ -600,117 +632,232 @@ function EditContenidosButton({
}) {
const [open, setOpen] = useState(false)
const [saving, setSaving] = useState(false)
type UnitDraft = { title: string; temas: string[] }
const [units, setUnits] = useState<UnitDraft[]>([])
const [initialUnits, setInitialUnits] = useState<UnitDraft[]>([])
// Normaliza el JSON (acepta estructuras flexibles)
function normalize(v: any): UnitDraft[] {
// --- Normaliza entrada flexible a estructura estable
const normalize = useCallback((v: any): UnitDraft[] => {
try {
const entries = Object.entries(v ?? {})
.sort(([a], [b]) => Number(a) - Number(b))
.map(([key, val]) => {
const obj = val as any
// soporta: { titulo, subtemas:{ "1":"t1" } } | { "1":"t1" } | ["t1","t2"]
const title = (typeof obj?.titulo === 'string' && obj.titulo.trim()) ? obj.titulo.trim() : `Unidad ${key}`
const title =
typeof obj?.titulo === "string" && obj.titulo.trim()
? obj.titulo.trim()
: `Unidad ${key}`
let temas: string[] = []
if (Array.isArray(obj?.subtemas)) temas = obj.subtemas.map(String)
else if (obj?.subtemas && typeof obj.subtemas === 'object') {
temas = Object.entries(obj.subtemas).sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
else if (obj?.subtemas && typeof obj.subtemas === "object") {
temas = Object.entries(obj.subtemas)
.sort(([a], [b]) => Number(a) - Number(b))
.map(([, t]) => String(t))
} else if (Array.isArray(obj)) {
temas = obj.map(String)
} else if (obj && typeof obj === 'object') {
const nums = Object.entries(obj).filter(([k, v]) => /^\d+$/.test(String(k)) && (typeof v === "string" || typeof v === "number"))
if (nums.length) temas = nums.sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
} else if (obj && typeof obj === "object") {
const nums = Object.entries(obj).filter(
([k, v]) => /^\d+$/.test(String(k)) && (typeof v === "string" || typeof v === "number"),
)
if (nums.length)
temas = nums.sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
}
return { title, temas }
})
return entries.length ? entries : [{ title: 'Unidad 1', temas: [] }]
return entries.length ? entries : [{ title: "Unidad 1", temas: [] }]
} catch {
return [{ title: 'Unidad 1', temas: [] }]
}
return [{ title: "Unidad 1", temas: [] }]
}
}, [])
// Construye un JSON estable para guardar
function buildPayload(us: UnitDraft[]) {
const out: any = {}
// --- Construye payload consistente { "1": { titulo, subtemas:{ "1": "t1" } } }
const buildPayload = useCallback((us: UnitDraft[]) => {
const out: Record<string, any> = {}
us.forEach((u, idx) => {
const k = String(idx + 1)
const sub: any = {}
u.temas.filter(t => t.trim()).forEach((t, i) => { sub[String(i + 1)] = t.trim() })
out[k] = { titulo: u.title.trim() || `Unidad ${k}`, subtemas: sub }
const sub: Record<string, string> = {}
u.temas
.map((t) => t.trim())
.filter(Boolean)
.forEach((t, i) => {
sub[String(i + 1)] = t
})
out[k] = { titulo: (u.title || "").trim() || `Unidad ${k}`, subtemas: sub }
})
return out
}, [])
// --- Limpia UI: recorta espacios, elimina líneas vacías/duplicadas (case-insensitive)
const cleanUnits = useCallback((us: UnitDraft[]) => {
return us.map((u, idx) => {
const seen = new Set<string>()
const temas = u.temas
.map((t) => t.trim())
.filter((t) => {
if (!t) return false
const key = t.toLowerCase()
if (seen.has(key)) return false
seen.add(key)
return true
})
return {
title: (u.title || "").trim() || `Unidad ${idx + 1}`,
temas,
}
})
}, [])
const openEditor = () => {
const base = normalize(value)
setUnits(cleanUnits(base))
setInitialUnits(cleanUnits(base))
setOpen(true)
}
function openEditor() {
setUnits(normalize(value))
setOpen(true)
const hasChanges = useMemo(
() => JSON.stringify(cleanUnits(units)) !== JSON.stringify(cleanUnits(initialUnits)),
[units, initialUnits, cleanUnits],
)
// --- Atajos: Guardar con Ctrl/Cmd + Enter
useEffect(() => {
if (!open) return
const handler = (e: KeyboardEvent) => {
const ctrlOrCmd = e.ctrlKey || e.metaKey
if (ctrlOrCmd && e.key.toLowerCase() === "enter") {
e.preventDefault()
void save()
}
}
window.addEventListener("keydown", handler)
return () => window.removeEventListener("keydown", handler)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, units, saving])
// --- Acciones por unidad
const removeUnit = (idx: number) => {
if (!confirm("¿Eliminar esta unidad?")) return
setUnits((prev) => prev.filter((_, i) => i !== idx))
}
const moveUnit = (idx: number, dir: -1 | 1) => {
setUnits((prev) => {
const next = [...prev]
const j = idx + dir
if (j < 0 || j >= next.length) return prev
;[next[idx], next[j]] = [next[j], next[idx]]
return next
})
}
const duplicateUnit = (idx: number) => {
setUnits((prev) => {
const next = [...prev]
next.splice(idx + 1, 0, {
title: `${prev[idx].title} (copia)`,
temas: [...prev[idx].temas],
})
return next
})
}
async function save() {
setSaving(true)
const contenidos = buildPayload(units)
const cleaned = cleanUnits(units)
const contenidos = buildPayload(cleaned)
const { data, error } = await supabase
.from('asignaturas')
.from("asignaturas")
.update({ contenidos })
.eq('id', asignaturaId)
.eq("id", asignaturaId)
.select()
.maybeSingle()
setSaving(false)
if (error) { alert(error.message || 'No se pudo guardar'); return }
if (error) {
alert(error.message || "No se pudo guardar")
return
}
setInitialUnits(cleaned)
onSaved((data as any)?.contenidos ?? contenidos)
setOpen(false)
}
const cancel = () => {
if (hasChanges && !confirm("Hay cambios sin guardar. ¿Cerrar de todos modos?")) return
setOpen(false)
}
return (
<>
<Button size="sm" variant="outline" onClick={openEditor}>
<Icons.Pencil className="h-4 w-4 mr-2" /> Editar contenidos
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Editar contenidos</DialogTitle>
<Dialog open={open} onOpenChange={(o) => (o ? openEditor() : cancel())}>
<DialogContent className="max-w-4xl p-0 overflow-hidden">
{/* Header sticky */}
<div className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
<DialogHeader className="px-6 pt-5 pb-3">
<DialogTitle className="flex items-center gap-2">
<Icons.BookOpen className="h-5 w-5" />
Editar contenidos
{hasChanges && (
<Badge variant="secondary" className="ml-1">Cambios sin guardar</Badge>
)}
</DialogTitle>
<DialogDescription>
Títulos de unidad y temas (un tema por línea). Se guardará en un formato consistente con <code>titulo</code> y <code>subtemas</code>.
Títulos de unidad y temas (uno por línea). Se guardará con <code>titulo</code> y <code>subtemas</code>.
</DialogDescription>
</DialogHeader>
<div className="px-6 pb-3 flex items-center justify-between text-xs text-muted-foreground">
<span>{units.length} unidad(es)</span>
<span className="inline-flex items-center gap-1">
<Icons.Keyboard className="h-3 w-3" /> Atajo: <kbd className="px-1 border rounded">Ctrl/</kbd>+<kbd className="px-1 border rounded">Enter</kbd>
</span>
</div>
</div>
<div className="space-y-4 max-h-[60vh] overflow-auto pr-1">
<ScrollArea className="max-h-[65vh] px-6">
<div className="space-y-4 py-4">
{units.map((u, i) => (
<div key={i} className="rounded-2xl border p-3">
<div className="flex items-center justify-between mb-2">
<div key={i} className="rounded-2xl border p-4">
<div className="flex items-center justify-between mb-3 gap-2">
<div className="font-medium text-sm">Unidad {i + 1}</div>
<div className="flex items-center gap-2">
<Button
size="icon"
variant="ghost"
title="Eliminar unidad"
onClick={() => setUnits(prev => prev.filter((_, idx) => idx !== i))}
>
<div className="flex items-center gap-1">
<Button size="icon" variant="ghost" title="Subir" onClick={() => moveUnit(i, -1)} disabled={i === 0}>
<Icons.ArrowUp className="w-4 h-4" />
</Button>
<Button size="icon" variant="ghost" title="Bajar" onClick={() => moveUnit(i, 1)} disabled={i === units.length - 1}>
<Icons.ArrowDown className="w-4 h-4" />
</Button>
<Button size="icon" variant="ghost" title="Duplicar unidad" onClick={() => duplicateUnit(i)}>
<Icons.Copy className="w-4 h-4" />
</Button>
<Separator orientation="vertical" className="mx-1 h-5" />
<Button size="icon" variant="ghost" title="Eliminar unidad" onClick={() => removeUnit(i)}>
<Icons.Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
<div className="grid gap-2">
<div className="grid md:grid-cols-2 gap-3">
<div className="space-y-1">
<Label>Título</Label>
<Input
value={u.title}
onChange={(e) => setUnits(prev => prev.map((uu, idx) => idx === i ? { ...uu, title: e.target.value } : uu))}
onChange={(e) =>
setUnits((prev) => prev.map((uu, idx) => (idx === i ? { ...uu, title: e.target.value } : uu)))
}
placeholder={`Unidad ${i + 1}`}
/>
</div>
<div className="space-y-1">
<div className="space-y-1 md:col-span-2">
<div className="flex items-center justify-between">
<Label>Temas (uno por línea)</Label>
<Badge variant="outline">{u.temas.filter((t) => t.trim()).length} tema(s)</Badge>
</div>
<Textarea
className="min-h-[120px]"
value={u.temas.join('\n')}
className="min-h-[140px]"
value={u.temas.join("\n")}
onChange={(e) => {
const lines = e.target.value.split('\n')
setUnits(prev => prev.map((uu, idx) => idx === i ? { ...uu, temas: lines } : uu))
const lines = e.target.value.split("\n")
setUnits((prev) => prev.map((uu, idx) => (idx === i ? { ...uu, temas: lines } : uu)))
}}
placeholder={`Tema 1\nTema 2\n…`}
/>
@@ -718,19 +865,31 @@ function EditContenidosButton({
</div>
</div>
))}
<div className="flex justify-end">
<Button
variant="secondary"
onClick={() => setUnits(prev => [...prev, { title: `Unidad ${prev.length + 1}`, temas: [] }])}
onClick={() =>
setUnits((prev) => [...prev, { title: `Unidad ${prev.length + 1}`, temas: [] }])
}
>
<Icons.Plus className="w-4 h-4 mr-2" /> Agregar unidad
</Button>
</div>
</div>
</ScrollArea>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
<Button onClick={save} disabled={saving}>{saving ? 'Guardando…' : 'Guardar'}</Button>
<DialogFooter className="px-6 pb-5">
<Button variant="outline" onClick={cancel}>Cancelar</Button>
<Button onClick={save} disabled={saving}>
{saving ? (
<span className="inline-flex items-center gap-2">
<Icons.Loader2 className="h-4 w-4 animate-spin" /> Guardando
</span>
) : (
"Guardar"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -738,6 +897,7 @@ function EditContenidosButton({
)
}
function EditBibliografiaButton({
asignaturaId,
value,
@@ -779,7 +939,7 @@ function EditBibliografiaButton({
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Editar bibliografía</DialogTitle>
<DialogTitle className="font-mono" >Editar bibliografía</DialogTitle>
<DialogDescription>Escribe una referencia por línea.</DialogDescription>
</DialogHeader>
<Textarea

View File

@@ -493,7 +493,7 @@ function RouteComponent() {
<Dialog open={cloneOpen} onOpenChange={setCloneOpen}>
<DialogContent className="w-[min(92vw,720px)] sm:max-w-xl">
<DialogHeader>
<DialogTitle>Clonar asignatura</DialogTitle>
<DialogTitle className="font-mono" >Clonar asignatura</DialogTitle>
</DialogHeader>
{cloneTarget && (
<div className="grid gap-3">
@@ -584,7 +584,7 @@ function RouteComponent() {
<Dialog open={bulkOpen} onOpenChange={setBulkOpen}>
<DialogContent className="w-[min(92vw,840px)]">
<DialogHeader>
<DialogTitle>Carrito de asignaturas ({cart.length})</DialogTitle>
<DialogTitle className="font-mono" >Carrito de asignaturas ({cart.length})</DialogTitle>
</DialogHeader>
{cart.length === 0 ? (

View File

@@ -182,7 +182,7 @@ function Page() {
Plan
</div>
<DialogHeader className="p-0">
<DialogTitle className="truncate text-xl sm:text-2xl">{planNombre}</DialogTitle>
<DialogTitle className="font-mono" className="truncate text-xl sm:text-2xl">{planNombre}</DialogTitle>
</DialogHeader>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
<KpiChip icon={Icons.BookOpen} label="Asignaturas" value={kpis.total} />

View File

@@ -199,7 +199,7 @@ function RouteComponent() {
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Nueva facultad</DialogTitle>
<DialogTitle className="font-mono" >Nueva facultad</DialogTitle>
</DialogHeader>
<FormFields form={form} setForm={setForm} />
@@ -223,7 +223,7 @@ function RouteComponent() {
<Dialog open={editOpen} onOpenChange={(o) => { setEditOpen(o); if (!o) setEditing(null) }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Editar facultad</DialogTitle>
<DialogTitle className="font-mono" >Editar facultad</DialogTitle>
</DialogHeader>
<FormFields form={form} setForm={setForm} />

View File

@@ -236,7 +236,7 @@ function EditPlanButton({ plan }: { plan: PlanFull }) {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Editar plan</DialogTitle>
<DialogTitle className="font-mono" >Editar plan</DialogTitle>
<DialogDescription>Actualiza datos básicos.</DialogDescription>
</DialogHeader>
<div className="grid gap-3">
@@ -287,7 +287,7 @@ function AdjustAIButton({ plan }: { plan: PlanFull }) {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Ajustar con IA</DialogTitle>
<DialogTitle className="font-mono" >Ajustar con IA</DialogTitle>
<DialogDescription>Describe cómo quieres modificar el plan actual.</DialogDescription>
</DialogHeader>
<Textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Ej.: Enfatiza ciberseguridad y proyectos prácticos…" className="min-h-[120px]" />
@@ -489,7 +489,7 @@ function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: ()
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="w-[min(92vw,760px)]">
<DialogHeader>
<DialogTitle>Nueva asignatura</DialogTitle>
<DialogTitle className="font-mono" >Nueva asignatura</DialogTitle>
<DialogDescription>Elige cómo crearla: manual o generada por IA.</DialogDescription>
</DialogHeader>

View File

@@ -367,7 +367,7 @@ function RouteComponent() {
{/* Dialog de edición */}
<Dialog open={!!editing} onOpenChange={(o) => { if (!o) setEditing(null) }}>
<DialogContent className="w-[min(92vw,720px)] sm:max-w-xl md:max-w-2xl">
<DialogHeader><DialogTitle>Editar usuario</DialogTitle></DialogHeader>
<DialogHeader><DialogTitle className="font-mono" >Editar usuario</DialogTitle></DialogHeader>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1"><Label>Nombre</Label><Input value={form.nombre ?? ""} onChange={(e) => setForm((s) => ({ ...s, nombre: e.target.value }))} /></div>
<div className="space-y-1"><Label>Apellidos</Label><Input value={form.apellidos ?? ""} onChange={(e) => setForm((s) => ({ ...s, apellidos: e.target.value }))} /></div>
@@ -452,7 +452,7 @@ function RouteComponent() {
{/* Modal: Nuevo usuario */}
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="w-[min(92vw,720px)] sm:max-w-xl md:max-w-2xl">
<DialogHeader><DialogTitle>Nuevo usuario</DialogTitle></DialogHeader>
<DialogHeader><DialogTitle className="font-mono" >Nuevo usuario</DialogTitle></DialogHeader>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1 md:col-span-2">