State Badge Overlay

State chip pinned to a canvas object's corner — idle, queued, running, complete, failed, stopped.

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/state-badge-overlay.json

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"; /** * State name — drives the badge tone. * * @public */ export type StateBadgeState = | "complete" | "failed" | "idle" | "queued" | "running" | "stopped"; const STATE_LABEL: Record<StateBadgeState, string> = { complete: "Complete", failed: "Failed", idle: "Idle", queued: "Queued", running: "Running", stopped: "Stopped", }; const STATE_TONE: Record<StateBadgeState, string> = { complete: "border-emerald-500/40 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300", failed: "border-red-500/40 bg-red-500/15 text-red-700 dark:text-red-300", idle: "border-border bg-muted/40 text-muted-foreground", queued: "border-amber-500/40 bg-amber-500/15 text-amber-700 dark:text-amber-300", running: "border-blue-500/40 bg-blue-500/15 text-blue-700 dark:text-blue-300", stopped: "border-border bg-background text-foreground", }; const STATE_DOT: Record<StateBadgeState, string> = { complete: "bg-emerald-500", failed: "bg-red-500", idle: "bg-muted-foreground", queued: "bg-amber-500", running: "bg-blue-500 animate-pulse", stopped: "bg-muted-foreground", }; /** * Anchor corner relative to the canvas object the badge attaches to. * * @public */ export type StateBadgeAnchor = | "bottom-left" | "bottom-right" | "top-left" | "top-right"; const ANCHOR_TRANSFORM: Record<StateBadgeAnchor, string> = { "bottom-left": "translate(-100%, 100%)", "bottom-right": "translate(0%, 100%)", "top-left": "translate(-100%, -100%)", "top-right": "translate(0%, -100%)", }; /** * Localizable strings. * * @public */ export type StateBadgeOverlayLabels = { /** Aria-label override. Defaults to `"State"`. */ region?: string; }; const DEFAULT_LABELS = { region: "State", } as const satisfies Required<StateBadgeOverlayLabels>; /** * Props for {@link StateBadgeOverlay}. * * @public */ export type StateBadgeOverlayProps = { /** Anchor corner. Defaults to `"top-right"`. */ anchor?: StateBadgeAnchor; /** Optional override label (defaults to humanized state name). */ label?: ReactNode; /** Localizable strings. */ labels?: StateBadgeOverlayLabels; /** State to display. */ state: StateBadgeState; /** Anchor X in canvas pixels. */ x: number; /** Anchor Y in canvas pixels. */ y: number; } & ComponentPropsWithoutRef<"div">; /** * State chip pinned to a canvas object's corner. Use when a single- * letter glyph in `ObjectCard` doesn't carry enough signal — for * runs that have transitioned, jobs that failed, agents idling. Pure * presentation; the host computes the anchor from the object's * bounding box. * * The wrapper is `pointer-events: none` — host gestures pass through. * * @example * ```tsx * <div className="relative h-screen w-screen"> * <Canvas /> * <StateBadgeOverlay state="running" x={420} y={180} anchor="top-right" /> * </div> * ``` * * @public */ export const StateBadgeOverlay = forwardRef< HTMLDivElement, StateBadgeOverlayProps >((props, ref) => { const { anchor = "top-right", className, label, labels, state, x, y, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; const text = label ?? STATE_LABEL[state]; return ( <div aria-label={`${resolvedLabels.region}: ${STATE_LABEL[state]}`} className={cn( "pointer-events-none absolute z-20 inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide shadow-sm backdrop-blur", STATE_TONE[state], className, )} data-state-anchor={anchor} data-state-badge-overlay data-state-name={state} ref={ref} role="status" style={{ left: x, top: y, transform: ANCHOR_TRANSFORM[anchor], }} {...rest} > <span aria-hidden="true" className={cn("size-1.5 rounded-full", STATE_DOT[state])} data-state-badge-dot /> {text} </div> ); }); StateBadgeOverlay.displayName = "StateBadgeOverlay";

Dependencies

  • @vllnt/ui@^0.2.1