Los totales de Métricas usaban solo ad_daily (cohort a 0). Ahora suman deals won; selectores y tipografía ganan contraste sobre fondo blanco. Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
3.1 KiB
TypeScript
128 lines
3.1 KiB
TypeScript
import React from 'react';
|
|
|
|
/** Título de página unificado para todo el Centro de Mando. */
|
|
export function PageHeader({
|
|
eyebrow,
|
|
title,
|
|
description,
|
|
actions,
|
|
}: {
|
|
eyebrow?: string;
|
|
title: string;
|
|
description?: string;
|
|
actions?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="mb-6 md:mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
<div className="min-w-0 space-y-1.5">
|
|
{eyebrow ? (
|
|
<p className="text-[10px] font-semibold uppercase tracking-[0.16em] text-brand-600">
|
|
{eyebrow}
|
|
</p>
|
|
) : null}
|
|
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 text-balance">
|
|
{title}
|
|
</h1>
|
|
{description ? (
|
|
<p className="text-sm text-gray-600 max-w-2xl leading-relaxed">
|
|
{description}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
{actions ? (
|
|
<div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Superficie de contenido estándar. */
|
|
export function Surface({
|
|
children,
|
|
className = '',
|
|
padded = true,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
padded?: boolean;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={`rounded-2xl border border-gray-200 bg-white shadow-sm ${
|
|
padded ? 'p-4 md:p-5' : ''
|
|
} ${className}`}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SectionTitle({
|
|
title,
|
|
description,
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
}) {
|
|
return (
|
|
<div className="mb-3">
|
|
<h2 className="text-base font-semibold text-gray-900">{title}</h2>
|
|
{description ? (
|
|
<p className="text-xs text-gray-500 mt-0.5">{description}</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function EmptyState({
|
|
title,
|
|
description,
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
}) {
|
|
return (
|
|
<div className="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-6 py-10 text-center">
|
|
<p className="text-sm font-medium text-gray-800">{title}</p>
|
|
{description ? (
|
|
<p className="text-xs text-gray-600 mt-1.5 max-w-md mx-auto leading-relaxed">
|
|
{description}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Selector de periodo / pestañas — contraste alto sobre fondo blanco. */
|
|
export function SegmentedControl<T extends string>({
|
|
value,
|
|
options,
|
|
onChange,
|
|
size = 'md',
|
|
}: {
|
|
value: T;
|
|
options: { id: T; label: string }[];
|
|
onChange: (id: T) => void;
|
|
size?: 'sm' | 'md';
|
|
}) {
|
|
const pad = size === 'sm' ? 'px-2.5 py-1 text-xs' : 'px-3 py-1.5 text-sm';
|
|
return (
|
|
<div className="flex gap-1 rounded-lg border border-gray-200 p-0.5 bg-gray-100">
|
|
{options.map((o) => (
|
|
<button
|
|
key={o.id}
|
|
type="button"
|
|
onClick={() => onChange(o.id)}
|
|
className={`${pad} rounded-md font-semibold transition-colors ${
|
|
value === o.id
|
|
? 'bg-brand-500 text-white shadow-sm'
|
|
: 'text-gray-600 hover:text-gray-900 hover:bg-white'
|
|
}`}
|
|
>
|
|
{o.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|