Web InnoventixFreeCode

Compact Timeline

Original · free

compact log timeline

byWeb InnoventixReact + Tailwind
tlcompacttimelines
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/tl-compact.json
tl-compact.tsx
"use client";

import { useId, useMemo, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Level = "success" | "info" | "warn" | "error";

interface LogEvent {
  id: string;
  time: string;
  stamp: string;
  level: Level;
  source: string;
  title: string;
  detail: string;
  duration: string;
}

const EVENTS: LogEvent[] = [
  {
    id: "evt-4821",
    time: "09:42:07",
    stamp: "2026-07-16 09:42:07 UTC",
    level: "success",
    source: "api-gateway",
    title: "Deployment v2.4.1 promoted to production",
    detail:
      "Rolling update completed across 24 pods in 3 regions. All readiness probes returned healthy and traffic shifted at 100% with zero failed requests.",
    duration: "1m 12s",
  },
  {
    id: "evt-4820",
    time: "09:41:52",
    stamp: "2026-07-16 09:41:52 UTC",
    level: "info",
    source: "ci-runner",
    title: "Build #4821 finished — 1,204 modules compiled",
    detail:
      "Compiled in 38.2s with a 91% cache hit ratio. Bundle size increased 4.1 KB gzip versus the previous green build on main.",
    duration: "38.2s",
  },
  {
    id: "evt-4819",
    time: "09:40:18",
    stamp: "2026-07-16 09:40:18 UTC",
    level: "warn",
    source: "billing-worker",
    title: "Retry queue depth above threshold (412 / 400)",
    detail:
      "Pending jobs crossed the configured ceiling for 90 seconds. Horizontal autoscaler added 2 workers; queue expected to drain within 4 minutes.",
    duration: "90s",
  },
  {
    id: "evt-4818",
    time: "09:38:44",
    stamp: "2026-07-16 09:38:44 UTC",
    level: "error",
    source: "auth-service",
    title: "Token refresh failed for tenant acme-co",
    detail:
      "Upstream identity provider returned HTTP 503 five times in a row. The circuit breaker opened and requests are shedding to the cached-grant fallback.",
    duration: "5 attempts",
  },
  {
    id: "evt-4817",
    time: "09:37:10",
    stamp: "2026-07-16 09:37:10 UTC",
    level: "success",
    source: "db-migrator",
    title: "Migration 0142_add_audit_index applied",
    detail:
      "Created a concurrent B-tree index on events.created_at without locking writes. Table now holds 41.2M rows; planner picked the new index on first query.",
    duration: "6.4s",
  },
  {
    id: "evt-4816",
    time: "09:35:29",
    stamp: "2026-07-16 09:35:29 UTC",
    level: "info",
    source: "edge-cache",
    title: "Purged CDN cache for /assets/*",
    detail:
      "Invalidated 8,431 objects across 210 edge locations. Estimated global propagation is roughly 30 seconds; origin shielding absorbed the refill traffic.",
    duration: "28s",
  },
  {
    id: "evt-4815",
    time: "09:33:55",
    stamp: "2026-07-16 09:33:55 UTC",
    level: "warn",
    source: "search-indexer",
    title: "Reindex lag detected — 4m 12s behind primary",
    detail:
      "Backfill is running at 3.1k docs/sec. Reads are served from the last consistent snapshot, so recent writes may not appear in results until it catches up.",
    duration: "4m 12s",
  },
  {
    id: "evt-4814",
    time: "09:31:02",
    stamp: "2026-07-16 09:31:02 UTC",
    level: "success",
    source: "webhook-dispatcher",
    title: "Delivered 2,048 events within SLA",
    detail:
      "Every subscriber acknowledged inside the 500ms window. P99 delivery latency was 214ms; no payloads were routed to the dead-letter queue.",
    duration: "3.7s",
  },
  {
    id: "evt-4813",
    time: "09:29:47",
    stamp: "2026-07-16 09:29:47 UTC",
    level: "error",
    source: "image-resizer",
    title: "Job rejected — unsupported color profile",
    detail:
      "File user-9921/banner.tiff uses a CMYK profile the pipeline cannot rasterize. The upload was quarantined and the owner was notified to re-export as sRGB.",
    duration: "—",
  },
  {
    id: "evt-4812",
    time: "09:27:33",
    stamp: "2026-07-16 09:27:33 UTC",
    level: "info",
    source: "scheduler",
    title: "Nightly backup enqueued for 3 volumes",
    detail:
      "Encrypted snapshots are scheduled for 02:00 UTC with a 30-day retention window. Prior run restored cleanly during last week's recovery drill.",
    duration: "queued",
  },
];

const LEVEL_STYLES: Record<
  Level,
  { label: string; dot: string; source: string; chipOn: string }
> = {
  success: {
    label: "Success",
    dot: "bg-emerald-500 ring-emerald-500/25 dark:bg-emerald-400 dark:ring-emerald-400/25",
    source: "text-emerald-700 dark:text-emerald-300",
    chipOn:
      "border-emerald-400 bg-emerald-50 text-emerald-700 dark:border-emerald-400/50 dark:bg-emerald-400/10 dark:text-emerald-300",
  },
  info: {
    label: "Info",
    dot: "bg-sky-500 ring-sky-500/25 dark:bg-sky-400 dark:ring-sky-400/25",
    source: "text-sky-700 dark:text-sky-300",
    chipOn:
      "border-sky-400 bg-sky-50 text-sky-700 dark:border-sky-400/50 dark:bg-sky-400/10 dark:text-sky-300",
  },
  warn: {
    label: "Warning",
    dot: "bg-amber-500 ring-amber-500/25 dark:bg-amber-400 dark:ring-amber-400/25",
    source: "text-amber-700 dark:text-amber-300",
    chipOn:
      "border-amber-400 bg-amber-50 text-amber-700 dark:border-amber-400/50 dark:bg-amber-400/10 dark:text-amber-300",
  },
  error: {
    label: "Error",
    dot: "bg-rose-500 ring-rose-500/25 dark:bg-rose-400 dark:ring-rose-400/25",
    source: "text-rose-700 dark:text-rose-300",
    chipOn:
      "border-rose-400 bg-rose-50 text-rose-700 dark:border-rose-400/50 dark:bg-rose-400/10 dark:text-rose-300",
  },
};

const LEVEL_ORDER: Level[] = ["success", "info", "warn", "error"];

function LevelIcon({ level, className }: { level: Level; className?: string }) {
  const common = {
    className,
    viewBox: "0 0 20 20",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round" as const,
    strokeLinejoin: "round" as const,
    "aria-hidden": true,
  };
  if (level === "success") {
    return (
      <svg {...common}>
        <circle cx="10" cy="10" r="7.25" />
        <path d="m6.75 10 2.25 2.25 4.25-4.5" />
      </svg>
    );
  }
  if (level === "info") {
    return (
      <svg {...common}>
        <circle cx="10" cy="10" r="7.25" />
        <path d="M10 9.25v4" />
        <path d="M10 6.75h.01" />
      </svg>
    );
  }
  if (level === "warn") {
    return (
      <svg {...common}>
        <path d="M10 3.5 2.75 16h14.5L10 3.5Z" />
        <path d="M10 8.5v3.25" />
        <path d="M10 14h.01" />
      </svg>
    );
  }
  return (
    <svg {...common}>
      <circle cx="10" cy="10" r="7.25" />
      <path d="m7.75 7.75 4.5 4.5" />
      <path d="m12.25 7.75-4.5 4.5" />
    </svg>
  );
}

function SearchIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 20 20"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
    >
      <circle cx="9" cy="9" r="5.25" />
      <path d="m13 13 3.5 3.5" />
    </svg>
  );
}

function ChevronIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 20 20"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
    >
      <path d="m6 8 4 4 4-4" />
    </svg>
  );
}

export default function TlCompact() {
  const uid = useId();
  const reduce = useReducedMotion();
  const [active, setActive] = useState<Set<Level>>(
    () => new Set<Level>(LEVEL_ORDER),
  );
  const [query, setQuery] = useState("");
  const [openId, setOpenId] = useState<string | null>(EVENTS[0].id);

  const allOn = active.size === LEVEL_ORDER.length;

  const ring =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return EVENTS.filter((e) => {
      if (!active.has(e.level)) return false;
      if (!q) return true;
      return (
        e.title.toLowerCase().includes(q) ||
        e.source.toLowerCase().includes(q) ||
        e.detail.toLowerCase().includes(q) ||
        LEVEL_STYLES[e.level].label.toLowerCase().includes(q)
      );
    });
  }, [active, query]);

  function toggleLevel(level: Level) {
    setActive((prev) => {
      const next = new Set(prev);
      if (next.has(level)) next.delete(level);
      else next.add(level);
      return next;
    });
  }

  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes tlcompact-ping {
          0% { transform: scale(1); opacity: 0.6; }
          70%, 100% { transform: scale(2.6); opacity: 0; }
        }
        .tlcompact-ping { animation: tlcompact-ping 2.4s cubic-bezier(0,0,0.2,1) infinite; }
        @media (prefers-reduced-motion: reduce) {
          .tlcompact-ping { animation: none; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
          {/* Header */}
          <div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-4 py-4 sm:px-5 dark:border-slate-800">
            <div className="flex items-center gap-3">
              <span className="relative flex h-2.5 w-2.5" aria-hidden>
                <span className="tlcompact-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400" />
                <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
              </span>
              <div>
                <h2 className="text-sm font-semibold tracking-tight text-slate-900 dark:text-white">
                  Activity log
                </h2>
                <p className="text-xs text-slate-500 dark:text-slate-400">
                  Live · orion-prod cluster
                </p>
              </div>
            </div>
            <span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-mono text-xs tabular-nums text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
              {filtered.length} / {EVENTS.length}
            </span>
          </div>

          {/* Toolbar */}
          <div className="space-y-3 border-b border-slate-200 px-4 py-3 sm:px-5 dark:border-slate-800">
            <div className="relative">
              <SearchIcon className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500" />
              <label htmlFor={`${uid}-search`} className="sr-only">
                Filter events by text
              </label>
              <input
                id={`${uid}-search`}
                type="text"
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                placeholder="Filter by service, message, level…"
                className={`w-full rounded-lg border border-slate-200 bg-slate-50 py-2 pl-9 pr-8 text-sm text-slate-800 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500 ${ring}`}
              />
              {query && (
                <button
                  type="button"
                  onClick={() => setQuery("")}
                  aria-label="Clear filter text"
                  className={`absolute right-2 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 ${ring}`}
                >
                  <svg
                    viewBox="0 0 20 20"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={1.75}
                    strokeLinecap="round"
                    className="h-3.5 w-3.5"
                    aria-hidden
                  >
                    <path d="m5.5 5.5 9 9M14.5 5.5l-9 9" />
                  </svg>
                </button>
              )}
            </div>

            <div className="flex flex-wrap items-center gap-2">
              <button
                type="button"
                aria-pressed={allOn}
                onClick={() => setActive(new Set(LEVEL_ORDER))}
                className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors motion-reduce:transition-none ${ring} ${
                  allOn
                    ? "border-indigo-400 bg-indigo-50 text-indigo-700 dark:border-indigo-400/50 dark:bg-indigo-400/10 dark:text-indigo-300"
                    : "border-slate-200 bg-white text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
                }`}
              >
                All
              </button>
              {LEVEL_ORDER.map((level) => {
                const on = active.has(level);
                const s = LEVEL_STYLES[level];
                return (
                  <button
                    key={level}
                    type="button"
                    aria-pressed={on}
                    onClick={() => toggleLevel(level)}
                    className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors motion-reduce:transition-none ${ring} ${
                      on
                        ? s.chipOn
                        : "border-slate-200 bg-white text-slate-500 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
                    }`}
                  >
                    <span
                      className={`h-1.5 w-1.5 rounded-full ${
                        on ? s.dot.split(" ")[0] : "bg-slate-300 dark:bg-slate-600"
                      }`}
                      aria-hidden
                    />
                    {s.label}
                  </button>
                );
              })}
            </div>
          </div>

          {/* Log list */}
          <ul className="relative">
            <AnimatePresence initial={false}>
              {filtered.map((e, i) => {
                const s = LEVEL_STYLES[e.level];
                const open = openId === e.id;
                const panelId = `${uid}-panel-${e.id}`;
                const btnId = `${uid}-btn-${e.id}`;
                const isFirst = i === 0;
                const isLast = i === filtered.length - 1;
                return (
                  <motion.li
                    key={e.id}
                    initial={reduce ? false : { opacity: 0, y: 6 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
                    transition={{ duration: 0.2 }}
                    className="border-t border-slate-100 first:border-t-0 dark:border-slate-800/70"
                  >
                    <button
                      type="button"
                      id={btnId}
                      onClick={() => setOpenId(open ? null : e.id)}
                      aria-expanded={open}
                      aria-controls={panelId}
                      className={`group grid w-full grid-cols-[3.25rem_1.25rem_minmax(0,1fr)_1.25rem] items-start gap-x-3 px-4 py-3 text-left transition-colors hover:bg-slate-50 sm:px-5 dark:hover:bg-slate-800/50 ${ring}`}
                    >
                      <span className="pt-0.5 font-mono text-xs tabular-nums text-slate-400 dark:text-slate-500">
                        {e.time}
                      </span>

                      <span className="relative flex justify-center self-stretch">
                        <span
                          aria-hidden
                          className={`absolute left-1/2 w-px -translate-x-1/2 bg-slate-200 dark:bg-slate-700/70 ${
                            isFirst ? "top-2" : "top-0"
                          } ${isLast ? "bottom-2" : "bottom-0"}`}
                        />
                        <span
                          className={`relative z-10 mt-1.5 h-2.5 w-2.5 rounded-full ring-4 ${s.dot}`}
                          aria-hidden
                        />
                      </span>

                      <span className="min-w-0">
                        <span className="flex items-baseline gap-2">
                          <span className="sr-only">{s.label}: </span>
                          <span
                            className={`shrink-0 font-mono text-[11px] font-medium ${s.source}`}
                          >
                            {e.source}
                          </span>
                          <span className="truncate text-sm text-slate-700 dark:text-slate-200">
                            {e.title}
                          </span>
                        </span>
                      </span>

                      <ChevronIcon
                        className={`mt-1 h-4 w-4 text-slate-400 transition-transform duration-200 motion-reduce:transition-none dark:text-slate-500 ${
                          open ? "rotate-180" : ""
                        }`}
                      />
                    </button>

                    <AnimatePresence initial={false}>
                      {open && (
                        <motion.div
                          key="panel"
                          id={panelId}
                          role="region"
                          aria-labelledby={btnId}
                          initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
                          animate={
                            reduce ? { opacity: 1 } : { height: "auto", opacity: 1 }
                          }
                          exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
                          transition={{ duration: 0.22, ease: [0.4, 0, 0.2, 1] }}
                          className="overflow-hidden"
                        >
                          <div className="px-4 pb-4 pl-14 sm:px-5 sm:pl-[4.5rem]">
                            <div className="rounded-lg border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/40">
                              <div className="mb-2 flex items-center gap-1.5">
                                <LevelIcon
                                  level={e.level}
                                  className={`h-4 w-4 ${s.source}`}
                                />
                                <span
                                  className={`text-[11px] font-semibold uppercase tracking-wide ${s.source}`}
                                >
                                  {s.label}
                                </span>
                              </div>
                              <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                                {e.detail}
                              </p>
                              <dl className="mt-3 flex flex-wrap gap-x-6 gap-y-1.5 border-t border-slate-200 pt-3 text-xs dark:border-slate-700">
                                <div className="flex gap-1.5">
                                  <dt className="text-slate-400 dark:text-slate-500">
                                    Event
                                  </dt>
                                  <dd className="font-mono text-slate-600 dark:text-slate-300">
                                    {e.id}
                                  </dd>
                                </div>
                                <div className="flex gap-1.5">
                                  <dt className="text-slate-400 dark:text-slate-500">
                                    When
                                  </dt>
                                  <dd className="font-mono tabular-nums text-slate-600 dark:text-slate-300">
                                    {e.stamp}
                                  </dd>
                                </div>
                                <div className="flex gap-1.5">
                                  <dt className="text-slate-400 dark:text-slate-500">
                                    Duration
                                  </dt>
                                  <dd className="font-mono text-slate-600 dark:text-slate-300">
                                    {e.duration}
                                  </dd>
                                </div>
                              </dl>
                            </div>
                          </div>
                        </motion.div>
                      )}
                    </AnimatePresence>
                  </motion.li>
                );
              })}
            </AnimatePresence>

            {filtered.length === 0 && (
              <li className="px-5 py-12 text-center">
                <p className="text-sm font-medium text-slate-600 dark:text-slate-300">
                  No events match your filters
                </p>
                <p className="mt-1 text-xs text-slate-400 dark:text-slate-500">
                  Try enabling more levels or clearing the search.
                </p>
              </li>
            )}
          </ul>

          {/* Footer */}
          <div className="flex items-center justify-between border-t border-slate-200 px-4 py-2.5 sm:px-5 dark:border-slate-800">
            <p className="text-xs text-slate-400 dark:text-slate-500">
              Retained 30 days · UTC
            </p>
            <p aria-live="polite" className="text-xs text-slate-500 dark:text-slate-400">
              Showing{" "}
              <span className="font-mono tabular-nums text-slate-700 dark:text-slate-200">
                {filtered.length}
              </span>{" "}
              of {EVENTS.length} events
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →