summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/files/BodyRenderer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/files/BodyRenderer.tsx')
-rw-r--r--makima/frontend/src/components/files/BodyRenderer.tsx121
1 files changed, 120 insertions, 1 deletions
diff --git a/makima/frontend/src/components/files/BodyRenderer.tsx b/makima/frontend/src/components/files/BodyRenderer.tsx
index 867fc4c..cf99fde 100644
--- a/makima/frontend/src/components/files/BodyRenderer.tsx
+++ b/makima/frontend/src/components/files/BodyRenderer.tsx
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from "react";
import type { BodyElement } from "../../lib/api";
import { ChartRenderer } from "../charts/ChartRenderer";
+import { ElementContextMenu } from "./ElementContextMenu";
interface BodyRendererProps {
elements: BodyElement[];
@@ -10,11 +11,54 @@ interface BodyRendererProps {
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 }: BodyRendererProps) {
+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 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 (
@@ -73,6 +117,7 @@ export function BodyRenderer({ elements, isEditing = false, onUpdate, onReorder,
onDragOver={handleDragOver(index)}
onDragLeave={handleDragLeave}
onDrop={handleDrop(index)}
+ onContextMenu={handleContextMenu(index)}
>
{/* Drag handle - only show in edit mode */}
{isEditing && onReorder && (
@@ -109,6 +154,24 @@ export function BodyRenderer({ elements, isEditing = false, onUpdate, onReorder,
</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>
);
}
@@ -156,6 +219,20 @@ function BodyElementRenderer({
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
@@ -502,3 +579,45 @@ function ImageElement({
</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>
+ );
+}