Rediseño impeccable v2: refresh visual de todo el sistema

Segunda iteración del rediseño (sesión separada con skill impeccable).
Toca prácticamente toda la UI: layout base, tokens (global.css),
todas las páginas de alumno y admin, componentes de solicitudes,
inventario, catálogo, filtros, toaster y modales.

Docs y sistema:
- DESIGN.md y PRODUCT.md actualizados con la nueva iteración.
- .impeccable/design.json refleja tokens vigentes.
- .impeccable/review/ contiene screenshots de la revisión visual.

Dependencias:
- Añade @fontsource-variable/jetbrains-mono (fuente monoespaciada).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 17:11:20 -07:00
parent d996be5cf8
commit 0247b979ca
36 changed files with 511 additions and 468 deletions
+8 -8
View File
@@ -5,11 +5,11 @@ type Item = ToastPayload & { id: number; leaving?: boolean };
const KIND_META: Record<ToastKind, { color: string; d: string }> = {
success: {
color: 'var(--color-primary)',
color: 'var(--color-positive)',
d: 'M4.5 12.75l6 6 9-13.5',
},
error: {
color: 'var(--color-danger)',
color: 'var(--color-danger-text)',
d: 'M6 18L18 6M6 6l12 12',
},
info: {
@@ -71,22 +71,22 @@ export default function Toaster() {
onClick={() => dismiss(t.id, true)}
aria-live="polite"
className={
'toast-item pointer-events-auto text-left flex items-start gap-3 rounded-2xl px-4 py-3 ' +
'bg-white/85 backdrop-blur-xl border shadow-[0_16px_40px_-12px_rgba(0,0,0,0.25)] ' +
'toast-item pointer-events-auto text-left flex items-start gap-3 rounded-[2px] px-4 py-3 bg-white ' +
'motion-safe:transition-[opacity,transform] motion-safe:duration-200 ' +
(t.leaving ? 'toast-leave' : 'toast-enter')
}
style={{
borderColor: 'color-mix(in oklab, var(--color-ink) 10%, transparent)',
border: '2px solid var(--color-ink)',
boxShadow: 'var(--shadow-hard)',
color: 'var(--color-ink)',
}}
>
<span
aria-hidden="true"
className="shrink-0 grid place-items-center w-9 h-9 rounded-full text-white"
style={{ background: meta.color }}
className="shrink-0 grid place-items-center w-9 h-9 rounded-[2px]"
style={{ background: meta.color, border: '2px solid var(--color-ink)' }}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-ink)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d={meta.d} />
</svg>
</span>
@@ -81,7 +81,7 @@ export default function CategoriaForm({ mode, categoria }: Props) {
<dialog
ref={dialogRef}
aria-labelledby={titleId}
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] backdrop:bg-black/40 w-[min(92vw,24rem)]"
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40 w-[min(92vw,24rem)]"
>
<form onSubmit={submit} className="p-5 sm:p-6 flex flex-col gap-4" autoComplete="off">
<div className="flex items-start justify-between gap-3">
@@ -123,7 +123,7 @@ export default function CategoriaForm({ mode, categoria }: Props) {
role="alert"
tabIndex={-1}
className="text-sm"
style={{ color: 'var(--color-danger)' }}
style={{ color: 'var(--color-danger-text)' }}
>
{error}
</p>
@@ -72,7 +72,7 @@ export default function EliminarCategoria({ id, nombre, materialesCount }: Props
<dialog
ref={dialogRef}
aria-labelledby={titleId}
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] backdrop:bg-black/40 w-[min(92vw,28rem)]"
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40 w-[min(92vw,28rem)]"
>
<div className="p-5 sm:p-6 flex flex-col gap-4">
<h2 id={titleId} className="text-lg font-semibold">
@@ -102,7 +102,7 @@ export default function EliminarCategoria({ id, nombre, materialesCount }: Props
)}
{error && (
<p role="alert" className="text-sm" style={{ color: 'var(--color-danger)' }}>
<p role="alert" className="text-sm" style={{ color: 'var(--color-danger-text)' }}>
{error}
</p>
)}
@@ -115,7 +115,7 @@ export default function EliminarCategoria({ id, nombre, materialesCount }: Props
<button
type="button"
className="btn"
style={{ background: 'var(--color-danger)', color: 'white' }}
style={{ background: 'var(--color-danger)', color: 'var(--color-ink)' }}
onClick={submit}
disabled={loading}
>
@@ -53,7 +53,7 @@ export default function EliminarMaterial({ id, nombre, tienePrestamos }: Props)
<>
<button
type="button"
className="btn btn-ghost hover:!bg-[color:var(--color-danger)] hover:!text-white"
className="btn btn-ghost hover:!bg-[color:var(--color-danger)] hover:!text-[color:var(--color-ink)]"
style={{ minHeight: '36px', paddingBlock: '0.25rem' }}
onClick={open}
>
@@ -63,7 +63,7 @@ export default function EliminarMaterial({ id, nombre, tienePrestamos }: Props)
<dialog
ref={dialogRef}
aria-labelledby={titleId}
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] backdrop:bg-black/40 w-[min(92vw,26rem)]"
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40 w-[min(92vw,26rem)]"
>
<div className="p-5 sm:p-6 flex flex-col gap-4">
<h2 id={titleId} className="text-lg font-semibold">
@@ -73,12 +73,12 @@ export default function EliminarMaterial({ id, nombre, tienePrestamos }: Props)
¿Seguro que quieres eliminar <strong>{nombre}</strong>? Esta acción no se puede deshacer.
</p>
{tienePrestamos && (
<p className="text-sm" style={{ color: 'var(--color-danger)' }}>
<p className="text-sm" style={{ color: 'var(--color-danger-text)' }}>
Este material tiene préstamos asociados y no podrá eliminarse.
</p>
)}
{error && (
<p role="alert" className="text-sm" style={{ color: 'var(--color-danger)' }}>
<p role="alert" className="text-sm" style={{ color: 'var(--color-danger-text)' }}>
{error}
</p>
)}
@@ -89,7 +89,7 @@ export default function EliminarMaterial({ id, nombre, tienePrestamos }: Props)
<button
type="button"
className="btn"
style={{ background: 'var(--color-danger)', color: 'white' }}
style={{ background: 'var(--color-danger)', color: 'var(--color-ink)' }}
onClick={submit}
disabled={loading}
>
@@ -132,7 +132,7 @@ export default function MaterialForm({ mode, material, categorias }: Props) {
<dialog
ref={dialogRef}
aria-labelledby={titleId}
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] backdrop:bg-black/40 w-[min(92vw,32rem)]"
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40 w-[min(92vw,32rem)]"
>
<form onSubmit={submit} className="p-5 sm:p-6 flex flex-col gap-4" autoComplete="off">
<div className="flex items-start justify-between gap-3">
@@ -269,7 +269,7 @@ export default function MaterialForm({ mode, material, categorias }: Props) {
role="alert"
tabIndex={-1}
className="text-sm"
style={{ color: 'var(--color-danger)' }}
style={{ color: 'var(--color-danger-text)' }}
>
{error}
</p>
@@ -80,7 +80,7 @@ function AprobarDialog({ prestamo }: { prestamo: Prestamo }) {
return (
<>
<button type="button" className="btn btn-primary" onClick={dlg.open}>Aprobar</button>
<dialog ref={dlg.ref} className="rounded-lg p-0 max-w-md w-[calc(100%-2rem)] backdrop:bg-black/40">
<dialog ref={dlg.ref} className="rounded-lg p-0 max-w-md w-[calc(100%-2rem)] bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40">
<form onSubmit={submit} className="p-6">
<h2 className="text-lg font-semibold">Aprobar solicitud</h2>
<p className="text-sm opacity-70 mt-1">
@@ -88,7 +88,7 @@ function AprobarDialog({ prestamo }: { prestamo: Prestamo }) {
<strong>{prestamo.cantidad}</strong> de <strong>{prestamo.material?.nombre ?? '—'}</strong>.
</p>
{stockInsuficiente && (
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger)' }}>
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger-text)' }}>
Stock disponible ({prestamo.material?.cantidad_disponible ?? 0}) menor a lo solicitado.
</div>
)}
@@ -117,7 +117,7 @@ function AprobarDialog({ prestamo }: { prestamo: Prestamo }) {
/>
</div>
{error && (
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger)' }}>
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger-text)' }}>
{error}
</div>
)}
@@ -178,7 +178,7 @@ function RechazarDialog({ prestamo }: { prestamo: Prestamo }) {
>
Rechazar
</button>
<dialog ref={dlg.ref} className="rounded-lg p-0 max-w-md w-[calc(100%-2rem)] backdrop:bg-black/40">
<dialog ref={dlg.ref} className="rounded-lg p-0 max-w-md w-[calc(100%-2rem)] bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40">
<form onSubmit={submit} className="p-6">
<h2 className="text-lg font-semibold">Rechazar solicitud</h2>
<p className="text-sm opacity-70 mt-1">
@@ -199,7 +199,7 @@ function RechazarDialog({ prestamo }: { prestamo: Prestamo }) {
/>
</div>
{error && (
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger)' }}>
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger-text)' }}>
{error}
</div>
)}
@@ -209,7 +209,7 @@ function RechazarDialog({ prestamo }: { prestamo: Prestamo }) {
type="submit"
className="btn"
disabled={loading}
style={{ background: 'var(--color-danger)', color: 'white' }}
style={{ background: 'var(--color-danger)', color: 'var(--color-ink)' }}
>
{loading ? 'Rechazando…' : 'Rechazar solicitud'}
</button>
@@ -43,14 +43,14 @@ export default function MarcarDevuelto({ prestamoId, nombreMaterial }: { prestam
<button type="button" className="btn btn-secondary" onClick={() => ref.current?.showModal()}>
Marcar devuelto
</button>
<dialog ref={ref} className="rounded-lg p-0 max-w-md w-[calc(100%-2rem)] backdrop:bg-black/40">
<dialog ref={ref} className="rounded-lg p-0 max-w-md w-[calc(100%-2rem)] bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40">
<form onSubmit={submit} className="p-6">
<h2 className="text-lg font-semibold">Confirmar devolución</h2>
<p className="text-sm opacity-80 mt-2">
¿Confirmas la devolución de <strong>{nombreMaterial}</strong>? El material regresará al inventario disponible.
</p>
{error && (
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger)' }}>
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger-text)' }}>
{error}
</div>
)}
@@ -51,7 +51,7 @@ export default function VerDetalles({ prestamoId }: { prestamoId: number }) {
return (
<>
<button type="button" className="btn btn-ghost" onClick={open}>Detalles</button>
<dialog ref={ref} className="rounded-lg p-0 max-w-lg w-[calc(100%-2rem)] backdrop:bg-black/40">
<dialog ref={ref} className="rounded-lg p-0 max-w-lg w-[calc(100%-2rem)] bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40">
<div className="p-6">
<div className="flex items-start justify-between gap-4">
<h2 className="text-lg font-semibold">Detalle del préstamo</h2>
@@ -64,7 +64,7 @@ export default function VerDetalles({ prestamoId }: { prestamoId: number }) {
{loading && <p className="mt-4 text-sm opacity-70">Cargando</p>}
{error && (
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger)' }}>
<div role="alert" className="mt-3 rounded-lg p-2 text-sm" style={{ background: 'color-mix(in oklab, var(--color-danger) 12%, transparent)', color: 'var(--color-danger-text)' }}>
{error}
</div>
)}
+1 -1
View File
@@ -46,7 +46,7 @@ export default function FiltroCategorias({ categorias }: Props) {
const chipStyle = (v: string): React.CSSProperties =>
v === active
? { background: 'var(--color-primary)', color: 'white', minHeight: '40px', paddingBlock: '0.375rem' }
? { background: 'var(--color-primary)', color: 'var(--color-ink)', minHeight: '40px', paddingBlock: '0.375rem' }
: { minHeight: '40px', paddingBlock: '0.375rem' };
return (
+3 -3
View File
@@ -95,7 +95,7 @@ export default function SolicitarModal({ material }: Props) {
<dialog
ref={dialogRef}
aria-labelledby={titleId}
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] backdrop:bg-black/40 w-[min(92vw,28rem)]"
className="rounded-lg p-0 bg-white text-[color:var(--color-ink)] border-2 border-[color:var(--color-ink)] shadow-[var(--shadow-hard)] backdrop:bg-black/40 w-[min(92vw,28rem)]"
>
<form onSubmit={submit} className="p-5 sm:p-6 flex flex-col gap-4">
<div className="flex items-start justify-between gap-3">
@@ -155,12 +155,12 @@ export default function SolicitarModal({ material }: Props) {
</div>
{error && (
<p role="alert" className="text-sm" style={{ color: 'var(--color-danger)' }}>
<p role="alert" className="text-sm" style={{ color: 'var(--color-danger-text)' }}>
{error} · Ajusta la cantidad o intenta de nuevo.
</p>
)}
{ok && (
<p role="status" className="text-sm" style={{ color: 'var(--color-primary)' }}>
<p role="status" className="text-sm" style={{ color: 'var(--color-positive)' }}>
Solicitud enviada. Actualizando
</p>
)}