Files
gitmost/apps/client/src/components/layouts/global/app-header.tsx
T
agent_coder 731a4f0dca refactor(#292 review): extract NAVBAR_COLLAPSE_BREAKPOINT constant (F2)
The AppShell navbar breakpoint and both burger toggles' hiddenFrom/visibleFrom
must be equal, or the sidebar becomes unreachable on tablet widths (the round-1
regression). A comment guarded that before; now a shared const does. Add
NAVBAR_COLLAPSE_BREAKPOINT='md' to sidebar-atom.ts and reference it from the navbar
breakpoint (global-app-shell) + both toggles (app-header). aside.breakpoint and the
sm brand/search gates are intentionally separate contracts, left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:02:37 +03:00

130 lines
4.5 KiB
TypeScript

import {
ActionIcon,
Box,
Group,
Text,
Tooltip,
} from "@mantine/core";
import { IconMessage } from "@tabler/icons-react";
import classes from "./app-header.module.css";
import { BrandLogo } from "@/components/ui/brand-logo";
import TopMenu from "@/components/layouts/global/top-menu.tsx";
import { Link } from "react-router-dom";
import { useAtom } from "jotai";
import {
NAVBAR_COLLAPSE_BREAKPOINT,
desktopSidebarAtom,
mobileSidebarAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useOpenAiChatForCurrentPage } from "@/features/ai-chat/hooks/use-open-ai-chat.ts";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
import { useTranslation } from "react-i18next";
import {
SearchControl,
SearchMobileControl,
} from "@/features/search/components/search-control.tsx";
import {
searchSpotlight,
} from "@/features/search/constants.ts";
import { NotificationPopover } from "@/features/notification/components/notification-popover.tsx";
export function AppHeader() {
const { t } = useTranslation();
const [mobileOpened] = useAtom(mobileSidebarAtom);
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
const [workspace] = useAtom(workspaceAtom);
// Opening from the header auto-opens the document's bound chat (last chat
// created on the current page); off a page it keeps the current selection.
const openAiChat = useOpenAiChatForCurrentPage();
// AI chat entry point: only shown when the workspace enables it (A7 gate).
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
return (
<>
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
<Group wrap="nowrap">
<Tooltip label={t("Sidebar toggle")}>
<SidebarToggle
aria-label={t("Sidebar toggle")}
opened={mobileOpened}
onClick={toggleMobile}
// Must match the AppShell navbar breakpoint (md). The navbar
// collapses to the MOBILE drawer below md, so the mobile toggle
// (which flips mobileOpened) must be the one visible across the
// whole <md band — otherwise at 768-991 the desktop toggle showed
// but flipped the wrong atom, leaving the drawer unopenable (the
// regression from the initial sm->md navbar change).
hiddenFrom={NAVBAR_COLLAPSE_BREAKPOINT}
size="sm"
/>
</Tooltip>
<Tooltip label={t("Sidebar toggle")}>
<SidebarToggle
aria-label={t("Sidebar toggle")}
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom={NAVBAR_COLLAPSE_BREAKPOINT}
size="sm"
/>
</Tooltip>
<Group gap={6} align="flex-end" wrap="nowrap">
<Link to="/home" className={classes.brand} aria-label="Gitmost">
<Box hiddenFrom="sm" className={classes.brandIcon}>
<BrandLogo markOnly height={26} />
</Box>
<Box visibleFrom="sm" className={classes.brandIcon}>
<BrandLogo height={30} />
</Box>
</Link>
<Tooltip label={t("Version")}>
<Text
component="span"
visibleFrom="sm"
className={classes.brandVersion}
>
{APP_VERSION}
</Text>
</Tooltip>
</Group>
</Group>
<div>
<Group visibleFrom="sm">
<SearchControl onClick={searchSpotlight.open} />
</Group>
<Group hiddenFrom="sm">
<SearchMobileControl onSearch={searchSpotlight.open} />
</Group>
</div>
<Group px={"xl"} wrap="nowrap">
{aiChatEnabled && (
<Tooltip label={t("AI chat")} withArrow>
<ActionIcon
variant="subtle"
color="dark"
size="sm"
aria-label={t("AI chat")}
onClick={openAiChat}
>
<IconMessage size={20} />
</ActionIcon>
</Tooltip>
)}
<NotificationPopover />
<TopMenu />
</Group>
</Group>
</>
);
}