Plan

The card an assistant hands you before it acts: what it intends to do, as a short list of to-dos you can read, edit and approve. Deliberately quiet, so status lives in a small mark at the head of each row.

Plan overview

Marketing site analytics

Replace the legacy tracking with a consented, typed event schema. Includes a safe rollout path for production.

To-dos6
  1. Audit the current tracking setup
    Not started
  2. Add a consent banner
    Not started
  3. Map the new event schema
    Not started

6 to-dos · about 6 min

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add plan

Usage#

usage.tsxTSX
import {  Plan,  PlanHeader,  PlanIcon,  PlanLabel,  PlanActions,  PlanHeading,  PlanTitle,  PlanDescription,  PlanStatus,  PlanGroup,  PlanGroupHeader,  PlanGroupTitle,  PlanGroupCount,  PlanSteps,  PlanStep,  PlanStepContent,  PlanStepTitle,  PlanStepDescription,  PlanStepTrigger,  PlanStepDetail,  PlanTasks,  PlanTask,  PlanStepAction,  PlanStepEdit,  PlanStepInput,  PlanStepSkeleton,  PlanAddStep,  PlanMore,  PlanFooter,  PlanSummary,} from "@/components/ui/plan"export function Example() {  return (    <Plan status="review">      <PlanHeader>        <PlanIcon><ListChecks weight="bold" /></PlanIcon>        <PlanLabel>Plan overview</PlanLabel>      </PlanHeader>      <PlanHeading>        <PlanTitle>Refresh the pricing page</PlanTitle>      </PlanHeading>      <PlanGroup>        <PlanGroupHeader>          <PlanGroupTitle>To-dos</PlanGroupTitle>          <PlanGroupCount>3</PlanGroupCount>        </PlanGroupHeader>        <PlanSteps>          <PlanStep>            <PlanStepContent>              <PlanStepTitle>Pull the three current tiers</PlanStepTitle>            </PlanStepContent>          </PlanStep>          {/* …more to-dos */}        </PlanSteps>      </PlanGroup>      <PlanFooter>        <PlanSummary>3 to-dos · about 2 min</PlanSummary>        <Button size="sm">Approve</Button>      </PlanFooter>    </Plan>  )}

Anatomy#

Four blocks, in reading order. An eyebrow says what kind of card this is (PlanIcon + PlanLabel, with PlanActions trailing). A heading says what the plan will do. A PlanGroup holds the work as a flat list under its own quiet label and count. A footer carries the meta and the decision — ghost on the left, primary on the right, per the house rule.

The card is the only container. Nothing inside it is boxed again and there are no dividers: groups are made by a label and whitespace, the way the Accordion's minimal variant makes rows. A panel inside a card reads as a slab in the dark themes and as a redundant line in the light ones, and every row shares the card's own left edge, so a plan has exactly one text axis from the eyebrow down to the footer. Every block is droppable.

Plan overview

Refresh the pricing page

Rewrite the three tiers and the feature matrix in plain language.

To-dos3
  1. Pull the three current tiers from the CMS
    Not started
  2. Rewrite the feature matrix
    Not started
  3. Swap the annual toggle copy
    Not started

3 to-dos · about 2 min

To-do states#

Each PlanStep carries a status, and that mark plus one step of ink is the whole of its styling: a dashed ring while it waits, a brand spinner while it runs, a green check once it's done, a red warning when it isn't. Only the live row goes to full ink; a finished one fades back. No row fill, no rail, no numerals — a column of solid circles reads as a diagram, and this is a paragraph you happen to be able to click.

Plan overview

Refresh the pricing page

Rewrite the three tiers and the feature matrix in plain language.

To-dos4
  1. Collect the merged pull requests
    Done
  2. Draft the highlights
    In progress
  3. Attach the screenshots
    Failed
  4. Publish to the changelog
    Not started

2 of 4

Collapsible to-dos#

Give a to-do collapsible and its PlanStepTrigger unfolds a PlanStepDetail with the sub-tasks it breaks down into. It wraps Radix Collapsible, so keyboard and ARIA come for free, and the panel opens on the DS collapsible keyframes. A plan mixes both shapes freely: rows with nothing to show stay plain. Sub-tasks are marked with a dash rather than a ring and sit a step quieter, so an unfolded row can never be mistaken for more to-dos of the plan itself.

Plan overview

Migrate the changelog to MDX

Open a to-do to see what it's doing.

To-dos4
  1. Done
    • Port the frontmatterDone
    • Rewrite inline HTML as componentsDone
    • Move the images into public/changelogNot started
    • Check every internal link still resolvesNot started
    In progress
  2. Not started
  3. Redirect the old URLs
    Not started

2 of 4

Read-only, then edit#

A plan is read-only by default. That is how it's looked at nine times out of ten: you read what the assistant proposes and approve it. Editing is a mode you deliberately enter, so PlanStepEdit and PlanAddStep render nothing at all — not hidden, not transparent, not in the DOM — until the root gets editing. Leave them in your markup unconditionally and let the one flag decide; no call site can leave half the card in the wrong mode.

Inside edit mode, every row is already a field — there is no pencil to press first, because a pencil whose only job is to focus the input sitting under it is a button that shouldn't exist. Render a PlanStepInput in place of each PlanStepTitle: it inherits the row's type and its resting tone and paints no frame, so the rows sit exactly where they sat, in the ink they were in, and simply start taking keys. Ink arrives on the one you reach for or land in. The field grows with the copy, Escape cancels, and Enter is yours to wire — the demo opens the next to-do with it, the way every list editor behaves.

That leaves PlanStepEdit holding one button: remove. It fades in with the pointer — six rows each carrying a trash can is a toolbar, not a plan — but never away from the keyboard or from a touch screen. It is also out of the row's flow, which matters more than it sounds: in flow, a 32px button is taller than a 20px line, so every single-line to-do would grow to the button's height while a wrapped one stayed taller and the rhythm would fall apart the moment you pressed Edit. Instead the row reserves one uniform 36px gutter for it, so the list is sized by its copy in both modes.

Plan overview

Refresh the pricing page

Read it through, then approve.

To-dos3
  1. Pull the three current tiers from the CMS
    Not started
  2. Rewrite the feature matrix in plain language
    Not started
  3. Swap the annual toggle copy
    Not started

3 to-dos · about 2 min

Long lists and drafting#

A plan is something you scan, so a long one opens at a readable length and offers the rest: PlanMore is the row that closes the panel — “3 more” collapsed, “Show less” open. Which rows you actually render is yours; this is the control and the label.

While the model is still writing, drop PlanStepSkeleton rows in. They keep the list's exact geometry — the same mark column, the same row height — so the real to-dos land in place instead of pushing the panel around when they arrive. Vary width across rows so a draft reads organic rather than striped, and each row that mounts while the plan is drafting plays the DS entrance, which is all the cascade a stream needs: rows arrive one at a time already.

Plan overview

Refresh the pricing page

Rewrite the three tiers and the feature matrix in plain language.

To-dos6
  1. Pull the three current tiers from the CMS
    Not started
  2. Rewrite the feature matrix
    Not started
  3. Swap the annual toggle copy
    Not started

6 to-dos · about 6 min

Plan overviewDrafting

Clean up the component registry

To-dos

Writing the plan…

Status chip#

PlanStatus derives its chip from the root's status, so a label and a state can never drift apart, and the root sets aria-busy while the plan is still moving. It is optional on purpose: in the quiet default the eyebrow says what the card is and the footer says what happens next, and a chip on top of both is one badge too many. Reach for it when a plan sits in a feed where it has to announce itself.

Plan overviewReady for review

Refresh the pricing page

  1. Pull the three current tiers
    Not started
  2. Rewrite the feature matrix
    Not started
Plan overviewRunning

Refresh the pricing page

  1. Pull the three current tiers
    Done
  2. Rewrite the feature matrix
    In progress
Plan overviewComplete

Refresh the pricing page

  1. Pull the three current tiers
    Done
  2. Rewrite the feature matrix
    Done
Plan overviewFailed

Refresh the pricing page

  1. Pull the three current tiers
    Done
  2. Rewrite the feature matrix
    Failed

Inside a conversation#

Set variant="plain" for a plan the assistant hands back inside a Chat turn or an AI Panel body. It drops the ring, the lift and the gutter, so the plan reads as part of the answer instead of a card inside a card. The panel keeps its own edge, which is all the structure the turn needs.

Can you get the docs search working across every component page?

Here's how I'd approach it. Say the word and I'll start.

To-dos3
  1. Index every MDX page at build time
    Not started
  2. Wire the index into the command palette
    Not started
  3. Add keyboard-only tests
    Not started

3 to-dos · about 4 min

Density#

Like Card and Dialog, the Plan reads the density context (or a density prop). Here it tunes the row rhythm and the block gaps only. The card's 12px gutter and the panel's 4px inset are the concentric ladder — they set the panel's and the rows' corners — so they are fixed in both tiers, because a gutter that moves with density breaks that nesting in one of them.

Comfortable

Fix the failing suite

To-dos3
  1. Read the failing test
    Done
  2. Patch the reducer
    In progress
  3. Re-run the suite
    Not started
Compact

Fix the failing suite

To-dos3
  1. Read the failing test
    Done
  2. Patch the reducer
    In progress
  3. Re-run the suite
    Not started

API reference#

Plan

The root card and the context provider. Forwards all div props.

  • status?: "drafting" | "review" | "running" | "complete" | "failed" — the plan's own state. Drives PlanStatus, the row entrance and aria-busy. Defaults to "review".
  • editing?: boolean — put the card in edit mode. A plan is read-only by default; PlanStepEdit and PlanAddStep render nothing until this is on. Also exposed as data-editing.
  • variant?: "card" | "plain" — card is the standalone artifact; plain drops the chrome and the gutter for a plan rendered inside a Chat turn or an AI Panel.
  • density?: "comfortable" | "compact" — row rhythm and block gaps; defaults from the density context.
  • asChild?: boolean — render as a child element via Radix Slot.

PlanStep

  • status?: "pending" | "active" | "done" | "failed" — the row's 14px mark and its ink. Defaults to "pending".
  • collapsible?: boolean plus open / defaultOpen / onOpenChange — turns the row into a Radix Collapsible whose trigger is PlanStepTrigger and whose panel is PlanStepDetail.

PlanStepAction / PlanStepEdit

Two trailing slots with different lifetimes. PlanStepAction is what the row is telling you — a duration, a Badge, a Retry on a failed row — and is always visible. PlanStepEdit is what you can do to the row — in practice one Remove button, since every row is already a field in edit mode — renders nothing unless the plan is editing, and fades in with the pointer (staying visible on keyboard focus and on a coarse pointer, where there is no hover).

PlanStepInput

The inline editor that replaces a row's title while the plan is editing — every row at once, so there is no rename button. It inherits the row's type and resting tone, so flipping the mode changes nothing on screen. Controlled: value + onValueChange, with onCommit (Enter or blur) and onCancel (Escape). Shift+Enter still breaks a line and an open IME composition is never hijacked. Forwards all textarea props.

PlanMore / PlanAddStep

The two rows that close a list, both forwarding all button props. PlanMore takes expanded?: boolean and count?: number and labels itself “3 more” / “Show less”. PlanAddStep renders nothing unless the plan is editing.

PlanStepSkeleton

The placeholder row for a plan still being written. Takes width?: "full" | "long" | "short" so a draft reads organic.

Layout parts

PlanHeader, PlanIcon, PlanLabel, PlanActions, PlanHeading, PlanTitle (takes asChild), PlanDescription, PlanGroup + PlanGroupHeader / PlanGroupTitle / PlanGroupCount, PlanSteps, PlanTasks / PlanTask, PlanFooter and PlanSummary. Every part is droppable: a plan renders correctly with any subset of them.

FAQ#

Because a numbered column of filled discs reads as a diagram, and an approval card is something you scan in a second. The list is a semantic ordered list, so position and count are still announced by a screen reader; visually, the only mark a row carries is the 14px status glyph at its head. If a plan genuinely needs visible ordering, the group title and count carry it.