design: sistema visual de marca (Midnight/Orange, Canela, shell oscuro)

This commit is contained in:
Marcos Fargas
2026-10-17 18:36:16 +02:00
parent 812052b7eb
commit ae52f2f50a
6 changed files with 289 additions and 170 deletions

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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)",
}, },
}, },
}, },