style: rediseno de layout profesional en las 7 apps

Headers sticky con marca y acciones, contenedores max-w-[1280px],
grids de contenido con rails laterales sticky (cotizador, mi-dia,
detalle de cobranza), stepper vertical en el wizard de alta, toolbar+
tabla unificadas en cobranza, tabs como segmented control en
dashboards, chat con barra de entrada fija y preguntas sugeridas,
landing con navbar + hero + grid de modulos + footer.
This commit is contained in:
2026-09-03 04:28:56 +00:00
parent 599dce2629
commit d59c8c0d7e
39 changed files with 1622 additions and 1235 deletions

View File

@@ -1,33 +1,44 @@
import { MODULOS } from './data/modulos'
import { Navbar } from './components/Navbar'
import { TarjetaModulo } from './components/TarjetaModulo'
import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
export default function App() {
return (
<div className="flex min-h-screen flex-col">
<header className="border-b border-[#E5E7EB] bg-white px-6 pt-20 pb-16 sm:px-12 lg:px-24">
<p className="text-xs font-semibold tracking-wider text-accent uppercase">
FRASA Desarrollos <span aria-hidden="true" className="text-[#F97316]">●</span>
</p>
<h1 className="mt-4 text-4xl font-light text-[#111827] sm:text-5xl">
Ecosistema comercial
</h1>
</header>
<Navbar />
<main className="mx-auto w-full max-w-[1280px] flex-1 px-6">
<section aria-labelledby="titulo-hero" className="py-12 sm:py-16">
<h1
id="titulo-hero"
className="text-3xl font-semibold tracking-tight text-[#111827] sm:text-4xl"
>
Ecosistema comercial
</h1>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[#64748B] sm:text-base">
Las herramientas del equipo comercial de Frasa Desarrollos en un solo lugar:
cotizaciones, alta de clientes, cobranza, tableros y asistente IA.
</p>
</section>
<main className="flex-1 px-6 sm:px-12 lg:px-24">
<nav aria-label="Módulos del ecosistema">
<ul className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{MODULOS.map((modulo) => (
<TarjetaModulo key={modulo.id} modulo={modulo} />
))}
</ul>
</nav>
<ConfiguracionCuenta />
<div className="mt-6">
<ConfiguracionCuenta />
</div>
</main>
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
<p className="text-xs text-[#64748B]">Consult (AS) Technology</p>
<footer className="mt-16 border-t border-[#E5E7EB]">
<div className="mx-auto max-w-[1280px] px-6 py-8">
<p className="text-xs text-[#64748B]">Consult (AS) Technology</p>
</div>
</footer>
</div>
)

View File

@@ -95,7 +95,11 @@ export function ConfiguracionCuenta() {
perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null
return (
<section aria-labelledby="titulo-cuenta" className="mt-24 border-t border-[#E5E7EB] pt-16">
<section
id="configuracion"
aria-labelledby="titulo-cuenta"
className="scroll-mt-20 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm sm:p-8"
>
<h2 id="titulo-cuenta" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
Configuración de cuenta
</h2>
@@ -103,7 +107,7 @@ export function ConfiguracionCuenta() {
{!conSesion ? (
<form
onSubmit={(e) => void entrar(e)}
className="mt-8 max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm"
className="mt-6 max-w-sm"
>
<p className="text-sm leading-relaxed text-[#64748B]">
Inicia sesión para vincular tu usuario de Monday.
@@ -150,7 +154,7 @@ export function ConfiguracionCuenta() {
</button>
</form>
) : (
<div className="mt-8 max-w-md rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
<div className="mt-6 max-w-md">
{cargandoPerfil && <p className="text-sm text-[#64748B]">Cargando tu perfil…</p>}
{perfil && (

View File

@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react'
import { getSesion } from '../services/auth'
/** Barra superior: marca a la izquierda, estado de sesión y acceso a configuración a la derecha */
export function Navbar() {
const [conSesion, setConSesion] = useState(() => getSesion() !== null)
useEffect(() => {
const actualizar = () => setConSesion(getSesion() !== null)
window.addEventListener('frasa:sesion', actualizar)
window.addEventListener('storage', actualizar)
return () => {
window.removeEventListener('frasa:sesion', actualizar)
window.removeEventListener('storage', actualizar)
}
}, [])
return (
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur">
<div className="mx-auto flex h-14 max-w-[1280px] items-center justify-between px-6">
<span className="text-base font-semibold tracking-tight text-[#111827]">
FRASA<span className="text-accent">.</span>
</span>
<div className="flex items-center gap-4">
<span
className={`hidden items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium sm:inline-flex ${
conSesion ? 'bg-green-50 text-green-700' : 'bg-[#F1F5F9] text-[#64748B]'
}`}
>
<span
aria-hidden="true"
className={`h-1.5 w-1.5 rounded-full ${conSesion ? 'bg-green-500' : 'bg-[#94A3B8]'}`}
/>
{conSesion ? 'Sesión activa' : 'Sin sesión'}
</span>
<a
href="#configuracion"
className="text-sm font-medium text-[#64748B] transition-colors hover:text-accent"
>
Configuración
</a>
</div>
</div>
</header>
)
}

View File

@@ -4,17 +4,34 @@ interface Props {
modulo: Modulo
}
// Iniciales del "icono" de cada tarjeta; fallback: primeras 2 letras del nombre
const INICIALES: Record<string, string> = {
cotizador: 'CO',
'alta-clientes': 'AC',
cobranza: 'CB',
'agente-ia': 'IA',
tableros: 'TB',
'mi-dia': 'MD',
}
/** Tarjeta de acceso a un módulo del ecosistema */
export function TarjetaModulo({ modulo }: Props) {
const iniciales = INICIALES[modulo.id] ?? modulo.nombre.slice(0, 2).toUpperCase()
return (
<li>
<a
href={modulo.url}
className="group flex h-full flex-col rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
className="group flex h-full flex-col rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
>
<h2 className="text-2xl font-light text-[#111827]">{modulo.nombre}</h2>
<p className="mt-3 text-sm leading-relaxed text-[#64748B]">{modulo.descripcion}</p>
<span className="mt-auto pt-8 text-sm font-medium text-accent transition-colors duration-100 ease-out group-hover:text-accent-hover">
<span
aria-hidden="true"
className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50 text-sm font-semibold text-accent"
>
{iniciales}
</span>
<h2 className="mt-4 text-base font-semibold text-[#111827]">{modulo.nombre}</h2>
<p className="mt-1 text-sm leading-relaxed text-[#64748B]">{modulo.descripcion}</p>
<span className="mt-auto pt-6 text-sm font-medium text-accent transition-colors duration-100 ease-out group-hover:text-accent-hover">
Entrar <span aria-hidden="true">&rarr;</span>
</span>
</a>

View File

@@ -40,6 +40,12 @@ export function getToken(): string | null {
export function logout(): void {
localStorage.removeItem(STORAGE_KEY)
notificarCambioSesion()
}
/** Avisa a la UI (p. ej. el navbar) que cambió el estado de sesión */
function notificarCambioSesion(): void {
if (typeof window !== 'undefined') window.dispatchEvent(new Event('frasa:sesion'))
}
/** POST /auth/login; guarda token + usuario en localStorage */
@@ -57,6 +63,7 @@ export async function login(username: string, password: string): Promise<Usuario
if (!res.ok) throw new ErrorLogin()
const data = (await res.json()) as Sesion
localStorage.setItem(STORAGE_KEY, JSON.stringify({ token: data.token, usuario: data.usuario }))
notificarCambioSesion()
return data.usuario
}