"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { Share2, Copy, MoreHorizontal, Check } from "lucide-react"; import { formatCopyText } from "@/lib/source-reference"; import { KautilyaIcon, useMediaQuery, useCopyFeedback, type Position, type ToolbarAction, } from "./selection-toolbar/shared"; import { FloatingToolbar } from "./selection-toolbar/floating-toolbar"; import { ShareModal } from "./selection-toolbar/share-modal"; import { CollectionsDrawer } from "./selection-toolbar/collections-drawer"; import { MorePopover } from "./selection-toolbar/more-popover"; // --------------------------------------------------------------------------- // Main Component // --------------------------------------------------------------------------- export function TextSelectionToolbar() { const [selectedText, setSelectedText] = useState(""); const [toolbarPos, setToolbarPos] = useState(null); const [showMore, setShowMore] = useState(false); const [morePos, setMorePos] = useState({ x: 0, y: 0 }); const [showShare, setShowShare] = useState(false); const [showCollections, setShowCollections] = useState(false); const { copied, copy } = useCopyFeedback(); const isMobile = useMediaQuery("(max-width: 767px)"); // Track whether the user is interacting with our toolbar/popovers const interactingRef = useRef(false); const clearSelection = useCallback(() => { setSelectedText(""); setToolbarPos(null); setShowMore(false); }, []); // Listen for selection changes useEffect(() => { function handleSelectionChange() { // Don't dismiss while interacting with toolbar menus if (interactingRef.current || showMore || showShare || showCollections) { return; } const sel = window.getSelection(); const text = sel?.toString().trim() ?? ""; if (text.length < 2) { clearSelection(); return; } // Check that the selection is inside a content area const anchor = sel?.anchorNode; if (!anchor) { clearSelection(); return; } const el = anchor.nodeType === Node.ELEMENT_NODE ? (anchor as Element) : anchor.parentElement; if (!el) { clearSelection(); return; } // Don't show toolbar for selections inside inputs, textareas, or toolbar itself const tag = el.closest( "input, textarea, select, [data-selection-toolbar], [data-slot=card-header], nav, header, aside, [role=dialog]", ); if (tag) { clearSelection(); return; } // Must be inside main content area const isInContent = el.closest( "main, [data-slot=card-content], article, .prose, [data-content-area]", ); if (!isInContent) { clearSelection(); return; } // Get the position of the selection const range = sel?.getRangeAt(0); if (!range) { clearSelection(); return; } // iOS Safari fix: getBoundingClientRect() can return a zero rect let rect = range.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) { const rects = range.getClientRects(); if (rects.length > 0) { rect = rects[0]; } } if (!rect || (rect.width === 0 && rect.height === 0)) { clearSelection(); return; } setSelectedText(text); setToolbarPos({ x: rect.left + rect.width / 2, y: rect.top >= 60 ? rect.top : rect.bottom, }); } // Debounce so the toolbar doesn't flicker during active selection let debounceTimer: ReturnType | null = null; function debouncedCheck(delay: number) { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(handleSelectionChange, delay); } function onSelectionChange() { debouncedCheck(150); } function onMouseUp() { debouncedCheck(50); } function onTouchEnd() { debouncedCheck(200); } function onTouchStart() { interactingRef.current = false; } function onKeyUp(e: KeyboardEvent) { if (e.key === "Escape") { clearSelection(); window.getSelection()?.removeAllRanges(); } } document.addEventListener("selectionchange", onSelectionChange); document.addEventListener("mouseup", onMouseUp); document.addEventListener("touchend", onTouchEnd); document.addEventListener("touchstart", onTouchStart); document.addEventListener("keyup", onKeyUp); return () => { if (debounceTimer) clearTimeout(debounceTimer); document.removeEventListener("selectionchange", onSelectionChange); document.removeEventListener("mouseup", onMouseUp); document.removeEventListener("touchend", onTouchEnd); document.removeEventListener("touchstart", onTouchStart); document.removeEventListener("keyup", onKeyUp); }; }, [clearSelection, showMore, showShare, showCollections]); // Intercept native Ctrl+C / Cmd+C to append source reference useEffect(() => { function handleCopy(e: ClipboardEvent) { const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) return; const text = sel.toString().trim(); if (text.length < 2) return; const anchor = sel.anchorNode; if (!anchor) return; const el = anchor.nodeType === Node.ELEMENT_NODE ? (anchor as Element) : anchor.parentElement; if (!el) return; if (el.closest("input, textarea, select, [role=dialog]")) return; const inContent = el.closest( "main, [data-slot=card-content], article, .prose, [data-content-area]", ); if (!inContent) return; if (window.location.pathname.startsWith("/rtfs/")) return; e.preventDefault(); const formatted = formatCopyText(text); e.clipboardData?.setData("text/plain", formatted); } document.addEventListener("copy", handleCopy); return () => document.removeEventListener("copy", handleCopy); }, []); // Close toolbar on mouse click outside (desktop only) useEffect(() => { function handlePointerDown(e: PointerEvent) { if (e.pointerType !== "mouse") return; const target = e.target as Element; if (target.closest("[data-selection-toolbar]")) return; if (showMore || showShare || showCollections) return; const sel = window.getSelection(); const text = sel?.toString().trim() ?? ""; if (text.length < 2) { clearSelection(); } } document.addEventListener("pointerdown", handlePointerDown); return () => document.removeEventListener("pointerdown", handlePointerDown); }, [clearSelection, showMore, showShare, showCollections]); // Don't render on RTF viewer or if nothing is selected if ( typeof window !== "undefined" && window.location.pathname.startsWith("/rtfs/") ) { return null; } if (!toolbarPos || !selectedText) return null; // --- Toolbar actions --- const toolbarActions: ToolbarAction[] = [ { key: "ask", label: "Ask Kautilya", icon: , onClick: () => { // TODO: Implement Ask Kautilya clearSelection(); }, className: "text-amber-400 hover:bg-amber-400/15 hover:text-amber-300", }, { key: "share", label: "Share", icon: , onClick: () => { interactingRef.current = true; setShowShare(true); }, }, { key: "copy", label: copied ? "Copied!" : "Copy", icon: copied ? ( ) : ( ), onClick: () => { copy(formatCopyText(selectedText)); }, className: copied ? "text-green-400 bg-green-400/15" : "text-white/70 hover:bg-white/10 hover:text-white", }, { key: "more", label: "More", icon: , onClick: () => { interactingRef.current = true; setMorePos({ x: toolbarPos.x + 60, y: toolbarPos.y, }); setShowMore(true); }, }, ]; // --- Render --- return (
{ setShowMore(false); interactingRef.current = false; }} onBookmark={() => { setShowMore(false); interactingRef.current = false; }} onSaveToCollection={() => { setShowMore(false); setShowCollections(true); }} onMention={() => { setShowMore(false); interactingRef.current = false; }} /> { setShowShare(false); interactingRef.current = false; }} selectedText={selectedText} isMobile={isMobile} /> { setShowCollections(false); interactingRef.current = false; }} isMobile={isMobile} />
); }