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

@@ -0,0 +1,201 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import Editor, { type OnMount, type BeforeMount } from '@monaco-editor/react'
import type { editor as MonacoEditor } from 'monaco-editor'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { treeMarkdownApi } from '@/api/treeMarkdown'
import { resolutionFlowLanguage, LANGUAGE_ID } from './resolutionFlowLanguage'
import { resolutionFlowTheme, THEME_ID } from './resolutionFlowTheme'
import { createCompletionProvider } from './resolutionFlowCompletions'
import { CodeModeToolbar } from './CodeModeToolbar'
import { SyntaxHelpPanel } from './SyntaxHelpPanel'
// Module-level ref so TreeEditorPage can trigger Monaco undo/redo from toolbar buttons
let _monacoEditor: MonacoEditor.IStandaloneCodeEditor | null = null
export function getMonacoEditor() { return _monacoEditor }
export function CodeModeEditor() {
const editorRef = useRef<MonacoEditor.IStandaloneCodeEditor | null>(null)
const validationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const {
markdownSource,
markdownValidationErrors,
isMarkdownValid,
isValidating,
setMarkdownSource,
setMarkdownValidationResult,
getAvailableTargetNodes,
} = useTreeEditorStore()
const [syntaxHelpOpen, setSyntaxHelpOpen] = useState(false)
// Register language and theme before editor mounts
const handleEditorWillMount: BeforeMount = useCallback((monaco) => {
// Register language if not already registered
const langs = monaco.languages.getLanguages()
if (!langs.some((l: { id: string }) => l.id === LANGUAGE_ID)) {
monaco.languages.register({ id: LANGUAGE_ID })
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, resolutionFlowLanguage)
}
// Register theme
monaco.editor.defineTheme(THEME_ID, resolutionFlowTheme)
// Register completion provider
monaco.languages.registerCompletionItemProvider(
LANGUAGE_ID,
createCompletionProvider(() =>
getAvailableTargetNodes().map(n => ({
id: n.id,
label: n.label,
type: n.type,
}))
)
)
}, [getAvailableTargetNodes])
// Editor mounted
const handleEditorDidMount: OnMount = useCallback((editor) => {
editorRef.current = editor
_monacoEditor = editor
editor.focus()
}, [])
// Debounced validation on change
const handleEditorChange = useCallback((value: string | undefined) => {
const md = value ?? ''
setMarkdownSource(md)
// Cancel pending validation
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current)
}
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
// Debounce 800ms
validationTimeoutRef.current = setTimeout(async () => {
abortControllerRef.current = new AbortController()
try {
const result = await treeMarkdownApi.validateMarkdown(md)
setMarkdownValidationResult(result)
// Set Monaco markers
if (editorRef.current) {
const monaco = (await import('monaco-editor')).default ?? await import('monaco-editor')
const model = editorRef.current.getModel()
if (model && monaco.editor?.setModelMarkers) {
const markers = result.errors.map((err) => ({
startLineNumber: err.line,
startColumn: err.column,
endLineNumber: err.line,
endColumn: err.column + 1,
message: err.message,
severity: err.severity === 'error' ? 8 : 4, // MarkerSeverity.Error : Warning
}))
monaco.editor.setModelMarkers(model, 'resolutionflow', markers)
}
}
} catch {
// Validation cancelled or failed — ignore
}
}, 800)
}, [setMarkdownSource, setMarkdownValidationResult])
// Insert template at cursor
const handleInsertTemplate = useCallback((template: string) => {
const editor = editorRef.current
if (!editor) return
const position = editor.getPosition()
if (!position) return
const model = editor.getModel()
if (!model) return
// Insert at end of document
const lastLine = model.getLineCount()
const lastColumn = model.getLineMaxColumn(lastLine)
editor.executeEdits('insert-template', [{
range: {
startLineNumber: lastLine,
startColumn: lastColumn,
endLineNumber: lastLine,
endColumn: lastColumn,
},
text: template,
}])
// Move cursor to the inserted template
const newLastLine = model.getLineCount()
editor.setPosition({ lineNumber: newLastLine, column: 1 })
editor.revealLine(newLastLine)
editor.focus()
}, [])
// Cleanup on unmount
useEffect(() => {
return () => {
_monacoEditor = null
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current)
}
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
}, [])
return (
<div className="flex h-full flex-col">
<CodeModeToolbar
validationErrors={markdownValidationErrors}
isValidating={isValidating}
isValid={isMarkdownValid}
onInsertTemplate={handleInsertTemplate}
onToggleSyntaxHelp={() => setSyntaxHelpOpen(!syntaxHelpOpen)}
syntaxHelpOpen={syntaxHelpOpen}
/>
<div className="relative flex-1 min-h-0">
<Editor
height="100%"
language={LANGUAGE_ID}
theme={THEME_ID}
value={markdownSource ?? ''}
onChange={handleEditorChange}
beforeMount={handleEditorWillMount}
onMount={handleEditorDidMount}
loading={
<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>
}
options={{
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
wordWrap: 'on',
scrollBeyondLastLine: false,
renderLineHighlight: 'line',
tabSize: 2,
insertSpaces: true,
automaticLayout: true,
suggestOnTriggerCharacters: true,
quickSuggestions: true,
padding: { top: 12, bottom: 12 },
accessibilitySupport: 'on',
}}
/>
{/* Syntax help as absolute overlay on right side */}
{syntaxHelpOpen && (
<div className="absolute right-0 top-0 bottom-0 z-20 w-[280px]">
<SyntaxHelpPanel open={syntaxHelpOpen} onClose={() => setSyntaxHelpOpen(false)} />
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,186 @@
import { ChevronDown, AlertCircle, CheckCircle2, Loader2, Plus, HelpCircle } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
import { cn } from '@/lib/utils'
import type { TreeMarkdownValidationError } from '@/types'
interface CodeModeToolbarProps {
validationErrors: TreeMarkdownValidationError[]
isValidating: boolean
isValid: boolean
onInsertTemplate: (template: string) => void
onToggleSyntaxHelp: () => void
syntaxHelpOpen: boolean
}
const NODE_TEMPLATES = {
decision: [
'',
'---',
'id: new_decision',
'type: decision',
'parent: root',
'---',
'# What is the question?',
'',
'> Help text for the engineer',
'',
'- [A] Option A → @target_id',
'- [B] Option B → @target_id',
'',
].join('\n'),
action: [
'',
'---',
'id: new_action',
'type: action',
'parent: root',
'---',
'## Action Title',
'',
'Description of what to do.',
'',
'```commands',
'command here',
'```',
'',
'**Expected:** Expected outcome',
'',
'→ @next_node_id',
'',
].join('\n'),
solution: [
'',
'---',
'id: new_solution',
'type: solution',
'parent: root',
'---',
'## Solution Title',
'',
'Description of the resolution.',
'',
'1. Step 1',
'2. Step 2',
'',
].join('\n'),
}
export function CodeModeToolbar({
validationErrors,
isValidating,
isValid,
onInsertTemplate,
onToggleSyntaxHelp,
syntaxHelpOpen,
}: CodeModeToolbarProps) {
const [insertOpen, setInsertOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const errorCount = validationErrors.filter(e => e.severity === 'error').length
const warningCount = validationErrors.filter(e => e.severity === 'warning').length
// Close dropdown on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setInsertOpen(false)
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [])
return (
<div className="flex items-center justify-between border-b border-white/[0.06] bg-black/50 px-3 py-1.5">
<div className="flex items-center gap-2">
{/* Insert Node dropdown */}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setInsertOpen(!insertOpen)}
className={cn(
'flex items-center gap-1 rounded-md border border-white/10 px-2.5 py-1 text-xs font-medium text-white/60',
'hover:bg-white/10 hover:text-white'
)}
>
<Plus className="h-3 w-3" />
Insert Node
<ChevronDown className="h-3 w-3" />
</button>
{insertOpen && (
<div className="absolute left-0 top-full z-50 mt-1 w-44 rounded-lg border border-white/10 bg-[#111] py-1 shadow-xl">
<button
onClick={() => { onInsertTemplate(NODE_TEMPLATES.decision); setInsertOpen(false) }}
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-white/70 hover:bg-white/10"
>
<span className="h-2 w-2 rounded-full bg-blue-400" />
Decision
</button>
<button
onClick={() => { onInsertTemplate(NODE_TEMPLATES.action); setInsertOpen(false) }}
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-white/70 hover:bg-white/10"
>
<span className="h-2 w-2 rounded-full bg-amber-400" />
Action
</button>
<button
onClick={() => { onInsertTemplate(NODE_TEMPLATES.solution); setInsertOpen(false) }}
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-white/70 hover:bg-white/10"
>
<span className="h-2 w-2 rounded-full bg-emerald-400" />
Solution
</button>
</div>
)}
</div>
</div>
<div className="flex items-center gap-3">
{/* Validation status */}
<div className="flex items-center gap-1.5 text-xs">
{isValidating ? (
<>
<Loader2 className="h-3 w-3 animate-spin text-white/40" />
<span className="text-white/40">Validating...</span>
</>
) : isValid ? (
<>
<CheckCircle2 className="h-3 w-3 text-emerald-400" />
<span className="text-emerald-400/70">Synced</span>
{warningCount > 0 && (
<span className="text-yellow-400/70">
({warningCount} warning{warningCount !== 1 ? 's' : ''})
</span>
)}
</>
) : (
<>
<AlertCircle className="h-3 w-3 text-red-400" />
<span className="text-red-400/70">
{errorCount} error{errorCount !== 1 ? 's' : ''}
</span>
{warningCount > 0 && (
<span className="text-yellow-400/70">
, {warningCount} warning{warningCount !== 1 ? 's' : ''}
</span>
)}
</>
)}
</div>
{/* Syntax Help toggle */}
<button
onClick={onToggleSyntaxHelp}
className={cn(
'flex items-center gap-1 rounded-md px-2 py-1 text-xs',
syntaxHelpOpen
? 'bg-white/10 text-white'
: 'text-white/40 hover:bg-white/[0.06] hover:text-white/60'
)}
>
<HelpCircle className="h-3 w-3" />
Syntax
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,109 @@
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
interface SyntaxHelpPanelProps {
open: boolean
onClose: () => void
}
export function SyntaxHelpPanel({ open, onClose }: SyntaxHelpPanelProps) {
if (!open) return null
return (
<div className="flex h-full flex-col border-l border-white/[0.06] bg-[#0a0a0a]">
<div className="flex items-center justify-between border-b border-white/[0.06] px-3 py-2">
<span className="text-xs font-medium text-white/60">Syntax Reference</span>
<button
onClick={onClose}
className="rounded p-0.5 text-white/30 hover:bg-white/10 hover:text-white/60"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
<div className="flex-1 overflow-y-auto px-3 py-3 text-[11px] leading-relaxed text-white/50">
<Section title="Node Structure">
<Code>{`---
id: node_id
type: decision|action|solution
parent: parent_id
---`}</Code>
</Section>
<Section title="Decision Node">
<Code>{`# Question text here
> Help text (blockquote)
- [A] Option label → @target_id
- [B] Another option → @target_id`}</Code>
</Section>
<Section title="Action Node">
<Code>{`## Action Title
Description paragraph.
\`\`\`commands
ping 8.8.8.8
tracert gateway
\`\`\`
**Expected:** Expected outcome
→ @next_node_id`}</Code>
</Section>
<Section title="Solution Node">
<Code>{`## Solution Title
Description paragraph.
1. Resolution step one
2. Resolution step two`}</Code>
</Section>
<Section title="Variables">
<Row label="User input" code="[USER_INPUT:prompt]" />
<Row label="Reference" code="[VAR:name]" />
<Row label="Save value" code="[SAVE_AS:name]" />
</Section>
<Section title="Keyboard Shortcuts">
<Row label="Toggle mode" code="Ctrl+Shift+M" />
<Row label="Insert Decision" code="Ctrl+Shift+D" />
<Row label="Insert Action" code="Ctrl+Shift+A" />
<Row label="Insert Solution" code="Ctrl+Shift+S" />
</Section>
</div>
</div>
)
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="mb-4">
<h4 className="mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-white/30">{title}</h4>
{children}
</div>
)
}
function Code({ children }: { children: string }) {
return (
<pre className={cn(
'mb-2 overflow-x-auto rounded-md border border-white/[0.06] bg-black/50 px-2 py-1.5',
'text-[10px] leading-relaxed text-white/50 whitespace-pre'
)}>
{children}
</pre>
)
}
function Row({ label, code }: { label: string; code: string }) {
return (
<div className="flex items-center justify-between py-0.5">
<span className="text-white/40">{label}</span>
<code className="rounded bg-white/[0.06] px-1 py-0.5 text-[10px] text-white/50">{code}</code>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export { CodeModeEditor, getMonacoEditor } from './CodeModeEditor'
export { CodeModeToolbar } from './CodeModeToolbar'
export { SyntaxHelpPanel } from './SyntaxHelpPanel'

View File

@@ -0,0 +1,158 @@
import type { languages, editor, Position, IRange } from 'monaco-editor'
interface NodeInfo {
id: string
label: string
type: string
}
/**
* Create a completion provider for ResolutionFlow markdown.
* Provides autocomplete for node references, types, and templates.
*/
export function createCompletionProvider(
getNodes: () => NodeInfo[]
): languages.CompletionItemProvider {
return {
triggerCharacters: ['@', ':', '['],
provideCompletionItems(
model: editor.ITextModel,
position: Position,
): languages.ProviderResult<languages.CompletionList> {
const lineContent = model.getLineContent(position.lineNumber)
const textUntilPosition = lineContent.substring(0, position.column - 1)
const word = model.getWordUntilPosition(position)
const range: IRange = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
const suggestions: languages.CompletionItem[] = []
// After @ — suggest node IDs
if (textUntilPosition.endsWith('@') || textUntilPosition.match(/@\w*$/)) {
const atPos = textUntilPosition.lastIndexOf('@')
const atRange: IRange = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: atPos + 2, // after the @
endColumn: position.column,
}
const nodes = getNodes()
for (const node of nodes) {
const typeIcon = node.type === 'decision' ? '?' : node.type === 'action' ? '!' : '='
suggestions.push({
label: `${node.id}${node.label}`,
kind: 18, // CompletionItemKind.Reference
insertText: node.id,
detail: `${typeIcon} ${node.type}`,
range: atRange,
})
}
return { suggestions }
}
// After "type:" — suggest node types
if (textUntilPosition.match(/^type:\s*$/)) {
for (const t of ['decision', 'action', 'solution']) {
suggestions.push({
label: t,
kind: 12, // CompletionItemKind.Value
insertText: t,
range,
})
}
return { suggestions }
}
// After "---\n" at beginning — suggest node template
if (lineContent.trim() === '---' && position.lineNumber <= 2) {
suggestions.push(
{
label: 'Decision Node',
kind: 14, // CompletionItemKind.Snippet
insertText: [
'',
'id: ${1:node_id}',
'type: decision',
'---',
'# ${2:What is the question?}',
'',
'> ${3:Help text for the engineer}',
'',
'- [A] ${4:Option A} → @${5:target_id}',
'- [B] ${6:Option B} → @${7:target_id}',
].join('\n'),
insertTextRules: 4, // InsertTextRule.InsertAsSnippet
detail: 'Decision node template',
range,
},
{
label: 'Action Node',
kind: 14,
insertText: [
'',
'id: ${1:node_id}',
'type: action',
'parent: ${2:parent_id}',
'---',
'## ${3:Action Title}',
'',
'${4:Description of what to do}',
'',
'```commands',
'${5:command here}',
'```',
'',
'**Expected:** ${6:Expected outcome}',
'',
'→ @${7:next_node_id}',
].join('\n'),
insertTextRules: 4,
detail: 'Action node template',
range,
},
{
label: 'Solution Node',
kind: 14,
insertText: [
'',
'id: ${1:node_id}',
'type: solution',
'parent: ${2:parent_id}',
'---',
'## ${3:Solution Title}',
'',
'${4:Description}',
'',
'1. ${5:Step 1}',
'2. ${6:Step 2}',
].join('\n'),
insertTextRules: 4,
detail: 'Solution node template',
range,
},
)
return { suggestions }
}
// After [VAR: — suggest variable names
if (textUntilPosition.match(/\[VAR:$/)) {
// Could be extended to track variable names from the document
suggestions.push(
{ label: 'hostname', kind: 5, insertText: 'hostname', range },
{ label: 'username', kind: 5, insertText: 'username', range },
{ label: 'ticket_number', kind: 5, insertText: 'ticket_number', range },
{ label: 'client_name', kind: 5, insertText: 'client_name', range },
)
return { suggestions }
}
return { suggestions }
},
}
}

View File

@@ -0,0 +1,72 @@
import type { languages } from 'monaco-editor'
/**
* Monarch tokenizer for ResolutionFlow tree markdown syntax.
* Provides syntax highlighting for frontmatter, headings, options, references, and variables.
*/
export const resolutionFlowLanguage: languages.IMonarchLanguage = {
tokenizer: {
root: [
// Frontmatter delimiter
[/^---\s*$/, 'delimiter.frontmatter', '@frontmatter'],
// Headings
[/^##\s+.*$/, 'heading.action'],
[/^#\s+.*$/, 'heading.decision'],
// Blockquote (help text)
[/^>\s.*$/, 'string.blockquote'],
// Option lines: - [X] Label → @target_id
[/^-\s*\[[A-Za-z0-9]+\]/, 'keyword.option', '@optionLine'],
// Next node reference: → @node_id
[/^→\s*@\S+/, 'variable.reference'],
// Expected outcome
[/^\*\*Expected:\*\*\s*.*$/, 'keyword.expected'],
// Command block
[/^```commands\s*$/, 'delimiter.commands', '@commandBlock'],
[/^```\s*$/, 'delimiter.commands'],
// Variable tokens
[/\[USER_INPUT:[^\]]+\]/, 'variable.input'],
[/\[VAR:[^\]]+\]/, 'variable.reference'],
[/\[SAVE_AS:[^\]]+\]/, 'variable.save'],
// Ordered list items
[/^\d+\.\s+/, 'keyword.step'],
// Bold
[/\*\*[^*]+\*\*/, 'strong'],
// Inline code
[/`[^`]+`/, 'string.code'],
// Regular text
[/./, 'text'],
],
frontmatter: [
[/^---\s*$/, 'delimiter.frontmatter', '@pop'],
[/^(id)(:)(.*)$/, ['keyword.frontmatter', 'delimiter', 'string.id']],
[/^(type)(:)(.*)$/, ['keyword.frontmatter', 'delimiter', 'type.value']],
[/^(parent)(:)(.*)$/, ['keyword.frontmatter', 'delimiter', 'string.parent']],
[/./, 'variable.other'],
],
optionLine: [
[/→\s*@\S+/, 'variable.reference'],
[/$/, '', '@pop'],
[/./, 'string.option'],
],
commandBlock: [
[/^```\s*$/, 'delimiter.commands', '@pop'],
[/./, 'string.command'],
],
},
}
export const LANGUAGE_ID = 'resolutionflow'

View File

@@ -0,0 +1,71 @@
import type { editor } from 'monaco-editor'
/**
* Dark theme for ResolutionFlow markdown syntax.
* Matches the monochrome design system with functional color for node types.
*/
export const resolutionFlowTheme: editor.IStandaloneThemeData = {
base: 'vs-dark',
inherit: true,
rules: [
// Frontmatter
{ token: 'delimiter.frontmatter', foreground: '6b7280' },
{ token: 'keyword.frontmatter', foreground: '9ca3af' },
{ token: 'string.id', foreground: 'e5e7eb', fontStyle: 'bold' },
{ token: 'type.value', foreground: '60a5fa' },
{ token: 'string.parent', foreground: 'a78bfa' },
// Headings
{ token: 'heading.decision', foreground: '60a5fa', fontStyle: 'bold' },
{ token: 'heading.action', foreground: 'fbbf24', fontStyle: 'bold' },
// Options
{ token: 'keyword.option', foreground: '60a5fa' },
{ token: 'string.option', foreground: 'e5e7eb' },
// References
{ token: 'variable.reference', foreground: 'a78bfa' },
// Variables
{ token: 'variable.input', foreground: 'fb923c', fontStyle: 'bold' },
{ token: 'variable.save', foreground: 'fb923c' },
// Commands
{ token: 'delimiter.commands', foreground: '6b7280' },
{ token: 'string.command', foreground: '34d399' },
// Expected outcome
{ token: 'keyword.expected', foreground: '34d399' },
// Ordered list
{ token: 'keyword.step', foreground: '9ca3af' },
// Help text
{ token: 'string.blockquote', foreground: '9ca3af', fontStyle: 'italic' },
// Formatting
{ token: 'strong', fontStyle: 'bold' },
{ token: 'string.code', foreground: '34d399' },
// Default
{ token: 'text', foreground: 'd1d5db' },
],
colors: {
'editor.background': '#0a0a0a',
'editor.foreground': '#d1d5db',
'editor.lineHighlightBackground': '#ffffff08',
'editor.selectionBackground': '#ffffff20',
'editorCursor.foreground': '#ffffff',
'editor.inactiveSelectionBackground': '#ffffff10',
'editorLineNumber.foreground': '#4b5563',
'editorLineNumber.activeForeground': '#9ca3af',
'editorGutter.background': '#0a0a0a',
'editorWidget.background': '#111111',
'editorWidget.border': '#ffffff10',
'editorSuggestWidget.background': '#111111',
'editorSuggestWidget.border': '#ffffff10',
'editorSuggestWidget.selectedBackground': '#ffffff15',
},
}
export const THEME_ID = 'resolutionflow-dark'