style: reskin NovaSpark en las 7 apps

Del look editorial (verde #1B4332, esquinas a 0, botones en mayusculas)
al sistema NovaSpark: primario azul #2563EB, detalle naranja #F97316,
fondo #F8FAFC con tarjetas blancas rounded-xl + shadow-sm, chips
rounded-full semanticos, focus ring azul, transiciones 100-200ms.
Incluye headers de tabla en los PDFs (cotizador y estado de cuenta).
This commit is contained in:
2026-09-03 02:35:34 +00:00
parent 89fcd1c579
commit 599dce2629
78 changed files with 842 additions and 828 deletions

View File

@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
}
return (
<div className="min-h-screen bg-white text-neutral-900">
<div className="border-b border-neutral-200">
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
<div className="border-b border-[#E5E7EB] bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
<span className="text-xs font-medium text-[#64748B]">
{sesion.usuario.nombre} · {sesion.usuario.rol}
</span>
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => setModalPassword(true)}
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
className="rounded-md px-2 py-1.5 text-xs font-medium text-[#64748B] transition-all duration-100 ease-out hover:text-[#111827]"
>
Contraseña
</button>
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
logout()
setSesion(null)
}}
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
className="rounded-md px-2 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:text-accent-hover"
>
Salir
</button>

View File

@@ -6,8 +6,10 @@ function Burbuja({ mensaje }: { mensaje: Mensaje }) {
<div className={`flex ${esUsuario ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] ${esUsuario ? 'text-right' : 'text-left'}`}>
<div
className={`inline-block whitespace-pre-wrap px-4 py-3 text-sm leading-relaxed ${
esUsuario ? 'bg-accent text-white' : 'bg-neutral-100 text-neutral-900'
className={`inline-block whitespace-pre-wrap rounded-2xl px-4 py-3 text-sm leading-relaxed ${
esUsuario
? 'rounded-br-md bg-accent text-white shadow-sm'
: 'rounded-bl-md border border-[#E5E7EB] bg-white text-[#111827] shadow-sm'
}`}
>
{mensaje.contenido}
@@ -18,7 +20,7 @@ function Burbuja({ mensaje }: { mensaje: Mensaje }) {
<span
key={f.titulo}
title={`Relevancia: ${(f.score * 100).toFixed(0)}%`}
className="border border-neutral-300 px-2 py-1 text-xs font-bold uppercase tracking-widest text-neutral-500"
className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-700"
>
{f.titulo}
</span>

View File

@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
import { cambiarPassword } from '../services/auth'
const CLASE_INPUT =
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
/**
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
aria-modal="true"
aria-label="Cambiar contraseña"
>
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
Cambiar contraseña
</h2>
{exito ? (
<>
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
<p className="rounded-lg bg-green-50 px-4 py-3 text-xs font-medium text-green-700">
Contraseña actualizada
</p>
<button
type="button"
onClick={onCerrar}
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
className="mt-6 w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
>
Cerrar
</button>
@@ -92,7 +92,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
required
className={CLASE_INPUT}
/>
<p className="mt-2 text-xs text-neutral-400">Mínimo 8 caracteres y un número</p>
<p className="mt-2 text-xs text-[#94A3B8]">Mínimo 8 caracteres y un número</p>
</div>
<div>
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
{error && (
<p
role="alert"
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
>
{error}
</p>
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
<button
type="submit"
disabled={cargando}
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
className="flex-1 rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
>
{cargando ? 'Guardando…' : 'Guardar'}
</button>
<button
type="button"
onClick={onCerrar}
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-3 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Cancelar
</button>

View File

@@ -1,7 +1,7 @@
import { useState, type FormEvent } from 'react'
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
/** Pantalla de login StudioBlank: marca FRASA, usuario + contraseña, Enter envía */
/** Pantalla de login NovaSpark: marca FRASA, usuario + contraseña, Enter envía */
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
@@ -22,11 +22,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
}
return (
<div className="flex min-h-screen items-center justify-center bg-white px-4">
<div className="w-full max-w-sm">
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
<div className="mb-10 text-center">
<p className="text-lg font-bold tracking-widest">FRASA</p>
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
<p className="text-lg font-bold tracking-wide">
FRASA<span className="text-[#F97316]">.</span>
</p>
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
Acceso al ecosistema
</p>
</div>
@@ -34,7 +36,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<div>
<label
htmlFor="login-usuario"
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
className="mb-2 block text-xs font-medium text-[#4B5563]"
>
Usuario
</label>
@@ -46,13 +48,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
autoComplete="username"
autoFocus
required
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
/>
</div>
<div>
<label
htmlFor="login-password"
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
className="mb-2 block text-xs font-medium text-[#4B5563]"
>
Contraseña
</label>
@@ -63,13 +65,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
required
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
/>
</div>
{error && (
<p
role="alert"
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
>
{error}
</p>
@@ -77,7 +79,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
<button
type="submit"
disabled={cargando}
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
className="w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
>
{cargando ? 'Entrando…' : 'Entrar'}
</button>

View File

@@ -2,22 +2,17 @@
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--color-accent: #1B4332;
--radius-sm: 0;
--radius-md: 0;
--radius-lg: 0;
--radius-xl: 0;
--radius-2xl: 0;
--radius-3xl: 0;
--color-accent: #2563EB;
--color-accent-hover: #1D4ED8;
}
@layer base {
body {
@apply bg-white text-neutral-900 antialiased;
@apply bg-[#F8FAFC] text-[#111827] antialiased;
}
*:focus-visible {
outline: 2px solid #1B4332;
outline-offset: 2px;
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
}
}

View File

@@ -69,17 +69,18 @@ export default function ChatPage() {
}
return (
<div className="flex min-h-screen flex-col bg-white text-neutral-900">
<header className="border-b border-neutral-200">
<div className="flex min-h-screen flex-col bg-[#F8FAFC] text-[#111827]">
<header className="border-b border-[#E5E7EB] bg-white">
<div className="mx-auto flex w-full max-w-3xl items-center justify-between px-4 py-4">
<span className="text-sm tracking-widest">
<span className="font-bold">FRASA</span>{' '}
<span className="font-light text-neutral-500">ASISTENTE IA</span>
<span className="text-sm tracking-wide">
<span className="font-bold">FRASA</span>
<span className="text-[#F97316]"> · </span>
<span className="font-light text-[#64748B]">ASISTENTE IA</span>
</span>
<button
type="button"
onClick={nuevaConversacion}
className="border border-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Nueva conversación
</button>
@@ -88,19 +89,23 @@ export default function ChatPage() {
<main className="mx-auto flex w-full max-w-3xl flex-1 flex-col px-4 py-8">
{sinConexion && (
<p className="mb-6 text-xs text-amber-700">
<p className="mb-6 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
Sin conexión al API del asistente. Revisa que el servicio esté disponible e inténtalo de
nuevo.
</p>
)}
{error && <p className="mb-6 text-xs text-red-700">{error}</p>}
{error && (
<p className="mb-6 rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700">
{error}
</p>
)}
{mensajes.length === 0 && !escribiendo ? (
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center">
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
Asistente interno
</p>
<p className="max-w-md text-2xl font-light text-neutral-900">
<p className="max-w-md text-2xl font-light text-[#111827]">
Pregunta sobre el catálogo DIAGONAL, las propuestas del cotizador, el proceso de venta
o las políticas de Frasa.
</p>
@@ -112,7 +117,7 @@ export default function ChatPage() {
))}
{escribiendo && (
<div className="flex justify-start">
<p className="bg-neutral-100 px-4 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500">
<p className="rounded-full bg-white px-4 py-2 text-xs font-medium text-[#64748B] shadow-sm">
Escribiendo…
</p>
</div>
@@ -122,7 +127,7 @@ export default function ChatPage() {
)}
</main>
<footer className="border-t border-neutral-200">
<footer className="border-t border-[#E5E7EB] bg-white">
<form
className="mx-auto flex w-full max-w-3xl items-stretch gap-4 px-4 py-4"
onSubmit={(e) => {
@@ -140,12 +145,12 @@ export default function ChatPage() {
onChange={(e) => setEntrada(e.target.value)}
placeholder="Escribe tu pregunta…"
autoComplete="off"
className="flex-1 border border-neutral-300 px-4 py-3 text-sm placeholder:text-neutral-400"
className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm placeholder:text-[#94A3B8] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
/>
<button
type="submit"
disabled={escribiendo || entrada.trim().length === 0}
className="bg-accent px-6 py-3 text-xs font-bold uppercase tracking-widest text-white transition-opacity duration-150 hover:opacity-90 disabled:opacity-40"
className="rounded-lg bg-accent px-6 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-40"
>
Enviar
</button>