# Project

> A complete screen built from AppShell, Avatar, AvatarStack, Badge and 11 more components.

Pattern · Pro kit · [Live page](https://ui.twentyfirst.media/pro/patterns/ProjectScreen/)

## Code

```tsx
import * as React from "react";
import {
  AppShell,
  Avatar,
  AvatarStack,
  Badge,
  Button,
  Checkbox,
  FileRow,
  PageHeader,
  Panel,
  ProgressRing,
  Stagger,
  StatTile,
  Tabs,
  Timeline,
} from "@twentyfirstmedia/pro";

function App() {
  // task, done, assignee
  const tasks: [string, boolean, string][] = [
    ["Draft new tier names", true, "Mara Ellis"],
    ["Comparison table component", true, "Jonah Petit"],
    ["Localise tiers for DE and FR", false, "Sun Lee"],
    ["Checkout seat selector", false, "Rui Costa"],
    ["Write FAQ for annual plans", false, "Ava Brandt"],
  ];
  return (
    <AppShell
      brand="Halo"
      tag="Admin"
      user={{ name: "Mara Ellis", email: "mara@halo.app" }}
      active="Projects"
      side={
        <Panel side title="Activity" subtitle="Last 7 days">
          <Timeline
            delay={300}
            items={[
              {
                avatar: "Mara Ellis",
                text: (
                  <span>
                    <b>Mara</b>
                    {" approved the tier names"}
                  </span>
                ),
                time: "2 min ago",
              },
              {
                avatar: "Jonah Petit",
                text: (
                  <span>
                    <b>Jonah</b>
                    {" moved Comparison table to review"}
                  </span>
                ),
                time: "1 h ago",
              },
              {
                icon: "sparkle",
                tint: "mint",
                text: "Preview deployed to staging",
                time: "Yesterday",
              },
              {
                avatar: "Rui Costa",
                text: (
                  <span>
                    <b>Rui</b>
                    {" flagged the seat selector as blocked"}
                  </span>
                ),
                time: "Mon",
              },
              {
                icon: "flag",
                tint: "blush",
                text: "Deadline set to Fri, Apr 17",
                time: "Mon",
              },
            ]}
          />
        </Panel>
      }
    >
      <div className="tf-in">
        <PageHeader
          crumbs={["Projects", "Pricing page refresh"]}
          title="Pricing page refresh"
          subtitle="New tiers and a comparison table for annual plans"
          actions={[
            <AvatarStack key="a" names={["Mara Ellis", "Jonah Petit", "Sun Lee", "Rui Costa"]} />,
            <Button key="s" variant="secondary" icon="share">
              Share
            </Button>,
            <Button key="p" icon="play">
              Preview
            </Button>,
          ]}
        />
      </div>
      <div
        className="tf-in"
        style={
          {
            "--d": "100ms",
          } as React.CSSProperties
        }
      >
        <Tabs items={["Overview", "Tasks", "Files", "Settings"]} />
      </div>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "1.3fr 1fr",
          } as React.CSSProperties
        }
        delay={160}
      >
        <div
          className="tf-card tf-bg-lilac"
          style={{
            minHeight: 0,
            flexDirection: "row",
            alignItems: "center",
            gap: 24,
          }}
        >
          <ProgressRing value={64} size={104} stroke={11} caption="done" />
          <div
            className="tf-col"
            style={{
              gap: 8,
            }}
          >
            <span
              className="tf-card-title"
              style={{
                padding: 0,
              }}
            >
              On track for Friday
            </span>
            <span className="tf-card-desc">
              11 of 18 tasks done. One blocker on checkout needs a decision from billing.
            </span>
            <div className="tf-row">
              <Badge tone="success">On track</Badge>
              <Badge tone="warning">1 blocker</Badge>
            </div>
          </div>
        </div>
        <div
          className="tf-grid"
          style={
            {
              "--cols": "1fr 1fr",
              "--cols-sm": "1fr 1fr",
              gap: 12,
            } as React.CSSProperties
          }
        >
          <StatTile tint="sky" value="18" label="Tasks" />
          <StatTile tint="butter" value="3" label="Days left" />
          <StatTile tint="mint" value="24" label="Comments" />
          <StatTile tint="placeholder" value="2" label="In review" />
        </div>
      </Stagger>
      <Stagger
        className="tf-grid"
        style={
          {
            "--cols": "1.3fr 1fr",
          } as React.CSSProperties
        }
        delay={320}
      >
        <Panel
          title="Milestone tasks"
          action={
            <Button size="sm" variant="soft" icon="plus">
              Add
            </Button>
          }
        >
          <Stagger
            className="tf-col"
            style={{
              gap: 4,
            }}
            delay={420}
          >
            {tasks.map((t) => (
              <div
                key={t[0]}
                className="tf-row-item"
                style={{
                  gridTemplateColumns: "auto 1fr auto",
                  cursor: "default",
                }}
              >
                <Checkbox defaultChecked={t[1]} label={t[0]} />
                <span
                  style={{
                    fontWeight: "var(--type-label-weight)",
                    fontSize: 14,
                    textDecoration: t[1] ? "line-through" : "none",
                    color: t[1] ? "var(--ink-muted)" : "var(--ink)",
                  }}
                >
                  {t[0]}
                </span>
                <Avatar name={t[2]} size={24} />
              </div>
            ))}
          </Stagger>
        </Panel>
        <Panel title="Files" subtitle="4 attachments">
          <Stagger
            className="tf-col"
            style={{
              gap: 8,
            }}
            delay={480}
          >
            <FileRow name="brief-v4.pdf" meta="2.4 MB · PDF" kind="pdf" />
            <FileRow name="pricing-tiers.fig" meta="18 MB · Design" kind="fig" />
            <FileRow name="q2-forecast.xlsx" meta="640 KB · Sheet" kind="sheet" />
          </Stagger>
        </Panel>
      </Stagger>
    </AppShell>
  );
}
export default function Preview() {
  return <App />;
}
```

## Props

### AppShell

| Prop | Type | Notes |
| --- | --- | --- |
| `active?` | `string` |  |
| `brand?` | `ReactNode` | The product name, or a node (a logo mark with the name; collapsed sidebars show it whole). |
| `tag?` | `string` |  |
| `nav?` | `NavItem[]` |  |
| `user?` | `{ name: string; email?: string }` |  |
| `onNavigate?` | `(label: string) => void` |  |
| `topExtra?` | `ReactNode` |  |
| `sheetStyle?` | `CSSProperties` |  |
| `contentKey?` | `string \| number` |  |
| `side?` | `ReactNode` |  |
| `dock?` | `boolean` |  |
| `dockItems?` | `DockProps["items"]` |  |
| `railTop?` | `RailItem[]` |  |
| `railBottom?` | `RailItem[]` |  |
| `navigation?` | `"top" \| "sidebar"` | `top` (default): navigation in the top bar, tools in the side rail. `sidebar`: a labelled sidebar on the frame, with groups, nesting and a drawer on phones. |
| `sidebarGroups?` | `SidebarGroup[]` | With `navigation="sidebar"`: groups instead of the flat `nav` list. |
| `sidebar?` | `Partial<Omit<SidebarProps, "variant" \| "groups">>` | With `navigation="sidebar"`: more sidebar props (collapsed, storageKey, shortcut, linkAs …). |
| `workspaces?` | `WorkspaceSwitcherProps` | With `navigation="sidebar"`: the workspace switcher at the top instead of the brand. |
| `userMenu?` | `(MenuItem \| "-")[]` | The account menu's entries (with `navigation="sidebar"` at the foot of the sidebar). |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Avatar

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `src?` | `string` |  |
| `size?` | `number` |  |
| `tint?` | `Tint \| "surface-sunken"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### AvatarStack

| Prop | Type | Notes |
| --- | --- | --- |
| `names` | `string[]` | Required |
| `max?` | `number` |  |
| `size?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Badge

| Prop | Type | Notes |
| --- | --- | --- |
| `tone?` | `Tone` |  |
| `dot?` | `boolean` |  |
| `live?` | `boolean` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Button

| Prop | Type | Notes |
| --- | --- | --- |
| `variant?` | `"primary" \| "secondary" \| "soft" \| "ghost" \| "danger" \| "danger-solid"` |  |
| `size?` | `"md" \| "sm"` |  |
| `icon?` | `IconName` |  |
| `iconRight?` | `IconName` |  |
| `disabled?` | `boolean` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `asChild?` | `boolean` | Render the single child element (a link, a router Link) with the button's look instead of a <button>. |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Checkbox

| Prop | Type | Notes |
| --- | --- | --- |
| `name?` | `string` |  |
| `value?` | `string` |  |
| `required?` | `boolean` |  |
| `checked?` | `boolean` |  |
| `defaultChecked?` | `boolean` |  |
| `onChange?` | `(e: ChangeEvent<HTMLInputElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `disabled?` | `boolean` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### FileRow

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `meta` | `string` | Required |
| `kind?` | `"pdf" \| "fig" \| "sheet" \| "doc" \| "zip"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### IconButton

| Prop | Type | Notes |
| --- | --- | --- |
| `icon` | `IconName` | Required |
| `label` | `string` | Accessible name and tooltip (`aria-label` and `title`); `aria-label` or `title` override them. |
| `variant?` | `"soft" \| "solid" \| "outline" \| "ghost"` |  |
| `size?` | `"sm" \| "md" \| "lg"` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `disabled?` | `boolean` |  |
| `asChild?` | `boolean` | Render the single child element (a link) with the button's look; the icon is placed inside it. |
| `children?` | `ReactNode` |  |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### PageHeader

| Prop | Type | Notes |
| --- | --- | --- |
| `crumbs?` | `(string \| Crumb)[]` | A breadcrumb above the title (see `Breadcrumb`): labels, or items with `href`/`onClick`. |
| `crumbLinkAs?` | `ElementType` | The element crumb links render as, e.g. Next.js `Link`. |
| `title` | `string` | Required |
| `subtitle?` | `string` |  |
| `actions?` | `ReactNode[]` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Panel

| Prop | Type | Notes |
| --- | --- | --- |
| `side?` | `boolean` |  |
| `title?` | `string` |  |
| `subtitle?` | `string` |  |
| `action?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `surface?` | `Surface` | `"glass"`: translucent glass over what is behind (maps, 3D, video, photos); `"solid"` opts out inside a glass `UIProvider`. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ProgressRing

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `number` | Required |
| `tint?` | `Tint \| "ink"` |  |
| `size?` | `number` |  |
| `stroke?` | `number` |  |
| `caption?` | `string` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Stagger

| Prop | Type | Notes |
| --- | --- | --- |
| `as?` | `string` |  |
| `className?` | `string` |  |
| `style?` | `CSSProperties` |  |
| `delay?` | `number` | Extra delay before the first child, in ms. |
| `reveal?` | `boolean` | Below the fold, wait until the group scrolls into view (default true). |
| `children?` | `ReactNode` |  |

### StatTile

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `ReactNode` | Required |
| `label` | `string` | Required |
| `tint?` | `Tint \| "surface" \| "placeholder"` |  |
| `delta?` | `string` |  |
| `delay?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Tabs

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `string[]` | Required |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(v: string) => void` |  |
| `block?` | `boolean` |  |
| `panels?` | `Record<string, ReactNode>` | Content per tab; renders the tab panels under the bar, wired with tab/tabpanel roles. |
| `label?` | `string` | Accessible name of the tab list. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Timeline

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `{ avatar?: string; icon?: IconName; tint?: Tint \| "plain"; text: ReactNode; time: string; }[]` | Required |
| `delay?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
