From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Tue, 6 Jan 2026 04:08:11 +0000
Subject: Initial Control system

---
 .../frontend/src/components/files/BodyRenderer.tsx | 121 ++++++++++++++++++++-
 1 file changed, 120 insertions(+), 1 deletion(-)

(limited to 'makima/frontend/src/components/files/BodyRenderer.tsx')

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>
+  );
+}
-- 
cgit v1.2.3

