import { CaretDown, DotsThreeVertical } from "@phosphor-icons/react"; import { useEffect, useMemo, useRef, useState, type MouseEvent, type ReactNode, } from "react"; import { cn } from "../../lib/cn"; import { IconButton } from "../actions/icon-button"; import { Menu, type MenuItem } from "../actions/menu"; import { SearchInput } from "../inputs/search-input"; import { PanelFooterRoutes, type PanelFooterRoute } from "../layout/panel"; import { useUhuruPanelState } from "../panel-context"; import { getUhuruPanelLocation, matchesUhuruPanelRoute, useUhuruPanelRoute, } from "./panel-route-context"; import { Margin } from "../layout"; import { filterPanelItems, flattenPanelItems, panelIconMotionClassName, type UhuruPanelAction, type UhuruPanelItem, type UhuruPanelItemRenderContext, } from "./panel-item"; export type { UhuruPanelAction, UhuruPanelItem, UhuruPanelItemRenderContext, } from "./panel-item"; export type UhuruPanelGroup = { collapsible?: boolean; defaultExpanded?: boolean; id: string; icon?: ReactNode; items: UhuruPanelItem[]; label?: ReactNode; }; export type UhuruGroupedPanelProps = { activeId?: string; ariaLabel?: string; className?: string; defaultRoute?: string; footer?: ReactNode; footerRoutes?: PanelFooterRoute[]; groups: UhuruPanelGroup[]; header?: ReactNode; collapsedHeader?: ReactNode; loading?: boolean; emptyState?: ReactNode; onItemSelect?: (item: UhuruPanelItem) => void; renderItem?: ( item: UhuruPanelItem, context: UhuruPanelItemRenderContext, ) => ReactNode; searchPlaceholder?: string; searchable?: boolean; }; export function UhuruGroupedPanel({ activeId, ariaLabel = "Side panel navigation", className, defaultRoute, footer, footerRoutes, groups, header, collapsedHeader, loading = false, emptyState = (
No panel items found.
), onItemSelect, renderItem, searchPlaceholder = "Search panel", searchable = false, }: UhuruGroupedPanelProps) { const panelState = useUhuruPanelState(); const collapsed = panelState?.collapsed ?? false; const iconHoverAnimation = panelState?.iconHoverAnimation ?? "none"; const panelRoute = useUhuruPanelRoute(); const [search, setSearch] = useState(""); function selectItem(item: UhuruPanelItem) { if (item.route !== false && item.path && item.element) { panelRoute?.navigate({ element: item.element, path: item.path }); } else { panelRoute?.clear(); } onItemSelect?.(item); item.onSelect?.(); } const visibleGroups = useMemo(() => { const query = search.trim().toLowerCase(); if (!query) return groups; return groups.flatMap((group) => { if ( String(group.label ?? "") .toLowerCase() .includes(query) ) return [group]; const items = filterPanelItems(group.items, query); return items.length > 0 ? [{ ...group, items }] : []; }); }, [groups, search]); useEffect(() => { const routableItems = groups.flatMap((group) => flattenPanelItems(group.items), ); const footerDefinitions = footerRoutes ?? []; const location = getUhuruPanelLocation(); const routeItems = routableItems .flatMap((item) => item.path && item.element ? [{ element: item.element, path: item.path }] : [], ); const allRoutes = [...routeItems, ...footerDefinitions]; const matchingRoute = allRoutes.find((route) => route.path === location) ?? allRoutes.find((route) => matchesUhuruPanelRoute(route.path, location)); const defaultItem = matchingRoute ?? (defaultRoute ? routableItems.find( (item) => item.id === defaultRoute || item.path === defaultRoute, ) : routableItems[0]); if ( defaultItem?.path && defaultItem.element && panelRoute?.activeRoute?.path !== defaultItem.path ) { panelRoute?.navigate({ element: defaultItem.element, path: defaultItem.path, }); } }, [activeId, defaultRoute, footerRoutes, groups, panelRoute]); return ( ); } export type UhuruUngroupedPanelProps = Omit< UhuruGroupedPanelProps, "groups" > & { items: UhuruPanelItem[]; }; /** Flat, API-friendly panel navigation with the same item actions and routing as grouped panels. */ export function UhuruUngroupedPanel({ items, ...props }: UhuruUngroupedPanelProps) { const panelState = useUhuruPanelState(); const collapsed = panelState?.collapsed ?? false; const panelRoute = useUhuruPanelRoute(); const [search, setSearch] = useState(""); const query = search.trim().toLowerCase(); const visibleItems = query ? filterPanelItems(items, query) : items; const selectItem = (item: UhuruPanelItem) => { if (item.route !== false && item.path && item.element) panelRoute?.navigate({ element: item.element, path: item.path }); props.onItemSelect?.(item); item.onSelect?.(); }; return ( ); } function UhuruPanelGroupView({ activeId, group, onItemSelect, renderItem, collapsed = false, }: { activeId?: string; group: UhuruPanelGroup; onItemSelect?: (item: UhuruPanelItem) => void; renderItem?: ( item: UhuruPanelItem, context: UhuruPanelItemRenderContext, ) => ReactNode; collapsed?: boolean; }) { const [expanded, setExpanded] = useState(group.defaultExpanded ?? true); const collapsible = group.collapsible ?? Boolean(group.label); const panelState = useUhuruPanelState(); const iconMotionClass = panelIconMotionClassName(panelState?.iconHoverAnimation ?? "none"); function handleGroupClick() { if (collapsed && !panelState?.expandOnHover) { panelState?.onToggleCollapsed?.(); setExpanded(true); const firstItem = group.items[0]; if (firstItem) { onItemSelect?.(firstItem); } return; } setExpanded((current) => !current); } return (