Ir al contenido
Evaluar mi proyecto
Componentes · Datos y visualizaciónDisponible

Historial de actividad agrupado

Timeline semántico con eventos, tiempos, estados y filtro de pendientes; responde a datos reales mediante props.

Actividad

  1. Proyecto creado09:00

    Se abrió el espacio de trabajo y se definió el objetivo.

  2. Primera revisión10:30

    El equipo compartió comentarios sobre la dirección visual.

  3. Siguiente entregaPendiente

    El prototipo estará listo para la próxima revisión.

3 eventos · historial de muestra

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

datos-timeline.tsx111 líneas
Descargar
"use client";

import { useState, type CSSProperties } from "react";

/** Historial de actividad agrupado. React + TypeScript, sin dependencias. Personaliza con variables --gczl-*. */
export type TimelineEvent = {
  id: string;
  title: string;
  time: string;
  description: string;
  status?: "done" | "pending";
};
export type DatosTimelineProps = {
  events?: TimelineEvent[];
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gczl-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczl-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczl-root *, .gczl-root *::before, .gczl-root *::after { box-sizing: border-box; }
.gczl-root button, .gczl-root input, .gczl-root textarea, .gczl-root select { font: inherit; }
.gczl-root button, .gczl-root a { -webkit-tap-highlight-color: transparent; }
.gczl-root :focus-visible { outline: 2px solid var(--gczl-accent, #25724b); outline-offset: 4px; }
.gczl-root ::selection { background: var(--gczl-accent, #25724b); color: #fff; }
.gczl-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczl-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczl-root *, .gczl-root *::before, .gczl-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczl-root { width:480px; }
.gczl-heading { display:flex; align-items:center; justify-content:space-between; gap:20px; margin-bottom:26px; }
.gczl-heading h3 { font-size:20px; font-weight:600; margin:0; }
.gczl-heading button { padding:8px 12px; min-height:44px; border:1px solid #c2cbbc; border-radius:20px; background:transparent; color:#4e6148; font-size:11px; cursor:pointer; }
.gczl-heading button[aria-pressed="true"] { background:#35583a; color:#fff; border-color:transparent; }
.gczl-root ol { list-style:none; margin:0; padding:0; }
.gczl-root li { display:flex; position:relative; gap:18px; padding:0 0 30px; }
.gczl-root li:not(:last-child)::before { content:""; position:absolute; top:16px; bottom:0; left:5px; width:1px; background:#c3cebe; }
.gczl-dot { width:11px; height:11px; margin-top:5px; flex-shrink:0; border:1px solid #62825a; border-radius:50%; background:#477341; }
.gczl-root li[data-status="pending"] .gczl-dot { background:#eef2e9; }
.gczl-root li>div { flex:1; min-width:0; }
.gczl-line { display:flex; align-items:baseline; justify-content:space-between; gap:20px; }
.gczl-line strong { font-size:14px; font-weight:550; }
.gczl-line>span { font-size:11px; color:#607259; font-variant-numeric:tabular-nums; flex-shrink:0; }
.gczl-root li p { margin:8px 0 0; font-size:12px; color:#607259; max-width:44ch; }
.gczl-note { display:block; color:#607259; font-size:11px; }`;

export function DatosTimeline({
  events = [
    {
      id: "1",
      title: "Proyecto creado",
      time: "09:00",
      description: "Se abrió el espacio de trabajo y se definió el objetivo.",
      status: "done",
    },
    {
      id: "2",
      title: "Primera revisión",
      time: "10:30",
      description: "El equipo compartió comentarios sobre la dirección visual.",
      status: "done",
    },
    {
      id: "3",
      title: "Siguiente entrega",
      time: "Pendiente",
      description: "El prototipo estará listo para la próxima revisión.",
      status: "pending",
    },
  ],
  className = "",
  style,
}: DatosTimelineProps) {
  const [filter, setFilter] = useState<"all" | "pending">("all");
  const visible = events.filter(
    (event) => filter === "all" || event.status === "pending",
  );
  return (
    <section className={`gczl-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gczl-heading">
        <h3>Actividad</h3>
        <button
          type="button"
          aria-pressed={filter === "pending"}
          onClick={() => setFilter(filter === "all" ? "pending" : "all")}
        >
          Solo pendientes
        </button>
      </div>
      <ol>
        {visible.map((event) => (
          <li key={event.id} data-status={event.status}>
            <span className="gczl-dot" aria-hidden="true" />
            <div>
              <div className="gczl-line">
                <strong>{event.title}</strong>
                <span>{event.time}</span>
              </div>
              <p>{event.description}</p>
            </div>
          </li>
        ))}
      </ol>
      {!visible.length && <p>No hay actividades pendientes.</p>}
      <span className="gczl-note" role="status">
        {visible.length} eventos · historial de muestra
      </span>
    </section>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/datos-timeline

Ejemplo de uso de Historial de actividad agrupado

Después de copiar datos-timeline.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { DatosTimeline } from "./datos-timeline";

export default function Ejemplo() {
  return <DatosTimeline />;
}

Propiedades de DatosTimeline

DatosTimeline acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
events?TimelineEvent[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Historial de actividad agrupado con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gczl-fg#202421
--gczl-accent#25724b

Accesibilidad y rendimiento

El archivo tiene 111 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Historial de actividad agrupado 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).
  • Declara roles ARIA para lectores de pantalla: status.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)