"use client"; import { useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { AppSidebarWorkspaceActions } from "@/components/app-sidebar/app-sidebar-workspace-actions"; import { MainTopBarPluginActions } from "@/components/kanban/main-top-bar-plugin-actions"; import { DestinationRows } from "@/components/navigation/destination-rows"; import { resolveDestinations } from "@/lib/navigation/resolve-destinations"; import { NO_WORKSPACE_CONTEXT } from "@/lib/navigation/surface-policy"; import { usePluginRegistry } from "@/lib/plugins/registry"; import type { TaskListingPage } from "@/lib/task-listing/view-navigation"; import { cn } from "@/lib/utils"; import { PluginSlotPresence } from "./plugin-slot-presence"; export type MobilePluginWorkspaceContext = { workspaceId?: string; workspaceLabel?: string; currentPage: TaskListingPage; }; type MobilePluginNavSectionProps = { /** Closes the surrounding menu sheet once a plugin page is opened. */ onNavigate: () => void; /** Page-scoped plugin status and controls for this menu. */ actions?: ReactNode; /** Custom sidebar layouts already place plugin destinations themselves. */ includeDestinations?: boolean; /** Phone app navigation groups workspace slots with page contributions. */ workspaceContext?: MobilePluginWorkspaceContext; }; /** * Mobile counterpart to the desktop sidebar's ``: the * hamburger-sheet surface for plugin-registered "main"-section nav items * (`registerNavItem({ section: "main" })`, the default). The desktop rail is * `hidden md:block`, so without this section a plugin's own page has no phone * entry point at all. "integrations"-section items keep rendering inside * `MobileIntegrationsSection`. */ export function MobilePluginNavSection({ onNavigate, actions, includeDestinations = true, workspaceContext, }: MobilePluginNavSectionProps) { const { t } = useTranslation(); const registry = usePluginRegistry(); const [renderedTaskPluginIds, setRenderedTaskPluginIds] = useState([]); const taskPluginIds = actions ? renderedTaskPluginIds : []; const { hasMainActions, hasSidebarActions } = workspaceSlotAvailability( registry, workspaceContext, taskPluginIds, ); const hasWorkspaceActions = hasMainActions || hasSidebarActions; // Resolved directly rather than through `useStaticDestinations`: this group's // hrefs are static plugin paths, so it needs neither workspace context nor the // availability subscription. Matches the desktop rail in `plugin-nav-items.tsx`. const destinations = includeDestinations ? resolveDestinations({ surface: "mobileMenu", section: "plugins", ctx: NO_WORKSPACE_CONTEXT, pluginItems: registry.getNavRegistrations(), }) : []; if (!actions && !hasWorkspaceActions && destinations.length === 0) return null; return ( {t("common:plugins")} {(hasWorkspaceActions || actions) && ( {hasWorkspaceActions && workspaceContext && ( )} {actions && ( {actions} )} )} {destinations.length > 0 && ( )} ); } function WorkspacePluginControls({ context, hasMainActions, hasSidebarActions, excludePluginIds, }: { context: MobilePluginWorkspaceContext; hasMainActions: boolean; hasSidebarActions: boolean; excludePluginIds: readonly string[]; }) { return ( {hasMainActions && ( )} {hasSidebarActions && context.workspaceId && ( )} ); } function workspaceSlotAvailability( registry: ReturnType, context?: MobilePluginWorkspaceContext, excludePluginIds: readonly string[] = [], ) { return { hasMainActions: !!context && registry .getSlotRegistrations("main-top-bar") .some(({ pluginId }) => !excludePluginIds.includes(pluginId)), hasSidebarActions: !!context?.workspaceId && registry.getSlotRegistrations("sidebar-workspace-actions").length > 0, }; }