"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@kandev/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@kandev/ui/dropdown-menu"; import { ToggleGroup, ToggleGroupItem } from "@kandev/ui/toggle-group"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip"; import { IconPlus, IconSettings, IconList, IconLayoutKanban, IconMenu2, IconChartBar, IconTimeline, IconStethoscope, IconDots, IconSparkles, IconAlertTriangle, } from "@tabler/icons-react"; import { ImproveKandevDialog } from "@/components/improve-kandev-dialog"; import { IntegrationsMenu } from "@/components/integrations/integrations-menu"; import { PageTopbar } from "@/components/page-topbar"; import { KanbanDisplayDropdown } from "../kanban-display-dropdown"; import { ReleaseNotesDialog } from "../release-notes/release-notes-dialog"; import { HealthIndicatorButton, HealthIssuesDialog } from "../system-health/health-indicator"; import { TaskSearchInput } from "./task-search-input"; import { QuickChatButton } from "@/components/task/quick-chat-button"; import { KanbanHeaderMobile } from "./kanban-header-mobile"; import { MobileMenuSheet } from "./mobile-menu-sheet"; import { linkToTask, linkToTasks } from "@/lib/links"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { useAppStore } from "@/components/state-provider"; import { useKanbanDisplaySettings } from "@/hooks/use-kanban-display-settings"; import { useReleaseNotes } from "@/hooks/use-release-notes"; import { useSystemHealthIndicator } from "@/hooks/use-system-health-indicator"; import type { ComponentProps, RefObject } from "react"; type KanbanHeaderProps = { onCreateTask: () => void; workspaceId?: string; currentPage?: "kanban" | "tasks"; searchQuery?: string; onSearchChange?: (query: string) => void; isSearchLoading?: boolean; }; type ViewToggleItem = { value: string; icon: typeof IconLayoutKanban; label: string; }; type HeaderUtilityMenuProps = { showReleaseNotesButton: boolean; onOpenReleaseNotes: () => void; showHealthIndicator: boolean; onOpenHealthDialog: () => void; showStatsLink?: boolean; buttonSize?: ComponentProps["size"]; }; const VIEW_TOGGLE_ITEMS: ViewToggleItem[] = [ { value: "kanban", icon: IconLayoutKanban, label: "Kanban" }, { value: "pipeline", icon: IconTimeline, label: "Pipeline" }, { value: "list", icon: IconList, label: "List" }, ]; const WORKBENCH_TOPBAR_CLASSNAME = "h-10 px-3 py-1"; const DESKTOP_HEADER_NARROW_PX = 1100; function getWorkspaceLabel( workspaces: Array<{ id: string; name: string }>, activeWorkspaceId: string | null, ): string { if (!activeWorkspaceId) return "All workspaces"; return workspaces.find((workspace) => workspace.id === activeWorkspaceId)?.name ?? "Workspace"; } function getHeaderTitle(currentPage: string): string { return currentPage === "tasks" ? "Tasks" : "Home"; } function BoardUtilitiesMenu({ showReleaseNotesButton, onOpenReleaseNotes, showHealthIndicator, onOpenHealthDialog, showStatsLink = true, buttonSize = "icon", }: HeaderUtilityMenuProps) { return ( Utilities {showReleaseNotesButton && ( Release notes )} {showHealthIndicator ? "Health issues" : "System health"} {showStatsLink && ( <> Stats )} Settings ); } function ImproveKandevTopbarButton({ workspaceId, buttonSize = "icon-lg", }: { workspaceId: string | undefined; buttonSize?: ComponentProps["size"]; }) { const router = useRouter(); const [open, setOpen] = useState(false); return ( <> Improve KanDev router.push(linkToTask(task.id))} /> ); } function HomeLeftActions({ workspaceId }: { workspaceId?: string }) { return ( <> ); } function WorkspaceLeftActions({ workspaceId }: { workspaceId?: string }) { return ( <> ); } function ViewToggleGroup({ toggleValue, onValueChange, size, className, itemClassName, }: { toggleValue: string; onValueChange: (value: string) => void; size?: ComponentProps["size"]; className?: string; itemClassName?: string; }) { return ( {VIEW_TOGGLE_ITEMS.map(({ value, icon: Icon, label }) => ( {label} ))} ); } function getToggleValue(currentPage: string, kanbanViewMode: string | null): string { if (currentPage === "tasks") return "list"; if (kanbanViewMode === "graph2") return "pipeline"; return "kanban"; } function useIsHeaderNarrow(ref: RefObject): boolean { const [isNarrow, setIsNarrow] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const update = () => setIsNarrow(el.clientWidth < DESKTOP_HEADER_NARROW_PX); update(); const observer = new ResizeObserver(update); observer.observe(el); return () => observer.disconnect(); }, [ref]); return isNarrow; } function TabletHeader({ onCreateTask, workspaceId, title, workspaceLabel, searchQuery, onSearchChange, isSearchLoading, toggleValue, handleViewChange, setMenuOpen, showHealthIndicator, onOpenHealthDialog, }: { onCreateTask: () => void; workspaceId?: string; title: string; workspaceLabel: string; searchQuery: string; onSearchChange?: (query: string) => void; isSearchLoading: boolean; toggleValue: string; handleViewChange: (value: string) => void; setMenuOpen: (open: boolean) => void; showHealthIndicator: boolean; onOpenHealthDialog: () => void; }) { const isHome = title === "Home"; return ( ) : ( ) } actionsClassName="gap-2" actions={ <> {onSearchChange && ( )} } /> ); } function DesktopHeader({ onCreateTask, workspaceId, title, workspaceLabel, searchQuery, onSearchChange, isSearchLoading, toggleValue, handleViewChange, showReleaseNotesButton, onOpenReleaseNotes, showHealthIndicator, onOpenHealthDialog, }: { onCreateTask: () => void; workspaceId?: string; title: string; workspaceLabel: string; searchQuery: string; onSearchChange?: (query: string) => void; isSearchLoading: boolean; toggleValue: string; handleViewChange: (value: string) => void; showReleaseNotesButton: boolean; onOpenReleaseNotes: () => void; showHealthIndicator: boolean; onOpenHealthDialog: () => void; }) { const headerRef = useRef(null); const isNarrow = useIsHeaderNarrow(headerRef); const searchInput = onSearchChange ? ( ) : null; const isHome = title === "Home"; const centerSearch = isHome && searchInput && !isNarrow ? (
{searchInput}
) : null; const leftActions = isHome ? ( ) : ( ); return ( {!isHome && searchInput} } /> ); } function useHeaderViewChange( currentPage: string, workspaceId: string | undefined, onViewModeChange: (mode: string) => void, ) { const router = useRouter(); return (value: string) => { if (value === "list") { if (currentPage !== "tasks") router.push(linkToTasks(workspaceId)); } else if (value === "kanban") { if (currentPage !== "kanban") router.push("/"); onViewModeChange(""); } else if (value === "pipeline") { if (currentPage !== "kanban") router.push("/"); onViewModeChange("graph2"); } }; } export function KanbanHeader({ onCreateTask, workspaceId, currentPage = "kanban", searchQuery = "", onSearchChange, isSearchLoading = false, }: KanbanHeaderProps) { const { isMobile, isTablet } = useResponsiveBreakpoint(); const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen); const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen); const { kanbanViewMode, onViewModeChange, workspaces, activeWorkspaceId } = useKanbanDisplaySettings(); const releaseNotes = useReleaseNotes(); const healthIndicator = useSystemHealthIndicator(); const toggleValue = getToggleValue(currentPage, kanbanViewMode); const handleViewChange = useHeaderViewChange(currentPage, workspaceId, onViewModeChange); const title = getHeaderTitle(currentPage); const workspaceLabel = getWorkspaceLabel(workspaces, activeWorkspaceId); const indicatorProps = { showReleaseNotesButton: releaseNotes.showTopbarButton, onOpenReleaseNotes: releaseNotes.openDialog, showHealthIndicator: healthIndicator.hasIssues, onOpenHealthDialog: healthIndicator.openDialog, }; const sharedSearch = { searchQuery, onSearchChange, isSearchLoading }; const sharedActions = { onCreateTask, workspaceId }; const renderHeader = () => { if (isMobile) { return ( ); } if (isTablet) { return ( <> ); } return ( ); }; return ( <> {renderHeader()} {releaseNotes.hasNotes && ( )} ); }