diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx new file mode 100644 index 00000000..9a3046e4 --- /dev/null +++ b/frontend/src/components/ContextMenu.tsx @@ -0,0 +1,122 @@ +import { SyntheticEvent, useRef, useEffect } from 'react'; + +export interface MenuOption { + icon?: string; + label: string; + onClick: (event: SyntheticEvent) => void; + variant?: 'primary' | 'danger'; + iconClassName?: string; + iconWidth?: number; + iconHeight?: number; +} + +interface ContextMenuProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + options: MenuOption[]; + anchorRef: React.RefObject; + className?: string; + position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; + offset?: { x: number; y: number }; +} + +export default function ContextMenu({ + isOpen, + setIsOpen, + options, + anchorRef, + className = '', + position = 'bottom-right', + offset = { x: 1, y: 5 }, +}: ContextMenuProps) { + const menuRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const getPositionClasses = () => { + const positionMap = { + 'bottom-right': 'translate-x-1 translate-y-5', + 'bottom-left': '-translate-x-full translate-y-5', + 'top-right': 'translate-x-1 -translate-y-full', + 'top-left': '-translate-x-full -translate-y-full', + }; + return positionMap[position]; + }; + + if (!isOpen) return null; + + const getMenuPosition = () => { + if (!anchorRef.current) return {}; + + const rect = anchorRef.current.getBoundingClientRect(); + return { + top: `${rect.top + window.scrollY + offset.y}px`, + }; + }; + + const getOptionStyles = (option: MenuOption, index: number) => { + if (option.variant === 'danger') { + return ` + dark:text-red-2000 dark:hover:bg-charcoal-grey + text-rosso-corsa hover:bg-bright-gray + }`; + } + + return ` + dark:text-bright-gray dark:hover:bg-charcoal-grey + text-eerie-black hover:bg-bright-gray + }`; + }; + + return ( +
+
+ {options.map((option, index) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index 44156761..d7ad4051 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -8,6 +8,7 @@ import { selectPrompt, } from '../preferences/preferenceSlice'; import Dropdown from '../components/Dropdown'; +import ToggleSwitch from '../components/ToggleSwitch'; import { Doc } from '../models/misc'; import Spinner from '../assets/spinner.svg'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; @@ -101,38 +102,21 @@ export const ShareConversationModal = ({ return (
-

+

{t('modals.shareConv.label')}

-

+

{t('modals.shareConv.note')}

{t('modals.shareConv.option')} - +
{allowPrompt && (
@@ -149,19 +133,19 @@ export const ShareConversationModal = ({
)}
- + {`${domain}/share/${identifier ?? '....'}`} {status === 'fetched' ? ( ) : (