import { useEffect, useRef, useState, type RefObject } from "react"; import { createPortal } from "react-dom"; import { Settings2 } from "lucide-react"; import { Button } from "antd"; import { reasoningEffortLabel, TextSettingsPanel } from "@/components/text-settings-panel"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import type { AiConfig, ReasoningEffort } from "@/stores/use-config-store"; type CanvasTextSettingsPopoverProps = { config: AiConfig; onConfigChange: (key: "reasoningEffort", value: ReasoningEffort) => void; buttonClassName?: string; placement?: "topLeft" | "top" | "topRight" | "bottomLeft" | "bottom" | "bottomRight"; }; export function CanvasTextSettingsPopover({ config, onConfigChange, buttonClassName, placement = "topLeft" }: CanvasTextSettingsPopoverProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const buttonRef = useRef(null); const panelRef = useRef(null); const [open, setOpen] = useState(false); const [buttonRect, setButtonRect] = useState(null); useEffect(() => { if (!open) return; const syncPosition = () => setButtonRect(buttonRef.current?.getBoundingClientRect() || null); const closeOnOutsidePointer = (event: PointerEvent) => { const target = event.target; if (!(target instanceof Node) || buttonRef.current?.contains(target) || panelRef.current?.contains(target)) return; setOpen(false); }; syncPosition(); window.addEventListener("resize", syncPosition); window.addEventListener("scroll", syncPosition, true); window.addEventListener("pointerdown", closeOnOutsidePointer, true); return () => { window.removeEventListener("resize", syncPosition); window.removeEventListener("scroll", syncPosition, true); window.removeEventListener("pointerdown", closeOnOutsidePointer, true); }; }, [open]); const panel = open && buttonRect ? : null; return ( <> {panel} ); } function TextSettingsPortal({ buttonRect, panelRef, placement, theme, config, onConfigChange }: { buttonRect: DOMRect; panelRef: RefObject; placement: CanvasTextSettingsPopoverProps["placement"]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; config: AiConfig; onConfigChange: CanvasTextSettingsPopoverProps["onConfigChange"]; }) { const width = 356; const gap = 8; const margin = 12; const alignRight = placement?.endsWith("Right"); const alignCenter = placement === "top" || placement === "bottom"; const left = alignCenter ? buttonRect.left + buttonRect.width / 2 - width / 2 : alignRight ? buttonRect.right - width : buttonRect.left; const topPlacement = placement?.startsWith("top"); const style = { position: "fixed", zIndex: 1200, width, left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)), ...(topPlacement ? { bottom: window.innerHeight - buttonRect.top + gap } : { top: buttonRect.bottom + gap }), background: theme.toolbar.panel, borderRadius: 18, boxShadow: "0 18px 54px rgba(28, 25, 23, 0.16)", padding: 18, color: theme.node.text, } as const; return createPortal(
event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
, document.body, ); }