# Tasks

> The parts every task view shares — the status badge and task chip that show where a task is, the status pixels, and the shell of a task panel.

Source: https://raft-design.botiverse.build/components/tasks. Generated from the current release; where it differs from what you remember, this page is right.

These are the parts every task view shares. `TaskStatusBadge` writes a task's status as a status chip: the status's soft tint, its pixel and its name. `TaskChip` is the same chip carrying a task instead: the task marker under a message, or a task mentioned in running text. `TasksPanelRoot` and its parts are the shell of a page of tasks. The five statuses and how tasks are shown are in [Tasks](https://raft-design.botiverse.build/patterns/tasks.md); their colors are in [Color](https://raft-design.botiverse.build/foundations/color.md#statuses).

```tsx
import {
  TaskChip,
  TaskStatusBadge,
  TaskStatusIcon,
  Timeline,
  TimelineItem,
  TimelineTitle,
} from "@botiverse/raft-ui";
import type { TaskStatusId } from "@botiverse/raft-ui";

/**
 * The task-status contract, laid out as one table: five states (rows) × four
 * parts (columns) — the bare icon, the badge, the chip, and the Timeline dot —
 * so a reviewer can see that every part of one status resolves to the same
 * colour family.
 *
 * This is the surface the before/after comparison is captured from.
 */

const STATUS_LABEL: Record<string, string> = {
  todo: "Todo",
  "in-progress": "In Progress",
  "in-review": "In Review",
  done: "Done",
  closed: "Closed",
};

const TASK_STATUS_IDS: readonly TaskStatusId[] = [
  "todo",
  "in-progress",
  "in-review",
  "done",
  "closed",
];

function Part({ children }: { children: React.ReactNode }) {
  return <div className="flex min-h-12 items-center">{children}</div>;
}

export function TaskStatusComposition() {
  return (
    <div className="overflow-x-auto border-t border-l border-line-hairline">
      <table className="w-full border-collapse text-sm">
        <thead>
          <tr className="bg-layer-canvas">
            {["Status", "Icon", "Badge", "Chip", "Timeline dot"].map((heading) => (
              <th
                key={heading}
                className="border-r border-b border-line-hairline px-4 py-2 text-left font-heading font-bold"
              >
                {heading}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {TASK_STATUS_IDS.map((status) => (
            <tr key={status}>
              <td className="border-r border-b border-line-hairline px-4 py-3 font-mono text-xs">
                {status}
              </td>
              <td className="border-r border-b border-line-hairline px-4">
                <Part>
                  <TaskStatusIcon status={status} className="size-[18px]" />
                </Part>
              </td>
              <td className="border-r border-b border-line-hairline px-4">
                <Part>
                  <TaskStatusBadge status={status}>{STATUS_LABEL[status]}</TaskStatusBadge>
                </Part>
              </td>
              <td className="border-r border-b border-line-hairline px-4">
                <Part>
                  <TaskChip status={status}>
                    <TaskStatusIcon status={status} />
                    <span>#42</span>
                  </TaskChip>
                </Part>
              </td>
              <td className="border-r border-b border-line-hairline px-4">
                <Part>
                  <Timeline>
                    <TimelineItem taskStatus={status} lineType="dashed">
                      <TimelineTitle>{STATUS_LABEL[status]}</TimelineTitle>
                    </TimelineItem>
                  </Timeline>
                </Part>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

## When to use

| Situation                                             | Use                                                                               |
| ----------------------------------------------------- | --------------------------------------------------------------------------------- |
| A task's status: on a card, in properties, in details | `TaskStatusBadge`                                                                 |
| A task under a message                                | `TaskChip` in the message footer ([MessageItem](https://raft-design.botiverse.build/components/message-item.md)) |
| A task mentioned in running text                      | `TaskChip` with `variant="inline"`                                                |
| A task's status as a pixel beside a word              | `TaskStatusIcon`                                                                  |
| A page or a tab of tasks                              | `TasksPanelRoot` with its toolbar and viewport                                    |
| A task as a row on a board or in a list               | [TaskCard](https://raft-design.botiverse.build/components/task-card.md)                                             |
| How an agent is                                       | An agent state chip ([Status](https://raft-design.botiverse.build/components/status.md#agent-state-chips))             |

## Anatomy

- `TaskStatusBadge`: a `TaskChip` with the status pixel and the status name, which the caller supplies.
- `TaskChip`: the chip. `status` sets its tint. Its children are a status icon, then the task number, then the assignee.
- `TaskStatusIcon`, and `TaskTodoIcon`, `TaskInProgressIcon`, `TaskInReviewIcon`, `TaskDoneIcon`, `TaskClosedIcon`: the status pixel.
- `TasksPanelRoot`: the column of a task page.
  - `TasksPanelToolbar`: the bar on top, with a seam under it. It holds `TasksPanelToolbarNewTask`, `TasksPanelToolbarFilters` and `TasksPanelToolbarView` (Board / List), and lays them out for narrow screens. `TasksPanelToolbarGroup` and `TasksPanelToolbarLabel` are for anything else in it.
  - `TasksPanelViewport`: the scrolling content: a [TaskBoard](https://raft-design.botiverse.build/components/task-board.md) or a [TaskList](https://raft-design.botiverse.build/components/task-list.md).
  - `TasksPanelEmpty`: holds an [EmptyState](https://raft-design.botiverse.build/components/empty-state.md) when there are no tasks.
- `TaskLoadingList`, `TaskLoadingRow`: placeholder rows while tasks load.
- `taskStatusSoft`, `taskStatusStrong` and `TASK_STATUS_IDS`: the recipes for building another status surface from the same `task-status-*` tokens.

```tsx
import type { ReactNode } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import {
  SegmentedControl,
  SegmentedControlItem,
  SegmentedControlLabel,
  TasksPanelRoot,
  TasksPanelToolbar,
  TasksPanelToolbarFilters,
  TasksPanelToolbarView,
  TasksPanelViewport,
} from "@botiverse/raft-ui";
import { Columns3, LayoutList } from "lucide-react";

import {
  getTaskStatusLabel,
  moveTaskFixture,
  TASK_FIXTURES,
} from "#/modules/task-card/task-card.fixtures.ts";
import type { TaskFixture, TaskStatus } from "#/modules/task-card/task-card.fixtures.ts";
import { TasksPanelBoardView } from "../tasks-panel-board-view.tsx";
import { TasksPanelEmptyState } from "../tasks-panel-empty-state.tsx";
import { TasksPanelListView } from "../tasks-panel-list-view.tsx";
import { TasksIdentityFiltersComposition } from "./tasks-identity-filters.composition.tsx";

type TaskView = "board" | "list";

export type TasksPanelCompositionProps = {
  tasks?: readonly TaskFixture[];
  toolbarFilters?: ReactNode;
  filterTask?: (task: TaskFixture) => boolean;
  view?: TaskView;
  initialView?: TaskView;
  onViewChange?: (view: TaskView) => void;
  onTasksChange?: (tasks: readonly TaskFixture[]) => void;
  onOpenTask?: (task: TaskFixture) => void;
  blockedDropStatus?: TaskStatus;
  showChannelName?: boolean;
  /** When false, hide the toolbar Board/List control (e.g. mobile page header owns it). */
  showToolbarView?: boolean;
};

export function TasksPanelComposition({
  tasks: controlledTasks,
  toolbarFilters,
  filterTask,
  view: controlledView,
  initialView = "board",
  onViewChange,
  onTasksChange,
  onOpenTask,
  blockedDropStatus,
  showChannelName = false,
  showToolbarView = true,
}: TasksPanelCompositionProps) {
  const portalRef = useRef<HTMLDivElement>(null);
  const [localTasks, setLocalTasks] = useState<readonly TaskFixture[]>(TASK_FIXTURES);
  const [localView, setLocalView] = useState<TaskView>(initialView);
  const [collapsed, setCollapsed] = useState<Record<TaskStatus, boolean>>({
    todo: false,
    "in-progress": false,
    "in-review": false,
    done: true,
    closed: true,
  });
  const [announcement, setAnnouncement] = useState("Ready");

  const tasks = controlledTasks ?? localTasks;
  const visibleTasks = useMemo(
    () => (filterTask ? tasks.filter(filterTask) : tasks),
    [filterTask, tasks],
  );
  const view = controlledView ?? localView;
  const updateTasks = useCallback(
    (nextTasks: readonly TaskFixture[]) => {
      if (controlledTasks === undefined) setLocalTasks(nextTasks);
      onTasksChange?.(nextTasks);
    },
    [controlledTasks, onTasksChange],
  );
  const setView = useCallback(
    (nextView: TaskView) => {
      if (controlledView === undefined) setLocalView(nextView);
      onViewChange?.(nextView);
      setAnnouncement(`${nextView === "board" ? "Board" : "List"} view selected`);
    },
    [controlledView, onViewChange],
  );

  const updateStatus = useCallback(
    (taskId: string, status: TaskStatus) => {
      const task = tasks.find((item) => item.id === taskId);
      if (!task || task.status === status) return;
      updateTasks(moveTaskFixture(tasks, taskId, { status }));
      setAnnouncement(`Task #${task.taskNumber} moved to ${getTaskStatusLabel(status)}`);
    },
    [tasks, updateTasks],
  );
  const moveTask = useCallback(
    (
      taskId: string,
      target: { status: TaskStatus; taskId?: string; placement?: "before" | "after" | null },
    ) => {
      const task = tasks.find((item) => item.id === taskId);
      if (!task) return;
      updateTasks(moveTaskFixture(tasks, taskId, target));
      setAnnouncement(`Task #${task.taskNumber} moved to ${getTaskStatusLabel(target.status)}`);
    },
    [tasks, updateTasks],
  );
  const openTask = useCallback(
    (task: TaskFixture) => {
      onOpenTask?.(task);
      setAnnouncement(`Opened task #${task.taskNumber}: ${task.title}`);
    },
    [onOpenTask],
  );
  const handleCollapsedChange = useCallback((status: TaskStatus, nextOpen: boolean) => {
    setCollapsed((current) => ({ ...current, [status]: !nextOpen }));
  }, []);

  return (
    <div ref={portalRef} className="relative flex size-full min-h-0  min-w-0">
      <TasksPanelRoot aria-label="Tasks panel">
        <TasksPanelToolbar>
          <TasksPanelToolbarFilters>
            {toolbarFilters ?? <TasksIdentityFiltersComposition />}
          </TasksPanelToolbarFilters>
          {showToolbarView ? (
            <TasksPanelToolbarView>
              <SegmentedControl value={view} onValueChange={setView} aria-label="Task view">
                <SegmentedControlItem value="board">
                  <Columns3 aria-hidden />
                  <SegmentedControlLabel className="max-sm:sr-only">Board</SegmentedControlLabel>
                </SegmentedControlItem>
                <SegmentedControlItem value="list">
                  <LayoutList aria-hidden />
                  <SegmentedControlLabel className="max-sm:sr-only">List</SegmentedControlLabel>
                </SegmentedControlItem>
              </SegmentedControl>
            </TasksPanelToolbarView>
          ) : null}
        </TasksPanelToolbar>

        <TasksPanelViewport>
          {tasks.length === 0 ? (
            <TasksPanelEmptyState filtered={false} />
          ) : visibleTasks.length === 0 ? (
            <TasksPanelEmptyState filtered />
          ) : view === "board" ? (
            <TasksPanelBoardView
              tasks={visibleTasks}
              collapsed={collapsed}
              onCollapsedChange={handleCollapsedChange}
              onOpenTask={openTask}
              onStatusChange={updateStatus}
              onMoveTask={moveTask}
              portalRef={portalRef}
              blockedDropStatus={blockedDropStatus}
              showChannelName={showChannelName}
            />
          ) : (
            <TasksPanelListView
              tasks={visibleTasks}
              collapsed={collapsed}
              onCollapsedChange={handleCollapsedChange}
              onOpenTask={openTask}
              onStatusChange={updateStatus}
              portalRef={portalRef}
              showChannelName={showChannelName}
            />
          )}
        </TasksPanelViewport>
      </TasksPanelRoot>

      <span className="sr-only" aria-live="polite">
        {announcement}
      </span>
    </div>
  );
}
```

## Variants

| Variant             | Use for                                                                                   |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `variant="default"` | A chip on its own: under a message, on a card, in a details row                           |
| `variant="inline"`  | A chip in running text; it follows the size of the text around it                         |
| `size="sm"`         | A default chip in one row with small badges, such as the metadata row of an Activity item |

```tsx
import { TaskChip, TaskInProgressIcon } from "@botiverse/raft-ui";

import { MESSAGE_EXAMPLE_HREFS } from "#/modules/message-item/message-item.fixtures.ts";

export function TaskChipVariantsComposition() {
  return (
    <div className="flex flex-wrap items-start justify-center gap-6">
        <TaskChip
          status="in-progress"
          render={<a aria-label="Open task #205" href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}
        >
          <TaskInProgressIcon />
          #205
        </TaskChip>
        <TaskChip
          variant="inline"
          status="in-progress"
          render={<a aria-label="Open task #205" href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}
        >
          <TaskInProgressIcon />
          #205
        </TaskChip>
    </div>
  );
}
```

```tsx
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
  TaskChip,
  TaskClosedIcon,
  TaskDoneIcon,
  TaskInProgressIcon,
  TaskInReviewIcon,
  TaskTodoIcon,
} from "@botiverse/raft-ui";
import {
  MessageItem,
  MessageItemAvatarSlot,
  MessageItemBody,
  MessageItemContent,
  MessageItemFooter,
  MessageItemHeader,
  MessageItemSender,
  MessageItemTime,
} from "@botiverse/raft-ui";
import { MESSAGE_FIXTURE_AVATAR_SRC } from "#/modules/message-item/message-item.fixtures.ts";

export function TaskChipInFooterComposition() {
  return (
    <MessageItem>
      <MessageItemAvatarSlot>
        <Avatar size="md" type="agent">
          <AvatarImage src={MESSAGE_FIXTURE_AVATAR_SRC} alt="" />
          <AvatarFallback>AT</AvatarFallback>
        </Avatar>
      </MessageItemAvatarSlot>
      <MessageItemContent>
        <MessageItemHeader>
          <MessageItemSender>Atlas</MessageItemSender>
          <MessageItemTime>2:23 PM</MessageItemTime>
        </MessageItemHeader>
        <MessageItemBody>
          Split the ownership boundary work so the migration notes stay reviewable.
        </MessageItemBody>
        <MessageItemFooter>
          <TaskChip status="todo" title="task #16 @Robin · Todo">
            <TaskTodoIcon />
            <span className="shrink-0">#16</span>
            <span className="min-w-0 truncate">@Robin</span>
          </TaskChip>
          <TaskChip status="in-progress" title="task #205 @Noah · In Progress">
            <TaskInProgressIcon />
            <span className="shrink-0">#205</span>
            <span className="min-w-0 truncate">@Noah</span>
          </TaskChip>
          <TaskChip status="in-review" title="task #340 @Ada · In Review">
            <TaskInReviewIcon />
            <span className="shrink-0">#340</span>
            <span className="min-w-0 truncate">@Ada</span>
          </TaskChip>
          <TaskChip status="done" title="task #47 @Atlas · Done">
            <TaskDoneIcon />
            <span className="shrink-0">#47</span>
            <span className="min-w-0 truncate">@Atlas</span>
          </TaskChip>
          <TaskChip status="closed" title="task #480 · Closed">
            <TaskClosedIcon />
            <span className="shrink-0">#480</span>
          </TaskChip>
          <TaskChip status="in-progress" loading title="Resolving task #512">
            <TaskInProgressIcon />
            <span className="shrink-0">#512</span>
          </TaskChip>
        </MessageItemFooter>
      </MessageItemContent>
    </MessageItem>
  );
}
```

```tsx
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
  MarkdownParagraph,
  TaskChip,
  TaskClosedIcon,
  TaskDoneIcon,
  TaskInProgressIcon,
  TaskInReviewIcon,
  TaskTodoIcon,
} from "@botiverse/raft-ui";
import {
  MessageItem,
  MessageItemAvatarSlot,
  MessageItemBody,
  MessageItemContent,
  MessageItemHeader,
  MessageItemSender,
  MessageItemTime,
} from "@botiverse/raft-ui";
import {
  MESSAGE_FIXTURE_AVATAR_SRC,
  MESSAGE_EXAMPLE_HREFS,
} from "#/modules/message-item/message-item.fixtures.ts";

export function TaskChipInMarkdownComposition() {
  return (
    <MessageItem>
      <MessageItemAvatarSlot>
        <Avatar size="md" type="agent">
          <AvatarImage src={MESSAGE_FIXTURE_AVATAR_SRC} alt="" />
          <AvatarFallback>AT</AvatarFallback>
        </Avatar>
      </MessageItemAvatarSlot>
      <MessageItemContent>
        <MessageItemHeader>
          <MessageItemSender>Atlas</MessageItemSender>
          <MessageItemTime>2:22 PM</MessageItemTime>
        </MessageItemHeader>
        <MessageItemBody>
          <MarkdownParagraph>
            Pick up{" "}
            <TaskChip
              variant="inline"
              status="todo"
              render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}
            >
              <TaskTodoIcon />
              #16
            </TaskChip>
            , continue{" "}
            <TaskChip
              variant="inline"
              status="in-progress"
              render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}
            >
              <TaskInProgressIcon />
              #205
            </TaskChip>
            , then close{" "}
            <TaskChip
              variant="inline"
              status="done"
              render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}
            >
              <TaskDoneIcon />
              #47
            </TaskChip>{" "}
            and archive{" "}
            <TaskChip
              variant="inline"
              status="closed"
              render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}
            >
              <TaskClosedIcon />
              #480
            </TaskChip>
            . Resolving{" "}
            <TaskChip variant="inline" status="in-review" loading title="Resolving task #340">
              <TaskInReviewIcon />
              #340
            </TaskChip>
            .
          </MarkdownParagraph>
        </MessageItemBody>
      </MessageItemContent>
    </MessageItem>
  );
}
```

```tsx
import { Badge, TaskChip, TaskInProgressIcon } from "@botiverse/raft-ui";

export function TaskChipSizesComposition() {
  return (
    <div className="flex flex-col items-center gap-4">
        <TaskChip status="in-progress">
          <TaskInProgressIcon />
          #746 @Martin
        </TaskChip>
      <div className="flex items-center gap-1.5">
        <TaskChip status="in-progress" size="sm">
          <TaskInProgressIcon />
          #746 @Martin
        </TaskChip>
        <Badge appearance="outline" variant="muted">
          12 replies
        </Badge>
        <Badge variant="accent">1 new</Badge>
      </div>
    </div>
  );
}
```

## States

- **Loading**: `loading` dims a `TaskChip` and ignores the pointer while the task is resolved.
- **The panel is loading**: `TaskLoadingList`, the same size as the cards it stands for ([Feedback](https://raft-design.botiverse.build/patterns/feedback.md#empty-loading-and-failed)).
- **No tasks**: `TasksPanelEmpty`, with one empty state when there are no tasks and another when the filters match none.
- **Failed to load**: an error at the panel's scope, with Retry.

```tsx
import {
  TaskChip,
  TaskClosedIcon,
  TaskDoneIcon,
  TaskInProgressIcon,
  TaskInReviewIcon,
  TaskTodoIcon,
} from "@botiverse/raft-ui";
import { MESSAGE_EXAMPLE_HREFS } from "#/modules/message-item/message-item.fixtures.ts";

export function TaskChipStatusesComposition() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
        <TaskChip status="todo" render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}>
          <TaskTodoIcon />
          #16
        </TaskChip>
        <TaskChip status="in-progress" render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}>
          <TaskInProgressIcon />
          #205
        </TaskChip>
        <TaskChip status="in-review" render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}>
          <TaskInReviewIcon />
          #340
        </TaskChip>
        <TaskChip status="done" render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}>
          <TaskDoneIcon />
          #47
        </TaskChip>
        <TaskChip status="closed" render={<a href={MESSAGE_EXAMPLE_HREFS.stateModelTask} />}>
          <TaskClosedIcon />
          #480
        </TaskChip>
        <TaskChip status="in-progress" loading title="Resolving task #512">
          <TaskInProgressIcon />
          #512
        </TaskChip>
    </div>
  );
}
```

```tsx
import {
  Button,
  EmptyState,
  EmptyStateDescription,
  EmptyStateIcon,
  EmptyStateTitle,
  TasksPanelRoot,
  TasksPanelEmpty,
  TasksPanelToolbar,
  TasksPanelToolbarFilters,
  TasksPanelToolbarLabel,
  TasksPanelViewport,
} from "@botiverse/raft-ui";
import { CheckSquare, ChevronDown, User } from "lucide-react";

export function TasksPanelEmptyContent() {
  return (
    <TasksPanelRoot>
      <TasksPanelViewport>
        <TaskEmptyState filtered={false} />
      </TasksPanelViewport>
    </TasksPanelRoot>
  );
}

export function TasksPanelFilteredEmptyContent() {
  return (
    <TasksPanelRoot>
      <TasksPanelToolbar>
        <TasksPanelToolbarFilters>
          <TasksPanelToolbarLabel aria-hidden>
            <User aria-hidden />
            Assignee
            <ChevronDown aria-hidden />
          </TasksPanelToolbarLabel>
          <Button variant="ghost" size="md">
            Clear
          </Button>
        </TasksPanelToolbarFilters>
      </TasksPanelToolbar>
      <TasksPanelViewport>
        <TaskEmptyState filtered />
      </TasksPanelViewport>
    </TasksPanelRoot>
  );
}

function TaskEmptyState({ filtered }: { filtered: boolean }) {
  return (
    <TasksPanelEmpty render={<EmptyState />}>
      <EmptyStateIcon>
        <CheckSquare aria-hidden />
      </EmptyStateIcon>
      <EmptyStateTitle>{filtered ? "No tasks match this filter" : "No tasks yet"}</EmptyStateTitle>
      <EmptyStateDescription>
        {filtered
          ? "Try a different combination or clear the current selection."
          : "Create one with the New Task button."}
      </EmptyStateDescription>
    </TasksPanelEmpty>
  );
}
```

```tsx
import {
  Banner,
  BannerAction,
  BannerDescription,
  Button,
  TasksPanelRoot,
  TasksPanelViewport,
} from "@botiverse/raft-ui";
import { RotateCcw } from "lucide-react";

function reloadPage() {
  window.location.reload();
}

export function TasksErrorComposition() {
  return (
    <div className="flex min-h-80 w-full">
      <TasksPanelRoot>
        <TasksPanelViewport>
          <Banner status="warning">
            <RotateCcw aria-hidden />
            <BannerDescription>Couldn&apos;t load tasks.</BannerDescription>
            <BannerAction>
              <Button variant="outline" size="md" onClick={reloadPage}>
                Retry
              </Button>
            </BannerAction>
          </Banner>
        </TasksPanelViewport>
      </TasksPanelRoot>
    </div>
  );
}
```

## Behavior

- Make a status chip on a task card a button that opens the list of statuses, with a down arrow after the name ([TaskCard](https://raft-design.botiverse.build/components/task-card.md)). The other ways to change a status are in [Tasks](https://raft-design.botiverse.build/patterns/tasks.md#rules).
- Make the task marker under a message open the task.

## Content

- Spell the statuses Todo, In Progress, In Review, Done and Closed. `TaskStatusBadge` has no built-in label: the caller passes it, and translates it.
- Set the task number in Space Mono: `#25` ([Typography](https://raft-design.botiverse.build/foundations/typography.md#usage)).
- In a task marker, write the number, then the assignee: `#25` @Bugen.

## Accessibility

None of these parts wraps a Base UI primitive; `TaskChip` renders a `<span>` unless `render` makes it a link or a button.

- The status pixel is hidden from assistive technology. Always pass the status name, or keep the task number next to it.
- Render a task marker that opens the task as an `<a>` or a `<button>`, and give it an `aria-label` that says what it opens ("Open task #25") when its text doesn't. A `title` is not an accessible name.

## Rules

- Build any new surface that shows a task status from `taskStatusSoft` and `taskStatusStrong`, so a status has one color everywhere.
- Don't give a chip a border: the tint is a highlight, not a sticker ([Status](https://raft-design.botiverse.build/components/status.md#rules)).
- A chip splits its padding evenly between its two ends: 7px each, 5px when `sm`, 4.5px inline, the same total as before. The status pixel is drawn to its edges, so the space before it is all padding; with 2px less on that end, the pixel sat 6px from the edge and the last letter 8.5px ([Alignment](https://raft-design.botiverse.build/foundations/alignment.md#chips)).

## API

### TasksPanelEmpty

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelToolbar

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelToolbarFilters

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelToolbarGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelToolbarLabel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelToolbarNewTask

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelToolbarView

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TasksPanelViewport

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TaskLoadingList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TaskLoadingRow

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TaskChip

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `false` |  |
| `size` | `TaskChipSize` | `"default"` | `sm` lines the chip up with an `sm` Badge in the same row. |
| `status` (required) | `TaskStatusId` |  |  |
| `variant` | `TaskChipVariant` | `"default"` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### TaskClosedIcon

Also takes the attributes of its element.

### TaskDoneIcon

Also takes the attributes of its element.

### TaskInProgressIcon

Also takes the attributes of its element.

### TaskInReviewIcon

Also takes the attributes of its element.

### TaskStatusIcon

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` (required) | `TaskStatusId` |  |  |

Also takes the attributes of its element.

### TaskTodoIcon

Also takes the attributes of its element.

### TaskStatusBadge

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  | Badge text. The caller owns the copy (and its localization); Raft never hardcodes a status label here. |
| `size` | `TaskChipSize` | `"default"` | Same as `TaskChip`'s `size`: `sm` lines up with an `sm` Badge. |
| `status` (required) | `TaskStatusId` |  |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.5.58 (Fixed): `TaskChip` / `TaskStatusBadge` `size="sm"` take the `sm` Badge's own box (padding, border, line height), so it no longer drifts from the Badge beside it.
- 0.5.55 (Fixed): `TaskChip` / `TaskStatusBadge` `size="sm"` now also match the `sm` Badge's font weight and letter spacing (Elegant 500 / 0.22px, Brutal 700), so the chip no longer reads as a different font beside the Badges in slock's Activity card row. The default size and the inline variant are unchanged.
- 0.5.54 (Added): `TaskChip` and `TaskStatusBadge` take `size="sm"`, which gives the chip the `sm` Badge's text size and height (Elegant 11px / 18px, Brutal 10px / 20px; icon 10px) for a chip that shares a row with Badges, such as slock's Activity card metadata row. The default size and the inline variant are unchanged.
- 0.5.41 (Changed): `TaskStatusBadge` now renders through `TaskChip` and has no look of its own: a status badge in a task panel and the task chip under a message share one shape, size, border and colour and differ only in content (status icon + the caller's label). Visible change: in Elegant the badge goes from 12px to the chip's 13px with a hairline border and a 19px height; in Brutal from bold 12px at 20px tall to the chip's medium 13px at about 18px tall, so rows that carry it are about 2px shorter. `data-slot="task-status-badge"` and `data-status` are kept; no API change.
- 0.5.39 (Fixed): Brutal `TaskStatusBadge` draws a 1px black keyline instead of 2px, matching `TaskChip` and `Badge`; `py-px` keeps it exactly 20px tall, so surrounding layout does not move (Elegant unchanged).
- 0.5.38 (Fixed): `TaskChip`'s `inline` variant no longer paints its own neutral fill and transparent border over the status styling: task references inside message bodies take the same per-status fill, border, text and icon colours as the task chip under a message (Elegant; Brutal unchanged; font size and baseline rules untouched).
- 0.5.33 (Changed): `Steps` markers take the task-status colours shared with `TaskChip` (`--task-status-*`), so each status has its own colour: finished = Done (green), in progress = In Progress (blue), failed = danger (red), and waiting is neutral grey (the Closed ink). Brutal: solid status fill, ink outline and ink text, as the Brutal `TaskChip`. Elegant: the status's soft wash, strong ink and a 35% hairline, as the Elegant `TaskChip`; waiting steps follow naive-ui with no fill and the ring, number and title in the disabled grey.
- 0.5.31 (Fixed): `TaskStatusBadge` in Brutal is a square, hard-edged tag again (no radius, 2px black border, bold, 20px tall), matching the Brutal `TaskChip` and `Badge`. 0.5.30 rendered it as a rounded pill. Elegant is unchanged.
