"use client"; import type { ReactNode } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme, } from "@mantine/core"; import { IconBrandDocker, IconHome, IconLayoutDashboard, IconReplace, IconRobot, IconSettings, IconSunMoon, } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { invariantTechnicalLabels } from "@homarr/definitions"; import { useModalAction } from "@homarr/modals"; import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; import type { HeaderItem } from "@homarr/validation/user"; import { getHeaderItemKey, getHeaderItems } from "@homarr/validation/user"; import { useOptionalHomarrAssistant } from "~/components/assistant/assistant-context"; import { BoardSwitcher } from "~/components/board/board-switcher"; import type { BoardSwitcherControls } from "~/components/board/board-switcher"; import { ClientBurger } from "./burger"; import { HeaderButton } from "./button"; import { DockerQuickAccessModal } from "./docker-quick-access-modal"; import { HeaderLogo } from "./header-logo"; import { LazySpotlight } from "./lazy-spotlight"; import { DesktopSearchInput, MobileSearchButton } from "./search"; import { TourTarget } from "./tour-target"; import { UserButtonClient } from "./user-client"; import classes from "./configurable-header.module.css"; interface ConfigurableHeaderProps { logo: ReactNode; logoWithTitle: ReactNode; actions?: ReactNode; boardEditAction?: ReactNode; boardSettingsAction?: ReactNode; hasNavigation: boolean; avatar: ReactNode; userId: string | null; isAdmin: boolean; isDockerEnabled: boolean; } type HeaderBoard = RouterOutputs["board"]["getAllBoards"][number]; const floatingControlsIntroDurationMs = 5_000; const floatingControlsDismissDelayMs = 900; export const ConfigurableHeader = ({ logo, logoWithTitle, actions, boardEditAction, boardSettingsAction, hasNavigation, avatar, userId, isAdmin, isDockerEnabled, }: ConfigurableHeaderProps) => { const { headerPreferences } = useSettings(); const assistant = useOptionalHomarrAssistant(); const { toggleColorScheme } = useMantineColorScheme(); const { openModal: openDockerModal } = useModalAction(DockerQuickAccessModal); const t = useI18n("management.page.user.setting.general.header"); const allHeaderItems = getHeaderItems(headerPreferences.zones); const hasBoardShortcuts = allHeaderItems.some((item) => item.type === "board"); const { data: boards = [] } = clientApi.board.getAllBoards.useQuery(undefined, { enabled: hasBoardShortcuts, staleTime: 60_000, }); const boardsById = useMemo(() => new Map(boards.map((board) => [board.id, board])), [boards]); return ( {(boardSwitcher) => { const renderItems = (items: HeaderItem[]) => items.map((item) => ( t(`items.${key}` as never)} boardUnavailableLabel={t("items.boardUnavailable")} /> )); return ( <> {headerPreferences.visible ? ( {hasNavigation ? : null} {renderItems(headerPreferences.zones.left)} {renderItems(headerPreferences.zones.center)} {renderItems(headerPreferences.zones.right)} {actions ? {actions} : null} ) : ( {hasNavigation ? : null} )} > ); }} ); }; const FloatingHeaderControls = ({ children }: { children: ReactNode }) => { const [isIntroComplete, setIsIntroComplete] = useState(false); const [isVisible, setIsVisible] = useState(true); const isInteractingRef = useRef(false); const dismissTimeoutRef = useRef(null); const clearDismissTimeout = () => { if (dismissTimeoutRef.current === null) return; window.clearTimeout(dismissTimeoutRef.current); dismissTimeoutRef.current = null; }; const dismissSoon = () => { if (!isIntroComplete) return; clearDismissTimeout(); dismissTimeoutRef.current = window.setTimeout(() => { if (!isInteractingRef.current) setIsVisible(false); }, floatingControlsDismissDelayMs); }; const revealFromCorner = () => { if (!isIntroComplete) return; clearDismissTimeout(); setIsVisible(true); }; const startInteraction = () => { isInteractingRef.current = true; clearDismissTimeout(); setIsVisible(true); }; const endInteraction = () => { isInteractingRef.current = false; dismissSoon(); }; useEffect(() => { const introTimeout = window.setTimeout(() => { setIsIntroComplete(true); if (!isInteractingRef.current) setIsVisible(false); }, floatingControlsIntroDurationMs); return () => { window.clearTimeout(introTimeout); if (dismissTimeoutRef.current !== null) window.clearTimeout(dismissTimeoutRef.current); }; }, []); return ( <> { if (event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget)) return; endInteraction(); }} > {children} > ); }; interface HeaderItemProps { item: HeaderItem; logo: ReactNode; logoWithTitle: ReactNode; boardEditAction: ReactNode; boardSettingsAction: ReactNode; avatar: ReactNode; userId: string | null; isAdmin: boolean; isDockerEnabled: boolean; board: HeaderBoard | undefined; searchDisplay: "input" | "icon"; logoDisplay: "logo" | "logoAndText"; boardSwitcher: BoardSwitcherControls; assistant: ReturnType; openDockerModal: () => void; toggleColorScheme: () => void; label: (key: string) => string; boardUnavailableLabel: string; } const HeaderItem = ({ item, logo, logoWithTitle, boardEditAction, boardSettingsAction, avatar, userId, isAdmin, isDockerEnabled, board, searchDisplay, logoDisplay, boardSwitcher, assistant, openDockerModal, toggleColorScheme, label, boardUnavailableLabel, }: HeaderItemProps) => { if (item.type === "board") { if (!board) { return ( ); } return ( ); } if (item.id === "logo") { return ( ); } if (item.id === "boardEdit") return boardEditAction; if (item.id === "boardSettings") return boardSettingsAction; if (item.id === "search") { return ( {searchDisplay === "input" ? ( <> > ) : ( )} ); } if (item.id === "home") { return ( ); } if (item.id === "boardSwitcher") { return ( ); } if (item.id === "assistant") { if (!assistant?.enabled) return null; return ( : assistant.unreadCount} > ); } if (item.id === "docker") { if (!isDockerEnabled) return null; return ( ); } if (item.id === "settings") { const href = userId ? `/manage/users/${userId}/general` : "/auth/login"; return ( ); } if (item.id === "themeToggle") { return ( ); } return ( ); };