feat: replace textarea with Lexical WYSIWYG markdown composer

Replace the plain <textarea> in ChatPane with a full WYSIWYG
markdown composer built on Lexical 0.42.0.

Composer features:
- Rich text editing with inline formatting (bold, italic, code)
- Block-level elements: headings, bullet/numbered lists, code blocks,
  blockquotes, links via markdown shortcuts (e.g. ## , - , \\\)
- Formatting toolbar with active-state indicators
- Markdown paste auto-import when the editor is empty
- Enter to send, Shift+Enter for newline (matches prior behavior)
- Disabled state syncs with session busy/config states
- Serializes to markdown on submit via the backend helpers

Files:
- New: MarkdownComposer.tsx (Lexical editor, toolbar, internal plugins)
- Modified: ChatPane.tsx (swap textarea for MarkdownComposer, simplify state)
- Modified: styles.css (add markdown composer theme classes)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 19:21:13 +01:00
co-authored by Copilot
parent d07bf30e81
commit 77194d1f01
3 changed files with 524 additions and 25 deletions
+106
View File
@@ -140,6 +140,112 @@ textarea {
max-height: 200px;
}
/* ── Markdown composer (Lexical) ────────────────────────── */
.markdown-composer-content {
position: relative;
}
.markdown-composer-editable {
min-height: 40px;
max-height: 200px;
overflow-y: auto;
padding: 8px 48px 8px 16px;
font-size: 14px;
line-height: 1.6;
color: #f4f4f5;
outline: none;
cursor: text;
}
.markdown-composer-editable[contenteditable="false"] {
opacity: 0.5;
cursor: not-allowed;
}
.markdown-composer-placeholder {
position: absolute;
top: 8px;
left: 16px;
font-size: 14px;
line-height: 1.6;
color: #52525b;
pointer-events: none;
user-select: none;
}
/* Paragraphs */
.mc-p {
margin: 0;
}
/* Headings */
.mc-h1, .mc-h2, .mc-h3 {
font-weight: 600;
margin: 0;
line-height: 1.4;
}
.mc-h1 { font-size: 1.1em; }
.mc-h2 { font-size: 1.05em; }
.mc-h3 { font-size: 1em; }
/* Inline formatting */
.mc-bold { font-weight: 600; }
.mc-italic { font-style: italic; }
.mc-strikethrough { text-decoration: line-through; }
.mc-underline { text-decoration: underline; }
.mc-inline-code {
background: rgba(63, 63, 70, 0.6);
border-radius: 3px;
padding: 1px 4px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
font-size: 0.9em;
}
/* Lists */
.mc-ul {
list-style-type: disc;
padding-left: 1.5em;
margin: 2px 0;
}
.mc-ol {
list-style-type: decimal;
padding-left: 1.5em;
margin: 2px 0;
}
.mc-li {
margin: 1px 0;
}
/* Blockquote */
.mc-blockquote {
border-left: 3px solid #3f3f46;
padding-left: 0.75em;
color: #a1a1aa;
margin: 2px 0;
}
/* Code block */
.mc-code-block {
background: rgba(24, 24, 27, 0.8);
border: 1px solid #27272a;
border-radius: 6px;
padding: 8px 12px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
font-size: 0.9em;
white-space: pre-wrap;
tab-size: 2;
margin: 4px 0;
overflow-x: auto;
}
/* Link */
.mc-link {
color: #818cf8;
text-decoration: underline;
text-underline-offset: 2px;
}
/* React Flow dark theme overrides */
.react-flow__node {
font-family: inherit;