/* Copyright 2026 Marimo. All rights reserved. */ import type { JSX, PropsWithChildren } from "react"; import React from "react"; import { z } from "zod"; import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle, } from "@/components/ui/navigation"; import { Tooltip } from "@/components/ui/tooltip"; import { renderHTML } from "@/plugins/core/RenderHTML"; import { cn } from "@/utils/cn"; import { appendQueryParams } from "@/utils/urls"; import type { IStatelessPlugin, IStatelessPluginProps, } from "../stateless-plugin"; import "./navigation-menu.css"; import { KnownQueryParams } from "@/core/constants"; interface MenuItem { label: string; href: string; description?: string | null; } interface MenuItemGroup { label: string; items: MenuItem[]; } interface Data { /** * The labels for each item; raw HTML. */ items: (MenuItem | MenuItemGroup)[]; /** * The orientation of the menu. */ orientation: "horizontal" | "vertical"; } export class NavigationMenuPlugin implements IStatelessPlugin { tagName = "marimo-nav-menu"; private menuItemValidator = z.object({ label: z.string(), href: z.string(), description: z.string().nullish(), }); private menuItemGroupValidator = z.object({ label: z.string(), items: z.array(this.menuItemValidator), }); validator = z.object({ items: z.array( z.union([this.menuItemValidator, this.menuItemGroupValidator]), ), orientation: z.enum(["horizontal", "vertical"]), }); render(props: IStatelessPluginProps): JSX.Element { return ; } } type NavMenuComponentProps = Data; const NavMenuComponent = ({ items, orientation, }: PropsWithChildren): JSX.Element => { const maybeWithTooltip = ( component: JSX.Element, description?: string | null, ) => { return description ? ( {component} ) : ( component ); }; const target = (href: string) => { if (href.startsWith("http")) { return "_blank"; } return "_self"; }; const preserveQueryParams = (href: string) => { const currentUrl = new URL(globalThis.location.href); return appendQueryParams({ href, queryParams: currentUrl.search, keys: [KnownQueryParams.filePath], }); }; const renderMenuItem = (item: MenuItem | MenuItemGroup) => { if ("items" in item) { return orientation === "horizontal" ? ( {renderHTML({ html: item.label })}
    {item.items.map((subItem) => ( {subItem.description && renderHTML({ html: subItem.description })} ))}
) : (
{renderHTML({ html: item.label })}
{item.items.map((subItem) => ( {maybeWithTooltip( {renderHTML({ html: subItem.label })} , subItem.description, )} ))}
); } return ( {renderHTML({ html: item.label })} ); }; return ( {items.map((item) => renderMenuItem(item))} ); }; const ListItem = React.forwardRef< React.ElementRef<"a">, React.ComponentPropsWithoutRef<"a"> & { label: string; } >(({ className, label, children, ...props }, ref) => { return (
  • {renderHTML({ html: label })}
    {children && (

    {children}

    )}
  • ); }); ListItem.displayName = "ListItem";