Metric Cluster

Compact stack of related metrics pinned to a canvas object's corner.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/metric-cluster.json
bash

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

"use client";

import {
  type ComponentPropsWithoutRef,
  forwardRef,
  type ReactNode,
} from "react";

import { cn } from "../../lib/utils";

/**
 * Tone of a single metric line — drives the dot color.
 *
 * @public
 */
export type MetricClusterTone = "danger" | "neutral" | "success" | "warn";

const TONE_DOT: Record<MetricClusterTone, string> = {
  danger: "bg-red-500",
  neutral: "bg-muted-foreground",
  success: "bg-emerald-500",
  warn: "bg-amber-500",
};

/**
 * Anchor corner relative to the canvas object the cluster attaches to.
 *
 * @public
 */
export type MetricClusterAnchor =
  | "bottom-left"
  | "bottom-right"
  | "top-left"
  | "top-right";

const ANCHOR_TRANSFORM: Record<MetricClusterAnchor, string> = {
  "bottom-left": "translate(-100%, 100%)",
  "bottom-right": "translate(0%, 100%)",
  "top-left": "translate(-100%, -100%)",
  "top-right": "translate(0%, -100%)",
};

/**
 * One metric in the cluster.
 *
 * @public
 */
export type MetricClusterEntry = {
  /** Stable identifier — used as the React key + analytics hook. */
  id: string;
  /** Short label (e.g. `"errs/min"`). */
  label: ReactNode;
  /** Optional tone for the leading dot. Defaults to `"neutral"`. */
  tone?: MetricClusterTone;
  /** Display value (already formatted by host). */
  value: ReactNode;
};

/**
 * Localizable strings.
 *
 * @public
 */
export type MetricClusterLabels = {
  /** Aria-label override. Defaults to `"Metric cluster"`. */
  region?: string;
};

const DEFAULT_LABELS = {
  region: "Metric cluster",
} as const satisfies Required<MetricClusterLabels>;

/**
 * Props for {@link MetricCluster}.
 *
 * @public
 */
export type MetricClusterProps = {
  /** Anchor corner. Defaults to `"top-right"`. */
  anchor?: MetricClusterAnchor;
  /** Localizable strings. */
  labels?: MetricClusterLabels;
  /** Metric entries in render order. */
  metrics: MetricClusterEntry[];
  /** Optional cluster title rendered above the rows. */
  title?: ReactNode;
  /** Anchor X in canvas pixels. */
  x: number;
  /** Anchor Y in canvas pixels. */
  y: number;
} & ComponentPropsWithoutRef<"div">;

const Row = (props: { entry: MetricClusterEntry }): React.ReactElement => {
  const { entry } = props;
  const tone = entry.tone ?? "neutral";
  return (
    <li
      className="flex items-center justify-between gap-2 text-[11px]"
      data-metric-cluster-row={entry.id}
      data-metric-cluster-tone={tone}
    >
      <span className="flex items-center gap-1.5 text-muted-foreground">
        <span
          aria-hidden="true"
          className={cn("size-1.5 rounded-full", TONE_DOT[tone])}
        />
        {entry.label}
      </span>
      <span className="font-semibold text-foreground">{entry.value}</span>
    </li>
  );
};

/**
 * Compact stack of related metrics pinned to a canvas object's corner.
 * Use when a single `StickyMetric` doesn't carry enough signal — for
 * runs whose throughput, latency, and error rate must all be visible
 * at once. Pure presentation; the host supplies the anchor coords + the
 * metric list.
 *
 * The wrapper is `pointer-events: none` — host gestures pass through.
 *
 * @example
 * ```tsx
 * <MetricCluster
 *   x={420} y={180}
 *   anchor="top-right"
 *   title="research-2025"
 *   metrics={[
 *     { id: "qps",  label: "qps",   value: "240", tone: "success" },
 *     { id: "errs", label: "errs",  value: "14",  tone: "danger" },
 *     { id: "p95",  label: "p95",   value: "180ms" },
 *   ]}
 * />
 * ```
 *
 * @public
 */
export const MetricCluster = forwardRef<HTMLDivElement, MetricClusterProps>(
  (props, ref) => {
    const {
      anchor = "top-right",
      className,
      labels,
      metrics,
      title,
      x,
      y,
      ...rest
    } = props;
    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
    return (
      <div
        aria-label={resolvedLabels.region}
        className={cn(
          "pointer-events-none absolute z-20 flex flex-col gap-1 rounded-md border border-border bg-background/90 p-2 shadow-sm backdrop-blur",
          className,
        )}
        data-metric-cluster
        data-metric-cluster-anchor={anchor}
        ref={ref}
        role="status"
        style={{
          left: x,
          top: y,
          transform: ANCHOR_TRANSFORM[anchor],
        }}
        {...rest}
      >
        {title ? (
          <header
            className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground"
            data-metric-cluster-title
          >
            {title}
          </header>
        ) : null}
        <ul className="space-y-0.5">
          {metrics.map((entry) => (
            <Row entry={entry} key={entry.id} />
          ))}
        </ul>
      </div>
    );
  },
);
MetricCluster.displayName = "MetricCluster";
typescript

Dependencies

  • @vllnt/ui@^0.2.1