# Workspace Setup

> Vertical onboarding stepper in a card: each of four steps opens a panel with an input, a primary action and a do-this-later option; skipped steps keep an open ring instead of a check.

A shadcn/ui block from the [@01orbital](https://01orbital.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/01orbital/workspace-setup).

- Page, with a live preview: https://shadcn.exchange/r/01orbital/workspace-setup
- Screenshot: https://shadcn.exchange/shot/01orbital/workspace-setup/light.png
- Registry item: https://01orbital.dev/r/workspace-setup.json
- Categories: stepper
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/01orbital/workspace-setup.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@phosphor-icons/react`, `cn`
- Registry items: `@01orbital/button`, `card`, `@01orbital/input`, `@01orbital/stepper`

## Files

### `blocks/stepper/workspace-setup/index.tsx`

```tsx
"use client"

import { useEffect, useRef, useState } from "react"
import { CheckIcon } from "@phosphor-icons/react"
import { cn } from "cn"

import {
  Stepper,
  StepperContent,
  StepperDescription,
  StepperIndicator,
  StepperItem,
  StepperList,
  StepperSeparator,
  StepperTitle,
  StepperTrigger,
} from "@/components/stepper/stepper"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"

// The copy: change text here, not in the markup.
const COPY = {
  title: "Set up your workspace",
  body: "Four steps. Most teams finish in ten minutes.",
  done: "Done",
  skip: "Do this later",
  ready: "Your workspace is ready.",
  steps: [
    {
      title: "Name the workspace",
      body: "Shown in the sidebar and on invites.",
      action: "Save name",
      placeholder: "Orbcore Design",
    },
    {
      title: "Invite your team",
      body: "Add people by email. Admins can approve requests later.",
      action: "Send invites",
      placeholder: "tomas@orbcore.dev, ines@orbcore.dev",
    },
    {
      title: "Connect a repository",
      body: "GitHub, GitLab or Bitbucket.",
      action: "Connect",
      placeholder: "github.com/orbcore/registry",
    },
    {
      title: "Install the CLI",
      body: "One command, any package manager.",
      action: "I ran it",
      placeholder: "pnpm dlx shadcn@latest init",
    },
  ],
} as const

// Vertical: the list stacks, each separator runs down from its indicator, and
// the current step's panel opens under its own title.
export default function WorkspaceSetup() {
  const [step, setStep] = useState(1)
  const last = COPY.steps.length - 1
  // Done is its own set: Do this later opens the next step without a check.
  const [done, setDone] = useState<number[]>([0])
  const finish = (i: number) => {
    setDone((d) => (d.includes(i) ? d : [...d, i]))
    setStep(Math.min(i + 1, last))
  }
  const ready = done.includes(last)
  const readyRef = useRef<HTMLParagraphElement>(null)
  useEffect(() => {
    if (ready) readyRef.current?.focus()
  }, [ready])
  return (
    <Card className="mx-auto w-full max-w-md">
      <CardHeader>
        <CardTitle>{COPY.title}</CardTitle>
        <CardDescription>{COPY.body}</CardDescription>
      </CardHeader>
      <CardContent>
        <Stepper value={step} onValueChange={setStep} linear={false}>
          <StepperList className="flex-col items-stretch">
            {COPY.steps.map((s, i) => (
              <StepperItem
                key={s.title}
                value={i}
                className="flex-col items-stretch last:flex-1"
              >
                <div className="flex gap-3">
                  <div className="flex flex-col items-center">
                    <StepperIndicator
                      className={cn(
                        "size-6 text-[11px]",
                        done.includes(i)
                          ? "bg-primary text-primary-foreground ring-0"
                          : // Skipped: positionally completed, but it keeps the
                            // open ring so only done steps read as done.
                            "data-[state=completed]:bg-card data-[state=completed]:text-card-foreground data-[state=completed]:ring-1 data-[state=completed]:ring-border"
                      )}
                    >
                      {done.includes(i) ? (
                        <CheckIcon
                          weight="bold"
                          aria-hidden
                          className="size-3"
                        />
                      ) : (
                        <span>{i + 1}</span>
                      )}
                    </StepperIndicator>
                    <StepperSeparator className="mx-0 my-1 h-auto w-px flex-1 group-last/step:hidden" />
                  </div>
                  <div className="min-w-0 flex-1 pt-0.5 pb-6 group-last/step:pb-0">
                    <StepperTrigger className="h-auto justify-start px-0 py-0 text-left hover:bg-transparent">
                      <StepperTitle>{s.title}</StepperTitle>
                    </StepperTrigger>
                    <StepperContent
                      keepMounted
                      value={i}
                      className="mt-1 gap-3"
                    >
                      <StepperDescription>{s.body}</StepperDescription>
                      <Input
                        defaultValue={s.placeholder}
                        aria-label={s.title}
                      />
                      <div className="flex flex-wrap gap-2">
                        <Button size="sm" onClick={() => finish(i)}>
                          {i === last ? COPY.done : s.action}
                        </Button>
                        <Button
                          size="sm"
                          variant="ghost"
                          onClick={() => setStep(Math.min(i + 1, last))}
                        >
                          {COPY.skip}
                        </Button>
                      </div>
                    </StepperContent>
                  </div>
                </div>
              </StepperItem>
            ))}
          </StepperList>
        </Stepper>
        <p
          ref={readyRef}
          tabIndex={-1}
          aria-live="polite"
          className={cn("text-sm outline-none", ready ? "mt-4" : "sr-only")}
        >
          {ready ? COPY.ready : ""}
        </p>
      </CardContent>
    </Card>
  )
}
```
