Elements

The data-TREE page — a hierarchical lister: lazy children, toggles at either end, inline groups
Element
Category
Parent
Children

The rules — the data-tree page

  • Children are real rows. children: { has, load } on the lister: has(item) says whether a row can open (a field name or a function), load(item) answers its child rows — return the g.fetch promise of your list endpoint with the parent id in data; the rows pass through map and render like the roots and get the checkbox, the row actions, refreshItem, the details box and the keyboard.
  • Outside pagination. Page size counts ROOTS; a root may reveal any number of descendants. The server paginates roots only and answers a parent id with the whole child set.
  • The approved row (Andrei, Sep 2026). The selection checkbox comes FIRST and never indents — one vertical line down the list whatever the depth. The caret sits next to the code and indents with it; it opens the CHILDREN (childrenToggle: 'start', the default). Only the caret and the first cell indent — the lib pads .lister-cols > :first-child by --lister-indent, the row keeps its own padding, so the name, the parent and every other column hold their place at every level (an indent on the row would stagger them down the page). The step is --lister-level-step on the root: 1.5rem, halved below md so a four-level tree stays readable at 390px; override it inline.
  • THE STANDARD ROW (lister.md "The standard row"; the reference: the data list). No details toggle on the row: rowClick: 'expand' — a click anywhere on the card outside controls opens the details (links, buttons, inputs, editable values and a text selection never trigger it) — plus a "Details" item FIRST in the row's dropdown, above a divider, marked lister-expand (the lib toggles the row on it, no handler) for the keyboard and assistive road. The open details are an INSET panel (lister-more-inset on the root: bordered, on the secondary surface) so an open row stands out instead of sinking into the list; they show the hidden columns first, then renderMore's own fields in the row g-3 small grid (label .text-body-secondary over value).
  • FIT columns (lister.md "Fit columns"): every cell declares its share + readable minimum (lister-col-w / lister-col-min), the hideable ones a lister-col-priority + lister-col="Label"; the element cell (code + name) never hides. Open the filter panel or narrow the window: Parent, then Children, then Category step into the details — by the LIST's width, not the viewport's. On a phone the row actions go compact by themselves (a menu button + the details caret).
  • The shell is THE STANDARD SHELL (lister.md "Page markup"; the data list): the toolbar inside .lister-main (the Filters toggle + the bulk Actions left, New right — no toolbar Select all), the panel as the Filters + Display cards (Display holds "Roots per page"), the list card with the header (its select-all checkbox first — it takes every visible element row, children included, never a group row), and the count + the pager on the .lister-footer line.
  • Toggle positions are an ALTERNATIVE, not the standard. The standard row above has only the tree caret at the start. A page that needs a visible details toggle may still place it: the tree toggle ([lister-children]) and the details toggle ([lister-expand]) each sit at the row START (the lib-injected bare chevron — right closed, down open) or at the row END (the page authors the round .btn-contrast caret — down closed, up open); one may take the start (lister.md "Toggle positions"). The second lister below shows that alternative: details at the start, children at the end.
  • Codes are never clipped. A .code value wraps between its segments only (g.format.code() plants the breaks) — shrink the window to 390px and open three levels.
  • Every level groups on its own. The category rows are synthetic rows the page's map inserts (rowClass gives them lister-group, selectable keeps the checkbox off them); children go through the same map, so a node's children group among themselves. An element appears exactly once: under its parent, inside its own category group.
  • Memory. Open nodes and open details survive reload() by idKey (nested nodes re-open too); page/search/filter changes start closed. refreshChildren(id) re-fetches ONE node's children after a create/move under it.
  • Flat on search. A search may match a row whose ancestors do not: the endpoint answers a flat list from every level and has says no while a search is active.
  • Inline cell editing, per field. editable: { code: { type: 'text', save }, category: { type: 'selekt', options }, locked: { type: 'boolean', save } } — the PROJECT opts each field in with the input type it needs; in render the value carries lister-edit="code". Click the value → an input in the card with a check + a cross (Enter saves, Escape cancels) → save(item, value) answers the fresh row (a g.fetch envelope) → the row repaints IN PLACE, never moving; a validation answer (data.validated: false + data.fields) lands under the input the lister way. Here: the code (try x, and a locked element), the name, the category (a select), and Locked in the details box (a switch).
  • Keyboard. → opens a node (then steps into its first child), ← closes it (then climbs to the parent); ↑/↓ walk the visible rows in order, children included.

Alternative: the reverse assignment — details at the start, children at the end, card click = details (not the standard row)

Element
Category
Children