- Read the failing test1.2s
Opened prompt-input.test.tsx and located the Enter handler assertion.
- Patch the submit handlerrunning
Reading from the event target instead of the stale prop.
- onSubmit?.(value); + onSubmit?.(e.currentTarget.value);
- Run the full test suite
Needs your approval before executing commands.
- Open a pull request
import {
Step,
StepBody,
StepDescription,
StepDetails,
StepHeader,
StepIndicator,
StepMeta,
StepTitle,
Steps,
} from "@/components/pandacoderz-ui/steps";
export default function StepsDemo() {
return (
<div className="w-full max-w-md">
<Steps>
<Step status="completed">
<StepIndicator />
<StepBody>
<StepHeader>
<StepTitle>Read the failing test</StepTitle>
<StepMeta>1.2s</StepMeta>
</StepHeader>
<StepDescription>Opened prompt-input.test.tsx and located the Enter handler assertion.</StepDescription>
</StepBody>
</Step>
<Step status="running">
<StepIndicator />
<StepBody>
<StepHeader>
<StepTitle>Patch the submit handler</StepTitle>
<StepMeta>running</StepMeta>
</StepHeader>
<StepDescription>Reading from the event target instead of the stale prop.</StepDescription>
<StepDetails label="Show diff" defaultOpen>
<pre className="rounded-lg border bg-code p-3 font-mono text-xs leading-5">
{`- onSubmit?.(value);
+ onSubmit?.(e.currentTarget.value);`}
</pre>
</StepDetails>
</StepBody>
</Step>
<Step status="waiting">
<StepIndicator />
<StepBody>
<StepHeader>
<StepTitle>Run the full test suite</StepTitle>
</StepHeader>
<StepDescription>Needs your approval before executing commands.</StepDescription>
</StepBody>
</Step>
<Step status="pending" isLast>
<StepIndicator />
<StepBody>
<StepHeader>
<StepTitle>Open a pull request</StepTitle>
</StepHeader>
</StepBody>
</Step>
</Steps>
</div>
);
}Installation
npx shadcn@latest add https://ui.spencerwueste.com/r/steps.jsonpnpm dlx shadcn@latest add https://ui.spencerwueste.com/r/steps.jsonyarn dlx shadcn@latest add https://ui.spencerwueste.com/r/steps.jsonbunx --bun shadcn@latest add https://ui.spencerwueste.com/r/steps.jsonInstall the dependencies:
npm install @heroicons-animated/react motionAdd the shadcn primitives it builds on:
npx shadcn@latest add collapsibleCopy the source into your project:
"use client";
import * as React from "react";
import {
ArrowPathIcon,
CheckCircleIcon,
ChevronDownIcon,
ClockIcon,
HandRaisedIcon,
XCircleIcon,
} from "@heroicons-animated/react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
type StepStatus = "pending" | "running" | "completed" | "error" | "waiting";
type StepIconType = React.ComponentType<{ size?: number; className?: string }>;
const STEP_META: Record<
StepStatus,
{ label: string; icon: StepIconType; className: string; iconClassName?: string }
> = {
pending: {
label: "Pending",
icon: ClockIcon,
className: "border-border bg-background text-muted-foreground",
},
running: {
label: "Running",
icon: ArrowPathIcon,
className: "border-brand/40 bg-brand-soft text-brand",
iconClassName: "animate-spin",
},
completed: {
label: "Completed",
icon: CheckCircleIcon,
className: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
},
error: {
label: "Failed",
icon: XCircleIcon,
className: "border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400",
},
waiting: {
label: "Needs approval",
icon: HandRaisedIcon,
className: "border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400",
},
};
type StepContextValue = { status: StepStatus };
const StepContext = React.createContext<StepContextValue | null>(null);
function useStepContext(component: string) {
const ctx = React.useContext(StepContext);
if (!ctx) throw new Error(`${component} must be used within <Step>`);
return ctx;
}
function Steps({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="steps"
className={cn("flex flex-col", className)}
{...props}
/>
);
}
type StepProps = Omit<React.ComponentProps<"li">, "children"> & {
status?: StepStatus;
/** Hide the connector below this step (defaults to auto via CSS). */
isLast?: boolean;
children?: React.ReactNode;
};
function Step({ status = "pending", className, isLast, children, ...props }: StepProps) {
return (
<StepContext.Provider value={{ status }}>
<li
data-slot="step"
data-status={status}
className={cn(
"group/step relative flex gap-3 pb-6 last:pb-0",
!isLast &&
"before:absolute before:top-7 before:bottom-0 before:left-[13px] before:w-px before:bg-border last:before:hidden",
className,
)}
{...props}
>
{children}
</li>
</StepContext.Provider>
);
}
function StepIndicator({ className, children, ...props }: React.ComponentProps<"div">) {
const { status } = useStepContext("StepIndicator");
const meta = STEP_META[status];
const Icon = meta.icon;
return (
<div
data-slot="step-indicator"
aria-label={meta.label}
className={cn(
"relative z-10 flex size-7 shrink-0 items-center justify-center rounded-full border",
meta.className,
className,
)}
{...props}
>
{children ?? <Icon size={14} className={cn("flex", meta.iconClassName)} />}
</div>
);
}
function StepBody({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="step-body"
className={cn("flex min-w-0 flex-1 flex-col gap-1.5 pt-0.5", className)}
{...props}
/>
);
}
function StepHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="step-header"
className={cn("flex min-h-6 items-center justify-between gap-2", className)}
{...props}
/>
);
}
function StepTitle({ className, ...props }: React.ComponentProps<"div">) {
const { status } = useStepContext("StepTitle");
return (
<div
data-slot="step-title"
className={cn(
"truncate text-sm font-medium",
status === "pending" && "text-muted-foreground",
className,
)}
{...props}
/>
);
}
function StepMeta({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="step-meta"
className={cn("shrink-0 text-xs tabular-nums text-muted-foreground", className)}
{...props}
/>
);
}
function StepDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="step-description"
className={cn("text-sm leading-6 text-muted-foreground", className)}
{...props}
/>
);
}
/** Optional collapsible detail area (logs, tool calls, output). */
type StepDetailsProps = React.ComponentProps<typeof Collapsible> & {
label?: string;
};
function StepDetails({ label = "Details", className, children, ...props }: StepDetailsProps) {
return (
<Collapsible data-slot="step-details" className={cn("mt-1", className)} {...props}>
<CollapsibleTrigger className="group/trigger flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
<ChevronDownIcon
size={12}
className="flex transition-transform group-data-[state=open]/trigger:rotate-180"
/>
{label}
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 flex flex-col gap-2 data-closed:animate-out data-closed:fade-out-0 data-open:animate-in data-open:fade-in-0">
{children}
</CollapsibleContent>
</Collapsible>
);
}
export type { StepStatus };
export {
Steps,
Step,
StepIndicator,
StepBody,
StepHeader,
StepTitle,
StepMeta,
StepDescription,
StepDetails,
};Usage
import {
Step,
StepBody,
StepDescription,
StepDetails,
StepHeader,
StepIndicator,
StepMeta,
StepTitle,
Steps,
} from "@/components/pandacoderz-ui/steps";<Steps>
<Step status="completed">
<StepIndicator />
<StepBody>
<StepHeader>
<StepTitle>Read the failing test</StepTitle>
<StepMeta>1.2s</StepMeta>
</StepHeader>
<StepDescription>Located the Enter handler assertion.</StepDescription>
</StepBody>
</Step>
<Step status="running" isLast>
<StepIndicator />
<StepBody>
<StepHeader><StepTitle>Patch the handler</StepTitle></StepHeader>
<StepDetails label="Show diff">{/* logs, tool calls, code */}</StepDetails>
</StepBody>
</Step>
</Steps>The waiting status is for human-in-the-loop gates. Render your approve and reject buttons inside StepBody.
API Reference
Step
| Prop | Type | Description |
|---|---|---|
status |
"pending" | "running" | "completed" | "error" | "waiting" |
Drives the indicator icon and colors. |
isLast |
boolean |
Hides the connector line below this step. |
StepIndicator
Renders the status icon. Pass children to override it (a number, an avatar).
StepDetails
| Prop | Type | Description |
|---|---|---|
label |
string |
Trigger text. Default "Details". |
defaultOpen |
boolean |
Initial expanded state. |