ui-header/dist/primitives/HeaderNav.js
autocommit 4215404598 chore: initial package split from monorepo
Package: @lilith/ui-header
Split from: lilith/ui.git or lilith/build.git
Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
2026-04-20 01:11:51 -07:00

201 lines
No EOL
8.4 KiB
JavaScript

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* HeaderNav - Navigation wrapper with dropdown support
*
* Wraps @lilith/ui-navigation Navigation component for headers.
* Supports hover-triggered dropdown menus on desktop.
* Falls back to accordion on mobile.
*/
import { useState, useRef, useEffect } from 'react';
import { Link } from '@lilith/ui-router';
import styled from '@lilith/ui-styled-components';
/**
* Get the href from a navigation item.
* Supports both `href` (ui-navigation standard) and `path` (marketplace config).
*/
const getItemHref = (item) => {
// Type assertion to access path property which exists in marketplace configs
const itemWithPath = item;
return item.href || itemWithPath.path || '#';
};
/**
* Check if a URL is external (absolute http/https).
* External URLs must render as <a> tags, not React Router <Link>.
*/
const isExternalUrl = (url) => url.startsWith('http://') || url.startsWith('https://');
// Desktop navigation container
const DesktopNav = styled.nav `
display: flex;
align-items: center;
gap: ${(props) => props.theme.spacing.sm};
@media (max-width: ${(props) => props.theme.breakpoints.md}) {
display: none;
}
`;
const NavItem = styled.div `
position: relative;
`;
const NavLink = styled(Link) `
display: flex;
align-items: center;
gap: ${(props) => props.theme.spacing.xs};
font-family: ${(props) => props.theme.typography.fontFamily.body};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.medium};
color: ${({ $active, theme }) => $active ? theme.colors.text.primary : theme.colors.text.secondary};
text-decoration: none;
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border-radius: ${(props) => props.theme.borderRadius.sm};
transition: ${(props) => props.theme.transitions.fast};
cursor: pointer;
${({ $active, theme }) => $active &&
`
background: ${theme.colors.primary.main}20;
color: ${theme.colors.text.primary};
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background: ${theme.colors.primary.main};
border-radius: 1px;
}
`}
&:hover {
background: ${(props) => props.theme.colors.hover.surface};
color: ${(props) => props.theme.colors.text.primary};
}
svg {
width: 14px;
height: 14px;
transition: transform ${(props) => props.theme.transitions.fast};
}
`;
const ExternalNavLink = styled.a `
display: flex;
align-items: center;
gap: ${(props) => props.theme.spacing.xs};
font-family: ${(props) => props.theme.typography.fontFamily.body};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.medium};
color: ${({ $active, theme }) => $active ? theme.colors.text.primary : theme.colors.text.secondary};
text-decoration: none;
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border-radius: ${(props) => props.theme.borderRadius.sm};
transition: ${(props) => props.theme.transitions.fast};
cursor: pointer;
&:hover {
background: ${(props) => props.theme.colors.hover.surface};
color: ${(props) => props.theme.colors.text.primary};
}
`;
const Dropdown = styled.ul `
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background-color: ${(props) => props.theme.colors.surface};
border: 1px solid ${(props) => props.theme.colors.border.default};
border-radius: ${(props) => props.theme.borderRadius.lg};
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
list-style: none;
margin: ${(props) => props.theme.spacing.sm} 0 0;
padding: ${(props) => props.theme.spacing.sm};
opacity: ${({ $isOpen }) => ($isOpen ? 1 : 0)};
visibility: ${({ $isOpen }) => ($isOpen ? 'visible' : 'hidden')};
transform: translateY(${({ $isOpen }) => ($isOpen ? '0' : '-8px')});
transition: all ${(props) => props.theme.transitions.normal};
pointer-events: ${({ $isOpen }) => ($isOpen ? 'auto' : 'none')};
z-index: 100;
`;
const DropdownItem = styled.li `
margin: 0;
`;
const dropdownLinkStyles = `
display: block;
text-decoration: none;
cursor: pointer;
`;
const DropdownLink = styled(Link) `
${dropdownLinkStyles}
font-family: ${(props) => props.theme.typography.fontFamily.body};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.normal};
color: ${(props) => props.theme.colors.text.secondary};
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border-radius: ${(props) => props.theme.borderRadius.md};
transition: all ${(props) => props.theme.transitions.fast};
&:hover {
background-color: ${(props) => props.theme.colors.hover.surface};
color: ${(props) => props.theme.colors.text.primary};
}
`;
const ExternalDropdownLink = styled.a `
${dropdownLinkStyles}
font-family: ${(props) => props.theme.typography.fontFamily.body};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.normal};
color: ${(props) => props.theme.colors.text.secondary};
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border-radius: ${(props) => props.theme.borderRadius.md};
transition: all ${(props) => props.theme.transitions.fast};
&:hover {
background-color: ${(props) => props.theme.colors.hover.surface};
color: ${(props) => props.theme.colors.text.primary};
}
`;
const ChevronIcon = () => (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { fillRule: "evenodd", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", clipRule: "evenodd" }) }));
const NavItemWithDropdown = ({ item, dropdownTrigger }) => {
const [isOpen, setIsOpen] = useState(false);
const timeoutRef = useRef(undefined);
const hasChildren = !!(item.children && item.children.length > 0);
const handleMouseEnter = () => {
if (dropdownTrigger === 'hover' && hasChildren) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setIsOpen(true);
}
};
const handleMouseLeave = () => {
if (dropdownTrigger === 'hover' && hasChildren) {
timeoutRef.current = setTimeout(() => setIsOpen(false), 150);
}
};
const handleClick = (e) => {
if (hasChildren && dropdownTrigger === 'click') {
e.preventDefault();
setIsOpen(!isOpen);
}
else if (item.onClick) {
// Call onClick (e.g. for sound effects) but DON'T prevent default
// so the <a href="..."> link still navigates
item.onClick();
}
};
useEffect(() => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}, []);
const href = getItemHref(item);
const external = isExternalUrl(href);
return (_jsxs(NavItem, { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [external ? (_jsx(ExternalNavLink, { href: href, target: "_blank", rel: "noopener noreferrer", onClick: () => item.onClick?.(), children: item.label })) : (_jsxs(NavLink, { to: href, onClick: handleClick, "$hasChildren": hasChildren, children: [item.label, hasChildren && _jsx(ChevronIcon, {})] })), hasChildren && !external && (_jsx(Dropdown, { "$isOpen": isOpen, children: item.children.map((child, index) => {
const childHref = getItemHref(child);
const childExternal = isExternalUrl(childHref);
return (_jsx(DropdownItem, { children: childExternal ? (_jsx(ExternalDropdownLink, { href: childHref, target: "_blank", rel: "noopener noreferrer", onClick: child.onClick, children: child.label })) : (_jsx(DropdownLink, { to: childHref, onClick: child.onClick, children: child.label })) }, `${childHref}-${index}`));
}) }))] }));
};
export const HeaderNav = ({ items, dropdownTrigger = 'hover', className, }) => (_jsx(DesktopNav, { className: className, children: items.map((item, index) => (_jsx(NavItemWithDropdown, { item: item, dropdownTrigger: dropdownTrigger }, `${getItemHref(item)}-${index}`))) }));
//# sourceMappingURL=HeaderNav.js.map