import { useState, useRef, useEffect } from "react";
import type { BodyElement } from "../../lib/api";
import { ChartRenderer } from "../charts/ChartRenderer";
import { ElementContextMenu } from "./ElementContextMenu";
import { copyMarkdownToClipboard } from "../../lib/markdown";
interface BodyRendererProps {
elements: BodyElement[];
isEditing?: boolean;
onUpdate?: (index: number, element: BodyElement) => void;
onReorder?: (fromIndex: number, toIndex: number) => void;
onEditingChange?: (isEditing: boolean) => void;
hasPendingRemoteUpdate?: boolean;
onOverwrite?: () => void;
onFocusElement?: (index: number) => void;
onDeleteElement?: (index: number) => void;
onDuplicateElement?: (index: number) => void;
onConvertElement?: (index: number, toType: string) => void;
onGenerateFromElement?: (index: number, action: string) => void;
onCreateTaskFromElement?: (index: number, selectedText?: string) => void;
}
export function BodyRenderer({
elements,
isEditing = false,
onUpdate,
onReorder,
onEditingChange,
hasPendingRemoteUpdate,
onOverwrite,
onFocusElement,
onDeleteElement,
onDuplicateElement,
onConvertElement,
onGenerateFromElement,
onCreateTaskFromElement,
}: BodyRendererProps) {
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
elementIndex: number;
selectedText?: string;
} | null>(null);
const [copiedMarkdown, setCopiedMarkdown] = useState(false);
const handleCopyMarkdown = async () => {
const success = await copyMarkdownToClipboard(elements);
if (success) {
setCopiedMarkdown(true);
setTimeout(() => setCopiedMarkdown(false), 2000);
}
};
const handleContextMenu = (index: number) => (e: React.MouseEvent) => {
e.preventDefault();
// Get any selected text
const selection = window.getSelection();
const selectedText = selection?.toString().trim() || undefined;
setContextMenu({
x: e.clientX,
y: e.clientY,
elementIndex: index,
selectedText,
});
};
const closeContextMenu = () => {
setContextMenu(null);
};
if (elements.length === 0) {
return (
<div className="text-[#555] font-mono text-sm italic">
No content yet. Use the CLI below to add content.
</div>
);
}
const handleDragStart = (index: number) => (e: React.DragEvent) => {
setDraggedIndex(index);
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", index.toString());
};
const handleDragOver = (index: number) => (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
if (draggedIndex !== null && draggedIndex !== index) {
setDragOverIndex(index);
}
};
const handleDragLeave = () => {
setDragOverIndex(null);
};
const handleDrop = (toIndex: number) => (e: React.DragEvent) => {
e.preventDefault();
const fromIndex = draggedIndex;
setDraggedIndex(null);
setDragOverIndex(null);
if (fromIndex !== null && fromIndex !== toIndex && onReorder) {
onReorder(fromIndex, toIndex);
}
};
const handleDragEnd = () => {
setDraggedIndex(null);
setDragOverIndex(null);
};
return (
<div className="space-y-1">
{/* Markdown Export Toolbar */}
<div className="flex justify-end mb-2">
<button
onClick={handleCopyMarkdown}
className="flex items-center gap-1 px-2 py-1 text-[10px] font-mono text-[#555] hover:text-[#75aafc] transition-colors"
title="Copy content as markdown"
>
{copiedMarkdown ? (
<>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="20 6 9 17 4 12" />
</svg>
Copied!
</>
) : (
<>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
<rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
</svg>
Copy as Markdown
</>
)}
</button>
</div>
{elements.map((element, index) => (
<div
key={index}
className={`group flex items-start gap-2 py-1 transition-all ${
draggedIndex === index ? "opacity-50" : ""
} ${
dragOverIndex === index
? "border-t-2 border-[#75aafc] -mt-[2px] pt-[calc(0.25rem+2px)]"
: ""
}`}
onDragOver={handleDragOver(index)}
onDragLeave={handleDragLeave}
onDrop={handleDrop(index)}
onContextMenu={handleContextMenu(index)}
>
{/* Drag handle - only show in edit mode */}
{isEditing && onReorder && (
<div
draggable
onDragStart={handleDragStart(index)}
onDragEnd={handleDragEnd}
className="flex-shrink-0 w-5 h-6 flex items-center justify-center cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 transition-opacity text-[#555] hover:text-[#75aafc]"
title="Drag to reorder"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="currentColor"
>
<circle cx="3" cy="2" r="1.5" />
<circle cx="9" cy="2" r="1.5" />
<circle cx="3" cy="6" r="1.5" />
<circle cx="9" cy="6" r="1.5" />
<circle cx="3" cy="10" r="1.5" />
<circle cx="9" cy="10" r="1.5" />
</svg>
</div>
)}
<div className="flex-1">
<BodyElementRenderer
element={element}
onUpdate={isEditing && onUpdate ? (el) => onUpdate(index, el) : undefined}
onEditingChange={onEditingChange}
hasPendingRemoteUpdate={hasPendingRemoteUpdate}
onOverwrite={onOverwrite}
/>
</div>
</div>
))}
{/* Context Menu */}
{contextMenu && (
<ElementContextMenu
x={contextMenu.x}
y={contextMenu.y}
element={elements[contextMenu.elementIndex]}
elementIndex={contextMenu.elementIndex}
selectedText={contextMenu.selectedText}
onClose={closeContextMenu}
onFocus={(index) => onFocusElement?.(index)}
onDelete={(index) => onDeleteElement?.(index)}
onDuplicate={(index) => onDuplicateElement?.(index)}
onConvert={(index, toType) => onConvertElement?.(index, toType)}
onGenerate={(index, action) => onGenerateFromElement?.(index, action)}
onCreateTask={(index, selectedText) => onCreateTaskFromElement?.(index, selectedText)}
/>
)}
</div>
);
}
function BodyElementRenderer({
element,
onUpdate,
onEditingChange,
hasPendingRemoteUpdate,
onOverwrite,
}: {
element: BodyElement;
onUpdate?: (element: BodyElement) => void;
onEditingChange?: (isEditing: boolean) => void;
hasPendingRemoteUpdate?: boolean;
onOverwrite?: () => void;
}) {
switch (element.type) {
case "heading":
return (
<HeadingElement
level={element.level}
text={element.text}
onUpdate={
onUpdate
? (text) => onUpdate({ ...element, text })
: undefined
}
onEditingChange={onEditingChange}
hasPendingRemoteUpdate={hasPendingRemoteUpdate}
onOverwrite={onOverwrite}
/>
);
case "paragraph":
return (
<ParagraphElement
text={element.text}
onUpdate={
onUpdate
? (text) => onUpdate({ ...element, text })
: undefined
}
onEditingChange={onEditingChange}
hasPendingRemoteUpdate={hasPendingRemoteUpdate}
onOverwrite={onOverwrite}
/>
);
case "code":
return (
<CodeElement
language={element.language}
content={element.content}
/>
);
case "list":
return (
<ListElement
ordered={element.ordered}
items={element.items}
/>
);
case "chart":
return (
<ChartElement
chartType={element.chartType}
data={element.data}
title={element.title}
config={element.config}
/>
);
case "image":
return (
<ImageElement
src={element.src}
alt={element.alt}
caption={element.caption}
/>
);
case "markdown":
return (
<MarkdownElement
content={element.content}
onUpdate={
onUpdate
? (content) => onUpdate({ ...element, content })
: undefined
}
onEditingChange={onEditingChange}
hasPendingRemoteUpdate={hasPendingRemoteUpdate}
onOverwrite={onOverwrite}
/>
);
default:
return null;
}
}
function HeadingElement({
level,
text,
onUpdate,
onEditingChange,
hasPendingRemoteUpdate,
onOverwrite,
}: {
level: number;
text: string;
onUpdate?: (text: string) => void;
onEditingChange?: (isEditing: boolean) => void;
hasPendingRemoteUpdate?: boolean;
onOverwrite?: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Only update editText if not currently editing
if (!isEditing) {
setEditText(text);
}
}, [text, isEditing]);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
const startEditing = () => {
setIsEditing(true);
onEditingChange?.(true);
};
const stopEditing = () => {
setIsEditing(false);
onEditingChange?.(false);
};
const handleSave = () => {
// Don't auto-save if there's a pending remote update
if (hasPendingRemoteUpdate) return;
if (onUpdate && editText.trim() !== text) {
onUpdate(editText.trim());
}
stopEditing();
};
const handleOverwrite = () => {
if (onUpdate && editText.trim() !== text) {
onUpdate(editText.trim());
}
onOverwrite?.();
stopEditing();
};
const handleCancel = () => {
setEditText(text);
stopEditing();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
// Disable Enter save if there's a pending remote update
if (e.key === "Enter" && !hasPendingRemoteUpdate) {
handleSave();
} else if (e.key === "Escape") {
handleCancel();
}
};
const handleBlur = () => {
// Don't auto-save on blur if there's a pending remote update
if (!hasPendingRemoteUpdate) {
handleSave();
}
};
const baseClassName = "font-mono text-[#9bc3ff]";
const sizeClasses: Record<number, string> = {
1: "text-2xl font-bold",
2: "text-xl font-bold",
3: "text-lg font-semibold",
4: "text-base font-semibold",
5: "text-sm font-semibold",
6: "text-xs font-semibold",
};
const sizeClass = sizeClasses[level] || sizeClasses[3];
if (isEditing && onUpdate) {
return (
<div>
<input
ref={inputRef}
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className={`${baseClassName} ${sizeClass} w-full bg-transparent border-b border-[#3f6fb3] outline-none`}
/>
{hasPendingRemoteUpdate && (
<div className="flex items-center gap-2 mt-2">
<span className="text-yellow-500 text-xs font-mono">Remote update pending</span>
<button
onClick={handleOverwrite}
onMouseDown={(e) => e.preventDefault()}
className="px-2 py-1 font-mono text-xs text-yellow-500 border border-yellow-500/50 hover:border-yellow-500 transition-colors"
>
Overwrite
</button>
<button
onClick={handleCancel}
onMouseDown={(e) => e.preventDefault()}
className="px-2 py-1 font-mono text-xs text-[#555] hover:text-white/70 transition-colors"
>
Cancel
</button>
</div>
)}
</div>
);
}
const HeadingTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
return (
<HeadingTag
className={`${baseClassName} ${sizeClass} ${onUpdate ? "cursor-text hover:bg-[rgba(117,170,252,0.05)] px-1 -mx-1 rounded" : ""}`}
onClick={() => onUpdate && startEditing()}
>
{text}
</HeadingTag>
);
}
function ParagraphElement({
text,
onUpdate,
onEditingChange,
hasPendingRemoteUpdate,
onOverwrite,
}: {
text: string;
onUpdate?: (text: string) => void;
onEditingChange?: (isEditing: boolean) => void;
hasPendingRemoteUpdate?: boolean;
onOverwrite?: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
// Only update editText if not currently editing
if (!isEditing) {
setEditText(text);
}
}, [text, isEditing]);
useEffect(() => {
if (isEditing && textareaRef.current) {
textareaRef.current.focus();
// Auto-resize textarea
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = textareaRef.current.scrollHeight + "px";
}
}, [isEditing]);
const startEditing = () => {
setIsEditing(true);
onEditingChange?.(true);
};
const stopEditing = () => {
setIsEditing(false);
onEditingChange?.(false);
};
const handleSave = () => {
// Don't auto-save if there's a pending remote update
if (hasPendingRemoteUpdate) return;
if (onUpdate && editText.trim() !== text) {
onUpdate(editText.trim());
}
stopEditing();
};
const handleOverwrite = () => {
if (onUpdate && editText.trim() !== text) {
onUpdate(editText.trim());
}
onOverwrite?.();
stopEditing();
};
const handleCancel = () => {
setEditText(text);
stopEditing();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
handleCancel();
}
// Ctrl/Cmd + Enter to save - disabled if there's a pending remote update
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && !hasPendingRemoteUpdate) {
handleSave();
}
};
const handleBlur = () => {
// Don't auto-save on blur if there's a pending remote update
if (!hasPendingRemoteUpdate) {
handleSave();
}
};
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditText(e.target.value);
// Auto-resize
e.target.style.height = "auto";
e.target.style.height = e.target.scrollHeight + "px";
};
if (isEditing && onUpdate) {
return (
<div className="relative">
<textarea
ref={textareaRef}
value={editText}
onChange={handleInput}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className="font-mono text-sm text-white/80 leading-relaxed w-full bg-transparent border border-[#3f6fb3] outline-none p-2 resize-none min-h-[60px]"
/>
{hasPendingRemoteUpdate ? (
<div className="flex items-center gap-2 mt-2">
<span className="text-yellow-500 text-xs font-mono">Remote update pending</span>
<button
onClick={handleOverwrite}
onMouseDown={(e) => e.preventDefault()}
className="px-2 py-1 font-mono text-xs text-yellow-500 border border-yellow-500/50 hover:border-yellow-500 transition-colors"
>
Overwrite
</button>
<button
onClick={handleCancel}
onMouseDown={(e) => e.preventDefault()}
className="px-2 py-1 font-mono text-xs text-[#555] hover:text-white/70 transition-colors"
>
Cancel
</button>
</div>
) : (
<div className="text-[10px] text-[#555] font-mono mt-1">
Ctrl+Enter to save, Esc to cancel
</div>
)}
</div>
);
}
return (
<p
className={`font-mono text-sm text-white/80 leading-relaxed ${onUpdate ? "cursor-text hover:bg-[rgba(117,170,252,0.05)] px-1 -mx-1 rounded" : ""}`}
onClick={() => onUpdate && startEditing()}
>
{text}
</p>
);
}
function ChartElement({
chartType,
data,
title,
config,
}: {
chartType: "line" | "bar" | "pie" | "area";
data: Record<string, unknown>[];
title?: string;
config?: Record<string, unknown>;
}) {
return (
<div className="border border-[#333] p-4 bg-black/30">
<ChartRenderer
chartType={chartType}
data={data}
title={title}
config={config}
/>
</div>
);
}
function ImageElement({
src,
alt,
caption,
}: {
src: string;
alt?: string;
caption?: string;
}) {
return (
<figure className="space-y-2">
<img
src={src}
alt={alt || ""}
className="max-w-full border border-[#333]"
/>
{caption && (
<figcaption className="text-[#555] font-mono text-xs italic">
{caption}
</figcaption>
)}
</figure>
);
}
function CodeElement({
language,
content,
}: {
language?: string;
content: string;
}) {
return (
<div className="relative">
{language && (
<div className="absolute top-0 right-0 px-2 py-0.5 font-mono text-[10px] text-[#555] bg-[#1a1a1a] border-b border-l border-[#333]">
{language}
</div>
)}
<pre className="bg-[#0d0d0d] border border-[#333] p-4 overflow-x-auto">
<code className="font-mono text-sm text-[#9bc3ff] whitespace-pre">
{content}
</code>
</pre>
</div>
);
}
function ListElement({
ordered,
items,
}: {
ordered: boolean;
items: string[];
}) {
const ListTag = ordered ? "ol" : "ul";
return (
<ListTag className={`font-mono text-sm text-white/80 leading-relaxed pl-6 space-y-1 ${ordered ? "list-decimal" : "list-disc"}`}>
{items.map((item, index) => (
<li key={index} className="pl-1">
{item}
</li>
))}
</ListTag>
);
}
/**
* Simple inline markdown renderer.
* Renders basic markdown syntax to HTML elements.
*/
function renderMarkdown(content: string): React.ReactNode {
const lines = content.split('\n');
const elements: React.ReactNode[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Code blocks
if (line.startsWith('```')) {
const lang = line.slice(3).trim();
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i].startsWith('```')) {
codeLines.push(lines[i]);
i++;
}
i++; // skip closing ```
elements.push(
<div key={elements.length} className="relative my-2">
{lang && (
<div className="absolute top-0 right-0 px-2 py-0.5 font-mono text-[10px] text-[#555] bg-[#1a1a1a] border-b border-l border-[#333]">
{lang}
</div>
)}
<pre className="bg-[#0d0d0d] border border-[#333] p-4 overflow-x-auto">
<code className="font-mono text-sm text-[#9bc3ff] whitespace-pre">
{codeLines.join('\n')}
</code>
</pre>
</div>
);
continue;
}
// Headings
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const text = headingMatch[2];
const HeadingTag = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
const sizeClasses: Record<number, string> = {
1: "text-2xl font-bold",
2: "text-xl font-bold",
3: "text-lg font-semibold",
4: "text-base font-semibold",
5: "text-sm font-semibold",
6: "text-xs font-semibold",
};
elements.push(
<HeadingTag key={elements.length} className={`font-mono text-[#9bc3ff] ${sizeClasses[level]} my-2`}>
{renderInlineMarkdown(text)}
</HeadingTag>
);
i++;
continue;
}
// Unordered lists
if (line.match(/^[-*]\s+/)) {
const items: string[] = [];
while (i < lines.length && lines[i].match(/^[-*]\s+/)) {
items.push(lines[i].replace(/^[-*]\s+/, ''));
i++;
}
elements.push(
<ul key={elements.length} className="font-mono text-sm text-white/80 leading-relaxed pl-6 space-y-1 list-disc my-2">
{items.map((item, idx) => (
<li key={idx} className="pl-1">{renderInlineMarkdown(item)}</li>
))}
</ul>
);
continue;
}
// Ordered lists
if (line.match(/^\d+\.\s+/)) {
const items: string[] = [];
while (i < lines.length && lines[i].match(/^\d+\.\s+/)) {
items.push(lines[i].replace(/^\d+\.\s+/, ''));
i++;
}
elements.push(
<ol key={elements.length} className="font-mono text-sm text-white/80 leading-relaxed pl-6 space-y-1 list-decimal my-2">
{items.map((item, idx) => (
<li key={idx} className="pl-1">{renderInlineMarkdown(item)}</li>
))}
</ol>
);
continue;
}
// Empty lines
if (line.trim() === '') {
i++;
continue;
}
// Regular paragraphs
elements.push(
<p key={elements.length} className="font-mono text-sm text-white/80 leading-relaxed my-2">
{renderInlineMarkdown(line)}
</p>
);
i++;
}
return <>{elements}</>;
}
/**
* Render inline markdown (bold, italic, code, links).
*/
function renderInlineMarkdown(text: string): React.ReactNode {
// Process inline elements: **bold**, *italic*, `code`, [link](url)
const parts: React.ReactNode[] = [];
let remaining = text;
let keyCounter = 0;
while (remaining.length > 0) {
// Check for inline code
const codeMatch = remaining.match(/^`([^`]+)`/);
if (codeMatch) {
parts.push(
<code key={keyCounter++} className="bg-[#1a1a1a] px-1 py-0.5 text-[#9bc3ff] border border-[#333] text-xs">
{codeMatch[1]}
</code>
);
remaining = remaining.slice(codeMatch[0].length);
continue;
}
// Check for bold
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
if (boldMatch) {
parts.push(<strong key={keyCounter++} className="font-bold">{boldMatch[1]}</strong>);
remaining = remaining.slice(boldMatch[0].length);
continue;
}
// Check for italic
const italicMatch = remaining.match(/^\*([^*]+)\*/);
if (italicMatch) {
parts.push(<em key={keyCounter++} className="italic">{italicMatch[1]}</em>);
remaining = remaining.slice(italicMatch[0].length);
continue;
}
// Check for links
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
if (linkMatch) {
parts.push(
<a key={keyCounter++} href={linkMatch[2]} className="text-[#75aafc] hover:underline" target="_blank" rel="noopener noreferrer">
{linkMatch[1]}
</a>
);
remaining = remaining.slice(linkMatch[0].length);
continue;
}
// Find next special character or end
const nextSpecial = remaining.search(/[`*\[]/);
if (nextSpecial === -1) {
parts.push(remaining);
break;
} else if (nextSpecial === 0) {
// Special char at start but didn't match a pattern - treat as text
parts.push(remaining[0]);
remaining = remaining.slice(1);
} else {
parts.push(remaining.slice(0, nextSpecial));
remaining = remaining.slice(nextSpecial);
}
}
return <>{parts}</>;
}
function MarkdownElement({
content,
onUpdate,
onEditingChange,
hasPendingRemoteUpdate,
onOverwrite,
}: {
content: string;
onUpdate?: (content: string) => void;
onEditingChange?: (isEditing: boolean) => void;
hasPendingRemoteUpdate?: boolean;
onOverwrite?: () => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState(content);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (!isEditing) {
setEditContent(content);
}
}, [content, isEditing]);
useEffect(() => {
if (isEditing && textareaRef.current) {
textareaRef.current.focus();
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = textareaRef.current.scrollHeight + "px";
}
}, [isEditing]);
const startEditing = () => {
setIsEditing(true);
onEditingChange?.(true);
};
const stopEditing = () => {
setIsEditing(false);
onEditingChange?.(false);
};
const handleSave = () => {
if (hasPendingRemoteUpdate) return;
if (onUpdate && editContent !== content) {
onUpdate(editContent);
}
stopEditing();
};
const handleOverwrite = () => {
if (onUpdate && editContent !== content) {
onUpdate(editContent);
}
onOverwrite?.();
stopEditing();
};
const handleCancel = () => {
setEditContent(content);
stopEditing();
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
handleCancel();
}
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && !hasPendingRemoteUpdate) {
handleSave();
}
};
const handleBlur = () => {
if (!hasPendingRemoteUpdate) {
handleSave();
}
};
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditContent(e.target.value);
e.target.style.height = "auto";
e.target.style.height = e.target.scrollHeight + "px";
};
if (isEditing && onUpdate) {
return (
<div className="relative">
<div className="text-[10px] text-[#555] font-mono mb-1 flex items-center gap-2">
<span className="text-[#75aafc]">Editing Markdown</span>
</div>
<textarea
ref={textareaRef}
value={editContent}
onChange={handleInput}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className="font-mono text-sm text-white/80 leading-relaxed w-full bg-[#0d0d0d] border border-[#3f6fb3] outline-none p-3 resize-none min-h-[120px]"
placeholder="Enter markdown content..."
/>
{hasPendingRemoteUpdate ? (
<div className="flex items-center gap-2 mt-2">
<span className="text-yellow-500 text-xs font-mono">Remote update pending</span>
<button
onClick={handleOverwrite}
onMouseDown={(e) => e.preventDefault()}
className="px-2 py-1 font-mono text-xs text-yellow-500 border border-yellow-500/50 hover:border-yellow-500 transition-colors"
>
Overwrite
</button>
<button
onClick={handleCancel}
onMouseDown={(e) => e.preventDefault()}
className="px-2 py-1 font-mono text-xs text-[#555] hover:text-white/70 transition-colors"
>
Cancel
</button>
</div>
) : (
<div className="text-[10px] text-[#555] font-mono mt-1">
Ctrl+Enter to save, Esc to cancel
</div>
)}
</div>
);
}
return (
<div
className={`border border-[#333] bg-[#0a0a0a] p-4 rounded ${onUpdate ? "cursor-text hover:border-[#3f6fb3] transition-colors" : ""}`}
onClick={() => onUpdate && startEditing()}
>
<div className="text-[10px] text-[#555] font-mono mb-2 flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path d="M14 2v6h6" />
<path d="M16 13H8" />
<path d="M16 17H8" />
<path d="M10 9H8" />
</svg>
<span>Markdown</span>
</div>
{renderMarkdown(content)}
</div>
);
}