Vancetope — Document Kind tree
Specifies the
treepayload for documents that carry a hierarchical, ordered set of items. Builds directly on thelistschema — same per-item shape, same three on-disk formats — and addschildren-based nesting. See also: doc-kind-items | web-ui
1. Purpose
kind: list (see doc-kind-items) is explicitly flat. As soon as items carry true hierarchy — outliners, structural plans, bulleted lists with sub-items — kind: tree comes into play. Both Kinds use the same codec style, a different item model (recursive children).
Separation instead of a shared editor with mode-switch:
- list is 1D — Reorder is one-dimensional, editor is minimal.
- tree is 2D — Reorder + Indent/Outdent. Different affordances, different keybindings, different D&D rules.
- A shared editor would impose the complexity of tree on list documents as well. Two components are preferred.
What this spec defines:
- Extended item schema (
childrenrecursive). - Format mapping md/json/yaml with nesting.
- Editor behavior v1 — Keyboard-Indent instead of full-2D-Drag.
What it does not define:
- Multi-select bulk operations across hierarchy boundaries (see §6).
- Drag between levels (§4 v1-Limit, §6 Future).
kind: mindmap— its own Kind, its own spec, different UI (graph-layout).
2. Items Model
Per item:
| Field | Type | Required | Meaning |
|---|---|---|---|
text |
string |
yes | Display text, single- to multi-line. |
children |
Item[] |
no | Subordinate items, recursive. Missing / empty array = leaf node. |
Forward compatibility: As with list, unknown fields are passed through via an extra map (json/yaml). Markdown can only represent text + nesting — other fields are lost during the md round-trip, which is documented behavior.
Canonical Form (JSON):
{
"$meta": { "kind": "tree" },
"items": [
{ "text": "parent", "children": [
{ "text": "child", "children": [
{ "text": "grandchild", "children": [] }
]}
]},
{ "text": "second top-level", "children": [] }
]
}
children may be omitted for leaves or written as an empty array — when writing, canonically [] for “explicitly leaf”, omitted is also accepted when reading.
Header Convention per Format: identical to doc-kind-items §2 — both JSON and YAML carry kind in a $meta mapping at the top level. MD frontmatter unchanged.
3. On-Disk Formats
3.1 Markdown
---
kind: tree
---
- parent
- child
- grandchild
- second top-level
Reading Rules:
- Front-Matter as with
list(----fences, flat key/value,kind:privileged). - Body: any line starting with
\s*[-*]\s+is an item. The number of spaces before the bullet marker determines the depth. - Depth calculation: Indent in spaces, divided by 2 (two spaces per level — Markdown convention). Tabs are interpreted as 4 spaces. Items whose indent is not a multiple of 2 are mapped to the next shallower level.
- An item indented deeper than its direct predecessor + 1 is capped at predecessor + 1. There are no “skip-level” nodes.
- Continuation lines (multi-line text) as with
list: subsequent lines with indent ≥ item indent + 2 and without their own bullet marker are appended totextwith\n-separator.
Writing Rules:
- Pre-order DFS. Per item: Indent (depth × 2 Spaces) +
-+ firsttext-line. - Continuation: subsequent lines with indent + 2 additional spaces.
extrafields per item are lost (md-limitation).- Trailing newline at EOF.
3.2 JSON
$meta wrapper for kind, items as top-level array with children: [] recursively. When writing:
- 2-space indent, pretty-printed over multiple lines.
- Order of top-level keys:
$meta,items, then passthrough. - Order of item keys:
text, thenchildren, thenextrafields in insertion order.
3.3 YAML
$meta:
kind: tree
items:
- text: parent
children:
- text: child
children:
- text: grandchild
children: []
- text: second top-level
children: []
Single-document with $meta as the first top-level key. Block-style sequences, 2-space indent, one item per line (with continuation block for multi-line text). String shorthand as with list is not accepted for Tree: an item without children must be written as an object, otherwise the distinction between “item text” and “sub-item” becomes ambiguous.
4. Server Path
Like list: no dedicated endpoint, no server-side parser. The editor loads the Document via GET /documents/{id}, parses in the browser, writes the serialized body back with PUT /documents/{id}. The existing HeaderStrategy path automatically mirrors kind: tree to DocumentDocument.kind — no server changes needed.
5. Web-UI
5.1 Editor Activation
kind === 'tree'+ Format ∈ {md, json, yaml} → the Tree Editor is offered in addition to the Raw Editor. Tab switchTree/Raw, DefaultTree.- Otherwise: Raw Editor only.
Tab switching is in-memory (codec round-trip), no REST calls. Save path is the existing Document update path.
5.2 Feature Set v1
| Feature | v1 | Note |
|---|---|---|
| Read-only render (recursive) | ✓ | first incarnation |
| Add / edit / delete per node | ✓ | inline edit, same keyboard shortcuts as list |
| Reorder (drag & drop) at the same level | ✓ | vue-draggable-plus, nested per Group tree-{depth} (or same Group name; Re-Parent test via parent-Path) |
| Indent / Outdent | ✓ | Keyboard shortcut only: Tab / Shift+Tab in edit mode |
| Add-child-Button on item | ✓ | appears on hover, inserts empty item as first child |
| Expand / Collapse | ✓ | per node click on disclosure triangle, not persisted |
| Drag between Levels (Re-Parent via Drag) | ✗ | see §6 |
| Multi-Select across Hierarchy | ✗ | see §6 |
5.3 Keyboard Shortcuts
In edit mode of a node:
| Key | Action |
|---|---|
Enter |
Commit, new sibling item after (same depth), edit on new item |
Shift+Enter |
Newline in text (multi-line item) |
Tab |
Indent — the current item becomes a child of the direct preceding sibling. Edit remains active, cursor unchanged |
Shift+Tab |
Outdent — the item moves one level up (becomes a sibling of its parent). If already at top-level: no-op |
Esc |
Cancel |
Backspace (empty) |
Delete item, focus on previous node (DFS-prev) |
Indent Rules:
- Indent only works if a direct preceding sibling exists. First sibling in the level → no-op.
- Children of the indented item move with it (the entire subtree remains together).
- Outdent moves the item after the current parent (to the end of the parent’s parent’s sibling row). Children move with it.
5.4 Drag-Reorder Rules
- Within the same level: grab drag handle, move vertically between siblings. Reorder under the common parent.
- Not allowed v1: Move item between levels via drag. Use Tab/Shift+Tab for this.
- Implementation: all
VueDraggableinstances do not share the samegroupname — each level has its own group, sovue-draggable-plusprohibits cross-level drops from the outset. Visually clear: items from the current level show drop position, other levels do not.
5.5 Components
<TreeView>— Top-level container, renders the<VueDraggable>of the top-level items.<TreeNode>— per node: Disclosure-Triangle, Drag-Handle, Text/Edit, Trash-Icon, Add-Child-Button. Contains itself recursively for itschildren(enclosed again by its own<VueDraggable>with the level-specific Group ID).
5.6 Visual Conventions
- Indent per level: 1.5rem left.
- Disclosure-Triangle:
▾(open) /▸(closed). Default open. State is in the Tree-Component, lost on reload — no persistence in v1. - Drag-Handle:
⠿as with list, but per level-instance separately.
6. Future (not v1)
6.1 Drag between Levels (Re-Parent via Drag)
Full 2D drag with drop zones between items, at item end edges, and on hover over a node (= “make me a child”). Implementation focus:
- Drop-Zone-Detection: on
dragover, detect position within the row — upper half = Drop-Above, lower half = Drop-Below, X-offset right of indent = “become Child”. - Self-Drop Protection: an item must not be dragged into its own subtree. On drag start, we mark all nodes in the subtree and block drops there.
- Live-Preview: Indent indicator during drag shows what depth would result on drop.
This is 3-4× more code than the keyboard variant and requires its own UX iteration. We will do this as soon as real usage shows that Tab/Shift+Tab is not sufficient.
6.2 Multi-Select across Hierarchy
list has Shift-/Ctrl-Click selection and bulk delete. In tree, this is open:
- What happens on bulk delete of a parent — delete children with it or promote them? Both behaviors are valid; the default is not unambiguous.
- Range-Select across Levels — the range follows DFS order, which is intuitive, but visually it can be confusing because the selected nodes are not contiguous.
Better to wait for a use case, then build.
6.3 Persistent Expand/Collapse
Retain state per node on reload — either as a per-user setting (webui.tree.<docId>.collapsed) or directly in the Document as an extra field per item. Both approaches have disadvantages: user setting grows indefinitely, item extra makes Document diffs less readable. Not now.
7. Open Points
- Markdown Indent Size: 2 spaces is the common default, but 4 is also widespread. Codec reads both (each indent level is normalized on read), writes canonically 2.
- Empty Items in the Hierarchy: an empty
textwithchildrenis allowed — the node acts as a pure grouping element. In md format, it is written as-without text. Stable on round-trip. - Order of Item Keys when writing:
textfirst, thenchildren, then passthrough-extra. Sufficient for now; Recipe tooling and LLM output quality can refine the convention later if needed.