feat: add dual-mode tree editor with Code Mode, variables, and markdown sync

Implements the full dual-mode tree editor (Plan Phases 1-5):

Backend:
- JSONB↔Markdown bidirectional serializer/parser with mistune
- Markdown validator with line/column error reporting
- 3 API endpoints: export-markdown, import-markdown, validate-markdown
- Variable extraction/resolution service ([USER_INPUT], [VAR], [SAVE_AS])
- Session variables JSONB column (migration 028)
- 39 tree markdown tests + variable service tests (403 total passing)

Frontend:
- Monaco-based Code Mode with custom Monarch tokenizer and dark theme
- Autocomplete for @node_id refs, type values, variable names
- Debounced validation (800ms) with inline Monaco error markers
- Syntax help panel (absolute overlay, toggleable)
- Starter template for new trees with valid cross-references
- Bidirectional metadata sync (name/description/category/tags frontmatter)
- Synchronous tree→markdown serializer (fixes async race condition)
- Pre-save validation blocks save on broken refs or missing tree name
- Mode-aware undo/redo: Monaco native in Code Mode, throttled zundo in Flow Mode
- Variable prompt modal and frontend resolver for session navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-10 09:45:26 -05:00
parent 2bd47004e7
commit eac6e184ec
32 changed files with 3369 additions and 52 deletions

View File

@@ -1,13 +1,22 @@
import { lazy, Suspense } from 'react'
import { TreeMetadataForm } from './TreeMetadataForm'
import { NodeList } from './NodeList'
import { TreePreviewPanel } from '@/components/tree-preview/TreePreviewPanel'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { cn } from '@/lib/utils'
// Lazy load CodeModeEditor (Monaco is ~2MB)
const CodeModeEditor = lazy(() =>
import('./code-mode/CodeModeEditor').then(m => ({ default: m.CodeModeEditor }))
)
interface TreeEditorLayoutProps {
isMobile?: boolean
}
export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
const editorMode = useTreeEditorStore(s => s.editorMode)
return (
<div
className={cn(
@@ -15,28 +24,52 @@ export function TreeEditorLayout({ isMobile = false }: TreeEditorLayoutProps) {
isMobile ? 'flex-col' : 'flex-row'
)}
>
{/* Left Panel - Form Editor */}
<div
className={cn(
'flex flex-col overflow-y-auto border-white/[0.06]',
isMobile ? 'h-full w-full border-b' : 'w-3/5 border-r'
)}
>
<div className="space-y-4 p-4">
<TreeMetadataForm />
<NodeList />
</div>
</div>
{editorMode === 'code' ? (
<>
{/* Code Mode: Monaco editor (60%) + Preview (40%) */}
<div className={cn(
'flex flex-col overflow-hidden border-white/[0.06]',
isMobile ? 'h-full w-full border-b' : 'w-3/5 border-r'
)}>
<Suspense fallback={
<div className="flex h-full items-center justify-center bg-[#0a0a0a]">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-white/20 border-t-white" />
</div>
}>
<CodeModeEditor />
</Suspense>
</div>
{/* Right Panel - Preview */}
<div
className={cn(
'flex-1 overflow-hidden bg-white/[0.02]',
isMobile ? 'hidden' : 'block'
)}
>
<TreePreviewPanel />
</div>
{/* Right Panel - Preview */}
<div className={cn(
'flex-1 overflow-hidden bg-white/[0.02]',
isMobile ? 'hidden' : 'block'
)}>
<TreePreviewPanel />
</div>
</>
) : (
<>
{/* Flow Mode: Form editor (60%) + Preview (40%) */}
<div className={cn(
'flex flex-col overflow-y-auto border-white/[0.06]',
isMobile ? 'h-full w-full border-b' : 'w-3/5 border-r'
)}>
<div className="space-y-4 p-4">
<TreeMetadataForm />
<NodeList />
</div>
</div>
{/* Right Panel - Preview */}
<div className={cn(
'flex-1 overflow-hidden bg-white/[0.02]',
isMobile ? 'hidden' : 'block'
)}>
<TreePreviewPanel />
</div>
</>
)}
</div>
)
}