docs: add canvas UX fixes design doc (scroll, tooltips, answer stubs)
Captures approved design for three post-implementation UX improvements to the tree canvas editor: card scroll fix, info tooltip replacement for hint text, and the new 'answer' node type for sketching decision branches before assigning types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
258
docs/plans/2026-02-18-canvas-ux-fixes-design.md
Normal file
258
docs/plans/2026-02-18-canvas-ux-fixes-design.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# Canvas UX Fixes — Design Document
|
||||||
|
|
||||||
|
**Date:** 2026-02-18
|
||||||
|
**Branch:** `feature/tree-editor-canvas`
|
||||||
|
**Status:** Approved, pending implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The new TreeCanvas editor (Phase 1–4) was tested and three UX problems were identified:
|
||||||
|
|
||||||
|
1. **Scroll**: Expanded card forms have no height limit — long forms are cut off and unreachable
|
||||||
|
2. **Busy forms**: Inline hint text (`<p className="text-xs">`) inside NodeForm components creates visual clutter
|
||||||
|
3. **Answer stubs**: When building a decision node, users must immediately pick a child node type — there's no way to sketch out answer options first and decide types later
|
||||||
|
|
||||||
|
All three fixes apply exclusively to the canvas editor. No session, navigation, backend session-saving, or procedural flow code is affected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fix 1: Card Scroll
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`TreeCanvasNode.tsx` renders the expanded editing area as an unbounded `<div>`. On decision nodes with many options, or on any node when the browser viewport is short, the card overflows off-screen. There is no scrollbar — content is unreachable. Tab cycling doesn't scroll the canvas to bring hidden fields into view.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
Apply `max-h-[70vh] overflow-y-auto` to the expanded editing `<div>` inside `TreeCanvasNode.tsx`.
|
||||||
|
|
||||||
|
Make the save/cancel header row sticky (`sticky top-0 z-10 bg-card`) so the action buttons are always visible when the user scrolls the form content.
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
- `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
|
||||||
|
- Add `max-h-[70vh] overflow-y-auto` to the expanded area `<div>` (currently `border-t border-border px-3 pb-3 pt-3`)
|
||||||
|
- Add `sticky top-0 z-10 bg-card` to the card header `<div>` containing the save/cancel row when in expanded state
|
||||||
|
|
||||||
|
**No other files affected.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fix 2: Info Tooltips
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
`NodeFormDecision.tsx`, `NodeFormAction.tsx`, and `NodeFormResolution.tsx` each contain `<p className="mb-1 text-xs text-muted-foreground">` hint paragraphs below field labels. These add vertical height and visual noise inside a card that's already compact.
|
||||||
|
|
||||||
|
Examples of the current hint text:
|
||||||
|
- "Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, \`code\`"
|
||||||
|
- "PowerShell or CLI commands to execute"
|
||||||
|
- "Step-by-step instructions for resolving the issue"
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
Replace each hint `<p>` with a small `ⓘ` icon placed inline next to the field label. The icon shows a tooltip on hover containing the same text.
|
||||||
|
|
||||||
|
**Tooltip implementation:**
|
||||||
|
|
||||||
|
Use `title=""` on the icon element for a native browser tooltip. No third-party tooltip library needed — keeps the implementation minimal and consistent with the existing codebase pattern (the validation badge already uses `title={nodeErrors.map(...).join('\n')}`).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Before
|
||||||
|
<label className="block text-sm font-medium text-foreground">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<p className="mb-1 text-xs text-muted-foreground">
|
||||||
|
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||||||
|
</p>
|
||||||
|
|
||||||
|
// After
|
||||||
|
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||||
|
Description
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help"
|
||||||
|
title="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`"
|
||||||
|
>
|
||||||
|
i
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files changed:**
|
||||||
|
- `frontend/src/components/tree-editor/NodeFormDecision.tsx` — remove help_text hint `<p>`, replace with `ⓘ` on `Help Text` label
|
||||||
|
- `frontend/src/components/tree-editor/NodeFormAction.tsx` — remove description markdown hint `<p>` and commands hint `<p>`, add `ⓘ` on those labels
|
||||||
|
- `frontend/src/components/tree-editor/NodeFormResolution.tsx` — remove description markdown hint `<p>` and steps hint `<p>`, add `ⓘ` on those labels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fix 3: Answer Stubs (New `answer` Node Type)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
Decision nodes require the user to pick child node types at the same time they're creating the decision. This is backwards — you naturally know the answer options before you know what each one should do. The NodePicker in NodeFormDecision forces a concrete type selection (decision / action / solution) or leaves the option disconnected (`next_node_id: null`).
|
||||||
|
|
||||||
|
Users want to type answer labels first, see those answers appear as placeholder cards in the canvas, and then click each placeholder to assign a type and fill in details.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
Introduce `'answer'` as a new internal NodeType that represents a typed-but-unresolved branch placeholder. Answer nodes are:
|
||||||
|
- Created when a user types an answer label in the decision node form
|
||||||
|
- Shown in the canvas as a dashed-border stub card with the answer label
|
||||||
|
- Clickable to open a type picker (decision / action / solution) and convert to a real node
|
||||||
|
- **Not publishable** — blocked by backend and frontend validation on publish
|
||||||
|
|
||||||
|
Answer nodes persist to draft saves so users don't lose their sketch when they navigate away.
|
||||||
|
|
||||||
|
### Data Model
|
||||||
|
|
||||||
|
**`frontend/src/types/tree.ts`**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
export type NodeType = 'decision' | 'action' | 'solution'
|
||||||
|
|
||||||
|
// After
|
||||||
|
export type NodeType = 'decision' | 'action' | 'solution' | 'answer'
|
||||||
|
```
|
||||||
|
|
||||||
|
`TreeStructure` interface: no new fields needed. Answer nodes use:
|
||||||
|
- `id`: auto-generated UUID (same as other nodes)
|
||||||
|
- `type`: `'answer'`
|
||||||
|
- `title`: the answer label text (e.g. "Server", "Desktop")
|
||||||
|
- No other fields required
|
||||||
|
|
||||||
|
### NodeFormDecision Redesign
|
||||||
|
|
||||||
|
Replace the current options UI (NodePicker per option → picks existing or creates new) with a two-zone layout:
|
||||||
|
|
||||||
|
**Zone 1 — Answer Labels**
|
||||||
|
A simple list of text inputs, one per answer option. Each input edits `options[i].label`. Add/remove/reorder via `DynamicArrayField` (already available).
|
||||||
|
|
||||||
|
No `next_node_id` selection here. When the user saves, for each option that has a label but no `next_node_id`, a new answer-type stub node is created and linked automatically.
|
||||||
|
|
||||||
|
```
|
||||||
|
Options (answer labels):
|
||||||
|
[ Server ] [×]
|
||||||
|
[ Desktop ] [×]
|
||||||
|
[ + Add Answer ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zone 2 — (removed) NodePicker per option**
|
||||||
|
The per-option NodePicker is removed entirely. The canvas becomes the way to traverse to a child and set its type.
|
||||||
|
|
||||||
|
### TreeCanvas Changes
|
||||||
|
|
||||||
|
**Rendering answer nodes:**
|
||||||
|
|
||||||
|
When a node has `type === 'answer'`, render an `AnswerStubCard` instead of a full `TreeCanvasNode`. The stub card:
|
||||||
|
- Dashed border: `border-2 border-dashed border-border`
|
||||||
|
- Colored left accent: none (neutral/muted)
|
||||||
|
- Shows the answer label (`node.title`) centered
|
||||||
|
- Shows a "+ Choose Type" label below the title
|
||||||
|
- On click: opens an inline type picker (3 buttons: Decision / Action / Solution)
|
||||||
|
- On type selection: calls `updateNode(node.id, { type: selectedType })` and immediately expands the node for editing
|
||||||
|
|
||||||
|
**New component:** `frontend/src/components/tree-editor/AnswerStubCard.tsx`
|
||||||
|
|
||||||
|
Props:
|
||||||
|
```typescript
|
||||||
|
interface AnswerStubCardProps {
|
||||||
|
node: TreeStructure // type === 'answer'
|
||||||
|
fromOption?: string // the answer label (same as node.title)
|
||||||
|
onSelectType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stub Creation Logic (TreeCanvas)
|
||||||
|
|
||||||
|
When a decision node is saved (`onSave`), the canvas compares options before/after:
|
||||||
|
|
||||||
|
For each option in the saved node:
|
||||||
|
- If `option.next_node_id` is null/undefined → create a new answer stub node with `title = option.label` and link `option.next_node_id` to its ID.
|
||||||
|
- If `option.next_node_id` already points to a node → leave it.
|
||||||
|
|
||||||
|
This creation logic lives in `TreeCanvas.tsx`'s `handleNodeSave()` function, which already handles pending link resolution.
|
||||||
|
|
||||||
|
### Backend Validation
|
||||||
|
|
||||||
|
**`backend/app/core/tree_validation.py`**
|
||||||
|
|
||||||
|
`_validate_node()` currently rejects unknown node types:
|
||||||
|
```python
|
||||||
|
if node_type not in ('decision', 'action', 'solution'):
|
||||||
|
errors.append(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
1. Allow `'answer'` type through without structural validation (answer nodes have no required fields beyond `id` and `type`).
|
||||||
|
2. Add a publish-time check in `can_publish_tree()` (or in `validate_tree_structure()` before publish): if any node has `type == 'answer'`, reject with a clear message: `"Answer placeholders must be resolved to a node type before publishing."`
|
||||||
|
3. The draft save endpoint (`PUT /trees/:id`) does not call `can_publish_tree()`, so draft saves continue to work with answer nodes present.
|
||||||
|
|
||||||
|
### Frontend Publish Guard
|
||||||
|
|
||||||
|
In `TreeEditorPage.tsx`, before calling the publish API, add a check:
|
||||||
|
```typescript
|
||||||
|
const hasAnswerNodes = findAllAnswerNodes(tree.tree_structure).length > 0
|
||||||
|
if (hasAnswerNodes) {
|
||||||
|
// Show toast or inline error: "Resolve all answer placeholders before publishing."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`findAllAnswerNodes` is a simple recursive traversal (can be a small utility function in `TreeCanvas.tsx` or a new file `lib/treeUtils.ts`).
|
||||||
|
|
||||||
|
### Visual Design (AnswerStubCard)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
|
||||||
|
Server
|
||||||
|
[ ? Decision ] [ ⚡ Action ] [ ✓ Solution ]
|
||||||
|
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Card: `min-w-[180px] max-w-[280px] rounded-xl border-2 border-dashed border-border bg-card/50`
|
||||||
|
- Title: `text-sm font-heading font-medium text-foreground text-center py-2 px-3`
|
||||||
|
- Type picker row (default state — not yet clicked): `text-xs text-muted-foreground text-center pb-2 cursor-pointer hover:text-foreground`
|
||||||
|
- Clicking the card reveals three compact buttons for type selection
|
||||||
|
- Type picker (expanded): three small buttons side-by-side in the card footer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Changed Summary
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `frontend/src/components/tree-editor/TreeCanvasNode.tsx` | Fix 1: max-h + overflow-y + sticky header |
|
||||||
|
| `frontend/src/components/tree-editor/NodeFormDecision.tsx` | Fix 2: ⓘ tooltip on help_text; Fix 3: replace NodePicker with answer label list |
|
||||||
|
| `frontend/src/components/tree-editor/NodeFormAction.tsx` | Fix 2: ⓘ tooltips on description + commands fields |
|
||||||
|
| `frontend/src/components/tree-editor/NodeFormResolution.tsx` | Fix 2: ⓘ tooltips on description + steps fields |
|
||||||
|
| `frontend/src/components/tree-editor/TreeCanvas.tsx` | Fix 3: stub creation in handleNodeSave; AnswerStubCard rendering |
|
||||||
|
| `frontend/src/components/tree-editor/AnswerStubCard.tsx` | Fix 3: NEW — dashed stub card with inline type picker |
|
||||||
|
| `frontend/src/types/tree.ts` | Fix 3: add `'answer'` to NodeType union |
|
||||||
|
| `backend/app/core/tree_validation.py` | Fix 3: allow 'answer' in draft; block on publish |
|
||||||
|
| `frontend/src/pages/TreeEditorPage.tsx` | Fix 3: frontend publish guard |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No changes to session navigation, procedural flows, or maintenance flows
|
||||||
|
- No changes to the Code mode editor
|
||||||
|
- No changes to `treeEditorStore.ts` store actions (addNode, updateNode, deleteNode are used as-is)
|
||||||
|
- No third-party tooltip library
|
||||||
|
- No new backend endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Open a troubleshooting tree in the canvas editor
|
||||||
|
2. Click a decision node → card expands, form is scrollable with sticky save/cancel header
|
||||||
|
3. Field labels show `ⓘ` icons; hovering reveals the hint text
|
||||||
|
4. Type answer labels in the Options section; click ✓ to save
|
||||||
|
5. Answer stub cards appear as dashed cards below the decision node
|
||||||
|
6. Click a stub card → type picker appears; select "Decision" → card converts and expands for editing
|
||||||
|
7. Draft save works with answer nodes present (no backend error)
|
||||||
|
8. Attempt to publish with unresolved answer nodes → blocked with a clear error message
|
||||||
|
9. `npm run build` passes with no TypeScript errors
|
||||||
Reference in New Issue
Block a user