Tree / file tree
File tree: arrow-key nav · click a file to select (one cyan indicator)
Task subtree: multi-select · checking a parent propagates (tri-state)
Selected file: TreeDemo.tsx · checked subtasks: 2. The island is live.
// Web: React, consuming @dizyx/nockerl-tokens. NockerlTree ships from// @dizyx/nockerl-react: the recursive rows, depth indent + guide-line rails,// disclosure chevrons, single-select cyan wash OR multi-select tri-state// checkboxes (parent↔child propagation), and the full WAI-ARIA tree keyboard// pattern are all in the package.import { useState } from 'react';import { NockerlTree, type NockerlTreeNode } from '@dizyx/nockerl-react';
const files: NockerlTreeNode[] = [ { id: 'src', name: 'src', kind: 'folder', children: [ { id: 'src/components', name: 'components', kind: 'folder', children: [ { id: 'src/components/Tree.tsx', name: 'Tree.tsx', kind: 'file', fileType: 'typescript' }, ] }, { id: 'src/styles', name: 'styles', kind: 'folder', count: 3, children: [/* … */] }, ], }, { id: 'package.json', name: 'package.json', kind: 'file', fileType: 'json' },];
export function FileTree() { const [expanded, setExpanded] = useState(new Set(['src', 'src/components'])); const [selected, setSelected] = useState<string | null>(null); return ( <NockerlTree nodes={files} selectable="single" // or "multi" for tri-state checkboxes ariaLabel="Project files" expandedIds={expanded} onToggleExpand={setExpanded} selectedId={selected} onSelect={setSelected} /> );}// Android: Jetpack Compose (canonical). The recursive FileTreeNode is flattened// depth-first (respecting expand/collapse) into FlatFileNode rows in a LazyColumn;// FileTreeRow draws the depth*20 indent, the chevron, and the file-type icon.// com.nockerl.app.files.*: FileTreeNode / FlatFileNode / FileTreeRow / FileTreeViewModelimport com.nockerl.app.files.domain.FlatFileNodeimport com.nockerl.app.files.ui.FileTreeRow
@Composablefun FileTree( nodes: List<FlatFileNode>, // viewModel.uiState.flatNodes onToggleFolder: (String) -> Unit, // viewModel::toggleFolder (path) onOpenFile: (String) -> Unit, // viewModel::openFile (path)) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(nodes, key = { it.id }) { node -> FileTreeRow( node = node, // carries depth, isFolder, isExpanded, gitStatus, modifiedChildCount onClick = { if (node.isFolder) onToggleFolder(node.id) else onOpenFile(node.id) }, ) } }}
// A task subtree reuses the same idea with EpicListRow + ChildListRow// (chevron is its own hit area; a "done/total" subtask badge rides the accent).// macOS: SwiftUI, from the NockerlDesign SwiftPM package. `NockerlTree` rides the// native hierarchical `List(children:)` (platform disclosure chevrons, indentation,// and keyboard/VoiceOver come from the system) and brands each row: folder/file// glyph + single-select with the soft-accent wash + cyan ink.import NockerlDesign
// The recursive model: `nil` children = a leaf (no chevron); `[]` = an empty,// expandable folder. `kind` defaults from children when omitted.let roots: [NockerlTreeNode] = [ NockerlTreeNode(id: "src", name: "src", children: [ NockerlTreeNode(id: "src/components", name: "components", children: [ NockerlTreeNode(id: "src/components/Tree.swift", name: "Tree.swift"), ]), ]), NockerlTreeNode(id: "Package.swift", name: "Package.swift"),]
struct FileTree: View { @State private var selected: String?
var body: some View { NockerlTree(nodes: roots, selected: selected, label: "Project files") { node in // Called for folders AND files. The caller decides whether a folder tap // navigates or only discloses. if node.kind == .file { selected = node.id } } }}Parameters
Section titled “Parameters”NockerlTree takes the recursive nodes forest + controlled expand/select state. Auto-expand
root folders on first load to match the canonical behaviour. A NockerlTreeNode is
{ id, name, kind: 'folder' | 'file', fileType?, children?, count?, loading?, disabled? }.
| Prop | Type | Default | Description |
|---|---|---|---|
nodes * | NockerlTreeNode[] | The recursive forest. Auto-expand root folders on first load to match the canonical behaviour. | |
selectable | NockerlTreeSelectable | 'single' | single: one selected node (soft cyan wash). multi: tri-state checkboxes that propagate parent↔child. |
ariaLabel * | string | REQUIRED accessible name for the role="tree" container. | |
expandedIds * | Set<string> | Controlled set of expanded folder ids. | |
onToggleExpand | (next: Set<string>) => void | Fired with the next expanded set when a chevron (or a folder row) toggles. | |
selectedId | string | null | Selected node id (single-select). | |
onSelect | (id: string) => void | Fired when a leaf is activated (single-select). | |
checkedIds | Set<string> | Checked leaf ids (multi-select). A folder reads on / off / mixed from its descendants. | |
onCheckedChange | (next: Set<string>) => void | Fired when a node toggles its whole leaf-subtree (multi-select). |
| Parameter | Type | Default | Description |
|---|---|---|---|
node * | FlatFileNode | One flattened row: id, name, isFolder, depth (indent = depth * 20), isExpanded, hasChildren, gitStatus, modifiedChildCount. | |
onClick * | () -> Unit | Row tap: toggle the folder (FileTreeViewModel.toggleFolder) or open the file (openFile). | |
modifier | Modifier | Modifier | External modifier applied to the row. |
horizontalScrollState | ScrollState? | null | When set, the row sizes to its content and pans with one shared state (the Files sheet 2D scroll) so deep nesting / long names overflow instead of truncating. |
minRowWidth | Dp | 0.dp | Minimum content width in scrollable mode, sized to the viewport so short rows stay fully tappable. |
NockerlTree rides the native hierarchical List(children:) over a recursive
NockerlTreeNode model and brands the rows. Single-select via selected +
onSelect (multi-select tri-state is a follow-up). Disclosure chevrons, indentation,
and hairline guides come from the platform list.
| Parameter | Type | Default | Description |
|---|---|---|---|
nodes * | [NockerlTreeNode] | The root nodes. Each is { id, name, kind: .folder | .file, children? }; kind defaults from children. | |
selected | String? | nil | The selected node id. Draws the soft-accent wash + cyan ink on that row. |
label * | String | The REQUIRED accessible name for the whole tree ("Project files"). Empty is a runtime assert (law §13). | |
onSelect * | (NockerlTreeNode) -> Void | Invoked with the tapped node (folders AND files). The caller decides whether a folder tap navigates or only discloses. |
NockerlTreeNode.children is nil for a leaf (no chevron) and [] for an empty,
expandable folder.