
Onboarding checklist card with a read-only segmented bar counting done tasks, struck-through completed items, inline actions for open ones, a revealable CLI command and a Hide control.
blocks/stepper/setup-progress/index.tsx
"use client"
import { useState } from "react"
import { CheckIcon } from "@phosphor-icons/react"
import {
Stepper,
StepperItem,
StepperList,
StepperSeparator,
StepperTitle,
} from "@/components/stepper/stepper"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
// The copy: change text here, not in the markup.
const COPY = {
title: "Finish setting up",
done: "done",
of: "of",
dismiss: "Hide",
show: "Show setup",
command: "pnpm dlx shadcn@latest add @01orbital/setup-progress",
tasks: [
{ title: "Create the workspace", done: true },
{ title: "Add a payment method", done: true },
{ title: "Invite a teammate", done: true },
{
title: "Connect a repository",
done: false,
action: "Connect",
kind: "connect",
},
{
title: "Install the CLI",
done: false,
action: "Show command",
kind: "command",
},
],
} as const
// Read-only bars under a card title: a count of done tasks, not a workflow
// stage, so current={false} keeps aria-current off. Bars fill through
// value, so done - 1 is the last done task and none done leaves the strip
// empty. The open tasks are listed with their action.
export default function SetupProgress() {
// Local state for the demo: Hide collapses the card, Connect completes its
// task, Show command reveals the command. A real block reads these from the
// account.
const [hidden, setHidden] = useState(false)
const [connected, setConnected] = useState(false)
const [showCommand, setShowCommand] = useState(false)
const tasks = COPY.tasks.map((t) =>
"kind" in t && t.kind === "connect" ? { ...t, done: connected } : t
)
const done = tasks.filter((t) => t.done).length
if (hidden)
return (
<div className="mx-auto flex w-full max-w-sm justify-end">
<Button variant="ghost" size="sm" onClick={() => setHidden(false)}>
{COPY.show}
</Button>
</div>
)
return (
<Card className="mx-auto w-full max-w-sm">
<CardHeader>
<CardTitle>{COPY.title}</CardTitle>
<CardDescription>
{done} {COPY.of} {COPY.tasks.length} {COPY.done}
</CardDescription>
<CardAction>
<Button variant="ghost" size="sm" onClick={() => setHidden(true)}>
{COPY.dismiss}
</Button>
</CardAction>
</CardHeader>
<CardContent className="grid gap-5">
<Stepper
value={done - 1}
variant="bars"
rail="ink"
current={false}
className="gap-0"
>
<StepperList>
{tasks.map((t, i) => (
<StepperItem key={t.title} value={i}>
<StepperSeparator />
<StepperTitle className="sr-only">{t.title}</StepperTitle>
</StepperItem>
))}
</StepperList>
</Stepper>
<ul className="grid gap-2.5 text-sm">
{tasks.map((t) => (
<li key={t.title} className="flex flex-wrap items-center gap-3">
<span
className={
t.done
? "flex size-4 items-center justify-center rounded-full bg-foreground text-background"
: "size-4 rounded-full ring-1 ring-border ring-inset"
}
>
{t.done && (
<CheckIcon className="size-2.5" weight="bold" aria-hidden />
)}
</span>
<span
className={
t.done ? "text-muted-foreground line-through" : "flex-1"
}
>
{t.title}
</span>
{"action" in t && !t.done && (
<Button
size="xs"
variant="outline"
className="ml-auto"
aria-expanded={t.kind === "command" ? showCommand : undefined}
onClick={() =>
t.kind === "connect"
? setConnected(true)
: setShowCommand((v) => !v)
}
>
{t.action}
</Button>
)}
{"kind" in t && t.kind === "command" && showCommand && (
<pre className="basis-full overflow-x-auto rounded-md bg-muted px-3 py-2 font-mono text-xs leading-6">
{COPY.command}
</pre>
)}
</li>
))}
</ul>
</CardContent>
</Card>
)
}