From 19e06d02adcd2580586fea20fd11b498e69046c3 Mon Sep 17 00:00:00 2001 From: lucamzanon Date: Fri, 9 Oct 2026 08:27:01 +0200 Subject: [PATCH] fix(mountain-view): keep the same top bar on every app Calendar, contacts, files and settings still drew the left navigation rail under Mountain View, so moving away from mail switched to the default frame. They now show the Mountain View top bar too, without the mail search and the menu button, which only make sense in mail. --- components/calendar/calendar-app.tsx | 15 ++- components/contacts/contacts-app.tsx | 15 ++- components/files/files-app.tsx | 15 ++- components/layout/mv-top-bar.tsx | 167 ++++++++++++++------------- components/mail/mail-app.tsx | 22 ++-- components/settings/settings-app.tsx | 15 ++- hooks/use-mountain-view-shell.ts | 19 +++ 7 files changed, 176 insertions(+), 92 deletions(-) create mode 100644 hooks/use-mountain-view-shell.ts diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index cfbce5667..b19cfff9c 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -92,6 +92,8 @@ import { import { useLiteLinkSegments } from "@/hooks/use-lite-link-segments"; import { findMeetingLink } from "@/lib/event-links"; import { useDocumentTitle } from "@/hooks/use-document-title"; +import { MvTopBar } from "@/components/layout/mv-top-bar"; +import { useMountainViewShell } from "@/hooks/use-mountain-view-shell"; type PendingScopeAction = | { type: "edit"; event: CalendarEvent; updates: Partial; sendScheduling?: boolean } @@ -119,6 +121,8 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const isMobile = useIsMobile(); const isDesktop = useIsDesktop(); const isEmbedded = useIsEmbedded(); + // Mountain View's desktop chrome: its top bar in place of the left rail. + const mvShell = useMountainViewShell(isEmbedded); // When the pane (Pro shell) or window is narrower than `lg`, the sidebar // collapses into a burger-toggled overlay instead of taking inline space. const isNarrow = !isDesktop; @@ -1571,9 +1575,18 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
{refreshIndicator} + {mvShell && ( + + )}
{/* Left Navigation Rail (hidden when embedded in Pro shell) */} - {!isMobile && !isEmbedded && ( + {!isMobile && !isEmbedded && !mvShell && (
{refreshIndicator} + {mvShell && ( + + )}
{/* Navigation Rail - desktop only (hidden when embedded in Pro shell) */} - {!isMobile && !isEmbedded && ( + {!isMobile && !isEmbedded && !mvShell && (
(() => loadFilesSettings().folderLayout); // The account the files client was last initialised for (`undefined` = never). // Tracked so a Pro-shell account switch re-initialises instead of showing the @@ -573,8 +577,17 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) {
{refreshIndicator} + {mvShell && ( + + )}
- {!isMobile && !isEmbedded && ( + {!isMobile && !isEmbedded && !mvShell && (
void; - onSearchSubmit: (query: string) => void; - onSearchClear: () => void; +/** Mail search, which only the mail surface puts into the bar. */ +interface MvTopBarSearch { + query: string; + onQueryChange: (value: string) => void; + onSubmit: (query: string) => void; + onClear: () => void; onSelectContact: (contact: ContactSuggestion, field: ContactSearchField) => void; /** Opens the advanced-filter panel that lives under the message list. */ onToggleFilters: () => void; filtersOpen: boolean; activeFilterCount: number; - searchDisabled?: boolean; - onShowShortcuts?: () => void; - onManageApps?: () => void; - onInlineApp?: (appId: string, url: string, name: string) => void; - onCloseInlineApp?: () => void; - activeAppId?: string | null; - /** Opens the merged across-accounts inbox from the account popover. */ - onSelectAllInboxes?: () => void; - allInboxesSelected?: boolean; + disabled?: boolean; /** * Receives the node under the search field that the advanced-filter panel is * portalled into, so the panel drops from the field itself; without this @@ -58,23 +51,36 @@ interface MvTopBarProps { onFilterAnchorChange?: (node: HTMLDivElement | null) => void; } +interface MvTopBarProps { + /** + * The mail search field. Other surfaces leave it out and keep their own + * search where it was; the bar then carries the brand, the app grid and the + * account avatar only. + */ + search?: MvTopBarSearch; + /** The hamburger folds the mail navigation, so only mail shows it. */ + showMenuButton?: boolean; + onShowShortcuts?: () => void; + onManageApps?: () => void; + onInlineApp?: (appId: string, url: string, name: string) => void; + onCloseInlineApp?: () => void; + activeAppId?: string | null; + /** Opens the merged across-accounts inbox from the account popover. */ + onSelectAllInboxes?: () => void; + allInboxesSelected?: boolean; +} + /** * Mountain View's global header: one 64px row that owns the hamburger, the * brand, the search field and the account cluster. It only renders on - * desktop under `interfaceLayout === 'mountain-view'`; the default layout keeps - * search inside the list column and navigation in the left rail, and neither - * of those is touched here. + * desktop under `interfaceLayout === 'mountain-view'` (see + * useMountainViewShell), on every surface, in place of the left rail; the + * default layout keeps search inside the list column and navigation in the + * left rail, and neither of those is touched here. */ export function MvTopBar({ - searchQuery, - onSearchQueryChange, - onSearchSubmit, - onSearchClear, - onSelectContact, - onToggleFilters, - filtersOpen, - activeFilterCount, - searchDisabled = false, + search, + showMenuButton = true, onShowShortcuts, onManageApps, onInlineApp, @@ -82,7 +88,6 @@ export function MvTopBar({ activeAppId, onSelectAllInboxes, allInboxesSelected = false, - onFilterAnchorChange, }: MvTopBarProps) { const t = useTranslations("sidebar"); const tSearch = useTranslations("advanced_search"); @@ -96,6 +101,8 @@ export function MvTopBar({ const calendarEnabled = usePolicyStore((s) => s.isFeatureEnabled("calendarEnabled")); const contactsEnabled = usePolicyStore((s) => s.isFeatureEnabled("contactsEnabled")); const filesEnabled = usePolicyStore((s) => s.isFeatureEnabled("filesEnabled")); + // The same gate the navigation rail puts on its "add app" button. + const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled("sidebarAppsEnabled")); const sidebarApps = useResolvedSidebarApps(); const [appsOpen, setAppsOpen] = useState(false); @@ -147,15 +154,17 @@ export function MvTopBar({ data-mv-topbar="" > {/* Brand block: occupies the sidebar's own column width */} -
- +
+ {showMenuButton && ( + + )} {logoUrl ? : null} {appName} @@ -163,48 +172,50 @@ export function MvTopBar({
{/* Search: left-anchored and capped, not centred */} -
- - + {search && ( +
+ + - {/* Anchor for the advanced-filter panel: the search field's own box, - filter button included, so the panel shares the field's edges. It - only marks the spot; the panel itself is drawn at the end of . */} -
-
+ {/* Anchor for the advanced-filter panel: the search field's own box, + filter button included, so the panel shares the field's edges. It + only marks the spot; the panel itself is drawn at the end of . */} +
+
+ )} {/* Spacer keeps the right cluster pinned while search stays left */}
@@ -306,7 +317,7 @@ export function MvTopBar({ ); })}
- {onManageApps && ( + {sidebarAppsEnabled && onManageApps && (