managarten/packages/shared-landing-ui/src/layouts/Navigation.astro
Till-JS 264149a913 feat(shared-landing-ui): unify landing pages with shared components
Add new reusable components to shared-landing-ui package:
- AppScrollerSection, TimelineSection, MasonryGridSection, PrinciplesSection
- LegalPageTemplate for privacy/terms/cookies/imprint pages
- Navigation component with mobile menu and language switcher
- GradientText and LanguageSwitcher atoms
- i18n system with getLangFromUrl, useTranslations, localizePath
- Theme files for picture (indigo), chat (blue), zitare (teal)

Add legal pages to ManaDeck and Chat landing pages:
- privacy, terms, cookies, imprint pages using shared template
- Updated footers with cookies link
2026-01-23 15:45:47 +01:00

510 lines
10 KiB
Text

---
/**
* Navigation - Shared header navigation component
*
* Usage:
* ```astro
* <Navigation
* brand={{ name: 'MyApp', logo: '/logo.svg', href: '/' }}
* links={[
* { label: 'Features', href: '#features' },
* { label: 'Pricing', href: '/pricing' },
* { label: 'Docs', href: 'https://docs.example.com', external: true }
* ]}
* ctaButton={{ text: 'Get Started', href: '/signup' }}
* showLanguageSwitcher={true}
* currentLang="en"
* languages={{ de: 'Deutsch', en: 'English', fr: 'Français' }}
* />
* ```
*/
export interface NavLink {
label: string;
href: string;
external?: boolean;
}
export interface Brand {
name: string;
logo?: string;
href?: string;
}
export interface CtaButton {
text: string;
href: string;
}
interface Props {
brand: Brand;
links?: NavLink[];
ctaButton?: CtaButton;
showLanguageSwitcher?: boolean;
currentLang?: string;
languages?: Record<string, string>;
getLocalizedPath?: (lang: string) => string;
class?: string;
}
const {
brand,
links = [],
ctaButton,
showLanguageSwitcher = false,
currentLang = 'en',
languages = {},
getLocalizedPath,
class: className = '',
} = Astro.props;
---
<header class:list={['nav-header', className]}>
<div class="nav-container">
<!-- Brand -->
<a href={brand.href || '/'} class="nav-brand">
{
brand.logo ? (
<img src={brand.logo} alt={brand.name} class="nav-logo" />
) : (
<span class="nav-brand-text">{brand.name}</span>
)
}
</a>
<!-- Desktop Navigation -->
<nav class="nav-links">
{
links.map((link) => (
<a
href={link.href}
class="nav-link"
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
>
{link.label}
{link.external && (
<svg class="nav-external-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
)}
</a>
))
}
</nav>
<!-- Right side: Language switcher & CTA -->
<div class="nav-right">
{
showLanguageSwitcher && Object.keys(languages).length > 0 && (
<div class="nav-language">
<button class="language-trigger" aria-haspopup="true" aria-expanded="false">
<span>{languages[currentLang] || currentLang.toUpperCase()}</span>
<svg class="language-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div class="language-dropdown" role="menu">
{Object.entries(languages).map(([code, label]) => (
<a
href={getLocalizedPath ? getLocalizedPath(code) : `/${code}`}
class:list={[
'language-option',
{ 'language-option-active': code === currentLang },
]}
role="menuitem"
>
{label}
</a>
))}
</div>
</div>
)
}
{
ctaButton && (
<a href={ctaButton.href} class="nav-cta">
{ctaButton.text}
</a>
)
}
<!-- Mobile Menu Toggle -->
<button class="nav-mobile-toggle" aria-label="Toggle menu" aria-expanded="false">
<svg class="nav-hamburger" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<svg class="nav-close" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div class="nav-mobile-menu">
<nav class="nav-mobile-links">
{
links.map((link) => (
<a
href={link.href}
class="nav-mobile-link"
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
>
{link.label}
</a>
))
}
</nav>
{
ctaButton && (
<a href={ctaButton.href} class="nav-mobile-cta">
{ctaButton.text}
</a>
)
}
</div>
</header>
<style>
.nav-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: var(--color-background-page);
border-bottom: 1px solid var(--color-border);
backdrop-filter: blur(10px);
background: color-mix(in srgb, var(--color-background-page) 80%, transparent);
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 24px;
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 32px;
}
.nav-brand {
display: flex;
align-items: center;
text-decoration: none;
flex-shrink: 0;
}
.nav-logo {
height: 36px;
width: auto;
}
.nav-brand-text {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: -0.02em;
}
.nav-links {
display: none;
align-items: center;
gap: 8px;
}
@media (min-width: 768px) {
.nav-links {
display: flex;
}
}
.nav-link {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.9375rem;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s ease;
}
.nav-link:hover {
color: var(--color-text-primary);
background: var(--color-background-card);
}
.nav-external-icon {
width: 14px;
height: 14px;
opacity: 0.5;
}
.nav-right {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.nav-language {
position: relative;
display: none;
}
@media (min-width: 768px) {
.nav-language {
display: block;
}
}
.language-trigger {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
background: transparent;
border: 1px solid var(--color-border);
border-radius: 8px;
color: var(--color-text-secondary);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
}
.language-trigger:hover {
border-color: var(--color-border-hover);
color: var(--color-text-primary);
}
.language-chevron {
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
.nav-language.open .language-chevron {
transform: rotate(180deg);
}
.language-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 140px;
background: var(--color-background-card);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
z-index: 100;
overflow: hidden;
}
.nav-language.open .language-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.language-option {
display: block;
padding: 10px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: all 0.15s ease;
}
.language-option:hover {
background: var(--color-background-card-hover);
color: var(--color-text-primary);
}
.language-option-active {
color: var(--color-primary);
background: var(--color-primary-glow);
}
.nav-cta {
display: none;
padding: 10px 20px;
background: var(--color-primary);
color: white;
text-decoration: none;
font-size: 0.9375rem;
font-weight: 600;
border-radius: 8px;
transition: all 0.2s ease;
}
@media (min-width: 640px) {
.nav-cta {
display: inline-flex;
}
}
.nav-cta:hover {
background: var(--color-primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--color-primary-glow);
}
.nav-mobile-toggle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
background: transparent;
border: none;
color: var(--color-text-primary);
cursor: pointer;
}
@media (min-width: 768px) {
.nav-mobile-toggle {
display: none;
}
}
.nav-hamburger,
.nav-close {
width: 24px;
height: 24px;
}
.nav-close {
display: none;
}
.nav-header.mobile-open .nav-hamburger {
display: none;
}
.nav-header.mobile-open .nav-close {
display: block;
}
.nav-mobile-menu {
display: none;
padding: 16px 24px 24px;
background: var(--color-background-page);
border-top: 1px solid var(--color-border);
}
.nav-header.mobile-open .nav-mobile-menu {
display: block;
}
@media (min-width: 768px) {
.nav-mobile-menu {
display: none !important;
}
}
.nav-mobile-links {
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-mobile-link {
display: block;
padding: 12px 16px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 1rem;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s ease;
}
.nav-mobile-link:hover {
color: var(--color-text-primary);
background: var(--color-background-card);
}
.nav-mobile-cta {
display: block;
margin-top: 16px;
padding: 14px 24px;
background: var(--color-primary);
color: white;
text-decoration: none;
font-size: 1rem;
font-weight: 600;
text-align: center;
border-radius: 8px;
transition: all 0.2s ease;
}
.nav-mobile-cta:hover {
background: var(--color-primary-hover);
}
</style>
<script>
// Mobile menu toggle
document.querySelectorAll('.nav-mobile-toggle').forEach((toggle) => {
toggle.addEventListener('click', () => {
const header = toggle.closest('.nav-header');
header?.classList.toggle('mobile-open');
});
});
// Language dropdown toggle
document.querySelectorAll('.nav-language .language-trigger').forEach((trigger) => {
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const dropdown = trigger.closest('.nav-language');
dropdown?.classList.toggle('open');
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', () => {
document.querySelectorAll('.nav-language.open').forEach((dropdown) => {
dropdown.classList.remove('open');
});
});
// Close mobile menu on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.nav-header.mobile-open').forEach((header) => {
header.classList.remove('mobile-open');
});
document.querySelectorAll('.nav-language.open').forEach((dropdown) => {
dropdown.classList.remove('open');
});
}
});
</script>