UI: navegación SPA, tabs sin bordes, patrón de fondo fijo
- ClientRouter (view transitions) para navegación sin recarga entera; Toaster y BadgeSolicitudes marcados con transition:persist para no rearmar la suscripción realtime en cada nav. - Overlay de carga ahora activa a los 250ms del swap (evita parpadeo). - Tabs de subnav (inventario/categorías/maestros, solicitudes, reportes) reescritos: sin border-b-2 ni underline verde; activo usa bg-linen como el sidebar. Coherente y sin patrón AI-slop. - Marco horizontal debajo de todos los tab strips removido. - Patrón de puntos verde del canvas ahora con background-attachment: fixed — estático en scroll. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xaxs8cGcQe9NwbMqA1gvn
This commit is contained in:
@@ -243,8 +243,8 @@ const activeCheck = (item: NavItem) => {
|
|||||||
</form>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<Toaster client:idle />
|
<Toaster client:idle transition:persist />
|
||||||
{isAdmin && <BadgeSolicitudes client:idle />}
|
{isAdmin && <BadgeSolicitudes client:idle transition:persist />}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
|||||||
+23
-20
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import '@/styles/global.css';
|
import '@/styles/global.css';
|
||||||
|
import { ClientRouter } from 'astro:transitions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -18,6 +19,7 @@ const { title = 'Sistema de Préstamos — Laboratorio UABC' } = Astro.props;
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Mulish:ital,wght@0,200..1000;1,200..1000&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Mulish:ital,wght@0,200..1000;1,200..1000&display=swap" rel="stylesheet" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<ClientRouter />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -63,27 +65,28 @@ const { title = 'Sistema de Préstamos — Laboratorio UABC' } = Astro.props;
|
|||||||
</style>
|
</style>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
(() => {
|
(() => {
|
||||||
const overlay = document.getElementById('page-loader');
|
const show = () => {
|
||||||
if (!overlay) return;
|
const overlay = document.getElementById('page-loader');
|
||||||
const show = () => { overlay.hidden = false; };
|
if (overlay) overlay.hidden = false;
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
const overlay = document.getElementById('page-loader');
|
||||||
|
if (overlay) overlay.hidden = true;
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
// Con ClientRouter, la navegación entre páginas dispara eventos propios.
|
||||||
const a = (e.target instanceof Element) ? e.target.closest('a[href]') : null;
|
// La navegación es rápida (fetch + swap sin recargar) — mostramos el
|
||||||
if (!a) return;
|
// overlay solo si el swap tarda >250ms para no parpadear en cambios rápidos.
|
||||||
if (e.defaultPrevented) return;
|
let pendingShow = null;
|
||||||
if (e.button !== 0) return;
|
document.addEventListener('astro:before-preparation', () => {
|
||||||
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
pendingShow = setTimeout(show, 250);
|
||||||
if (a.target && a.target !== '_self') return;
|
});
|
||||||
const href = a.getAttribute('href') || '';
|
document.addEventListener('astro:after-swap', () => {
|
||||||
if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
|
if (pendingShow) { clearTimeout(pendingShow); pendingShow = null; }
|
||||||
try {
|
hide();
|
||||||
const url = new URL(a.href, location.href);
|
});
|
||||||
if (url.origin !== location.origin) return;
|
|
||||||
if (url.pathname === location.pathname && url.search === location.search) return;
|
|
||||||
} catch { return; }
|
|
||||||
show();
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
|
// Submits que hagan navegación real (no fetch client-side) sí ameritan overlay inmediato.
|
||||||
document.addEventListener('submit', (e) => {
|
document.addEventListener('submit', (e) => {
|
||||||
if (e.defaultPrevented) return;
|
if (e.defaultPrevented) return;
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
@@ -98,7 +101,7 @@ const { title = 'Sistema de Préstamos — Laboratorio UABC' } = Astro.props;
|
|||||||
show();
|
show();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
window.addEventListener('pageshow', () => { overlay.hidden = true; });
|
window.addEventListener('pageshow', hide);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -30,23 +30,23 @@ const categorias = ((data ?? []) as unknown as CategoriaRow[]).map((c) => ({
|
|||||||
<CategoriaForm mode="create" client:load />
|
<CategoriaForm mode="create" client:load />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="flex gap-2 mb-6 border-b border-hairline-gray" aria-label="Sub-navegación de inventario">
|
<nav class="flex gap-2 mb-6" aria-label="Sub-navegación de inventario">
|
||||||
<a
|
<a
|
||||||
href="/admin/inventario"
|
href="/admin/inventario"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-transparent text-graphite hover:text-verde-institucional"
|
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors text-graphite hover:bg-linen hover:text-verde-institucional"
|
||||||
>
|
>
|
||||||
Materiales
|
Materiales
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/admin/inventario/categorias"
|
href="/admin/inventario/categorias"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-verde-institucional text-verde-institucional"
|
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors bg-linen text-verde-institucional"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
>
|
>
|
||||||
Categorías
|
Categorías
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/admin/maestros"
|
href="/admin/maestros"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-transparent text-graphite hover:text-verde-institucional"
|
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors text-graphite hover:bg-linen hover:text-verde-institucional"
|
||||||
>
|
>
|
||||||
Maestros
|
Maestros
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ const estadoBadge = (e: Material['estado']) => {
|
|||||||
<MaterialForm mode="create" categorias={categorias} client:load />
|
<MaterialForm mode="create" categorias={categorias} client:load />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="flex gap-2 mb-6 border-b border-hairline-gray" aria-label="Sub-navegación de inventario">
|
<nav class="flex gap-2 mb-6" aria-label="Sub-navegación de inventario">
|
||||||
<a href="/admin/inventario" class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-verde-institucional text-verde-institucional" aria-current="page">
|
<a href="/admin/inventario" class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors bg-linen text-verde-institucional" aria-current="page">
|
||||||
Materiales
|
Materiales
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/inventario/categorias" class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-transparent text-graphite hover:text-verde-institucional">
|
<a href="/admin/inventario/categorias" class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors text-graphite hover:bg-linen hover:text-verde-institucional">
|
||||||
Categorías
|
Categorías
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/maestros" class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-transparent text-graphite hover:text-verde-institucional">
|
<a href="/admin/maestros" class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors text-graphite hover:bg-linen hover:text-verde-institucional">
|
||||||
Maestros
|
Maestros
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -34,22 +34,22 @@ const maestros = ((data ?? []) as unknown as MaestroRow[]).map((m) => ({
|
|||||||
<MaestroForm mode="create" client:load />
|
<MaestroForm mode="create" client:load />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="flex gap-2 mb-6 border-b border-hairline-gray" aria-label="Sub-navegación de inventario">
|
<nav class="flex gap-2 mb-6" aria-label="Sub-navegación de inventario">
|
||||||
<a
|
<a
|
||||||
href="/admin/inventario"
|
href="/admin/inventario"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-transparent text-graphite hover:text-verde-institucional"
|
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors text-graphite hover:bg-linen hover:text-verde-institucional"
|
||||||
>
|
>
|
||||||
Materiales
|
Materiales
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/admin/inventario/categorias"
|
href="/admin/inventario/categorias"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-transparent text-graphite hover:text-verde-institucional"
|
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors text-graphite hover:bg-linen hover:text-verde-institucional"
|
||||||
>
|
>
|
||||||
Categorías
|
Categorías
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/admin/maestros"
|
href="/admin/maestros"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors border-b-2 -mb-px border-verde-institucional text-verde-institucional"
|
class="px-4 py-2 text-sm font-medium rounded-[8px] transition-colors bg-linen text-verde-institucional"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
>
|
>
|
||||||
Maestros
|
Maestros
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ const exportFilename = `reporte-${today}.csv`;
|
|||||||
<p class="text-sm opacity-70 mt-1">Historial filtrable de préstamos y lista de vencidos.</p>
|
<p class="text-sm opacity-70 mt-1">Historial filtrable de préstamos y lista de vencidos.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="mb-6 border-b border-hairline-gray" aria-label="Vistas de reportes">
|
<nav class="mb-6" aria-label="Vistas de reportes">
|
||||||
<ul class="flex gap-2 -mb-px">
|
<ul class="flex gap-2">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="?vista=historial"
|
href="?vista=historial"
|
||||||
@@ -141,8 +141,8 @@ const exportFilename = `reporte-${today}.csv`;
|
|||||||
class:list={[
|
class:list={[
|
||||||
'inline-block px-4 py-2 text-sm font-medium rounded-[8px] transition-colors',
|
'inline-block px-4 py-2 text-sm font-medium rounded-[8px] transition-colors',
|
||||||
vista === 'historial'
|
vista === 'historial'
|
||||||
? 'text-verde-institucional border-b-2 border-verde-institucional'
|
? 'bg-linen text-verde-institucional'
|
||||||
: 'text-graphite hover:text-verde-institucional border-b-2 border-transparent',
|
: 'text-graphite hover:bg-linen hover:text-verde-institucional',
|
||||||
]}
|
]}
|
||||||
>Historial</a>
|
>Historial</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -153,8 +153,8 @@ const exportFilename = `reporte-${today}.csv`;
|
|||||||
class:list={[
|
class:list={[
|
||||||
'inline-block px-4 py-2 text-sm font-medium rounded-[8px] transition-colors',
|
'inline-block px-4 py-2 text-sm font-medium rounded-[8px] transition-colors',
|
||||||
vista === 'vencidos'
|
vista === 'vencidos'
|
||||||
? 'text-verde-institucional border-b-2 border-verde-institucional'
|
? 'bg-linen text-verde-institucional'
|
||||||
: 'text-graphite hover:text-verde-institucional border-b-2 border-transparent',
|
: 'text-graphite hover:bg-linen hover:text-verde-institucional',
|
||||||
]}
|
]}
|
||||||
>Vencidos</a>
|
>Vencidos</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -44,15 +44,15 @@ const tabs = [
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav aria-label="Vistas de solicitudes" class="mb-6 border-b border-hairline-gray flex gap-2 overflow-x-auto">
|
<nav aria-label="Vistas de solicitudes" class="mb-6 flex gap-2 overflow-x-auto">
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => {
|
||||||
const active = path === t.href;
|
const active = path === t.href;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={t.href}
|
href={t.href}
|
||||||
class:list={[
|
class:list={[
|
||||||
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors border-b -mb-px',
|
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors rounded-[8px]',
|
||||||
active ? 'border-verde-institucional text-verde-institucional' : 'border-transparent text-graphite/70 hover:text-verde-institucional'
|
active ? 'bg-linen text-verde-institucional' : 'text-graphite/70 hover:bg-linen hover:text-verde-institucional'
|
||||||
]}
|
]}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,15 +55,15 @@ const chipPorEstado: Record<string, string> = {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav aria-label="Vistas de solicitudes" class="mb-6 border-b border-hairline-gray flex gap-2 overflow-x-auto">
|
<nav aria-label="Vistas de solicitudes" class="mb-6 flex gap-2 overflow-x-auto">
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => {
|
||||||
const active = path === t.href;
|
const active = path === t.href;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={t.href}
|
href={t.href}
|
||||||
class:list={[
|
class:list={[
|
||||||
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors border-b -mb-px',
|
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors rounded-[8px]',
|
||||||
active ? 'border-verde-institucional text-verde-institucional' : 'border-transparent text-graphite/70 hover:text-verde-institucional'
|
active ? 'bg-linen text-verde-institucional' : 'text-graphite/70 hover:bg-linen hover:text-verde-institucional'
|
||||||
]}
|
]}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,15 +36,15 @@ const tabs = [
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav aria-label="Vistas de solicitudes" class="mb-6 border-b border-hairline-gray flex gap-2 overflow-x-auto">
|
<nav aria-label="Vistas de solicitudes" class="mb-6 flex gap-2 overflow-x-auto">
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => {
|
||||||
const active = path === t.href || (t.href === '/admin/solicitudes' && path === '/admin/solicitudes/');
|
const active = path === t.href || (t.href === '/admin/solicitudes' && path === '/admin/solicitudes/');
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={t.href}
|
href={t.href}
|
||||||
class:list={[
|
class:list={[
|
||||||
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors border-b -mb-px',
|
'px-4 py-2 text-sm font-medium whitespace-nowrap transition-colors rounded-[8px]',
|
||||||
active ? 'border-verde-institucional text-verde-institucional' : 'border-transparent text-graphite/70 hover:text-verde-institucional'
|
active ? 'bg-linen text-verde-institucional' : 'text-graphite/70 hover:bg-linen hover:text-verde-institucional'
|
||||||
]}
|
]}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
background-image: radial-gradient(circle, color-mix(in oklab, var(--color-verde-institucional) 12%, transparent) 1.5px, transparent 1.5px);
|
background-image: radial-gradient(circle, color-mix(in oklab, var(--color-verde-institucional) 12%, transparent) 1.5px, transparent 1.5px);
|
||||||
background-size: 22px 22px;
|
background-size: 22px 22px;
|
||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
|
background-attachment: fixed;
|
||||||
color: var(--color-graphite);
|
color: var(--color-graphite);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
Reference in New Issue
Block a user