@flamingo-stack/openframe-frontend-core 0.0.704 → 0.0.705-snapshot.20261006160157
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-7LFIKJX2.js → chunk-3EZ4HHAP.js} +3 -3
- package/dist/{chunk-I7DFUQMN.js → chunk-3NCM4ZQB.js} +17 -18
- package/dist/{chunk-4DQMTBMK.cjs → chunk-3PLDQDE6.cjs} +15 -15
- package/dist/{chunk-VTASHWER.js → chunk-4CJMQLB5.js} +4 -4
- package/dist/{chunk-VR2ZLFCP.js → chunk-4IBPRIQH.js} +1 -1
- package/dist/{chunk-ZQFZSFQI.js → chunk-4ILM7W2R.js} +2 -2
- package/dist/{chunk-FXENTYE5.js → chunk-6232S532.js} +3 -3
- package/dist/{chunk-MMOODW57.cjs → chunk-6NTPY5QA.cjs} +1887 -94
- package/dist/{chunk-2JDVL7UV.js → chunk-7GEBFJT2.js} +1829 -3447
- package/dist/{chunk-LDPEQGKT.cjs → chunk-7OFPU7WE.cjs} +942 -2560
- package/dist/{chunk-L3IC42IF.cjs → chunk-7SCNMP2B.cjs} +57 -58
- package/dist/{chunk-2G546OND.js → chunk-BHI6GVC4.js} +1094 -693
- package/dist/{chunk-MP2C3LJ4.js → chunk-CUME3VTQ.js} +1 -1
- package/dist/{chunk-WUJNYGO2.js → chunk-CZ7Y7UG4.js} +4 -76
- package/dist/{chunk-7JIL3WOH.cjs → chunk-DDVBX42E.cjs} +21 -21
- package/dist/{chunk-6WQIUQUC.js → chunk-DWPAAWMR.js} +6 -6
- 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-4KGXLVCF.js → chunk-G6UMNAVI.js} +57 -61
- package/dist/{chunk-LTSTC3N5.cjs → chunk-GWQEYPYM.cjs} +728 -732
- package/dist/{chunk-N3TO7JV3.cjs → chunk-HZ7CBXPL.cjs} +89 -89
- package/dist/{chunk-XATTBO5X.js → chunk-IKJA3Y7Z.js} +2 -2
- package/dist/{chunk-5HBHO5TB.js → chunk-JPG7OHEG.js} +1 -1
- package/dist/{chunk-AMYY3PNL.cjs → chunk-K6O43WDL.cjs} +46 -46
- package/dist/{chunk-IHU5ANUY.js → chunk-KUC673HM.js} +383 -142
- package/dist/{chunk-X72J4ACL.cjs → chunk-LLSUVPQC.cjs} +252 -8
- package/dist/{chunk-RKV5B7EU.cjs → chunk-LM7TXMLU.cjs} +72 -4
- package/dist/{chunk-XJKZ4IKT.cjs → chunk-LVSFKENJ.cjs} +4 -4
- package/dist/{chunk-IZGDG5X6.cjs → chunk-M75XEEGT.cjs} +940 -539
- package/dist/{chunk-UK62YTH6.cjs → chunk-MB4PNY7C.cjs} +6 -6
- package/dist/{chunk-2YXVWZVF.js → chunk-N45D4ZXQ.js} +959 -715
- package/dist/{chunk-Z2VNQQ74.js → chunk-NCPTID44.js} +978 -942
- package/dist/{chunk-JEYRXT5L.js → chunk-O2RSAA5M.js} +11 -11
- package/dist/{chunk-ABU3TGXA.cjs → chunk-OF6GEI5A.cjs} +3 -3
- package/dist/{chunk-OHFS7VOU.cjs → chunk-PLYEKV3N.cjs} +2 -2
- package/dist/{chunk-5QJDM6UV.js → chunk-PXOQIA5K.js} +5 -5
- package/dist/{chunk-VHCACDKP.cjs → chunk-RQRX4YC3.cjs} +39 -39
- package/dist/{chunk-HFJW24JJ.cjs → chunk-RTIPR53L.cjs} +104 -104
- package/dist/{chunk-262YQFOS.cjs → chunk-SEKYEWQG.cjs} +26 -26
- package/dist/{chunk-SYXQLETR.cjs → chunk-STHZBOQI.cjs} +17 -17
- package/dist/{chunk-MI3JUJ75.cjs → chunk-SW5ZV2BN.cjs} +34 -34
- package/dist/{chunk-QVSFBSWV.js → chunk-T5TMCHWM.js} +9 -9
- package/dist/{chunk-ERCV6HHJ.js → chunk-TE773HEH.js} +10 -10
- package/dist/{chunk-C7XGRLT5.cjs → chunk-TQ4LB2DS.cjs} +14 -14
- package/dist/{chunk-XOYH2YQE.cjs → chunk-UDFRUKLC.cjs} +2 -2
- package/dist/{chunk-XIFOBQRQ.cjs → chunk-UJ33ZXDT.cjs} +6 -6
- package/dist/{chunk-C3BWIU7R.cjs → chunk-UVTD52MD.cjs} +14 -86
- package/dist/{chunk-N3COXDBH.cjs → chunk-VX4E6YSB.cjs} +41 -5
- package/dist/{chunk-HJAYVKWQ.js → chunk-VZICIQW2.js} +15 -15
- package/dist/{chunk-MGGGBHYD.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-NRLJUBCS.js → chunk-YGJKKDDM.js} +6 -6
- package/dist/components/case-studies/index.cjs +30 -32
- package/dist/components/case-studies/index.js +14 -16
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.d.ts +8 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- 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/mingo-chat-rail.d.ts +3 -1
- package/dist/components/chat/mingo-chat-rail.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 +89 -158
- package/dist/components/index.d.ts +0 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +333 -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/app-layout-side-panel.d.ts +18 -1
- package/dist/components/navigation/app-layout-side-panel.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +30 -16
- package/dist/components/navigation/index.d.ts +15 -10
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +40 -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/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/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 +58 -21
- package/dist/index.js +233 -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/chat-input.tsx +5 -1
- package/src/components/chat/chat-message-enhanced.tsx +4 -3
- package/src/components/chat/embeddable-chat.tsx +64 -6
- package/src/components/chat/entity-cards/case-study-card.tsx +115 -5
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/mingo-chat-rail.tsx +24 -7
- 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/app-layout-side-panel.tsx +30 -2
- package/src/components/navigation/index.ts +22 -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/SoftwareSourceBadge.tsx +1 -1
- 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/app-globals.css +13 -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
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import Link from '../../embed-shims/next-link';
|
|
5
|
-
import type { HeaderConfig, NavigationItem } from '../../types/navigation';
|
|
6
|
-
import { cn } from '../../utils';
|
|
7
|
-
import { Menu01Icon, XmarkIcon } from '../icons-v2-generated';
|
|
8
|
-
import { Button } from '../ui/button';
|
|
9
|
-
import { HeaderButton } from './header-button';
|
|
10
|
-
import { MingoAiButton } from './mingo-ai-button';
|
|
11
|
-
import { MOBILE_NAV_PANEL_ID } from './mobile-nav-panel';
|
|
12
|
-
import { TicketAlertsButton } from './ticket-alerts-button';
|
|
13
|
-
import { TopNavigation } from './top-navigation';
|
|
14
|
-
|
|
15
|
-
export interface HeaderProps {
|
|
16
|
-
config: HeaderConfig;
|
|
17
|
-
platform?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Re-export from types for convenience
|
|
21
|
-
export type { HeaderConfig } from '../../types/navigation';
|
|
22
|
-
|
|
23
|
-
// Top-level nav-link typography (Figma 2936-6815): the compact h6 step
|
|
24
|
-
// (DM Sans 500, 14/20) with 24px icons. Overrides the `font="regular"` h4
|
|
25
|
-
// label and the Button base's 20px svg cap via cn()'s tailwind-merge.
|
|
26
|
-
const NAV_ITEM_CLASSES = 'text-h6 [&_svg]:h-6 [&_svg]:w-6';
|
|
27
|
-
|
|
28
|
-
export function Header({ config }: HeaderProps) {
|
|
29
|
-
const [show, setShow] = useState(true);
|
|
30
|
-
// Not state: nothing renders the previous offset, and as `useState` it
|
|
31
|
-
// re-rendered the whole header on every scroll event just to store a number.
|
|
32
|
-
const lastScrollY = useRef(0);
|
|
33
|
-
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
|
|
34
|
-
const dropdownRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
35
|
-
const triggerRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
|
36
|
-
|
|
37
|
-
// Handle click outside and escape key for custom dropdown
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
40
|
-
const target = event.target as HTMLElement;
|
|
41
|
-
if (!target) return;
|
|
42
|
-
|
|
43
|
-
// Check if click is outside all dropdowns
|
|
44
|
-
const isOutsideAllDropdowns = Object.keys(openDropdowns).every(id => {
|
|
45
|
-
const dropdown = dropdownRefs.current[id];
|
|
46
|
-
const trigger = triggerRefs.current[id];
|
|
47
|
-
|
|
48
|
-
if (!dropdown || !trigger) return true;
|
|
49
|
-
|
|
50
|
-
return !dropdown.contains(target) && !trigger.contains(target);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (isOutsideAllDropdowns) {
|
|
54
|
-
setOpenDropdowns({});
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const handleEscapeKey = (event: KeyboardEvent) => {
|
|
59
|
-
if (event.key === 'Escape') {
|
|
60
|
-
setOpenDropdowns({});
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Only add listeners if any dropdown is open
|
|
65
|
-
const hasOpenDropdowns = Object.values(openDropdowns).some(Boolean);
|
|
66
|
-
if (hasOpenDropdowns) {
|
|
67
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
68
|
-
document.addEventListener('keydown', handleEscapeKey);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
document.removeEventListener('mousedown', handleClickOutside);
|
|
73
|
-
document.removeEventListener('keydown', handleEscapeKey);
|
|
74
|
-
};
|
|
75
|
-
}, [openDropdowns]);
|
|
76
|
-
|
|
77
|
-
// Force close all dropdowns and cleanup on unmount
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
return () => {
|
|
80
|
-
// Close all dropdowns before unmounting to prevent focus errors
|
|
81
|
-
setOpenDropdowns({});
|
|
82
|
-
// Clear any stored refs
|
|
83
|
-
dropdownRefs.current = {};
|
|
84
|
-
triggerRefs.current = {};
|
|
85
|
-
};
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
// Only add scroll listener if autoHide is enabled. Nothing to reset here:
|
|
90
|
-
// `show` is masked by `config.autoHide` where it is read (see `headerShown`
|
|
91
|
-
// below), so turning auto-hide off can never leave the header stuck
|
|
92
|
-
// off-screen, and it does not cost the extra render pass that writing
|
|
93
|
-
// `true` back into state from this effect did.
|
|
94
|
-
if (!config.autoHide) return undefined;
|
|
95
|
-
|
|
96
|
-
const handleScroll = () => {
|
|
97
|
-
const currentScrollY = window.scrollY;
|
|
98
|
-
|
|
99
|
-
const prevScrollY = lastScrollY.current;
|
|
100
|
-
lastScrollY.current = currentScrollY;
|
|
101
|
-
|
|
102
|
-
// Determine if we should show or hide the header
|
|
103
|
-
const shouldHide = currentScrollY > prevScrollY && currentScrollY > 50;
|
|
104
|
-
const shouldShow = currentScrollY < prevScrollY || currentScrollY <= 10;
|
|
105
|
-
|
|
106
|
-
if (shouldHide) {
|
|
107
|
-
setShow(false);
|
|
108
|
-
} else if (shouldShow) {
|
|
109
|
-
setShow(true);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
114
|
-
|
|
115
|
-
return () => {
|
|
116
|
-
window.removeEventListener('scroll', handleScroll);
|
|
117
|
-
};
|
|
118
|
-
}, [config.autoHide]);
|
|
119
|
-
|
|
120
|
-
const renderNavigationItem = (item: NavigationItem) => {
|
|
121
|
-
// If custom element provided, use it
|
|
122
|
-
if (item.element) {
|
|
123
|
-
return <React.Fragment key={item.id}>{item.element}</React.Fragment>;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// If it has children, render as custom dropdown
|
|
127
|
-
if (item.children && item.children.length > 0) {
|
|
128
|
-
const isOpen = openDropdowns[item.id] || false;
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div key={item.id} className="relative">
|
|
132
|
-
<Button
|
|
133
|
-
ref={el => {
|
|
134
|
-
triggerRefs.current[item.id] = el;
|
|
135
|
-
}}
|
|
136
|
-
variant="transparent"
|
|
137
|
-
leftIcon={item.icon}
|
|
138
|
-
rightIcon={item.badge}
|
|
139
|
-
onClick={() => {
|
|
140
|
-
// Single-open: opening a dropdown closes any other open one —
|
|
141
|
-
// only one nav dropdown may be expanded at a time.
|
|
142
|
-
setOpenDropdowns(prev => ({
|
|
143
|
-
[item.id]: !prev[item.id],
|
|
144
|
-
}));
|
|
145
|
-
}}
|
|
146
|
-
size="default"
|
|
147
|
-
font="regular"
|
|
148
|
-
className={cn(
|
|
149
|
-
// Top-level nav links (Figma 2936-6815): compact h6 label
|
|
150
|
-
// (DM Sans 500, 14/20) with 24px icons — overrides the
|
|
151
|
-
// font="regular" h4 step and the button's 20px svg cap.
|
|
152
|
-
NAV_ITEM_CLASSES,
|
|
153
|
-
item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background
|
|
154
|
-
isOpen && 'bg-ods-bg-hover', // Open dropdowns get gray background
|
|
155
|
-
item.className,
|
|
156
|
-
)}
|
|
157
|
-
>
|
|
158
|
-
{item.label}
|
|
159
|
-
</Button>
|
|
160
|
-
|
|
161
|
-
{/* Always render dropdown in DOM so crawlers see child <a> links;
|
|
162
|
-
toggle visibility via CSS + `inert`.
|
|
163
|
-
|
|
164
|
-
Why `inert` (not `aria-hidden`):
|
|
165
|
-
- `aria-hidden=true` HIDES from screen readers but DOES NOT
|
|
166
|
-
remove focusable descendants from the tab order. If a child
|
|
167
|
-
retains focus (e.g., the dropdown closes while a child was
|
|
168
|
-
hovered/focused), the browser correctly flags "Blocked
|
|
169
|
-
aria-hidden on an element because its descendant retained
|
|
170
|
-
focus" — a real WAI-ARIA violation.
|
|
171
|
-
- `inert` is the HTML-standard attribute that does BOTH:
|
|
172
|
-
removes from a11y tree + blocks focus + prevents click +
|
|
173
|
-
removes from tab order. Native React 19 support. */}
|
|
174
|
-
<div
|
|
175
|
-
ref={el => {
|
|
176
|
-
dropdownRefs.current[item.id] = el;
|
|
177
|
-
}}
|
|
178
|
-
inert={!isOpen}
|
|
179
|
-
className={cn(
|
|
180
|
-
'absolute left-0 top-full mt-1',
|
|
181
|
-
item.dropdownClassName ? '' : 'border border-ods-border bg-ods-card',
|
|
182
|
-
'z-[9999] rounded-lg shadow-xl',
|
|
183
|
-
item.id === 'community' ? 'min-w-[240px]' : 'min-w-[220px]',
|
|
184
|
-
'transition-opacity duration-150',
|
|
185
|
-
isOpen ? 'pointer-events-auto visible opacity-100' : 'pointer-events-none invisible opacity-0',
|
|
186
|
-
item.dropdownClassName || '',
|
|
187
|
-
)}
|
|
188
|
-
>
|
|
189
|
-
<div className="p-2">
|
|
190
|
-
{item.children.map((child, index) => (
|
|
191
|
-
<Button
|
|
192
|
-
key={child.id}
|
|
193
|
-
variant="transparent"
|
|
194
|
-
size="small-legacy"
|
|
195
|
-
href={child.href} // Use href for navigation
|
|
196
|
-
leftIcon={child.icon}
|
|
197
|
-
rightIcon={child.badge}
|
|
198
|
-
onClick={() => {
|
|
199
|
-
// Always close dropdown when any item is clicked
|
|
200
|
-
setOpenDropdowns(prev => ({ ...prev, [item.id]: false }));
|
|
201
|
-
// If there's a custom onClick, call it too
|
|
202
|
-
if (child.onClick) {
|
|
203
|
-
child.onClick();
|
|
204
|
-
}
|
|
205
|
-
}}
|
|
206
|
-
className={cn(
|
|
207
|
-
'flex w-full justify-start',
|
|
208
|
-
// Same caption step as the top-level nav links (designer:
|
|
209
|
-
// dropdowns follow the h6 caption size/weight too). The
|
|
210
|
-
// explicit font-medium beats small-legacy's font-bold via
|
|
211
|
-
// tailwind-merge instead of stylesheet order.
|
|
212
|
-
'font-medium text-h6',
|
|
213
|
-
index < (item.children?.length ?? 0) - 1 && 'mb-1',
|
|
214
|
-
'text-ods-text-primary', // All dropdown items use primary text color
|
|
215
|
-
child.isActive && 'bg-ods-bg-hover', // Active dropdown items get gray background
|
|
216
|
-
)}
|
|
217
|
-
{...(child.isExternal && { isExternal: true })}
|
|
218
|
-
>
|
|
219
|
-
{child.label}
|
|
220
|
-
</Button>
|
|
221
|
-
))}
|
|
222
|
-
</div>
|
|
223
|
-
{item.dropdownContent && (
|
|
224
|
-
<>
|
|
225
|
-
{item.showDropdownDivider !== false && <div className="mx-2 my-2 h-px bg-ods-border" />}
|
|
226
|
-
<div className="px-2 pb-2">{item.dropdownContent}</div>
|
|
227
|
-
</>
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Regular navigation item
|
|
235
|
-
if (item.href || item.onClick) {
|
|
236
|
-
return (
|
|
237
|
-
<Button
|
|
238
|
-
key={item.id}
|
|
239
|
-
variant="transparent"
|
|
240
|
-
href={item.href} // Use href for navigation
|
|
241
|
-
onClick={item.onClick} // Only for non-navigation actions
|
|
242
|
-
leftIcon={item.icon}
|
|
243
|
-
rightIcon={item.badge}
|
|
244
|
-
size="default"
|
|
245
|
-
font="regular"
|
|
246
|
-
className={cn(
|
|
247
|
-
NAV_ITEM_CLASSES,
|
|
248
|
-
'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
|
|
249
|
-
'whitespace-nowrap',
|
|
250
|
-
'text-ods-text-primary', // All items use primary text color
|
|
251
|
-
item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background
|
|
252
|
-
item.className,
|
|
253
|
-
)}
|
|
254
|
-
{...(item.isExternal && { isExternal: true })}
|
|
255
|
-
>
|
|
256
|
-
{item.label}
|
|
257
|
-
</Button>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Button with onClick
|
|
262
|
-
return (
|
|
263
|
-
<Button
|
|
264
|
-
key={item.id}
|
|
265
|
-
variant="transparent"
|
|
266
|
-
onClick={item.onClick}
|
|
267
|
-
leftIcon={item.icon}
|
|
268
|
-
rightIcon={item.badge}
|
|
269
|
-
size="default"
|
|
270
|
-
font="regular"
|
|
271
|
-
className={cn(
|
|
272
|
-
NAV_ITEM_CLASSES,
|
|
273
|
-
'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
|
|
274
|
-
'whitespace-nowrap',
|
|
275
|
-
'text-ods-text-primary', // All items use primary text color
|
|
276
|
-
item.isActive && 'bg-ods-bg-hover', // Active items get gray background
|
|
277
|
-
item.className,
|
|
278
|
-
)}
|
|
279
|
-
>
|
|
280
|
-
{item.label}
|
|
281
|
-
</Button>
|
|
282
|
-
);
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// `show` only means anything while auto-hide is on — with it off the header
|
|
286
|
-
// is always up, regardless of whatever the scroll listener last recorded.
|
|
287
|
-
const headerShown = !config.autoHide || show;
|
|
288
|
-
|
|
289
|
-
const hasNav = !!config.navigation && config.navigation.items.length > 0;
|
|
290
|
-
const hasCta =
|
|
291
|
-
!!config.actions?.right?.length || !!(config.actions?.persistent && config.actions.persistent.length > 0);
|
|
292
|
-
|
|
293
|
-
return (
|
|
294
|
-
<div
|
|
295
|
-
className={cn('sticky top-0 z-[50] w-full transition-transform duration-300 ease-in-out')}
|
|
296
|
-
style={{
|
|
297
|
-
transform: !headerShown ? 'translateY(-100%)' : 'translateY(0)',
|
|
298
|
-
}}
|
|
299
|
-
>
|
|
300
|
-
{/* Unified ODS top-navigation shell (Figma 2797-5978): 48px mobile /
|
|
301
|
-
56px md+, cell model with per-cell dividers. NOTE: no `backdrop-blur`
|
|
302
|
-
anywhere in this bar. Every platform ships an OPAQUE header
|
|
303
|
-
background (`backgroundColor` resolves to an opaque ODS token), so a
|
|
304
|
-
backdrop-filter would blur a backdrop that is then fully painted
|
|
305
|
-
over: zero visual effect, but the browser still re-rasterizes the
|
|
306
|
-
strip behind this sticky bar every scroll frame and content flickers
|
|
307
|
-
as it passes under the header. If a consumer ever wants a
|
|
308
|
-
translucent "glass" header, add the blur together with a translucent
|
|
309
|
-
`backgroundColor` deliberately. */}
|
|
310
|
-
<TopNavigation
|
|
311
|
-
// `relative` anchors the absolutely-centered nav (position 'center')
|
|
312
|
-
// to the bar itself, so the links sit at the true viewport center
|
|
313
|
-
// regardless of the asymmetric CTA/Mingo cluster on the right.
|
|
314
|
-
className={cn('relative', config.className)}
|
|
315
|
-
style={config.style}
|
|
316
|
-
backgroundClassName={config.backgroundColor}
|
|
317
|
-
centerBreakpoint="lg"
|
|
318
|
-
size="big"
|
|
319
|
-
leading={
|
|
320
|
-
<>
|
|
321
|
-
{/* Length-guarded: platform configs often pass `left: []`, and an
|
|
322
|
-
empty array is truthy — without the guard the cell would render
|
|
323
|
-
as a bare divider + padding. */}
|
|
324
|
-
{/* No padding on the wrapper: leading cells (HeaderButton-based
|
|
325
|
-
admin toggles) size themselves to the bar's square cell. */}
|
|
326
|
-
{!!config.actions?.left?.length && (
|
|
327
|
-
<div className="flex h-full items-center border-r border-ods-border">{config.actions.left}</div>
|
|
328
|
-
)}
|
|
329
|
-
{/* Mobile/tablet menu toggle — a leading cell per the ODS spec
|
|
330
|
-
(banded with the nav breakpoint `lg` so the desktop nav and the
|
|
331
|
-
toggle never co-show). */}
|
|
332
|
-
{config.mobile?.enabled && (
|
|
333
|
-
<HeaderButton
|
|
334
|
-
className="border-r border-ods-border lg:hidden"
|
|
335
|
-
onClick={() => {
|
|
336
|
-
config.mobile?.onToggle?.();
|
|
337
|
-
}}
|
|
338
|
-
isActive={config.mobile?.isOpen ?? false}
|
|
339
|
-
aria-label={config.mobile?.isOpen ? 'Close menu' : 'Open menu'}
|
|
340
|
-
aria-expanded={config.mobile?.isOpen ?? false}
|
|
341
|
-
// Conditional: the panel unmounts when closed, so an unconditional
|
|
342
|
-
// reference would dangle (axe aria-valid-attr-value).
|
|
343
|
-
aria-controls={config.mobile?.isOpen ? MOBILE_NAV_PANEL_ID : undefined}
|
|
344
|
-
icon={
|
|
345
|
-
config.mobile?.isOpen
|
|
346
|
-
? config.mobile?.closeIcon || <XmarkIcon className="h-6 w-6" />
|
|
347
|
-
: config.mobile?.menuIcon || <Menu01Icon className="h-6 w-6" />
|
|
348
|
-
}
|
|
349
|
-
/>
|
|
350
|
-
)}
|
|
351
|
-
</>
|
|
352
|
-
}
|
|
353
|
-
logo={
|
|
354
|
-
config.logo.href ? (
|
|
355
|
-
<Link href={config.logo.href} className="transition-opacity duration-200 hover:opacity-80">
|
|
356
|
-
{config.logo.element}
|
|
357
|
-
</Link>
|
|
358
|
-
) : (
|
|
359
|
-
config.logo.element
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
|
-
// Big-bar rule (Figma 2936-6812): 24px fixed left inset on the logo
|
|
363
|
-
// zone at every breakpoint — the same 24px also reads as the gap
|
|
364
|
-
// between a leading cell (burger / admin toggle) and the logo.
|
|
365
|
-
logoClassName="pl-[var(--spacing-system-lf)] md:pl-[var(--spacing-system-lf)] lg:pl-[var(--spacing-system-lf)]"
|
|
366
|
-
center={
|
|
367
|
-
hasNav ? (
|
|
368
|
-
<nav
|
|
369
|
-
className={cn(
|
|
370
|
-
'flex items-center gap-2',
|
|
371
|
-
// True centering relative to the bar (not the leftover flex
|
|
372
|
-
// space) — same treatment the pre-unification header had.
|
|
373
|
-
config.navigation?.position === 'center' && 'absolute left-1/2 -translate-x-1/2',
|
|
374
|
-
)}
|
|
375
|
-
role="navigation"
|
|
376
|
-
aria-label="Main navigation"
|
|
377
|
-
>
|
|
378
|
-
{config.navigation?.items.map(renderNavigationItem)}
|
|
379
|
-
</nav>
|
|
380
|
-
) : undefined
|
|
381
|
-
}
|
|
382
|
-
centerClassName={cn(
|
|
383
|
-
config.navigation?.position === 'left' && 'justify-start',
|
|
384
|
-
config.navigation?.position === 'right' && 'justify-end pr-[var(--spacing-system-m)]',
|
|
385
|
-
)}
|
|
386
|
-
cta={
|
|
387
|
-
hasCta ? (
|
|
388
|
-
<>
|
|
389
|
-
{/* Desktop actions — banded with the nav/burger breakpoint (lg)
|
|
390
|
-
so the desktop right-cluster and the mobile toggle never
|
|
391
|
-
co-show. */}
|
|
392
|
-
{!!config.actions?.right?.length && (
|
|
393
|
-
<div className="hidden items-center gap-3 lg:flex">{config.actions.right}</div>
|
|
394
|
-
)}
|
|
395
|
-
{config.actions?.persistent && config.actions.persistent.length > 0 && (
|
|
396
|
-
<div className="flex items-center">{config.actions.persistent}</div>
|
|
397
|
-
)}
|
|
398
|
-
</>
|
|
399
|
-
) : undefined
|
|
400
|
-
}
|
|
401
|
-
ctaClassName="gap-3"
|
|
402
|
-
sideActions={
|
|
403
|
-
config.tickets || config.mingo?.enabled ? (
|
|
404
|
-
<>
|
|
405
|
-
{/* Support-ticket alerts cell — before Mingo, flush cell row.
|
|
406
|
-
Attention-only: renders nothing unless there are unread
|
|
407
|
-
replies (and the host mounted <TicketLiveProvider>). */}
|
|
408
|
-
{config.tickets && (
|
|
409
|
-
<TicketAlertsButton
|
|
410
|
-
href={config.tickets.href}
|
|
411
|
-
onNavigate={config.tickets.onClick}
|
|
412
|
-
className="border-l border-ods-border"
|
|
413
|
-
/>
|
|
414
|
-
)}
|
|
415
|
-
{config.mingo?.enabled && (
|
|
416
|
-
<MingoAiButton
|
|
417
|
-
source={config.mingo.source}
|
|
418
|
-
icon={config.mingo.icon}
|
|
419
|
-
label={config.mingo.label}
|
|
420
|
-
className={config.mingo.className}
|
|
421
|
-
/>
|
|
422
|
-
)}
|
|
423
|
-
</>
|
|
424
|
-
) : undefined
|
|
425
|
-
}
|
|
426
|
-
/>
|
|
427
|
-
</div>
|
|
428
|
-
);
|
|
429
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { usePreventScroll } from '@react-aria/overlays';
|
|
4
|
-
import { X } from 'lucide-react';
|
|
5
|
-
import type React from 'react';
|
|
6
|
-
import { useRef } from 'react';
|
|
7
|
-
import { useFocusTrap } from '../../hooks/ui/use-focus-trap';
|
|
8
|
-
import { useHeaderHeight } from '../../hooks/ui/use-header-height';
|
|
9
|
-
import type { MobileNavConfig, NavigationItem } from '../../types/navigation';
|
|
10
|
-
import { cn } from '../../utils';
|
|
11
|
-
import { Button } from '../ui/button';
|
|
12
|
-
import { OVERLAY_BACKDROP_CLASS } from '../ui/drawer';
|
|
13
|
-
|
|
14
|
-
/** DOM id of the panel — referenced by the header hamburger's `aria-controls`. */
|
|
15
|
-
export const MOBILE_NAV_PANEL_ID = 'mobile-nav-panel';
|
|
16
|
-
|
|
17
|
-
export interface MobileNavPanelProps {
|
|
18
|
-
isOpen: boolean;
|
|
19
|
-
config: MobileNavConfig;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function MobileNavPanel({ isOpen, config }: MobileNavPanelProps) {
|
|
23
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
24
|
-
|
|
25
|
-
// Shared ref-counted, iOS-aware scroll lock (react-aria) — one counter with
|
|
26
|
-
// the modals and chat overlays, restores prior styles on release.
|
|
27
|
-
usePreventScroll({ isDisabled: !isOpen });
|
|
28
|
-
// Initial focus, Tab containment, Escape-to-close, guarded focus restore.
|
|
29
|
-
useFocusTrap(panelRef, isOpen, { onEscape: config.onClose });
|
|
30
|
-
// The panel is layout-mounted on every page — observers only while open.
|
|
31
|
-
const headerHeight = useHeaderHeight(64, { enabled: isOpen });
|
|
32
|
-
|
|
33
|
-
if (!isOpen) return null;
|
|
34
|
-
|
|
35
|
-
const renderNavigationItem = (item: NavigationItem) => {
|
|
36
|
-
if (item.element) {
|
|
37
|
-
return <div key={item.id}>{item.element}</div>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const handleClick = () => {
|
|
41
|
-
if (item.onClick) {
|
|
42
|
-
item.onClick();
|
|
43
|
-
}
|
|
44
|
-
config.onClose?.();
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (item.href) {
|
|
48
|
-
return (
|
|
49
|
-
<Button
|
|
50
|
-
key={item.id}
|
|
51
|
-
variant="outline"
|
|
52
|
-
size="small-legacy"
|
|
53
|
-
href={item.href}
|
|
54
|
-
onClick={handleClick}
|
|
55
|
-
leftIcon={item.icon}
|
|
56
|
-
rightIcon={item.badge}
|
|
57
|
-
className={cn(
|
|
58
|
-
'h-12 justify-start gap-3 rounded-md border-none bg-transparent px-4 text-ods-text-primary transition-colors hover:bg-ods-bg-hover',
|
|
59
|
-
item.isActive ? 'bg-ods-bg-hover' : '',
|
|
60
|
-
)}
|
|
61
|
-
>
|
|
62
|
-
{item.label}
|
|
63
|
-
</Button>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<Button
|
|
69
|
-
key={item.id}
|
|
70
|
-
variant="outline"
|
|
71
|
-
size="small-legacy"
|
|
72
|
-
onClick={handleClick}
|
|
73
|
-
leftIcon={item.icon}
|
|
74
|
-
className={cn(
|
|
75
|
-
'h-12 justify-start gap-3 rounded-md border-none bg-transparent px-4 text-ods-text-primary transition-colors hover:bg-ods-bg-hover',
|
|
76
|
-
item.isActive ? 'bg-ods-bg-hover' : '',
|
|
77
|
-
)}
|
|
78
|
-
>
|
|
79
|
-
<span className="flex-1 text-left">{item.label}</span>
|
|
80
|
-
{item.badge !== undefined && <span className="ml-auto">{item.badge}</span>}
|
|
81
|
-
</Button>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<>
|
|
87
|
-
{/* Backdrop - closes nav when clicked outside */}
|
|
88
|
-
<div className={cn('fixed inset-0 z-[9998]', OVERLAY_BACKDROP_CLASS)} onClick={config.onClose} />
|
|
89
|
-
|
|
90
|
-
{/* Navigation Panel - top-anchored floating card */}
|
|
91
|
-
<div
|
|
92
|
-
ref={panelRef}
|
|
93
|
-
id={MOBILE_NAV_PANEL_ID}
|
|
94
|
-
role="dialog"
|
|
95
|
-
aria-modal="true"
|
|
96
|
-
aria-label="Navigation menu"
|
|
97
|
-
tabIndex={-1}
|
|
98
|
-
style={{ '--mobile-nav-top': `${headerHeight + 16}px` } as React.CSSProperties}
|
|
99
|
-
className={cn(
|
|
100
|
-
'fixed z-[9999] rounded-lg shadow-xl',
|
|
101
|
-
config.className ? '' : 'border border-ods-border bg-ods-card',
|
|
102
|
-
// Responsive positioning and sizing — LEFT-anchored from md, flush
|
|
103
|
-
// under the burger cell at the left edge of the unified
|
|
104
|
-
// top-navigation (a larger inset read as a stray gap between the
|
|
105
|
-
// burger and its panel).
|
|
106
|
-
'left-2 right-2 md:right-auto',
|
|
107
|
-
'md:left-2 md:w-[400px] md:max-w-[calc(100vw-1rem)]',
|
|
108
|
-
// Small-viewport (svh) sizing anchored below the measured header —
|
|
109
|
-
// 100vh overflowed under mobile browser chrome, leaving the bottom
|
|
110
|
-
// entries and footer unreachable while the body was scroll-locked.
|
|
111
|
-
'top-[var(--mobile-nav-top)]',
|
|
112
|
-
'max-h-[calc(100svh-var(--mobile-nav-top)-50px)] md:max-h-[calc(100svh-var(--mobile-nav-top)-8px)]',
|
|
113
|
-
'flex flex-col',
|
|
114
|
-
config.className || '',
|
|
115
|
-
)}
|
|
116
|
-
onClick={e => e.stopPropagation()} // Prevent closing when clicking inside nav
|
|
117
|
-
>
|
|
118
|
-
{/* Header with close button - fixed at top */}
|
|
119
|
-
<div className="flex flex-shrink-0 justify-end border-b border-ods-border p-2">
|
|
120
|
-
<Button
|
|
121
|
-
aria-label="Close menu"
|
|
122
|
-
size="icon"
|
|
123
|
-
variant="transparent"
|
|
124
|
-
leftIcon={<X className="h-4 w-4 text-ods-text-primary" />}
|
|
125
|
-
onClick={config.onClose}
|
|
126
|
-
className="hover:bg-ods-bg-hover"
|
|
127
|
-
/>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
{/* Scrollable content area */}
|
|
131
|
-
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-2 py-2">
|
|
132
|
-
<div className="flex flex-col space-y-1">
|
|
133
|
-
{config.sections.map((section, index) => (
|
|
134
|
-
<div key={index}>
|
|
135
|
-
{section.title && (
|
|
136
|
-
<div className="px-4 pb-1 pt-2 font-semibold uppercase text-ods-text-secondary text-h6">
|
|
137
|
-
{section.title}
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
140
|
-
<div className="flex flex-col gap-1">{section.items.map(renderNavigationItem)}</div>
|
|
141
|
-
</div>
|
|
142
|
-
))}
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Footer with action button - fixed at bottom, clears the iOS home indicator */}
|
|
147
|
-
{config.footer && (
|
|
148
|
-
<div className="flex-shrink-0 border-t border-ods-border p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
|
149
|
-
{config.footer}
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
152
|
-
</div>
|
|
153
|
-
</>
|
|
154
|
-
);
|
|
155
|
-
}
|