Tree
Árbol jerárquico con nodos expandibles. Compón .tree-node con .tree-row (caret + icono + .label + .meta) y .tree-children recursivo. La interactividad se enchufa con Datastar.
Importante · scope de signals. Datastar v1 trata todos los signals como globales. Si dos nodos declaran data-signals="{open: false}", comparten el mismo $open y se abren/cierran a la vez. Cada nodo debe usar un namespace único: data-signals="{n40: {open: false}}" y referenciar $n40.open. El macro tree_node ya lo hace; pásale node_id explícito para garantizar unicidad.
▸
📁
4 · Acreedores y deudores
142 cuentas
▸
📁
40 · Proveedores
38
📄
400 — Proveedores
−12.483,90 €
📄
401 — Efectos comerciales a pagar
−2.140,00 €
📄
410 — Acreedores por servicios
−864,32 €
▸
📁
5 · Cuentas financieras
29
▸
📁
6 · Compras y gastos
88
▸
📁
7 · Ventas e ingresos
41
Copy
<div class="tree" role="tree" data-signals="{sel: '4-40-400'}">
<div class="tree-node"
data-signals="{ n4: { open: true } }"
data-attr:data-state="$n4.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$n4.open = !$n4.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($n4.open = !$n4.open)"
data-attr:aria-expanded="$n4.open"
data-attr:data-rotated="$n4.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label"><b>4 ·</b> Acreedores y deudores</span>
<span class="tree-meta">142 cuentas</span>
</div>
<div class="tree-children" data-show="$n4.open" role="group">
<div class="tree-node"
data-signals="{ n4_40: { open: true } }"
data-attr:data-state="$n4_40.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$n4_40.open = !$n4_40.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($n4_40.open = !$n4_40.open)"
data-attr:aria-expanded="$n4_40.open"
data-attr:data-rotated="$n4_40.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label"><b>40 ·</b> Proveedores</span>
<span class="tree-meta">38</span>
</div>
<div class="tree-children" data-show="$n4_40.open" role="group">
<div class="tree-node">
<div class="tree-row" data-on:click="$sel = '4-40-400'" data-attr:aria-selected="$sel === '4-40-400'">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">400 — Proveedores</span>
<span class="tree-meta">−12.483,90 €</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row" data-on:click="$sel = '4-40-401'" data-attr:aria-selected="$sel === '4-40-401'">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">401 — Efectos comerciales a pagar</span>
<span class="tree-meta">−2.140,00 €</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row" data-on:click="$sel = '4-40-410'" data-attr:aria-selected="$sel === '4-40-410'">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">410 — Acreedores por servicios</span>
<span class="tree-meta">−864,32 €</span>
</div>
</div>
</div>
</div>
<div class="tree-node"
data-signals="{ n4_43: { open: false } }"
data-attr:data-state="$n4_43.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$n4_43.open = !$n4_43.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($n4_43.open = !$n4_43.open)"
data-attr:aria-expanded="$n4_43.open"
data-attr:data-rotated="$n4_43.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label"><b>43 ·</b> Clientes</span>
<span class="tree-meta">62</span>
</div>
<div class="tree-children" data-show="$n4_43.open" role="group">
</div>
</div>
</div>
</div>
<div class="tree-node"
data-signals="{ n5: { open: false } }"
data-attr:data-state="$n5.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$n5.open = !$n5.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($n5.open = !$n5.open)"
data-attr:aria-expanded="$n5.open"
data-attr:data-rotated="$n5.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label"><b>5 ·</b> Cuentas financieras</span>
<span class="tree-meta">29</span>
</div>
<div class="tree-children" data-show="$n5.open" role="group">
</div>
</div>
<div class="tree-node"
data-signals="{ n6: { open: false } }"
data-attr:data-state="$n6.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$n6.open = !$n6.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($n6.open = !$n6.open)"
data-attr:aria-expanded="$n6.open"
data-attr:data-rotated="$n6.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label"><b>6 ·</b> Compras y gastos</span>
<span class="tree-meta">88</span>
</div>
<div class="tree-children" data-show="$n6.open" role="group">
</div>
</div>
<div class="tree-node"
data-signals="{ n7: { open: false } }"
data-attr:data-state="$n7.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$n7.open = !$n7.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($n7.open = !$n7.open)"
data-attr:aria-expanded="$n7.open"
data-attr:data-rotated="$n7.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label"><b>7 ·</b> Ventas e ingresos</span>
<span class="tree-meta">41</span>
</div>
<div class="tree-children" data-show="$n7.open" role="group">
</div>
</div>
</div>
Copy
{% from "tree.jinja" import tree, tree_node, tree_leaf %}
{% call tree(attrs={"data-signals": "{sel: '4-40-400'}"}) %}
{% call tree_node(label="<b>4 ·</b> Acreedores y deudores", icon="📁", meta="142 cuentas", expanded=true, node_id="n4") %}
{% call tree_node(label="<b>40 ·</b> Proveedores", icon="📁", meta="38", expanded=true, node_id="n4_40") %}
{{ tree_leaf(label="400 — Proveedores", icon="📄", meta="−12.483,90 €", value="4-40-400") }}
{{ tree_leaf(label="401 — Efectos comerciales a pagar", icon="📄", meta="−2.140,00 €", value="4-40-401") }}
{{ tree_leaf(label="410 — Acreedores por servicios", icon="📄", meta="−864,32 €", value="4-40-410") }}
{% endcall %}
{% call tree_node(label="<b>43 ·</b> Clientes", icon="📁", meta="62", node_id="n4_43") %}{% endcall %}
{% endcall %}
{% call tree_node(label="<b>5 ·</b> Cuentas financieras", icon="📁", meta="29", node_id="n5") %}{% endcall %}
{% call tree_node(label="<b>6 ·</b> Compras y gastos", icon="📁", meta="88", node_id="n6") %}{% endcall %}
{% call tree_node(label="<b>7 ·</b> Ventas e ingresos", icon="📁", meta="41", node_id="n7") %}{% endcall %}
{% endcall %}
Copy
<Tree data-signals="{sel: '4-40-400'}">
<div class="tree-node" data-signals="{n4: {open: true}}" data-attr:data-state="$n4.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$n4.open = !$n4.open" data-attr:aria-expanded="$n4.open" data-attr:data-rotated="$n4.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label"><b>4 ·</b> Acreedores y deudores</span>
<span class="meta">142 cuentas</span>
</div>
<div class="tree-children" data-show="$n4.open">
<div class="tree-node" data-signals="{n4_40: {open: true}}" data-attr:data-state="$n4_40.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$n4_40.open = !$n4_40.open" data-attr:aria-expanded="$n4_40.open" data-attr:data-rotated="$n4_40.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label"><b>40 ·</b> Proveedores</span>
<span class="meta">38</span>
</div>
<div class="tree-children" data-show="$n4_40.open">
<div class="tree-node">
<div class="tree-row" data-on:click="$sel = '4-40-400'" data-attr:aria-selected="$sel === '4-40-400'">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">400 — Proveedores</span>
<span class="meta">−12.483,90 €</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row" data-on:click="$sel = '4-40-401'" data-attr:aria-selected="$sel === '4-40-401'">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">401 — Efectos comerciales a pagar</span>
<span class="meta">−2.140,00 €</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row" data-on:click="$sel = '4-40-410'" data-attr:aria-selected="$sel === '4-40-410'">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">410 — Acreedores por servicios</span>
<span class="meta">−864,32 €</span>
</div>
</div>
</div>
</div>
<div class="tree-node" data-signals="{n4_43: {open: false}}" data-attr:data-state="$n4_43.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$n4_43.open = !$n4_43.open" data-attr:aria-expanded="$n4_43.open" data-attr:data-rotated="$n4_43.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label"><b>43 ·</b> Clientes</span>
<span class="meta">62</span>
</div>
</div>
</div>
</div>
<div class="tree-node" data-signals="{n5: {open: false}}" data-attr:data-state="$n5.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$n5.open = !$n5.open" data-attr:aria-expanded="$n5.open" data-attr:data-rotated="$n5.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label"><b>5 ·</b> Cuentas financieras</span>
<span class="meta">29</span>
</div>
</div>
<div class="tree-node" data-signals="{n6: {open: false}}" data-attr:data-state="$n6.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$n6.open = !$n6.open" data-attr:aria-expanded="$n6.open" data-attr:data-rotated="$n6.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label"><b>6 ·</b> Compras y gastos</span>
<span class="meta">88</span>
</div>
</div>
<div class="tree-node" data-signals="{n7: {open: false}}" data-attr:data-state="$n7.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$n7.open = !$n7.open" data-attr:aria-expanded="$n7.open" data-attr:data-rotated="$n7.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label"><b>7 ·</b> Ventas e ingresos</span>
<span class="meta">41</span>
</div>
</div>
</Tree>
Install the Jinja addon and import the component:
pip install outfitkit
Then in your template:
{% from "<component>.jinja" import <component> %}
{{ <component>(...) }}
Or with JinjaX:
<Component ... />
Copy
<div class="tree dense" role="tree">
<div class="tree-node"
data-signals="{ d_root: { open: true } }"
data-attr:data-state="$d_root.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$d_root.open = !$d_root.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($d_root.open = !$d_root.open)"
data-attr:aria-expanded="$d_root.open"
data-attr:data-rotated="$d_root.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label">outfitkit</span>
</div>
<div class="tree-children" data-show="$d_root.open" role="group">
<div class="tree-node"
data-signals="{ d_css: { open: true } }"
data-attr:data-state="$d_css.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret"
role="button"
tabindex="0"
data-on:click="$d_css.open = !$d_css.open"
data-on:keydown="(evt.key === 'Enter' || evt.key === ' ') && ($d_css.open = !$d_css.open)"
data-attr:aria-expanded="$d_css.open"
data-attr:data-rotated="$d_css.open">▸</span>
<span class="tree-icon folder">📁</span>
<span class="tree-label">css</span>
</div>
<div class="tree-children" data-show="$d_css.open" role="group">
<div class="tree-node">
<div class="tree-row">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">tokens.css</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row" aria-selected="true">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">navigation.css</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">forms.css</span>
</div>
</div>
</div>
</div>
<div class="tree-node">
<div class="tree-row">
<span class="tree-caret leaf" aria-hidden="true"></span>
<span class="tree-icon file">📄</span>
<span class="tree-label">README.md</span>
</div>
</div>
</div>
</div>
</div>
Copy
{% from "tree.jinja" import tree, tree_node, tree_leaf %}
{% call tree(variant="dense") %}
{% call tree_node(label="outfitkit", icon="📁", expanded=true, node_id="d_root") %}
{% call tree_node(label="css", icon="📁", expanded=true, node_id="d_css") %}
{{ tree_leaf(label="tokens.css", icon="📄") }}
{{ tree_leaf(label="navigation.css", icon="📄", selected=true) }}
{{ tree_leaf(label="forms.css", icon="📄") }}
{% endcall %}
{{ tree_leaf(label="README.md", icon="📄") }}
{% endcall %}
{% endcall %}
Copy
<Tree variant="dense">
<div class="tree-node" data-signals="{d_root: {open: true}}" data-attr:data-state="$d_root.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$d_root.open = !$d_root.open" data-attr:aria-expanded="$d_root.open" data-attr:data-rotated="$d_root.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label">outfitkit</span>
</div>
<div class="tree-children" data-show="$d_root.open">
<div class="tree-node" data-signals="{d_css: {open: true}}" data-attr:data-state="$d_css.open ? 'open' : 'closed'">
<div class="tree-row">
<span class="tree-caret" data-on:click="$d_css.open = !$d_css.open" data-attr:aria-expanded="$d_css.open" data-attr:data-rotated="$d_css.open">▸</span>
<span class="tree-icon tree-icon--folder">📁</span>
<span class="label">css</span>
</div>
<div class="tree-children" data-show="$d_css.open">
<div class="tree-node">
<div class="tree-row">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">tokens.css</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row" aria-selected="true">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">navigation.css</span>
</div>
</div>
<div class="tree-node">
<div class="tree-row">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">forms.css</span>
</div>
</div>
</div>
</div>
<div class="tree-node">
<div class="tree-row">
<span class="tree-caret tree-caret--leaf"></span>
<span class="tree-icon tree-icon--file">📄</span>
<span class="label">README.md</span>
</div>
</div>
</div>
</div>
</Tree>
Install the Jinja addon and import the component:
pip install outfitkit
Then in your template:
{% from "<component>.jinja" import <component> %}
{{ <component>(...) }}
Or with JinjaX:
<Component ... />
API · tree
Prop Tipo Default Descripción
variant"default"|"dense" "default" Densidad reducida (rows 24px, etc).
attrsdict {} Atributos HTML extra (p.ej. data-signals).
(slot) — — Nodos .ok-tree-node con .ok-tree-row y .ok-tree-children recursivo. Recomendado: usar tree_node y tree_leaf.
API · tree_node
Nodo expandible con toggle Datastar ya cableado: signal local open bajo namespace único $<node_id>.open (evita el leak global de signals en Datastar v1), aria-expanded y data-state reactivos, y caret con data-rotated para la animación de giro. Importar con {% from "tree.jinja" import tree_node, tree_leaf %}.
Prop Tipo Default Descripción
labelstr "" Etiqueta visible (HTML permitido).
iconstr "" Icono (emoji o SVG inline).
metastr "" Texto meta a la derecha.
expandedbool False Estado inicial.
icon_kind"folder"|"file" "folder" Aplica .ok-tree-icon--folder o --file.
node_idstr|None None Recomendado. Namespace único para el signal open (evita el leak global de Datastar v1). Si se omite, se genera uno aleatorio por render.
attrsdict {} Atributos HTML extra en el nodo.
(slot) — — Hijos del nodo (recursivos).
API · tree_leaf
Prop Tipo Default Descripción
labelstr "" Etiqueta visible.
iconstr "" Icono.
metastr "" Texto meta a la derecha.
selectedbool False Marca aria-selected="true" estático.
valuestr|None None Si se proporciona, cablea selección reactiva contra el signal $sel del ancestro.
attrsdict {} Atributos extra en la fila.