Características en bento
Cinco mosaicos con miniaturas de interfaz en CSS: barras, interruptores, chat, estado y rejilla de actividad.
Todo lo que necesitas, en un solo lugar
Reportes en tiempo real
Los indicadores que importan, actualizados al momento y sin exportar a hojas de cálculo.
Automatizaciones
Activa o pausa cada flujo.
Comunicación
Mensajes dentro del flujo.
Estado de sistemas
Salud de cada integración a la vista.
Trazabilidad
Quién hizo qué y cuándo, siempre registrado.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Sección de características en bento: cinco mosaicos de tamaños distintos con miniaturas de interfaz dibujadas en CSS (gráfica de barras, interruptores, chat, lista y rejilla). Estilo oscuro de producto. React + TypeScript, sin dependencias. */
export type SeccionBentoProps = {
title?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcsbe-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(28px, 5cqi, 72px) clamp(16px, 4cqi, 56px); background: #08090c; color: #e9ebf2; font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcsbe-root *, .gcsbe-root *::before, .gcsbe-root *::after { box-sizing: border-box; }
.gcsbe-root button { font: inherit; }
.gcsbe-root :focus-visible { outline: 2px solid #8fb4ff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcsbe-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcsbe-title { max-width: 16ch; margin: 0 0 32px; font: 700 clamp(30px, 4.8cqi, 56px)/1.04 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.04em; text-wrap: balance; }
.gcsbe-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: minmax(190px, auto); gap: 12px; }
.gcsbe-tile { position: relative; display: grid; align-content: space-between; gap: 16px; padding: 20px; overflow: hidden; border: 1px solid #1f222b; border-radius: 20px; background: #101218; transition: border-color .25s, background .25s; }
.gcsbe-tile:hover { border-color: #323747; background: #12151c; }
.gcsbe-tile h3 { margin: 0 0 4px; font-size: 17px; font-weight: 600; }
.gcsbe-tile p { margin: 0; color: #8f96a8; font-size: 14px; max-width: 34ch; }
.gcsbe-a { grid-column: span 4; grid-row: span 2; }
.gcsbe-b { grid-column: span 2; }
.gcsbe-c { grid-column: span 2; }
.gcsbe-d { grid-column: span 3; }
.gcsbe-e { grid-column: span 3; }
.gcsbe-bars { display: flex; align-items: flex-end; gap: 10px; height: 180px; padding-top: 10px; }
.gcsbe-bars i { flex: 1; border-radius: 6px 6px 0 0; background: #1b2030; animation: gcsbe-grow .9s cubic-bezier(.16, 1, .3, 1) backwards; transition: background .2s; }
.gcsbe-bars i:hover, .gcsbe-bars i[data-hot="true"] { background: #6f95ff; }
.gcsbe-sw { display: grid; gap: 10px; }
.gcsbe-sw button { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border: 1px solid #1f222b; border-radius: 10px; background: #0b0d12; color: inherit; cursor: pointer; }
.gcsbe-sw i { position: relative; width: 34px; height: 20px; border-radius: 99px; background: #2a2f3d; transition: background .2s; }
.gcsbe-sw i::after { content: ""; position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .25s cubic-bezier(.34, 1.4, .64, 1); }
.gcsbe-sw button[aria-pressed="true"] i { background: #4b7bff; }
.gcsbe-sw button[aria-pressed="true"] i::after { transform: translateX(14px); }
.gcsbe-chat { display: grid; gap: 8px; }
.gcsbe-chat span { max-width: 86%; padding: 9px 12px; border-radius: 14px; font-size: 13px; }
.gcsbe-chat span:nth-child(odd) { background: #1b2030; border-bottom-left-radius: 4px; }
.gcsbe-chat span:nth-child(even) { justify-self: end; background: #3b63e0; border-bottom-right-radius: 4px; }
.gcsbe-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.gcsbe-list li { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; background: #0b0d12; font-size: 13px; }
.gcsbe-list i { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; }
.gcsbe-list li:nth-child(2) i { background: #fbbf24; }
.gcsbe-dots { display: grid; grid-template-columns: repeat(12, 1fr); gap: 6px; }
.gcsbe-dots i { aspect-ratio: 1; border-radius: 4px; background: #1b2030; animation: gcsbe-blink 3s ease-in-out infinite; animation-delay: calc(var(--i) * 80ms); }
@keyframes gcsbe-grow { from { transform: scaleY(0); transform-origin: bottom; } }
@keyframes gcsbe-blink { 40%, 60% { background: #6f95ff; } }
@container (max-width: 760px) { .gcsbe-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .gcsbe-a { grid-column: span 2; grid-row: auto; } .gcsbe-b, .gcsbe-c, .gcsbe-d, .gcsbe-e { grid-column: span 1; } .gcsbe-d, .gcsbe-e { grid-column: span 2; } }`;
export function SeccionBento({ title = "Todo lo que necesitas, en un solo lugar", className = "", style }: SeccionBentoProps) {
const [on, setOn] = useState<Record<string, boolean>>({ a: true, b: false, c: true });
const heights = [38, 56, 44, 72, 52, 88, 66];
return (
<section className={`gcsbe-root ${className}`} style={style}>
<style>{CSS}</style>
<h2 className="gcsbe-title">{title}</h2>
<div className="gcsbe-grid">
<article className="gcsbe-tile gcsbe-a">
<div>
<h3>Reportes en tiempo real</h3>
<p>Los indicadores que importan, actualizados al momento y sin exportar a hojas de cálculo.</p>
</div>
<div className="gcsbe-bars" aria-hidden="true">
{heights.map((h, i) => (
<i key={i} data-hot={i === 5} style={{ height: `${h}%`, animationDelay: `${i * 70}ms` }} />
))}
</div>
</article>
<article className="gcsbe-tile gcsbe-b">
<div>
<h3>Automatizaciones</h3>
<p>Activa o pausa cada flujo.</p>
</div>
<div className="gcsbe-sw">
{[["a", "Avisar al cliente"], ["b", "Crear factura"], ["c", "Asignar tarea"]].map(([key, label]) => (
<button key={key} type="button" aria-pressed={on[key]} onClick={() => setOn((prev) => ({ ...prev, [key]: !prev[key] }))}>
{label}
<i aria-hidden="true" />
</button>
))}
</div>
</article>
<article className="gcsbe-tile gcsbe-c">
<div>
<h3>Comunicación</h3>
<p>Mensajes dentro del flujo.</p>
</div>
<div className="gcsbe-chat" aria-hidden="true">
<span>¿Ya llegó el pedido?</span>
<span>Sí, sale hoy a las 5.</span>
</div>
</article>
<article className="gcsbe-tile gcsbe-d">
<div>
<h3>Estado de sistemas</h3>
<p>Salud de cada integración a la vista.</p>
</div>
<ul className="gcsbe-list" aria-hidden="true">
<li><i />Facturación</li>
<li><i />Inventario</li>
<li><i />Pagos</li>
</ul>
</article>
<article className="gcsbe-tile gcsbe-e">
<div>
<h3>Trazabilidad</h3>
<p>Quién hizo qué y cuándo, siempre registrado.</p>
</div>
<div className="gcsbe-dots" aria-hidden="true">
{Array.from({ length: 36 }, (_, i) => (
<i key={i} style={{ "--i": i } as CSSProperties} />
))}
</div>
</article>
</div>
</section>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/seccion-bentoEjemplo de uso de Características en bento
Después de copiar seccion-bento.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { SeccionBento } from "./seccion-bento";
export default function Ejemplo() {
return <SeccionBento title="Ejemplo" />;
}Propiedades de SeccionBento
SeccionBento acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 121 líneas (7 KB) y define 2 animaciones con @keyframes. Esto es lo que hace Características en bento por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".