import { PlusIcon } from "lucide-react"; import { Suspense, useEffect, useRef, useState, type ComponentProps, type ComponentType, } from "react"; import type { ComposerMobilePlusPanel as ComposerMobilePlusPanelType, ComposerPlusProps, Panel, } from "./composer-mobile-plus-panel"; import { Button } from "../../ui/button"; import { COMPOSER_MENU_PANEL_CLASS } from "../../ui/composer-menu"; import { Dialog } from "../../ui/dialog"; import { MENU_NOTE_CLASS } from "../../ui/menu-styles"; import { ComposerMenuRowsSkeleton, lazyComposerPanel, preloadComposerMenuPanels, } from "../../ui/composer-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "../../ui/dropdown-menu"; export type { ComposerPlusProps, Panel as ComposerPlusPanel } from "./composer-mobile-plus-panel"; type PanelProps = ComponentProps; /** The "+" menu itself: rendered at once when "+" opens after the preload. */ const LazyComposerPanel = lazyComposerPanel(() => import("./composer-mobile-plus-panel") .then((module) => module.ComposerMobilePlusPanel) .catch(() => ComposerPanelLoadFailed as ComponentType), ); /** The "+" menu and every drill-in body, loaded before the first open. */ function preloadComposerMenus() { preloadComposerMenuPanels(); } function ComposerPanelNotice(props: ComposerPlusProps & { failed?: boolean | undefined }) { return ( {props.failed ? (

Composer actions could not be loaded.

) : ( )} {props.failed ? ( ) : null}
); } function ComposerPanelLoadFailed(props: ComposerPlusProps) { return ; } /** Keep the composer trigger/state eager; load its optional menu only when opened. */ export function ComposerMobilePlus( props: ComposerPlusProps & { /** Opens the menu on one panel, for example from the capabilities chip. */ openRequest?: { panel: Panel; nonce: number } | undefined; }, ) { const triggerRef = useRef(null); const dialogFocusOwnerRef = useRef(false); const [open, setOpen] = useState(false); const [panel, setPanel] = useState("root"); const openNonce = props.openRequest?.nonce; useEffect(() => { if (openNonce === undefined || !props.openRequest) return; setPanel(props.openRequest.panel); setOpen(true); // oxlint-disable-next-line react-hooks/exhaustive-deps -- react to a new request only }, [openNonce]); // Warm the menus once the composer is idle, so "+" never opens onto a load. useEffect(() => { const idleWindow = window as Window & { requestIdleCallback?: ((run: () => void) => number) | undefined; cancelIdleCallback?: ((handle: number) => void) | undefined; }; if (idleWindow.requestIdleCallback && idleWindow.cancelIdleCallback) { const handle = idleWindow.requestIdleCallback(preloadComposerMenus); return () => idleWindow.cancelIdleCallback?.(handle); } const handle = window.setTimeout(preloadComposerMenus, 1500); return () => window.clearTimeout(handle); }, []); // The usage limit panel holds a text field, which a menu cannot contain, so // it always opens as a dialog. const dialogOpen = open && panel !== "root" && (panel === "usage-limit" || (panel !== "tools" && panel !== "capabilities" && panel !== "settings" && panel !== "visibility" && props.expandedPanelPresentation === "dialog")); // The retired menu's deferred callback must observe the current owner, // including after the lazy panel has changed presentation or unmounted. dialogFocusOwnerRef.current = dialogOpen; return ( { if (!next) { setOpen(false); setPanel("root"); } }} > { if (dialogOpen) return; setOpen(next); if (!next) setPanel("root"); }} > {open ? ( }> ) : null} ); }