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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user