Object Inspector

Right-dock detail header — kind chip, status dot, title/subtitle, with property-section slots.

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/object-inspector.json

Storybook

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

View in Storybook

4 stories available:

Code

"use client"; import { type ComponentPropsWithoutRef, forwardRef, type ReactNode, } from "react"; import { cn } from "../../lib/utils"; /** * Object kind — drives the chip glyph + label. * * @public */ export type ObjectInspectorKind = | "agent" | "artifact" | "input" | "output" | "run" | "task"; const KIND_LABEL: Record<ObjectInspectorKind, string> = { agent: "Agent", artifact: "Artifact", input: "Input", output: "Output", run: "Run", task: "Task", }; const KIND_GLYPH: Record<ObjectInspectorKind, string> = { agent: "◇", artifact: "◌", input: "↘", output: "↗", run: "▶", task: "▢", }; /** * Live status — drives the colored dot. * * @public */ export type ObjectInspectorStatus = | "complete" | "failed" | "idle" | "queued" | "running"; const STATUS_DOT: Record<ObjectInspectorStatus, string> = { complete: "bg-emerald-500", failed: "bg-red-500", idle: "bg-muted-foreground", queued: "bg-amber-500", running: "bg-blue-500 animate-pulse", }; const STATUS_LABEL: Record<ObjectInspectorStatus, string> = { complete: "Complete", failed: "Failed", idle: "Idle", queued: "Queued", running: "Running", }; /** * Localizable strings. * * @public */ export type ObjectInspectorLabels = { /** Empty-state copy. Defaults to `"No selection"`. */ empty?: string; /** Aria-label for the inspector. Defaults to `"Object inspector"`. */ region?: string; }; const DEFAULT_LABELS = { empty: "No selection", region: "Object inspector", } as const satisfies Required<ObjectInspectorLabels>; /** * Props for {@link ObjectInspector}. * * @public */ export type ObjectInspectorProps = { /** Object kind. When omitted, the inspector renders the empty state. */ kind?: ObjectInspectorKind; /** Localizable strings. */ labels?: ObjectInspectorLabels; /** Object status. Defaults to `"idle"`. */ status?: ObjectInspectorStatus; /** Optional subtitle (id, owner, model). */ subtitle?: ReactNode; /** Object title. */ title?: ReactNode; } & ComponentPropsWithoutRef<"section">; const KindChip = (props: { kind: ObjectInspectorKind }): React.ReactElement => ( <span aria-label={KIND_LABEL[props.kind]} className="inline-flex items-center gap-1 rounded-full border border-border bg-background px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground" > <span aria-hidden="true">{KIND_GLYPH[props.kind]}</span> {KIND_LABEL[props.kind]} </span> ); const StatusDot = (props: { status: ObjectInspectorStatus; }): React.ReactElement => ( <span aria-label={`Status ${STATUS_LABEL[props.status]}`} className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground" > <span aria-hidden="true" className={cn("size-1.5 rounded-full", STATUS_DOT[props.status])} /> {STATUS_LABEL[props.status]} </span> ); const Header = (props: { kind: ObjectInspectorKind; status: ObjectInspectorStatus; subtitle?: ReactNode; title: ReactNode; }): React.ReactElement => ( <header className="flex flex-col gap-2"> <div className="flex items-center justify-between gap-2"> <KindChip kind={props.kind} /> <StatusDot status={props.status} /> </div> <div className="space-y-0.5"> <h3 className="truncate text-sm font-semibold text-foreground"> {props.title} </h3> {props.subtitle ? ( <p className="truncate text-xs text-muted-foreground"> {props.subtitle} </p> ) : null} </div> </header> ); /** * Inspector header for the right dock. Renders kind chip + status dot * + title + subtitle, then any children below (typically a stack of * {@link "../property-section/property-section".PropertySection} blocks). Pure presentation; the host * derives props from the current selection and slots the property * sections. * * @example * ```tsx * <ObjectInspector * kind="run" * status="running" * title="research-2025-04-15" * subtitle="claude-3.7" * > * <PropertySection title="Layout" entries={} /> * <PropertySection title="State" entries={} /> * </ObjectInspector> * ``` * * @public */ export const ObjectInspector = forwardRef<HTMLElement, ObjectInspectorProps>( (props, ref) => { const { children, className, kind, labels, status = "idle", subtitle, title, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; if (!kind || !title) { return ( <section aria-label={resolvedLabels.region} className={cn( "flex w-full flex-col items-center justify-center gap-1 rounded-2xl border bg-background p-6 text-center text-xs text-muted-foreground", className, )} data-object-inspector data-object-state="empty" ref={ref} {...rest} > <span aria-hidden="true" className="text-lg"> </span> <span>{resolvedLabels.empty}</span> </section> ); } return ( <section aria-label={resolvedLabels.region} className={cn( "flex w-full flex-col gap-3 rounded-2xl border bg-background p-3 text-foreground", className, )} data-object-inspector data-object-kind={kind} data-object-status={status} ref={ref} {...rest} > <Header kind={kind} status={status} subtitle={subtitle} title={title} /> {children ? <div className="space-y-2">{children}</div> : null} </section> ); }, ); ObjectInspector.displayName = "ObjectInspector";

Dependencies

  • @vllnt/ui@^0.2.1