BlocksUpdated September 15, 2026

Audit log

A source-copy activity feed with consumer-owned events, filters, actions, timestamps, permissions, and persistence.

Present account activity without owning audit infrastructureLink to section

AuditLogPanel renders a named activity section, ordered events, machine-readable timestamps, optional details, filters and actions. Your application supplies the audit data and decides which controls are available.

Presentation only

This Block does not collect events, write an audit trail, fetch activity, authorize viewers, retain logs or perform mutations. Treat production audit ingestion, integrity, retention and access control as application/backend responsibilities.

Canonical identityLink to section

  • Category: Account & workspace, slug account.
  • Block: audit-log, export AuditLogPanel.
  • Canonical source: apps/marketing/content/blocks/account/audit-log/.
  • Complete entry point: index.tsx.

There is no Blocks npm package, Registry installer or CLI in this flow. The canonical file is React presentation source that you copy into your application.

Copy sourceLink to section

Configure the public UI package and tokens, then create src/components/blocks/audit-log/index.tsx in your application. Open the source below and use its copy button, or select the code manually. This is the complete canonical file, not a shortened example or a second implementation.

View and copy the complete source
src/components/blocks/audit-log/index.tsx
import * as React from "react";

export interface AuditLogEvent {
  /** Stable identity, unique within this panel; used as the React key. */
  id: string;
  actor: string;
  action: string;
  target?: string;
  /** Machine-readable timestamp, ideally ISO 8601. */
  occurredAt: string;
  /** Human-readable timestamp controlled by the consuming application. */
  occurredAtLabel: string;
  details?: React.ReactNode;
  actions?: React.ReactNode;
}

export interface AuditLogPanelProps {
  /** Stable HTML id, unique across the consuming page. */
  id: string;
  heading: string;
  description?: string;
  events: readonly AuditLogEvent[];
  filters?: React.ReactNode;
  actions?: React.ReactNode;
  emptyTitle?: string;
  emptyDescription?: string;
  className?: string;
}

function hasContent(children?: React.ReactNode) {
  return React.Children.toArray(children).some((child) => child !== "");
}

function Slot({
  children,
  slot,
}: {
  children?: React.ReactNode;
  slot: string;
}) {
  if (!hasContent(children)) return null;

  return (
    <div
      data-slot={slot}
      className="flex min-w-0 flex-wrap items-center gap-2 [&>*]:max-w-full [&>*]:whitespace-normal [&>*]:break-words"
    >
      {children}
    </div>
  );
}

export function AuditLogPanel({
  id,
  heading,
  description,
  events,
  filters,
  actions,
  emptyTitle = "No activity to display",
  emptyDescription,
  className,
}: AuditLogPanelProps) {
  const headingId = `${id}-heading`;
  const descriptionId = description ? `${id}-description` : undefined;

  return (
    <section
      id={id}
      aria-labelledby={headingId}
      aria-describedby={descriptionId}
      data-slot="audit-log-panel"
      className={[
        "min-w-0 rounded-xl border border-border bg-card text-card-foreground",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
    >
      <div className="flex min-w-0 flex-col gap-4 border-b border-border p-4 sm:p-6 lg:flex-row lg:items-start lg:justify-between">
        <div className="min-w-0 space-y-1">
          <h2 id={headingId} className="break-words text-base font-semibold">
            {heading}
          </h2>
          {description ? (
            <p
              id={descriptionId}
              className="break-words text-sm text-muted-foreground"
            >
              {description}
            </p>
          ) : null}
        </div>

        <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
          <Slot slot="audit-log-filters">{filters}</Slot>
          <Slot slot="audit-log-actions">{actions}</Slot>
        </div>
      </div>

      {events.length > 0 ? (
        <ol
          role="list"
          aria-labelledby={headingId}
          className="divide-y divide-border"
        >
          {events.map((event) => (
            <li
              key={event.id}
              data-slot="audit-log-event"
              className="min-w-0 p-4 sm:p-6"
            >
              <article className="flex min-w-0 flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
                <div className="min-w-0 flex-1 space-y-2">
                  <p className="min-w-0 break-words text-sm leading-6">
                    <span className="font-medium">{event.actor}</span>{" "}
                    <span>{event.action}</span>
                    {event.target ? (
                      <>
                        {" "}
                        <span className="font-medium">{event.target}</span>
                      </>
                    ) : null}
                  </p>

                  <time
                    dateTime={event.occurredAt}
                    className="block break-words text-xs text-muted-foreground"
                  >
                    {event.occurredAtLabel}
                  </time>

                  {hasContent(event.details) ? (
                    <div
                      data-slot="audit-log-details"
                      className="min-w-0 break-words text-sm text-muted-foreground [&_pre]:max-w-full [&_pre]:overflow-x-auto"
                    >
                      {event.details}
                    </div>
                  ) : null}
                </div>

                <Slot slot="audit-log-event-actions">{event.actions}</Slot>
              </article>
            </li>
          ))}
        </ol>
      ) : (
        <div
          data-slot="audit-log-empty"
          className="min-w-0 space-y-1 p-6 text-center sm:p-8"
        >
          <p className="break-words text-sm font-medium">{emptyTitle}</p>
          {emptyDescription ? (
            <p className="break-words text-sm text-muted-foreground">
              {emptyDescription}
            </p>
          ) : null}
        </div>
      )}
    </section>
  );
}

The documentation includes this file from canonical source at build time. Your application copy does not receive automatic updates. Connect your own audit data and permissions, then validate it in your application. Return to the Blocks catalog to choose another pattern.

Install by copying sourceLink to section

  1. Copy the complete audit-log directory into an application-owned path.
  2. Configure React, @pycolors/ui, tokens and semantic styles through the UI installation guide.
  3. Supply stable event identities, timestamps, labels and consumer-owned actions.
  4. Run lint, type-check, tests and build in the consuming application.
src/components/workspace-audit-log.tsx
"use client";

import { Button } from "@pycolors/ui";
import { AuditLogPanel } from "./blocks/audit-log";

export function WorkspaceAuditLog() {
  return (
    <AuditLogPanel
      id="workspace-audit-log"
      heading="Recent activity"
      description="Security and workspace events supplied by the application."
      events={[
        {
          id: "event-1",
          actor: "Alex Morgan",
          action: "updated",
          target: "workspace settings",
          occurredAt: "2026-09-15T08:30:00Z",
          occurredAtLabel: "15 September 2026, 08:30 UTC",
          details: "Example event metadata owned by the application.",
          actions: (
            <Button type="button" variant="outline">
              View details
            </Button>
          ),
        },
      ]}
    />
  );
}

The button above is intentionally unconnected example UI. Your application owns its handler, destination, authorization and resulting behavior.

Data and permissions remain yoursLink to section

id, heading and events are required. Use a stable, unique HTML id for each panel. Every event needs a stable id, actor, action, machine-readable occurredAt value and consumer-formatted occurredAtLabel.

target, details and per-event actions are optional. The Block renders values in the order supplied; it does not sort, filter, redact, format timestamps or infer event severity. Supply already-authorized, display-ready data.

filters and top-level actions are arbitrary React nodes. They can host native controls or public PyColors UI components, but the Block never reads their state, changes a query or fetches results. Keep filtering and pagination logic in the consumer when an audit history grows beyond a bounded list.

Do not use visible UI state as an authorization boundary. Check workspace roles, audit-log permissions and resource scope on trusted application/backend paths before returning events or performing actions.

Empty, responsive and accessible statesLink to section

With events={[]}, the Block renders emptyTitle and optional emptyDescription. A successful empty result is not a loading or error state; render those states explicitly in the application when required.

The section is labelled by its heading, events use an ordered list, and each machine timestamp is rendered with a semantic <time dateTime> value. Event text, details and actions wrap on narrow layouts, while preformatted detail content may scroll horizontally within its bounded area.

The Block does not move focus or intercept keyboard behavior. Consumer controls keep their native focus and disabled semantics. Give repeated actions distinct accessible names when their visible labels would otherwise be ambiguous.

Ownership and operational boundariesLink to section

Your application owns event production, storage, immutability guarantees, retention, export, search, pagination, permissions, redaction and incident response. The Block does not provide audit compliance by itself.

After copying, review future canonical changes deliberately and preserve local customizations. Validate long actors/actions/targets, empty states, narrow widths, both themes, keyboard focus and real permission combinations before production.

No audit ingestion, database/schema, telemetry pipeline, authentication, authorization, backend/network request, Registry or CLI behavior is introduced by this Block or guide.