import { useEffect, useRef, useState, type RefObject } from "react"; import { createPortal } from "react-dom"; import { Settings2 } from "lucide-react"; import { Button } from "antd"; import { AudioSettingsPanel } from "@/components/audio-settings-panel"; import { audioFormatLabel, audioSpeedLabel, audioVoiceLabel } from "@/lib/audio-generation"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import type { AiConfig } from "@/stores/use-config-store"; export type CanvasAudioSettingKey = "audioVoice" | "audioFormat" | "audioSpeed" | "audioInstructions"; type CanvasAudioSettingsPopoverProps = { config: AiConfig; onConfigChange: (key: CanvasAudioSettingKey, value: string) => void; buttonClassName?: string; placement?: "topLeft" | "top" | "topRight" | "bottomLeft" | "bottom" | "bottomRight"; }; export function CanvasAudioSettingsPopover({ config, onConfigChange, buttonClassName, placement = "topLeft" }: CanvasAudioSettingsPopoverProps) { 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)) return; if (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 AudioSettingsPortal({ buttonRect, panelRef, placement, theme, config, onConfigChange, }: { buttonRect: DOMRect; panelRef: RefObject; placement: CanvasAudioSettingsPopoverProps["placement"]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; config: AiConfig; onConfigChange: (key: CanvasAudioSettingKey, value: string) => void; }) { 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, maxHeight: Math.max(260, buttonRect.top - margin * 2) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, window.innerHeight - buttonRect.bottom - margin * 2) }), background: theme.toolbar.panel, borderRadius: 18, boxShadow: "0 18px 54px rgba(28, 25, 23, 0.16)", padding: 18, overflowY: "auto", color: theme.node.text, } as const; return createPortal(
event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()} > onConfigChange(key, value)} theme={theme} className="space-y-4" />
, document.body, ); }