Files
4ventures-client-portal/src/components/ui.tsx

137 lines
3.4 KiB
TypeScript

import React from 'react';
/**
* 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({
eyebrow,
title,
description,
actions,
}: {
eyebrow?: string;
title: string;
description?: string;
actions?: React.ReactNode;
}) {
return (
<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">
{eyebrow ? (
<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}
</p>
) : null}
<h1 className="display text-[2rem] leading-[1.05] md:text-[2.5rem] text-ink text-balance">
{title}
</h1>
{description ? (
<p className="mt-2.5 max-w-[62ch] text-[15px] leading-relaxed text-ink-muted">
{description}
</p>
) : null}
</div>
{actions ? (
<div className="flex shrink-0 flex-wrap items-center gap-2">{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-xl border border-border bg-card ${
padded ? 'p-5 md:p-6' : ''
} ${className}`}
>
{children}
</div>
);
}
export function SectionTitle({
title,
description,
}: {
title: string;
description?: string;
}) {
return (
<div className="mb-4">
<h2 className="text-[17px] font-medium leading-tight text-ink">{title}</h2>
{description ? (
<p className="mt-1 text-[13px] text-ink-muted">{description}</p>
) : null}
</div>
);
}
/** Estado vacío: dice qué pasa y qué hacer, sin decorar. */
export function EmptyState({
title,
description,
}: {
title: string;
description?: string;
}) {
return (
<div className="rounded-xl border border-dashed border-mist-300 bg-mist-50 px-6 py-12 text-center">
<p className="text-[15px] font-medium text-ink">{title}</p>
{description ? (
<p className="mx-auto mt-1.5 max-w-md text-[13px] leading-relaxed text-ink-muted">
{description}
</p>
) : null}
</div>
);
}
/** Selector de periodo / pestañas. Activo en tinta sobre blanco; el naranja se reserva para acciones. */
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-[13px]' : 'px-3 py-1.5 text-sm';
return (
<div className="inline-flex gap-0.5 rounded-md border border-border bg-mist-100 p-0.5">
{options.map((o) => (
<button
key={o.id}
type="button"
onClick={() => onChange(o.id)}
aria-pressed={value === o.id}
className={`${pad} rounded-sm font-medium transition-colors ${
value === o.id
? 'bg-ink text-white'
: 'text-ink-muted hover:bg-white hover:text-ink'
}`}
>
{o.label}
</button>
))}
</div>
);
}