---
/**
* Header Component
* Flexible navigation header with variant-based configuration
*
* Variants:
* - layout: 'default' | 'centered' | 'minimal'
* - position: 'fixed' | 'sticky' | 'static'
* - size: 'sm' | 'md' | 'lg'
* - variant: 'default' | 'solid' | 'transparent'
* - colorScheme: 'default' | 'invert' (use 'invert' for dark backgrounds)
* - shape: 'bar' | 'floating' (use 'floating' for capsule header)
*
* Features:
* - Dynamic navigation from nav.config.ts (default) or custom nav prop
* - Optional CTA button with customization
* - Mobile menu with Escape key support
* - Theme toggle
* - GitHub/action buttons
* - Full slot support for customization
* - Inverted color scheme for use on dark/image backgrounds
* - Floating capsule shape with scroll-reactive bg + color flip
*/
import type { HTMLAttributes } from 'astro/types';
import { cn } from '@/lib/cn';
import { getNavItems, type NavItem as NavConfigItem } from '@/config/nav.config';
import { headerVariants, headerInnerVariants } from './header.variants';
import Button from '@/components/ui/form/Button/Button.astro';
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
import Logo from '@/components/ui/marketing/Logo/Logo.astro';
import ThemeToggle from '@/components/layout/ThemeToggle.astro';
import ThemeSelector from '@/components/layout/ThemeSelector.astro';
import ThemeSelectorDropdown from '@/components/layout/ThemeSelectorDropdown.astro';
import LanguageSwitcherDropdown from '@/components/layout/LanguageSwitcherDropdown.astro';
import siteConfig from '@/config/site.config';
import type { Locale } from '@/i18n/ui';
export interface NavItem {
label: string;
href: string;
}
export interface HeaderAction {
icon: string;
href: string;
label: string;
iconOnly?: boolean;
target?: string;
}
interface Props extends HTMLAttributes<'header'> {
/** Layout style: default (logo left, nav right), centered (logo center), minimal (logo + cta only) */
layout?: 'default' | 'centered' | 'minimal';
/** Position behavior */
position?: 'fixed' | 'sticky' | 'static';
/** Header height */
size?: 'sm' | 'md' | 'lg';
/** Background variant */
variant?: 'default' | 'solid' | 'transparent';
/** Color scheme for text/icons - use 'invert' for dark backgrounds */
colorScheme?: 'default' | 'invert';
/** Shape: 'bar' (full-width, default) or 'floating' (centered capsule) */
shape?: 'bar' | 'floating';
/** Override default navigation (replaces getNavRoutes()) */
nav?: NavItem[];
/** Additional navigation items (e.g., #features for landing pages) */
extraNav?: NavItem[];
/** Show CTA button */
showCta?: boolean;
/** CTA button configuration */
cta?: { label?: string; href?: string; icon?: string };
/** Action buttons (GitHub, etc.) */
actions?: HeaderAction[];
/** Show theme toggle (default: true) */
showThemeToggle?: boolean;
/** Show colour-theme selector swatches */
showThemeSelector?: boolean;
/** Show mobile menu (default: true) */
showMobileMenu?: boolean;
/** Show active state for current page (default: true) */
showActiveState?: boolean;
/** Logo text override */
logoText?: string;
/** Hide logo entirely */
hideLogo?: boolean;
/** Show language switcher */
showLanguageSwitcher?: boolean;
/** Current locale for language switcher */
currentLocale?: Locale;
/** Show social icon links (desktop/tablet only, reads from siteConfig.socialLinks) */
showSocialLinks?: boolean;
/** Show scroll progress bar at the bottom of the header */
showScrollProgress?: boolean;
/** Position of the scroll progress bar: 'top' (above header) or 'bottom' (below header, default) */
scrollProgressPosition?: 'top' | 'bottom';
}
const {
layout = 'default',
position = 'fixed',
size = 'lg',
variant = 'solid',
colorScheme = 'default',
shape = 'bar',
nav,
extraNav = [],
showCta = false,
cta = { label: 'Start a project', href: '/contact' },
actions = [],
showThemeToggle = true,
showThemeSelector = false,
showMobileMenu = true,
showSocialLinks = false,
showActiveState = true,
showScrollProgress = false,
scrollProgressPosition = 'bottom',
showLanguageSwitcher = false,
currentLocale = 'de',
logoText,
hideLogo = false,
class: className,
...attrs
} = Astro.props;
// Shape + color scheme helpers
const isFloating = shape === 'floating';
const isInvert = colorScheme === 'invert';
// Get navigation items
const defaultNav = getNavItems().map((item: NavConfigItem) => ({
label: item.label,
href: item.href,
}));
const navItems: NavItem[] = nav || [...extraNav, ...defaultNav];
// Current path for active state
const currentPath = Astro.url.pathname;
// Check if we're on the landing page
const isLandingPage = currentPath === '/';
// Process CTA href for landing page anchor links
const ctaHref = cta.href?.startsWith('#') && !isLandingPage ? `/${cta.href}` : cta.href;
// Check slots
const hasLogoSlot = Astro.slots.has('logo');
const hasNavSlot = Astro.slots.has('nav');
const hasActionsSlot = Astro.slots.has('actions');
const hasMobileMenuSlot = Astro.slots.has('mobile-menu');
// Compute header classes
const headerClasses = cn(
headerVariants({ position, variant, shape }),
isInvert && !isFloating && 'invert-section',
className
);
// Compute inner container classes
const innerClasses = headerInnerVariants({ size, shape });
// Check if a nav item is active
function isActive(href: string): boolean {
if (!showActiveState) return false;
if (href.startsWith('#')) return false;
return currentPath === href || currentPath.startsWith(href + '/');
}
// Map a social URL to its icon name + accessible label
function getSocialIconData(url: string): { icon: string; label: string } {
if (url.includes('github.com')) return { icon: 'github', label: 'GitHub' };
if (url.includes('instagram.com')) return { icon: 'instagram', label: 'Instagram' };
if (url.includes('x.com') || url.includes('twitter.com')) return { icon: 'x-twitter', label: 'X' };
if (url.includes('linkedin.com')) return { icon: 'linkedin', label: 'LinkedIn' };
if (url.includes('bsky.app')) return { icon: 'bluesky', label: 'Bluesky' };
return { icon: 'link', label: 'Social' };
}
// Generate unique ID for this header instance
const menuId = `mobile-menu-${Math.random().toString(36).slice(2, 9)}`;
const buttonId = `${menuId}-button`;
---