# XWUITreeGrid

Virtualized, lazy-loading tree grid: hierarchical rows over grid columns, with
tri-state checkboxes, drag re-parenting, and full ARIA `treegrid` semantics.

`basic/XWUITreeTable` remains the free, simple, fully-rendered tree table.
XWUITreeGrid is the separate deep component, exactly as `enterprise/XWUIDataGrid`
is separate from `basic/XWUITable`. Neither replaces the other.

## Row virtualization

`conf_comp.virtualize` (`true | false | 'auto'`, default `'auto'`) decides
whether the grid paints a window or the whole tree.

- `'auto'` virtualizes once the **visible** row count passes
  `virtualizeThreshold` (default 200). Visible, not total: a 50k-node tree with
  every branch collapsed paints 12 rows, and expansion is what makes a tree deep.
- The virtual path renders only the visible window plus `overscanRows`
  (default 6) at each edge, into pooled row nodes re-bound on scroll - 50k rows
  cost the same DOM as 50.
- `rowHeight` (default 32) drives the row axis; `viewportHeight` (default 420) is
  the scroll-viewport height used until the element has been laid out.

There is one paint path. Virtualized and non-virtualized rendering differ only by
the `fixed` flag on `computeViewportWindow` (paint the whole axis) and by whether
the viewport is height-capped, so crossing the threshold - which a tree does
every time somebody opens a branch - never rebuilds the DOM.

```ts
new XWUITreeGrid(host, { columns, nodes }, {
  virtualize: 'auto',   // default
  rowHeight: 32,
  overscanRows: 6,
});
```

## Lazy children

A node whose `children` is the string `'lazy'` (or whose `hasChildren` is `true`
with nothing materialized) fetches on first expand through
`data.loadChildren(node)`.

```ts
new XWUITreeGrid(host, {
  columns,
  nodes: [{ id: 'root', cells: { name: 'root' }, children: 'lazy' }],
  loadChildren: async (node) => fetch(`/api/tree/${node.id}`).then((r) => r.json()),
});
```

While the fetch is in flight the branch shows a spinner row; a rejection shows an
error row carrying the message and a **Retry** button. Each node is fetched at
most once per data generation: an expand while a fetch is in flight rides the
same promise, and a collapse followed by a re-expand re-uses what landed.

Every load emits `load` with `phase: 'start'`, then exactly one of
`phase: 'done'` (carrying `children`) or `phase: 'error'` (carrying `error`).

## Tri-state checkboxes

`conf_comp.checkable` turns on a checkbox per row; `conf_comp.checkPropagate`
decides how a toggle travels.

| Value | Effect |
|---|---|
| `'both'` (default) | The subtree follows the toggle, and every ancestor is re-derived: all children checked becomes checked, some becomes indeterminate. |
| `'down'` | Subtree only. Use when a parent carries its own meaning. |
| `'none'` | The toggled row only. Flat multi-select. |

Check state is maintained eagerly in two id sets rather than derived per paint,
because a scrolling window asks for a row's state thousands of times a second.
Children that arrive from a lazy load under an already-checked parent inherit its
state.

## Drag to re-parent / reorder

`conf_comp.draggable` turns on pointer drag. The top and bottom quarters of a row
reorder among siblings (`'before'` / `'after'`); the middle half re-parents
(`'inside'`). A drop indicator tracks the resolved position and turns invalid
when the drop is refused.

`data.canDrop(drag, drop, position)` vetoes a drop by returning `false`.
Structural impossibilities are refused before it is consulted: a node on itself,
a node into its own subtree, and anything inside a lazy node whose children have
not loaded (the insertion index would be a lie, and the pending fetch would
overwrite it).

A committed drop emits `move` with `from` / `to` slots (`{ parent, index }`).
`moveNode(dragId, targetId, position)` does the same thing programmatically and
returns `false` when the move was refused.

## Accessibility

The root is `role="treegrid"` with `aria-colcount` / `aria-rowcount`; rows are
`role="row"` with `aria-level`, `aria-expanded` (expandable rows only),
`aria-setsize`, `aria-posinset`, `aria-selected` and `aria-rowindex`; cells are
`role="gridcell"`. The grid is ONE tab stop and moves a roving active row through
`aria-activedescendant` - a tab stop per row is unusable at 50k rows.

| Key | Action |
|---|---|
| Down / Up | Move the active row |
| Right | Expand a collapsed row, else step to its first child |
| Left | Collapse an expanded row, else step to its parent |
| Home / End | First / last row |
| Enter | Select the active row |
| Space | Toggle its checkbox when `checkable`, else toggle expansion |

## Safety

The row model is `_shared/tree-flatten.ts`, so cyclic input terminates (a node
re-appearing on its own ancestor path is painted once, marked, and never
descended into) and `conf_comp.maxDepth` (default 200) bounds pathological depth.
Guarded rows carry `is-guarded`, so "we stopped here" is visibly different from
"no children".

## Events

`expand`, `collapse`, `check`, `move`, `load`, `select` - plus the framework's
`change`, batched to one emission per gesture.

## Lazy Loading

A tree whose branches are fetched on demand.

```example
file: examples/LazyLoading.ts
html: examples/LazyLoading.html
title: Lazy Loading
```

## Virtualized Tree

A wide, fully expanded tree that paints only its visible window.

```example
file: examples/VirtualizedTree.ts
html: examples/VirtualizedTree.html
title: Virtualized Tree
```

## Checkable Filesystem

A file tree with tri-state checkboxes and drag re-parenting.

```example
file: examples/CheckableFilesystem.ts
html: examples/CheckableFilesystem.html
title: Checkable Filesystem
```
