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>
|
||||
)}
|
||||
|
||||
+46
-42
@@ -61,26 +61,25 @@ const activeCheck = (item: NavItem) => {
|
||||
<div class="min-h-screen flex flex-col md:block">
|
||||
|
||||
<!-- Sidebar desktop -->
|
||||
<aside id="app-sidebar" class="hidden md:flex md:fixed md:top-0 md:left-0 md:w-64 md:h-screen bg-[color:var(--color-primary)] text-white md:flex-col md:z-40 sidebar-panel transition-transform duration-200 ease-out">
|
||||
<a href="/" class="p-6 flex items-center gap-3 border-b border-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" aria-label="LabPréstamos — Inicio">
|
||||
<div class="w-9 h-9 rounded-lg bg-white/15 grid place-items-center p-2" aria-hidden="true">
|
||||
<BrandMark class="w-full h-full text-white" />
|
||||
<aside id="app-sidebar" class="hidden md:flex md:fixed md:top-0 md:left-0 md:w-64 md:h-screen bg-white text-[color:var(--color-ink)] md:flex-col md:z-40 sidebar-panel transition-transform duration-200 ease-out" style="border-right: 2px solid var(--color-ink);">
|
||||
<a href="/" class="p-6 flex items-center gap-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-ink)]" aria-label="LabPréstamos — Inicio" style="border-bottom: 2px solid var(--color-ink);">
|
||||
<div class="w-9 h-9 rounded-[2px] bg-[color:var(--color-primary)] grid place-items-center p-2" style="border: 2px solid var(--color-ink);" aria-hidden="true">
|
||||
<BrandMark class="w-full h-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-tight">LabPréstamos</div>
|
||||
<div class="text-xs text-white/70">UABC · Lab. Sistemas</div>
|
||||
<div class="font-semibold leading-tight uppercase tracking-wide text-sm">LabPréstamos</div>
|
||||
<div class="text-xs" style="color: var(--color-pencil);">UABC · Lab. Sistemas</div>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="flex-1 p-3 flex flex-col gap-1" aria-label="Navegación principal">
|
||||
<nav class="flex-1 p-3 flex flex-col gap-1.5" aria-label="Navegación principal">
|
||||
{nav.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
|
||||
activeCheck(item)
|
||||
? 'bg-white/15 text-white'
|
||||
: 'text-white/85 hover:bg-white/10 hover:text-white'
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-[2px] text-sm font-medium uppercase tracking-wide whitespace-nowrap transition-colors',
|
||||
activeCheck(item) ? 'bg-[color:var(--color-primary)]' : 'hover:bg-[color:var(--color-chalk)]'
|
||||
]}
|
||||
style={activeCheck(item) ? 'border: 2px solid var(--color-ink);' : 'border: 2px solid transparent;'}
|
||||
aria-current={activeCheck(item) ? 'page' : undefined}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
@@ -90,13 +89,13 @@ const activeCheck = (item: NavItem) => {
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<div class="p-3 border-t border-white/10">
|
||||
<div class="p-3" style="border-top: 2px solid var(--color-ink);">
|
||||
<div class="px-3 py-2 text-sm">
|
||||
<div class="font-medium truncate">{profile?.nombre ?? profile?.email}</div>
|
||||
<div class="text-white/70 text-xs capitalize">{profile?.rol}</div>
|
||||
<div class="text-xs uppercase tracking-wide" style="color: var(--color-pencil);">{profile?.rol}</div>
|
||||
</div>
|
||||
<form method="POST" action="/api/auth/signout">
|
||||
<button type="submit" class="w-full text-left px-3 py-2 rounded-lg text-sm hover:bg-white/10 transition-colors flex items-center gap-2">
|
||||
<button type="submit" class="w-full text-left px-3 py-2 rounded-[2px] text-sm uppercase tracking-wide hover:bg-[color:var(--color-chalk)] transition-colors flex items-center gap-2">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS.logout} />
|
||||
</svg>
|
||||
@@ -107,39 +106,40 @@ const activeCheck = (item: NavItem) => {
|
||||
</aside>
|
||||
|
||||
<!-- Header mobile mínimo -->
|
||||
<header class="md:hidden flex items-center justify-between px-4 py-3 bg-[color:var(--color-primary)] text-white">
|
||||
<a href="/" class="flex items-center gap-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white rounded-lg" aria-label="LabPréstamos — Inicio">
|
||||
<div class="w-8 h-8 rounded-lg bg-white/15 grid place-items-center p-1.5" aria-hidden="true">
|
||||
<BrandMark class="w-full h-full text-white" />
|
||||
<header class="md:hidden flex items-center justify-between px-4 py-3 bg-white" style="border-bottom: 2px solid var(--color-ink);">
|
||||
<a href="/" class="flex items-center gap-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-ink)] rounded-[2px]" aria-label="LabPréstamos — Inicio">
|
||||
<div class="w-8 h-8 rounded-[2px] bg-[color:var(--color-primary)] grid place-items-center p-1.5" style="border: 2px solid var(--color-ink);" aria-hidden="true">
|
||||
<BrandMark class="w-full h-full" />
|
||||
</div>
|
||||
<span class="font-semibold text-sm">LabPréstamos</span>
|
||||
<span class="font-semibold text-sm uppercase tracking-wide">LabPréstamos</span>
|
||||
</a>
|
||||
<div class="text-xs text-white/80 truncate max-w-[45%] text-right">
|
||||
<div class="text-xs truncate max-w-[45%] text-right">
|
||||
<div class="font-medium truncate">{profile?.nombre ?? profile?.email}</div>
|
||||
<div class="text-white/60 capitalize">{profile?.rol}</div>
|
||||
<div class="uppercase tracking-wide" style="color: var(--color-pencil);">{profile?.rol}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="flex-1 p-4 md:p-8 pb-28 md:pb-8 md:ml-64 sidebar-aware transition-[margin] duration-200 ease-out">
|
||||
<main id="main" class="flex-1 p-4 md:p-8 pb-24 md:pb-8 md:ml-64 sidebar-aware transition-[margin] duration-200 ease-out">
|
||||
<!-- Botón hamburguesa desktop: toggle sidebar -->
|
||||
<button
|
||||
type="button"
|
||||
id="sidebar-toggle"
|
||||
aria-label="Alternar menú lateral"
|
||||
aria-controls="app-sidebar"
|
||||
class="hidden md:inline-flex items-center justify-center w-10 h-10 rounded-lg mb-4 text-[color:var(--color-ink)]/80 hover:bg-[color:var(--color-ink)]/5 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-primary)]"
|
||||
class="hidden md:inline-flex items-center justify-center w-10 h-10 rounded-[2px] mb-4 hover:bg-[color:var(--color-chalk)] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-ink)]"
|
||||
style="border: 2px solid var(--color-ink);"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS.menu} />
|
||||
</svg>
|
||||
</button>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<!-- Dock flotante mobile -->
|
||||
<!-- Dock mobile: barra inferior fija, plana (sin sombra, como el nav de MotherDuck) -->
|
||||
<nav
|
||||
class="md:hidden fixed bottom-4 left-1/2 -translate-x-1/2 z-50 rounded-full px-2 py-2 flex items-center gap-1 backdrop-blur bg-white/95 shadow-[0_10px_30px_-8px_rgba(0,0,0,0.25)] border"
|
||||
style="border-color: color-mix(in oklab, var(--color-ink) 10%, transparent); padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));"
|
||||
class="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-white flex items-stretch justify-around"
|
||||
style="border-top: 2px solid var(--color-ink); padding-bottom: env(safe-area-inset-bottom);"
|
||||
aria-label="Navegación principal"
|
||||
>
|
||||
{nav.map((item) => {
|
||||
@@ -149,29 +149,33 @@ const activeCheck = (item: NavItem) => {
|
||||
href={item.href}
|
||||
aria-label={item.label}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
class:list={[
|
||||
'grid place-items-center rounded-full w-11 h-11 transition-[background-color,color,transform] duration-150 motion-safe:active:scale-95',
|
||||
active
|
||||
? 'bg-[color:var(--color-primary)] text-white'
|
||||
: 'text-[color:var(--color-ink)]/70 hover:text-[color:var(--color-ink)] hover:bg-[color:var(--color-ink)]/5'
|
||||
]}
|
||||
class="flex-1 grid place-items-center py-2.5"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS[item.icon as keyof typeof ICONS]} />
|
||||
</svg>
|
||||
<span
|
||||
class:list={[
|
||||
'grid place-items-center rounded-[2px] w-11 h-11 transition-[background-color,transform] duration-100 motion-safe:active:scale-95',
|
||||
active ? 'bg-[color:var(--color-primary)]' : ''
|
||||
]}
|
||||
style={active ? 'border: 2px solid var(--color-ink);' : ''}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS[item.icon as keyof typeof ICONS]} />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
<span class="w-px h-6 mx-1" style="background: color-mix(in oklab, var(--color-ink) 15%, transparent);" aria-hidden="true"></span>
|
||||
<form method="POST" action="/api/auth/signout" class="contents">
|
||||
<form method="POST" action="/api/auth/signout" class="flex-1">
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Cerrar sesión"
|
||||
class="grid place-items-center rounded-full w-11 h-11 text-[color:var(--color-ink)]/70 hover:text-[color:var(--color-danger)] hover:bg-[color:var(--color-danger)]/10 transition-[background-color,color,transform] duration-150 motion-safe:active:scale-95"
|
||||
class="w-full grid place-items-center py-2.5"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS.logout} />
|
||||
</svg>
|
||||
<span class="grid place-items-center rounded-[2px] w-11 h-11 transition-[background-color,transform] duration-100 motion-safe:active:scale-95 hover:bg-[color:var(--color-danger)]/15">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d={ICONS.logout} />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
@@ -11,13 +11,29 @@ const { title = 'Sistema de Préstamos — Laboratorio UABC' } = Astro.props;
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#00723F" />
|
||||
<meta name="theme-color" content="#f4efea" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
THESIS: Trade the institutional-green bitácora look for MotherDuck's crayon-terminal-on-cream
|
||||
neobrutalism, per a fully pinned user brief — hard edges and offset shadow instead of soft,
|
||||
rounded institutional polish.
|
||||
OWN-WORLD: Cream Paper canvas, Frost White cards, Charcoal Ink borders/text everywhere, Sky
|
||||
Crayon as the sole filled-action color, Coral Sketch for danger, Mint Sketch for positive
|
||||
status, Duck Bill Orange as sparse accent, JetBrains Mono throughout, 2px radius everywhere,
|
||||
-6px/6px hard offset shadow with press-in hover on buttons and modals.
|
||||
STORY: alumno/admin still request, approve, and return lab equipment; the interface now reads
|
||||
like a terminal readout instead of a soft green form.
|
||||
FIRST VIEWPORT: login card — cream page, white bordered card, charcoal offset shadow, uppercase
|
||||
mono headline, sky-blue press button.
|
||||
FORM: user-pinned brief (MotherDuck DESIGN.md) — concept roll skipped per "the brief wins."
|
||||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the
|
||||
verdict, DESIGN.md, and every shipping raster carrying its provenance.
|
||||
-->
|
||||
<a href="#main" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 btn btn-primary z-50">
|
||||
Saltar al contenido
|
||||
</a>
|
||||
|
||||
+4
-4
@@ -7,11 +7,11 @@ Astro.response.status = 403;
|
||||
<Layout title="Acceso denegado — LabPréstamos">
|
||||
<main id="main" class="min-h-screen grid place-items-center p-4">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<div class="inline-grid place-items-center w-16 h-16 rounded-2xl mb-4 p-4" style="background: color-mix(in oklab, var(--color-danger) 12%, transparent);">
|
||||
<BrandMark class="w-full h-full" style="color: var(--color-danger);" />
|
||||
<div class="inline-grid place-items-center w-16 h-16 rounded-[2px] mb-4 p-4" style="background: var(--color-danger); border: 2px solid var(--color-ink); box-shadow: var(--shadow-hard);">
|
||||
<BrandMark class="w-full h-full" style="color: var(--color-ink);" />
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold">Acceso denegado</h1>
|
||||
<p class="text-sm opacity-70 mt-2">Esta sección es solo para administradores del laboratorio.</p>
|
||||
<h1 class="text-2xl font-semibold uppercase tracking-wide">Acceso denegado</h1>
|
||||
<p class="text-sm mt-2" style="color: var(--color-pencil);">Esta sección es solo para administradores del laboratorio.</p>
|
||||
<a href="/" class="btn btn-primary mt-6 inline-flex">Volver al inicio</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+4
-4
@@ -7,11 +7,11 @@ Astro.response.status = 404;
|
||||
<Layout title="Página no encontrada — LabPréstamos">
|
||||
<main id="main" class="min-h-screen grid place-items-center p-4">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<div class="inline-grid place-items-center w-16 h-16 rounded-2xl bg-[color:var(--color-primary)] mb-4 p-4">
|
||||
<BrandMark class="w-full h-full text-white" />
|
||||
<div class="inline-grid place-items-center w-16 h-16 rounded-[2px] bg-[color:var(--color-primary)] mb-4 p-4" style="border: 2px solid var(--color-ink); box-shadow: var(--shadow-hard);">
|
||||
<BrandMark class="w-full h-full" />
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold">Página no encontrada</h1>
|
||||
<p class="text-sm opacity-70 mt-2">La dirección no existe o cambió de lugar.</p>
|
||||
<h1 class="text-2xl font-semibold uppercase tracking-wide">Página no encontrada</h1>
|
||||
<p class="text-sm mt-2" style="color: var(--color-pencil);">La dirección no existe o cambió de lugar.</p>
|
||||
<a href="/" class="btn btn-primary mt-6 inline-flex">Volver al inicio</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -38,32 +38,29 @@ const estadoLabel: Record<string, string> = {
|
||||
<div class="max-w-6xl">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">Panel</h1>
|
||||
<p class="text-sm opacity-70 mt-1">Resumen del laboratorio.</p>
|
||||
<p class="text-sm mt-1" style="color: var(--color-pencil);">Resumen del laboratorio.</p>
|
||||
</header>
|
||||
|
||||
<section aria-label="Indicadores" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mb-8">
|
||||
{kpis.map((k) => (
|
||||
<a
|
||||
href={k.href}
|
||||
class="card block group focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-primary)] motion-safe:hover:-translate-y-0.5 transition-transform duration-150"
|
||||
>
|
||||
<a href={k.href} class="card-link">
|
||||
<div
|
||||
class="text-4xl font-semibold leading-none"
|
||||
style={`font-variant-numeric: tabular-nums; color: var(--color-${k.tone === 'ink' ? 'ink' : k.tone});`}
|
||||
>
|
||||
{k.value}
|
||||
</div>
|
||||
<div class="mt-2 text-sm opacity-70 group-hover:opacity-100 transition-opacity">{k.label}</div>
|
||||
<div class="mt-2 text-sm uppercase tracking-wide" style="color: var(--color-pencil);">{k.label}</div>
|
||||
</a>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="ultimas-h">
|
||||
<div class="flex items-baseline justify-between mb-3">
|
||||
<h2 id="ultimas-h" class="text-lg font-semibold">Últimas 5 solicitudes</h2>
|
||||
<a href="/admin/solicitudes" class="text-sm underline decoration-[color:var(--color-primary)] underline-offset-4">Ver todas</a>
|
||||
<h2 id="ultimas-h" class="text-lg font-semibold uppercase tracking-wide">Últimas 5 solicitudes</h2>
|
||||
<a href="/admin/solicitudes" class="text-sm underline decoration-[color:var(--color-ink)] underline-offset-4">Ver todas</a>
|
||||
</div>
|
||||
<div class="card p-0 overflow-hidden">
|
||||
<div class="card-flat p-0 overflow-hidden">
|
||||
{ultimas.data && ultimas.data.length > 0 ? (
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-left" style="background: color-mix(in oklab, var(--color-ink) 4%, transparent);">
|
||||
|
||||
@@ -41,7 +41,7 @@ const categorias = ((data ?? []) as unknown as CategoriaRow[]).map((c) => ({
|
||||
|
||||
{error && (
|
||||
<div class="card mb-6" role="alert" style="border-color: color-mix(in oklab, var(--color-danger) 30%, transparent);">
|
||||
<p class="text-sm" style="color: var(--color-danger);">
|
||||
<p class="text-sm" style="color: var(--color-danger-text);">
|
||||
No se pudieron cargar las categorías. Recarga la página.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -83,16 +83,16 @@ const estadoBadge = (e: Material['estado']) => {
|
||||
if (e === 'disponible')
|
||||
return {
|
||||
label: 'Disponible',
|
||||
style: 'background: color-mix(in oklab, var(--color-primary) 12%, transparent); color: var(--color-primary);',
|
||||
style: 'background: color-mix(in oklab, var(--color-positive) 18%, white); color: var(--color-ink); border: 1.5px solid var(--color-positive);',
|
||||
};
|
||||
if (e === 'mantenimiento')
|
||||
return {
|
||||
label: 'Mantenimiento',
|
||||
style: 'background: color-mix(in oklab, var(--color-secondary) 15%, transparent); color: var(--color-secondary-hover);',
|
||||
style: 'background: color-mix(in oklab, var(--color-secondary) 18%, white); color: var(--color-ink); border: 1.5px solid var(--color-secondary);',
|
||||
};
|
||||
return {
|
||||
label: 'Baja',
|
||||
style: 'background: color-mix(in oklab, var(--color-ink) 8%, transparent); color: color-mix(in oklab, var(--color-ink) 60%, transparent);',
|
||||
style: 'background: var(--color-chalk); color: var(--color-ink); border: 1.5px solid var(--color-graphite);',
|
||||
};
|
||||
};
|
||||
---
|
||||
@@ -166,7 +166,7 @@ const estadoBadge = (e: Material['estado']) => {
|
||||
|
||||
{matError && (
|
||||
<div class="card mb-6" role="alert" style="border-color: color-mix(in oklab, var(--color-danger) 30%, transparent);">
|
||||
<p class="text-sm" style="color: var(--color-danger);">
|
||||
<p class="text-sm" style="color: var(--color-danger-text);">
|
||||
No se pudo cargar el inventario. Recarga la página.
|
||||
</p>
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ const estadoBadge = (e: Material['estado']) => {
|
||||
{m.cantidad_disponible} / {m.cantidad_total}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full whitespace-nowrap" style={badge.style}>
|
||||
<span class="text-xs px-2 py-0.5 rounded-[2px] whitespace-nowrap" style={badge.style}>
|
||||
{badge.label}
|
||||
</span>
|
||||
</td>
|
||||
@@ -257,7 +257,7 @@ const estadoBadge = (e: Material['estado']) => {
|
||||
<p class="text-xs opacity-70 mt-0.5">{m.categoria.nombre}</p>
|
||||
)}
|
||||
</div>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full whitespace-nowrap" style={badge.style}>
|
||||
<span class="text-xs px-2 py-0.5 rounded-[2px] whitespace-nowrap" style={badge.style}>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -338,7 +338,7 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
</td>
|
||||
<td class="p-3" style="font-variant-numeric: tabular-nums;">{r.cantidad}</td>
|
||||
<td class="p-3 whitespace-nowrap">{fmt(r.fecha_devolucion_estimada)}</td>
|
||||
<td class="p-3 font-medium" style={`font-variant-numeric: tabular-nums; color: var(--color-danger);`}>
|
||||
<td class="p-3 font-medium" style={`font-variant-numeric: tabular-nums; color: var(--color-danger-text);`}>
|
||||
{dias} {dias === 1 ? 'día' : 'días'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -361,7 +361,7 @@ const exportFilename = `reporte-${today}.csv`;
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between text-sm">
|
||||
<span>Debía devolver el {fmt(r.fecha_devolucion_estimada)}</span>
|
||||
<span class="font-medium" style="font-variant-numeric: tabular-nums; color: var(--color-danger);">
|
||||
<span class="font-medium" style="font-variant-numeric: tabular-nums; color: var(--color-danger-text);">
|
||||
{dias} {dias === 1 ? 'día' : 'días'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,8 @@ const tabs = [
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">En préstamo</h1>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: color-mix(in oklab, var(--color-primary) 12%, transparent); color: var(--color-primary); font-variant-numeric: tabular-nums;"
|
||||
class="inline-flex items-center rounded-[2px] px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: var(--color-positive); color: var(--color-ink); border: 1.5px solid var(--color-ink); font-variant-numeric: tabular-nums;"
|
||||
aria-label={`${filas.length} préstamos activos`}
|
||||
>
|
||||
{filas.length}
|
||||
@@ -56,7 +56,7 @@ const tabs = [
|
||||
</nav>
|
||||
|
||||
{error && (
|
||||
<div role="alert" class="mb-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 12%, transparent); color: var(--color-danger);">
|
||||
<div role="alert" class="mb-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 20%, white); color: var(--color-ink); border: 1.5px solid var(--color-danger);">
|
||||
Error cargando préstamos: {error.message}
|
||||
</div>
|
||||
)}
|
||||
@@ -108,8 +108,8 @@ const tabs = [
|
||||
</span>
|
||||
{vencido && (
|
||||
<span
|
||||
class="ml-2 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold"
|
||||
style="background: var(--color-danger); color: white;"
|
||||
class="ml-2 inline-flex items-center rounded-[2px] px-2 py-0.5 text-xs font-semibold"
|
||||
style="background: var(--color-danger); color: var(--color-ink); border: 1.5px solid var(--color-ink);"
|
||||
>
|
||||
Vencido
|
||||
</span>
|
||||
@@ -136,7 +136,7 @@ const tabs = [
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<div class="font-medium">{r.alumno?.nombre ?? '—'}</div>
|
||||
{vencido && (
|
||||
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-semibold" style="background: var(--color-danger); color: white;">Vencido</span>
|
||||
<span class="inline-flex rounded-[2px] px-2 py-0.5 text-xs font-semibold" style="background: var(--color-danger); color: var(--color-ink); border: 1.5px solid var(--color-ink);">Vencido</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="text-xs opacity-70 mb-2">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
|
||||
@@ -41,8 +41,8 @@ const estadoLabel: Record<string, string> = {
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">Historial</h1>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: color-mix(in oklab, var(--color-ink) 12%, transparent); color: var(--color-ink); font-variant-numeric: tabular-nums;"
|
||||
class="inline-flex items-center rounded-[2px] px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: var(--color-chalk); color: var(--color-ink); border: 1.5px solid var(--color-ink); font-variant-numeric: tabular-nums;"
|
||||
aria-label={`${filas.length} registros`}
|
||||
>
|
||||
{filas.length}
|
||||
@@ -87,7 +87,7 @@ const estadoLabel: Record<string, string> = {
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
<div role="alert" class="mb-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 12%, transparent); color: var(--color-danger);">
|
||||
<div role="alert" class="mb-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 20%, white); color: var(--color-ink); border: 1.5px solid var(--color-danger);">
|
||||
Error cargando historial: {error.message}
|
||||
</div>
|
||||
)}
|
||||
@@ -136,7 +136,7 @@ const estadoLabel: Record<string, string> = {
|
||||
<li class="card">
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<div class="font-medium">{r.alumno?.nombre ?? '—'}</div>
|
||||
<span class="text-xs rounded-full px-2 py-0.5" style="background: color-mix(in oklab, var(--color-ink) 10%, transparent);">{estadoLabel[r.estado] ?? r.estado}</span>
|
||||
<span class="text-xs rounded-[2px] px-2 py-0.5" style="background: color-mix(in oklab, var(--color-ink) 10%, transparent);">{estadoLabel[r.estado] ?? r.estado}</span>
|
||||
</div>
|
||||
<div class="text-xs opacity-70">{r.alumno?.matricula ?? ''} {r.alumno?.email ? `· ${r.alumno.email}` : ''}</div>
|
||||
<div class="text-sm mt-2">{r.material?.nombre ?? '—'} · Cantidad <strong style="font-variant-numeric: tabular-nums;">{r.cantidad}</strong></div>
|
||||
|
||||
@@ -26,8 +26,8 @@ const tabs = [
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">Solicitudes pendientes</h1>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: color-mix(in oklab, var(--color-primary) 12%, transparent); color: var(--color-primary); font-variant-numeric: tabular-nums;"
|
||||
class="inline-flex items-center rounded-[2px] px-2.5 py-0.5 text-sm font-semibold"
|
||||
style="background: var(--color-primary); color: var(--color-ink); border: 1.5px solid var(--color-ink); font-variant-numeric: tabular-nums;"
|
||||
aria-label={`${filas.length} pendientes`}
|
||||
>
|
||||
{filas.length}
|
||||
@@ -52,7 +52,7 @@ const tabs = [
|
||||
</nav>
|
||||
|
||||
{error && (
|
||||
<div role="alert" class="mb-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 12%, transparent); color: var(--color-danger);">
|
||||
<div role="alert" class="mb-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 20%, white); color: var(--color-ink); border: 1.5px solid var(--color-danger);">
|
||||
Error cargando solicitudes: {error.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ const categoriasFiltro = grupoList
|
||||
|
||||
{error && (
|
||||
<div class="card mb-6" role="alert" style="border-color: color-mix(in oklab, var(--color-danger) 30%, transparent);">
|
||||
<p class="text-sm" style="color: var(--color-danger);">
|
||||
<p class="text-sm" style="color: var(--color-danger-text);">
|
||||
No se pudo cargar el catálogo. Recarga la página o intenta más tarde.
|
||||
</p>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@ const categoriasFiltro = grupoList
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h3 class="font-semibold leading-snug">{m.nombre}</h3>
|
||||
{m.categoria && (
|
||||
<span class="text-xs px-2 py-0.5 rounded-full whitespace-nowrap" style="background: color-mix(in oklab, var(--color-primary) 10%, transparent); color: var(--color-primary);">
|
||||
<span class="text-xs px-2 py-0.5 rounded-[2px] whitespace-nowrap uppercase tracking-wide" style="background: color-mix(in oklab, var(--color-primary) 25%, white); color: var(--color-ink); border: 1.5px solid var(--color-primary);">
|
||||
{m.categoria.nombre}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -43,15 +43,15 @@ const estadoLabel: Record<Estado, string> = {
|
||||
|
||||
const estadoColor: Record<Estado, string> = {
|
||||
pendiente: 'var(--color-ink)',
|
||||
aprobado: 'var(--color-primary)',
|
||||
activo: 'var(--color-primary)',
|
||||
aprobado: 'var(--color-positive)',
|
||||
activo: 'var(--color-positive)',
|
||||
devuelto: 'var(--color-ink)',
|
||||
rechazado: 'var(--color-danger)',
|
||||
vencido: 'var(--color-danger)',
|
||||
};
|
||||
|
||||
const badgeStyle = (e: Estado) =>
|
||||
`background: color-mix(in oklab, ${estadoColor[e]} 15%, transparent); color: ${estadoColor[e]};`;
|
||||
`background: color-mix(in oklab, ${estadoColor[e]} 18%, white); color: var(--color-ink); border: 1.5px solid ${estadoColor[e]};`;
|
||||
---
|
||||
<AppLayout title="Mis préstamos — LabPréstamos">
|
||||
<div class="max-w-4xl">
|
||||
@@ -62,7 +62,7 @@ const badgeStyle = (e: Estado) =>
|
||||
|
||||
{error && (
|
||||
<div class="card mb-6" role="alert" style="border-color: color-mix(in oklab, var(--color-danger) 30%, transparent);">
|
||||
<p class="text-sm" style="color: var(--color-danger);">
|
||||
<p class="text-sm" style="color: var(--color-danger-text);">
|
||||
No se pudieron cargar tus préstamos. Recarga la página o intenta más tarde.
|
||||
</p>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@ const badgeStyle = (e: Estado) =>
|
||||
{p.material?.numero_inventario ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-medium px-2 py-1 rounded-full whitespace-nowrap" style={badgeStyle(p.estado)}>
|
||||
<span class="text-xs font-medium px-2 py-1 rounded-[2px] whitespace-nowrap" style={badgeStyle(p.estado)}>
|
||||
{estadoLabel[p.estado]}
|
||||
</span>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@ const badgeStyle = (e: Estado) =>
|
||||
{p.material?.numero_inventario ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-medium px-2 py-1 rounded-full whitespace-nowrap" style={badgeStyle(p.estado)}>
|
||||
<span class="text-xs font-medium px-2 py-1 rounded-[2px] whitespace-nowrap" style={badgeStyle(p.estado)}>
|
||||
{estadoLabel[p.estado]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+17
-17
@@ -8,35 +8,35 @@ const nombre = profile?.nombre ?? profile?.email?.split('@')[0] ?? '';
|
||||
<AppLayout title="Inicio — LabPréstamos">
|
||||
<div class="max-w-3xl">
|
||||
<header class="mb-8">
|
||||
<p class="text-sm opacity-60">Bienvenido{nombre ? ',' : ''}</p>
|
||||
<p class="text-sm" style="color: var(--color-pencil);">Bienvenido{nombre ? ',' : ''}</p>
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">{nombre || 'Laboratorio de Sistemas'}</h1>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
{isAdmin ? (
|
||||
<>
|
||||
<a href="/admin/solicitudes" class="card block hover:brightness-105 transition-[filter,transform] duration-150 motion-safe:hover:-translate-y-0.5">
|
||||
<h2 class="font-semibold text-lg mb-1">Solicitudes</h2>
|
||||
<p class="text-sm opacity-70">Aprobar o rechazar peticiones de material.</p>
|
||||
<a href="/admin/solicitudes" class="card-link">
|
||||
<h2 class="font-semibold text-lg mb-1 uppercase tracking-wide">Solicitudes</h2>
|
||||
<p class="text-sm" style="color: var(--color-pencil);">Aprobar o rechazar peticiones de material.</p>
|
||||
</a>
|
||||
<a href="/admin/inventario" class="card block hover:brightness-105 transition-[filter,transform] duration-150 motion-safe:hover:-translate-y-0.5">
|
||||
<h2 class="font-semibold text-lg mb-1">Inventario</h2>
|
||||
<p class="text-sm opacity-70">Materiales y categorías del laboratorio.</p>
|
||||
<a href="/admin/inventario" class="card-link">
|
||||
<h2 class="font-semibold text-lg mb-1 uppercase tracking-wide">Inventario</h2>
|
||||
<p class="text-sm" style="color: var(--color-pencil);">Materiales y categorías del laboratorio.</p>
|
||||
</a>
|
||||
<a href="/admin/reportes" class="card block hover:brightness-105 transition-[filter,transform] duration-150 motion-safe:hover:-translate-y-0.5 md:col-span-2">
|
||||
<h2 class="font-semibold text-lg mb-1">Reportes</h2>
|
||||
<p class="text-sm opacity-70">Historial y exportación de préstamos.</p>
|
||||
<a href="/admin/reportes" class="card-link md:col-span-2">
|
||||
<h2 class="font-semibold text-lg mb-1 uppercase tracking-wide">Reportes</h2>
|
||||
<p class="text-sm" style="color: var(--color-pencil);">Historial y exportación de préstamos.</p>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a href="/alumno/catalogo" class="card block hover:brightness-105 transition-[filter,transform] duration-150 motion-safe:hover:-translate-y-0.5">
|
||||
<h2 class="font-semibold text-lg mb-1">Catálogo</h2>
|
||||
<p class="text-sm opacity-70">Ver material disponible y solicitar un préstamo.</p>
|
||||
<a href="/alumno/catalogo" class="card-link">
|
||||
<h2 class="font-semibold text-lg mb-1 uppercase tracking-wide">Catálogo</h2>
|
||||
<p class="text-sm" style="color: var(--color-pencil);">Ver material disponible y solicitar un préstamo.</p>
|
||||
</a>
|
||||
<a href="/alumno/mis-prestamos" class="card block hover:brightness-105 transition-[filter,transform] duration-150 motion-safe:hover:-translate-y-0.5">
|
||||
<h2 class="font-semibold text-lg mb-1">Mis préstamos</h2>
|
||||
<p class="text-sm opacity-70">Estado de tus solicitudes actuales e historial.</p>
|
||||
<a href="/alumno/mis-prestamos" class="card-link">
|
||||
<h2 class="font-semibold text-lg mb-1 uppercase tracking-wide">Mis préstamos</h2>
|
||||
<p class="text-sm" style="color: var(--color-pencil);">Estado de tus solicitudes actuales e historial.</p>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
+11
-11
@@ -16,33 +16,33 @@ const googleAuthUrl = '/api/auth/signin';
|
||||
<main id="main" class="min-h-screen grid place-items-center p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-grid place-items-center w-16 h-16 rounded-2xl bg-[color:var(--color-primary)] p-4 mb-4 motion-safe:animate-[fadein_.4s_ease-out]">
|
||||
<BrandMark class="w-full h-full text-white" />
|
||||
<div class="inline-grid place-items-center w-16 h-16 rounded-[2px] bg-[color:var(--color-primary)] p-4 mb-4 motion-safe:animate-[fadein_.4s_ease-out]" style="border: 2px solid var(--color-ink); box-shadow: var(--shadow-hard);">
|
||||
<BrandMark class="w-full h-full" />
|
||||
</div>
|
||||
<h1 class="text-2xl md:text-3xl font-semibold">Sistema de Préstamos</h1>
|
||||
<p class="text-sm mt-2 opacity-70">Laboratorio de Sistemas Computacionales · UABC</p>
|
||||
<h1 class="text-2xl md:text-3xl font-semibold uppercase tracking-wide">Sistema de Préstamos</h1>
|
||||
<p class="text-sm mt-2" style="color: var(--color-pencil);">Laboratorio de Sistemas Computacionales · UABC</p>
|
||||
</div>
|
||||
|
||||
<div class="card motion-safe:animate-[fadein_.5s_ease-out]">
|
||||
<h2 class="text-lg font-semibold mb-1">Inicia sesión</h2>
|
||||
<p class="text-sm opacity-70 mb-6">Accede con tu cuenta institucional.</p>
|
||||
<div class="card-raised motion-safe:animate-[fadein_.5s_ease-out]">
|
||||
<h2 class="text-lg font-semibold mb-1 uppercase tracking-wide">Inicia sesión</h2>
|
||||
<p class="text-sm mb-6" style="color: var(--color-pencil);">Accede con tu cuenta institucional.</p>
|
||||
|
||||
<a href={googleAuthUrl} class="btn btn-primary w-full">
|
||||
<svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true" focusable="false">
|
||||
<path fill="#fff" d="M44.5 20H24v8.5h11.7C34.4 33.4 29.7 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c2.9 0 5.5 1 7.6 2.7l6.2-6.2C33.8 5.1 29.2 3 24 3 12.4 3 3 12.4 3 24s9.4 21 21 21c11 0 20-8 20-21 0-1.4-.1-2.7-.5-4z"/>
|
||||
<path fill="var(--color-ink)" d="M44.5 20H24v8.5h11.7C34.4 33.4 29.7 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c2.9 0 5.5 1 7.6 2.7l6.2-6.2C33.8 5.1 29.2 3 24 3 12.4 3 3 12.4 3 24s9.4 21 21 21c11 0 20-8 20-21 0-1.4-.1-2.7-.5-4z"/>
|
||||
</svg>
|
||||
Continuar con Google
|
||||
</a>
|
||||
|
||||
{errorMsg && (
|
||||
<div role="alert" class="mt-4 rounded-lg p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 12%, transparent); color: var(--color-danger);">
|
||||
<div role="alert" class="mt-4 rounded-[2px] p-3 text-sm" style="background: color-mix(in oklab, var(--color-danger) 20%, transparent); border: 1.5px solid var(--color-danger); color: var(--color-ink);">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs opacity-60 mt-6">
|
||||
Acceso exclusivo con correo institucional <strong>@uabc.edu.mx</strong>
|
||||
<p class="text-center text-xs mt-6" style="color: var(--color-pencil);">
|
||||
Acceso exclusivo con correo institucional <strong style="color: var(--color-ink);">@uabc.edu.mx</strong>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+115
-30
@@ -1,18 +1,43 @@
|
||||
@import "tailwindcss";
|
||||
@import "@fontsource-variable/inter";
|
||||
@import "@fontsource-variable/jetbrains-mono";
|
||||
|
||||
@theme {
|
||||
--color-primary: #00723F;
|
||||
--color-primary-hover: #005f34;
|
||||
--color-secondary: #DD971A;
|
||||
--color-secondary-hover: #c78614;
|
||||
--color-surface: #F4F7F5;
|
||||
--color-ink: #2D3748;
|
||||
--color-danger: #C53030;
|
||||
/* MotherDuck-derived palette, remapped to LabPréstamos' semantics */
|
||||
--color-cream: #f4efea;
|
||||
--color-frost: #ffffff;
|
||||
--color-chalk: #f8f8f7;
|
||||
--color-ice: #ebf9ff;
|
||||
|
||||
--font-sans: "Inter Variable", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
--color-ink: #383838;
|
||||
--color-pencil: #a1a1a1;
|
||||
--color-graphite: #818181;
|
||||
|
||||
--radius-card: 0.5rem;
|
||||
--color-primary: #6fc2ff;
|
||||
--color-primary-hover: #56b4f7;
|
||||
--color-secondary: #ff9538;
|
||||
--color-secondary-hover: #ef8526;
|
||||
--color-danger: #f38e84;
|
||||
--color-danger-hover: #ec7469;
|
||||
/* AA-contrast text variant of Coral Sketch — the fill color reads ~2:1 on white, too light for body text */
|
||||
--color-danger-text: #a83224;
|
||||
--color-positive: #38c1b0;
|
||||
--color-attention: #ffde00;
|
||||
|
||||
--color-surface: var(--color-cream);
|
||||
|
||||
--font-sans: "JetBrains Mono Variable", ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--radius-card: 2px;
|
||||
--shadow-hard: -6px 6px 0 0 var(--color-ink);
|
||||
--shadow-hard-sm: -3px 3px 0 0 var(--color-ink);
|
||||
|
||||
/* Every Tailwind radius utility collapses to the system's 2px — non-negotiable per brief */
|
||||
--radius-sm: 2px;
|
||||
--radius-md: 2px;
|
||||
--radius-lg: 2px;
|
||||
--radius-xl: 2px;
|
||||
--radius-2xl: 2px;
|
||||
--radius-3xl: 2px;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -20,58 +45,118 @@
|
||||
color-scheme: light;
|
||||
}
|
||||
body {
|
||||
background: var(--color-surface);
|
||||
background: var(--color-cream);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
letter-spacing: 0.02em;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline: 2px solid var(--color-ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
::selection {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
/* Tabular data keeps real square corners even where a nested control forgets to ask */
|
||||
table {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 font-medium
|
||||
transition-[filter,background-color,color,border-color] duration-150 ease-out
|
||||
motion-safe:hover:brightness-110
|
||||
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-primary)]
|
||||
disabled:opacity-50 disabled:pointer-events-none;
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-[2px] px-4 py-2.5 font-medium
|
||||
motion-safe:transition-transform motion-safe:duration-100 motion-safe:ease-out
|
||||
disabled:opacity-50 disabled:pointer-events-none disabled:translate-x-0 disabled:translate-y-0;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
border: 2px solid var(--color-ink);
|
||||
color: var(--color-ink);
|
||||
box-shadow: var(--shadow-hard);
|
||||
}
|
||||
.btn:hover,
|
||||
.btn:focus-visible {
|
||||
transform: translate(-6px, 6px);
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
.btn:active {
|
||||
transform: translate(-3px, 3px);
|
||||
box-shadow: var(--shadow-hard-sm);
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
background: var(--color-frost);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-ink);
|
||||
border: 1px solid color-mix(in oklab, var(--color-ink) 15%, transparent);
|
||||
border-color: color-mix(in oklab, var(--color-ink) 35%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-ghost:hover,
|
||||
.btn-ghost:focus-visible {
|
||||
transform: none;
|
||||
background: var(--color-chalk);
|
||||
border-color: var(--color-ink);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply rounded-lg bg-white p-6;
|
||||
border: 1px solid color-mix(in oklab, var(--color-ink) 10%, transparent);
|
||||
@apply rounded-[2px] bg-white p-6;
|
||||
border: 2px solid var(--color-ink);
|
||||
}
|
||||
.card-flat {
|
||||
@apply rounded-[2px] bg-white p-6;
|
||||
border: 1.5px solid var(--color-ink);
|
||||
}
|
||||
.card-raised {
|
||||
@apply rounded-[2px] bg-white p-6;
|
||||
border: 2px solid var(--color-ink);
|
||||
box-shadow: var(--shadow-hard);
|
||||
}
|
||||
.card-link {
|
||||
@apply rounded-[2px] bg-white p-6 block
|
||||
motion-safe:transition-transform motion-safe:duration-100 motion-safe:ease-out;
|
||||
border: 2px solid var(--color-ink);
|
||||
box-shadow: var(--shadow-hard);
|
||||
}
|
||||
.card-link:hover,
|
||||
.card-link:focus-visible {
|
||||
transform: translate(-6px, 6px);
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply w-full rounded-lg bg-white px-3 py-2.5
|
||||
transition-[border-color,box-shadow] duration-150
|
||||
@apply w-full rounded-[2px] bg-white px-3 py-2.5
|
||||
transition-[border-color] duration-100
|
||||
focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2
|
||||
focus-visible:outline-[color:var(--color-primary)];
|
||||
border: 1px solid color-mix(in oklab, var(--color-ink) 20%, transparent);
|
||||
focus-visible:outline-[color:var(--color-ink)];
|
||||
border: 2px solid var(--color-ink);
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.label {
|
||||
@apply block text-sm font-medium mb-1;
|
||||
color: color-mix(in oklab, var(--color-ink) 90%, transparent);
|
||||
.input:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.label {
|
||||
@apply block text-sm font-medium mb-1 uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.badge {
|
||||
@apply inline-flex items-center rounded-[2px] px-2 py-0.5 text-xs font-semibold uppercase whitespace-nowrap;
|
||||
letter-spacing: 0.03em;
|
||||
border: 1.5px solid var(--color-ink);
|
||||
}
|
||||
|
||||
h1, h2, h3 { text-wrap: balance; }
|
||||
p { text-wrap: pretty; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user