@cronos-labs/ui 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import { useLocation } from 'react-router-dom';
4
- import { NavDropdown as NavDropdownRoot, NavDropdownChevron, NavDropdownLabel, NavDropdownList, NavDropdownMenu, NavDropdownOption, NavDropdownTrigger, } from './styles.js';
3
+ import { useNavigationLocation } from '../navigation/context.js';
4
+ import { NavDropdown as NavDropdownRoot, NavDropdownChevron, NavDropdownDisabledOption, NavDropdownLabel, NavDropdownList, NavDropdownMenu, NavDropdownOption, NavDropdownOptionBadge, NavDropdownTrigger, } from './styles.js';
5
5
  const buildMenuId = (label) => `header-nav-dropdown-${label.toLowerCase().replace(/\s+/gu, '-')}`;
6
6
  export function NavDropdown({ label, items, variant, contrast, }) {
7
7
  const [isOpen, setIsOpen] = useState(false);
8
8
  const rootRef = useRef(null);
9
- const location = useLocation();
9
+ const { pathname, hash } = useNavigationLocation();
10
10
  const menuId = buildMenuId(label);
11
11
  useEffect(() => {
12
12
  setIsOpen(false);
13
- }, [location.pathname, location.hash]);
13
+ }, [pathname, hash]);
14
14
  useEffect(() => {
15
15
  if (!isOpen)
16
16
  return;
@@ -32,7 +32,7 @@ export function NavDropdown({ label, items, variant, contrast, }) {
32
32
  document.removeEventListener('keydown', handleKeyDown);
33
33
  };
34
34
  }, [isOpen]);
35
- return (_jsxs(NavDropdownRoot, { ref: rootRef, onMouseEnter: () => setIsOpen(true), onMouseLeave: () => setIsOpen(false), children: [_jsxs(NavDropdownTrigger, { type: "button", "$variant": variant, "$contrast": contrast, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, onClick: () => setIsOpen((currentValue) => !currentValue), children: [_jsx(NavDropdownLabel, { "$contrast": contrast, children: label }), _jsx(NavDropdownChevron, { "$open": isOpen, "aria-hidden": "true" })] }), _jsx(NavDropdownMenu, { "$open": isOpen, id: menuId, role: "menu", "aria-label": label, children: _jsx(NavDropdownList, { children: items.map((item) => (_jsx(NavDropdownOption, { role: "menuitem", href: item.href, "aria-label": item.ariaLabel, onClick: (event) => {
35
+ return (_jsxs(NavDropdownRoot, { ref: rootRef, onMouseEnter: () => setIsOpen(true), onMouseLeave: () => setIsOpen(false), children: [_jsxs(NavDropdownTrigger, { type: "button", "$variant": variant, "$contrast": contrast, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, onClick: () => setIsOpen((currentValue) => !currentValue), children: [_jsx(NavDropdownLabel, { "$contrast": contrast, children: label }), _jsx(NavDropdownChevron, { "$open": isOpen, "aria-hidden": "true" })] }), _jsx(NavDropdownMenu, { "$open": isOpen, id: menuId, role: "menu", "aria-label": label, children: _jsx(NavDropdownList, { children: items.map((item) => item.disabled ? (_jsxs(NavDropdownDisabledOption, { role: "menuitem", "aria-disabled": "true", "aria-label": `${item.label} ${item.badge ?? ''}`.trim(), children: [item.label, item.badge ? _jsx(NavDropdownOptionBadge, { children: item.badge }) : null] }, item.key)) : (_jsx(NavDropdownOption, { role: "menuitem", href: item.href, "aria-label": item.ariaLabel, onClick: (event) => {
36
36
  setIsOpen(false);
37
37
  item.onClick?.(event);
38
38
  }, ...(item.external ? { target: '_blank', rel: 'noreferrer' } : {}), children: item.label }, item.key))) }) })] }));
@@ -1,47 +1,7 @@
1
- export type HeaderPresentationVariant = 'default' | 'inverse';
2
- export interface HeaderPresentationState {
3
- backgroundColor: string;
4
- contrast?: boolean;
5
- textColor: string;
6
- variant: HeaderPresentationVariant;
7
- }
8
- export interface HeaderNavChild {
9
- key: string;
10
- label: string;
11
- route?: string;
12
- external?: boolean;
13
- }
14
- export interface HeaderNavItem {
15
- label: string;
16
- route?: string;
17
- enabled: boolean;
18
- order: number;
19
- external?: boolean;
20
- placeholder?: boolean;
21
- badge?: string;
22
- children?: HeaderNavChild[];
23
- }
24
- export interface HeaderContent {
25
- joinWaitlist: string;
26
- waitlistEmailCta: string;
27
- localeSelectorAriaLabel: string;
28
- }
29
- export interface HeaderWaitlistCta {
30
- href: string;
31
- onClick?: (event: React.MouseEvent<HTMLAnchorElement>, ctaLocation: 'nav_desktop' | 'nav_mobile') => void;
32
- }
33
- interface HeaderProps {
34
- /** Overrides the wordmark from `<BrandProvider>`. Optional when one is present. */
35
- brandWordmarkSrc?: string;
36
- content: HeaderContent;
37
- forceDocumentNavigation?: boolean;
38
- homeHref: string;
39
- isExternalHref: (href: string) => boolean;
40
- mobilePresentation?: HeaderPresentationState | undefined;
41
- navItems: HeaderNavItem[];
42
- presentation: HeaderPresentationState;
43
- shouldAnimatePresentation: boolean;
44
- waitlistCta?: HeaderWaitlistCta | undefined;
45
- }
46
- export declare function Header({ brandWordmarkSrc, content, forceDocumentNavigation, homeHref, isExternalHref, mobilePresentation, navItems, presentation, shouldAnimatePresentation, waitlistCta, }: HeaderProps): React.JSX.Element;
47
- export {};
1
+ import type { HeaderProps } from './types.js';
2
+ export type { HeaderContent, HeaderNavChild, HeaderNavItem, HeaderPresentationState, HeaderPresentationVariant, HeaderProps, HeaderWaitlistCta, } from './types.js';
3
+ /**
4
+ * The site header for apps with a react-router `<Router>` above it. Static
5
+ * sites without a router render `HeaderStatic` instead.
6
+ */
7
+ export declare function Header(props: HeaderProps): React.JSX.Element;
@@ -1,171 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { useLocation, useNavigate } from 'react-router-dom';
4
- import { LocaleSelector } from '../LocaleSelector/index.js';
5
- import { buildLocalizedPath, stripLocalePrefix } from '../locale/routes.js';
6
- import { useCurrentLocale } from '../locale/hooks.js';
7
- import { desktopViewportQuery } from '../tokens.js';
8
- import { useOptionalBrand } from '../brand/index.js';
9
- import { normalizePathname } from './helper.js';
10
- import { NavDropdown } from './NavDropdown.js';
11
- import { BrandWordmarkMask, HeaderAction, HeaderActionArrow, HeaderBrandLink, HeaderFrame, HeaderMobileNav, HeaderMobileNavDisabledItem, HeaderMobileNavGroup, HeaderMobileNavGroupChevron, HeaderMobileNavGroupPanel, HeaderMobileNavGroupPanelInner, HeaderMobileNavGroupToggle, HeaderMobileNavSubLink, HeaderMobileNavSubList, HeaderMobileNavLink, HeaderMobileOverlay, HeaderMobileLocaleSlot, HeaderMobileSheet, HeaderMobileSoonBadge, HeaderMobileToggle, HeaderMobileToggleIcon, HeaderMobileWaitlistArrow, HeaderMobileWaitlistLabel, HeaderMobileWaitlistLink, HeaderShell, Nav, NavBadge, NavBadgeLabel, Wordmark, } from './styles.js';
12
- function MobileNavGroup({ label, items }) {
13
- const [isExpanded, setIsExpanded] = useState(false);
14
- const panelId = `mobile-nav-group-${label.toLowerCase().replace(/\s+/gu, '-')}`;
15
- return (_jsxs(HeaderMobileNavGroup, { children: [_jsxs(HeaderMobileNavGroupToggle, { type: "button", "aria-expanded": isExpanded, "aria-controls": panelId, onClick: () => setIsExpanded((currentValue) => !currentValue), children: [label, _jsx(HeaderMobileNavGroupChevron, { "$open": isExpanded, "aria-hidden": "true" })] }), _jsx(HeaderMobileNavGroupPanel, { "$open": isExpanded, id: panelId, children: _jsx(HeaderMobileNavGroupPanelInner, { "$open": isExpanded, children: _jsx(HeaderMobileNavSubList, { "aria-hidden": !isExpanded, children: items.map((item) => (_jsx(HeaderMobileNavSubLink, { href: item.href, "aria-label": item.ariaLabel, tabIndex: isExpanded ? undefined : -1, onClick: item.onClick, ...(item.external ? { target: '_blank', rel: 'noreferrer' } : {}), children: item.label }, item.key))) }) }) })] }));
16
- }
17
- export function Header({ brandWordmarkSrc, content, forceDocumentNavigation = false, homeHref, isExternalHref, mobilePresentation, navItems, presentation, shouldAnimatePresentation, waitlistCta, }) {
18
- const brand = useOptionalBrand();
19
- const wordmarkSrc = brandWordmarkSrc ?? brand?.wordmarkSrc;
20
- const brandName = brand?.name ?? '';
21
- // Rendering a header with no wordmark is never what the caller meant, and a
22
- // broken image is harder to diagnose than a named error.
23
- if (wordmarkSrc === undefined) {
24
- throw new Error('<Header> needs a brandWordmarkSrc prop or a <BrandProvider>.');
25
- }
26
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
27
- const locale = useCurrentLocale();
28
- const location = useLocation();
29
- const navigate = useNavigate();
30
- const localizedHomePath = normalizePathname(homeHref);
31
- const isHomeRoute = normalizePathname(location.pathname) === localizedHomePath;
32
- const enabledNavItems = navItems.filter((item) => item.enabled).sort((a, b) => a.order - b.order);
33
- const canonicalizeDocumentHref = (href) => {
34
- const hashStart = href.indexOf('#');
35
- const path = hashStart === -1 ? href : href.slice(0, hashStart);
36
- const hash = hashStart === -1 ? '' : href.slice(hashStart);
37
- if (path === '/' || path.endsWith('/')) {
38
- return href;
39
- }
40
- return `${path}/${hash}`;
41
- };
42
- useEffect(() => {
43
- setIsMobileMenuOpen(false);
44
- }, [location.pathname, location.hash]);
45
- useEffect(() => {
46
- const desktopMediaQuery = window.matchMedia(desktopViewportQuery);
47
- const handleChange = (event) => {
48
- if (event.matches) {
49
- setIsMobileMenuOpen(false);
50
- }
51
- };
52
- desktopMediaQuery.addEventListener('change', handleChange);
53
- return () => {
54
- desktopMediaQuery.removeEventListener('change', handleChange);
55
- };
56
- }, []);
57
- const resolveHref = (route) => {
58
- const href = route.startsWith('#')
59
- ? isHomeRoute
60
- ? route
61
- : `${localizedHomePath}${route}`
62
- : route.startsWith('/')
63
- ? buildLocalizedPath(route, locale)
64
- : route;
65
- return forceDocumentNavigation || isExternalHref(stripLocalePrefix(normalizePathname(href)))
66
- ? canonicalizeDocumentHref(href)
67
- : href;
68
- };
69
- const handleInternalNavigation = (event, href) => {
70
- if (event.defaultPrevented ||
71
- event.button !== 0 ||
72
- event.metaKey ||
73
- event.altKey ||
74
- event.ctrlKey ||
75
- event.shiftKey) {
76
- return;
77
- }
78
- const isSameRouteNavigation = normalizePathname(href) === normalizePathname(location.pathname);
79
- if (isSameRouteNavigation) {
80
- event.preventDefault();
81
- window.scrollTo(0, 0);
82
- document
83
- .querySelector('[data-home-shell="true"]')
84
- ?.scrollTo({ top: 0, left: 0, behavior: 'auto' });
85
- return;
86
- }
87
- if (isExternalHref(stripLocalePrefix(normalizePathname(href)))) {
88
- return;
89
- }
90
- event.preventDefault();
91
- void navigate(href);
92
- };
93
- const handleWaitlistCtaClick = (event, ctaLocation) => {
94
- setIsMobileMenuOpen(false);
95
- waitlistCta?.onClick?.(event, ctaLocation);
96
- };
97
- return (_jsxs(HeaderFrame, { "data-header-frame": "true", "$shouldAnimatePresentation": shouldAnimatePresentation, "$backgroundColor": presentation.backgroundColor, "$mobileBackgroundColor": mobilePresentation?.backgroundColor, "$contrast": presentation.contrast, children: [_jsxs(HeaderShell, { "$shouldAnimatePresentation": shouldAnimatePresentation, "$textColor": presentation.textColor, "$mobileTextColor": mobilePresentation?.textColor, children: [_jsx(HeaderBrandLink, { href: forceDocumentNavigation ? canonicalizeDocumentHref(homeHref) : homeHref, "aria-label": brandName ? `${brandName} home` : 'Home', onClick: forceDocumentNavigation
98
- ? undefined
99
- : (event) => handleInternalNavigation(event, homeHref), children: presentation.contrast ? (_jsx(BrandWordmarkMask, { "$url": wordmarkSrc, role: "img", "aria-label": brandName })) : (_jsx(Wordmark, { "$shouldAnimatePresentation": shouldAnimatePresentation, "$variant": presentation.variant, "$mobileVariant": mobilePresentation?.variant, src: wordmarkSrc, alt: brandName })) }), _jsx(Nav, { "$variant": presentation.variant, "$contrast": presentation.contrast, "aria-label": "Global navigation", children: enabledNavItems.map((item) => {
100
- const label = item.label;
101
- if (item.children && item.children.length > 0) {
102
- const dropdownItems = item.children
103
- .filter((child) => Boolean(child.route))
104
- .map((child) => {
105
- const childHref = resolveHref(child.route);
106
- return {
107
- key: child.key,
108
- label: child.label,
109
- href: childHref,
110
- external: child.external,
111
- onClick: child.external || forceDocumentNavigation
112
- ? undefined
113
- : (event) => {
114
- handleInternalNavigation(event, childHref);
115
- },
116
- };
117
- });
118
- return (_jsx(NavDropdown, { label: label, items: dropdownItems, variant: presentation.variant, contrast: presentation.contrast }, item.label));
119
- }
120
- const content_ = (_jsxs(_Fragment, { children: [label, item.badge ? (_jsx(NavBadge, { children: _jsx(NavBadgeLabel, { children: item.badge }) })) : null] }));
121
- if (item.placeholder || !item.route) {
122
- return (_jsx("span", { "aria-disabled": "true", "aria-label": `${label} ${item.badge ?? ''}`.trim(), "data-has-badge": item.badge ? 'true' : undefined, children: content_ }, item.label));
123
- }
124
- const href = resolveHref(item.route);
125
- return (_jsx("a", { href: href, "data-has-badge": item.badge ? 'true' : undefined, onClick: item.external
126
- ? undefined
127
- : forceDocumentNavigation
128
- ? undefined
129
- : (event) => {
130
- handleInternalNavigation(event, href);
131
- }, ...(item.external ? { target: '_blank', rel: 'noreferrer' } : {}), children: content_ }, item.label));
132
- }) }), _jsx(HeaderMobileToggle, { type: "button", "aria-label": isMobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu', "aria-expanded": isMobileMenuOpen, "aria-controls": "mobile-navigation", onClick: () => setIsMobileMenuOpen((currentValue) => !currentValue), children: _jsx(HeaderMobileToggleIcon, { "$open": isMobileMenuOpen, children: _jsx("span", {}) }) }), waitlistCta ? (_jsxs(HeaderAction, { "$variant": presentation.variant, href: resolveHref(waitlistCta.href), onClick: forceDocumentNavigation
133
- ? undefined
134
- : (event) => handleWaitlistCtaClick(event, 'nav_desktop'), children: [content.joinWaitlist, _jsx(HeaderActionArrow, { "aria-hidden": "true", children: "\u2192" })] })) : null] }), isMobileMenuOpen ? (_jsx(HeaderMobileOverlay, { onClick: () => setIsMobileMenuOpen(false), children: _jsxs(HeaderMobileSheet, { id: "mobile-navigation", role: "dialog", "aria-modal": "true", "aria-label": "Navigation menu", onClick: (event) => event.stopPropagation(), children: [_jsx(HeaderMobileNav, { "aria-label": "Mobile navigation", children: enabledNavItems.map((item) => {
135
- const label = item.label;
136
- if (item.children && item.children.length > 0) {
137
- const groupItems = item.children
138
- .filter((child) => Boolean(child.route))
139
- .map((child) => {
140
- const childHref = resolveHref(child.route);
141
- return {
142
- key: child.key,
143
- label: child.label,
144
- href: childHref,
145
- external: child.external,
146
- onClick: child.external || forceDocumentNavigation
147
- ? () => setIsMobileMenuOpen(false)
148
- : (event) => {
149
- setIsMobileMenuOpen(false);
150
- handleInternalNavigation(event, childHref);
151
- },
152
- };
153
- });
154
- return _jsx(MobileNavGroup, { label: label, items: groupItems }, item.label);
155
- }
156
- if (item.placeholder || !item.route) {
157
- return (_jsxs(HeaderMobileNavDisabledItem, { "aria-disabled": "true", "aria-label": `${label} ${item.badge ?? ''}`.trim(), children: [label, item.badge ? (_jsx(HeaderMobileSoonBadge, { children: item.badge })) : null] }, item.label));
158
- }
159
- const href = resolveHref(item.route);
160
- return (_jsxs(HeaderMobileNavLink, { href: href, onClick: item.external
161
- ? () => setIsMobileMenuOpen(false)
162
- : forceDocumentNavigation
163
- ? () => setIsMobileMenuOpen(false)
164
- : (event) => {
165
- setIsMobileMenuOpen(false);
166
- handleInternalNavigation(event, href);
167
- }, ...(item.external ? { target: '_blank', rel: 'noreferrer' } : {}), children: [label, item.badge ? (_jsx(HeaderMobileSoonBadge, { children: item.badge })) : null] }, item.label));
168
- }) }), _jsx(HeaderMobileLocaleSlot, { children: _jsx(LocaleSelector, { ariaLabel: content.localeSelectorAriaLabel, forceDocumentNavigation: forceDocumentNavigation, inlineMenu: true, menuPlacement: "down", onLocaleSelect: () => setIsMobileMenuOpen(false) }) }), waitlistCta ? (_jsxs(HeaderMobileWaitlistLink, { href: resolveHref(waitlistCta.href), onClick: forceDocumentNavigation
169
- ? () => setIsMobileMenuOpen(false)
170
- : (event) => handleWaitlistCtaClick(event, 'nav_mobile'), children: [_jsx(HeaderMobileWaitlistLabel, { children: content.waitlistEmailCta }), _jsx(HeaderMobileWaitlistArrow, { "aria-hidden": "true", children: "\u2192" })] })) : null] }) })) : null] }));
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { RouterNavigationProvider } from '../navigation/router.js';
3
+ import { HeaderView } from './view.js';
4
+ /**
5
+ * The site header for apps with a react-router `<Router>` above it. Static
6
+ * sites without a router render `HeaderStatic` instead.
7
+ */
8
+ export function Header(props) {
9
+ return (_jsx(RouterNavigationProvider, { children: _jsx(HeaderView, { ...props }) }));
171
10
  }
@@ -0,0 +1,15 @@
1
+ import type { AppLocale } from '../locale/types.js';
2
+ import type { HeaderProps } from './types.js';
3
+ export interface HeaderStaticProps extends Omit<HeaderProps, 'forceDocumentNavigation'> {
4
+ /** The page's locale, resolved by the host (Astro) rather than a router param. */
5
+ locale: AppLocale;
6
+ /** The page's pathname; drives the home-route check and the mobile menu's close-on-navigate. */
7
+ pathname: string;
8
+ hash?: string | undefined;
9
+ }
10
+ /**
11
+ * The site header for statically rendered pages with no client router. Every
12
+ * link is a document navigation, so this variant never imports react-router
13
+ * and needs no `<MemoryRouter>` around the island.
14
+ */
15
+ export declare function HeaderStatic({ locale, pathname, hash, ...props }: HeaderStaticProps): React.JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { StaticNavigationProvider } from '../navigation/static.js';
3
+ import { HeaderView } from './view.js';
4
+ /**
5
+ * The site header for statically rendered pages with no client router. Every
6
+ * link is a document navigation, so this variant never imports react-router
7
+ * and needs no `<MemoryRouter>` around the island.
8
+ */
9
+ export function HeaderStatic({ locale, pathname, hash, ...props }) {
10
+ return (_jsx(StaticNavigationProvider, { locale: locale, pathname: pathname, hash: hash, children: _jsx(HeaderView, { ...props, forceDocumentNavigation: true }) }));
11
+ }
@@ -1,4 +1,4 @@
1
- import type { HeaderPresentationVariant } from './index.js';
1
+ import type { HeaderPresentationVariant } from './types.js';
2
2
  export declare const HeaderFrame: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
3
3
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
4
4
  }, {
@@ -7,6 +7,9 @@ export declare const HeaderFrame: import("styled-components/dist/types").IStyled
7
7
  $mobileBackgroundColor?: string | undefined;
8
8
  $contrast?: boolean | undefined;
9
9
  }>> & string;
10
+ export declare const HeaderSkipLink: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
11
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
12
+ }> & string;
10
13
  export declare const HeaderShell: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>, "style"> & {
11
14
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
12
15
  }, {
@@ -77,6 +80,12 @@ export declare const NavDropdownList: import("styled-components/dist/types").ISt
77
80
  export declare const NavDropdownOption: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
78
81
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
79
82
  }> & string;
83
+ export declare const NavDropdownDisabledOption: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
84
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
85
+ }> & string;
86
+ export declare const NavDropdownOptionBadge: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
87
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
88
+ }> & string;
80
89
  export declare const NavBadge: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
81
90
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
82
91
  }> & string;
@@ -87,6 +96,7 @@ export declare const HeaderAction: import("styled-components/dist/types").IStyle
87
96
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
88
97
  }, {
89
98
  $variant: HeaderPresentationVariant;
99
+ $disabled?: boolean | undefined;
90
100
  }>> & string;
91
101
  export declare const HeaderActionArrow: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
92
102
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
@@ -138,6 +148,9 @@ export declare const HeaderMobileNavSubList: import("styled-components/dist/type
138
148
  export declare const HeaderMobileNavSubLink: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
139
149
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
140
150
  }> & string;
151
+ export declare const HeaderMobileNavSubDisabledItem: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
152
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
153
+ }> & string;
141
154
  export declare const HeaderMobileNavDisabledItem: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
142
155
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
143
156
  }> & string;
@@ -147,9 +160,11 @@ export declare const HeaderMobileSoonBadge: import("styled-components/dist/types
147
160
  export declare const HeaderMobileLocaleSlot: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
148
161
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
149
162
  }> & string;
150
- export declare const HeaderMobileWaitlistLink: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
163
+ export declare const HeaderMobileWaitlistLink: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "style"> & {
151
164
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
152
- }> & string;
165
+ }, {
166
+ $disabled?: boolean | undefined;
167
+ }>> & string;
153
168
  export declare const HeaderMobileWaitlistLabel: import("styled-components/dist/types").IStyledComponentBase<"web", Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "style"> & {
154
169
  style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
155
170
  }> & string;
@@ -88,6 +88,29 @@ const verticalSwapText = (top, bottom) => `
88
88
  -webkit-background-clip: text;
89
89
  background-clip: text;
90
90
  `;
91
+ // Off-canvas until focused, then a solid pill over the header so a keyboard
92
+ // user's first Tab lands on a visible control rather than the wordmark.
93
+ export const HeaderSkipLink = styled.a `
94
+ position: absolute;
95
+ top: ${({ theme }) => theme.sizes.xs};
96
+ left: ${({ theme }) => theme.sizes.xs};
97
+ z-index: 1;
98
+ padding: 10px 16px;
99
+ border-radius: ${({ theme }) => theme.borderRadius.pill};
100
+ background: ${({ theme }) => theme.colors.ink};
101
+ color: ${({ theme }) => theme.colors.light};
102
+ font-size: 0.875rem;
103
+ font-weight: ${({ theme }) => theme.fontWeight.semibold};
104
+ line-height: 1.25;
105
+ text-decoration: none;
106
+ transform: translateY(calc(-100% - ${({ theme }) => theme.sizes.md}));
107
+
108
+ &:focus-visible {
109
+ outline: 2px solid ${({ theme }) => theme.colors.light};
110
+ outline-offset: 2px;
111
+ transform: none;
112
+ }
113
+ `;
91
114
  export const HeaderShell = styled.header `
92
115
  width: min(
93
116
  calc(100% - (2 * ${({ theme }) => theme.layout.pagePaddingInline})),
@@ -398,6 +421,43 @@ export const NavDropdownOption = styled.a `
398
421
  outline-offset: -3px;
399
422
  }
400
423
  `;
424
+ /* A greyed dropdown entry: the option's shape without its hover or link
425
+ semantics, the badge in the disabled nav item's muted pill. */
426
+ export const NavDropdownDisabledOption = styled.span `
427
+ display: flex;
428
+ align-items: center;
429
+ gap: 8px;
430
+ width: 100%;
431
+ min-height: 44px;
432
+ padding: 12px;
433
+ border-radius: 8px;
434
+ color: inherit;
435
+ font-size: 1rem;
436
+ font-weight: ${({ theme }) => theme.fontWeight.semibold};
437
+ letter-spacing: -0.02em;
438
+ line-height: ${({ theme }) => theme.typography.lineHeight.tight};
439
+ text-align: left;
440
+ white-space: nowrap;
441
+ opacity: 0.5;
442
+ cursor: default;
443
+ `;
444
+ export const NavDropdownOptionBadge = styled.span `
445
+ display: inline-flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ min-width: 44px;
449
+ height: 18px;
450
+ padding: 0 8px;
451
+ border-radius: ${({ theme }) => theme.borderRadius.pill};
452
+ background: rgb(0 0 0 / 0.08);
453
+ color: currentColor;
454
+ font-size: 0.6875rem;
455
+ font-weight: ${({ theme }) => theme.fontWeight.black};
456
+ letter-spacing: 0.1em;
457
+ line-height: ${({ theme }) => theme.typography.lineHeight.tight};
458
+ text-transform: uppercase;
459
+ white-space: nowrap;
460
+ `;
401
461
  export const NavBadge = styled.span `
402
462
  display: inline-flex;
403
463
  align-items: center;
@@ -407,8 +467,8 @@ export const NavBadge = styled.span `
407
467
  padding: 0 8px;
408
468
  border-radius: ${({ theme }) => theme.borderRadius.pill};
409
469
  background: ${({ theme }) => theme.colors.accentSky};
410
- color: ${({ theme }) => theme.colors.light};
411
- font-size: 0.5rem;
470
+ color: ${({ theme }) => theme.colors.ink};
471
+ font-size: 0.6875rem;
412
472
  font-weight: ${({ theme }) => theme.fontWeight.black};
413
473
  letter-spacing: 0.1em;
414
474
  line-height: ${({ theme }) => theme.typography.lineHeight.tight};
@@ -458,6 +518,20 @@ export const HeaderAction = styled.a `
458
518
  outline-offset: 3px;
459
519
  }
460
520
 
521
+ /* Disabled: rendered as a span, so no hover lift and no focus ring;
522
+ dimmed in place so the layout does not move when it goes live. */
523
+ ${({ $disabled }) => $disabled
524
+ ? `
525
+ opacity: 0.55;
526
+ cursor: default;
527
+
528
+ &:hover {
529
+ opacity: 0.55;
530
+ transform: none;
531
+ }
532
+ `
533
+ : ''}
534
+
461
535
  ${({ theme }) => theme.mediaQueries.lg} {
462
536
  display: inline-flex;
463
537
  min-width: ${({ theme }) => theme.layout.headerActionWidth};
@@ -698,6 +772,22 @@ export const HeaderMobileNavSubLink = styled.a `
698
772
  outline-offset: -4px;
699
773
  }
700
774
  `;
775
+ /* A greyed entry inside an expanded mobile group: the sub-link's shape,
776
+ dimmed, with no hover. */
777
+ export const HeaderMobileNavSubDisabledItem = styled.span `
778
+ display: flex;
779
+ align-items: center;
780
+ gap: 6px;
781
+ min-height: 40px;
782
+ padding: 8px 12px;
783
+ color: ${({ theme }) => theme.colors.textDarkSubtle};
784
+ font-size: ${({ theme }) => theme.typography.fontSize.bodySm};
785
+ font-weight: ${({ theme }) => theme.fontWeight.semibold};
786
+ letter-spacing: -0.02em;
787
+ line-height: ${({ theme }) => theme.typography.lineHeight.compact};
788
+ opacity: 0.55;
789
+ cursor: default;
790
+ `;
701
791
  export const HeaderMobileNavDisabledItem = styled.span `
702
792
  display: inline-flex;
703
793
  align-items: center;
@@ -724,7 +814,7 @@ export const HeaderMobileSoonBadge = styled.span `
724
814
  background: rgb(255 255 255 / 0.42);
725
815
  color: currentColor;
726
816
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.08);
727
- font-size: 0.5rem;
817
+ font-size: 0.6875rem;
728
818
  font-weight: ${({ theme }) => theme.fontWeight.black};
729
819
  letter-spacing: 0.1em;
730
820
  line-height: ${({ theme }) => theme.typography.lineHeight.tight};
@@ -751,6 +841,8 @@ export const HeaderMobileWaitlistLink = styled.a `
751
841
  color: ${({ theme }) => theme.colors.textDarkSubtle};
752
842
  text-decoration: none;
753
843
  overflow: hidden;
844
+ opacity: ${({ $disabled }) => ($disabled ? 0.55 : 1)};
845
+ cursor: ${({ $disabled }) => ($disabled ? 'default' : 'pointer')};
754
846
 
755
847
  &:focus-visible {
756
848
  outline: 2px solid ${({ theme }) => theme.colors.ink};
@@ -0,0 +1,61 @@
1
+ export type HeaderPresentationVariant = 'default' | 'inverse';
2
+ export interface HeaderPresentationState {
3
+ backgroundColor: string;
4
+ contrast?: boolean;
5
+ textColor: string;
6
+ variant: HeaderPresentationVariant;
7
+ }
8
+ export interface HeaderNavChild {
9
+ key: string;
10
+ label: string;
11
+ route?: string;
12
+ external?: boolean;
13
+ /**
14
+ * Renders the child greyed and non-interactive, with `badge` beside it if
15
+ * given — for a destination that is not open yet. A disabled child needs
16
+ * no `route`.
17
+ */
18
+ disabled?: boolean;
19
+ badge?: string;
20
+ }
21
+ export interface HeaderNavItem {
22
+ label: string;
23
+ route?: string;
24
+ enabled: boolean;
25
+ order: number;
26
+ external?: boolean;
27
+ placeholder?: boolean;
28
+ badge?: string;
29
+ children?: HeaderNavChild[];
30
+ }
31
+ export interface HeaderContent {
32
+ joinWaitlist: string;
33
+ waitlistEmailCta: string;
34
+ localeSelectorAriaLabel: string;
35
+ /**
36
+ * Label of the skip-to-content link rendered as the header's first focusable
37
+ * element. Omit to render no link (existing consumers keep their markup).
38
+ */
39
+ skipToContent?: string | undefined;
40
+ }
41
+ export interface HeaderWaitlistCta {
42
+ href: string;
43
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>, ctaLocation: 'nav_desktop' | 'nav_mobile') => void;
44
+ /** Renders the CTA greyed and non-interactive, keeping its place and label. */
45
+ disabled?: boolean;
46
+ }
47
+ export interface HeaderProps {
48
+ /** Overrides the wordmark from `<BrandProvider>`. Optional when one is present. */
49
+ brandWordmarkSrc?: string;
50
+ content: HeaderContent;
51
+ forceDocumentNavigation?: boolean;
52
+ homeHref: string;
53
+ isExternalHref: (href: string) => boolean;
54
+ /** `id` of the page's main landmark the skip link targets. Defaults to `main`. */
55
+ mainContentId?: string | undefined;
56
+ mobilePresentation?: HeaderPresentationState | undefined;
57
+ navItems: HeaderNavItem[];
58
+ presentation: HeaderPresentationState;
59
+ shouldAnimatePresentation: boolean;
60
+ waitlistCta?: HeaderWaitlistCta | undefined;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { HeaderProps } from './types.js';
2
+ export declare function HeaderView({ brandWordmarkSrc, content, forceDocumentNavigation, homeHref, isExternalHref, mainContentId, mobilePresentation, navItems, presentation, shouldAnimatePresentation, waitlistCta, }: HeaderProps): React.JSX.Element;