Implement Tree Editor with visual preview and documentation updates

Tree Editor Features:
- Zustand store with immer middleware and zundo for undo/redo
- Form-based node editing (Decision, Action, Solution types)
- Visual tree preview with solution connection indicators
- NodePicker with type-grouped dropdown (Decisions/Actions/Solutions)
- SharedLinksMap for detecting nodes with multiple sources
- Modal component with scrollable body, fixed header/footer

New Components:
- TreeEditorLayout, TreeMetadataForm, NodeList, NodeEditorModal
- NodeFormDecision, NodeFormAction, NodeFormResolution
- DynamicArrayField, NodePicker
- TreePreviewPanel, TreePreviewNode

Documentation:
- Updated README.md status to Phase 2
- Added Tree Editor details to CURRENT-STATE.md
- Added modal/Zustand lessons to LESSONS-LEARNED.md
- Updated file structure in CLAUDE-SETUP.md
- Added Tree Editor progress to PROGRESS.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-01-28 03:00:00 -05:00
parent 088333174c
commit 4cee013733
26 changed files with 4073 additions and 91 deletions

View File

@@ -0,0 +1,132 @@
import { useEffect, useState } from 'react'
import { treesApi } from '@/api'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { cn } from '@/lib/utils'
export function TreeMetadataForm() {
const { name, description, category, setName, setDescription, setCategory, validationErrors } =
useTreeEditorStore()
const [categories, setCategories] = useState<string[]>([])
const [customCategory, setCustomCategory] = useState(false)
// Load existing categories
useEffect(() => {
treesApi.categories().then(setCategories).catch(console.error)
}, [])
const handleCategoryChange = (value: string) => {
if (value === '__custom__') {
setCustomCategory(true)
setCategory('')
} else {
setCustomCategory(false)
setCategory(value)
}
}
const nameError = validationErrors.find(
(e) => !e.nodeId && e.message.toLowerCase().includes('name')
)
return (
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
<h2 className="text-sm font-semibold text-card-foreground">Tree Details</h2>
{/* Name */}
<div>
<label htmlFor="tree-name" className="block text-sm font-medium text-foreground">
Name <span className="text-destructive">*</span>
</label>
<input
id="tree-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., VDA Registration Troubleshooting"
className={cn(
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
nameError ? 'border-destructive' : 'border-input'
)}
/>
{nameError && (
<p className="mt-1 text-xs text-destructive">{nameError.message}</p>
)}
</div>
{/* Description */}
<div>
<label htmlFor="tree-description" className="block text-sm font-medium text-foreground">
Description
</label>
<textarea
id="tree-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of what this tree troubleshoots..."
rows={2}
className={cn(
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
/>
</div>
{/* Category */}
<div>
<label htmlFor="tree-category" className="block text-sm font-medium text-foreground">
Category
</label>
{!customCategory ? (
<select
id="tree-category"
value={category || ''}
onChange={(e) => handleCategoryChange(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-input px-3 py-2 text-sm',
'bg-background text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
>
<option value="">No category</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
<option value="__custom__">+ Add custom category</option>
</select>
) : (
<div className="mt-1 flex gap-2">
<input
type="text"
value={category}
onChange={(e) => setCategory(e.target.value)}
placeholder="Enter new category"
className={cn(
'block flex-1 rounded-md border border-input px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
autoFocus
/>
<button
onClick={() => {
setCustomCategory(false)
setCategory('')
}}
className="rounded-md border border-input px-3 py-2 text-sm hover:bg-accent"
>
Cancel
</button>
</div>
)}
</div>
</div>
)
}
export default TreeMetadataForm