TaskList

Stacked status sections for the Tasks list view. Compose TaskCard (and optionally TaskDnd) as children.

Preview#

import {
  TaskCard,
  TaskListRoot,
  TaskSection,
  TaskSectionBadge,
  TaskSectionChevron,
  TaskSectionCount,
  TaskSectionEmpty,
  TaskSectionHeading,
  TaskSectionItems,
  TaskSectionPanel,
  TaskSectionTrigger,
} from "raft-ui";

<TaskListRoot>
  <TaskSection>
    <TaskSectionTrigger>
      <TaskSectionHeading>
        <TaskSectionBadge status="todo" />
        <TaskSectionCount />
      </TaskSectionHeading>
      <TaskSectionChevron />
    </TaskSectionTrigger>
    <TaskSectionPanel>
      <TaskSectionEmpty />
      <TaskSectionItems>
        <TaskCard />
      </TaskSectionItems>
    </TaskSectionPanel>
  </TaskSection>
</TaskListRoot>

Status sections#

Group cards by status with collapsible sections. Collapse and counts are driven by the composition.

Scroll area#

Use one ScrollAreaViewport around all status sections. The list remains a single vertical scroll surface, while TaskListRoot and sections only own their layout and collapse state.

Section empty states#

Sparse tasks leave In Review / Done / Closed empty so TaskSectionEmpty shows. List has a single empty host (no DnD idle/allowed/blocked triad).

Virtualization#

Keep one shared ScrollAreaViewport, and mount a useVirtualizer stack inside each tall section. Virtualization stays consumer-owned; TaskList does not provide it.

const viewportRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => viewportRef.current,
  getItemKey: (index) => items[index]!.id,
  estimateSize: () => 116,
  gap: 10,
  overscan: 6,
});

<ScrollArea>
  <ScrollAreaViewport ref={viewportRef}>
    <TaskSectionItems>
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((row) => (
          <div
            key={row.key}
            ref={virtualizer.measureElement}
            style={{ position: "absolute", transform: `translateY(${row.start}px)` }}
          >
            {renderItem(items[row.index]!)}
          </div>
        ))}
      </div>
    </TaskSectionItems>
  </ScrollAreaViewport>
</ScrollArea>

API#WIP

PropTypeDefaultDescription
...propsuseRender.ComponentProps<"div">List view root for TaskSection children.
PropTypeDefaultDescription
openbooleanControlled collapsed state for the section.
defaultOpenbooleanfalseInitial collapsed state for the section.
...propsCollapsible.Root.PropsList section root. Defaults to a section host via render.
PropTypeDefaultDescription
...propsCollapsible.Trigger.PropsExpands or collapses the section.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"span">Heading row for badge and count.
PropTypeDefaultDescription
status"todo" | "in-progress" | "in-review" | "done" | "closed"Task status that drives the badge surface color.
...propsuseRender.ComponentProps<"span">Section status badge.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"span">Task count in the section heading.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"span">Disclosure chevron for the section trigger.
PropTypeDefaultDescription
...propsCollapsible.Panel.PropsCollapsible panel hosting empty state and items.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"div">List of task cards inside the section.
PropTypeDefaultDescription
...propsuseRender.ComponentProps<"div">Empty-state host for a section with no tasks.