# Project Views

> Shows one set of projects as a Board, a List, or a Timeline, switched by tabs, with search, filters, and a sheet for each project. The name column stays still between List and Timeline, and only the dates side changes.

- Name: `project-views`
- Status: beta
- Categories: projects, board, kanban, list, timeline, gantt, tasks
- Files: the install writes `project-views.tsx`, and any part files named `project-views-*.tsx`, to your `components` alias, `@/components` by default. Edit them freely. They are yours.
- npm packages: `lucide-react`. The install adds them.
- Other registry items: `@txbrendan/avatar-stack`, `@txbrendan/badge`, `@txbrendan/board`, `@txbrendan/button`, `@txbrendan/gantt-chart`, `@txbrendan/glide-highlight`, `@txbrendan/input`, `@txbrendan/menu`, `@txbrendan/motion-timing`, `@txbrendan/notice`, `@txbrendan/progress`, `@txbrendan/rolling-number`, `@txbrendan/search-field`, `@txbrendan/sheet`, `@txbrendan/skeleton`, `@txbrendan/tabs`, `@txbrendan/toast`, `@txbrendan/tooltip`. The install adds them.
- Gallery: https://txbrendan.dev/blocks/project-views
- Registry item: https://txbrendan.dev/r/project-views.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the block. It also adds every component it uses:

   ```bash
   npx shadcn@latest add @txbrendan/project-views
   ```

## Use it for

- Work that moves through stages, like projects, when people want a board, a list, and dates on one screen.
- Pass defaultProjects for the block to own the list, or projects with onProjectsChange to own it yourself.
- Give each project a stage id from stages. The default stages are To do, In progress, In review, and Done.
- Pass today as an ISO day from the server. It marks the timeline and dates new projects, and keeps the first render the same on the server and in the browser.
- Dates are ISO days, like "2026-11-06". start and due draw the timeline bar.
- Move and Remove apply at once and offer Undo in a toast for 8 seconds, so place one Toaster on the page.
- Set loading while the projects load, and error with onRetry when they fail.

## Avoid it for

- A single list with no stages. Use Table or List Row.
- Only a timeline, with no board or list. Use Gantt Chart.
- Dragging cards between lanes. Cards do not drag. Each card's menu moves it.

## Props

- `projects`: `{ id: string; name: string; kind?: string; stage: string; description?: string; priority?: "high" | "medium" | "low"; urgent?: boolean; tasks?: { done: number; total: number }; people?: string[]; comments?: number; files?: number; start?: string; due?: string }[]`. The projects, in order inside each stage. Use with onProjectsChange to own them. Each id is unique, and stage is a stage's id.
- `defaultProjects`: `Project[]`. Default `[]`. The projects to start with, when the block owns them.
- `onProjectsChange`: `(projects: Project[]) => void`. Called with the whole new list after an add, a move, a remove, or an undo.
- `view`: `"board" | "list" | "timeline"`. Which view shows. Use with onViewChange.
- `defaultView`: `"board" | "list" | "timeline"`. Default `"board"`. The view to start on.
- `onViewChange`: `(view: ProjectView) => void`. Called when a tab picks another view.
- `defaultSearch`: `string`. Default `""`. The search to start with.
- `stages`: `{ id: string; title: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning" }[]`. Default `To do, In progress, In review, Done`. The stages, in order. Each is a lane on the board and a group in the list.
- `today`: `string`. Today as an ISO day, passed from the server.
- `locale`: `string`. Default `"en-US"`. The locale for dates.
- `timeZone`: `string`. Default `"UTC"`. The time zone the timeline reads days in.
- `loading`: `boolean`. Default `false`. Each view holds its space with placeholders, and the count waits.
- `error`: `string`. Says what went wrong in place of the views.
- `onRetry`: `() => void`. Shows Retry beside the error.
- `className`: `string`. Extra classes on the outer section, which holds the container query.

## Accessibility

- The block is a section named by its heading. On a page with more than one, give each an aria-label, which then names it instead. The views are tabs, and the arrow keys move between them.
- Each card and each row has one button named by the project, which opens its sheet. The rest of a row opens it with a pointer.
- Each project's "…" button is named "More for" and the project, and opens Open, Move to, and Remove.
- After a move or a remove, focus goes to the project in its new place, or to the one beside where it was.
- List and Timeline are one table, with column headers, a row header for each project, and a row group for each stage.
- Each lane is a group named by its stage and count. Each "+" button is named for its stage, and its tooltip says Add project.
- The view that leaves is hidden from screen readers and from Tab while it fades.
- The count line and each stage's count roll to new numbers. Reduced motion takes out the slide, and motion off shows each view at once.

## Examples

### Projects the block keeps

```tsx
<ProjectViews
  today="2026-10-30"
  defaultProjects={[
    { id: "payments", name: "Payments page", kind: "Web", stage: "doing", priority: "high", urgent: true, tasks: { done: 9, total: 20 }, people: ["AK", "BL"], start: "2026-10-19", due: "2026-11-06" },
    { id: "help", name: "Help center", kind: "Web", stage: "review", tasks: { done: 18, total: 20 }, start: "2026-10-12", due: "2026-10-31" },
  ]}
/>
```

### Your own state owns the projects and the view

```tsx
"use client";

function TeamProjects({ initial, today }: { initial: Project[]; today: string }) {
  const [projects, setProjects] = useState(initial);
  const [view, setView] = useState<ProjectView>("list");
  return (
    <ProjectViews
      projects={projects}
      onProjectsChange={(next) => {
        setProjects(next);
        saveProjects(next);
      }}
      view={view}
      onViewChange={setView}
      today={today}
    />
  );
}
```

### Your own stages

```tsx
<ProjectViews
  today="2026-10-30"
  stages={[
    { id: "ideas", title: "Ideas" },
    { id: "building", title: "Building", tone: "brand" },
    { id: "shipped", title: "Shipped", tone: "success" },
  ]}
  defaultProjects={[{ id: "search", name: "Search", stage: "ideas" }]}
/>
```

### While loading, and when loading fails

```tsx
<ProjectViews
  projects={data ?? []}
  loading={isLoading}
  error={failed ? "Projects could not load." : undefined}
  onRetry={refetch}
/>
```
