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:
+81
-148
@@ -1,99 +1,30 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"generatedAt": "2026-08-16T00:00:00Z",
|
||||
"generatedAt": "2026-08-16T00:00:00.000Z",
|
||||
"title": "Design System: LabPréstamos",
|
||||
"extensions": {
|
||||
"colorMeta": {
|
||||
"verde-institucional": {
|
||||
"role": "primary",
|
||||
"displayName": "Verde Institucional",
|
||||
"canonical": "oklch(48.5% 0.12 154.8)",
|
||||
"tonalRamp": [
|
||||
"oklch(15% 0.12 154.8)",
|
||||
"oklch(28% 0.12 154.8)",
|
||||
"oklch(40% 0.12 154.8)",
|
||||
"oklch(52% 0.12 154.8)",
|
||||
"oklch(65% 0.12 154.8)",
|
||||
"oklch(76% 0.12 154.8)",
|
||||
"oklch(86% 0.12 154.8)",
|
||||
"oklch(95% 0.12 154.8)"
|
||||
]
|
||||
},
|
||||
"dorado-uabc": {
|
||||
"role": "secondary",
|
||||
"displayName": "Dorado UABC",
|
||||
"canonical": "oklch(72.8% 0.148 74.4)",
|
||||
"tonalRamp": [
|
||||
"oklch(15% 0.148 74.4)",
|
||||
"oklch(28% 0.148 74.4)",
|
||||
"oklch(40% 0.148 74.4)",
|
||||
"oklch(52% 0.148 74.4)",
|
||||
"oklch(65% 0.148 74.4)",
|
||||
"oklch(76% 0.148 74.4)",
|
||||
"oklch(86% 0.148 74.4)",
|
||||
"oklch(95% 0.148 74.4)"
|
||||
]
|
||||
},
|
||||
"superficie": {
|
||||
"role": "neutral",
|
||||
"displayName": "Superficie",
|
||||
"canonical": "oklch(97.3% 0.004 157.2)",
|
||||
"tonalRamp": [
|
||||
"oklch(15% 0.004 157.2)",
|
||||
"oklch(28% 0.004 157.2)",
|
||||
"oklch(40% 0.004 157.2)",
|
||||
"oklch(52% 0.004 157.2)",
|
||||
"oklch(65% 0.004 157.2)",
|
||||
"oklch(76% 0.004 157.2)",
|
||||
"oklch(86% 0.004 157.2)",
|
||||
"oklch(95% 0.004 157.2)"
|
||||
]
|
||||
},
|
||||
"tinta": {
|
||||
"role": "neutral",
|
||||
"displayName": "Tinta",
|
||||
"canonical": "oklch(33.5% 0.033 260.9)",
|
||||
"tonalRamp": [
|
||||
"oklch(15% 0.033 260.9)",
|
||||
"oklch(28% 0.033 260.9)",
|
||||
"oklch(40% 0.033 260.9)",
|
||||
"oklch(52% 0.033 260.9)",
|
||||
"oklch(65% 0.033 260.9)",
|
||||
"oklch(76% 0.033 260.9)",
|
||||
"oklch(86% 0.033 260.9)",
|
||||
"oklch(95% 0.033 260.9)"
|
||||
]
|
||||
},
|
||||
"peligro": {
|
||||
"role": "danger",
|
||||
"displayName": "Peligro",
|
||||
"canonical": "oklch(54.4% 0.186 26)",
|
||||
"tonalRamp": [
|
||||
"oklch(15% 0.186 26)",
|
||||
"oklch(28% 0.186 26)",
|
||||
"oklch(40% 0.186 26)",
|
||||
"oklch(52% 0.186 26)",
|
||||
"oklch(65% 0.186 26)",
|
||||
"oklch(76% 0.186 26)",
|
||||
"oklch(86% 0.186 26)",
|
||||
"oklch(95% 0.186 26)"
|
||||
]
|
||||
}
|
||||
"cream": { "role": "neutral", "displayName": "Cream Paper", "canonical": "#f4efea" },
|
||||
"frost": { "role": "neutral", "displayName": "Frost White", "canonical": "#ffffff" },
|
||||
"chalk": { "role": "neutral", "displayName": "Chalk Gray", "canonical": "#f8f8f7" },
|
||||
"ink": { "role": "neutral", "displayName": "Charcoal Ink", "canonical": "#383838" },
|
||||
"pencil": { "role": "neutral", "displayName": "Pencil Gray", "canonical": "#a1a1a1" },
|
||||
"primary": { "role": "primary", "displayName": "Sky Crayon", "canonical": "#6fc2ff" },
|
||||
"secondary": { "role": "secondary", "displayName": "Duck Bill Orange", "canonical": "#ff9538" },
|
||||
"danger": { "role": "danger", "displayName": "Coral Sketch (fill)", "canonical": "#f38e84" },
|
||||
"danger-text": { "role": "danger", "displayName": "Coral Sketch (text, AA-darkened)", "canonical": "#a83224" },
|
||||
"positive": { "role": "success", "displayName": "Mint Sketch", "canonical": "#38c1b0" }
|
||||
},
|
||||
"typographyMeta": {
|
||||
"headline": { "displayName": "Headline", "purpose": "Page-level h1 — dashboard/section titles, login title." },
|
||||
"title": { "displayName": "Title", "purpose": "Card and section headers within a page." },
|
||||
"body": { "displayName": "Body", "purpose": "Default text size across the whole system: table cells, descriptions, list content." },
|
||||
"label": { "displayName": "Label", "purpose": "Form field labels, always paired with htmlFor/id." },
|
||||
"caption": { "displayName": "Caption", "purpose": "Metadata: matrícula, inventory numbers, timestamps, field hints." }
|
||||
"body": { "displayName": "Body / Mono", "purpose": "Universal voice — every role through weight, size, and case." },
|
||||
"label": { "displayName": "Label", "purpose": "Uppercase structural labels: nav, buttons, form labels, headers." }
|
||||
},
|
||||
"shadows": [
|
||||
{ "name": "floating-chrome", "value": "0 10px 30px -8px rgba(0,0,0,0.25)", "purpose": "Mobile dock nav — soft ambient shadow signaling it floats above the scrolling page." },
|
||||
{ "name": "overlay-notification", "value": "0 16px 40px -12px rgba(0,0,0,0.25)", "purpose": "Toast stack — same ambient quality, slightly heavier, paired with backdrop-blur-xl." }
|
||||
{ "name": "hard", "value": "-6px 6px 0 0 #383838", "purpose": "The system's only shadow — every raised element at rest." },
|
||||
{ "name": "hard-sm", "value": "-3px 3px 0 0 #383838", "purpose": "Active/pressed state of a raised element (halfway press)." }
|
||||
],
|
||||
"motion": [
|
||||
{ "name": "toast-entrance", "value": "cubic-bezier(0.22, 1, 0.36, 1)", "purpose": "260ms translate+scale+fade entrance for toast notifications." },
|
||||
{ "name": "chrome-transition", "value": "cubic-bezier(0, 0, 0.2, 1)", "purpose": "150-200ms default ease-out for sidebar collapse, hover states, dock icon transitions." }
|
||||
{ "name": "press", "value": "transform: translate(-6px, 6px); transition: transform 100ms ease-out;", "purpose": "Hover/focus on any raised element — presses into its own shadow." }
|
||||
],
|
||||
"breakpoints": [
|
||||
{ "name": "md", "value": "768px" }
|
||||
@@ -104,100 +35,102 @@
|
||||
"name": "Primary Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-primary",
|
||||
"description": "Default call to action: submit, approve, continue with Google.",
|
||||
"html": "<button class=\"ds-btn ds-btn-primary\">Enviar solicitud</button>",
|
||||
"css": ".ds-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 10px 16px; border-radius: 8px; font-weight: 500; font-size: 0.875rem; border: none; cursor: pointer; touch-action: manipulation; transition: filter 150ms ease-out, background-color 150ms ease-out; } .ds-btn-primary { background: #00723F; color: #ffffff; } .ds-btn-primary:hover { filter: brightness(1.1); } .ds-btn-primary:focus-visible { outline: 2px solid #00723F; outline-offset: 2px; } .ds-btn-primary:disabled { opacity: 0.5; pointer-events: none; }"
|
||||
"description": "The system's default call to action.",
|
||||
"html": "<button class=\"ds-btn ds-btn-primary\">Solicitar</button>",
|
||||
"css": ".ds-btn { display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:44px; padding:10px 16px; border-radius:2px; border:2px solid #383838; font-family:'JetBrains Mono Variable',monospace; letter-spacing:0.02em; font-weight:500; box-shadow:-6px 6px 0 0 #383838; transition:transform 100ms ease-out; } .ds-btn:hover, .ds-btn:focus-visible { transform:translate(-6px,6px); box-shadow:0 0 0 0 transparent; } .ds-btn-primary { background:#6fc2ff; color:#383838; }"
|
||||
},
|
||||
{
|
||||
"name": "Secondary Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-secondary",
|
||||
"description": "Sparingly used accent action — filtering, non-primary confirms.",
|
||||
"html": "<button class=\"ds-btn ds-btn-secondary\">Filtrar</button>",
|
||||
"css": ".ds-btn-secondary { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 10px 16px; border-radius: 8px; font-weight: 500; font-size: 0.875rem; border: none; cursor: pointer; touch-action: manipulation; background: #DD971A; color: #ffffff; transition: filter 150ms ease-out; } .ds-btn-secondary:hover { filter: brightness(1.1); } .ds-btn-secondary:focus-visible { outline: 2px solid #00723F; outline-offset: 2px; }"
|
||||
"description": "White-filled, charcoal-outlined — never a second brand color.",
|
||||
"html": "<button class=\"ds-btn ds-btn-secondary\">Exportar CSV</button>",
|
||||
"css": ".ds-btn-secondary { background:#ffffff; color:#383838; }"
|
||||
},
|
||||
{
|
||||
"name": "Ghost Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-ghost",
|
||||
"description": "Low-commitment counterpart to Primary — cancel, clear filters.",
|
||||
"html": "<button class=\"ds-btn ds-btn-ghost\">Cancelar</button>",
|
||||
"css": ".ds-btn-ghost { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 10px 16px; border-radius: 8px; font-weight: 500; font-size: 0.875rem; cursor: pointer; touch-action: manipulation; background: transparent; color: #2D3748; border: 1px solid color-mix(in oklab, #2D3748 15%, transparent); transition: background-color 150ms ease-out; } .ds-btn-ghost:hover { background: color-mix(in oklab, #2D3748 5%, transparent); } .ds-btn-ghost:focus-visible { outline: 2px solid #00723F; outline-offset: 2px; }"
|
||||
"description": "Low-commitment counterpart — Cancelar/Limpiar.",
|
||||
"html": "<button class=\"ds-btn-ghost\">Cancelar</button>",
|
||||
"css": ".ds-btn-ghost { display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:44px; padding:10px 16px; border-radius:2px; border:2px solid rgba(56,56,56,0.35); background:transparent; color:#383838; font-family:'JetBrains Mono Variable',monospace; letter-spacing:0.02em; font-weight:500; } .ds-btn-ghost:hover { background:#f8f8f7; border-color:#383838; }"
|
||||
},
|
||||
{
|
||||
"name": "Status Pill",
|
||||
"kind": "chip",
|
||||
"refersTo": "badge",
|
||||
"description": "Tinted-fill status indicator used for every préstamo/material estado; solid-fill only for the confirmed Vencido exception.",
|
||||
"html": "<span class=\"ds-pill ds-pill-primary\">Disponible</span> <span class=\"ds-pill ds-pill-secondary\">Mantenimiento</span> <span class=\"ds-pill ds-pill-danger-solid\">Vencido</span>",
|
||||
"css": ".ds-pill { display: inline-flex; align-items: center; border-radius: 9999px; padding: 2px 10px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; } .ds-pill-primary { background: color-mix(in oklab, #00723F 13%, transparent); color: #00723F; } .ds-pill-secondary { background: color-mix(in oklab, #DD971A 15%, transparent); color: #c78614; } .ds-pill-danger-solid { background: #C53030; color: #ffffff; }"
|
||||
},
|
||||
{
|
||||
"name": "Card",
|
||||
"name": "Raised Card",
|
||||
"kind": "card",
|
||||
"refersTo": "card",
|
||||
"description": "Flat white container — the system's default surface for content, forms, and KPI tiles. No shadow, hairline ink-mix border only.",
|
||||
"html": "<div class=\"ds-card\"><h3 class=\"ds-card-title\">Inicia sesión</h3><p class=\"ds-card-body\">Accede con tu cuenta institucional.</p></div>",
|
||||
"css": ".ds-card { background: #ffffff; border-radius: 8px; padding: 24px; border: 1px solid color-mix(in oklab, #2D3748 10%, transparent); } .ds-card-title { font-size: 1.125rem; font-weight: 600; margin: 0 0 4px; color: #2D3748; } .ds-card-body { font-size: 0.875rem; opacity: 0.7; margin: 0; color: #2D3748; }"
|
||||
"description": "Discrete, singular, actionable surface — login card, dashboard tile, KPI tile.",
|
||||
"html": "<a class=\"ds-card-raised\" href=\"#\"><h2>Catálogo</h2><p>Ver material disponible.</p></a>",
|
||||
"css": ".ds-card-raised { display:block; background:#ffffff; border:2px solid #383838; border-radius:2px; padding:24px; box-shadow:-6px 6px 0 0 #383838; transition:transform 100ms ease-out; } .ds-card-raised:hover, .ds-card-raised:focus-visible { transform:translate(-6px,6px); box-shadow:0 0 0 0 transparent; } .ds-card-raised h2 { font-family:'JetBrains Mono Variable',monospace; text-transform:uppercase; letter-spacing:0.02em; font-size:1.125rem; margin:0 0 4px; color:#383838; } .ds-card-raised p { color:#a1a1a1; font-family:'JetBrains Mono Variable',monospace; font-size:0.875rem; margin:0; }"
|
||||
},
|
||||
{
|
||||
"name": "Input Field",
|
||||
"name": "Flat Card / Table Wrapper",
|
||||
"kind": "card",
|
||||
"refersTo": "card",
|
||||
"description": "Dense, repeated content — tables, list rows. Bordered, no shadow.",
|
||||
"html": "<div class=\"ds-card-flat\">Fila de tabla o lista</div>",
|
||||
"css": ".ds-card-flat { background:#ffffff; border:1.5px solid #383838; border-radius:2px; padding:24px; font-family:'JetBrains Mono Variable',monospace; color:#383838; }"
|
||||
},
|
||||
{
|
||||
"name": "Text Input",
|
||||
"kind": "input",
|
||||
"refersTo": "input",
|
||||
"description": "Labeled text field with hairline border and single-color focus outline (no glow, no border-color shift).",
|
||||
"html": "<div class=\"ds-field\"><label class=\"ds-label\" for=\"demo-input\">Cantidad</label><input id=\"demo-input\" class=\"ds-input\" type=\"number\" value=\"1\" /></div>",
|
||||
"css": ".ds-label { display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 4px; color: color-mix(in oklab, #2D3748 90%, transparent); } .ds-input { width: 100%; min-height: 44px; border-radius: 8px; padding: 10px 12px; background: #ffffff; border: 1px solid color-mix(in oklab, #2D3748 20%, transparent); font-size: 0.875rem; touch-action: manipulation; transition: border-color 150ms, box-shadow 150ms; } .ds-input:focus-visible { outline: 2px solid #00723F; outline-offset: 2px; }"
|
||||
"description": "Flat, bordered, no shadow, no glow focus.",
|
||||
"html": "<input class=\"ds-input\" type=\"text\" placeholder=\"Nombre o Nº inventario…\" />",
|
||||
"css": ".ds-input { width:100%; min-height:44px; padding:10px 12px; border-radius:2px; border:2px solid #383838; background:#ffffff; font-family:'JetBrains Mono Variable',monospace; color:#383838; } .ds-input:focus { outline:none; border-color:#6fc2ff; }"
|
||||
},
|
||||
{
|
||||
"name": "Mobile Dock Nav Item",
|
||||
"name": "Status Badge",
|
||||
"kind": "chip",
|
||||
"refersTo": "badge",
|
||||
"description": "Tinted fill, semantic-color border, always charcoal text.",
|
||||
"html": "<span class=\"ds-badge ds-badge-positive\">Disponible</span>",
|
||||
"css": ".ds-badge { display:inline-flex; align-items:center; border-radius:2px; padding:2px 8px; font-family:'JetBrains Mono Variable',monospace; font-size:0.75rem; font-weight:600; text-transform:uppercase; letter-spacing:0.03em; color:#383838; } .ds-badge-positive { background:color-mix(in oklab, #38c1b0 18%, white); border:1.5px solid #38c1b0; } .ds-badge-danger { background:color-mix(in oklab, #f38e84 18%, white); border:1.5px solid #f38e84; }"
|
||||
},
|
||||
{
|
||||
"name": "Nav Item (desktop sidebar)",
|
||||
"kind": "nav",
|
||||
"refersTo": "nav-dock-item",
|
||||
"description": "Signature component: icon-only floating pill navigation for the alumno/admin mobile layout, replacing a conventional bottom tab bar.",
|
||||
"html": "<nav class=\"ds-dock\"><a href=\"#\" class=\"ds-dock-item ds-dock-item-active\" aria-label=\"Inicio\"><svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.25 12l8.954-8.955a1.5 1.5 0 012.122 0L22.28 12M4.5 9.75v10.125A1.125 1.125 0 005.625 21H9.75v-6h4.5v6h4.125A1.125 1.125 0 0019.5 19.875V9.75\"/></svg></a><a href=\"#\" class=\"ds-dock-item\" aria-label=\"Catálogo\"><svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 8.25V6z\"/></svg></a></nav>",
|
||||
"css": ".ds-dock { display: inline-flex; align-items: center; gap: 4px; border-radius: 9999px; padding: 8px; backdrop-filter: blur(8px); background: rgba(255,255,255,0.95); box-shadow: 0 10px 30px -8px rgba(0,0,0,0.25); border: 1px solid color-mix(in oklab, #2D3748 10%, transparent); } .ds-dock-item { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 9999px; color: color-mix(in oklab, #2D3748 70%, transparent); transition: background-color 150ms, color 150ms, transform 150ms; text-decoration: none; } .ds-dock-item:hover { background: color-mix(in oklab, #2D3748 5%, transparent); color: #2D3748; } .ds-dock-item:active { transform: scale(0.95); } .ds-dock-item-active { background: #00723F; color: #ffffff; }"
|
||||
},
|
||||
{
|
||||
"name": "Toast",
|
||||
"kind": "custom",
|
||||
"refersTo": "toast",
|
||||
"description": "Signature component: floating top-center notification with a colored circular icon chip, survives page reload via a sessionStorage queue.",
|
||||
"html": "<div class=\"ds-toast\"><span class=\"ds-toast-icon\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4.5 12.75l6 6 9-13.5\"/></svg></span><span class=\"ds-toast-text\"><span class=\"ds-toast-title\">Solicitud enviada</span><span class=\"ds-toast-desc\">Multímetro digital · pendiente de aprobación</span></span></div>",
|
||||
"css": ".ds-toast { display: flex; align-items: flex-start; gap: 12px; border-radius: 16px; padding: 12px 16px; background: rgba(255,255,255,0.85); backdrop-filter: blur(20px); box-shadow: 0 16px 40px -12px rgba(0,0,0,0.25); border: 1px solid color-mix(in oklab, #2D3748 10%, transparent); color: #2D3748; max-width: 26rem; } .ds-toast-icon { flex-shrink: 0; display: grid; place-items: center; width: 36px; height: 36px; border-radius: 9999px; background: #00723F; color: #ffffff; } .ds-toast-title { display: block; font-weight: 600; font-size: 0.875rem; } .ds-toast-desc { display: block; font-size: 0.75rem; opacity: 0.7; margin-top: 2px; }"
|
||||
"refersTo": "nav-item",
|
||||
"description": "Uppercase mono label; active = solid Sky Crayon chip with charcoal border.",
|
||||
"html": "<a class=\"ds-nav-item ds-nav-item-active\" href=\"#\">Catálogo</a>",
|
||||
"css": ".ds-nav-item { display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:2px; border:2px solid transparent; font-family:'JetBrains Mono Variable',monospace; text-transform:uppercase; letter-spacing:0.02em; font-size:0.875rem; color:#383838; } .ds-nav-item-active { background:#6fc2ff; border-color:#383838; } .ds-nav-item:not(.ds-nav-item-active):hover { background:#f8f8f7; }"
|
||||
}
|
||||
],
|
||||
"narrative": {
|
||||
"northStar": "La Bitácora Digital",
|
||||
"overview": "LabPréstamos is the printed loan sheet and the Excel logbook it used to feed, collapsed into one live surface. Every visual decision defends that lineage: nothing decorative, nothing that would feel out of place next to an official UABC form, but nothing that feels like a cold spreadsheet either. The system is cálido pero confiable — warm but reliable — and it earns the \"warm\" half narrowly: a gold accent, soft rounded corners, and gentle motion sit on top of an otherwise flat, administrative base.\n\nThe page itself stays flat and quiet — white cards on a pale green-grey surface, hairline borders instead of shadows, tabular numbers lined up like a real ledger. Depth is reserved for the two things that are genuinely floating above the page: the toast notifications and the mobile navigation dock. That asymmetry is deliberate — it is how the system tells the eye \"this is the document\" from \"this is the chrome.\"\n\nTwo audiences read this bitácora differently. The alumno's default device is the phone — the catalog, the request modal, the pill-shaped floating dock are all tuned for thumbs. The admin's default device is the desktop — dense tables, a persistent sidebar, KPI tiles. Neither view degrades on the other's device; every screen ships both a table and a card-list rendering.",
|
||||
"northStar": "The Equipment Tag on a Terminal Readout",
|
||||
"overview": "LabPréstamos was rebuilt against a fully pinned external brief (MotherDuck's crayon-terminal-on-cream neobrutalism), translated from a marketing site's vocabulary into an operational lab tool's. The identity keeps the source system's mechanics intact — a warm cream canvas, monospace type standing in for a terminal readout, hard 2px corners, and a flat charcoal-offset shadow that never blurs — while dropping what only belonged to marketing (hand-drawn duck illustrations, scrolling marquees, testimonial cards). What replaces the duck is the product's own mark: a loan tag with a punch-hole, echoing the paper ticket this system replaced. Depth here is binary, not a hierarchy: an element either sits flat on the page or it's a discrete raised object that presses into its own shadow on hover. Color plays a stricter role than in the source brief: Sky Crayon blue is reserved for actionable, filled controls; Mint Sketch was pulled from the source's decorative rainbow set and fixed as the positive-status color; Coral Sketch was fixed as the sole danger color, split into a light fill and a separately darkened AA-compliant text variant.",
|
||||
"keyCharacteristics": [
|
||||
"Flat administrative base (white cards, hairline ink-tinted borders, no ambient shadow)",
|
||||
"Shadow appears only on floating overlay chrome — the mobile dock and toasts — never on the page plane",
|
||||
"Every status is a tinted pill: 12–15% color wash background, full-strength text",
|
||||
"Tabular numbers everywhere a quantity, date, or stock figure appears",
|
||||
"One typeface (Inter Variable) carrying every role through weight and size alone",
|
||||
"Motion is a courtesy, not a performance: motion-safe:-gated, specific-property transitions, no transition-all"
|
||||
"Cream page canvas, white card surfaces, charcoal (never pure black) as the universal line, border, and text color",
|
||||
"Every corner is 2px — no pills, no circles, no rounded-full anywhere",
|
||||
"One shadow, one behavior: -6px 6px 0 0 charcoal, hard-edged, flattens into the element on hover/focus",
|
||||
"JetBrains Mono Variable everywhere, 0.02em tracking, uppercase on structural labels only",
|
||||
"Status pills are tinted fills with a matching-color border and always-charcoal text",
|
||||
"Dense, repeated surfaces stay flat and bordered; only discrete, single-instance elements carry the shadow"
|
||||
],
|
||||
"rules": [
|
||||
{ "name": "The Ink-Mix Border Rule", "body": "Every hairline border and divider is color-mix(in oklab, var(--color-tinta) N%, transparent), never a literal gray hex. A border is always \"ink, diluted,\" so it stays coherent with body text at any opacity.", "section": "colors" },
|
||||
{ "name": "The Vencido Exception", "body": "Every status pill uses the tinted-fill pattern except an overdue loan (\"Vencido\"), which gets a solid peligro fill with white text — on both the table badge and, on mobile, a light danger wash across the entire card background. This is the system's one permitted alarm.", "section": "colors" },
|
||||
{ "name": "The Tabular Numbers Rule", "body": "Any figure that represents a count — cantidad, stock (disponible/total), KPI values, dates — is rendered with font-variant-numeric: tabular-nums. Numbers must align like a ledger column.", "section": "typography" },
|
||||
{ "name": "The Table/Card Swap Rule", "body": "Every data-dense admin view ships two complete renderings of the same data: a table for md:block and a card list for md:hidden. This is a structural swap, not a responsive squeeze.", "section": "layout" },
|
||||
{ "name": "The Flat Page, Floating Chrome Rule", "body": "If an element is part of the document (a card, a table, a form), it stays flat with a hairline border. If an element floats above the document regardless of scroll (the dock, a toast), it gets a soft ambient shadow plus backdrop-blur.", "section": "elevation" }
|
||||
{ "name": "The Charcoal Text Rule", "body": "No filled or tinted surface ever carries white or colored text — text is always Charcoal Ink. Color differentiates status and action through background fill and border only.", "section": "colors" },
|
||||
{ "name": "The One Shadow Rule", "body": "-6px 6px 0 0 var(--color-ink) is the only shadow value in the system. It never blurs, never softens, never gets a second 'subtle' variant.", "section": "colors" },
|
||||
{ "name": "The Tabular Numbers Rule", "body": "Any figure representing a count carries font-variant-numeric: tabular-nums.", "section": "typography" },
|
||||
{ "name": "The Press Rule", "body": "Every raised element's hover state is the same transform: translate exactly the shadow's own offset, so the shadow disappears under the element. No brightness filter, no color darken.", "section": "elevation" },
|
||||
{ "name": "The Tinted-Fill-Bordered Rule", "body": "Badge background = the semantic color mixed 18-25% into white; border = the semantic color at full strength; text = Charcoal Ink always.", "section": "components" }
|
||||
],
|
||||
"dos": [
|
||||
"Do use color-mix(in oklab, var(--color-tinta) N%, transparent) for any new border or divider — never a literal gray.",
|
||||
"Do keep shadow exclusive to floating overlay chrome (toasts, the mobile dock).",
|
||||
"Do use the tinted-pill pattern (12–15% background, full-strength text) for any new status or state indicator.",
|
||||
"Do apply font-variant-numeric: tabular-nums to any new numeric quantity, stock figure, or date.",
|
||||
"Do ship both a md:block table and a md:hidden card-list rendering for any new data-dense admin view.",
|
||||
"Do gate all motion with motion-safe: and animate specific properties, never transition-all.",
|
||||
"Do give every interactive control a 44px minimum tap target and touch-action: manipulation."
|
||||
"Do use the exact shadow -6px 6px 0 0 var(--color-ink) for every raised element, and the exact press transform translate(-6px, 6px) on hover/focus.",
|
||||
"Do keep every radius at 2px — buttons, cards, inputs, dialogs, badges, chips.",
|
||||
"Do keep text Charcoal Ink on every fill, tint, or border color.",
|
||||
"Do reserve Sky Crayon for actionable, filled controls; use Positive/Danger/Secondary for status.",
|
||||
"Do keep dense/repeated surfaces flat and bordered; reserve the hard shadow for discrete, singular surfaces.",
|
||||
"Do draw every icon as an SVG — never a unicode glyph or emoji.",
|
||||
"Do apply font-variant-numeric: tabular-nums to every numeric quantity, stock figure, or date.",
|
||||
"Do ship both a table and a card-list rendering for every data-dense admin view."
|
||||
],
|
||||
"donts": [
|
||||
"Don't add a shadow to .card, .btn, or .input — the system's flatness on the page plane is deliberate.",
|
||||
"Don't introduce a second typeface. Inter Variable carries every role through weight and size.",
|
||||
"Don't give a status pill a solid saturated fill — the tinted wash is the rule, and Vencido is the one confirmed exception, not a precedent to extend.",
|
||||
"Don't reach for rounded-2xl outside the login mark and toasts; it is reserved for \"first impression / interruption\" moments.",
|
||||
"Don't replace the mobile bottom dock with a hamburger drawer, or the desktop sidebar with a top bar — the device-appropriate nav pattern per breakpoint is deliberate."
|
||||
"Don't use blur, glass, or a soft ambient shadow anywhere.",
|
||||
"Don't introduce a radius above 2px, or a pill/circle shape, anywhere.",
|
||||
"Don't use a second filled brand color for secondary actions.",
|
||||
"Don't reuse Sky Crayon for status pills.",
|
||||
"Don't introduce a second typeface.",
|
||||
"Don't stand a unicode glyph in for an icon."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -170,3 +170,20 @@ Fases 3, 4/5 y 6 tocan carpetas de rutas distintas (`src/pages/alumno/*`, `src/p
|
||||
- Estado: cambios visuales completos y verificados. Servidor de dev quedó corriendo en background (`astro dev status`/`astro dev logs`/`astro dev stop` para gestionarlo). Pendiente: deploy a producción cuando el usuario lo confirme.
|
||||
|
||||
- **2026-08-16 — Deploy del rediseño impeccable a producción**. Confirmado por el usuario. `git push` + `ssh buglabs 'cd ~/labre-web && git pull && docker compose up -d --build'`. Container recreado (healthy en ~8s), sin downtime perceptible. Smoke test: `/login` → 200, `/` → 302 (redirige por middleware sin sesión). Cambios ya vivos en https://prestamos.buglabs.dev.
|
||||
|
||||
- **2026-08-16 — Reemplazo total de identidad visual: MotherDuck (`/impeccable`)**. El usuario pegó el `DESIGN.md` completo de MotherDuck ("crayon-coded terminal on cream paper", neobrutalista) y pidió que este proyecto adoptara ese estilo. Se trató como redesign de mundo visual completo (no refinamiento) siguiendo `reference/new-work.md` del skill impeccable: el brief venía 100% fijado (colores, tipografía, sombra, radios, componentes documentados), así que se saltó el proceso de generación de conceptos/dice-roll ("el brief gana" — instrucción explícita del skill cuando hay un mundo visual ya pinneado) y se fue directo a comprometer el mundo, construir, e inspeccionar. Sin generación de imágenes disponible en este entorno → build "code-led" (sin comps).
|
||||
- Contrato de dirección grabado como comentario HTML al inicio de `<body>` en `Layout.astro` (THESIS/OWN-WORLD/STORY/FIRST VIEWPORT/FORM/FINISH), verificado que sobrevive en el build de producción (`dist/`).
|
||||
- **Reasignación semántica de la paleta** (la traducción real del trabajo — MotherDuck es un sitio de marketing sin estados de status; este es un panel operativo que sí los necesita):
|
||||
- Verde institucional → **Sky Crayon `#6fc2ff`**, único color de acción rellena (botones primarios, nav activo).
|
||||
- Dorado institucional → **Duck Bill Orange `#ff9538`**, acento disperso (antes reservado a "ilustración/marquee" en el brief original, reutilizado aquí porque la app no tiene ninguna de las dos cosas).
|
||||
- Rojo de error → **Coral Sketch**, mismo problema que el sistema anterior con el verde: el fill claro (`#f38e84`) falla contraste AA como texto, así que se separó en `--color-danger` (fill/borde) y `--color-danger-text` (`#a83224`, oscurecido) — fix aplicado con sed dirigido en los 14 archivos que usaban `color: var(--color-danger)` como texto, verificado que no tocó ningún `border-color`/`background` de paso.
|
||||
- **Mint Sketch `#38c1b0`** se sacó del set decorativo "rainbow" del brief original y se fijó como color de status "positivo" (disponible/aprobado/devuelto) — el brief de MotherDuck nunca necesitó esto porque no tiene estados de negocio.
|
||||
- **Regla del texto siempre carbón**: ningún fill o borde de color lleva texto blanco o del mismo color — siempre `--color-ink` (`#383838`). Se auditó y corrigió cada instancia de `color: white`/`text-white` en botones de eliminar/rechazar y el chip activo de filtro de categorías.
|
||||
- **Radio 2px en todo el sistema**: override de `--radius-lg/2xl/3xl` a `2px` en el `@theme` de Tailwind (cascada automática a casi todo el árbol) + `sed` global de `rounded-full` → `rounded-[2px]` en 8 archivos (dock móvil, toaster, badges de estado). Cero píldoras/círculos sobrevivieron.
|
||||
- **Sombra dura única** `-6px 6px 0 0 var(--color-ink)` con interacción "press" (`translate(-6px,6px)` en hover, vuelve a su posición en active a mitad de camino) aplicada vía clases compartidas nuevas en `global.css` (`.btn`, `.card-raised`, `.card-link`) — cero cambios por archivo necesarios en la mayoría de botones/links porque ya usaban esas clases compartidas. Los 9 `<dialog>` nativos (modales) sí se tocaron uno por uno vía `sed` para añadir borde + sombra ya que no usaban una clase compartida.
|
||||
- **Reskin manual** (no cubierto por cascada de tokens): `AppLayout.astro` (sidebar pasó de panel verde sólido a panel blanco con borde carbón; dock móvil pasó de píldora flotante con blur a barra inferior plana sin sombra, fiel a la regla del brief "nav buttons are flat"), `Toaster.tsx` (quitado `backdrop-blur-xl`/`bg-white/85`/sombra ambiental → borde + sombra dura), `login.astro`/`403.astro`/`404.astro` (headline uppercase, mismo tratamiento de card elevada), favicon (recoloreado carbón-sobre-crema, regenerado `.ico` vía chromium headless + imagemagick).
|
||||
- **Nuevo `BrandMark`**: mismo glifo de ficha/etiqueta de la sesión anterior, sin cambios de forma — solo de color (antes verde/dorado institucional, ahora carbón-sobre-azul-cielo o wash blanco translúcido sobre panel blanco).
|
||||
- Verificación: `npm run build` limpio; detector mecánico (`detect.mjs`) sin hallazgos sobre los 27 archivos tocados; ronda de screenshots batched (desktop 1440px + mobile 390px) sobre 11 páginas vía bypass temporal de middleware solo-DEV (mismo patrón de sesiones anteriores, revertido al terminar) — sin defectos materiales, no hizo falta una segunda ronda de fixes.
|
||||
- **Revisión de cierre y documentación**: sin subagentes `impeccable-finish-reviewer`/`impeccable-documenter` instalados en este entorno, ambos pases corrieron in-thread siguiendo `reference/degraded/finish-reviewer.md` y `reference/degraded/documenter.md` — disclosure explícito de la sustitución. `DESIGN.md` reescrito completo desde el mundo construido (ya no describe "La Bitácora Digital" verde institucional). `PRODUCT.md` → Brand Commitments actualizado con nota de reemplazo fechada y los valores activos.
|
||||
- **Deliberadamente NO tocado**: arquitectura de layout (sidebar desktop + dock mobile se mantienen, solo cambió su piel), copy/contenido, lógica de negocio, el escudo oficial UABC (sigue sin fabricarse, por instrucción explícita en `PRODUCT.md`).
|
||||
- Estado: cambios completos y verificados localmente, **sin commitear ni desplegar** — el usuario ya tiene un deploy previo (UABC verde) en producción; este reemplazo de identidad es mucho más grande y espera confirmación explícita antes de `git push` + deploy.
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
---
|
||||
name: LabPréstamos
|
||||
description: Sistema institucional UABC para préstamo de material de laboratorio — la bitácora de papel y Excel, digitalizada.
|
||||
description: Sistema institucional UABC para préstamo de material de laboratorio — un ticket de equipo, cruzado con un terminal de desarrollador.
|
||||
colors:
|
||||
verde-institucional: "#00723F"
|
||||
verde-institucional-hover: "#005f34"
|
||||
dorado-uabc: "#DD971A"
|
||||
dorado-uabc-hover: "#c78614"
|
||||
superficie: "#F4F7F5"
|
||||
tinta: "#2D3748"
|
||||
peligro: "#C53030"
|
||||
cream: "#f4efea"
|
||||
frost: "#ffffff"
|
||||
chalk: "#f8f8f7"
|
||||
ice: "#ebf9ff"
|
||||
ink: "#383838"
|
||||
pencil: "#a1a1a1"
|
||||
graphite: "#818181"
|
||||
primary: "#6fc2ff"
|
||||
primary-hover: "#56b4f7"
|
||||
secondary: "#ff9538"
|
||||
secondary-hover: "#ef8526"
|
||||
danger: "#f38e84"
|
||||
danger-hover: "#ec7469"
|
||||
danger-text: "#a83224"
|
||||
positive: "#38c1b0"
|
||||
attention: "#ffde00"
|
||||
typography:
|
||||
headline:
|
||||
fontFamily: "Inter Variable, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
||||
fontSize: "1.5rem (2xl) → 1.875rem (3xl) md+"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.25
|
||||
title:
|
||||
fontFamily: "Inter Variable, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
||||
fontSize: "1.125rem"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.4
|
||||
body:
|
||||
fontFamily: "Inter Variable, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
||||
fontFamily: "JetBrains Mono Variable, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
||||
fontSize: "0.875rem"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.5
|
||||
letterSpacing: "0.02em"
|
||||
label:
|
||||
fontFamily: "Inter Variable, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
||||
fontSize: "0.875rem"
|
||||
fontWeight: 500
|
||||
lineHeight: 1.3
|
||||
caption:
|
||||
fontFamily: "Inter Variable, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
||||
fontFamily: "JetBrains Mono Variable, ui-monospace, monospace"
|
||||
fontSize: "0.75rem"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.4
|
||||
fontWeight: 500
|
||||
letterSpacing: "0.05em"
|
||||
textTransform: uppercase
|
||||
rounded:
|
||||
control: "0.5rem"
|
||||
emphasis: "1rem"
|
||||
pill: "9999px"
|
||||
control: "2px"
|
||||
spacing:
|
||||
tight: "0.5rem"
|
||||
base: "1rem"
|
||||
@@ -46,28 +40,28 @@ spacing:
|
||||
section: "2rem"
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.verde-institucional}"
|
||||
textColor: "#ffffff"
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.control}"
|
||||
padding: "10px 16px"
|
||||
button-secondary:
|
||||
backgroundColor: "{colors.dorado-uabc}"
|
||||
textColor: "#ffffff"
|
||||
backgroundColor: "{colors.frost}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.control}"
|
||||
padding: "10px 16px"
|
||||
button-ghost:
|
||||
backgroundColor: "transparent"
|
||||
textColor: "{colors.tinta}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.control}"
|
||||
padding: "10px 16px"
|
||||
card:
|
||||
backgroundColor: "#ffffff"
|
||||
textColor: "{colors.tinta}"
|
||||
backgroundColor: "{colors.frost}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.control}"
|
||||
padding: "24px"
|
||||
input:
|
||||
backgroundColor: "#ffffff"
|
||||
textColor: "{colors.tinta}"
|
||||
backgroundColor: "{colors.frost}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.control}"
|
||||
padding: "10px 12px"
|
||||
---
|
||||
@@ -76,151 +70,140 @@ components:
|
||||
|
||||
## Overview
|
||||
|
||||
**Creative North Star: "La Bitácora Digital"**
|
||||
**Creative North Star: "The Equipment Tag on a Terminal Readout"**
|
||||
|
||||
LabPréstamos is the printed loan sheet and the Excel logbook it used to feed, collapsed into one live surface. Every visual decision defends that lineage: nothing decorative, nothing that would feel out of place next to an official UABC form, but nothing that feels like a cold spreadsheet either. The system is **cálido pero confiable** — warm but reliable — and it earns the "warm" half narrowly: a gold accent, soft rounded corners, and gentle motion sit on top of an otherwise flat, administrative base.
|
||||
LabPréstamos was rebuilt against a fully pinned external brief (MotherDuck's crayon-terminal-on-cream neobrutalism), translated from a marketing site's vocabulary into an operational lab tool's. The identity keeps the source system's mechanics intact — a warm cream canvas, monospace type standing in for a terminal readout, hard 2px corners, and a flat charcoal-offset shadow that never blurs — while dropping what only belonged to marketing (hand-drawn duck illustrations, scrolling marquees, testimonial cards). What replaces the duck is the product's own mark: a loan tag with a punch-hole, echoing the paper ticket this system replaced.
|
||||
|
||||
The page itself stays flat and quiet — white cards on a pale green-grey surface, hairline borders instead of shadows, tabular numbers lined up like a real ledger. Depth is reserved for the two things that are genuinely *floating above* the page: the toast notifications and the mobile navigation dock. That asymmetry is deliberate — it is how the system tells the eye "this is the document" from "this is the chrome."
|
||||
Depth here is binary, not a hierarchy: an element either sits flat on the page (bordered, no shadow — tables, list rows, inputs, the nav) or it's a discrete raised object that "presses" into its own shadow on hover (buttons, standalone cards, modals, the login card). There is no soft, no blur, no ambient glow anywhere in the system — every visual weight decision reads as a hard yes/no.
|
||||
|
||||
Two audiences read this bitácora differently. The alumno's default device is the phone — the catalog, the request modal, the pill-shaped floating dock are all tuned for thumbs. The admin's default device is the desktop — dense tables, a persistent sidebar, KPI tiles. Neither view degrades on the other's device; every screen ships both a table and a card-list rendering.
|
||||
Color plays a stricter role here than in the source brief. Sky Crayon blue is reserved for actionable, filled controls — never for status. A teal (Mint Sketch) was pulled out of the source's decorative rainbow set and fixed as the system's "positive/available" status color, because an operational tool needs that distinction and a marketing site never did. Coral Sketch was likewise fixed as the sole danger color, with a separately darkened text variant (`--color-danger-text`, `#a83224`) for body copy, because the source coral fails AA contrast on white at normal text weight — the fill stays light and loud, the text goes dark and readable.
|
||||
|
||||
**Key Characteristics:**
|
||||
- Flat administrative base (white cards, hairline ink-tinted borders, no ambient shadow)
|
||||
- Shadow appears only on floating overlay chrome — the mobile dock and toasts — never on the page plane
|
||||
- Every status is a tinted pill: 12–15% color wash background, full-strength text
|
||||
- Tabular numbers everywhere a quantity, date, or stock figure appears
|
||||
- One typeface (Inter Variable) carrying every role through weight and size alone
|
||||
- Motion is a courtesy, not a performance: `motion-safe:`-gated, specific-property transitions, no `transition-all`
|
||||
- Cream page canvas, white card surfaces, charcoal (`#383838`, never pure black) as the universal line, border, and text color
|
||||
- Every corner is 2px — buttons, cards, inputs, dialogs, badges, the brand mark's own chip. No pills, no circles, no `rounded-full` anywhere in the system
|
||||
- One shadow, one behavior: `-6px 6px 0 0 var(--color-ink)`, hard-edged, zero blur, on every button, standalone card, toast, and modal — and it flattens into the element on hover/focus (`translate(-6px, 6px)`)
|
||||
- JetBrains Mono Variable everywhere, 0.02em tracking, uppercase treatment on nav labels, button labels, section labels, and headlines
|
||||
- Status pills and count badges are tinted fills with a matching-color 1.5px border and always-charcoal text — color differentiates via background and border, never via colored type
|
||||
- Dense, repeated surfaces (tables, list-item cards, form inputs) stay flat and bordered; only discrete, single-instance elements carry the shadow
|
||||
|
||||
## Colors
|
||||
|
||||
Two institutional hues carry the whole palette under a strict 60/30/10 split — green dominant, gold as a deliberately rare accent, everything else neutral ink and white.
|
||||
The palette is inherited near-verbatim from the pinned MotherDuck brief; only the semantic *assignment* changes to fit an operational tool instead of a marketing site.
|
||||
|
||||
### Primary
|
||||
- **Verde Institucional** (`#00723F`): the UABC green. Primary buttons, the desktop sidebar's entire background, active nav states, focus rings, "healthy/positive" status pills (disponible, aprobado, devuelto), the login mark. This is the color of the system's authority — it appears on anything that confirms, approves, or orients.
|
||||
- **Sky Crayon** (`#6fc2ff`): the system's only filled-action color. Primary buttons, active nav state, focused/selected chips, "Solicitar"/"Aprobar"/"Filtrar"/"Continuar con Google." Text on it is always Charcoal Ink, never white — matching every other filled control in the system.
|
||||
|
||||
### Secondary
|
||||
- **Dorado UABC** (`#DD971A`): the institutional gold. Used sparingly and only as an accent — secondary buttons, the "en préstamo" KPI tile, "mantenimiento" status pills, the logo-mark square on the sidebar/mobile header. Never the dominant color of a screen.
|
||||
- **Duck Bill Orange** (`#ff9538`): sparse accent, repurposed from the source's "illustration/marquee" role (this app has neither) into the operational tool's low-emphasis-but-real accent — "Exportar CSV," "Mantenimiento" status tint, the "En préstamo" KPI tone.
|
||||
|
||||
### Neutral
|
||||
- **Superficie** (`#F4F7F5`): the page background — a near-white green-grey, not pure white. Everything else (cards, dialogs, the desktop sidebar's text, dock) sits on top of it.
|
||||
- **Tinta** (`#2D3748`): body text, and — critically — the **source color for every border in the system**. Borders are never a fixed gray; they're `color-mix(in oklab, var(--color-ink) N%, transparent)` at 8–20% depending on prominence. This ties every hairline to the same ink token instead of a separate gray scale.
|
||||
- **Peligro** (`#C53030`): errors, destructive actions, and the one status that is allowed to break the tinted-pill convention (see Named Rules).
|
||||
- **Cream Paper** (`#f4efea`): the page canvas — every screen's base layer.
|
||||
- **Frost White** (`#ffffff`): card, dialog, sidebar, and nav surfaces — the raised layer on cream.
|
||||
- **Chalk Gray** (`#f8f8f7`): hover wash on flat (non-shadowed) interactive rows, and the neutral badge fill for "Baja"/inactive states.
|
||||
- **Charcoal Ink** (`#383838`): the universal line — every border, every default text color, the shadow color itself. Never pure black.
|
||||
- **Pencil Gray** (`#a1a1a1`): secondary/muted text (labels under headings, metadata, roles) — a real token, not an opacity trick.
|
||||
|
||||
### Status colors (extension beyond the source brief)
|
||||
- **Mint Sketch → Positive** (`#38c1b0`): "disponible," "aprobado," "devuelto en préstamo" status. Pulled out of the source's decorative rainbow-button palette because the operational tool needs a status color distinct from the primary action color; the source brief, built for a marketing site, never had this problem.
|
||||
- **Coral Sketch → Danger** (`#f38e84` fill / `#a83224` text): "rechazado," "vencido," destructive actions. Split into two values because the fill color (used for badge backgrounds, borders, button fills) is deliberately light and loud, while body text needs the darker `-text` variant to clear 4.5:1 contrast on white.
|
||||
|
||||
### Named Rules
|
||||
**The Ink-Mix Border Rule.** Every hairline border and divider is `color-mix(in oklab, var(--color-tinta) N%, transparent)`, never a literal gray hex. A border is always "ink, diluted," so it stays coherent with body text at any opacity.
|
||||
**The Charcoal Text Rule.** No filled or tinted surface ever carries white or colored text — text is always Charcoal Ink, full stop. Color differentiates status and action through background fill and border only. This is a deliberate tightening of the source brief, which used white nav-button text as an exception; LabPréstamos has no exception.
|
||||
|
||||
**The Vencido Exception.** Every status pill uses the tinted-fill pattern (see Components → Badges) *except* an overdue loan ("Vencido"), which gets a solid `peligro` fill with white text — on both the table badge and, on mobile, a light danger wash across the entire card background. This is the system's one permitted alarm: reserve a solid semantic fill for the single state that genuinely needs to interrupt a scan, and only that state.
|
||||
**The One Shadow Rule.** `-6px 6px 0 0 var(--color-ink)` is the only shadow value in the system. It never blurs, never softens, never gets a second variant for "subtle" elevation — an element either has this exact shadow or it has none.
|
||||
|
||||
## Typography
|
||||
|
||||
**Body & Display Font:** Inter Variable (with `ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif` fallback)
|
||||
**Body & Display Font:** JetBrains Mono Variable (weights 100–800 available; the system uses 400/500/600), with `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace` fallback. Chosen as the pinned brief's own named substitute for the licensed Aeonik Mono.
|
||||
|
||||
**Character:** One typeface for the entire system — hierarchy comes from weight and size, never a second family. It reads as a single, consistent institutional voice rather than a "brand font + UI font" split.
|
||||
**Character:** One monospace face for the entire system, 0.02em letter-spacing throughout to open up the mechanical density of mono at small sizes. Uppercase tracking is reserved for structural labels (nav items, button text, form labels, section headers, the login headline) — body copy, names, and descriptions stay in normal case so real data (student names, material names) stays legible rather than shouting.
|
||||
|
||||
### Hierarchy
|
||||
- **Headline** (600, 1.5rem → 1.875rem at `md:`, 1.25 line-height): page-level `<h1>`s — "Mis préstamos", "Estadísticas", the login screen's title.
|
||||
- **Title** (600, 1.125rem, 1.4 line-height): section and card headers — "Inicia sesión", "Inventario", empty-state headings.
|
||||
- **Body** (400, 0.875rem, 1.5 line-height): the default text size for the whole system — table cells, descriptions, list content. Notably small-scale throughout; this is a dense operational tool, not editorial content.
|
||||
- **Label** (500, 0.875rem): form field labels (`.label`), rendered at 90% ink opacity.
|
||||
- **Caption** (400, 0.75rem, opacity 60–80%): metadata — matrícula, inventory numbers, timestamps, hint text under fields.
|
||||
- **Headline** (600, 1.5rem → 1.875rem at `md:`, uppercase): page `<h1>`s, the login screen's title.
|
||||
- **Title** (600, 1.125rem): section and card headers, dialog titles — not uppercased by default, uppercased where it doubles as a nav/label element.
|
||||
- **Body** (400, 0.875rem, 1.5 line-height): the default text size — table cells, descriptions, list content.
|
||||
- **Label** (500, 0.75rem, 0.05em tracking, uppercase): every form field label (`.label`), nav items, badge text.
|
||||
- **Caption** (400, 0.75rem, Pencil Gray): metadata — matrícula, inventory numbers, timestamps.
|
||||
|
||||
### Named Rules
|
||||
**The Tabular Numbers Rule.** Any figure that represents a count — cantidad, stock (`disponible / total`), KPI values, dates — is rendered with `font-variant-numeric: tabular-nums`. Numbers must align like a ledger column, because that is literally what they replace.
|
||||
**The Tabular Numbers Rule.** Any figure representing a count — cantidad, stock, KPI values, dates — carries `font-variant-numeric: tabular-nums`, inherited unchanged from the prior system. JetBrains Mono's own distinctive dotted-zero glyph reinforces the "terminal readout" reading at large KPI sizes.
|
||||
|
||||
## Layout
|
||||
|
||||
The system runs on Tailwind's default breakpoint (`md:` = 768px) as the single hard split between the mobile and desktop layouts — there is no intermediate tablet treatment.
|
||||
|
||||
**Desktop (≥768px):** a fixed 256px (`w-64`) left sidebar in solid Verde Institucional, full page height. It is collapsible: a hamburger button above the content toggles a `sidebar-collapsed` class on `<html>`, animated with a 200ms `translateX(-100%)`; the choice persists in `localStorage` and is applied via a blocking inline `<script>` in `<head>` before paint, so there is no collapse flash on reload. Content sits in `max-w-6xl` (data-dense admin views) or `max-w-4xl`/`max-w-md` (narrower alumno views), with `p-8` outer padding.
|
||||
|
||||
**Mobile (<768px):** no sidebar. A minimal fixed header (logo mark + user identity) sits at the top; navigation lives instead in a floating pill-shaped dock anchored to the bottom-center of the viewport, respecting `env(safe-area-inset-bottom)`. Content padding drops to `p-4` with `pb-28` to clear the dock.
|
||||
|
||||
**The Table/Card Swap Rule.** Every data-dense admin view (solicitudes, inventario, reportes) ships two complete renderings of the same data: a `<table>` for `md:block` and a `<ul>` of `.card` items for `md:hidden`. This is a structural swap, not a responsive squeeze — the mobile version is not a scrolled-sideways table, it is a genuinely different layout for the same rows.
|
||||
Unchanged from the incumbent structural system — the redesign is a full re-skin, not a re-architecture. `md:` (768px) remains the single hard breakpoint between mobile and desktop. Desktop keeps the fixed 256px collapsible sidebar; mobile keeps a bottom navigation bar, now a flush, flat, full-width strip (not a floating pill) with a 2px charcoal top border and no shadow — matching the source brief's own flat, unshadowed nav-button treatment. Content still ships both a `md:block` table and a `md:hidden` card-list rendering for every data-dense admin view.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
The system is flat by default. Cards, buttons, inputs, and dialogs carry no shadow — only a 1px ink-mix hairline border. Depth is reserved, deliberately, for the two things that are not part of the page: the mobile dock nav and the toast stack, both of which visually float above whatever is scrolling underneath them.
|
||||
Binary, not graduated. An element is either **flat** (a 1.5–2px charcoal border, no shadow — the default for anything dense or repeated: tables, list-item cards, inputs, the nav) or **raised** (2px charcoal border plus the one hard shadow — reserved for discrete, single-instance, actionable surfaces: buttons, the login card, modals/dialogs, toasts, KPI tiles, and clickable dashboard tiles). There is no third state, no blur, no soft ambient shadow anywhere in the system.
|
||||
|
||||
### Shadow Vocabulary
|
||||
- **Floating chrome** (`box-shadow: 0 10px 30px -8px rgba(0,0,0,0.25)`): the mobile dock nav — a soft, diffuse shadow that reads as "hovering," combined with `backdrop-blur` and a translucent white background.
|
||||
- **Overlay notification** (`box-shadow: 0 16px 40px -12px rgba(0,0,0,0.25)`): the toast stack — same diffuse quality, slightly heavier, paired with `backdrop-blur-xl` and `bg-white/85`.
|
||||
- **Hard** (`box-shadow: -6px 6px 0 0 var(--color-ink)`, token `--shadow-hard`): every raised element at rest.
|
||||
- **Hard, pressed** (`transform: translate(-6px, 6px)`, shadow reads as `0 0 0 0`): the same element on hover/focus — it visually "presses" into its own shadow. Active state presses halfway (`--shadow-hard-sm`, `-3px 3px`) for a two-stage press.
|
||||
|
||||
### Named Rules
|
||||
**The Flat Page, Floating Chrome Rule.** If an element is part of the document (a card, a table, a form), it stays flat with a hairline border. If an element floats above the document regardless of scroll (the dock, a toast), it gets a soft ambient shadow plus backdrop-blur. Shadow is the visual signal for "this is not part of the page you're reading."
|
||||
**The Press Rule.** Every raised element's hover state is the same transform: translate exactly the shadow's own offset, so the shadow disappears under the element. No brightness filter, no color darken — motion is the only hover signal on raised elements.
|
||||
|
||||
## Shapes
|
||||
|
||||
Three radius steps, used by role rather than by component:
|
||||
|
||||
- **Control** (`0.5rem`, token `--radius-card`): the default — buttons, cards, inputs, dialogs, table containers. This is the system's baseline corner.
|
||||
- **Emphasis** (`1rem`, `rounded-2xl`): reserved for two "first impression" surfaces — the login screen's avatar mark and toast notifications. A visibly softer corner than the control radius, used to signal warmth at the exact moments the system greets or interrupts the user.
|
||||
- **Pill** (`9999px`, `rounded-full`): status badges, the mobile dock and its buttons, the pending-count chip. Anything that is a small, discrete piece of state rather than a container.
|
||||
|
||||
Borders are always hairline (1px) and always ink-mix (see Colors → Named Rules); the system has no heavy or decorative border weight.
|
||||
One radius for the entire system: **2px** (`--radius-card`, and Tailwind's `rounded-lg`/`rounded-2xl`/`rounded-full` are globally overridden to resolve to it). Buttons, cards, inputs, dialogs, badges, the brand mark's chip — everything is a near-square corner. There is no pill, no circle, no soft/emphasis radius tier; the prior system's `rounded-full` nav dock and status pills were converted wholesale. Borders are always 1.5–2px solid Charcoal Ink (never a tinted or literal-gray border) — the system has no hairline-border convention.
|
||||
|
||||
## Components
|
||||
|
||||
### Brand Mark
|
||||
- A single authored glyph (`BrandMark.astro`, 24×24 viewBox) stands in for the system's identity everywhere a mark is needed: the desktop sidebar header, the mobile header, the login screen, and the 403/404 states. It reads as a loan tag with a punch-hole — a quiet nod to the physical préstamo ticket the system replaces.
|
||||
- It is a solid silhouette (not an outline stroke like the nav icons), rendered via `currentColor` so it can sit white-on-primary or tinted-on-danger without a second asset.
|
||||
- It is the product's own mark, not an institutional crest — it never stands in for an official UABC or laboratorio seal.
|
||||
- The favicon (`public/favicon.svg`) is the same glyph, colored Verde Institucional in light mode and Superficie in dark mode.
|
||||
|
||||
### Buttons
|
||||
- **Shape:** Control radius (0.5rem), 44px minimum height, `touch-action: manipulation` — every button is a comfortable tap target regardless of platform.
|
||||
- **Primary:** Verde Institucional background, white text. The system's default call to action (submit, approve, continuar con Google).
|
||||
- **Secondary:** Dorado UABC background, white text. Used sparingly — filtering actions, not primary flows.
|
||||
- **Ghost:** Transparent background, ink text, hairline ink-mix border (15%). Used for "Cancelar" / "Limpiar" — the low-commitment counterpart to Primary.
|
||||
- **Hover / Focus:** hover is a uniform `motion-safe:hover:brightness-110` filter across all filled variants (not a swap to a separate hover-color token); focus is a 2px solid outline in Verde Institucional, 2px offset. Disabled drops to 50% opacity and disables pointer events.
|
||||
- **Shape:** 2px radius, 2px Charcoal Ink border, 44px minimum height, `touch-action: manipulation`.
|
||||
- **Primary:** Sky Crayon fill, Charcoal Ink text, hard shadow. The default call to action.
|
||||
- **Secondary:** Frost White fill (not a second brand color — the source brief is explicit that Sky Crayon is the sole filled-action color), Charcoal Ink border and text, hard shadow.
|
||||
- **Ghost:** transparent fill, Charcoal Ink text, a lighter border (35% ink-mix), no shadow. Used for "Cancelar"/"Limpiar."
|
||||
- **Hover / Focus / Active:** see Elevation → The Press Rule.
|
||||
|
||||
### Badges / Status Pills
|
||||
- **Style:** pill radius, `text-xs`/`text-sm font-semibold`, `px-2` to `px-2.5`, `py-0.5`.
|
||||
- **The Tinted Pill Rule.** Background = the semantic color at 12–15% via `color-mix`; text = the semantic color at full strength. Never a solid fill — with the one confirmed exception below.
|
||||
- **Semantic mapping:** disponible / aprobado / devuelto → Verde Institucional tint. mantenimiento → Dorado UABC tint (text at the `-hover` shade for contrast). baja / rechazado → ink tint (neutral, ~8–10%). **Vencido** (overdue) → the sole solid-fill exception: white text on full-strength Peligro, plus a light danger wash across the whole card on mobile.
|
||||
- **Style:** 2px radius, 1.5px border in the semantic color, uppercase text at 0.03em tracking, always Charcoal Ink.
|
||||
- **The Tinted-Fill-Bordered Rule.** Background = the semantic color mixed 18–25% into white; border = the semantic color at full strength; text = Charcoal Ink always. Count badges (the "N pendientes" chip beside a heading) use a solid semantic fill instead of a tint, bordered in Charcoal Ink — the one place a badge goes solid.
|
||||
- **Semantic mapping:** disponible / aprobado / devuelto → Positive (mint). mantenimiento / en préstamo (KPI) → Secondary (orange). rechazado / vencido → Danger (coral). baja / neutral counts → Chalk Gray with a graphite border.
|
||||
|
||||
### Cards / Containers
|
||||
- **Corner:** Control radius (0.5rem).
|
||||
- **Background:** white, always — the one surface color that appears identically on both the pale-green page background and the green sidebar.
|
||||
- **Border:** hairline ink-mix (10%); no shadow (see Elevation).
|
||||
- **Internal Padding:** `p-6` for standard cards and forms; `p-4` for dense KPI tiles; `p-0` when the card is a table/list wrapper (the table supplies its own cell padding).
|
||||
- **Corner:** 2px.
|
||||
- **Background:** Frost White, always.
|
||||
- **Flat variant** (`.card-flat`, `.card`): 1.5–2px Charcoal Ink border, no shadow. Used for tables, list rows, dense repeated content.
|
||||
- **Raised variant** (`.card-raised`, `.card-link`): adds the hard shadow. Used for the login card, clickable dashboard tiles, KPI tiles — anything discrete and singular rather than part of a repeated list.
|
||||
- **Internal Padding:** `p-6` standard, `p-4` for dense KPI tiles, `p-0` when the card wraps a table (the table supplies its own cell padding).
|
||||
|
||||
### Inputs / Fields
|
||||
- **Style:** white background, hairline ink-mix border (20%), Control radius, 44px minimum height.
|
||||
- **Focus:** 2px solid Verde Institucional outline, 2px offset — the same focus treatment as buttons and links, no separate glow or border-color shift.
|
||||
- **Label:** `.label` class, 0.875rem/500 weight, ink at 90% opacity, always paired via `htmlFor`/`id`.
|
||||
- **Error:** inline `role="alert"` text in Peligro below the field, not a border-color change.
|
||||
- **Style:** Frost White background, 2px Charcoal Ink border, 2px radius, 44px minimum height. No shadow — inputs are always flat, matching the source brief's own Text Input spec.
|
||||
- **Focus:** border color shifts to Sky Crayon; no glow, no ring.
|
||||
- **Label:** `.label` — uppercase, 0.75rem, 0.05em tracking, Charcoal Ink.
|
||||
- **Error:** inline `role="alert"` text in Danger-text below the field.
|
||||
|
||||
### Navigation
|
||||
- **Desktop sidebar:** solid Verde Institucional panel, white text at 85% opacity for inactive items, white/15% background wash for the active item, `aria-current="page"`. Collapsible (see Layout).
|
||||
- **Mobile dock:** floating pill, `backdrop-blur` + `bg-white/95`, icon-only (24×24 heroicons-outline, `stroke-width: 1.8`, `currentColor`), active item = solid Verde Institucional circle. Sign-out is visually separated by a vertical hairline divider and turns Peligro on hover.
|
||||
- **Desktop sidebar:** Frost White panel (not a colored panel — a deliberate departure from the prior UABC-green sidebar, matching the source brief's flat white nav bar), 2px Charcoal Ink right border. Active item = solid Sky Crayon chip with a 2px Charcoal border; inactive = transparent with a Chalk Gray hover wash. Uppercase labels throughout. Collapsible, same mechanism as before (localStorage + blocking inline script, no flash).
|
||||
- **Mobile nav:** a flush, full-width bottom bar (not a floating pill), Frost White, 2px Charcoal top border, no shadow — flat, matching the source brief's "nav buttons are flat" rule. Active icon = a 2px-radius Sky Crayon square with Charcoal border.
|
||||
- **Brand mark chip:** on colored (Sky Crayon) or panel backgrounds, a 2px-radius chip with a 2px Charcoal border; on the white sidebar/mobile header it's a translucent white wash instead, avoiding a third solid color competing with the mark itself.
|
||||
|
||||
### Toasts (signature component)
|
||||
- Fixed top-center overlay, `rounded-2xl`, `bg-white/85` + `backdrop-blur-xl`, floating-chrome shadow.
|
||||
- A circular color chip (36px) leads each toast: Verde Institucional for success, Peligro for error, Dorado UABC for info — each with a small stroke icon.
|
||||
- Entrance: 260ms `cubic-bezier(0.22, 1, 0.36, 1)` translate+scale+fade; auto-dismisses at 3.8s; tap anywhere to dismiss early; fully respects `prefers-reduced-motion`.
|
||||
- Survives a `location.reload()` via a `sessionStorage` queue — the system was built assuming most actions end in a reload, and the toast must not be lost.
|
||||
### Toasts
|
||||
- Frost White, 2px Charcoal Ink border, hard shadow, 2px radius (down from the prior system's blurred glass `rounded-2xl` treatment — MotherDuck bans blur and ambient shadow outright).
|
||||
- A 2px-bordered color chip (36px) leads each toast: Positive for success, Danger for error, Secondary for info — Charcoal Ink stroke icon, not white.
|
||||
- Entrance: 260ms translate+fade, same timing as before; still respects `prefers-reduced-motion`; still survives `location.reload()` via the sessionStorage queue.
|
||||
|
||||
### Modals
|
||||
- Native `<dialog>` element (no custom overlay library), Control radius, `backdrop:bg-black/40`, width `min(92vw, 28rem)`.
|
||||
- Focus moves to the first field on open; closes on backdrop click or the close-icon control (a drawn SVG X, never a `×` character); disabled while a submit is in flight.
|
||||
- Native `<dialog>`, 2px radius, 2px Charcoal Ink border, hard shadow, `backdrop:bg-black/40`. The prior system's borderless, shadowless dialog surface gained both a border and the system's one shadow — modals are a "raised" surface by the binary elevation rule.
|
||||
- Close controls are always a drawn SVG X (never a `×` character), matching the rest of the icon system.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do:
|
||||
- **Do** use `color-mix(in oklab, var(--color-tinta) N%, transparent)` for any new border or divider — never a literal gray.
|
||||
- **Do** keep shadow exclusive to floating overlay chrome (toasts, the mobile dock). A new floating element earns the same shadow vocabulary; a page element does not.
|
||||
- **Do** use the tinted-pill pattern (12–15% background, full-strength text) for any new status or state indicator.
|
||||
- **Do** apply `font-variant-numeric: tabular-nums` to any new numeric quantity, stock figure, or date.
|
||||
- **Do** ship both a `md:block` table and a `md:hidden` card-list rendering for any new data-dense admin view.
|
||||
- **Do** gate all motion with `motion-safe:` and animate specific properties (`transition-colors`, `transition-[filter,...]`), never `transition-all`.
|
||||
- **Do** give every interactive control a 44px minimum tap target and `touch-action: manipulation`.
|
||||
- **Do** draw every icon as an SVG (the shared heroicons-outline language, or the `BrandMark` glyph for identity) — close controls, chevrons, and status glyphs included.
|
||||
- **Do** use the exact shadow `-6px 6px 0 0 var(--color-ink)` (`--shadow-hard`) for every raised element, and the exact press transform `translate(-6px, 6px)` on its hover/focus state.
|
||||
- **Do** keep every radius at 2px — buttons, cards, inputs, dialogs, badges, chips. No exceptions, no emphasis tier.
|
||||
- **Do** keep text Charcoal Ink on every fill, tint, or border color. Never white, never the semantic color itself.
|
||||
- **Do** reserve Sky Crayon for actionable, filled controls; use Positive (mint), Danger (coral), or Secondary (orange) for status and non-primary accents.
|
||||
- **Do** keep dense/repeated surfaces (tables, list cards, inputs, nav) flat and bordered; reserve the hard shadow for discrete, singular, actionable surfaces.
|
||||
- **Do** draw every icon as an SVG in the shared heroicons-outline language (or the `BrandMark` glyph for identity) — never a unicode glyph or emoji standing in for one.
|
||||
- **Do** apply `font-variant-numeric: tabular-nums` to every numeric quantity, stock figure, or date.
|
||||
- **Do** ship both a `md:block` table and a `md:hidden` card-list rendering for every data-dense admin view.
|
||||
|
||||
### Don't:
|
||||
- **Don't** stand in a unicode glyph (`×`, `›`, `✓`, emoji) for an icon — every icon is a drawn SVG, one consistent stroke and weight.
|
||||
- **Don't** add a shadow to `.card`, `.btn`, or `.input` — the system's flatness on the page plane is deliberate.
|
||||
- **Don't** introduce a second typeface. Inter Variable carries every role through weight and size.
|
||||
- **Don't** give a status pill a solid saturated fill — the tinted wash is the rule, and "Vencido" is the system's one confirmed exception, not a precedent to extend.
|
||||
- **Don't** reach for `rounded-2xl` outside the login mark and toasts; it is reserved for "first impression / interruption" moments, not a general card radius.
|
||||
- **Don't** replace the mobile bottom dock with a hamburger drawer, or the desktop sidebar with a top bar — the device-appropriate nav pattern per breakpoint is an established, deliberate split.
|
||||
- **Don't** use blur, glass, or a soft ambient shadow anywhere — every shadow is the one hard-edged offset value, or there is no shadow.
|
||||
- **Don't** introduce a radius above 2px, or a pill/circle shape, anywhere in the UI.
|
||||
- **Don't** use a second filled brand color for "secondary" actions — Secondary buttons are white-filled and charcoal-outlined, never a second solid color.
|
||||
- **Don't** reuse Sky Crayon for status pills — status has its own colors (Positive/Danger/Secondary); Sky Crayon means "you can act on this."
|
||||
- **Don't** introduce a second typeface. JetBrains Mono Variable carries every role through weight, size, and case.
|
||||
- **Don't** stand a unicode glyph in for an icon.
|
||||
|
||||
+5
-3
@@ -41,10 +41,12 @@ Herramienta interna de un solo laboratorio (Laboratorio de Sistemas Computaciona
|
||||
|
||||
## Brand Commitments
|
||||
|
||||
- Paleta institucional UABC bajo regla 60/30/10: primario `#00723F` (verde), secundario `#DD971A` (dorado), superficie `#F4F7F5`, texto `#2D3748`, error `#C53030`.
|
||||
- Tipografía: Inter Variable.
|
||||
- **2026-08-16: identidad visual reemplazada por decisión explícita del usuario.** La paleta institucional UABC verde/dorada (60/30/10) fue retirada del sistema de diseño; se conserva únicamente como posible insumo para una futura marca oficial si el usuario la reintroduce. El sistema actual sigue un brief externo fijado por el usuario (estilo MotherDuck: "crayon-coded terminal on cream paper", neobrutalista) — ver `DESIGN.md` para los tokens completos y su justificación de por qué cada valor se adaptó a una herramienta operativa en vez de un sitio de marketing.
|
||||
- Paleta activa: canvas crema `#f4efea`, superficie blanca `#ffffff`, tinta carbón `#383838` (nunca negro puro), acción primaria azul cielo `#6fc2ff`, acento secundario naranja `#ff9538`, positivo/disponible menta `#38c1b0`, peligro coral `#f38e84` (texto `#a83224` para contraste AA).
|
||||
- Tipografía: JetBrains Mono Variable (sustituto abierto de Aeonik Mono, la fuente con licencia que nombra el brief original), monoespaciada en todo el sistema, tracking 0.02em.
|
||||
- Radios de esquina: 2px en todo — sin píldoras, sin círculos, sin `rounded-full`. Sombra dura única `-6px 6px 0 0` en tinta carbón sobre elementos elevados; superficies densas/repetidas quedan planas con borde.
|
||||
- La identidad visual usa un glifo propio de producto (`BrandMark`, una ficha/etiqueta con perforación — evoca la ficha de préstamo física) como wordmark gráfico y favicon; no es ni pretende ser el escudo/sello oficial de UABC o del laboratorio. **Existe un logo institucional oficial** pendiente de integrar — el usuario lo proporcionará como asset; hasta entonces no inventar ni aproximar ese escudo.
|
||||
- Admin es desktop-first, alumno es mobile-first; ambos cross-device. Minimalista, con animaciones ligeras (`motion-safe`).
|
||||
- Admin es desktop-first, alumno es mobile-first; ambos cross-device. La estructura de layout (sidebar/dock) no cambió — solo su piel visual.
|
||||
|
||||
## Evidence on Hand
|
||||
|
||||
|
||||
Generated
+10
@@ -11,6 +11,7 @@
|
||||
"@astrojs/node": "^11.1.2",
|
||||
"@astrojs/react": "^6.0.2",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@supabase/ssr": "^0.12.4",
|
||||
"@supabase/supabase-js": "^2.112.3",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
@@ -1256,6 +1257,15 @@
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/jetbrains-mono": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz",
|
||||
"integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@astrojs/node": "^11.1.2",
|
||||
"@astrojs/react": "^6.0.2",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@supabase/ssr": "^0.12.4",
|
||||
"@supabase/supabase-js": "^2.112.3",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
+2
-7
@@ -1,9 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M2 12 L9 4 H16.5 A2.5 2.5 0 0 1 19 6.5 V17.5 A2.5 2.5 0 0 1 16.5 20 H9 Z M7.3 12 a1.3 1.3 0 1 0 -2.6 0 a1.3 1.3 0 1 0 2.6 0 Z" />
|
||||
<style>
|
||||
path { fill: #00723F; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #F4F7F5; }
|
||||
}
|
||||
</style>
|
||||
<rect x="0.5" y="0.5" width="23" height="23" rx="2" fill="#f4efea" stroke="#383838" stroke-width="1.5" />
|
||||
<path fill-rule="evenodd" fill="#383838" d="M4.5 12 L10 6.5 H15.5 A1.8 1.8 0 0 1 17.3 8.3 V15.7 A1.8 1.8 0 0 1 15.5 17.5 H10 Z M8.6 12 a1 1 0 1 0 -2 0 a1 1 0 1 0 2 0 Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 354 B |
@@ -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>
|
||||
)}
|
||||
|
||||
+42
-38
@@ -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">
|
||||
<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">
|
||||
<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