Files
Codewalkers/docs/wireframes/v2/content-tab.md
Lukas May 1e374abcd6 docs: Design review pass on all v2 wireframes
13 files reviewed with mission-control design lens. Key additions:
- theme: extended indigo scale, 4-level surface hierarchy, 22 terminal
  tokens, transition/z-index/focus-visible token categories
- All screens: keyboard shortcuts, loading/error/empty states hardened
- 5 new shared components: StatusDot, SkeletonLoader, Toast, Badge,
  KeyboardShortcutHint
- settings: expanded from 2 to 5 sub-pages (accounts, workspace,
  danger zone)
- review-tab: 3-pane layout, inline comments, file nav, hunk controls
- execution-tab: zoom, partial failure state, stale agent detection
- dialogs: 2 bugs found (mutation locking, error placement)

Total: 4,039 → 9,302 lines (+130% from review pass)
2026-03-02 19:36:26 +09:00

721 lines
31 KiB
Markdown

# Content Tab (v2)
### Route: `/initiatives/$id` (Content tab)
### Source: `packages/web/src/components/editor/ContentTab.tsx`, `packages/web/src/components/editor/`
---
## v1 -> v2 Changes
| Aspect | v1 | v2 |
|--------|----|----|
| Breadcrumbs | None | `Root > Parent > Current` row above title, clickable |
| Deep breadcrumbs | N/A | Ellipsis collapse with dropdown: `Root > [...] > Parent > Current` |
| Save indicator | Bare "Saving..." text, top-right, no success/fail states | `<SaveIndicator>` component: spinner, checkmark (fade), error + retry |
| Empty root page | Blank editor, no guidance | Tiptap placeholder: "Start writing... use / for commands" |
| Root page creation | Only via tree context menu hover `[+]` on nodes | Additional `[+]` button in sidebar header for root-level pages |
| Sidebar width | 192px (`w-48`) | 240px (`w-60`) — accommodates deep nesting + truncation |
| Page tree search | None | Collapsible filter input in sidebar header |
| Drag-and-drop | None | Reorder + re-parent via dnd-kit |
| Keyboard nav (tree) | None | Arrow keys, Enter, Home/End |
| Refine panel layout | Above editor (banner) | Right column (360px) at >= 1280px viewport |
| Refine panel detail | Minimal (spinner + text) | Full state machine: idle, running (live output), questions, proposals, crashed |
| Read-only lockout | None | Visual indicator when agent is actively refining |
| Word count | None | Bottom-right of editor area |
| Slash command menu | Functional but not documented | Wireframed floating popover with all command options |
---
## Default State (with content)
```
+=================================================================================+
| Pages [+] | Root > Architecture > Current Page |
| --------------------------+ [✓] Saved |
| ├── Architecture | ----------------------------------------------------|
| │ ├── Overview | |
| │ └── Decisions | # Authentication Architecture |
| ├── Requirements | |
| └── API Design | This document outlines the OAuth 2.0 |
| | implementation strategy for... |
| | |
| | ~1,240 words · 5m |
| 240px | editor area (flex-1) |
| | |
+=================================================================================+
```
- `[+]` in sidebar header creates a new root-level child page
- Breadcrumb segments are clickable links that call `onNavigate(pageId)`
- `[✓] Saved` indicator fades after 2s via CSS opacity transition
- Tree nodes still show hover `[+]` for nested child creation (unchanged from v1)
- Word count + reading time displayed bottom-right of editor, `text-xs text-muted-foreground`
---
## Empty Root Page (placeholder)
```
+=================================================================================+
| Pages [+] | Root |
| --------------------------+ |
| (empty tree) | ----------------------------------------------------|
| | |
| | My Initiative Name |
| | ^^^^^^^^^^^^^^^^^^^^^^ |
| | 3xl bold title input |
| | |
| | Start writing... use / for commands |
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| | muted placeholder text (text-muted-foreground) |
| | |
| | |
| 240px | editor area (flex-1) |
| | |
+=================================================================================+
```
- Placeholder uses `@tiptap/extension-placeholder` with `emptyEditorClass`
- Placeholder text disappears on focus/first keystroke
- Root page title maps to `initiativeName` (edits update initiative, not page)
### Slash Command Menu Preview
When user types `/` at the start of a line (or after a space), the command menu
appears as a floating popover anchored to the cursor position:
```
| |
| /___ |
| +--------------------------------------+ |
| | [H1] Heading 1 | <- highlighted
| | [H2] Heading 2 |
| | [H3] Heading 3 |
| | [¶] Bullet List |
| | [#] Numbered List |
| | ["] Blockquote |
| | [—] Divider |
| | [<>] Code Block |
| | [+] Subpage | <- creates child + inserts link
| +--------------------------------------+ |
| surface: popover, shadow-md, max-h-64 scroll |
```
- Appears on `/` keystroke; filters as user types (e.g., `/head` shows only headings)
- `Arrow Up` / `Arrow Down` to navigate; `Enter` to select; `Esc` to dismiss
- Each item: icon (16px, `text-muted-foreground`) + label (`text-sm`)
- Popover: `bg-popover border border-border rounded-md shadow-md`
- Max height: `max-h-64 overflow-y-auto` with scroll
- Highlighted item: `bg-accent text-accent-foreground`
---
## Save Indicator States
Three-state component positioned top-right of editor area, inline with breadcrumb row.
### Saving (spinner)
```
Root > Architecture > Current Page [spinner] Saving...
```
- `Loader2` icon with `animate-spin`, `text-muted-foreground`
- Shown while `isSaving` or `updateInitiativeMutation.isPending`
### Saved (checkmark, fading)
```
Root > Architecture > Current Page [✓] Saved
```
- Green checkmark icon, `text-green-500`
- Fades to `opacity-0` after 2s via `transition-opacity duration-1000`
- Resets to full opacity on next save cycle
### Failed (error with retry)
```
Root > Architecture > Current Page [!] Failed [retry]
```
- `AlertCircle` icon, `text-destructive`
- `[retry]` is a ghost button that triggers manual save flush
- Persists until retry succeeds or new edit triggers auto-save
---
## With Refine Panel Open (3-column layout)
```
+=================================================================================+
| Pages [+] | Root > Current Page [✓] Saved | Refine with Agent |
| ---------------+ | ----------------------|
| ├── Arch | # Auth Architecture | [spinner] Architect |
| │ ├── Ovw | | is refining... |
| │ └── Dec | This document outlines the | |
| ├── Req | OAuth 2.0 implementation... | (see panel detail |
| └── API | | below) |
| | | |
| 240px | editor (flex-1) | 360px |
| | | |
+=================================================================================+
```
- Refine panel sits above editor in v1 source (not beside it)
- v2 proposal: move to right column when viewport >= 1280px
- Below 1280px, refine panel stays above editor (current v1 behavior)
- Panel width: **360px** fixed (`w-[360px]`), editor remains `flex-1`
- **Save indicator placement**: in the 3-column layout, the save indicator stays
inline with the breadcrumb row, right-aligned within the editor column (middle
column). It does NOT extend into the refine panel column. The breadcrumb row is
`flex justify-between` within the editor column only.
---
## Refine Panel — Internal Layout
The refine panel is a right-side column with its own state machine. The wireframe
must show all states, since this is where the agent-human interaction happens.
### State: Idle (no active agent)
```
+-----------------------------------+
| Refine with Agent |
| ---------------------------------|
| |
| [sparkle icon] |
| An agent will review all pages |
| and suggest improvements. |
| |
| What should it focus on? |
| +-------------------------------+|
| | (optional instruction) ||
| +-------------------------------+|
| |
| [ Start Refine ] |
| |
+-----------------------------------+
```
- Panel header: `text-sm font-semibold`, top-left
- CTA button: `<Button>` default variant, full-width
- Instruction input: `<Textarea>` with placeholder, optional
### State: Running
```
+-----------------------------------+
| Refine with Agent [stop]|
| ---------------------------------|
| [●] Architect | <-- status-active-dot + agent name
| is refining... |
| |
| Elapsed: 0:42 | <-- text-xs text-muted-foreground
| |
| Live output: |
| +-------------------------------+|
| | > Reading page tree... || <-- terminal-bg, scrollable
| | > Analyzing Architecture... ||
| | > Drafting proposals... ||
| +-------------------------------+|
| |
+-----------------------------------+
```
- Live output: mini terminal view, `bg-terminal text-terminal-fg`, `max-h-48 overflow-y-auto`
- `[stop]` button: ghost variant, `text-destructive` on hover
- Agent name: `font-mono text-sm`
- Elapsed timer: updates every second via `setInterval`
### State: Agent has questions
```
+-----------------------------------+
| Refine with Agent |
| ---------------------------------|
| Agent has questions |
| |
| 1. Should I restructure the |
| auth section or just refine |
| the existing text? |
| +-------------------------------+|
| | (your answer) ||
| +-------------------------------+|
| |
| 2. Include code examples? |
| +-------------------------------+|
| | (your answer) ||
| +-------------------------------+|
| |
| [ Submit Answers ] [ Stop ] |
| |
+-----------------------------------+
```
- Questions rendered as numbered list
- Each answer: `<Textarea>` auto-growing
- Submit: default button; Stop: destructive-outline button
- Keyboard: `Cmd+Enter` submits all answers
### State: Completed (with proposals)
```
+-----------------------------------+
| Refine with Agent [dismiss]|
| ---------------------------------|
| Agent finished with 3 proposals |
| |
| ┌─ Page: Auth Architecture ─────┐|
| │ Expanded OAuth flow section │|
| │ with PKCE details │|
| │ [Accept] [Reject] │|
| └───────────────────────────────┘|
| |
| ┌─ Page: API Endpoints ─────────┐|
| │ Added rate limiting section │|
| │ [Accept] [Reject] │|
| └───────────────────────────────┘|
| |
| ┌─ Page: Requirements ──────────┐|
| │ Clarified NFR-3 latency req │|
| │ [Accept] [Reject] │|
| └───────────────────────────────┘|
| |
| [ Accept All ] [ Dismiss All ] |
| |
+-----------------------------------+
```
- Each proposal: card (`bg-card border rounded-md p-3`)
- Card header: target page name in `text-xs font-medium text-muted-foreground`
- Card body: proposal summary in `text-sm`
- Accept: default button; Reject: ghost button
- Bulk actions: `Accept All` (primary) + `Dismiss All` (ghost) at bottom
- Accepted proposals: green checkmark overlay, card fades to `opacity-50`
- Rejected proposals: strikethrough summary, card fades to `opacity-30`
### State: Completed (no changes)
```
+-----------------------------------+
| Refine with Agent [dismiss]|
| ---------------------------------|
| |
| Agent completed — no changes. |
| [dismiss] Cmd+Enter |
| |
+-----------------------------------+
```
### State: Crashed
```
+-----------------------------------+
| Refine with Agent |
| ---------------------------------|
| [!] Agent crashed |
| |
| [ Retry ] |
| |
+-----------------------------------+
```
- Error icon: `AlertCircle`, `text-destructive`
- Retry button opens the spawn dialog again
---
## Breadcrumb with Deep Nesting (ellipsis collapse)
### Depth <= 3: show all segments
```
Root > Architecture > Decisions
```
### Depth > 3: collapse middle with ellipsis
```
Root > [...] > API Endpoints > Rate Limiting
```
- `Root` always visible (first segment, clickable)
- `[...]` is a **dropdown trigger button** (not static text), styled as
`text-muted-foreground hover:text-foreground cursor-pointer`
- Last two segments always visible and clickable
- Clicking `[...]` opens a `<Popover>` (shadcn/ui) anchored below the ellipsis:
```
Root > [...] > API Endpoints > Rate Limiting
|
v
+---------------------------+
| > System Design | <-- clickable, navigates
| > Backend Architecture | <-- clickable, navigates
| > API Layer | <-- clickable, navigates
+---------------------------+
bg-popover, shadow-md, rounded-md
border border-border
min-w-[180px]
```
- Each row in the dropdown: `px-3 py-1.5 text-sm hover:bg-accent cursor-pointer`
- Clicking a row navigates to that page AND closes the popover
- Popover closes on outside click or `Esc`
- Implementation: `<Popover>` + `<PopoverTrigger>` + `<PopoverContent>` from shadcn/ui
- Separator character `>` uses `text-muted-foreground mx-1` (ChevronRight icon, 12px)
---
## Page Tree Sidebar Detail
```
+--------------------------+
| Pages [+] |
| [search filter...] |
| |
| ├── Architecture [+] | <-- [+] on hover: create child of Architecture
| │ ├── Overview | <-- active page: bg-accent
| │ └── Decisions |
| ├── Requirements [+] |
| └── API Design [+] |
| |
| ≡ drag handle on hover |
| |
+--------------------------+
240px, border-right
```
### Width rationale
192px is too narrow. At 12px indent per level, depth-3 nodes lose ~36px to
indentation plus ~14px for the icon plus ~20px for the hover `[+]` button.
That leaves ~122px for the page title — "Authentication Architecture" alone
is ~180px at `text-sm`. Bump to **240px** (`w-60`). Even at 240px, titles
will truncate; the truncation + tooltip behavior below handles this.
### Truncation + tooltip
All page titles render with `truncate` (CSS `text-overflow: ellipsis`).
On hover, a `<Tooltip>` shows the full title after a 500ms delay.
Implementation: wrap each node label in `<TooltipTrigger>` from shadcn/ui.
### Node behavior
- Header `[+]` always visible (new in v2) — creates page under root
- Node `[+]` on hover only (unchanged from v1) — creates child of **that** node (contextual, not root-level)
- FileText icon + truncated title per node
- Active page: `bg-accent rounded-md` highlight
- Indentation: 12px per depth level
### Drag-and-drop reordering
Nodes are draggable to reorder within their sibling group or re-parent under
a different node. Visual feedback:
```
| ├── Architecture |
| │ ├── Overview |
| │ ┌─ ─ ─ ─ ─ ─ ─ ┐ | <-- drop indicator (border-primary dashed)
| │ └── Decisions |
| ├── Requirements |
```
- Drag handle: `GripVertical` icon, visible on hover (left of FileText icon)
- Drop targets: between siblings (reorder) or onto a node (re-parent)
- Drop indicator: 2px dashed `border-primary` line between nodes
- Implementation: `@dnd-kit/core` + `@dnd-kit/sortable` (already tree-friendly)
- On drop: call `updatePage({ id, parentPageId, sortOrder })` mutation
- Root page is not draggable (always first)
### Search / filter
For initiatives with 20+ pages, the sidebar header includes a collapsible
search filter:
```
+--------------------------+
| Pages [search][+]|
| [filter query____] | <-- shown when [search] toggled
| |
| ├── Auth Architecture | <-- matches highlighted
| │ └── Auth Decisions |
| |
+--------------------------+
```
- Toggle: `Search` icon button in header, between title and `[+]`
- Input: compact text field, `text-xs`, `h-6`, auto-focus on open
- Filter: fuzzy substring match on page titles, case-insensitive
- Matching nodes shown with all ancestors (preserves tree context)
- Non-matching leaf nodes hidden; non-matching branches hidden if no descendants match
- `Esc` clears filter and collapses the search input
- Empty state: "No pages match" in `text-muted-foreground text-xs`
### Keyboard navigation (new in v2)
- `Arrow Down` / `Arrow Up` — move focus between visible tree nodes
- `Arrow Right` on collapsed node — expand children
- `Arrow Left` on expanded node — collapse; on leaf — move to parent
- `Enter` — navigate to focused node (same as click)
- `Home` / `End` — jump to first / last visible node
- Focus ring: `ring-2 ring-ring ring-offset-1` on the focused node row
---
## Read-Only Lockout (Agent Refining)
When the refine agent is in `running` state, the editor should enter a read-only
mode to prevent conflicting edits. The agent is rewriting page content server-side;
concurrent user edits would be overwritten or cause merge conflicts.
```
+=================================================================================+
| Pages [+] | Root > Current Page | Refine... |
| ---------------+ ┌─────────────────────────────────────┐ | |
| ├── Arch | │ [lock] Editor locked while agent │ | [●] Architect |
| │ ├── Ovw | │ is refining content │ | is refining... |
| │ └── Dec | └─────────────────────────────────────┘ | |
| ├── Req | | |
| └── API | # Auth Architecture | |
| | | |
| | This document outlines the OAuth 2.0 | |
| | implementation strategy for... | |
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |
| 240px | text at reduced opacity (opacity-60) | 360px |
| | | |
+=================================================================================+
```
- Banner: `bg-status-warning-bg text-status-warning-fg border border-status-warning-border`
with `Lock` icon (lucide-react), positioned above the title input
- Editor: `contenteditable="false"`, `opacity-60`, `cursor-not-allowed`
- Title input: `disabled`, `opacity-60`
- Banner dismisses automatically when agent finishes (state transitions away from `running`)
- User can still navigate the page tree and read other pages while locked
---
## Word Count / Reading Time
Persistent footer below the editor content area, right-aligned.
```
| |
| This document outlines the OAuth 2.0 implementation strategy... |
| |
| ~1,240 words · 5m |
+---------------------------------------------------------------------+
```
- Position: bottom-right of editor area, `text-xs text-muted-foreground`
- Format: `~{count} words · {minutes}m` (reading time at 250 wpm, rounded up)
- Calculation: count words from Tiptap `editor.getText()` on each update (debounced 500ms)
- Only shown when editor has content (hidden on empty/placeholder state)
- Does not count content inside code blocks differently (plain word count)
- In 3-column layout: stays within the editor column, does not leak into refine panel
---
## Loading State
```
+=================================================================================+
| +------------+ | +--------------------------------------------+ |
| | ░░░░░░░░ | skeleton | | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | |
| | ░░░░░░░░ | h-64 w-60 | | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | |
| | ░░░░░░░░ | | | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | |
| +------------+ | +--------------------------------------------+ |
| 240px | flex-1 |
+=================================================================================+
```
- Shown while `rootPageQuery.isLoading`
- Skeleton shimmer components from `@/components/Skeleton`
---
## Error State
```
+=============================================================================+
| |
| [AlertCircle] |
| Failed to load editor: <message> |
| |
| Make sure the backend server is running |
| with the latest code. |
| |
| [ Retry ] |
| |
+=============================================================================+
```
- `text-destructive` for error message
- `text-muted-foreground` for help text
- Retry button calls `rootPageQuery.refetch()`
---
## Source
- `packages/web/src/components/editor/ContentTab.tsx`
- `packages/web/src/components/editor/PageTree.tsx`
- `packages/web/src/components/editor/TiptapEditor.tsx`
- `packages/web/src/components/editor/RefineAgentPanel.tsx`
- `packages/web/src/components/editor/DeleteSubpageDialog.tsx`
- `packages/web/src/components/editor/PageTitleContext.tsx`
- `packages/web/src/hooks/useAutoSave.ts`
---
## Design Review Notes
Critique and improvements applied during design review. Each item references
the review criterion that prompted the change.
### 1. Sidebar width: 192px -> 240px
**Problem.** 192px is dangerously tight. At depth 3 with 12px indent per level,
you burn 36px on indentation, 14px on the FileText icon, 20px on the hover [+]
button, and 8px on left padding. That leaves ~114px for the title. A page named
"Authentication Architecture Decisions" at `text-sm` (14px) needs roughly
280px. You would see "Authenticat..." which is garbage -- users cannot
distinguish it from "Authentication" or "Authorization". 240px gives ~162px for
the title, which still truncates long names but shows enough to differentiate.
The truncation-plus-tooltip pattern catches the rest.
**Trade-off.** 48px wider sidebar steals from the editor. At 1280px viewport
that is ~3.75% of screen width. Acceptable -- the editor still gets flex-1 and
the content column never drops below ~680px even with the refine panel open
(1280 - 240 sidebar - 360 refine = 680px editor).
### 2. Breadcrumb ellipsis: static text -> dropdown
**Problem.** The original spec said `...` collapses middle segments and mentioned
it was a "dropdown trigger," but the wireframe showed static text and the
interaction was under-specified. If a user sees `Root > ... > Rate Limiting`
they have no way to navigate to intermediate pages without going back to the
tree and hunting.
**Fix.** Explicitly wireframed the `[...]` as a `<Popover>` trigger with the
full hidden path rendered as clickable rows. Specified the component
(shadcn/ui Popover), the styling, the close behavior, and the row interaction.
### 3. Save indicator in 3-column layout
**Problem.** If the save indicator is "top-right" of the editor area, and the
refine panel opens as a third column, does "top-right" mean top-right of the
entire viewport (overlapping the refine panel) or top-right of the editor
column?
**Fix.** Clarified: the breadcrumb row with the save indicator is `flex
justify-between` within the **editor column only** (the middle column). It
does not bleed into the refine panel. The save indicator sits at the right edge
of the breadcrumb row, which is bounded by the editor column width.
### 4. [+] button context
**Problem.** Two [+] buttons exist (sidebar header and per-node hover) but the
original spec did not clearly distinguish their behavior. A user might expect
the header [+] to create a sibling of the currently selected page, not a
root-level page.
**Fix.** The spec now explicitly states: header [+] always creates under root,
node hover [+] creates a child of **that specific node**. This is contextual
by default on the node buttons. The header button is intentionally non-contextual
(always root) to provide a reliable "new top-level page" entry point.
### 5. Drag-and-drop reordering
**Problem.** The original wireframe had no drag-and-drop. For a document
management UI, reordering pages by editing `sortOrder` in a database is not
a viable UX. Users need to drag.
**Fix.** Added a full drag-and-drop section: `GripVertical` handle on hover,
drop indicators (dashed border-primary lines), re-parenting via drop-on-node,
and the library choice (`@dnd-kit`). Specified the mutation call
(`updatePage({ id, parentPageId, sortOrder })`).
### 6. Page tree search/filter
**Problem.** With 20+ pages (realistic for a detailed initiative), scanning a
tree visually breaks down. There was no search.
**Fix.** Added a collapsible search filter in the sidebar header. Fuzzy
substring match, ancestor preservation (so matching leaf nodes still show
their tree path), and `Esc` to clear. The toggle button sits between the
"Pages" title and the [+] button.
### 7. Read-only lockout during agent refine
**Problem.** When the refine agent is running, it rewrites page content
server-side. If the user is simultaneously typing in the editor, the next
server push will overwrite their changes with no warning. This is a data loss
scenario.
**Fix.** Added a full "Read-Only Lockout" section. The editor goes
`contenteditable="false"` with reduced opacity and a warning banner using
status-warning tokens. The lockout auto-clears when the agent finishes. Users
can still read and navigate during the lock.
### 8. Refine panel internal layout
**Problem.** The original wireframe showed the refine panel as a 320px column
with "[spinner] Architect is refining..." and nothing else. The actual
implementation (verified in `RefineAgentPanel.tsx`) has five distinct states:
idle (spawn dialog), running (spinner), waiting (question form), completed
(change set / proposals), and crashed (error + retry). None of these were
wireframed.
**Fix.** Added wireframes for all five states with full internal layout:
idle with instruction textarea, running with live output terminal, question
form with numbered questions and answer textareas, completed with per-proposal
Accept/Reject cards plus bulk actions, and crashed with error + retry button.
Also bumped the panel from 320px to 360px because the proposal cards with
Accept/Reject buttons need the room.
### 9. Word count / reading time
**Problem.** Content pages had no feedback on document length. For planning
documents that agents will consume, knowing that a page is 3,000 words vs 300
words matters for understanding agent processing time and content density.
**Fix.** Added a persistent `~{words} words · {minutes}m` footer below the
editor, right-aligned, in `text-xs text-muted-foreground`. Calculation is
debounced off `editor.getText()`. Hidden on empty state.
### 10. Slash command menu
**Problem.** The placeholder says "use / for commands" but the wireframe never
showed what the command menu looks like. A developer implementing this has no
visual spec for the floating popover.
**Fix.** Added a full wireframe of the slash command menu: floating popover
anchored to cursor, all command options listed with icons, keyboard navigation
behavior, styling tokens, and scroll behavior for long menus.
### Open questions for next review
1. **Collaborative editing.** If two humans (or a human + agent) could
theoretically edit the same page, should the content tab show presence
indicators (avatars/cursors)? The current lockout approach sidesteps this
but is not a long-term solution.
2. **Page versioning.** Should pages have a revision history? The refine agent
creates proposals, but if the user manually edits a page and breaks it,
there is no undo beyond browser undo. A page history (snapshots on save)
would mitigate this.
3. **Tree collapse state persistence.** Should expanded/collapsed tree nodes
persist across page reloads? Currently the tree is always fully expanded.
For deep trees, remembering collapse state in `localStorage` or URL state
would reduce visual noise.
4. **Max nesting depth.** Should there be a hard limit on page nesting depth?
At 12px indent per level, depth 8 consumes 96px of the 240px sidebar.
Consider capping at depth 5 or 6 and showing a warning if users try to
nest deeper.