@flamingo-stack/openframe-frontend-core 0.0.705 → 0.0.706-snapshot.20261006190707
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-4KX6OED2.cjs → chunk-2G7UYK35.cjs} +46 -46
- package/dist/{chunk-KOWVJD4V.cjs → chunk-2KXTOP6G.cjs} +70 -57
- package/dist/{chunk-A75QY6CD.cjs → chunk-2RNB5DTN.cjs} +21 -21
- package/dist/{chunk-PN2RKPDF.js → chunk-36BCTE7D.js} +10 -10
- package/dist/{chunk-ZSAGLQOD.cjs → chunk-3AHTOSXJ.cjs} +17 -17
- package/dist/{chunk-VR2ZLFCP.js → chunk-4IBPRIQH.js} +1 -1
- package/dist/{chunk-FCGC42TS.js → chunk-4L6XCMOU.js} +66 -62
- package/dist/{chunk-X5MQ7JRP.js → chunk-4UM2ETLM.js} +9 -9
- package/dist/{chunk-6NJKKO73.cjs → chunk-6I4ZAFO2.cjs} +15 -15
- package/dist/{chunk-RBDP4WDE.js → chunk-6IW7TWPV.js} +6 -6
- package/dist/{chunk-MMOODW57.cjs → chunk-6NTPY5QA.cjs} +1887 -94
- package/dist/{chunk-QFXRPRFT.js → chunk-6YKAT7NV.js} +3 -3
- package/dist/{chunk-HPWWNJE7.cjs → chunk-AOWDNSTM.cjs} +89 -89
- package/dist/{chunk-IHERL5V4.cjs → chunk-B73373AG.cjs} +34 -34
- package/dist/{chunk-DENB2XJC.js → chunk-CNWOMQQJ.js} +2 -2
- package/dist/{chunk-MP2C3LJ4.js → chunk-CUME3VTQ.js} +1 -1
- package/dist/{chunk-PDWVPI3S.js → chunk-CZ7Y7UG4.js} +4 -72
- package/dist/{chunk-YHM5AWTT.js → chunk-CZSZSDD4.js} +6 -6
- package/dist/{chunk-F6I6XZMN.js → chunk-DA5UDTLG.js} +5 -5
- package/dist/{chunk-MFFCUVWG.js → chunk-F57ECE2I.js} +1 -1
- package/dist/{chunk-CH5232HV.js → chunk-FI6GMDO7.js} +2455 -662
- package/dist/{chunk-NDESEPW5.js → chunk-FXJ3WWUB.js} +68 -0
- package/dist/{chunk-G5A3PSPV.js → chunk-HDOYSEOJ.js} +1080 -691
- package/dist/{chunk-VA5T32XA.js → chunk-I3ST4PLM.js} +2 -2
- package/dist/{chunk-5HBHO5TB.js → chunk-JPG7OHEG.js} +1 -1
- package/dist/{chunk-YFBM2K6X.cjs → chunk-JRU2CXLB.cjs} +4 -4
- package/dist/{chunk-MKXO4Q5P.js → chunk-KUC673HM.js} +383 -142
- package/dist/{chunk-JEZKGCZI.cjs → chunk-L6Y6OOKG.cjs} +26 -26
- package/dist/{chunk-F6AFPNDF.js → chunk-LFFB6EMB.js} +29 -16
- package/dist/{chunk-X72J4ACL.cjs → chunk-LLSUVPQC.cjs} +252 -8
- package/dist/{chunk-RKV5B7EU.cjs → chunk-LM7TXMLU.cjs} +72 -4
- package/dist/{chunk-6SEANMOU.js → chunk-LNW45N2C.js} +11 -11
- package/dist/{chunk-OUY3HHW6.cjs → chunk-LRQTJOF2.cjs} +928 -539
- package/dist/{chunk-DW7G4KG3.js → chunk-MR3A4MZV.js} +3 -3
- package/dist/{chunk-2YXVWZVF.js → chunk-N45D4ZXQ.js} +959 -715
- package/dist/{chunk-Z2VNQQ74.js → chunk-NCPTID44.js} +978 -942
- package/dist/{chunk-WA6GUYQ4.cjs → chunk-NSGXARGJ.cjs} +104 -104
- package/dist/{chunk-ABU3TGXA.cjs → chunk-OF6GEI5A.cjs} +3 -3
- package/dist/{chunk-IKTQ3ZAG.cjs → chunk-OJLR5MEO.cjs} +6 -6
- package/dist/{chunk-VESKITRQ.cjs → chunk-P42V6N34.cjs} +736 -732
- package/dist/{chunk-OHFS7VOU.cjs → chunk-PLYEKV3N.cjs} +2 -2
- package/dist/{chunk-7VVFKJY5.js → chunk-PO33GZVY.js} +1799 -3449
- package/dist/{chunk-7LS22BZU.cjs → chunk-QAOFMK2A.cjs} +14 -14
- package/dist/{chunk-VHCACDKP.cjs → chunk-RQRX4YC3.cjs} +39 -39
- package/dist/{chunk-3MX5S4CF.cjs → chunk-SEXWHIMS.cjs} +899 -2549
- package/dist/{chunk-AQWZNP6A.js → chunk-SS5MBHDP.js} +4 -4
- package/dist/{chunk-ZSAO2HBW.js → chunk-TDYIREDJ.js} +15 -15
- package/dist/{chunk-XOYH2YQE.cjs → chunk-UDFRUKLC.cjs} +2 -2
- package/dist/{chunk-TYWLNCNH.cjs → chunk-UVTD52MD.cjs} +14 -82
- package/dist/{chunk-N3COXDBH.cjs → chunk-VX4E6YSB.cjs} +41 -5
- package/dist/{chunk-2QEUFXNS.cjs → chunk-WCVZCGK5.cjs} +312 -71
- package/dist/{chunk-6XIF2Z7I.js → chunk-WMX26HB5.js} +3 -3
- package/dist/{chunk-D7BTSVMS.cjs → chunk-Y2GSOUCS.cjs} +2 -2
- package/dist/{chunk-4I45Y3DN.cjs → chunk-YBUT4AMG.cjs} +6 -6
- package/dist/components/case-studies/index.cjs +30 -32
- package/dist/components/case-studies/index.js +14 -16
- package/dist/components/chat/entity-cards/case-study-card.d.ts +11 -4
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +11 -11
- package/dist/components/chat/index.d.ts +1 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +16 -16
- package/dist/components/chat/nav-link-anchor-via-runtime.d.ts +34 -5
- package/dist/components/chat/nav-link-anchor-via-runtime.d.ts.map +1 -1
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +12 -14
- package/dist/components/contact/index.js +11 -13
- package/dist/components/docs/index.cjs +15 -17
- package/dist/components/docs/index.js +14 -16
- package/dist/components/embeds/index.cjs +12 -14
- package/dist/components/embeds/index.js +11 -13
- package/dist/components/faq/index.cjs +13 -15
- package/dist/components/faq/index.js +12 -14
- package/dist/components/features/index.cjs +12 -14
- package/dist/components/features/index.js +21 -23
- package/dist/components/help-center-pages/index.cjs +118 -120
- package/dist/components/help-center-pages/index.js +33 -35
- package/dist/components/icons/index.cjs +3 -5
- package/dist/components/icons/index.js +9 -11
- package/dist/components/icons-v2-generated/index.cjs +3 -5
- package/dist/components/icons-v2-generated/index.js +2 -4
- package/dist/components/index.cjs +87 -158
- package/dist/components/index.d.ts +0 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +331 -402
- package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
- package/dist/components/meeting-scheduler/index.cjs +62 -64
- package/dist/components/meeting-scheduler/index.js +12 -14
- package/dist/components/navigation/index.cjs +28 -16
- package/dist/components/navigation/index.d.ts +14 -10
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +38 -26
- package/dist/components/navigation/mingo-ai-button.d.ts +38 -3
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/navigation/mobile-nav-sheet-id.d.ts +3 -0
- package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +1 -0
- package/dist/components/navigation/mobile-nav-sheet.d.ts +48 -0
- package/dist/components/navigation/mobile-nav-sheet.d.ts.map +1 -0
- package/dist/components/navigation/nav-item-row.d.ts +27 -0
- package/dist/components/navigation/nav-item-row.d.ts.map +1 -0
- package/dist/components/navigation/nav-panel.d.ts +43 -0
- package/dist/components/navigation/nav-panel.d.ts.map +1 -0
- package/dist/components/navigation/navigation-sidebar-item.d.ts +1 -2
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/site-footer.d.ts +28 -0
- package/dist/components/navigation/site-footer.d.ts.map +1 -0
- package/dist/components/navigation/site-header.d.ts +61 -0
- package/dist/components/navigation/site-header.d.ts.map +1 -0
- package/dist/components/navigation/site-nav-link.d.ts +26 -0
- package/dist/components/navigation/site-nav-link.d.ts.map +1 -0
- 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 +15 -17
- package/dist/components/onboarding-guides/index.js +14 -16
- package/dist/components/platform/ToolBadge.d.ts +6 -0
- package/dist/components/platform/ToolBadge.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +10 -8
- package/dist/components/related-content/index.js +9 -7
- package/dist/components/tickets/index.cjs +16 -18
- package/dist/components/tickets/index.js +15 -17
- package/dist/components/ui/brand-association-card.d.ts +7 -0
- package/dist/components/ui/brand-association-card.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +14 -14
- package/dist/components/ui/index.js +19 -19
- package/dist/hooks/index.cjs +19 -11
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +19 -11
- package/dist/hooks/ui/index.d.ts +3 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-active-section.d.ts +10 -0
- package/dist/hooks/ui/use-active-section.d.ts.map +1 -0
- package/dist/hooks/ui/use-nav-menus.d.ts +48 -0
- package/dist/hooks/ui/use-nav-menus.d.ts.map +1 -0
- package/dist/hooks/ui/use-visitor-os.d.ts +15 -0
- package/dist/hooks/ui/use-visitor-os.d.ts.map +1 -0
- package/dist/hooks/use-location-hash.d.ts +9 -0
- package/dist/hooks/use-location-hash.d.ts.map +1 -0
- package/dist/index.cjs +56 -21
- package/dist/index.js +231 -196
- package/dist/types/case-study.d.ts +9 -0
- package/dist/types/case-study.d.ts.map +1 -1
- package/dist/types/navigation.d.ts +95 -65
- package/dist/types/navigation.d.ts.map +1 -1
- package/dist/utils/index.cjs +55 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +50 -1
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/dist/utils/visitor-os.d.ts +45 -0
- package/dist/utils/visitor-os.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/chat/entity-cards/case-study-card.tsx +115 -5
- package/src/components/chat/index.ts +7 -1
- package/src/components/chat/nav-link-anchor-via-runtime.tsx +62 -9
- package/src/components/chat/utils/icon-library.tsx +35 -1
- package/src/components/index.ts +0 -2
- package/src/components/loading/page-layout-skeleton.tsx +1 -1
- package/src/components/navigation/index.ts +21 -14
- package/src/components/navigation/mingo-ai-button.tsx +139 -63
- package/src/components/navigation/mobile-nav-sheet-id.ts +2 -0
- package/src/components/navigation/mobile-nav-sheet.tsx +241 -0
- package/src/components/navigation/nav-item-row.tsx +129 -0
- package/src/components/navigation/nav-panel.tsx +196 -0
- package/src/components/navigation/navigation-sidebar-item.tsx +1 -2
- package/src/components/navigation/site-footer.tsx +256 -0
- package/src/components/navigation/site-header.tsx +339 -0
- package/src/components/navigation/site-nav-link.tsx +41 -0
- package/src/components/navigation/ticket-alerts-button.tsx +2 -2
- package/src/components/navigation/top-navigation.tsx +2 -2
- package/src/components/platform/ToolBadge.tsx +18 -1
- package/src/components/ui/brand-association-card.tsx +25 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/ui/index.ts +3 -0
- package/src/hooks/ui/use-active-section.ts +44 -0
- package/src/hooks/ui/use-nav-menus.ts +200 -0
- package/src/hooks/ui/use-visitor-os.ts +33 -0
- package/src/hooks/use-location-hash.ts +34 -0
- package/src/styles/chat-animations.css +57 -2
- package/src/types/case-study.ts +16 -0
- package/src/types/navigation.ts +80 -66
- package/src/utils/index.ts +1 -0
- package/src/utils/source-icons.ts +13 -0
- package/src/utils/visitor-os.ts +84 -0
- package/dist/chunk-LMBC5FFP.js +0 -43
- package/dist/chunk-O43V3EOS.js +0 -252
- package/dist/chunk-QZVSJF2K.cjs +0 -252
- package/dist/chunk-RALRPFDW.cjs +0 -43
- package/dist/components/footer-waitlist-button.d.ts +0 -29
- package/dist/components/footer-waitlist-button.d.ts.map +0 -1
- package/dist/components/footer.d.ts +0 -47
- package/dist/components/footer.d.ts.map +0 -1
- package/dist/components/navigation/client-only-header.d.ts +0 -8
- package/dist/components/navigation/client-only-header.d.ts.map +0 -1
- package/dist/components/navigation/header-skeleton.d.ts +0 -12
- package/dist/components/navigation/header-skeleton.d.ts.map +0 -1
- package/dist/components/navigation/header.d.ts +0 -9
- package/dist/components/navigation/header.d.ts.map +0 -1
- package/dist/components/navigation/mobile-nav-panel.d.ts +0 -10
- package/dist/components/navigation/mobile-nav-panel.d.ts.map +0 -1
- package/src/components/footer-waitlist-button.tsx +0 -53
- package/src/components/footer.tsx +0 -195
- package/src/components/navigation/client-only-header.tsx +0 -22
- package/src/components/navigation/header-skeleton.tsx +0 -74
- package/src/components/navigation/header.tsx +0 -429
- package/src/components/navigation/mobile-nav-panel.tsx +0 -155
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type React from 'react';
|
|
4
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
5
|
+
import type { NavGroup, NavMenu } from '../../types/navigation';
|
|
6
|
+
import { cn } from '../../utils/cn';
|
|
7
|
+
import { CaseStudyCard } from '../chat/entity-cards/case-study-card';
|
|
8
|
+
import { NavItemRow } from './nav-item-row';
|
|
9
|
+
import { defaultRenderSiteNavLink, type SiteNavLinkRenderer } from './site-nav-link';
|
|
10
|
+
|
|
11
|
+
/** A column with more links than this lays them out in two columns. */
|
|
12
|
+
const TWO_COLUMN_OVER = 4;
|
|
13
|
+
|
|
14
|
+
/** True when a menu opens the full-width mega panel instead of a dropdown list. */
|
|
15
|
+
export function isMegaMenu(menu: Pick<NavMenu, 'columns' | 'features' | 'sideLinks'>): boolean {
|
|
16
|
+
return (menu.columns?.length ?? 0) > 1 || !!menu.features?.length || !!menu.sideLinks?.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NavPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
20
|
+
menu: NavMenu;
|
|
21
|
+
open: boolean;
|
|
22
|
+
renderLink?: SiteNavLinkRenderer;
|
|
23
|
+
/** Values for `NavLink.badgeKey`. */
|
|
24
|
+
badges?: Record<string, number>;
|
|
25
|
+
/** Rendered under the links of a menu with `showSocial`. */
|
|
26
|
+
social?: React.ReactNode;
|
|
27
|
+
/** Called when a link is activated, so the owner can close the menu. */
|
|
28
|
+
onNavigate?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* How an OPEN mega menu arrived: `open` from a closed bar (the panel fades
|
|
31
|
+
* in), `from-start` / `from-end` from the mega menu before or after it in
|
|
32
|
+
* the row (the surface stays put and only the content slides in from that
|
|
33
|
+
* side). Default `open`.
|
|
34
|
+
*/
|
|
35
|
+
motion?: NavPanelMotion;
|
|
36
|
+
/**
|
|
37
|
+
* Another mega menu is open: this closed one leaves without a fade, so the
|
|
38
|
+
* two panels never show through each other mid-switch.
|
|
39
|
+
*/
|
|
40
|
+
handoff?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type NavPanelMotion = 'open' | 'from-start' | 'from-end';
|
|
44
|
+
|
|
45
|
+
const HEADING_CLASS = 'mb-[var(--spacing-system-xsf)] ml-[var(--spacing-system-sf)] text-ods-text-muted text-h5';
|
|
46
|
+
|
|
47
|
+
function Column({
|
|
48
|
+
group,
|
|
49
|
+
wide,
|
|
50
|
+
rowProps,
|
|
51
|
+
}: {
|
|
52
|
+
group: NavGroup;
|
|
53
|
+
wide: boolean;
|
|
54
|
+
rowProps: Pick<NavPanelProps, 'renderLink' | 'badges' | 'onNavigate'>;
|
|
55
|
+
}) {
|
|
56
|
+
const twoColumns = wide && group.links.length > TWO_COLUMN_OVER;
|
|
57
|
+
return (
|
|
58
|
+
<div className={cn('flex min-w-0 flex-col', wide && (twoColumns ? 'flex-[2]' : 'flex-1'))}>
|
|
59
|
+
{group.title && <div className={HEADING_CLASS}>{group.title}</div>}
|
|
60
|
+
<div
|
|
61
|
+
className={
|
|
62
|
+
twoColumns
|
|
63
|
+
? 'grid grid-cols-2 gap-x-[var(--spacing-system-mf)] gap-y-[var(--spacing-system-xxs)]'
|
|
64
|
+
: 'flex flex-col gap-[var(--spacing-system-xxs)]'
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{group.links.map(link => (
|
|
68
|
+
<NavItemRow
|
|
69
|
+
key={link.id}
|
|
70
|
+
link={link}
|
|
71
|
+
renderLink={rowProps.renderLink}
|
|
72
|
+
onNavigate={rowProps.onNavigate}
|
|
73
|
+
badge={link.badgeKey ? rowProps.badges?.[link.badgeKey] : undefined}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The panel a header menu opens, in the one shape its data asks for: a single
|
|
83
|
+
* column is a compact dropdown under its trigger; more than one column, or
|
|
84
|
+
* features, or side links make the full-width mega menu under the bar.
|
|
85
|
+
*
|
|
86
|
+
* Always in the DOM, so crawlers see every link; closed, it is `inert` (out of
|
|
87
|
+
* the tab order and the accessibility tree, which `aria-hidden` alone would
|
|
88
|
+
* not do) and invisible. Plain `<div>`s on purpose: these are disclosures of
|
|
89
|
+
* links, not an ARIA menu, so there is no `role="menu"` and Tab moves through
|
|
90
|
+
* the links in DOM order.
|
|
91
|
+
*/
|
|
92
|
+
export const NavPanel = forwardRef<HTMLDivElement, NavPanelProps>(function NavPanelImpl(
|
|
93
|
+
{
|
|
94
|
+
menu,
|
|
95
|
+
open,
|
|
96
|
+
renderLink = defaultRenderSiteNavLink,
|
|
97
|
+
badges,
|
|
98
|
+
social,
|
|
99
|
+
onNavigate,
|
|
100
|
+
motion = 'open',
|
|
101
|
+
handoff = false,
|
|
102
|
+
className,
|
|
103
|
+
...panelProps
|
|
104
|
+
}: NavPanelProps,
|
|
105
|
+
ref,
|
|
106
|
+
) {
|
|
107
|
+
const columns = menu.columns ?? [];
|
|
108
|
+
const mega = isMegaMenu(menu);
|
|
109
|
+
const rowProps = { renderLink, badges, onNavigate };
|
|
110
|
+
const socialRow = menu.showSocial && social ? social : null;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
inert={!open}
|
|
115
|
+
data-state={open ? 'open' : 'closed'}
|
|
116
|
+
{...panelProps}
|
|
117
|
+
ref={ref}
|
|
118
|
+
className={cn(
|
|
119
|
+
'absolute top-full z-[60]',
|
|
120
|
+
// Between two mega menus the surface is handed over in one frame (same
|
|
121
|
+
// place, same size, same colour) and only the content moves; a fade
|
|
122
|
+
// here would let the page show through both for its whole length.
|
|
123
|
+
mega && (open ? motion !== 'open' : handoff) ? 'transition-none' : 'transition-opacity duration-150',
|
|
124
|
+
mega
|
|
125
|
+
? 'left-0 right-0 border-b border-ods-border bg-ods-card shadow-xl'
|
|
126
|
+
: 'left-0 mt-[var(--spacing-system-xxs)] min-w-[260px] rounded-lg border border-ods-border bg-ods-card p-[var(--spacing-system-xsf)] shadow-xl',
|
|
127
|
+
open ? 'visible opacity-100' : 'pointer-events-none invisible opacity-0',
|
|
128
|
+
className,
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{mega ? (
|
|
132
|
+
// ONE height for every mega menu, so moving between menus (and a
|
|
133
|
+
// card's cover loading) never resizes the panel under the pointer.
|
|
134
|
+
<div
|
|
135
|
+
data-motion={open ? motion : undefined}
|
|
136
|
+
className="nav-panel-content flex h-[400px] max-h-[calc(100svh-var(--top-nav-height,72px))] flex-col gap-[var(--spacing-system-mf)] overflow-y-auto px-[var(--spacing-system-lf)] pb-[var(--spacing-system-xlf)] pt-8"
|
|
137
|
+
>
|
|
138
|
+
<div className="flex min-h-0 flex-1 gap-[var(--spacing-system-xlf)]">
|
|
139
|
+
{columns.map(group => (
|
|
140
|
+
<Column key={group.id} group={group} wide rowProps={rowProps} />
|
|
141
|
+
))}
|
|
142
|
+
{!!menu.features?.length && (
|
|
143
|
+
// One row of cards, as many as the bar's width holds: a card that
|
|
144
|
+
// would wrap lands on a zero-height row, where its cell clips it.
|
|
145
|
+
<div className="grid min-w-0 flex-[3] auto-rows-[0px] grid-cols-[repeat(auto-fit,minmax(240px,1fr))] grid-rows-[minmax(0,1fr)] gap-x-[var(--spacing-system-mf)]">
|
|
146
|
+
{menu.features.map(feature => (
|
|
147
|
+
<div key={feature.id} className="min-h-0 min-w-0 overflow-hidden">
|
|
148
|
+
<CaseStudyCard
|
|
149
|
+
size="menu"
|
|
150
|
+
study={feature.study}
|
|
151
|
+
href={feature.href}
|
|
152
|
+
mediaMounted={open}
|
|
153
|
+
onNavigate={onNavigate}
|
|
154
|
+
className="h-full"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
{!!menu.sideLinks?.length && (
|
|
161
|
+
<div className="flex w-[260px] shrink-0 flex-col gap-0.5 border-l border-ods-border pl-8 pt-5">
|
|
162
|
+
{menu.sideTitle && <div className={HEADING_CLASS}>{menu.sideTitle}</div>}
|
|
163
|
+
{menu.sideLinks.map(link => (
|
|
164
|
+
<NavItemRow
|
|
165
|
+
key={link.id}
|
|
166
|
+
link={link}
|
|
167
|
+
variant="side"
|
|
168
|
+
{...rowProps}
|
|
169
|
+
badge={link.badgeKey ? badges?.[link.badgeKey] : undefined}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
{socialRow && (
|
|
176
|
+
<div className="border-t border-ods-border px-[var(--spacing-system-sf)] pt-[var(--spacing-system-mf)]">
|
|
177
|
+
{socialRow}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<>
|
|
183
|
+
{columns.map(group => (
|
|
184
|
+
<Column key={group.id} group={group} wide={false} rowProps={rowProps} />
|
|
185
|
+
))}
|
|
186
|
+
{socialRow && (
|
|
187
|
+
<div className="mt-[var(--spacing-system-xsf)] border-t border-ods-border px-[var(--spacing-system-sf)] pt-[var(--spacing-system-sf)]">
|
|
188
|
+
{socialRow}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
NavPanel.displayName = 'NavPanel';
|
|
@@ -155,8 +155,7 @@ export const NavigationSidebarItemButton = memo(function NavigationSidebarItemBu
|
|
|
155
155
|
*
|
|
156
156
|
* Deliberately in this file, beside `NavigationSidebarItemButton`: the two must
|
|
157
157
|
* agree on `h-14`, the `p-[var(--spacing-system-m)]` inset, the 24px icon box and
|
|
158
|
-
* the label column, or the nav resizes on the handoff.
|
|
159
|
-
* `header-skeleton.tsx` sits beside `header.tsx`.
|
|
158
|
+
* the label column, or the nav resizes on the handoff.
|
|
160
159
|
*
|
|
161
160
|
* `showLabel` is the sidebar's own minimized state, so the caller passes what it
|
|
162
161
|
* already passes the real row — no separate width handling.
|
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
}
|