Refactor App component styles and remove highlights section
- Updated background gradient colors for a lighter theme. - Changed button styles to improve visibility. - Removed the highlights section to streamline the layout. - Adjusted text colors for better contrast and readability.
This commit is contained in:
@@ -1,82 +1,109 @@
|
||||
import * as Icons from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { supabase } from "@/auth/supabase"
|
||||
|
||||
/* color helpers */
|
||||
/* ---------- helpers de color ---------- */
|
||||
function hexToRgb(hex?: string | null): [number, number, number] {
|
||||
if (!hex) return [37, 99, 235]
|
||||
const h = hex.replace("#", ""); const v = h.length === 3 ? h.split("").map(c => c + c).join("") : h
|
||||
const n = parseInt(v, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
|
||||
const h = hex.replace("#", "")
|
||||
const v = h.length === 3 ? h.split("").map(c => c + c).join("") : h
|
||||
const n = parseInt(v, 16)
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
|
||||
}
|
||||
const rgba = (rgb: [number, number, number], a: number) => `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`
|
||||
|
||||
/* texto con clamp */
|
||||
function ExpandableText({ text, mono = false }: { text?: string | null; mono?: boolean }) {
|
||||
/* ---------- texto expandible (acepta string o string[]) ---------- */
|
||||
function ExpandableText({ text, mono = false }: { text?: string | string[] | null; mono?: boolean }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
if (!text) return <span className="text-neutral-400">—</span>
|
||||
if (!text || (Array.isArray(text) && text.length === 0)) {
|
||||
return <span className="text-neutral-400">—</span>
|
||||
}
|
||||
const content = Array.isArray(text) ? text.join("\n• ") : text
|
||||
const rendered = Array.isArray(text) ? `• ${content}` : content
|
||||
return (
|
||||
<div>
|
||||
<div className={`${mono ? 'font-mono whitespace-pre-wrap' : ''} text-sm ${open ? '' : 'line-clamp-10'}`}>{text}</div>
|
||||
{text.length > 220 && (
|
||||
<div className={`${mono ? "font-mono whitespace-pre-wrap" : ""} text-sm ${open ? "" : "line-clamp-10"}`}>
|
||||
{rendered}
|
||||
</div>
|
||||
{String(rendered).length > 220 && (
|
||||
<button onClick={() => setOpen(v => !v)} className="mt-2 text-xs font-medium text-neutral-600 hover:underline">
|
||||
{open ? 'Ver menos' : 'Ver más'}
|
||||
{open ? "Ver menos" : "Ver más"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* panel con estilo */
|
||||
/* ---------- panel con aurora mesh ---------- */
|
||||
function SectionPanel({
|
||||
title, icon: Icon, color, children,
|
||||
}: { title: string; icon: any; color?: string | null; children: React.ReactNode }) {
|
||||
title, icon: Icon, color, children, id,
|
||||
}: { title: string; icon: any; color?: string | null; children: React.ReactNode; id: string }) {
|
||||
const rgb = hexToRgb(color)
|
||||
return (
|
||||
<div className="rounded-3xl border backdrop-blur shadow-sm overflow-hidden">
|
||||
<div className="px-4 py-3 flex items-center gap-2"
|
||||
style={{ background: `linear-gradient(180deg, ${rgba(rgb, .10)}, transparent)` }}>
|
||||
<span className="inline-flex items-center justify-center rounded-xl border px-2.5 py-2"
|
||||
style={{ borderColor: rgba(rgb, .25), background: "rgba(255,255,255,.75)" }}>
|
||||
<section id={id} className="relative rounded-3xl border overflow-hidden shadow-sm bg-white/70 dark:bg-neutral-900/60">
|
||||
{/* aurora mesh sutil */}
|
||||
<div className="pointer-events-none absolute inset-0 -z-0">
|
||||
<div
|
||||
className="absolute -top-20 -left-20 w-[28rem] h-[28rem] rounded-full blur-3xl"
|
||||
style={{ background: `radial-gradient(circle, ${rgba(rgb, .20)}, transparent 60%)` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-24 -right-16 w-[24rem] h-[24rem] rounded-full blur-[60px]"
|
||||
style={{ background: `radial-gradient(circle, ${rgba(rgb, .14)}, transparent 60%)` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative z-10 px-4 py-3 flex items-center gap-2 border-b"
|
||||
style={{ background: `linear-gradient(180deg, ${rgba(rgb, .10)}, transparent)` }}
|
||||
>
|
||||
<span
|
||||
className="inline-flex items-center justify-center rounded-xl border px-2.5 py-2 bg-white/80"
|
||||
style={{ borderColor: rgba(rgb, .25) }}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</span>
|
||||
<h3 className="font-semibold">{title}</h3>
|
||||
</div>
|
||||
<div className="p-5">{children}</div>
|
||||
</div>
|
||||
<div className="relative z-10 p-5">{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- TABS + EDIT DIALOG ---------- */
|
||||
/* ---------- Secciones integradas (sin tabs) ---------- */
|
||||
type PlanTextFields = {
|
||||
objetivo_general?: string | null; sistema_evaluacion?: string | null;
|
||||
perfil_ingreso?: string | null; perfil_egreso?: string | null;
|
||||
competencias_genericas?: string | null; competencias_especificas?: string | null;
|
||||
indicadores_desempeno?: string | null; pertinencia?: string | null; prompt?: string | null;
|
||||
objetivo_general?: string | string[] | null
|
||||
sistema_evaluacion?: string | string[] | null
|
||||
perfil_ingreso?: string | string[] | null
|
||||
perfil_egreso?: string | string[] | null
|
||||
competencias_genericas?: string | string[] | null
|
||||
competencias_especificas?: string | string[] | null
|
||||
indicadores_desempeno?: string | string[] | null
|
||||
pertinencia?: string | string[] | null
|
||||
prompt?: string | null
|
||||
}
|
||||
|
||||
export function AcademicSections({
|
||||
planId, plan, color,
|
||||
}: { planId: string; plan: PlanTextFields; color?: string | null }) {
|
||||
// estado local editable
|
||||
const [local, setLocal] = useState<PlanTextFields>({ ...plan })
|
||||
const [editing, setEditing] = useState<null | { key: keyof PlanTextFields; title: string }>(null)
|
||||
const [draft, setDraft] = useState("")
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const sections = useMemo(() => [
|
||||
{ id: "obj", title: "Objetivo general", icon: Icons.Target, key: "objetivo_general" as const, mono: false },
|
||||
{ id: "eval", title: "Sistema de evaluación", icon: Icons.CheckCircle2, key: "sistema_evaluacion" as const, mono: false },
|
||||
{ id: "ing", title: "Perfil de ingreso", icon: Icons.UserPlus, key: "perfil_ingreso" as const, mono: false },
|
||||
{ id: "egr", title: "Perfil de egreso", icon: Icons.GraduationCap, key: "perfil_egreso" as const, mono: false },
|
||||
{ id: "cg", title: "Competencias genéricas", icon: Icons.Sparkles, key: "competencias_genericas" as const, mono: false },
|
||||
{ id: "ce", title: "Competencias específicas", icon: Icons.Cog, key: "competencias_especificas" as const, mono: false },
|
||||
{ id: "ind", title: "Indicadores de desempeño", icon: Icons.LineChart, key: "indicadores_desempeno" as const, mono: false },
|
||||
{ id: "pert", title: "Pertinencia", icon: Icons.Lightbulb, key: "pertinencia" as const, mono: false },
|
||||
{ id: "prompt", title: "Prompt (origen)", icon: Icons.Code2, key: "prompt" as const, mono: true },
|
||||
{ id: "sec-obj", title: "Objetivo general", icon: Icons.Target, key: "objetivo_general" as const, mono: false },
|
||||
{ id: "sec-eval", title: "Sistema de evaluación", icon: Icons.CheckCircle2, key: "sistema_evaluacion" as const, mono: false },
|
||||
{ id: "sec-ing", title: "Perfil de ingreso", icon: Icons.UserPlus, key: "perfil_ingreso" as const, mono: false },
|
||||
{ id: "sec-egr", title: "Perfil de egreso", icon: Icons.GraduationCap, key: "perfil_egreso" as const, mono: false },
|
||||
{ id: "sec-cg", title: "Competencias genéricas", icon: Icons.Sparkles, key: "competencias_genericas" as const, mono: false },
|
||||
{ id: "sec-ce", title: "Competencias específicas", icon: Icons.Cog, key: "competencias_especificas" as const, mono: false },
|
||||
{ id: "sec-ind", title: "Indicadores de desempeño", icon: Icons.LineChart, key: "indicadores_desempeno" as const, mono: false },
|
||||
{ id: "sec-per", title: "Pertinencia", icon: Icons.Lightbulb, key: "pertinencia" as const, mono: false },
|
||||
{ id: "sec-prm", title: "Prompt (origen)", icon: Icons.Code2, key: "prompt" as const, mono: true },
|
||||
], [])
|
||||
|
||||
async function handleSave() {
|
||||
@@ -96,50 +123,55 @@ export function AcademicSections({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultValue={sections[0].id} className="w-full">
|
||||
{/* nav sticky con píldoras scrollables */}
|
||||
<div className="sticky top-0 z-10 backdrop-blur border-b">
|
||||
<TabsList className="w-full flex gap-2 p-3 scrollbar-none">
|
||||
{sections.map(s => (
|
||||
<TabsTrigger key={s.id} value={s.id}
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-white p-3 text-xs">
|
||||
{s.title}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* contenido */}
|
||||
{/* Todas las tarjetas visibles */}
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
{sections.map(s => {
|
||||
const text = local[s.key] ?? null
|
||||
return (
|
||||
<TabsContent key={s.id} value={s.id} className="mt-4">
|
||||
<SectionPanel title={s.title} icon={s.icon} color={color}>
|
||||
<ExpandableText text={text} mono={s.mono} />
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button variant="outline" size="sm" disabled={!text}
|
||||
onClick={() => text && navigator.clipboard.writeText(text)}>Copiar</Button>
|
||||
<Button variant="ghost" size="sm"
|
||||
onClick={() => { setEditing({ key: s.key, title: s.title }); setDraft(text ?? "") }}>
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
</TabsContent>
|
||||
<SectionPanel key={s.id} id={s.id} title={s.title} icon={s.icon} color={color}>
|
||||
<ExpandableText text={text} mono={s.mono} />
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!text || (Array.isArray(text) && text.length === 0)}
|
||||
onClick={() => {
|
||||
const toCopy = Array.isArray(text) ? text.join("\n") : (text ?? "")
|
||||
if (toCopy) navigator.clipboard.writeText(toCopy)
|
||||
}}
|
||||
>
|
||||
Copiar
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const current = Array.isArray(text) ? text.join("\n") : (text ?? "")
|
||||
setEditing({ key: s.key, title: s.title })
|
||||
setDraft(current)
|
||||
}}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Dialog de edición */}
|
||||
{/* Diálogo de edición */}
|
||||
<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>
|
||||
{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' : ''}`}
|
||||
className={`min-h-[260px] ${editing?.key === "prompt" ? "font-mono" : ""}`}
|
||||
placeholder="Escribe aquí…"
|
||||
/>
|
||||
<DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user