import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { useEffect, useState } from 'react'; import { fn } from 'storybook/test'; import { FlamingoLogo, OpenFrameLogo } from '../components/icons'; import { Header } from '../components/navigation/header'; import { Button } from '../components/ui/button'; import type { HeaderConfig } from '../types/navigation'; const flamingoLogo = (
Flamingo
); const baseNavigation: HeaderConfig['navigation'] = { position: 'center', items: [ { id: 'openframe', label: 'OpenFrame', icon: , isActive: true, children: [ { id: 'of-overview', label: 'Overview', href: '/openframe' }, { id: 'of-cases', label: 'Case Studies', href: '/openframe/case-studies' }, { id: 'of-roadmap', label: 'Roadmap & Releases', href: '/openframe/roadmap' }, { id: 'of-webinars', label: 'Webinars', href: '/openframe/webinars' }, { id: 'of-kb', label: 'Knowledge Hub', href: '/openframe/knowledge-hub' }, ], }, { id: 'openmsp', label: 'OpenMSP', children: [ { id: 'om-overview', label: 'Overview', href: '/openmsp' }, { id: 'om-blog', label: 'Blog', href: '/openmsp/blog' }, ], }, { id: 'company', label: 'Company', href: '/company', }, ], }; const rightActions = [ , ]; const meta = { title: 'Navigation/Header', component: Header, parameters: { layout: 'fullscreen', docs: { description: { component: 'Marketing/site header built on the unified `TopNavigation` shell (Figma `[UPD] top-navigation`, node 2797-5978): ' + '48px mobile / 56px md+ bar, cell model, burger as a leading cell below `lg`, nav links centered from `lg`. ' + 'Dropdown children are **always rendered in the DOM** — toggled via `visibility`/`opacity` — so their `` links are present in the initial HTML for SEO crawlers (no more "orphan" pages). Hidden anchors are automatically excluded from tab order and the accessibility tree via `visibility: hidden` + `aria-hidden`.', }, }, }, tags: ['autodocs'], decorators: [ Story => (

Page content goes here — scroll to test auto-hide behavior.

), ], } satisfies Meta; export default meta; type Story = StoryObj; /** * Default header with dropdown navigation. Click "OpenFrame" or "OpenMSP" to open * the dropdown. Inspect the DOM — child `
` links are present even when * dropdowns are closed (hidden via CSS `visibility: hidden`). */ export const Default: Story = { args: { config: { logo: { element: flamingoLogo, href: '/' }, navigation: baseNavigation, actions: { right: rightActions }, autoHide: true, mobile: { enabled: true, onToggle: fn() }, } satisfies HeaderConfig, }, }; /** * Programmatically opens the OpenFrame dropdown on mount so you can see the * SEO-visible child links in the viewport without clicking. Also useful for * visual regression snapshots of the open state. */ export const DropdownOpen: Story = { args: { config: { logo: { element: flamingoLogo, href: '/' }, navigation: baseNavigation, actions: { right: rightActions }, autoHide: false, mobile: { enabled: true, onToggle: fn() }, } satisfies HeaderConfig, }, render: args => { useEffect(() => { // Simulate user click on the first dropdown trigger after mount const trigger = document.querySelector('nav button'); trigger?.click(); }, []); return
; }, }; /** * Demonstrates that the dropdown links live in the DOM even when closed. * The panel below mirrors `document.querySelectorAll('nav a[href]').length` * and updates whenever you open/close a dropdown — the count stays the same, * which is exactly what a crawler would see. */ export const SeoLinksAlwaysInDom: Story = { args: { config: { logo: { element: flamingoLogo, href: '/' }, navigation: baseNavigation, actions: { right: rightActions }, autoHide: false, mobile: { enabled: true, onToggle: fn() }, } satisfies HeaderConfig, }, render: args => { const [linkHrefs, setLinkHrefs] = useState([]); useEffect(() => { const collect = () => { const anchors = document.querySelectorAll('header nav a[href]'); setLinkHrefs(Array.from(anchors, a => a.getAttribute('href') || '')); }; collect(); const interval = window.setInterval(collect, 500); return () => window.clearInterval(interval); }, []); return ( <>

{linkHrefs.length} anchor(s) currently in the header DOM (visible to crawlers regardless of dropdown state):

    {linkHrefs.map((href, i) => (
  • {href || '(empty)'}
  • ))}
); }, }; /** * Header with `autoHide: false` — stays fixed while scrolling. */ export const FixedHeader: Story = { args: { config: { logo: { element: flamingoLogo, href: '/' }, navigation: baseNavigation, actions: { right: rightActions }, autoHide: false, mobile: { enabled: true, onToggle: fn() }, } satisfies HeaderConfig, }, }; /** * Header with the Mingo AI launcher enabled via `config.mingo`. Per the unified * top-navigation spec the burger is a **leading** cell (left edge, below `lg`), * so the bar reads `hamburger | logo … Try for Free | Mingo AI`, with Mingo * full-height and flush to the right edge (its left border acts as the * divider). Resize under 800px to see the Mingo label collapse to icon-only. * Clicking it dispatches an `ask-ai:open` event. */ export const WithMingoAi: Story = { args: { config: { logo: { element: flamingoLogo, href: '/' }, navigation: baseNavigation, actions: { persistent: [ , ], }, autoHide: false, mobile: { enabled: true, onToggle: fn() }, mingo: { enabled: true, source: 'flamingo' }, } satisfies HeaderConfig, }, };