Skip to content
ilinxa/pro-ui

Task Tree

alphav0.4.0

Hierarchical task outline with multi-select, bulk operations, search and filter toolbar, dual drag-and-drop, and virtualization.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-20Author: ilinxa

Context

Task Tree is the lightweight cousin to task-card — same fixed TaskItem schema (cross-procomp type-only dep), thin two-line row instead of the time-aware card chrome. Use it for sub-issue lists, side-panel outlines, hierarchical task pickers, and bulk-management screens. Clicking a row opens task-card's edit popup (consumer-owned or via the TaskTreeWithEditor convenience wrapper). Shared DnD payload (application/x-ilinxa-task+json) lets drags cross between task-tree and task-card in both directions on pointer; touch DnD is internal-only by design. v0.1 ships feature-complete (no scheduled v0.2/v0.3 — see procomp plan).

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/task-tree

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/task-tree-fixtures

Preview

1 — Default tree

Out-of-the-box behaviour: toolbar (search + sort + filter + bulk), recursive children, dot status indicator, click to select, Cmd-A to select all visible, Cmd/Ctrl-click to toggle, Shift-click to range-select, drag-from-grip to reorder, Delete to remove the focused row.

Perf investigation — list renderAdaAD
Blocked on profiling access; ticket #482 escalated to platform team.
Ship Q3 planAdaAD
Outline goals, OKRs, and risks for the team review. Coordinate with PM + design for the kickoff slot.
Draft outlineAdaAD
Document the 4 themes and the ask for each team.
Review with leadsBoBO
Schedule 30-min syncs with each lead; collect feedback by Friday.
Design syncCoCO
Postponed until the brand refresh lands.
Engineering syncBoBO
Cover platform + product engineering ROCEs.
Triage bug backlogDeeDE
Audit the inbound bug queue (47 open); merge dupes; assign owners; promote 3 to P1.

2 — With editor (TaskTreeWithEditor)

Convenience wrapper. Clicking a row opens a Dialog containing the matching TaskCard in editable mode; live-saves propagate back into the tree.

Perf investigation — list renderAdaAD
Blocked on profiling access; ticket #482 escalated to platform team.
Ship Q3 planAdaAD
Outline goals, OKRs, and risks for the team review. Coordinate with PM + design for the kickoff slot.
Draft outlineAdaAD
Document the 4 themes and the ask for each team.
Review with leadsBoBO
Schedule 30-min syncs with each lead; collect feedback by Friday.
Design syncCoCO
Postponed until the brand refresh lands.
Engineering syncBoBO
Cover platform + product engineering ROCEs.
Triage bug backlogDeeDE
Audit the inbound bug queue (47 open); merge dupes; assign owners; promote 3 to P1.

3 — Strip status indicator + larger indent

The status indicator can render as a left-edge color strip (variant=strip) instead of the default dot. Combined with a wider indent for hierarchical scanning.

Perf investigation — list renderAdaAD
Blocked on profiling access; ticket #482 escalated to platform team.
Ship Q3 planAdaAD
Outline goals, OKRs, and risks for the team review. Coordinate with PM + design for the kickoff slot.
Draft outlineAdaAD
Document the 4 themes and the ask for each team.
Review with leadsBoBO
Schedule 30-min syncs with each lead; collect feedback by Friday.
Design syncCoCO
Postponed until the brand refresh lands.
Engineering syncBoBO
Cover platform + product engineering ROCEs.
Triage bug backlogDeeDE
Audit the inbound bug queue (47 open); merge dupes; assign owners; promote 3 to P1.

4 — Filter mode: hide (VSCode-style)

When filterMode='hide', non-matching rows are omitted entirely; ancestors-of-match still render so the result keeps tree context. Try the search input.

Perf investigation — list renderAdaAD
Blocked on profiling access; ticket #482 escalated to platform team.
Ship Q3 planAdaAD
Outline goals, OKRs, and risks for the team review. Coordinate with PM + design for the kickoff slot.
Draft outlineAdaAD
Document the 4 themes and the ask for each team.
Review with leadsBoBO
Schedule 30-min syncs with each lead; collect feedback by Friday.
Design syncCoCO
Postponed until the brand refresh lands.
Engineering syncBoBO
Cover platform + product engineering ROCEs.
Triage bug backlogDeeDE
Audit the inbound bug queue (47 open); merge dupes; assign owners; promote 3 to P1.

5 — Controlled + onChange logger

The tree is controlled via `value` + `onChange`. Every mutation routes through the consumer's reducer. Watch the live log for each event's `reason` field.

Perf investigation — list renderAdaAD
Blocked on profiling access; ticket #482 escalated to platform team.
Ship Q3 planAdaAD
Outline goals, OKRs, and risks for the team review. Coordinate with PM + design for the kickoff slot.
Draft outlineAdaAD
Document the 4 themes and the ask for each team.
Review with leadsBoBO
Schedule 30-min syncs with each lead; collect feedback by Friday.
Design syncCoCO
Postponed until the brand refresh lands.
Engineering syncBoBO
Cover platform + product engineering ROCEs.
Triage bug backlogDeeDE
Audit the inbound bug queue (47 open); merge dupes; assign owners; promote 3 to P1.
Event log
Interact with the tree to see events here.

6 — Imperative handle

Programmatic access via ref. Buttons drive the tree from outside its UI.

Perf investigation — list renderAdaAD
Blocked on profiling access; ticket #482 escalated to platform team.
Ship Q3 planAdaAD
Outline goals, OKRs, and risks for the team review. Coordinate with PM + design for the kickoff slot.
Draft outlineAdaAD
Document the 4 themes and the ask for each team.
Review with leadsBoBO
Schedule 30-min syncs with each lead; collect feedback by Friday.
Design syncCoCO
Postponed until the brand refresh lands.
Engineering syncBoBO
Cover platform + product engineering ROCEs.
Triage bug backlogDeeDE
Audit the inbound bug queue (47 open); merge dupes; assign owners; promote 3 to P1.

7 — Custom row renderer

Slot prop replaces the default row paint while keeping all DnD + click + drop-indicator wiring intact. `defaultRender` is available if you want to wrap rather than replace.

#0Perf investigation — list renderAda
#0Ship Q3 planAda
#1Draft outlineAda
#1Review with leadsBo
#2Design syncCo
#2Engineering syncBo
#0Triage bug backlogDee

Demo source

demo.tsxtsx

Usage

When to use

TaskTree is the lightweight sibling to @ilinxa/task-card. Same fixed TaskItem schema, but renders a thin two-line row (bold name + truncated description) instead of the time-driven card chrome.

  • Sub-issue / outline lists where dozens to hundreds of rows must scan quickly.
  • Side panels next to a primary editor (file-tree-style layout).
  • Hierarchical task pickers, often with a rich editor opening on row click.
  • Bulk management screens (multi-select + bulk-toggle / bulk-remove).

For time-aware urgency coloring or the full edit popup, use task-card. For kanban boards, compose @ilinxa/kanban-board with taskCardKanbanRenderer.

Quick start

import { TaskTree } from "@ilinxa/task-tree";
import type { TaskItem } from "@ilinxa/task-card";

const items: TaskItem[] = [
  {
    id: "t-1",
    name: "Ship Q3 plan",
    status: "in-progress",
    active: true,
    setAt: "2026-05-18T09:00:00Z",
    children: [
      { id: "t-1a", name: "Draft outline", status: "done", active: true, setAt: "2026-05-18T09:00:00Z" },
    ],
  },
];

<TaskTree
  defaultValue={items}
  statusOptions={[
    { value: "todo", label: "To do", variant: "outline" },
    { value: "in-progress", label: "In progress", variant: "secondary" },
    { value: "done", label: "Done", variant: "default" },
  ]}
  onChange={({ items, reason }) => save(items)}
/>

Controlled vs uncontrolled

Both modes work; the three-defenses pattern protects controlled consumers from echo storms and mid-drag setState races (microtask-defer + structural resync guard + drag-active notification suppression).

// Uncontrolled
<TaskTree defaultValue={items} onChange={({ items }) => save(items)} />

// Controlled — value wins, defaultValue ignored
<TaskTree value={items} onChange={({ items }) => setItems(items)} />

With editor: TaskTreeWithEditor

Pair the tree with a Dialog-mounted TaskCard in one line. Clicking a row opens the matching card editable; live-saves propagate back into the tree (Q-P1 auto-persistence).

import { TaskTreeWithEditor } from "@ilinxa/task-tree";

<TaskTreeWithEditor
  defaultValue={items}
  statusOptions={statusOptions}
  onChange={({ items }) => save(items)}
/>

For stricter integrations (confirm dialog before edit, custom editor surface) compose <TaskTree> + your own dialog using the onItemClick callback.

Imperative handle (26 methods)

const ref = useRef<TaskTreeHandle>(null);

// Tree state
ref.current?.getValue();
ref.current?.setValue(newItems);

// Item ops
ref.current?.addItem(item, { parentId: "t-1", index: 0 });
ref.current?.addChild("t-1", item);
ref.current?.removeItem("t-1a");
ref.current?.removeItems(["t-1a", "t-1b"]);
ref.current?.toggleActive("t-1", false);
ref.current?.toggleActiveBulk(["t-1", "t-2"], true);

// Focus + lookup
ref.current?.focusItem("t-1");
ref.current?.getItemById("t-1");

// Collapse
ref.current?.expandItem("t-1");
ref.current?.collapseItem("t-1");
ref.current?.toggleCollapse("t-1");
ref.current?.expandAll();
ref.current?.collapseAll();
ref.current?.isCollapsed("t-1");

// Selection
ref.current?.selectItem("t-1");
ref.current?.deselectItem("t-1");
ref.current?.selectRange("t-1", "t-3");
ref.current?.selectAll(); // visible only
ref.current?.clearSelection();
ref.current?.getSelectedIds();

// Toolbar state
ref.current?.setQuery("review");
ref.current?.setSort({ kind: "name", direction: "asc" });
ref.current?.setFilter({ statuses: ["done"] });
ref.current?.clearAllFilters();

Headless mode: useTaskTreeState

The same engine that powers <TaskTree> is also a hook. Drive your own toolbar / row layout / external state manager off the returned value, or feed it back into the default shell via the state prop.

import { useTaskTreeState, TaskTree } from "@ilinxa/task-tree";

function MyTree() {
  const state = useTaskTreeState({
    defaultValue: items,
    onChange: ({ items }) => save(items),
  });

  // Drive a custom search input:
  // <input value={state.query} onChange={(e) => state.setQuery(e.target.value)} />

  // OR feed back into the default shell:
  return <TaskTree state={state} />;
}

Slot props (8)

  • renderRow — full row paint; receives defaultRender.
  • renderName / renderDescription / renderPerson — per-field overrides.
  • renderStatusIndicator — receives the matched statusOption.
  • renderToolbar — wraps or replaces the toolbar; receives defaultToolbar + state.
  • renderEmptyState — replaces the default placeholder; receives hasFilter.
  • renderDragOverlay — replaces the cursor-follow visual; receives the dragged item.

Keyboard map

  • ↑ / ↓ — previous / next visible row
  • → — expand collapsed row OR move to first child
  • ← — collapse expanded row OR move to parent
  • Home / End — first / last visible row
  • Space — toggle active state of focused row
  • Enter — select + fire onItemClick
  • Delete / Backspace — remove focused row
  • Cmd/Ctrl + A — select all visible
  • Cmd/Ctrl + Click — toggle row in selection
  • Shift + Click — range select from anchor
  • Escape — clear selection

Features

  • Two-line row: chevron + status-indicator + checkbox + bold name + person label (top); thin truncated description (bottom)
  • Per-row collapsibility (chevron); UI-only state (collapsedIds), not in TaskItem; default expanded
  • Recursive children with infinite nesting
  • Multi-select: Shift-click range + Cmd/Ctrl-click toggle + Cmd-A select all; bulk-toggle-active / bulk-remove / bulk-edit callbacks
  • Default toolbar with search (200ms debounce) + sort (5 kinds: name/setAt/expireAt/status + custom) + filter (status/person/active)
  • Filter mode: 'fade' (dim non-matches) or 'hide' (omit but ancestors-of-match render — VSCode style)
  • Dual DnD: @dnd-kit (Mouse + Touch + Keyboard sensors) for internal drag; native HTML5 dataTransfer for cross-procomp drag with task-card
  • Edge-zone drops: top 25% / middle 50% / bottom 25% (capped 8px); top/bottom = sibling adjacent; middle = reparent as last child + auto-expand target
  • Circular-drop prevention (hit-test ban; onPermissionDenied fires with reason 'circular-drop')
  • Virtualization via @tanstack/react-virtual; auto-enables at ≥200 total items; suspends during drag
  • Permission matrix mirroring task-card (the `permissions` prop: default / byLevel / byItem with inherit cascade, + onPermissionDenied) gating 6 actions (edit / toggleActive / drag / dropAsSibling / dropIntoChildren / remove) — honored on BOTH the keyboard AND mouse/DnD paths (grip, active checkbox, drop targets, root-create)
  • 8 slot props: renderRow / renderName / renderDescription / renderPerson / renderStatusIndicator / renderToolbar / renderEmptyState / renderDragOverlay (slot wins over prop variant)
  • Headless useTaskTreeState hook — superset of TaskTreeHandle plus live state values + dispatch escape hatch
  • Controlled (value + onChange) and uncontrolled (defaultValue) modes; controlled mode uses the three-defenses pattern (microtask-defer + full-field resync guard + suppress mid-drag onChange)
  • 29-method imperative handle: tree state / item ops / single + bulk active-toggle + remove / focus / collapse / selection / query/sort/filter + v0.3 copy/cut/paste
  • v0.3 cross-surface clipboard: copy/cut/paste TaskItems through the shared `ilinxa/task` envelope (task-card/lib/clipboard) — ⌘/Ctrl+C·X·V (document-level, gated on focus + skipped over inputs, operates on the selection or focused row) + imperative copyItems/cutItems/pasteItems; paste re-ids each subtree under the focused row; interops with card-tree / gantt / calendar
  • v0.3 priorityOptions prop — threaded to the TaskTreeWithEditor edit card (parity with card-tree / gantt / calendar)
  • 17 object-args events (post-F-cross-12 convention)
  • Full WAI-ARIA tree pattern: role=tree + role=treeitem + aria-level + aria-expanded + aria-selected; arrow nav + Home/End + Space + Enter + Delete/Backspace + Cmd-A + Escape
  • Companion: <TaskTreeWithEditor> convenience export wires task-card edit popup inside a Dialog automatically
  • Toolbar '+ New' button (createItem factory + statusOptions[0] fallback); gated behind editable + !readOnly + the matrix's level-0 addChildren rule. Wrapper opens the edit panel on a pending item; commit deferred until Submit (onCreateRequest hook)
  • Keyboard Space + Delete honor the permissions matrix + item.locked + readOnly; fire onPermissionDenied on denial (F-perm closed)

Tags

task-treetodotreeoutlinetask-listhierarchicalmulti-selectdrag-and-dropvirtualization

Dependencies

shadcn primitives: checkbox, avatar, button, input, select, popover, badge, separator, dialog
npm peer deps: lucide-react@^1.11.0, @tanstack/react-virtual@^3.13.24, @dnd-kit/core@^6.3.1
internal: task-card