/******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ import { FunctionComponent, useContext, useEffect, useRef, useState } from 'react'; import { AppBar, Box, Toolbar } from '@mui/material'; import { alpha, styled, useTheme } from '@mui/material/styles'; import { Link as RouteLink } from 'react-router'; import { HeaderMenu } from '../header-menu'; import { MainContext } from '../context'; import { usePageSearchBar } from '../context/search/page-search-bar-context'; import { useExtensionTint } from '../context/extension-tint-context'; import { OpenVsxMark } from '../components/openvsx-mark'; import { NavSearchField } from './nav-search-field'; import { NAVBAR_HEIGHT_PX } from '../default/theme'; const ToolbarItem = styled(Box)({ display: 'flex', alignItems: 'center' }); // On-color logo for page bands: brand purple can't be trusted against an // arbitrary band, so the marks take the inherited content color. const LOGO_ON_BAND_SX = { '& svg path': { fill: 'currentColor' }, '& svg path:first-of-type': { fillOpacity: 0.72 } }; // Fade stops sampled from smootherstep — a plain linear fade ends with an // abrupt slope change the eye reads as a line (Mach banding). const FADE_EASE = [1, 0.984, 0.897, 0.725, 0.5, 0.275, 0.103, 0.016, 0]; // Progressive blur: each layer anchors to the top and fades out at a staggered // depth, so a row's blur compounds from the layers still active there (~6px at // the toolbar, ~0 at the fan's bottom). Saturation rides in the same filters — // every extra backdrop-filter layer re-snapshots the backdrop each frame. const BLUR_LAYERS = [ { blur: '5.2px', saturate: 1.35, fadeStart: 0 }, { blur: '2.75px', saturate: 1.25, fadeStart: 30 }, { blur: '1.2px', saturate: 1.2, fadeStart: 60 } ].map(({ blur, saturate, fadeStart }) => ({ filter: `blur(${blur}) saturate(${saturate})`, mask: `linear-gradient(to bottom, ${FADE_EASE.map( (k, j) => `rgba(0, 0, 0, ${k}) ${fadeStart + (40 * j) / (FADE_EASE.length - 1)}%` ).join(', ')})` })); // Nav-side view of the page's tint region: `navTint` is the color the nav // currently wears — the region's color until its depth scrolls past the nav // midpoint, then null to return to theme colors. `washColor` lags one step so // the wash gradient (not transitionable) can still fade out in the last color. const useNavTint = (): { navTint: string | null; washColor: string | null } => { const tint = useExtensionTint(); const [pastTint, setPastTint] = useState(false); useEffect(() => { if (!tint) { setPastTint(false); return; } const onScroll = () => setPastTint(window.scrollY > tint.depth - NAVBAR_HEIGHT_PX / 2); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, [tint]); const navTint = tint && !pastTint ? tint.color : null; const lastTint = useRef(navTint); useEffect(() => { if (navTint) lastTint.current = navTint; }, [navTint]); return { navTint, washColor: navTint ?? lastTint.current }; }; export const AppNavbar: FunctionComponent = () => { const { pageSettings } = useContext(MainContext); const { toolbarContent: ToolbarContent } = pageSettings.elements; const { hasPageSearchBar } = usePageSearchBar(); const theme = useTheme(); const baseAlpha = theme.palette.mode === 'dark' ? 0.74 : 0.78; const navbg = alpha(theme.palette.background.default, baseAlpha); // Lighter than the solid glass — the blur fan already provides legibility. const tintAlpha = theme.palette.mode === 'dark' ? 0.3 : 0.35; const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 20); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); const showSolid = !scrolled; const showFan = scrolled; const { navTint, washColor } = useNavTint(); const fanBottom = { xs: '-48px', sm: '-80px' }; const tintBottom = { xs: '-48px', sm: '-150px' }; // While a gallery band backs the nav, the chrome wears its color and the // content flips to the contrast color, so the two surfaces read as one. const contentColor = navTint ? theme.palette.getContrastText(navTint) : theme.palette.text.primary; const solidBg = navTint ?? navbg; const navBorder = navTint ?? theme.palette.divider; // The theme and tint washes cross-fade as separate layers. const washGradientOf = (color: string) => `linear-gradient(to bottom, ${FADE_EASE.map( (k, i) => `${alpha(color, tintAlpha * k)} ${(i * 100) / (FADE_EASE.length - 1)}%` ).join(', ')})`; const themeWash = washGradientOf(theme.palette.background.default); const tintWash = washColor ? washGradientOf(washColor) : null; return ( {/* Solid frosted glass — visible when not scrolled */} ); };