@flamingo-stack/openframe-frontend-core 0.0.705-snapshot.20261006160157 → 0.0.705
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.
- package/dist/{chunk-WCVZCGK5.cjs → chunk-2QEUFXNS.cjs} +71 -312
- package/dist/{chunk-N45D4ZXQ.js → chunk-2YXVWZVF.js} +715 -959
- package/dist/{chunk-7OFPU7WE.cjs → chunk-3MX5S4CF.cjs} +2547 -877
- package/dist/{chunk-MB4PNY7C.cjs → chunk-4I45Y3DN.cjs} +6 -6
- package/dist/{chunk-K6O43WDL.cjs → chunk-4KX6OED2.cjs} +46 -46
- package/dist/{chunk-JPG7OHEG.js → chunk-5HBHO5TB.js} +1 -1
- package/dist/{chunk-3PLDQDE6.cjs → chunk-6NJKKO73.cjs} +15 -15
- package/dist/{chunk-O2RSAA5M.js → chunk-6SEANMOU.js} +11 -11
- package/dist/{chunk-WMX26HB5.js → chunk-6XIF2Z7I.js} +3 -3
- package/dist/{chunk-TQ4LB2DS.cjs → chunk-7LS22BZU.cjs} +14 -14
- package/dist/{chunk-7GEBFJT2.js → chunk-7VVFKJY5.js} +3429 -1759
- package/dist/{chunk-DDVBX42E.cjs → chunk-A75QY6CD.cjs} +21 -21
- package/dist/{chunk-OF6GEI5A.cjs → chunk-ABU3TGXA.cjs} +3 -3
- package/dist/{chunk-4CJMQLB5.js → chunk-AQWZNP6A.js} +4 -4
- package/dist/{chunk-FI6GMDO7.js → chunk-CH5232HV.js} +662 -2455
- package/dist/{chunk-Y2GSOUCS.cjs → chunk-D7BTSVMS.cjs} +2 -2
- package/dist/{chunk-4ILM7W2R.js → chunk-DENB2XJC.js} +2 -2
- package/dist/{chunk-6232S532.js → chunk-DW7G4KG3.js} +3 -3
- package/dist/{chunk-3NCM4ZQB.js → chunk-F6AFPNDF.js} +15 -15
- package/dist/{chunk-PXOQIA5K.js → chunk-F6I6XZMN.js} +5 -5
- package/dist/{chunk-G6UMNAVI.js → chunk-FCGC42TS.js} +61 -57
- package/dist/{chunk-BHI6GVC4.js → chunk-G5A3PSPV.js} +691 -1080
- package/dist/{chunk-HZ7CBXPL.cjs → chunk-HPWWNJE7.cjs} +89 -89
- package/dist/{chunk-SW5ZV2BN.cjs → chunk-IHERL5V4.cjs} +34 -34
- package/dist/{chunk-UJ33ZXDT.cjs → chunk-IKTQ3ZAG.cjs} +6 -6
- package/dist/{chunk-SEKYEWQG.cjs → chunk-JEZKGCZI.cjs} +26 -26
- package/dist/{chunk-7SCNMP2B.cjs → chunk-KOWVJD4V.cjs} +56 -56
- package/dist/chunk-LMBC5FFP.js +43 -0
- package/dist/{chunk-F57ECE2I.js → chunk-MFFCUVWG.js} +1 -1
- package/dist/{chunk-KUC673HM.js → chunk-MKXO4Q5P.js} +142 -383
- package/dist/{chunk-6NTPY5QA.cjs → chunk-MMOODW57.cjs} +94 -1887
- package/dist/{chunk-CUME3VTQ.js → chunk-MP2C3LJ4.js} +1 -1
- package/dist/{chunk-VX4E6YSB.cjs → chunk-N3COXDBH.cjs} +5 -41
- package/dist/{chunk-FXJ3WWUB.js → chunk-NDESEPW5.js} +0 -68
- package/dist/chunk-O43V3EOS.js +252 -0
- package/dist/{chunk-PLYEKV3N.cjs → chunk-OHFS7VOU.cjs} +2 -2
- package/dist/{chunk-M75XEEGT.cjs → chunk-OUY3HHW6.cjs} +539 -928
- package/dist/{chunk-CZ7Y7UG4.js → chunk-PDWVPI3S.js} +72 -4
- package/dist/{chunk-TE773HEH.js → chunk-PN2RKPDF.js} +10 -10
- package/dist/{chunk-3EZ4HHAP.js → chunk-QFXRPRFT.js} +3 -3
- package/dist/chunk-QZVSJF2K.cjs +252 -0
- package/dist/chunk-RALRPFDW.cjs +43 -0
- package/dist/{chunk-YGJKKDDM.js → chunk-RBDP4WDE.js} +6 -6
- package/dist/{chunk-LM7TXMLU.cjs → chunk-RKV5B7EU.cjs} +4 -72
- package/dist/{chunk-UVTD52MD.cjs → chunk-TYWLNCNH.cjs} +82 -14
- package/dist/{chunk-IKJA3Y7Z.js → chunk-VA5T32XA.js} +2 -2
- package/dist/{chunk-GWQEYPYM.cjs → chunk-VESKITRQ.cjs} +732 -728
- package/dist/{chunk-RQRX4YC3.cjs → chunk-VHCACDKP.cjs} +39 -39
- package/dist/{chunk-4IBPRIQH.js → chunk-VR2ZLFCP.js} +1 -1
- package/dist/{chunk-RTIPR53L.cjs → chunk-WA6GUYQ4.cjs} +104 -104
- package/dist/{chunk-T5TMCHWM.js → chunk-X5MQ7JRP.js} +9 -9
- package/dist/{chunk-LLSUVPQC.cjs → chunk-X72J4ACL.cjs} +8 -252
- package/dist/{chunk-UDFRUKLC.cjs → chunk-XOYH2YQE.cjs} +2 -2
- package/dist/{chunk-LVSFKENJ.cjs → chunk-YFBM2K6X.cjs} +4 -4
- package/dist/{chunk-DWPAAWMR.js → chunk-YHM5AWTT.js} +6 -6
- package/dist/{chunk-NCPTID44.js → chunk-Z2VNQQ74.js} +942 -978
- package/dist/{chunk-STHZBOQI.cjs → chunk-ZSAGLQOD.cjs} +17 -17
- package/dist/{chunk-VZICIQW2.js → chunk-ZSAO2HBW.js} +15 -15
- package/dist/components/case-studies/index.cjs +32 -30
- package/dist/components/case-studies/index.js +16 -14
- package/dist/components/chat/entity-cards/case-study-card.d.ts +4 -11
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +11 -9
- package/dist/components/chat/index.js +16 -14
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +14 -12
- package/dist/components/contact/index.js +13 -11
- package/dist/components/docs/index.cjs +17 -15
- package/dist/components/docs/index.js +16 -14
- package/dist/components/embeds/index.cjs +14 -12
- package/dist/components/embeds/index.js +13 -11
- package/dist/components/faq/index.cjs +15 -13
- package/dist/components/faq/index.js +14 -12
- package/dist/components/features/index.cjs +14 -12
- package/dist/components/features/index.js +23 -21
- package/dist/components/footer-waitlist-button.d.ts +29 -0
- package/dist/components/footer-waitlist-button.d.ts.map +1 -0
- package/dist/components/footer.d.ts +47 -0
- package/dist/components/footer.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +120 -118
- package/dist/components/help-center-pages/index.js +35 -33
- package/dist/components/icons/index.cjs +5 -3
- package/dist/components/icons/index.js +11 -9
- package/dist/components/icons-v2-generated/index.cjs +5 -3
- package/dist/components/icons-v2-generated/index.js +4 -2
- package/dist/components/index.cjs +159 -86
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +402 -329
- package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
- package/dist/components/meeting-scheduler/index.cjs +64 -62
- package/dist/components/meeting-scheduler/index.js +14 -12
- package/dist/components/navigation/client-only-header.d.ts +8 -0
- package/dist/components/navigation/client-only-header.d.ts.map +1 -0
- package/dist/components/navigation/header-skeleton.d.ts +12 -0
- package/dist/components/navigation/header-skeleton.d.ts.map +1 -0
- package/dist/components/navigation/header.d.ts +9 -0
- package/dist/components/navigation/header.d.ts.map +1 -0
- package/dist/components/navigation/index.cjs +16 -28
- package/dist/components/navigation/index.d.ts +10 -14
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +26 -38
- package/dist/components/navigation/mingo-ai-button.d.ts +3 -38
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/navigation/mobile-nav-panel.d.ts +10 -0
- package/dist/components/navigation/mobile-nav-panel.d.ts.map +1 -0
- package/dist/components/navigation/navigation-sidebar-item.d.ts +2 -1
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/top-navigation.d.ts +2 -2
- package/dist/components/navigation/top-navigation.d.ts.map +1 -1
- package/dist/components/onboarding-guides/index.cjs +17 -15
- package/dist/components/onboarding-guides/index.js +16 -14
- package/dist/components/related-content/index.cjs +8 -10
- package/dist/components/related-content/index.js +7 -9
- package/dist/components/tickets/index.cjs +18 -16
- package/dist/components/tickets/index.js +17 -15
- package/dist/components/ui/index.cjs +14 -12
- package/dist/components/ui/index.js +19 -17
- package/dist/hooks/index.cjs +11 -19
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +11 -19
- package/dist/hooks/ui/index.d.ts +0 -3
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/index.cjs +22 -55
- package/dist/index.js +196 -229
- package/dist/types/case-study.d.ts +0 -9
- package/dist/types/case-study.d.ts.map +1 -1
- package/dist/types/navigation.d.ts +65 -95
- package/dist/types/navigation.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -55
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -50
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/case-study-card.tsx +5 -115
- package/src/components/chat/utils/icon-library.tsx +1 -35
- package/src/components/footer-waitlist-button.tsx +53 -0
- package/src/components/footer.tsx +195 -0
- package/src/components/index.ts +2 -0
- package/src/components/loading/page-layout-skeleton.tsx +1 -1
- package/src/components/navigation/client-only-header.tsx +22 -0
- package/src/components/navigation/header-skeleton.tsx +74 -0
- package/src/components/navigation/header.tsx +429 -0
- package/src/components/navigation/index.ts +14 -21
- package/src/components/navigation/mingo-ai-button.tsx +63 -139
- package/src/components/navigation/mobile-nav-panel.tsx +155 -0
- package/src/components/navigation/navigation-sidebar-item.tsx +2 -1
- package/src/components/navigation/ticket-alerts-button.tsx +2 -2
- package/src/components/navigation/top-navigation.tsx +2 -2
- package/src/hooks/index.ts +0 -1
- package/src/hooks/ui/index.ts +0 -3
- package/src/styles/chat-animations.css +2 -57
- package/src/types/case-study.ts +0 -16
- package/src/types/navigation.ts +66 -80
- package/src/utils/index.ts +0 -1
- package/src/utils/source-icons.ts +0 -13
- package/dist/components/navigation/mobile-nav-sheet-id.d.ts +0 -3
- package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +0 -1
- package/dist/components/navigation/mobile-nav-sheet.d.ts +0 -48
- package/dist/components/navigation/mobile-nav-sheet.d.ts.map +0 -1
- package/dist/components/navigation/nav-item-row.d.ts +0 -27
- package/dist/components/navigation/nav-item-row.d.ts.map +0 -1
- package/dist/components/navigation/nav-panel.d.ts +0 -43
- package/dist/components/navigation/nav-panel.d.ts.map +0 -1
- package/dist/components/navigation/site-footer.d.ts +0 -28
- package/dist/components/navigation/site-footer.d.ts.map +0 -1
- package/dist/components/navigation/site-header.d.ts +0 -61
- package/dist/components/navigation/site-header.d.ts.map +0 -1
- package/dist/components/navigation/site-nav-link.d.ts +0 -26
- package/dist/components/navigation/site-nav-link.d.ts.map +0 -1
- package/dist/hooks/ui/use-active-section.d.ts +0 -10
- package/dist/hooks/ui/use-active-section.d.ts.map +0 -1
- package/dist/hooks/ui/use-nav-menus.d.ts +0 -48
- package/dist/hooks/ui/use-nav-menus.d.ts.map +0 -1
- package/dist/hooks/ui/use-visitor-os.d.ts +0 -15
- package/dist/hooks/ui/use-visitor-os.d.ts.map +0 -1
- package/dist/hooks/use-location-hash.d.ts +0 -9
- package/dist/hooks/use-location-hash.d.ts.map +0 -1
- package/dist/utils/visitor-os.d.ts +0 -45
- package/dist/utils/visitor-os.d.ts.map +0 -1
- package/src/components/navigation/mobile-nav-sheet-id.ts +0 -2
- package/src/components/navigation/mobile-nav-sheet.tsx +0 -241
- package/src/components/navigation/nav-item-row.tsx +0 -129
- package/src/components/navigation/nav-panel.tsx +0 -196
- package/src/components/navigation/site-footer.tsx +0 -256
- package/src/components/navigation/site-header.tsx +0 -339
- package/src/components/navigation/site-nav-link.tsx +0 -41
- package/src/hooks/ui/use-active-section.ts +0 -44
- package/src/hooks/ui/use-nav-menus.ts +0 -200
- package/src/hooks/ui/use-visitor-os.ts +0 -33
- package/src/hooks/use-location-hash.ts +0 -34
- package/src/utils/visitor-os.ts +0 -84
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type React from 'react';
|
|
4
|
-
import { useState } from 'react';
|
|
5
|
-
import type { NavGroup, SiteNav } from '../../types/navigation';
|
|
6
|
-
import { cn } from '../../utils/cn';
|
|
7
|
-
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
|
|
8
|
-
import { SocialIconRow } from '../social-icon-row';
|
|
9
|
-
import { Button } from '../ui/button';
|
|
10
|
-
import { NavItemRow } from './nav-item-row';
|
|
11
|
-
import { defaultRenderSiteNavLink, NAV_FOCUS_CLASS, navLinkLabel, type SiteNavLinkRenderer } from './site-nav-link';
|
|
12
|
-
|
|
13
|
-
export interface SiteFooterProps {
|
|
14
|
-
nav: SiteNav;
|
|
15
|
-
logo: React.ReactNode;
|
|
16
|
-
/** The platform's name beside the logo: a string is set as the wordmark, a
|
|
17
|
-
* node is drawn as given (a platform with its own styled name). */
|
|
18
|
-
name?: React.ReactNode;
|
|
19
|
-
renderLink?: SiteNavLinkRenderer;
|
|
20
|
-
/** The closing band's primary CTA when `nav.primaryCta` is `'trial'` or `'waitlist'`. */
|
|
21
|
-
cta?: React.ReactNode;
|
|
22
|
-
/** Replaces the default social row built from `nav.brand.social` (each
|
|
23
|
-
* link's `iconName` is its social platform key). */
|
|
24
|
-
social?: React.ReactNode;
|
|
25
|
-
/** Rendered in the brand column, under the tagline and the social links. */
|
|
26
|
-
brandExtra?: React.ReactNode;
|
|
27
|
-
/** Opaque ODS background class. Default `bg-ods-bg`. */
|
|
28
|
-
backgroundClassName?: string;
|
|
29
|
-
/** Generated, never typed. Default: the current year. */
|
|
30
|
-
year?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Below `md`, more columns than this fold into accordions. */
|
|
34
|
-
const ACCORDION_OVER = 2;
|
|
35
|
-
|
|
36
|
-
// Literal class maps: Tailwind's scanner needs the full class strings.
|
|
37
|
-
const DESKTOP_COLUMNS: Record<number, string> = {
|
|
38
|
-
1: 'md:grid-cols-1',
|
|
39
|
-
2: 'md:grid-cols-2',
|
|
40
|
-
3: 'md:grid-cols-3',
|
|
41
|
-
4: 'md:grid-cols-3 lg:grid-cols-4',
|
|
42
|
-
5: 'md:grid-cols-3 lg:grid-cols-5',
|
|
43
|
-
6: 'md:grid-cols-3 lg:grid-cols-6',
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const HEADING_CLASS = 'text-ods-text-muted text-h5';
|
|
47
|
-
|
|
48
|
-
function FooterColumn({
|
|
49
|
-
group,
|
|
50
|
-
accordion,
|
|
51
|
-
renderLink,
|
|
52
|
-
}: {
|
|
53
|
-
group: NavGroup;
|
|
54
|
-
accordion: boolean;
|
|
55
|
-
renderLink: SiteNavLinkRenderer;
|
|
56
|
-
}) {
|
|
57
|
-
const [open, setOpen] = useState(false);
|
|
58
|
-
const listId = `site-footer-${group.id}`;
|
|
59
|
-
const links = group.links.map(link => (
|
|
60
|
-
<li key={link.id}>
|
|
61
|
-
<NavItemRow link={link} variant="label" renderLink={renderLink} />
|
|
62
|
-
</li>
|
|
63
|
-
));
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<div
|
|
67
|
-
className={cn(
|
|
68
|
-
'flex min-w-0 flex-col md:gap-[var(--spacing-system-mf)]',
|
|
69
|
-
accordion ? 'border-b border-ods-border md:border-b-0' : 'gap-[var(--spacing-system-mf)]',
|
|
70
|
-
)}
|
|
71
|
-
>
|
|
72
|
-
{group.title &&
|
|
73
|
-
(accordion ? (
|
|
74
|
-
<>
|
|
75
|
-
{/* Below md the heading is the accordion's button; from md it is a plain heading. */}
|
|
76
|
-
<h3 className="md:hidden">
|
|
77
|
-
<Button
|
|
78
|
-
type="button"
|
|
79
|
-
variant="glyph"
|
|
80
|
-
size="wrap"
|
|
81
|
-
font="regular"
|
|
82
|
-
aria-expanded={open}
|
|
83
|
-
aria-controls={listId}
|
|
84
|
-
onClick={() => setOpen(prev => !prev)}
|
|
85
|
-
className={cn(
|
|
86
|
-
'flex min-h-14 w-full items-center justify-between gap-[var(--spacing-system-sf)] text-left',
|
|
87
|
-
HEADING_CLASS,
|
|
88
|
-
NAV_FOCUS_CLASS,
|
|
89
|
-
)}
|
|
90
|
-
>
|
|
91
|
-
{group.title}
|
|
92
|
-
<Chevron02DownIcon
|
|
93
|
-
aria-hidden="true"
|
|
94
|
-
className={cn('h-5 w-5 shrink-0 transition-transform duration-150', open && 'rotate-180')}
|
|
95
|
-
/>
|
|
96
|
-
</Button>
|
|
97
|
-
</h3>
|
|
98
|
-
<h3 className={cn('hidden md:block', HEADING_CLASS)}>{group.title}</h3>
|
|
99
|
-
</>
|
|
100
|
-
) : (
|
|
101
|
-
<h3 className={HEADING_CLASS}>{group.title}</h3>
|
|
102
|
-
))}
|
|
103
|
-
{/* Always in the DOM (crawlers read the footer links); a folded
|
|
104
|
-
accordion only hides its list below md. */}
|
|
105
|
-
<ul
|
|
106
|
-
id={listId}
|
|
107
|
-
className={cn(
|
|
108
|
-
'flex-col md:flex md:gap-[var(--spacing-system-sf)] md:pb-0',
|
|
109
|
-
accordion
|
|
110
|
-
? open
|
|
111
|
-
? 'flex gap-[var(--spacing-system-sf)] pb-[var(--spacing-system-mf)]'
|
|
112
|
-
: 'hidden'
|
|
113
|
-
: 'flex gap-[var(--spacing-system-sf)]',
|
|
114
|
-
)}
|
|
115
|
-
>
|
|
116
|
-
{links}
|
|
117
|
-
</ul>
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* The site footer of every platform, drawn from one `SiteNav`: the closing
|
|
124
|
-
* band, the brand column, the link columns and the legal line.
|
|
125
|
-
*/
|
|
126
|
-
export function SiteFooter({
|
|
127
|
-
nav,
|
|
128
|
-
logo,
|
|
129
|
-
name,
|
|
130
|
-
renderLink = defaultRenderSiteNavLink,
|
|
131
|
-
cta,
|
|
132
|
-
social,
|
|
133
|
-
brandExtra,
|
|
134
|
-
backgroundClassName,
|
|
135
|
-
year = new Date().getFullYear(),
|
|
136
|
-
}: SiteFooterProps) {
|
|
137
|
-
const band = nav.closingBand;
|
|
138
|
-
const bandPrimary =
|
|
139
|
-
typeof nav.primaryCta === 'object' ? (
|
|
140
|
-
<Button variant="accent" href={nav.primaryCta.href}>
|
|
141
|
-
{nav.primaryCta.label}
|
|
142
|
-
</Button>
|
|
143
|
-
) : nav.primaryCta === 'none' ? null : (
|
|
144
|
-
(cta ?? null)
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const columns = nav.footerColumns;
|
|
148
|
-
const accordion = columns.length > ACCORDION_OVER;
|
|
149
|
-
const socialRow =
|
|
150
|
-
social ??
|
|
151
|
-
(nav.brand.social.length > 0 ? (
|
|
152
|
-
<SocialIconRow
|
|
153
|
-
compact
|
|
154
|
-
links={nav.brand.social.map(link => ({
|
|
155
|
-
platform: link.iconName ?? link.id,
|
|
156
|
-
href: link.href,
|
|
157
|
-
label: link.label,
|
|
158
|
-
}))}
|
|
159
|
-
/>
|
|
160
|
-
) : null);
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<footer
|
|
164
|
-
className={cn(
|
|
165
|
-
'relative z-[44] w-full border-t border-ods-border px-[var(--spacing-system-lf)] text-ods-text-primary',
|
|
166
|
-
backgroundClassName ?? 'bg-ods-bg',
|
|
167
|
-
)}
|
|
168
|
-
>
|
|
169
|
-
{band && (
|
|
170
|
-
<div className="flex flex-col gap-[var(--spacing-system-lf)] border-b border-ods-border py-[var(--spacing-system-xlf)] md:flex-row md:items-center md:justify-between md:gap-[var(--spacing-system-xlf)] md:py-14">
|
|
171
|
-
<div className="flex min-w-0 flex-col gap-[var(--spacing-system-xsf)]">
|
|
172
|
-
<h2 className="text-h2">{band.heading}</h2>
|
|
173
|
-
<p className="text-ods-text-secondary text-h4">{band.subheading}</p>
|
|
174
|
-
</div>
|
|
175
|
-
<div className="flex shrink-0 flex-col gap-[var(--spacing-system-sf)] md:flex-row">
|
|
176
|
-
{bandPrimary}
|
|
177
|
-
{band.secondary && (
|
|
178
|
-
<Button variant="outline" href={band.secondary.href} openInNewTab={band.secondary.external}>
|
|
179
|
-
{navLinkLabel(band.secondary)}
|
|
180
|
-
</Button>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
)}
|
|
185
|
-
|
|
186
|
-
<div className="flex flex-col gap-8 py-[var(--spacing-system-xlf)] lg:flex-row lg:gap-16 lg:py-14">
|
|
187
|
-
<div className="flex w-full shrink-0 flex-col items-start gap-[var(--spacing-system-mf)] lg:w-60">
|
|
188
|
-
<span className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
189
|
-
{logo}
|
|
190
|
-
{typeof name === 'string' ? (
|
|
191
|
-
<span className="whitespace-nowrap text-ods-text-primary text-wordmark">{name}</span>
|
|
192
|
-
) : (
|
|
193
|
-
name
|
|
194
|
-
)}
|
|
195
|
-
</span>
|
|
196
|
-
<p className="text-ods-text-secondary text-h6">{nav.brand.tagline}</p>
|
|
197
|
-
{socialRow}
|
|
198
|
-
{nav.brand.statusUrl &&
|
|
199
|
-
renderLink({
|
|
200
|
-
link: { href: nav.brand.statusUrl, external: true },
|
|
201
|
-
className: cn(
|
|
202
|
-
'inline-flex items-center gap-[var(--spacing-system-xsf)] text-ods-text-secondary transition-colors text-h6 hover:text-ods-text-primary',
|
|
203
|
-
NAV_FOCUS_CLASS,
|
|
204
|
-
),
|
|
205
|
-
children: (
|
|
206
|
-
<>
|
|
207
|
-
<span aria-hidden="true" className="h-2 w-2 rounded-full bg-ods-success" />
|
|
208
|
-
System status
|
|
209
|
-
</>
|
|
210
|
-
),
|
|
211
|
-
})}
|
|
212
|
-
{brandExtra}
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{columns.length > 0 && (
|
|
216
|
-
<nav
|
|
217
|
-
aria-label="Footer"
|
|
218
|
-
className={cn(
|
|
219
|
-
'grid min-w-0 flex-1 md:gap-7',
|
|
220
|
-
accordion ? 'grid-cols-1' : 'grid-cols-2 gap-7',
|
|
221
|
-
DESKTOP_COLUMNS[Math.min(columns.length, 6)],
|
|
222
|
-
)}
|
|
223
|
-
>
|
|
224
|
-
{columns.map(group => (
|
|
225
|
-
<FooterColumn key={group.id} group={group} accordion={accordion} renderLink={renderLink} />
|
|
226
|
-
))}
|
|
227
|
-
</nav>
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
{/* The legal line is text, not navigation: muted, with its links in the
|
|
232
|
-
accent so they still read as links (prototype `.nf-legal`). */}
|
|
233
|
-
<div
|
|
234
|
-
className={cn(
|
|
235
|
-
'flex flex-col gap-[var(--spacing-system-xxs)] border-t border-ods-border pb-[var(--spacing-system-xlf)] pt-[var(--spacing-system-lf)] text-ods-text-muted text-h6 md:flex-row md:items-center md:justify-between md:gap-[var(--spacing-system-lf)]',
|
|
236
|
-
)}
|
|
237
|
-
>
|
|
238
|
-
<span>
|
|
239
|
-
© {year} {nav.legal.company}
|
|
240
|
-
</span>
|
|
241
|
-
<span className="flex flex-wrap items-center gap-x-[var(--spacing-system-mf)] gap-y-[var(--spacing-system-xxs)]">
|
|
242
|
-
{nav.legal.notes.length > 0 && <span>{nav.legal.notes.join(' · ')}</span>}
|
|
243
|
-
{nav.legal.links.map(link => (
|
|
244
|
-
<NavItemRow
|
|
245
|
-
key={link.id}
|
|
246
|
-
link={link}
|
|
247
|
-
variant="label"
|
|
248
|
-
renderLink={renderLink}
|
|
249
|
-
className="text-ods-accent hover:text-ods-accent hover:underline"
|
|
250
|
-
/>
|
|
251
|
-
))}
|
|
252
|
-
</span>
|
|
253
|
-
</div>
|
|
254
|
-
</footer>
|
|
255
|
-
);
|
|
256
|
-
}
|
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type React from 'react';
|
|
4
|
-
import { useEffect, useRef, useState } from 'react';
|
|
5
|
-
import Link from '../../embed-shims/next-link';
|
|
6
|
-
import { useActiveSection } from '../../hooks/ui/use-active-section';
|
|
7
|
-
import { useNavMenus } from '../../hooks/ui/use-nav-menus';
|
|
8
|
-
import type { NavMenu, SiteNav } from '../../types/navigation';
|
|
9
|
-
import { cn } from '../../utils/cn';
|
|
10
|
-
import { EntityIcon } from '../icon-display';
|
|
11
|
-
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
|
|
12
|
-
import { Menu01Icon } from '../icons-v2-generated/interface/menu-01-icon';
|
|
13
|
-
import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';
|
|
14
|
-
import { Button } from '../ui/button';
|
|
15
|
-
import { HeaderButton } from './header-button';
|
|
16
|
-
import { MingoAiButton } from './mingo-ai-button';
|
|
17
|
-
import { MOBILE_NAV_SHEET_ID } from './mobile-nav-sheet-id';
|
|
18
|
-
import { isMegaMenu, NavPanel, type NavPanelMotion } from './nav-panel';
|
|
19
|
-
import { defaultRenderSiteNavLink, NAV_FOCUS_CLASS, type SiteNavLinkRenderer } from './site-nav-link';
|
|
20
|
-
import { TopNavigation } from './top-navigation';
|
|
21
|
-
|
|
22
|
-
export interface SiteHeaderProps {
|
|
23
|
-
nav: SiteNav;
|
|
24
|
-
/** The host router's pathname: marks the active section and closes menus on navigation. */
|
|
25
|
-
pathname: string;
|
|
26
|
-
logo: React.ReactNode;
|
|
27
|
-
/** Omitted: the logo is not a link. */
|
|
28
|
-
logoHref?: string;
|
|
29
|
-
/** The host's link (Next `Link`, unified navigation). Default: `<a>`. */
|
|
30
|
-
renderLink?: SiteNavLinkRenderer;
|
|
31
|
-
/** Leading cells (admin sidebar toggle). Each cell owns its divider. */
|
|
32
|
-
leading?: React.ReactNode;
|
|
33
|
-
/** Host actions at the right (profile button, custom buttons). Shown at every width. */
|
|
34
|
-
actions?: React.ReactNode;
|
|
35
|
-
/** The primary CTA node when `nav.primaryCta` is `'trial'` or `'waitlist'`. */
|
|
36
|
-
cta?: React.ReactNode;
|
|
37
|
-
/** Trailing cells (ticket alerts). Each cell owns its divider. */
|
|
38
|
-
sideActions?: React.ReactNode;
|
|
39
|
-
/**
|
|
40
|
-
* The Mingo AI launcher, first in the right cluster (before Sign in).
|
|
41
|
-
* Omitted: no launcher. `icon` and `label` are the server-configured
|
|
42
|
-
* assistant identity (the same ones the chat panel shows); `source` scopes
|
|
43
|
-
* the `ask-ai:open` event the launcher dispatches.
|
|
44
|
-
*/
|
|
45
|
-
mingo?: { source?: string; icon?: React.ReactNode; label?: string; shortcutHint?: boolean };
|
|
46
|
-
/** Omitted: no burger. */
|
|
47
|
-
mobile?: { isOpen: boolean; onToggle: () => void; controlsId?: string };
|
|
48
|
-
/** Values for `NavLink.badgeKey`. */
|
|
49
|
-
badges?: Record<string, number>;
|
|
50
|
-
/** Rendered under a menu with `showSocial`. */
|
|
51
|
-
social?: React.ReactNode;
|
|
52
|
-
autoHide?: boolean;
|
|
53
|
-
/** Opaque ODS background class. */
|
|
54
|
-
backgroundClassName?: string;
|
|
55
|
-
className?: string;
|
|
56
|
-
style?: React.CSSProperties;
|
|
57
|
-
/** The id the "Skip to content" link targets. Default `main-content`. */
|
|
58
|
-
skipToContentId?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// A top-level item (prototype `.nv-l`): 40px tall, quiet until hovered, open
|
|
62
|
-
// or current.
|
|
63
|
-
const TOP_ITEM_CLASS =
|
|
64
|
-
'relative inline-flex h-10 items-center gap-1.5 whitespace-nowrap rounded-md px-3.5 text-ods-text-secondary transition-colors text-h6 hover:bg-ods-bg-hover hover:text-ods-text-primary';
|
|
65
|
-
// The current section: a 2px accent underline and an accent wash.
|
|
66
|
-
const TOP_ITEM_CURRENT_CLASS =
|
|
67
|
-
'rounded-b-none bg-gradient-to-b from-transparent to-ods-accent/10 text-ods-text-primary after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-ods-accent';
|
|
68
|
-
|
|
69
|
-
/** A top-level item's optional glyph (`NavMenu.iconName`), before its label. */
|
|
70
|
-
const menuIcon = (menu: NavMenu) =>
|
|
71
|
-
menu.iconName ? <EntityIcon icon={{ name: menu.iconName }} size={20} className="size-5 shrink-0" /> : null;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The site header of every platform, drawn from one `SiteNav`: logo, the
|
|
75
|
-
* menus left-aligned after it, then the Mingo AI launcher, Sign in, the host's
|
|
76
|
-
* actions, the primary CTA (the only button) and the trailing cells. A
|
|
77
|
-
* platform differs only in the data and the slots it passes.
|
|
78
|
-
*
|
|
79
|
-
* Below `lg` the menus and Sign in leave the bar (they live in
|
|
80
|
-
* `MobileNavSheet`); the host's actions, the CTA and the launcher stay.
|
|
81
|
-
*/
|
|
82
|
-
export function SiteHeader({
|
|
83
|
-
nav,
|
|
84
|
-
pathname,
|
|
85
|
-
logo,
|
|
86
|
-
logoHref,
|
|
87
|
-
renderLink = defaultRenderSiteNavLink,
|
|
88
|
-
leading,
|
|
89
|
-
actions,
|
|
90
|
-
cta,
|
|
91
|
-
sideActions,
|
|
92
|
-
mingo,
|
|
93
|
-
mobile,
|
|
94
|
-
badges,
|
|
95
|
-
social,
|
|
96
|
-
autoHide = false,
|
|
97
|
-
backgroundClassName,
|
|
98
|
-
className,
|
|
99
|
-
style,
|
|
100
|
-
skipToContentId = 'main-content',
|
|
101
|
-
}: SiteHeaderProps) {
|
|
102
|
-
const [show, setShow] = useState(true);
|
|
103
|
-
// Not state: nothing renders the previous offset, and as `useState` it
|
|
104
|
-
// re-rendered the whole header on every scroll event just to store a number.
|
|
105
|
-
const lastScrollY = useRef(0);
|
|
106
|
-
|
|
107
|
-
const activeId = useActiveSection(nav.menus, pathname);
|
|
108
|
-
const { openId, close, getTriggerProps, getPanelProps } = useNavMenus({ pathname });
|
|
109
|
-
|
|
110
|
-
// The menu that was open before this one, kept during render (no effect, no
|
|
111
|
-
// late frame): it tells an opening mega menu which side its content comes from.
|
|
112
|
-
const [trail, setTrail] = useState<{ id: string | null; from: string | null }>({ id: null, from: null });
|
|
113
|
-
if (trail.id !== openId) setTrail({ id: openId, from: trail.id });
|
|
114
|
-
const previousId = trail.id === openId ? trail.from : trail.id;
|
|
115
|
-
const megaIds = nav.menus.filter(isMegaMenu).map(menu => menu.id);
|
|
116
|
-
const openMegaIndex = openId === null ? -1 : megaIds.indexOf(openId);
|
|
117
|
-
const previousMegaIndex = previousId === null ? -1 : megaIds.indexOf(previousId);
|
|
118
|
-
const megaMotion: NavPanelMotion =
|
|
119
|
-
openMegaIndex < 0 || previousMegaIndex < 0 ? 'open' : openMegaIndex > previousMegaIndex ? 'from-end' : 'from-start';
|
|
120
|
-
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
// Only add the scroll listener while auto-hide is on. Nothing to reset
|
|
123
|
-
// here: `show` is masked by `autoHide` where it is read (`headerShown`), so
|
|
124
|
-
// turning auto-hide off can never leave the header stuck off-screen.
|
|
125
|
-
if (!autoHide) return undefined;
|
|
126
|
-
|
|
127
|
-
const handleScroll = () => {
|
|
128
|
-
const currentScrollY = window.scrollY;
|
|
129
|
-
const prevScrollY = lastScrollY.current;
|
|
130
|
-
lastScrollY.current = currentScrollY;
|
|
131
|
-
|
|
132
|
-
if (currentScrollY > prevScrollY && currentScrollY > 50) setShow(false);
|
|
133
|
-
else if (currentScrollY < prevScrollY || currentScrollY <= 10) setShow(true);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
137
|
-
return () => window.removeEventListener('scroll', handleScroll);
|
|
138
|
-
}, [autoHide]);
|
|
139
|
-
|
|
140
|
-
// An open menu holds the header in place: hiding the bar would take the
|
|
141
|
-
// panel the visitor is reading with it.
|
|
142
|
-
const headerShown = !autoHide || show || openId !== null;
|
|
143
|
-
|
|
144
|
-
const renderMenu = (menu: NavMenu) => {
|
|
145
|
-
const current = menu.id === activeId;
|
|
146
|
-
const hasPanel = !!menu.columns?.length || !!menu.features?.length || !!menu.sideLinks?.length;
|
|
147
|
-
|
|
148
|
-
if (!hasPanel) {
|
|
149
|
-
if (!menu.href) return null;
|
|
150
|
-
return (
|
|
151
|
-
<div key={menu.id} className="flex">
|
|
152
|
-
<Button
|
|
153
|
-
asChild
|
|
154
|
-
variant="transparent"
|
|
155
|
-
size="wrap"
|
|
156
|
-
font="regular"
|
|
157
|
-
className={cn(TOP_ITEM_CLASS, NAV_FOCUS_CLASS, current && TOP_ITEM_CURRENT_CLASS)}
|
|
158
|
-
>
|
|
159
|
-
{renderLink({
|
|
160
|
-
link: { href: menu.href },
|
|
161
|
-
'aria-current': current ? 'page' : undefined,
|
|
162
|
-
children: (
|
|
163
|
-
<>
|
|
164
|
-
{menuIcon(menu)}
|
|
165
|
-
{menu.label}
|
|
166
|
-
</>
|
|
167
|
-
),
|
|
168
|
-
})}
|
|
169
|
-
</Button>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const open = openId === menu.id;
|
|
175
|
-
return (
|
|
176
|
-
// A dropdown is anchored to its trigger (`relative`); a mega menu to the
|
|
177
|
-
// bar itself, so its wrapper stays unpositioned.
|
|
178
|
-
<div key={menu.id} className={cn('flex', !isMegaMenu(menu) && 'relative')}>
|
|
179
|
-
<Button
|
|
180
|
-
type="button"
|
|
181
|
-
variant="transparent"
|
|
182
|
-
size="wrap"
|
|
183
|
-
font="regular"
|
|
184
|
-
{...getTriggerProps(menu.id)}
|
|
185
|
-
aria-current={current ? 'page' : undefined}
|
|
186
|
-
className={cn(
|
|
187
|
-
TOP_ITEM_CLASS,
|
|
188
|
-
NAV_FOCUS_CLASS,
|
|
189
|
-
open && 'bg-ods-bg-hover text-ods-text-primary',
|
|
190
|
-
current && TOP_ITEM_CURRENT_CLASS,
|
|
191
|
-
)}
|
|
192
|
-
>
|
|
193
|
-
{menuIcon(menu)}
|
|
194
|
-
{menu.label}
|
|
195
|
-
<Chevron02DownIcon
|
|
196
|
-
aria-hidden="true"
|
|
197
|
-
// `!`: the Button sizes every glyph inside it at 20px; the chevron is 16.
|
|
198
|
-
className={cn('!h-4 !w-4 transition-transform duration-150', open && 'rotate-180')}
|
|
199
|
-
/>
|
|
200
|
-
</Button>
|
|
201
|
-
<NavPanel
|
|
202
|
-
{...getPanelProps(menu.id)}
|
|
203
|
-
menu={menu}
|
|
204
|
-
open={open}
|
|
205
|
-
motion={open ? megaMotion : undefined}
|
|
206
|
-
handoff={openMegaIndex >= 0}
|
|
207
|
-
renderLink={renderLink}
|
|
208
|
-
badges={badges}
|
|
209
|
-
social={social}
|
|
210
|
-
onNavigate={close}
|
|
211
|
-
/>
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const primaryCta =
|
|
217
|
-
typeof nav.primaryCta === 'object' ? (
|
|
218
|
-
<Button variant="accent" href={nav.primaryCta.href}>
|
|
219
|
-
{nav.primaryCta.label}
|
|
220
|
-
</Button>
|
|
221
|
-
) : nav.primaryCta === 'none' ? null : (
|
|
222
|
-
(cta ?? null)
|
|
223
|
-
);
|
|
224
|
-
const hasRight = !!mingo || !!nav.signIn || !!actions || !!primaryCta;
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
<div
|
|
228
|
-
// Hook point for page-scoped header CSS (a full-screen deck hides the bar).
|
|
229
|
-
data-site-header=""
|
|
230
|
-
className="sticky top-0 z-[50] w-full transition-transform duration-300 ease-in-out"
|
|
231
|
-
style={{ transform: headerShown ? 'translateY(0)' : 'translateY(-100%)' }}
|
|
232
|
-
>
|
|
233
|
-
{/* First tab stop of the page; visible only while focused. */}
|
|
234
|
-
<Link
|
|
235
|
-
href={`#${skipToContentId}`}
|
|
236
|
-
className={cn(
|
|
237
|
-
'sr-only left-[var(--spacing-system-lf)] top-3 z-[70] rounded-md bg-ods-text-primary px-[var(--spacing-system-mf)] py-[var(--spacing-system-xsf)] text-ods-bg text-h6 focus:not-sr-only focus:absolute',
|
|
238
|
-
NAV_FOCUS_CLASS,
|
|
239
|
-
)}
|
|
240
|
-
>
|
|
241
|
-
Skip to content
|
|
242
|
-
</Link>
|
|
243
|
-
{/* Unified ODS top-navigation shell (Figma 2797-5978), cell model with
|
|
244
|
-
per-cell dividers. NOTE: no `backdrop-blur` anywhere in this bar.
|
|
245
|
-
Every platform ships an OPAQUE header background, so a
|
|
246
|
-
backdrop-filter would blur a backdrop that is then fully painted
|
|
247
|
-
over: zero visual effect, but the browser still re-rasterizes the
|
|
248
|
-
strip behind this sticky bar every scroll frame and content flickers
|
|
249
|
-
as it passes under the header. A translucent "glass" header needs
|
|
250
|
-
the blur and a translucent `backgroundClassName` together. */}
|
|
251
|
-
<TopNavigation
|
|
252
|
-
// `relative` anchors a mega menu to the bar: it opens full width
|
|
253
|
-
// right under it.
|
|
254
|
-
className={cn('relative', className)}
|
|
255
|
-
style={style}
|
|
256
|
-
backgroundClassName={backgroundClassName}
|
|
257
|
-
centerBreakpoint="lg"
|
|
258
|
-
size="big"
|
|
259
|
-
leading={
|
|
260
|
-
<>
|
|
261
|
-
{leading}
|
|
262
|
-
{/* Menu toggle: a leading cell, banded with the menus' breakpoint
|
|
263
|
-
(`lg`) so the desktop menus and the toggle never co-show. */}
|
|
264
|
-
{mobile && (
|
|
265
|
-
<HeaderButton
|
|
266
|
-
className="border-r border-ods-border lg:hidden"
|
|
267
|
-
onClick={mobile.onToggle}
|
|
268
|
-
isActive={mobile.isOpen}
|
|
269
|
-
aria-label={mobile.isOpen ? 'Close menu' : 'Open menu'}
|
|
270
|
-
aria-expanded={mobile.isOpen}
|
|
271
|
-
// Conditional: the sheet unmounts when closed, so an
|
|
272
|
-
// unconditional reference would dangle (axe aria-valid-attr-value).
|
|
273
|
-
aria-controls={mobile.isOpen ? (mobile.controlsId ?? MOBILE_NAV_SHEET_ID) : undefined}
|
|
274
|
-
icon={mobile.isOpen ? <XmarkIcon className="h-6 w-6" /> : <Menu01Icon className="h-6 w-6" />}
|
|
275
|
-
/>
|
|
276
|
-
)}
|
|
277
|
-
</>
|
|
278
|
-
}
|
|
279
|
-
logo={
|
|
280
|
-
logoHref
|
|
281
|
-
? renderLink({
|
|
282
|
-
link: { href: logoHref },
|
|
283
|
-
className: cn(
|
|
284
|
-
'flex min-w-0 items-center rounded-md transition-opacity duration-200 hover:opacity-80',
|
|
285
|
-
NAV_FOCUS_CLASS,
|
|
286
|
-
),
|
|
287
|
-
'aria-label': nav.brand.name,
|
|
288
|
-
children: logo,
|
|
289
|
-
})
|
|
290
|
-
: logo
|
|
291
|
-
}
|
|
292
|
-
// Big-bar rule (Figma 2936-6812): a fixed 24px left inset on the logo
|
|
293
|
-
// zone at every breakpoint, which also reads as the gap between a
|
|
294
|
-
// leading cell and the logo. `shrink` lets the logo, never the CTA,
|
|
295
|
-
// give way on the narrowest phones.
|
|
296
|
-
logoClassName="shrink pl-[var(--spacing-system-lf)] md:pl-[var(--spacing-system-lf)] lg:pl-[var(--spacing-system-lf)]"
|
|
297
|
-
center={
|
|
298
|
-
nav.menus.length > 0 ? (
|
|
299
|
-
<nav aria-label="Main" className="flex items-center gap-[var(--spacing-system-xxs)]">
|
|
300
|
-
{nav.menus.map(renderMenu)}
|
|
301
|
-
</nav>
|
|
302
|
-
) : undefined
|
|
303
|
-
}
|
|
304
|
-
centerClassName="justify-start pl-[var(--spacing-system-xlf)]"
|
|
305
|
-
cta={
|
|
306
|
-
hasRight ? (
|
|
307
|
-
<>
|
|
308
|
-
{mingo && (
|
|
309
|
-
<MingoAiButton
|
|
310
|
-
source={mingo.source}
|
|
311
|
-
icon={mingo.icon}
|
|
312
|
-
label={mingo.label}
|
|
313
|
-
shortcutHint={mingo.shortcutHint}
|
|
314
|
-
/>
|
|
315
|
-
)}
|
|
316
|
-
{nav.signIn && (
|
|
317
|
-
<div className="hidden lg:flex">
|
|
318
|
-
<Button
|
|
319
|
-
asChild
|
|
320
|
-
variant="transparent"
|
|
321
|
-
size="wrap"
|
|
322
|
-
font="regular"
|
|
323
|
-
className={cn(TOP_ITEM_CLASS, NAV_FOCUS_CLASS)}
|
|
324
|
-
>
|
|
325
|
-
{renderLink({ link: nav.signIn, children: nav.signIn.label })}
|
|
326
|
-
</Button>
|
|
327
|
-
</div>
|
|
328
|
-
)}
|
|
329
|
-
{actions}
|
|
330
|
-
{primaryCta}
|
|
331
|
-
</>
|
|
332
|
-
) : undefined
|
|
333
|
-
}
|
|
334
|
-
ctaClassName="gap-[var(--spacing-system-xsf)]"
|
|
335
|
-
sideActions={sideActions}
|
|
336
|
-
/>
|
|
337
|
-
</div>
|
|
338
|
-
);
|
|
339
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
|
-
import Link from '../../embed-shims/next-link';
|
|
3
|
-
import type { NavLink } from '../../types/navigation';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* How a host renders one navigation link (Next `Link`, a unified-navigation
|
|
7
|
-
* anchor). Every site navigation component takes one; omitted, links go
|
|
8
|
-
* through {@link defaultRenderSiteNavLink}.
|
|
9
|
-
*/
|
|
10
|
-
export type SiteNavLinkRenderer = (props: {
|
|
11
|
-
link: Pick<NavLink, 'href' | 'external'>;
|
|
12
|
-
className?: string;
|
|
13
|
-
children: React.ReactNode;
|
|
14
|
-
onClick?: (e: React.MouseEvent) => void;
|
|
15
|
-
'aria-current'?: 'page';
|
|
16
|
-
'aria-label'?: string;
|
|
17
|
-
}) => React.ReactNode;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The default link: the library's `Link` shim (a plain `<a>`, or the host's
|
|
21
|
-
* registered router link, so internal links soft-navigate), opened in a new
|
|
22
|
-
* tab for a link that leaves the site.
|
|
23
|
-
*/
|
|
24
|
-
export const defaultRenderSiteNavLink: SiteNavLinkRenderer = ({ link, children, ...rest }) =>
|
|
25
|
-
link.external ? (
|
|
26
|
-
<Link href={link.href} target="_blank" rel="noopener noreferrer" {...rest}>
|
|
27
|
-
{children}
|
|
28
|
-
</Link>
|
|
29
|
-
) : (
|
|
30
|
-
<Link href={link.href} {...rest}>
|
|
31
|
-
{children}
|
|
32
|
-
</Link>
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
/** A link's label, with its `{count}` token filled from `link.count`. */
|
|
36
|
-
export function navLinkLabel(link: Pick<NavLink, 'label' | 'count'>): string {
|
|
37
|
-
return link.count === undefined ? link.label : link.label.replace('{count}', String(link.count));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Focus ring shared by every navigation link and trigger. */
|
|
41
|
-
export const NAV_FOCUS_CLASS = 'outline-none focus-visible:ring-2 focus-visible:ring-ods-accent';
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useMemo } from 'react';
|
|
4
|
-
import type { NavMenu } from '../../types/navigation';
|
|
5
|
-
|
|
6
|
-
/** The path of an internal href, without its query or `#anchor`. `null` for an
|
|
7
|
-
* absolute URL: another site is never the current section. */
|
|
8
|
-
function pathOf(href: string | undefined): string | null {
|
|
9
|
-
if (!href || !href.startsWith('/') || href.startsWith('//')) return null;
|
|
10
|
-
const path = href.split(/[?#]/)[0];
|
|
11
|
-
return path.length > 1 ? path.replace(/\/+$/, '') : '/';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Boundary-aware prefix test: `/pricing` holds `/pricing` and `/pricing/x`,
|
|
15
|
-
* never `/pricing-old`; `/` holds only `/`. */
|
|
16
|
-
function prefixHolds(prefix: string, pathname: string): boolean {
|
|
17
|
-
if (prefix === '/') return pathname === '/';
|
|
18
|
-
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Pure rule behind {@link useActiveSection}: the id of the menu whose `match`
|
|
23
|
-
* prefixes (else its `href`) hold the pathname. The longest prefix wins, the
|
|
24
|
-
* first menu on a tie; `null` when none holds it.
|
|
25
|
-
*/
|
|
26
|
-
export function resolveActiveSection(menus: readonly NavMenu[], pathname: string | null | undefined): string | null {
|
|
27
|
-
const current = pathOf(pathname ?? undefined);
|
|
28
|
-
if (!current) return null;
|
|
29
|
-
let best: { id: string; length: number } | null = null;
|
|
30
|
-
for (const menu of menus) {
|
|
31
|
-
const prefixes = menu.match?.length ? menu.match : [menu.href];
|
|
32
|
-
for (const raw of prefixes) {
|
|
33
|
-
const prefix = pathOf(raw);
|
|
34
|
-
if (!prefix || !prefixHolds(prefix, current)) continue;
|
|
35
|
-
if (!best || prefix.length > best.length) best = { id: menu.id, length: prefix.length };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return best?.id ?? null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** The id of the top-level menu the current page belongs to, or `null`. */
|
|
42
|
-
export function useActiveSection(menus: readonly NavMenu[], pathname: string | null | undefined): string | null {
|
|
43
|
-
return useMemo(() => resolveActiveSection(menus, pathname), [menus, pathname]);
|
|
44
|
-
}
|