diff options
| author | soryu <soryu@soryu.co> | 2026-04-28 00:18:40 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-28 00:18:40 +0100 |
| commit | c8b169da8cb7eae0957e0ab5e7370b071093a224 (patch) | |
| tree | c3f9720a8acfe863ac0b65df9439abf9a941323a /frontend/src/components/document/ContextMenu.tsx | |
| parent | 3679ceb3325033faa2f889ef3dfee5668ef7aeea (diff) | |
| download | soryu-c8b169da8cb7eae0957e0ab5e7370b071093a224.tar.gz soryu-c8b169da8cb7eae0957e0ab5e7370b071093a224.zip | |
feat: Document UI for directive orchestration with Lexical editor (#93)
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Save previous goal on update and include history in re-planning prompt
* feat: soryu-co/soryu - makima: Install Lexical and create base document editor component
* feat: soryu-co/soryu - makima: Create directive file system sidebar and document layout
* feat: soryu-co/soryu - makima: Create custom Lexical step diagram block
* feat: soryu-co/soryu - makima: Add context menu and goal auto-update integration
* WIP: heartbeat checkpoint
Diffstat (limited to 'frontend/src/components/document/ContextMenu.tsx')
| -rw-r--r-- | frontend/src/components/document/ContextMenu.tsx | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/frontend/src/components/document/ContextMenu.tsx b/frontend/src/components/document/ContextMenu.tsx new file mode 100644 index 0000000..5aed940 --- /dev/null +++ b/frontend/src/components/document/ContextMenu.tsx @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useRef } from 'react'; +import './ContextMenu.css'; + +export interface ContextMenuAction { + label: string; + icon: string; + disabled?: boolean; + onClick: () => void; +} + +export interface ContextMenuProps { + x: number; + y: number; + actions: ContextMenuAction[]; + dividerAfter?: number[]; + onClose: () => void; +} + +export default function ContextMenu({ + x, + y, + actions, + dividerAfter = [], + onClose, +}: ContextMenuProps) { + const menuRef = useRef<HTMLDivElement>(null); + + // Adjust position so menu stays within viewport + const adjustedPosition = useCallback(() => { + const el = menuRef.current; + if (!el) return { left: x, top: y }; + const rect = el.getBoundingClientRect(); + const left = x + rect.width > window.innerWidth ? x - rect.width : x; + const top = y + rect.height > window.innerHeight ? y - rect.height : y; + return { left: Math.max(0, left), top: Math.max(0, top) }; + }, [x, y]); + + // Close on click outside + useEffect(() => { + const handler = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + onClose(); + } + }; + // Use capture so we catch clicks before any other handler + document.addEventListener('mousedown', handler, true); + return () => document.removeEventListener('mousedown', handler, true); + }, [onClose]); + + // Close on Escape + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keydown', handler); + return () => document.removeEventListener('keydown', handler); + }, [onClose]); + + // After mount, adjust position + useEffect(() => { + const el = menuRef.current; + if (!el) return; + const pos = adjustedPosition(); + el.style.left = `${pos.left}px`; + el.style.top = `${pos.top}px`; + }, [adjustedPosition]); + + const dividerSet = new Set(dividerAfter); + + return ( + <div + ref={menuRef} + className="ctx-menu" + style={{ left: x, top: y }} + role="menu" + > + {actions.map((action, i) => ( + <div key={i}> + <button + className={`ctx-menu-item ${action.disabled ? 'ctx-menu-item-disabled' : ''}`} + role="menuitem" + disabled={action.disabled} + onClick={() => { + if (!action.disabled) { + action.onClick(); + onClose(); + } + }} + > + <span className="ctx-menu-icon">{action.icon}</span> + <span className="ctx-menu-label">{action.label}</span> + </button> + {dividerSet.has(i) && <div className="ctx-menu-divider" />} + </div> + ))} + </div> + ); +} |
