Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fd816bfa1 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,13 @@ import type { AsignaturaDetail } from '@/data'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -14,6 +21,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
|
import { usePlanAsignaturas } from '@/data'
|
||||||
import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects'
|
import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects'
|
||||||
|
|
||||||
export interface BibliografiaEntry {
|
export interface BibliografiaEntry {
|
||||||
@@ -59,8 +67,12 @@ export default function AsignaturaDetailPage() {
|
|||||||
const { asignaturaId } = useParams({
|
const { asignaturaId } = useParams({
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
})
|
})
|
||||||
|
const { planId } = useParams({
|
||||||
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
})
|
||||||
const { data: asignaturaApi } = useSubject(asignaturaId)
|
const { data: asignaturaApi } = useSubject(asignaturaId)
|
||||||
|
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||||
|
usePlanAsignaturas(planId)
|
||||||
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
|
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
@@ -81,16 +93,54 @@ export default function AsignaturaDetailPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const asignaturaSeriada = useMemo(() => {
|
||||||
|
if (!asignaturaApi?.prerrequisito_asignatura_id || !asignaturasApi)
|
||||||
|
return null
|
||||||
|
return asignaturasApi.find(
|
||||||
|
(asig) => asig.id === asignaturaApi.prerrequisito_asignatura_id,
|
||||||
|
)
|
||||||
|
}, [asignaturaApi, asignaturasApi])
|
||||||
|
const requisitosFormateados = useMemo(() => {
|
||||||
|
if (!asignaturaSeriada) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'Pre-requisito',
|
||||||
|
code: asignaturaSeriada.codigo,
|
||||||
|
name: asignaturaSeriada.nombre,
|
||||||
|
id: asignaturaSeriada.id, // Guardamos el ID para el select
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [asignaturaSeriada])
|
||||||
|
|
||||||
|
const handleUpdatePrerrequisito = (newId: string | null) => {
|
||||||
|
updateAsignatura.mutate({
|
||||||
|
asignaturaId,
|
||||||
|
patch: {
|
||||||
|
prerrequisito_asignatura_id: newId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
/* ---------- sincronizar API ---------- */
|
/* ---------- sincronizar API ---------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (asignaturaApi) setAsignatura(asignaturaApi)
|
console.log(requisitosFormateados)
|
||||||
}, [asignaturaApi])
|
|
||||||
|
|
||||||
return <DatosGenerales onPersistDato={handlePersistDatoGeneral} />
|
if (asignaturaApi) setAsignatura(asignaturaApi)
|
||||||
|
}, [asignaturaApi, requisitosFormateados])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatosGenerales
|
||||||
|
pre={requisitosFormateados}
|
||||||
|
availableSubjects={asignaturasApi}
|
||||||
|
onPersistDato={handlePersistDatoGeneral}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DatosGenerales({
|
function DatosGenerales({
|
||||||
onPersistDato,
|
onPersistDato,
|
||||||
|
pre,
|
||||||
|
availableSubjects,
|
||||||
}: {
|
}: {
|
||||||
onPersistDato: (clave: string, value: string) => void
|
onPersistDato: (clave: string, value: string) => void
|
||||||
}) {
|
}) {
|
||||||
@@ -265,18 +315,19 @@ function DatosGenerales({
|
|||||||
<InfoCard
|
<InfoCard
|
||||||
title="Requisitos y Seriación"
|
title="Requisitos y Seriación"
|
||||||
type="requirements"
|
type="requirements"
|
||||||
initialContent={[
|
initialContent={pre}
|
||||||
{
|
// Pasamos las materias del plan para el Select (excluyendo la actual)
|
||||||
type: 'Pre-requisito',
|
availableSubjects={
|
||||||
code: 'PA-301',
|
availableSubjects?.filter((a) => a.id !== asignaturaId) || []
|
||||||
name: 'Programación Avanzada',
|
}
|
||||||
|
onPersist={({ value }) => {
|
||||||
|
updateAsignatura.mutate({
|
||||||
|
asignaturaId,
|
||||||
|
patch: {
|
||||||
|
prerrequisito_asignatura_id: value, // value ya viene como ID o null desde handleSave
|
||||||
},
|
},
|
||||||
{
|
})
|
||||||
type: 'Co-requisito',
|
}}
|
||||||
code: 'MAT-201',
|
|
||||||
name: 'Matemáticas Discretas',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tarjeta de Evaluación */}
|
{/* Tarjeta de Evaluación */}
|
||||||
@@ -316,6 +367,7 @@ interface InfoCardProps {
|
|||||||
containerRef?: React.RefObject<HTMLDivElement | null>
|
containerRef?: React.RefObject<HTMLDivElement | null>
|
||||||
forceEditToken?: number
|
forceEditToken?: number
|
||||||
highlightToken?: number
|
highlightToken?: number
|
||||||
|
availableSubjects?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoCard({
|
function InfoCard({
|
||||||
@@ -332,6 +384,7 @@ function InfoCard({
|
|||||||
containerRef,
|
containerRef,
|
||||||
forceEditToken,
|
forceEditToken,
|
||||||
highlightToken,
|
highlightToken,
|
||||||
|
availableSubjects,
|
||||||
}: InfoCardProps) {
|
}: InfoCardProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||||
@@ -349,7 +402,8 @@ function InfoCard({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(initialContent)
|
setData(initialContent)
|
||||||
setTempText(initialContent)
|
setTempText(initialContent)
|
||||||
|
console.log(data)
|
||||||
|
console.log(initialContent)
|
||||||
if (type === 'evaluation') {
|
if (type === 'evaluation') {
|
||||||
const raw = Array.isArray(initialContent) ? initialContent : []
|
const raw = Array.isArray(initialContent) ? initialContent : []
|
||||||
const rows: Array<CriterioEvaluacionRowDraft> = raw
|
const rows: Array<CriterioEvaluacionRowDraft> = raw
|
||||||
@@ -392,6 +446,8 @@ function InfoCard({
|
|||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
console.log('clave, valor:', clave, String(tempText ?? ''))
|
console.log('clave, valor:', clave, String(tempText ?? ''))
|
||||||
|
console.log(clave)
|
||||||
|
console.log(tempText)
|
||||||
|
|
||||||
if (type === 'evaluation') {
|
if (type === 'evaluation') {
|
||||||
const cleaned: Array<CriterioEvaluacionRow> = []
|
const cleaned: Array<CriterioEvaluacionRow> = []
|
||||||
@@ -422,6 +478,25 @@ function InfoCard({
|
|||||||
void onPersist?.({ type, clave, value: cleaned })
|
void onPersist?.({ type, clave, value: cleaned })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (type === 'requirements') {
|
||||||
|
console.log('entre aqui ')
|
||||||
|
|
||||||
|
// Si tempText es un array y tiene elementos, tomamos el ID del primero
|
||||||
|
// Si es "none" o está vacío, mandamos null (para limpiar la seriación)
|
||||||
|
const prerequisiteId =
|
||||||
|
Array.isArray(tempText) && tempText.length > 0 ? tempText[0].id : null
|
||||||
|
|
||||||
|
setData(tempText) // Actualiza la vista local
|
||||||
|
setIsEditing(false)
|
||||||
|
|
||||||
|
// Mandamos el ID específico a la base de datos
|
||||||
|
void onPersist?.({
|
||||||
|
type,
|
||||||
|
clave: 'prerrequisito_asignatura_id', // Forzamos la columna correcta
|
||||||
|
value: prerequisiteId,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setData(tempText)
|
setData(tempText)
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
@@ -541,7 +616,52 @@ function InfoCard({
|
|||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{type === 'evaluation' ? (
|
{/* Condicionales de edición según el tipo */}
|
||||||
|
{type === 'requirements' ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-medium text-slate-500">
|
||||||
|
Materia de Seriación
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={tempText?.[0]?.id || 'none'}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
const selected = availableSubjects?.find(
|
||||||
|
(s) => s.id === val,
|
||||||
|
)
|
||||||
|
if (val === 'none' || !selected) {
|
||||||
|
console.log('guardando')
|
||||||
|
|
||||||
|
setTempText([])
|
||||||
|
} else {
|
||||||
|
console.log('hola')
|
||||||
|
|
||||||
|
setTempText([
|
||||||
|
{
|
||||||
|
id: selected.id,
|
||||||
|
type: 'Pre-requisito',
|
||||||
|
code: selected.codigo,
|
||||||
|
name: selected.nombre,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Selecciona una materia" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">
|
||||||
|
Ninguna (Sin seriación)
|
||||||
|
</SelectItem>
|
||||||
|
{availableSubjects?.map((asig) => (
|
||||||
|
<SelectItem key={asig.id} value={asig.id}>
|
||||||
|
{asig.codigo} - {asig.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
) : type === 'evaluation' ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{evalRows.map((row) => (
|
{evalRows.map((row) => (
|
||||||
@@ -563,85 +683,36 @@ function InfoCard({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={row.porcentaje}
|
value={row.porcentaje}
|
||||||
placeholder="%"
|
placeholder="%"
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const raw = e.target.value
|
const raw = e.target.value
|
||||||
// Solo permitir '' o dígitos
|
|
||||||
if (raw !== '' && !/^\d+$/.test(raw)) return
|
if (raw !== '' && !/^\d+$/.test(raw)) return
|
||||||
|
|
||||||
if (raw === '') {
|
|
||||||
setEvalRows((prev) =>
|
|
||||||
prev.map((r) =>
|
|
||||||
r.id === row.id
|
|
||||||
? {
|
|
||||||
id: r.id,
|
|
||||||
criterio: r.criterio,
|
|
||||||
porcentaje: '',
|
|
||||||
}
|
|
||||||
: r,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = Number(raw)
|
|
||||||
if (!Number.isFinite(n)) return
|
|
||||||
const porcentaje = Math.trunc(n)
|
|
||||||
if (porcentaje < 1 || porcentaje > 100) return
|
|
||||||
|
|
||||||
// No permitir suma > 100
|
|
||||||
setEvalRows((prev) => {
|
setEvalRows((prev) => {
|
||||||
const next = prev.map((r) =>
|
const next = prev.map((r) =>
|
||||||
r.id === row.id
|
r.id === row.id ? { ...r, porcentaje: raw } : r,
|
||||||
? {
|
)
|
||||||
id: r.id,
|
const total = next.reduce(
|
||||||
criterio: r.criterio,
|
(acc, r) => acc + (Number(r.porcentaje) || 0),
|
||||||
porcentaje: raw,
|
0,
|
||||||
}
|
|
||||||
: r,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const total = next.reduce((acc, r) => {
|
|
||||||
const v = String(r.porcentaje).trim()
|
|
||||||
if (!v) return acc
|
|
||||||
const nn = Number(v)
|
|
||||||
if (!Number.isFinite(nn)) return acc
|
|
||||||
const vv = Math.trunc(nn)
|
|
||||||
if (vv < 1 || vv > 100) return acc
|
|
||||||
return acc + vv
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return total > 100 ? prev : next
|
return total > 100 ? prev : next
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className="text-sm text-slate-600">%</div>
|
||||||
<div
|
|
||||||
className="flex w-[1ch] items-center justify-center text-sm text-slate-600"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-red-600 hover:bg-red-50"
|
className="h-8 w-8 text-red-600 hover:bg-red-50"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
setEvalRows((prev) =>
|
setEvalRows((prev) =>
|
||||||
prev.filter((r) => r.id !== row.id),
|
prev.filter((r) => r.id !== row.id),
|
||||||
)
|
)
|
||||||
}}
|
}
|
||||||
aria-label="Quitar renglón"
|
|
||||||
title="Quitar"
|
|
||||||
>
|
>
|
||||||
<Minus className="h-4 w-4" />
|
<Minus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -651,22 +722,15 @@ function InfoCard({
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span
|
<span
|
||||||
className={
|
className={`text-sm ${evaluationTotal === 100 ? 'text-muted-foreground' : 'text-destructive font-semibold'}`}
|
||||||
'text-sm ' +
|
|
||||||
(evaluationTotal === 100
|
|
||||||
? 'text-muted-foreground'
|
|
||||||
: 'text-destructive font-semibold')
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Total: {evaluationTotal}/100
|
Total: {evaluationTotal}/100
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-emerald-700 hover:bg-emerald-50"
|
className="text-emerald-700 hover:bg-emerald-50"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
// Agregar una fila vacía (siempre permitido)
|
|
||||||
setEvalRows((prev) => [
|
setEvalRows((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -675,7 +739,7 @@ function InfoCard({
|
|||||||
porcentaje: '',
|
porcentaje: '',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" /> Agregar renglón
|
<Plus className="mr-2 h-4 w-4" /> Agregar renglón
|
||||||
</Button>
|
</Button>
|
||||||
@@ -689,28 +753,15 @@ function InfoCard({
|
|||||||
className="min-h-30 text-sm leading-relaxed"
|
className="min-h-30 text-sm leading-relaxed"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Botones de acción comunes */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
if (type === 'evaluation') {
|
// Lógica de reset si es necesario...
|
||||||
const raw = Array.isArray(data) ? data : []
|
|
||||||
setEvalRows(
|
|
||||||
raw.map((r: CriterioEvaluacionRow) => ({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
criterio:
|
|
||||||
typeof r.criterio === 'string' ? r.criterio : '',
|
|
||||||
porcentaje:
|
|
||||||
typeof r.porcentaje === 'number'
|
|
||||||
? String(Math.trunc(r.porcentaje))
|
|
||||||
: typeof r.porcentaje === 'string'
|
|
||||||
? String(Math.trunc(Number(r.porcentaje)))
|
|
||||||
: '',
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
@@ -726,6 +777,7 @@ function InfoCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
/* Modo Visualización */
|
||||||
<div className="text-sm leading-relaxed text-slate-600">
|
<div className="text-sm leading-relaxed text-slate-600">
|
||||||
{type === 'text' &&
|
{type === 'text' &&
|
||||||
(data ? (
|
(data ? (
|
||||||
@@ -734,9 +786,7 @@ function InfoCard({
|
|||||||
<p className="text-slate-400 italic">Sin información.</p>
|
<p className="text-slate-400 italic">Sin información.</p>
|
||||||
))}
|
))}
|
||||||
{type === 'requirements' && <RequirementsView items={data} />}
|
{type === 'requirements' && <RequirementsView items={data} />}
|
||||||
{type === 'evaluation' && (
|
{type === 'evaluation' && <EvaluationView items={data} />}
|
||||||
<EvaluationView items={data as Array<CriterioEvaluacionRow>} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
|
|||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,
|
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,prerrequisito_asignatura_id,
|
||||||
planes_estudio(
|
planes_estudio(
|
||||||
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
GripVertical,
|
||||||
Trash2,
|
Trash2,
|
||||||
Pencil,
|
Pencil,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -45,33 +46,16 @@ import {
|
|||||||
useUpdateAsignatura,
|
useUpdateAsignatura,
|
||||||
useUpdateLinea,
|
useUpdateLinea,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
||||||
const palette = [
|
|
||||||
'#4F46E5', // índigo
|
|
||||||
'#7C3AED', // violeta
|
|
||||||
'#EA580C', // naranja
|
|
||||||
'#059669', // esmeralda
|
|
||||||
'#DC2626', // rojo
|
|
||||||
'#0891B2', // cyan
|
|
||||||
'#CA8A04', // ámbar
|
|
||||||
'#C026D3', // fucsia
|
|
||||||
]
|
|
||||||
|
|
||||||
const mapLineasToLineaCurricular = (
|
const mapLineasToLineaCurricular = (
|
||||||
lineasApi: Array<any> = [],
|
lineasApi: Array<any> = [],
|
||||||
): Array<LineaCurricular> => {
|
): Array<LineaCurricular> => {
|
||||||
return lineasApi.map((linea, index) => ({
|
return lineasApi.map((linea) => ({
|
||||||
id: linea.id,
|
id: linea.id,
|
||||||
nombre: linea.nombre,
|
nombre: linea.nombre,
|
||||||
orden: linea.orden ?? 0,
|
orden: linea.orden ?? 0,
|
||||||
color: palette[index % palette.length],
|
color: '#1976d2',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,216 +121,52 @@ function StatItem({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
import * as Icons from 'lucide-react'
|
|
||||||
|
|
||||||
const estadoConfig: Record<
|
|
||||||
Asignatura['estado'],
|
|
||||||
{
|
|
||||||
label: string
|
|
||||||
dot: string
|
|
||||||
soft: string
|
|
||||||
icon: React.ComponentType<{ className?: string }>
|
|
||||||
}
|
|
||||||
> = {
|
|
||||||
borrador: {
|
|
||||||
label: 'Borrador',
|
|
||||||
dot: 'bg-slate-500',
|
|
||||||
soft: 'bg-slate-100 text-slate-700',
|
|
||||||
icon: Icons.FileText,
|
|
||||||
},
|
|
||||||
revisada: {
|
|
||||||
label: 'Revisada',
|
|
||||||
dot: 'bg-amber-500',
|
|
||||||
soft: 'bg-amber-100 text-amber-700',
|
|
||||||
icon: Icons.ScanSearch,
|
|
||||||
},
|
|
||||||
aprobada: {
|
|
||||||
label: 'Aprobada',
|
|
||||||
dot: 'bg-emerald-500',
|
|
||||||
soft: 'bg-emerald-100 text-emerald-700',
|
|
||||||
icon: Icons.BadgeCheck,
|
|
||||||
},
|
|
||||||
generando: {
|
|
||||||
label: 'Generando',
|
|
||||||
dot: 'bg-sky-500',
|
|
||||||
soft: 'bg-sky-100 text-sky-700',
|
|
||||||
icon: Icons.LoaderCircle,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgba(hex: string, alpha: number) {
|
|
||||||
const clean = hex.replace('#', '')
|
|
||||||
const bigint = parseInt(clean, 16)
|
|
||||||
const r = (bigint >> 16) & 255
|
|
||||||
const g = (bigint >> 8) & 255
|
|
||||||
const b = bigint & 255
|
|
||||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
||||||
}
|
|
||||||
|
|
||||||
function AsignaturaCardItem({
|
function AsignaturaCardItem({
|
||||||
asignatura,
|
asignatura,
|
||||||
lineaColor,
|
|
||||||
lineaNombre,
|
|
||||||
onDragStart,
|
onDragStart,
|
||||||
isDragging,
|
isDragging,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
asignatura: Asignatura
|
asignatura: Asignatura
|
||||||
lineaColor: string
|
|
||||||
lineaNombre?: string
|
|
||||||
onDragStart: (e: React.DragEvent, id: string) => void
|
onDragStart: (e: React.DragEvent, id: string) => void
|
||||||
isDragging: boolean
|
isDragging: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}) {
|
}) {
|
||||||
const estado = estadoConfig[asignatura.estado] ?? estadoConfig.borrador
|
|
||||||
const EstadoIcon = estado.icon
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={150}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
<button
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => onDragStart(e, asignatura.id)}
|
onDragStart={(e) => onDragStart(e, asignatura.id)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={[
|
className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${
|
||||||
'group relative h-[200px] w-[272px] shrink-0 overflow-hidden rounded-[22px] border text-left',
|
|
||||||
'transition-all duration-300 ease-out',
|
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30',
|
|
||||||
'active:cursor-grabbing cursor-grab',
|
|
||||||
isDragging
|
isDragging
|
||||||
? 'scale-[0.985] opacity-45 shadow-none'
|
? 'scale-95 opacity-40'
|
||||||
: 'hover:-translate-y-1 hover:shadow-lg',
|
: 'hover:border-teal-400 hover:shadow-md'
|
||||||
].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%)
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
title={asignatura.nombre}
|
|
||||||
>
|
>
|
||||||
{/* franja */}
|
<div className="mb-1 flex items-start justify-between">
|
||||||
<div
|
<span className="font-mono text-[10px] font-bold text-slate-400">
|
||||||
className="absolute inset-x-0 top-0 h-2"
|
{asignatura.clave}
|
||||||
style={{ backgroundColor: lineaColor }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* glow decorativo */}
|
|
||||||
<div
|
|
||||||
className="absolute -top-10 -right-10 h-28 w-28 rounded-full blur-2xl transition-transform duration-500 group-hover:scale-110"
|
|
||||||
style={{ backgroundColor: hexToRgba(lineaColor, 0.22) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative flex h-full flex-col p-4">
|
|
||||||
{/* top */}
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div
|
|
||||||
className="inline-flex h-8 max-w-[200px] items-center gap-1.5 rounded-full border px-2.5 text-[11px] font-semibold"
|
|
||||||
style={{
|
|
||||||
borderColor: hexToRgba(lineaColor, 0.2),
|
|
||||||
backgroundColor: hexToRgba(lineaColor, 0.1),
|
|
||||||
color: lineaColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.KeyRound className="h-3.5 w-3.5 shrink-0" />
|
|
||||||
<span className="truncate">{asignatura.clave || 'Sin clave'}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex h-8 items-center overflow-hidden rounded-full bg-background/70 px-2 backdrop-blur-sm">
|
|
||||||
<div className="flex gap-4 items-center gap-1.5 transition-transform duration-300 group-hover:-translate-x-[72px]">
|
|
||||||
<span className={`h-2.5 w-2.5 rounded-full ${estado.dot}`} />
|
|
||||||
<EstadoIcon
|
|
||||||
className={[
|
|
||||||
'h-3.5 w-3.5 text-foreground/65',
|
|
||||||
asignatura.estado === 'generando' ? 'animate-spin' : '',
|
|
||||||
].join(' ')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
'absolute right-2 flex translate-x-6 items-center gap-1.5 opacity-0 transition-all duration-300',
|
|
||||||
'group-hover:translate-x-0 group-hover:opacity-100'
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
<span className="text-[11px] font-semibold whitespace-nowrap">
|
|
||||||
{estado.label}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<Badge
|
||||||
|
variant="outline"
|
||||||
</div>
|
className={`px-1 py-0 text-[9px] uppercase ${statusBadge[asignatura.estado] || ''}`}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* titulo */}
|
|
||||||
<div className="mt-4 min-h-[72px]">
|
|
||||||
<h3
|
|
||||||
className="overflow-hidden text-[18px] leading-[1.08] font-bold text-foreground"
|
|
||||||
style={{
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 3,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{asignatura.estado}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mb-1 text-xs leading-tight font-bold text-slate-700">
|
||||||
{asignatura.nombre}
|
{asignatura.nombre}
|
||||||
</h3>
|
</p>
|
||||||
</div>
|
<div className="mt-2 flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-slate-500">
|
||||||
{/* bottom */}
|
{asignatura.creditos} CR • HD:{asignatura.hd} • HI:{asignatura.hi}
|
||||||
<div className="mt-auto grid grid-cols-3 gap-2">
|
|
||||||
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
|
||||||
<Icons.Award className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
|
||||||
CR
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<GripVertical
|
||||||
<div className="text-sm font-bold text-foreground">
|
size={12}
|
||||||
{asignatura.creditos}
|
className="text-slate-300 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
|
||||||
<Icons.Clock3 className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
|
||||||
HD
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm font-bold text-foreground">
|
|
||||||
{asignatura.hd}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
|
||||||
<Icons.BookOpenText className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
|
||||||
HI
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm font-bold text-foreground">
|
|
||||||
{asignatura.hi}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* drag affordance */}
|
|
||||||
<div className="pointer-events-none absolute right-3 bottom-3 rounded-full bg-background/70 p-1.5 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:opacity-100">
|
|
||||||
<Icons.GripVertical className="h-4 w-4 text-muted-foreground/55" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
<div className="text-xs">
|
|
||||||
{lineaNombre ? `${lineaNombre} · ` : ''}
|
|
||||||
{asignatura.nombre}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +618,8 @@ function MapaCurricularPage() {
|
|||||||
return (
|
return (
|
||||||
<Fragment key={linea.id}>
|
<Fragment key={linea.id}>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${lineColors[idx % lineColors.length]
|
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${
|
||||||
|
lineColors[idx % lineColors.length]
|
||||||
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
@@ -814,7 +635,8 @@ function MapaCurricularPage() {
|
|||||||
setTempNombreLinea(linea.nombre)
|
setTempNombreLinea(linea.nombre)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`block w-full text-xs font-bold break-words outline-none ${editingLineaId === linea.id
|
className={`block w-full text-xs font-bold break-words outline-none ${
|
||||||
|
editingLineaId === linea.id
|
||||||
? 'cursor-text border-b border-teal-500/50 pb-1'
|
? 'cursor-text border-b border-teal-500/50 pb-1'
|
||||||
: 'cursor-pointer'
|
: 'cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
@@ -855,8 +677,6 @@ function MapaCurricularPage() {
|
|||||||
<AsignaturaCardItem
|
<AsignaturaCardItem
|
||||||
key={m.id}
|
key={m.id}
|
||||||
asignatura={m}
|
asignatura={m}
|
||||||
lineaColor={linea.color || '#1976d2'}
|
|
||||||
lineaNombre={linea.nombre}
|
|
||||||
isDragging={draggedAsignatura === m.id}
|
isDragging={draggedAsignatura === m.id}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -907,81 +727,45 @@ function MapaCurricularPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Asignaturas Sin Asignar */}
|
{/* Asignaturas Sin Asignar */}
|
||||||
<div className="mt-12 rounded-[28px] border border-border bg-card/80 p-5 shadow-sm backdrop-blur-sm">
|
<div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6">
|
||||||
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="min-w-0">
|
<div className="flex items-center gap-2 text-slate-600">
|
||||||
<div className="flex items-center gap-2">
|
<h3 className="text-sm font-bold tracking-wider uppercase">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
Bandeja de Entrada / Asignaturas sin asignar
|
||||||
<Icons.Inbox className="h-4.5 w-4.5" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="text-sm font-bold tracking-wide text-foreground uppercase">
|
|
||||||
Bandeja de entrada
|
|
||||||
</h3>
|
</h3>
|
||||||
|
<Badge variant="secondary">{unassignedAsignaturas.length}</Badge>
|
||||||
<div className="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-muted px-2 text-[11px] font-semibold text-muted-foreground">
|
|
||||||
{unassignedAsignaturas.length}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p className="text-xs text-slate-400">
|
||||||
|
Arrastra una asignatura aquí para quitarla del mapa
|
||||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
||||||
Asignaturas sin ciclo o línea curricular
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 rounded-full border border-dashed border-border bg-background/80 px-3 py-1.5 text-xs text-muted-foreground">
|
|
||||||
<Icons.MoveDown className="h-3.5 w-3.5" />
|
|
||||||
<span>Arrastra aquí para desasignar</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onDragOver={handleDragOver}
|
className={`flex min-h-[120px] flex-wrap gap-4 rounded-xl border-2 border-dashed p-4 transition-colors ${
|
||||||
onDrop={(e) => handleDrop(e, null, null)}
|
|
||||||
className={[
|
|
||||||
'rounded-[24px] border-2 border-dashed p-4 transition-all duration-300',
|
|
||||||
'min-h-[220px]',
|
|
||||||
draggedAsignatura
|
draggedAsignatura
|
||||||
? 'border-primary/35 bg-primary/6 shadow-inner'
|
? 'border-teal-300 bg-teal-50/50'
|
||||||
: 'border-border bg-muted/20',
|
: 'border-slate-200 bg-white/50'
|
||||||
].join(' ')}
|
}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={(e) => handleDrop(e, null, null)} // Limpia ciclo y línea
|
||||||
>
|
>
|
||||||
{unassignedAsignaturas.length > 0 ? (
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
{unassignedAsignaturas.map((m) => (
|
{unassignedAsignaturas.map((m) => (
|
||||||
<div key={m.id} className="w-[272px] shrink-0">
|
<div key={m.id} className="w-[200px]">
|
||||||
<AsignaturaCardItem
|
<AsignaturaCardItem
|
||||||
asignatura={m}
|
asignatura={m}
|
||||||
lineaColor="#94A3B8"
|
|
||||||
lineaNombre="Sin asignar"
|
|
||||||
isDragging={draggedAsignatura === m.id}
|
isDragging={draggedAsignatura === m.id}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingData(m)
|
setEditingData(m) // Cargamos los datos en el estado de edición
|
||||||
setIsEditModalOpen(true)
|
setIsEditModalOpen(true)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
{unassignedAsignaturas.length === 0 && (
|
||||||
) : (
|
<div className="flex w-full items-center justify-center text-sm text-slate-400">
|
||||||
<div className="flex min-h-[188px] flex-col items-center justify-center rounded-[20px] border border-border/70 bg-background/70 px-6 text-center">
|
No hay asignaturas pendientes. Arrastra una asignatura aquí para
|
||||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
desasignarla.
|
||||||
<Icons.CheckCheck className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm font-semibold text-foreground">
|
|
||||||
No hay asignaturas pendientes
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-1 max-w-md text-sm text-muted-foreground">
|
|
||||||
Todo está colocado en el mapa. Arrastra una asignatura aquí para quitarle
|
|
||||||
ciclo y línea curricular.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+12
-139
@@ -4,145 +4,18 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-Light.otf') format('opentype');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-LightItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-Regular.otf') format('opentype');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-RegularItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-Bold.otf') format('opentype');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-BoldItalic.otf') format('opentype');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-Black.otf') format('opentype');
|
|
||||||
font-weight: 900;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Sans';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSans-BlackItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 900;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Serif */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-Light.otf') format('opentype');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-LightItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-Regular.otf') format('opentype');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-RegularItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-Bold.otf') format('opentype');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-BoldItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-Black.otf') format('opentype');
|
|
||||||
font-weight: 900;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Indivisa Serif';
|
|
||||||
src: url('/fonts/indivisa/IndivisaTextSerif-BlackItalic.otf')
|
|
||||||
format('opentype');
|
|
||||||
font-weight: 900;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply m-0;
|
@apply m-0;
|
||||||
font-family: var(--font-sans);
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||||
|
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: var(--font-mono);
|
font-family:
|
||||||
}
|
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
|
|
||||||
strong,
|
|
||||||
b,
|
|
||||||
.font-bold {
|
|
||||||
font-family: 'Indivisa Sans', serif;
|
|
||||||
font-weight: 900;
|
|
||||||
/* Inter letter space */
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -178,9 +51,9 @@ b,
|
|||||||
--sidebar-accent-foreground: oklch(0.6304 0.2472 28.2698);
|
--sidebar-accent-foreground: oklch(0.6304 0.2472 28.2698);
|
||||||
--sidebar-border: oklch(0.9401 0 0);
|
--sidebar-border: oklch(0.9401 0 0);
|
||||||
--sidebar-ring: oklch(0 0 0);
|
--sidebar-ring: oklch(0 0 0);
|
||||||
--font-sans: 'Indivisa Sans', sans-serif;
|
--font-sans: Plus Jakarta Sans, sans-serif;
|
||||||
--font-serif: 'Indivisa Serif', serif;
|
--font-serif: Lora, serif;
|
||||||
--font-mono: 'Indivisa Sans', monospace;
|
--font-mono: IBM Plex Mono, monospace;
|
||||||
--radius: 1.4rem;
|
--radius: 1.4rem;
|
||||||
--shadow-x: 0px;
|
--shadow-x: 0px;
|
||||||
--shadow-y: 2px;
|
--shadow-y: 2px;
|
||||||
@@ -228,7 +101,7 @@ b,
|
|||||||
--chart-1: oklch(0.6686 0.1794 251.7436);
|
--chart-1: oklch(0.6686 0.1794 251.7436);
|
||||||
--chart-2: oklch(0.6342 0.2516 22.4415);
|
--chart-2: oklch(0.6342 0.2516 22.4415);
|
||||||
--chart-3: oklch(0.8718 0.1716 90.9505);
|
--chart-3: oklch(0.8718 0.1716 90.9505);
|
||||||
--chart-4: oklch(11.492% 0.00001 271.152);
|
--chart-4: oklch(0.4503 0.229 263.0881);
|
||||||
--chart-5: oklch(0.8322 0.146 185.9404);
|
--chart-5: oklch(0.8322 0.146 185.9404);
|
||||||
--sidebar: oklch(0.1564 0.0688 261.2771);
|
--sidebar: oklch(0.1564 0.0688 261.2771);
|
||||||
--sidebar-foreground: oklch(0.9551 0 0);
|
--sidebar-foreground: oklch(0.9551 0 0);
|
||||||
@@ -238,9 +111,9 @@ b,
|
|||||||
--sidebar-accent-foreground: oklch(0.6786 0.2095 24.6583);
|
--sidebar-accent-foreground: oklch(0.6786 0.2095 24.6583);
|
||||||
--sidebar-border: oklch(0.3289 0.0092 268.3843);
|
--sidebar-border: oklch(0.3289 0.0092 268.3843);
|
||||||
--sidebar-ring: oklch(0.6048 0.2166 257.2136);
|
--sidebar-ring: oklch(0.6048 0.2166 257.2136);
|
||||||
--font-sans: 'Indivisa Sans', sans-serif;
|
--font-sans: Plus Jakarta Sans, sans-serif;
|
||||||
--font-serif: 'Indivisa Serif', serif;
|
--font-serif: Lora, serif;
|
||||||
--font-mono: 'Indivisa Sans', monospace;
|
--font-mono: IBM Plex Mono, monospace;
|
||||||
--radius: 1.4rem;
|
--radius: 1.4rem;
|
||||||
--shadow-x: 0px;
|
--shadow-x: 0px;
|
||||||
--shadow-y: 2px;
|
--shadow-y: 2px;
|
||||||
|
|||||||
Reference in New Issue
Block a user