design: sistema visual de marca (Midnight/Orange, Canela, shell oscuro)
This commit is contained in:
@@ -2,14 +2,52 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
Tipografía de marca.
|
||||||
|
Canela (titulares y cifras) y FH Oscar (texto) son fuentes comerciales:
|
||||||
|
coloca los .woff2 en /public/fonts y se activan solos. Mientras tanto se
|
||||||
|
usan Instrument Serif y Geist, cargadas en layout.tsx.
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Canela';
|
||||||
|
src: url('/fonts/Canela-Medium.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FH Oscar';
|
||||||
|
src: url('/fonts/FHOscar-Regular.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FH Oscar';
|
||||||
|
src: url('/fonts/FHOscar-Medium.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--font-sans: 'FH Oscar', 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
--foreground: #111827;
|
--font-display: 'Canela', 'Instrument Serif', Georgia, serif;
|
||||||
|
|
||||||
|
/* Marca */
|
||||||
|
--midnight: #07101e;
|
||||||
|
--orange: #ff5d00;
|
||||||
|
--mint: #f0fcf0;
|
||||||
|
--mist: #b1c3cd;
|
||||||
|
|
||||||
|
/* Roles */
|
||||||
|
--background: #f6f8f7; /* lienzo, ligeramente frío */
|
||||||
--card: #ffffff;
|
--card: #ffffff;
|
||||||
--sidebar: #ffffff;
|
--sidebar: var(--midnight);
|
||||||
--border: #e5e7eb;
|
--border: #dce4e8;
|
||||||
--brand: #e58a3c;
|
--brand: var(--orange);
|
||||||
--muted: #6b7280;
|
--foreground: var(--midnight);
|
||||||
|
--muted: #5b6b78; /* secundario teñido, no gris plano */
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@@ -19,18 +57,47 @@ html {
|
|||||||
body {
|
body {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
font-family: var(--font-sans);
|
||||||
|
font-feature-settings: 'ss01', 'cv11';
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trama de puntos del brandbook, apenas perceptible, sobre el lienzo */
|
||||||
|
.canvas-dots {
|
||||||
|
background-image: radial-gradient(rgba(7, 16, 30, 0.07) 1px, transparent 1px);
|
||||||
|
background-size: 22px 22px;
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cifras y titulares en Canela */
|
||||||
|
.display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #d1d5db transparent;
|
scrollbar-color: var(--mist) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(229, 138, 60, 0.25);
|
background: rgba(255, 93, 0, 0.22);
|
||||||
color: #111827;
|
color: var(--midnight);
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--orange);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
@@ -43,20 +110,20 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type=
|
|||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
color-scheme: light !important;
|
color-scheme: light !important;
|
||||||
color: #111827;
|
color: var(--midnight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Evitar que el autofill del navegador pinte campos en oscuro */
|
/* Evitar que el autofill del navegador pinte campos en oscuro */
|
||||||
input:-webkit-autofill,
|
input:-webkit-autofill,
|
||||||
input:-webkit-autofill:hover,
|
input:-webkit-autofill:hover,
|
||||||
input:-webkit-autofill:focus,
|
input:-webkit-autofill:focus,
|
||||||
textarea:-webkit-autofill,
|
textarea:-webkit-autofill,
|
||||||
textarea:-webkit-autofill:hover,
|
textarea:-webkit-autofill:hover,
|
||||||
textarea:-webkit-autofill:focus,
|
textarea:-webkit-autofill:focus,
|
||||||
select:-webkit-autofill,
|
select:-webkit-autofill,
|
||||||
select:-webkit-autofill:hover,
|
select:-webkit-autofill:hover,
|
||||||
select:-webkit-autofill:focus {
|
select:-webkit-autofill:focus {
|
||||||
-webkit-text-fill-color: #111827 !important;
|
-webkit-text-fill-color: var(--midnight) !important;
|
||||||
-webkit-box-shadow: 0 0 0px 1000px #ffffff inset !important;
|
-webkit-box-shadow: 0 0 0px 1000px #ffffff inset !important;
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ export default function RootLayout({
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,300..900;1,300..900&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Instrument+Serif&family=Geist:wght@400;500;600&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className="bg-background text-gray-900 min-h-screen antialiased">
|
<body className="bg-background text-ink min-h-screen antialiased">
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -140,13 +140,17 @@ function NavLink({
|
|||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onNavigate}
|
onClick={onNavigate}
|
||||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
aria-current={active ? 'page' : undefined}
|
||||||
|
className={`group relative flex items-center gap-3 rounded-md px-3 py-2 text-[14px] font-medium transition-colors ${
|
||||||
active
|
active
|
||||||
? 'bg-brand-50 text-gray-900 border border-brand-200'
|
? 'bg-white/[0.07] text-white'
|
||||||
: 'text-gray-600 border border-transparent hover:text-gray-900 hover:bg-gray-50'
|
: 'text-mist-300 hover:bg-white/[0.05] hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-4 h-4 shrink-0 ${item.iconClass}`} />
|
{active ? (
|
||||||
|
<span aria-hidden className="absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-r bg-brand-500" />
|
||||||
|
) : null}
|
||||||
|
<Icon className={`h-4 w-4 shrink-0 ${active ? 'text-brand-500' : 'text-mist-400 group-hover:text-mist-300'}`} />
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -203,7 +207,7 @@ export default function DashboardShell({
|
|||||||
|
|
||||||
const sidebar = (
|
const sidebar = (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex items-center gap-3 px-2 py-3 border-b border-gray-200 mb-5">
|
<div className="mb-6 flex items-center gap-3 border-b border-white/10 px-2 pb-4 pt-2">
|
||||||
{clientLogoUrl ? (
|
{clientLogoUrl ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
@@ -211,7 +215,7 @@ export default function DashboardShell({
|
|||||||
alt={title}
|
alt={title}
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
className="rounded-lg object-contain shrink-0 w-10 h-10 bg-white border border-gray-100"
|
className="h-10 w-10 shrink-0 rounded-md bg-white object-contain p-1"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<Image
|
||||||
@@ -219,21 +223,21 @@ export default function DashboardShell({
|
|||||||
alt="4Ventures"
|
alt="4Ventures"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
className="rounded-lg object-cover shrink-0"
|
className="h-10 w-10 shrink-0 rounded-md object-cover"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-bold text-sm tracking-wide text-gray-900 leading-tight truncate">
|
<p className="truncate text-[15px] font-medium leading-tight text-white">
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 font-medium leading-tight">
|
<p className="mt-0.5 text-[12px] leading-tight text-mist-400">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto md:hidden rounded-lg p-2 text-gray-500 hover:bg-gray-100"
|
className="ml-auto rounded-md p-2 text-mist-400 hover:bg-white/10 hover:text-white md:hidden"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
aria-label="Cerrar menú"
|
aria-label="Cerrar menú"
|
||||||
>
|
>
|
||||||
@@ -241,15 +245,15 @@ export default function DashboardShell({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 overflow-y-auto space-y-1.5 pr-1">
|
<nav className="flex-1 space-y-0.5 overflow-y-auto pr-1">
|
||||||
{visibleNav.map((item) => {
|
{visibleNav.map((item) => {
|
||||||
// Sub-enlaces de Social: siempre que Social esté en el menú.
|
// Sub-enlaces de Social: siempre que Social esté en el menú.
|
||||||
const underSocial = item.moduleId === 'social';
|
const underSocial = item.moduleId === 'social';
|
||||||
const subLinkCls = (href: string) =>
|
const subLinkCls = (href: string) =>
|
||||||
`ml-4 flex items-center gap-2.5 px-3 py-2 rounded-lg text-xs font-medium transition-all ${
|
`ml-7 flex items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors ${
|
||||||
isActive(pathname, href)
|
isActive(pathname, href)
|
||||||
? 'bg-brand-50 text-gray-900 border border-brand-200'
|
? 'bg-white/[0.07] text-white'
|
||||||
: 'text-gray-500 border border-transparent hover:text-gray-900 hover:bg-gray-50'
|
: 'text-mist-400 hover:bg-white/[0.05] hover:text-white'
|
||||||
}`;
|
}`;
|
||||||
return (
|
return (
|
||||||
<div key={item.href} className="space-y-0.5">
|
<div key={item.href} className="space-y-0.5">
|
||||||
@@ -262,7 +266,7 @@ export default function DashboardShell({
|
|||||||
onClick={close}
|
onClick={close}
|
||||||
className={subLinkCls('/dashboard/calendario')}
|
className={subLinkCls('/dashboard/calendario')}
|
||||||
>
|
>
|
||||||
<CalendarDays className="w-3.5 h-3.5 shrink-0 text-pink-500" />
|
<CalendarDays className="h-3.5 w-3.5 shrink-0" />
|
||||||
Calendario editorial
|
Calendario editorial
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -272,7 +276,7 @@ export default function DashboardShell({
|
|||||||
onClick={close}
|
onClick={close}
|
||||||
className={subLinkCls('/dashboard/creatives')}
|
className={subLinkCls('/dashboard/creatives')}
|
||||||
>
|
>
|
||||||
<Images className="w-3.5 h-3.5 shrink-0 text-fuchsia-500" />
|
<Images className="h-3.5 w-3.5 shrink-0" />
|
||||||
Pipeline RRSS
|
Pipeline RRSS
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -283,9 +287,9 @@ export default function DashboardShell({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{isAgency ? (
|
{isAgency ? (
|
||||||
<div className="pt-4 mt-4 border-t border-gray-200">
|
<div className="mt-5 border-t border-white/10 pt-4">
|
||||||
<span className="px-3 text-[10px] font-bold uppercase tracking-wider text-gray-500">
|
<span className="px-3 text-[12px] font-medium text-mist-400">
|
||||||
Agencia OS
|
Agencia
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1 space-y-0.5">
|
<div className="mt-1 space-y-0.5">
|
||||||
<NavLink
|
<NavLink
|
||||||
@@ -311,18 +315,18 @@ export default function DashboardShell({
|
|||||||
) : null}
|
) : null}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="pt-4 mt-4 border-t border-gray-200 space-y-3">
|
<div className="mt-4 space-y-3 border-t border-white/10 pt-4">
|
||||||
<div className="px-2 py-2 rounded-xl bg-gray-50 border border-gray-200">
|
<div className="rounded-md bg-white/[0.05] px-3 py-2.5">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="mb-1 flex items-center gap-2">
|
||||||
<UserCheck className="w-3.5 h-3.5 text-brand-500 shrink-0" />
|
<UserCheck className="h-3.5 w-3.5 shrink-0 text-mist-400" />
|
||||||
<p className="text-xs font-medium text-gray-900 truncate">{email}</p>
|
<p className="truncate text-[13px] font-medium text-white">{email}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[10px] text-gray-500 capitalize truncate">
|
<span className="truncate text-[12px] capitalize text-mist-400">
|
||||||
{company}
|
{company}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-[9px] uppercase tracking-wider font-semibold px-1.5 py-0.5 rounded bg-brand-50 text-brand-600 shrink-0"
|
className="shrink-0 rounded-sm bg-brand-500/15 px-1.5 py-0.5 text-[11px] font-medium capitalize text-brand-400"
|
||||||
title={role}
|
title={role}
|
||||||
>
|
>
|
||||||
{role === 'admin'
|
{role === 'admin'
|
||||||
@@ -337,16 +341,16 @@ export default function DashboardShell({
|
|||||||
<form action={logoutAction}>
|
<form action={logoutAction}>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-gray-500 hover:text-red-600 hover:bg-red-50 border border-transparent hover:border-red-100 transition-all"
|
className="flex w-full items-center justify-center gap-2 rounded-md px-3 py-2 text-[13px] font-medium text-mist-400 transition-colors hover:bg-white/[0.06] hover:text-white"
|
||||||
>
|
>
|
||||||
<LogOut className="w-3.5 h-3.5" />
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
Cerrar Sesión
|
Cerrar Sesión
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex items-center justify-between px-2 pt-1 text-[11px] text-gray-400">
|
<div className="flex items-center justify-between px-2 pt-1 text-[12px] text-mist-400">
|
||||||
<span className="flex items-center gap-1 text-emerald-600">
|
<span className="flex items-center gap-1.5">
|
||||||
<ShieldCheck className="w-3 h-3" /> RLS Activo
|
<ShieldCheck className="h-3 w-3 text-mint-200" /> Datos aislados por cliente
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,8 +358,8 @@ export default function DashboardShell({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-background text-gray-900">
|
<div className="flex min-h-screen bg-background text-ink">
|
||||||
<aside className="hidden md:flex w-64 shrink-0 flex-col border-r border-gray-200 bg-sidebar p-4">
|
<aside className="hidden w-[248px] shrink-0 flex-col bg-sidebar p-4 text-white md:flex">
|
||||||
{sidebar}
|
{sidebar}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -363,29 +367,29 @@ export default function DashboardShell({
|
|||||||
<div className="fixed inset-0 z-50 md:hidden">
|
<div className="fixed inset-0 z-50 md:hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute inset-0 bg-black/40"
|
className="absolute inset-0 bg-ink/50"
|
||||||
aria-label="Cerrar menú"
|
aria-label="Cerrar menú"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
/>
|
/>
|
||||||
<aside className="absolute left-0 top-0 h-full w-[280px] bg-sidebar border-r border-gray-200 p-4 shadow-xl">
|
<aside className="absolute left-0 top-0 h-full w-[280px] bg-sidebar p-4 text-white shadow-xl">
|
||||||
{sidebar}
|
{sidebar}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<header className="sticky top-0 z-30 border-b border-gray-200 bg-white/95 backdrop-blur-md px-4 md:px-8 py-2.5 space-y-2">
|
<header className="sticky top-0 z-30 space-y-2 border-b border-border bg-background/90 px-4 py-2.5 backdrop-blur-md md:px-8">
|
||||||
<div className="flex items-center gap-3 min-h-10">
|
<div className="flex items-center gap-3 min-h-10">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="md:hidden rounded-lg p-2 text-gray-600 hover:bg-gray-100"
|
className="rounded-md p-2 text-ink-muted hover:bg-mist-100 md:hidden"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
aria-label="Abrir menú"
|
aria-label="Abrir menú"
|
||||||
>
|
>
|
||||||
<Menu className="w-5 h-5" />
|
<Menu className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex min-w-0 items-center gap-2 text-sm text-gray-500">
|
<div className="flex min-w-0 items-center gap-2 text-sm text-ink-muted">
|
||||||
<span className="hidden sm:inline shrink-0">
|
<span className="hidden sm:inline shrink-0">
|
||||||
{isAgency ? 'Contexto:' : 'Cliente / Portal:'}
|
{isAgency ? 'Contexto:' : 'Cliente / Portal:'}
|
||||||
</span>
|
</span>
|
||||||
@@ -410,8 +414,8 @@ export default function DashboardShell({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="canvas-dots flex-1 overflow-y-auto">
|
||||||
<div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8 md:py-8">
|
<div className="mx-auto w-full max-w-7xl px-4 py-6 md:px-8 md:py-9">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ type Card = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function toneForDelta(v: number | null, invert?: boolean): string {
|
function toneForDelta(v: number | null, invert?: boolean): string {
|
||||||
if (v === null) return 'text-gray-500 bg-gray-50 border-gray-200';
|
if (v === null) return 'text-ink-faint bg-mist-50 border-mist-200';
|
||||||
if (v === 0) return 'text-gray-600 bg-gray-50 border-gray-200';
|
if (v === 0) return 'text-ink-muted bg-mist-50 border-mist-200';
|
||||||
const good = invert ? v < 0 : v > 0;
|
const good = invert ? v < 0 : v > 0;
|
||||||
return good
|
return good
|
||||||
? 'text-emerald-800 bg-emerald-50 border-emerald-200'
|
? 'text-emerald-800 bg-mint border-mint-200'
|
||||||
: 'text-red-700 bg-red-50 border-red-200';
|
: 'text-red-700 bg-red-50 border-red-200';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ function DeltaChip({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
title={title}
|
title={title}
|
||||||
className={`inline-flex items-center gap-0.5 rounded-full border px-2 py-0.5 text-[11px] font-semibold tabular-nums ${toneForDelta(
|
className={`inline-flex items-center gap-0.5 rounded-full border px-2 py-0.5 text-[12px] font-medium tabular-nums ${toneForDelta(
|
||||||
v,
|
v,
|
||||||
invert
|
invert
|
||||||
)}`}
|
)}`}
|
||||||
@@ -109,7 +109,7 @@ function DeltaRow({
|
|||||||
}) {
|
}) {
|
||||||
if (empty) {
|
if (empty) {
|
||||||
return (
|
return (
|
||||||
<p className="mt-3 text-[11px] leading-snug text-gray-500">
|
<p className="mt-3 text-[12px] leading-snug text-ink-muted">
|
||||||
Sin cierres en este rango. Amplía el periodo o revisa oportunidades
|
Sin cierres en este rango. Amplía el periodo o revisa oportunidades
|
||||||
ganadas en el CRM.
|
ganadas en el CRM.
|
||||||
</p>
|
</p>
|
||||||
@@ -144,7 +144,7 @@ function DeltaRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gapTone(gap: number): string {
|
function gapTone(gap: number): string {
|
||||||
if (gap === 0) return 'text-gray-700 bg-gray-100';
|
if (gap === 0) return 'text-ink-soft bg-mist-100';
|
||||||
if (gap > 0) return 'text-amber-900 bg-amber-50 border border-amber-200';
|
if (gap > 0) return 'text-amber-900 bg-amber-50 border border-amber-200';
|
||||||
const abs = Math.abs(gap);
|
const abs = Math.abs(gap);
|
||||||
if (abs <= 5) return 'text-emerald-800 bg-emerald-50 border border-emerald-200';
|
if (abs <= 5) return 'text-emerald-800 bg-emerald-50 border border-emerald-200';
|
||||||
@@ -161,7 +161,7 @@ function SparkBars({
|
|||||||
formatValue: (v: number) => string;
|
formatValue: (v: number) => string;
|
||||||
}) {
|
}) {
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
return <p className="text-xs text-gray-500 py-8 text-center">Sin filas diarias.</p>;
|
return <p className="text-xs text-ink-muted py-8 text-center">Sin filas diarias.</p>;
|
||||||
}
|
}
|
||||||
const max = Math.max(1, ...points.map((p) => p.value));
|
const max = Math.max(1, ...points.map((p) => p.value));
|
||||||
const first = points[0]?.date;
|
const first = points[0]?.date;
|
||||||
@@ -176,11 +176,11 @@ function SparkBars({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-end justify-between text-[10px] text-gray-500 mb-1 tabular-nums">
|
<div className="mb-1 flex items-end justify-between text-[11px] tabular-nums text-ink-muted">
|
||||||
<span>{formatValue(max)}</span>
|
<span>{formatValue(max)}</span>
|
||||||
<span>máx.</span>
|
<span>máx.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-px h-28 w-full rounded-lg bg-gray-50 px-1 pt-2 pb-0 border border-gray-100">
|
<div className="flex h-28 w-full items-end gap-px rounded-md border border-mist-200 bg-mist-50 px-1 pb-0 pt-2">
|
||||||
{points.map((d) => (
|
{points.map((d) => (
|
||||||
<div
|
<div
|
||||||
key={d.date}
|
key={d.date}
|
||||||
@@ -194,7 +194,7 @@ function SparkBars({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-[10px] text-gray-500 mt-1.5 tabular-nums">
|
<div className="mt-1.5 flex justify-between text-[11px] tabular-nums text-ink-muted">
|
||||||
<span>{fmtDay(first)}</span>
|
<span>{fmtDay(first)}</span>
|
||||||
<span>{fmtDay(last)}</span>
|
<span>{fmtDay(last)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,15 +212,15 @@ function SectionHead({
|
|||||||
hint: string;
|
hint: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 mb-4">
|
<div className="mb-5 flex items-start gap-3">
|
||||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-brand-50 text-brand-700 border border-brand-200/60">
|
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-ink text-white">
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-semibold text-gray-900 tracking-tight">
|
<h2 className="text-[17px] font-medium leading-tight text-ink">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-gray-500 mt-0.5">{hint}</p>
|
<p className="mt-1 text-[13px] text-ink-muted">{hint}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -250,17 +250,15 @@ function MetricTile({
|
|||||||
showWow: boolean;
|
showWow: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white p-4 md:p-5 shadow-sm h-full">
|
<div className="h-full rounded-xl border border-border bg-card p-5">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">
|
<p className="text-[13px] font-medium text-ink-muted">{label}</p>
|
||||||
{label}
|
<span className="text-mist-400">{icon}</span>
|
||||||
</p>
|
|
||||||
<span className="text-gray-400">{icon}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-3xl font-semibold tracking-tight text-gray-900 tabular-nums leading-none">
|
<p className="display mt-3 text-[2.5rem] leading-none tabular-nums text-ink">
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-2 leading-snug">{hint}</p>
|
<p className="mt-2.5 text-[13px] leading-snug text-ink-muted">{hint}</p>
|
||||||
<DeltaRow
|
<DeltaRow
|
||||||
delta={delta}
|
delta={delta}
|
||||||
invert={invert}
|
invert={invert}
|
||||||
@@ -285,15 +283,15 @@ function StatMini({
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3.5">
|
<div className="rounded-xl border border-border bg-card px-4 py-3.5">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500 inline-flex items-center gap-1.5">
|
<p className="inline-flex items-center gap-1.5 text-[13px] font-medium text-ink-muted">
|
||||||
{icon}
|
{icon}
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-2xl font-semibold text-gray-900 mt-1.5 tabular-nums tracking-tight">
|
<p className="display mt-1.5 text-[1.75rem] leading-none tabular-nums text-ink">
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">{hint}</p>
|
<p className="mt-1.5 text-[13px] text-ink-muted">{hint}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -580,7 +578,7 @@ export default function MetricsDashboard({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!hasCohort && (
|
{!hasCohort && (
|
||||||
<p className="text-xs text-gray-700 border border-gray-200 bg-white rounded-xl px-3 py-2">
|
<p className="text-xs text-ink-soft border border-border bg-white rounded-xl px-3 py-2">
|
||||||
Sin ventas en {rangeHint}
|
Sin ventas en {rangeHint}
|
||||||
{wonDeals.length
|
{wonDeals.length
|
||||||
? ` (hay ${formatNum(wonDeals.length)} en el histórico — prueba 30 días o Mes).`
|
? ` (hay ${formatNum(wonDeals.length)} en el histórico — prueba 30 días o Mes).`
|
||||||
@@ -597,8 +595,8 @@ export default function MetricsDashboard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-gradient-to-br from-white via-white to-brand-50/70 p-4 md:p-6 shadow-sm">
|
<div className="rounded-2xl border border-border bg-gradient-to-br from-white via-white to-brand-50/70 p-4 md:p-6 ">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-brand-700 mb-4">
|
<p className="text-[13px] font-medium text-brand-600 mb-4">
|
||||||
Resumen del periodo
|
Resumen del periodo
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 md:gap-6">
|
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 md:gap-6">
|
||||||
@@ -629,7 +627,7 @@ export default function MetricsDashboard({
|
|||||||
label={`Leads ${crmName}`}
|
label={`Leads ${crmName}`}
|
||||||
value={hasCrm ? formatNum(crmPeriodTotal) : '—'}
|
value={hasCrm ? formatNum(crmPeriodTotal) : '—'}
|
||||||
chip={
|
chip={
|
||||||
<span className="text-[11px] text-gray-500">
|
<span className="text-[11px] text-ink-muted">
|
||||||
{formatNum(periodTotals.leads)} plat.
|
{formatNum(periodTotals.leads)} plat.
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -641,7 +639,7 @@ export default function MetricsDashboard({
|
|||||||
hasCohort ? (
|
hasCohort ? (
|
||||||
<DeltaChip label="mes" v={heroDeltas.deals.mom} />
|
<DeltaChip label="mes" v={heroDeltas.deals.mom} />
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[11px] text-gray-500">sin cierres</span>
|
<span className="text-[11px] text-ink-muted">sin cierres</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -650,7 +648,7 @@ export default function MetricsDashboard({
|
|||||||
value={heroRoasEmpty ? '—' : `${formatNum(periodTotals.roas)}x`}
|
value={heroRoasEmpty ? '—' : `${formatNum(periodTotals.roas)}x`}
|
||||||
chip={
|
chip={
|
||||||
heroRoasEmpty ? (
|
heroRoasEmpty ? (
|
||||||
<span className="text-[11px] text-gray-500">pendiente CRM</span>
|
<span className="text-[11px] text-ink-muted">pendiente CRM</span>
|
||||||
) : (
|
) : (
|
||||||
<DeltaChip label="mes" v={heroDeltas.roas.mom} />
|
<DeltaChip label="mes" v={heroDeltas.roas.mom} />
|
||||||
)
|
)
|
||||||
@@ -659,26 +657,26 @@ export default function MetricsDashboard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details className="group rounded-xl border border-gray-200 bg-white">
|
<details className="group rounded-xl border border-border bg-white">
|
||||||
<summary className="cursor-pointer list-none flex items-center gap-2 px-4 py-2.5 text-xs font-medium text-gray-600 hover:text-gray-900">
|
<summary className="cursor-pointer list-none flex items-center gap-2 px-4 py-2.5 text-xs font-medium text-ink-muted hover:text-ink">
|
||||||
<Info className="w-3.5 h-3.5 text-brand-600" />
|
<Info className="w-3.5 h-3.5 text-brand-600" />
|
||||||
Cómo leer ads, CRM y web
|
Cómo leer ads, CRM y web
|
||||||
<span className="ml-auto text-gray-400">
|
<span className="ml-auto text-ink-faint">
|
||||||
<span className="group-open:hidden">mostrar</span>
|
<span className="group-open:hidden">mostrar</span>
|
||||||
<span className="hidden group-open:inline">ocultar</span>
|
<span className="hidden group-open:inline">ocultar</span>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="px-4 pb-3 text-xs text-gray-600 space-y-1.5 border-t border-gray-100 pt-2">
|
<div className="px-4 pb-3 text-xs text-ink-muted space-y-1.5 border-t border-mist-100 pt-2">
|
||||||
<p>
|
<p>
|
||||||
<strong className="text-gray-800">Publicidad:</strong> lo que Meta y
|
<strong className="text-ink-soft">Publicidad:</strong> lo que Meta y
|
||||||
Google reportan. No es el listado de clientes.
|
Google reportan. No es el listado de clientes.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong className="text-gray-800">{crmName}:</strong> altas y ventas
|
<strong className="text-ink-soft">{crmName}:</strong> altas y ventas
|
||||||
reales. Un lead de ads y un alta CRM casi nunca coinciden 1 a 1.
|
reales. Un lead de ads y un alta CRM casi nunca coinciden 1 a 1.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong className="text-gray-800">Web y SEO:</strong> Search Console
|
<strong className="text-ink-soft">Web y SEO:</strong> Search Console
|
||||||
y Analytics. No se suman a los leads de anuncios.
|
y Analytics. No se suman a los leads de anuncios.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -708,7 +706,7 @@ export default function MetricsDashboard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||||
<Surface>
|
<Surface>
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500 mb-3">
|
<p className="text-[13px] font-medium text-ink-muted mb-3">
|
||||||
Inversión por día
|
Inversión por día
|
||||||
</p>
|
</p>
|
||||||
<SparkBars
|
<SparkBars
|
||||||
@@ -718,7 +716,7 @@ export default function MetricsDashboard({
|
|||||||
/>
|
/>
|
||||||
</Surface>
|
</Surface>
|
||||||
<Surface>
|
<Surface>
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500 mb-3">
|
<p className="text-[13px] font-medium text-ink-muted mb-3">
|
||||||
Leads de plataforma por día
|
Leads de plataforma por día
|
||||||
</p>
|
</p>
|
||||||
<SparkBars
|
<SparkBars
|
||||||
@@ -747,20 +745,20 @@ export default function MetricsDashboard({
|
|||||||
value={cplCrm ? formatEurCents(cplCrm) : '—'}
|
value={cplCrm ? formatEurCents(cplCrm) : '—'}
|
||||||
hint="Inversión ads ÷ leads CRM"
|
hint="Inversión ads ÷ leads CRM"
|
||||||
/>
|
/>
|
||||||
<div className="rounded-xl border border-gray-200 bg-white px-4 py-3.5 col-span-2 lg:col-span-1">
|
<div className="rounded-xl border border-border bg-white px-4 py-3.5 col-span-2 lg:col-span-1">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">
|
<p className="text-[13px] font-medium text-ink-muted">
|
||||||
Gap ads vs CRM
|
Gap ads vs CRM
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={`mt-1.5 inline-flex items-center rounded-full px-2.5 py-0.5 text-xl font-semibold tabular-nums ${
|
className={`mt-1.5 inline-flex items-center rounded-full px-2.5 py-0.5 text-xl font-semibold tabular-nums ${
|
||||||
hasCrm ? gapTone(gapAdsCrm) : 'text-gray-400'
|
hasCrm ? gapTone(gapAdsCrm) : 'text-ink-faint'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{hasCrm
|
{hasCrm
|
||||||
? `${gapAdsCrm > 0 ? '+' : ''}${formatNum(gapAdsCrm)}`
|
? `${gapAdsCrm > 0 ? '+' : ''}${formatNum(gapAdsCrm)}`
|
||||||
: '—'}
|
: '—'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1.5">
|
<p className="text-xs text-ink-muted mt-1.5">
|
||||||
CRM − conversiones ads. Positivo = más altas en CRM que las que
|
CRM − conversiones ads. Positivo = más altas en CRM que las que
|
||||||
reporta el anuncio.
|
reporta el anuncio.
|
||||||
</p>
|
</p>
|
||||||
@@ -828,44 +826,44 @@ export default function MetricsDashboard({
|
|||||||
/>
|
/>
|
||||||
{channelInsight && (
|
{channelInsight && (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-4">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-4">
|
||||||
<div className="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2.5">
|
<div className="rounded-xl bg-mist-50 border border-border px-3 py-2.5">
|
||||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
|
<p className="text-[12px] font-medium text-ink-muted">
|
||||||
Plat.
|
Plat.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-semibold tabular-nums text-gray-900">
|
<p className="text-lg font-semibold tabular-nums text-ink">
|
||||||
{formatNum(channelInsight.platTotal)}
|
{formatNum(channelInsight.platTotal)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2.5">
|
<div className="rounded-xl bg-mist-50 border border-border px-3 py-2.5">
|
||||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
|
<p className="text-[12px] font-medium text-ink-muted">
|
||||||
CRM
|
CRM
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-semibold tabular-nums text-gray-900">
|
<p className="text-lg font-semibold tabular-nums text-ink">
|
||||||
{formatNum(channelInsight.crmTotal)}
|
{formatNum(channelInsight.crmTotal)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2.5">
|
<div className="rounded-xl bg-mist-50 border border-border px-3 py-2.5">
|
||||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
|
<p className="text-[12px] font-medium text-ink-muted">
|
||||||
Con gasto ads
|
Con gasto ads
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-semibold tabular-nums text-gray-900">
|
<p className="text-lg font-semibold tabular-nums text-ink">
|
||||||
{formatNum(channelInsight.crmPaid)}
|
{formatNum(channelInsight.crmPaid)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2.5">
|
<div className="rounded-xl bg-mist-50 border border-border px-3 py-2.5">
|
||||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
|
<p className="text-[12px] font-medium text-ink-muted">
|
||||||
Otras fuentes
|
Otras fuentes
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-semibold tabular-nums text-gray-900">
|
<p className="text-lg font-semibold tabular-nums text-ink">
|
||||||
{formatNum(channelInsight.crmOther)}
|
{formatNum(channelInsight.crmOther)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{channelInsight && (
|
{channelInsight && (
|
||||||
<p className="text-xs text-gray-500 mb-3">
|
<p className="text-xs text-ink-muted mb-3">
|
||||||
Mayor desfase en{' '}
|
Mayor desfase en{' '}
|
||||||
<span className="capitalize font-semibold text-gray-800">
|
<span className="capitalize font-semibold text-ink-soft">
|
||||||
{channelInsight.worstGap.channel}
|
{channelInsight.worstGap.channel}
|
||||||
</span>
|
</span>
|
||||||
. Ecommerce, messenger e integrations no son fallos de ads.
|
. Ecommerce, messenger e integrations no son fallos de ads.
|
||||||
@@ -873,14 +871,14 @@ export default function MetricsDashboard({
|
|||||||
)}
|
)}
|
||||||
<Surface padded={false}>
|
<Surface padded={false}>
|
||||||
{attributionRows.length === 0 ? (
|
{attributionRows.length === 0 ? (
|
||||||
<p className="text-sm text-gray-600 py-10 text-center">
|
<p className="text-sm text-ink-muted py-10 text-center">
|
||||||
Sin series de canal en el periodo.
|
Sin series de canal en el periodo.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-200 bg-gray-50/80 text-[11px] uppercase tracking-wider text-gray-500">
|
<tr className="border-b border-border bg-mist-50 text-[12px] font-medium text-ink-muted">
|
||||||
<th className="py-3 px-4 font-semibold">Canal</th>
|
<th className="py-3 px-4 font-semibold">Canal</th>
|
||||||
<th className="py-3 pr-4 font-semibold min-w-[160px]">
|
<th className="py-3 pr-4 font-semibold min-w-[160px]">
|
||||||
Inversión
|
Inversión
|
||||||
@@ -900,8 +898,8 @@ export default function MetricsDashboard({
|
|||||||
{attributionRows.map((row) => (
|
{attributionRows.map((row) => (
|
||||||
<tr
|
<tr
|
||||||
key={row.channel}
|
key={row.channel}
|
||||||
className={`border-b border-gray-100 hover:bg-brand-50/40 ${
|
className={`border-b border-mist-100 hover:bg-brand-50/40 ${
|
||||||
row.spend > 0 ? 'bg-white' : 'bg-gray-50/40'
|
row.spend > 0 ? 'bg-white' : 'bg-mist-50/40'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
@@ -913,7 +911,7 @@ export default function MetricsDashboard({
|
|||||||
{row.channel}
|
{row.channel}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-600 font-medium capitalize">
|
<span className="text-ink-muted font-medium capitalize">
|
||||||
{row.channel}
|
{row.channel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -921,10 +919,10 @@ export default function MetricsDashboard({
|
|||||||
<td className="py-3 pr-4">
|
<td className="py-3 pr-4">
|
||||||
{row.spend ? (
|
{row.spend ? (
|
||||||
<div className="flex items-center gap-2 min-w-[140px]">
|
<div className="flex items-center gap-2 min-w-[140px]">
|
||||||
<span className="tabular-nums text-gray-900 w-[4.5rem]">
|
<span className="tabular-nums text-ink w-[4.5rem]">
|
||||||
{formatEur(row.spend)}
|
{formatEur(row.spend)}
|
||||||
</span>
|
</span>
|
||||||
<div className="h-1.5 flex-1 rounded-full bg-gray-100">
|
<div className="h-1.5 flex-1 rounded-full bg-mist-100">
|
||||||
<div
|
<div
|
||||||
className="h-1.5 rounded-full bg-brand-500"
|
className="h-1.5 rounded-full bg-brand-500"
|
||||||
style={{
|
style={{
|
||||||
@@ -937,13 +935,13 @@ export default function MetricsDashboard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">—</span>
|
<span className="text-ink-faint">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{formatNum(row.platformLeads)}
|
{formatNum(row.platformLeads)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{hasCrm ? formatNum(row.crmLeads) : '—'}
|
{hasCrm ? formatNum(row.crmLeads) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4">
|
<td className="py-3 pr-4">
|
||||||
@@ -960,24 +958,24 @@ export default function MetricsDashboard({
|
|||||||
'—'
|
'—'
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums font-semibold text-gray-900">
|
<td className="py-3 pr-4 tabular-nums font-semibold text-ink">
|
||||||
{row.ventas > 0 ? formatNum(row.ventas) : '—'}
|
{row.ventas > 0 ? formatNum(row.ventas) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{row.cac > 0 ? formatEurCents(row.cac) : '—'}
|
{row.cac > 0 ? formatEurCents(row.cac) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{row.importe > 0 ? formatEur(row.importe) : '—'}
|
{row.importe > 0 ? formatEur(row.importe) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{row.roas > 0 ? `${formatNum(row.roas)}x` : '—'}
|
{row.roas > 0 ? `${formatNum(row.roas)}x` : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{row.cplPlatform
|
{row.cplPlatform
|
||||||
? formatEurCents(row.cplPlatform)
|
? formatEurCents(row.cplPlatform)
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-800">
|
<td className="py-3 pr-4 tabular-nums text-ink-soft">
|
||||||
{hasCrm && row.cplCrm
|
{hasCrm && row.cplCrm
|
||||||
? formatEurCents(row.cplCrm)
|
? formatEurCents(row.cplCrm)
|
||||||
: '—'}
|
: '—'}
|
||||||
@@ -987,9 +985,9 @@ export default function MetricsDashboard({
|
|||||||
</tbody>
|
</tbody>
|
||||||
{attributionRows.length > 0 && (
|
{attributionRows.length > 0 && (
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr className="bg-gray-50 font-semibold border-t border-gray-200">
|
<tr className="bg-mist-50 font-semibold border-t border-border">
|
||||||
<td className="py-3 px-4 text-gray-900">Total</td>
|
<td className="py-3 px-4 text-ink">Total</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-900">
|
<td className="py-3 pr-4 tabular-nums text-ink">
|
||||||
{attributionTotals.spend
|
{attributionTotals.spend
|
||||||
? formatEur(attributionTotals.spend)
|
? formatEur(attributionTotals.spend)
|
||||||
: '—'}
|
: '—'}
|
||||||
@@ -1014,10 +1012,10 @@ export default function MetricsDashboard({
|
|||||||
'—'
|
'—'
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-900">
|
<td className="py-3 pr-4 tabular-nums text-ink">
|
||||||
{formatNum(attributionTotals.ventas || ventasCount)}
|
{formatNum(attributionTotals.ventas || ventasCount)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-900">
|
<td className="py-3 pr-4 tabular-nums text-ink">
|
||||||
{(attributionTotals.ventas || ventasCount) > 0 &&
|
{(attributionTotals.ventas || ventasCount) > 0 &&
|
||||||
attributionTotals.spend > 0
|
attributionTotals.spend > 0
|
||||||
? formatEurCents(
|
? formatEurCents(
|
||||||
@@ -1028,7 +1026,7 @@ export default function MetricsDashboard({
|
|||||||
? formatEurCents(periodTotals.cac)
|
? formatEurCents(periodTotals.cac)
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 tabular-nums text-gray-900">
|
<td className="py-3 pr-4 tabular-nums text-ink">
|
||||||
{attributionTotals.importe > 0 ||
|
{attributionTotals.importe > 0 ||
|
||||||
periodTotals.revenue > 0
|
periodTotals.revenue > 0
|
||||||
? formatEur(
|
? formatEur(
|
||||||
@@ -1084,10 +1082,10 @@ function HeroStat({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">
|
<p className="text-[13px] font-medium text-ink-muted">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1.5 text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 tabular-nums leading-none">
|
<p className="mt-1.5 text-2xl md:text-3xl font-semibold tracking-tight text-ink tabular-nums leading-none">
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2">{chip}</div>
|
<div className="mt-2">{chip}</div>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
/** Título de página unificado para todo el Centro de Mando. */
|
/**
|
||||||
|
* Primitivas del Centro de Mando.
|
||||||
|
* Reglas: un acento (naranja), etiquetas en frase normal, radio 12px,
|
||||||
|
* jerarquía por tono de fondo y no por sombras.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Título de página unificado. El eyebrow se dibuja con la regla naranja del brandbook. */
|
||||||
export function PageHeader({
|
export function PageHeader({
|
||||||
eyebrow,
|
eyebrow,
|
||||||
title,
|
title,
|
||||||
@@ -13,24 +19,25 @@ export function PageHeader({
|
|||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 md:mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
<div className="mb-7 md:mb-9 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div className="min-w-0 space-y-1.5">
|
<div className="min-w-0">
|
||||||
{eyebrow ? (
|
{eyebrow ? (
|
||||||
<p className="text-[10px] font-semibold uppercase tracking-[0.16em] text-brand-600">
|
<p className="mb-2 flex items-center gap-2 text-[13px] font-medium text-ink-muted">
|
||||||
|
<span aria-hidden className="h-px w-5 bg-brand-500" />
|
||||||
{eyebrow}
|
{eyebrow}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 text-balance">
|
<h1 className="display text-[2rem] leading-[1.05] md:text-[2.5rem] text-ink text-balance">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
{description ? (
|
{description ? (
|
||||||
<p className="text-sm text-gray-600 max-w-2xl leading-relaxed">
|
<p className="mt-2.5 max-w-[62ch] text-[15px] leading-relaxed text-ink-muted">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{actions ? (
|
{actions ? (
|
||||||
<div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>
|
<div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -48,8 +55,8 @@ export function Surface({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded-2xl border border-gray-200 bg-white shadow-sm ${
|
className={`rounded-xl border border-border bg-card ${
|
||||||
padded ? 'p-4 md:p-5' : ''
|
padded ? 'p-5 md:p-6' : ''
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -65,15 +72,16 @@ export function SectionTitle({
|
|||||||
description?: string;
|
description?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-4">
|
||||||
<h2 className="text-base font-semibold text-gray-900">{title}</h2>
|
<h2 className="text-[17px] font-medium leading-tight text-ink">{title}</h2>
|
||||||
{description ? (
|
{description ? (
|
||||||
<p className="text-xs text-gray-500 mt-0.5">{description}</p>
|
<p className="mt-1 text-[13px] text-ink-muted">{description}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Estado vacío: dice qué pasa y qué hacer, sin decorar. */
|
||||||
export function EmptyState({
|
export function EmptyState({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -82,10 +90,10 @@ export function EmptyState({
|
|||||||
description?: string;
|
description?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-6 py-10 text-center">
|
<div className="rounded-xl border border-dashed border-mist-300 bg-mist-50 px-6 py-12 text-center">
|
||||||
<p className="text-sm font-medium text-gray-800">{title}</p>
|
<p className="text-[15px] font-medium text-ink">{title}</p>
|
||||||
{description ? (
|
{description ? (
|
||||||
<p className="text-xs text-gray-600 mt-1.5 max-w-md mx-auto leading-relaxed">
|
<p className="mx-auto mt-1.5 max-w-md text-[13px] leading-relaxed text-ink-muted">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -93,7 +101,7 @@ export function EmptyState({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selector de periodo / pestañas — contraste alto sobre fondo blanco. */
|
/** Selector de periodo / pestañas. Activo en tinta sobre blanco; el naranja se reserva para acciones. */
|
||||||
export function SegmentedControl<T extends string>({
|
export function SegmentedControl<T extends string>({
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
@@ -105,18 +113,19 @@ export function SegmentedControl<T extends string>({
|
|||||||
onChange: (id: T) => void;
|
onChange: (id: T) => void;
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
}) {
|
}) {
|
||||||
const pad = size === 'sm' ? 'px-2.5 py-1 text-xs' : 'px-3 py-1.5 text-sm';
|
const pad = size === 'sm' ? 'px-2.5 py-1 text-[13px]' : 'px-3 py-1.5 text-sm';
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1 rounded-lg border border-gray-200 p-0.5 bg-gray-100">
|
<div className="inline-flex gap-0.5 rounded-md border border-border bg-mist-100 p-0.5">
|
||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
<button
|
<button
|
||||||
key={o.id}
|
key={o.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(o.id)}
|
onClick={() => onChange(o.id)}
|
||||||
className={`${pad} rounded-md font-semibold transition-colors ${
|
aria-pressed={value === o.id}
|
||||||
|
className={`${pad} rounded-sm font-medium transition-colors ${
|
||||||
value === o.id
|
value === o.id
|
||||||
? 'bg-brand-500 text-white shadow-sm'
|
? 'bg-ink text-white'
|
||||||
: 'text-gray-600 hover:text-gray-900 hover:bg-white'
|
: 'text-ink-muted hover:bg-white hover:text-ink'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{o.label}
|
{o.label}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokens de marca 4Ventures (brandbook):
|
||||||
|
* Midnight #07101E · Burnt Orange #FF5D00 · Mint #F0FCF0 · Mist #B1C3CD
|
||||||
|
* Un solo acento (naranja). La jerarquía se construye con tono, no con sombras.
|
||||||
|
*/
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
@@ -10,21 +15,57 @@ const config: Config = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['var(--font-sans)', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
sans: ["var(--font-sans)", "ui-sans-serif", "system-ui", "sans-serif"],
|
||||||
|
display: ["var(--font-display)", "Georgia", "serif"],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "#FFFFFF",
|
background: "var(--background)",
|
||||||
card: "#FFFFFF",
|
card: "var(--card)",
|
||||||
sidebar: "#FFFFFF",
|
sidebar: "var(--sidebar)",
|
||||||
border: "#E5E7EB",
|
border: "var(--border)",
|
||||||
|
ink: {
|
||||||
|
DEFAULT: "#07101E",
|
||||||
|
soft: "#2B3744",
|
||||||
|
muted: "#5B6B78",
|
||||||
|
faint: "#8A99A6",
|
||||||
|
},
|
||||||
|
mist: {
|
||||||
|
50: "#F6F8F7",
|
||||||
|
100: "#EEF2F1",
|
||||||
|
200: "#DCE4E8",
|
||||||
|
300: "#B1C3CD",
|
||||||
|
400: "#8FA4B1",
|
||||||
|
},
|
||||||
|
mint: {
|
||||||
|
DEFAULT: "#F0FCF0",
|
||||||
|
200: "#D9F3DA",
|
||||||
|
},
|
||||||
brand: {
|
brand: {
|
||||||
50: "#FFF8F0",
|
50: "#FFF1E8",
|
||||||
200: "#F5C796",
|
100: "#FFE0CC",
|
||||||
400: "#EDA05A",
|
200: "#FFC4A0",
|
||||||
500: "#E58A3C",
|
400: "#FF7A2E",
|
||||||
600: "#D07528",
|
500: "#FF5D00",
|
||||||
700: "#B35E1B"
|
600: "#E04F00",
|
||||||
}
|
700: "#B84100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
DEFAULT: "12px",
|
||||||
|
sm: "8px",
|
||||||
|
md: "10px",
|
||||||
|
lg: "12px",
|
||||||
|
xl: "12px",
|
||||||
|
"2xl": "16px",
|
||||||
|
"3xl": "20px",
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
sm: "0 1px 0 0 rgba(7, 16, 30, 0.04)",
|
||||||
|
DEFAULT: "0 1px 2px 0 rgba(7, 16, 30, 0.06)",
|
||||||
|
md: "0 2px 8px -2px rgba(7, 16, 30, 0.10)",
|
||||||
|
lg: "0 12px 32px -12px rgba(7, 16, 30, 0.18)",
|
||||||
|
xl: "0 24px 48px -16px rgba(7, 16, 30, 0.24)",
|
||||||
|
brand: "0 8px 24px -8px rgba(255, 93, 0, 0.45)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user