@flamingo-stack/openframe-frontend-core 0.0.493 → 0.0.494
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-WPFLV4QJ.js → chunk-4BGDD6CU.js} +4 -4
- package/dist/{chunk-LIRXKEOE.cjs → chunk-7M24CJRH.cjs} +15 -15
- package/dist/{chunk-LIRXKEOE.cjs.map → chunk-7M24CJRH.cjs.map} +1 -1
- package/dist/{chunk-5ICPQAUI.cjs → chunk-CDRNCBLS.cjs} +59 -59
- package/dist/{chunk-5ICPQAUI.cjs.map → chunk-CDRNCBLS.cjs.map} +1 -1
- package/dist/{chunk-FG5ALORQ.js → chunk-DZU3FCOQ.js} +2 -2
- package/dist/{chunk-J2J36PEN.cjs → chunk-E5AIN5SS.cjs} +31 -31
- package/dist/{chunk-J2J36PEN.cjs.map → chunk-E5AIN5SS.cjs.map} +1 -1
- package/dist/{chunk-O6S6Q52W.cjs → chunk-FFL3D5LX.cjs} +26 -26
- package/dist/{chunk-O6S6Q52W.cjs.map → chunk-FFL3D5LX.cjs.map} +1 -1
- package/dist/{chunk-VML66FJQ.cjs → chunk-FP62TQ3A.cjs} +9 -9
- package/dist/{chunk-VML66FJQ.cjs.map → chunk-FP62TQ3A.cjs.map} +1 -1
- package/dist/{chunk-QHRNQTW5.js → chunk-GANUPKHV.js} +2 -2
- package/dist/{chunk-LVDI7R7T.cjs → chunk-J6MC4SY7.cjs} +37 -37
- package/dist/{chunk-LVDI7R7T.cjs.map → chunk-J6MC4SY7.cjs.map} +1 -1
- package/dist/{chunk-U5LU56JE.js → chunk-KDPQGHQ2.js} +2 -2
- package/dist/{chunk-56J7RLMM.js → chunk-KXOSLXL2.js} +1667 -1602
- package/dist/chunk-KXOSLXL2.js.map +1 -0
- package/dist/{chunk-OFFYASUY.cjs → chunk-L5GMHETR.cjs} +26 -26
- package/dist/{chunk-OFFYASUY.cjs.map → chunk-L5GMHETR.cjs.map} +1 -1
- package/dist/{chunk-P3ITKTUE.js → chunk-OQFXEWW2.js} +2 -2
- package/dist/{chunk-2XAAFAQT.cjs → chunk-P77L6MPJ.cjs} +11 -11
- package/dist/{chunk-2XAAFAQT.cjs.map → chunk-P77L6MPJ.cjs.map} +1 -1
- package/dist/{chunk-4J3CB4KO.js → chunk-PBAFZETL.js} +2 -2
- package/dist/{chunk-PI5BMKV3.cjs → chunk-QHGGQZFK.cjs} +62 -62
- package/dist/{chunk-PI5BMKV3.cjs.map → chunk-QHGGQZFK.cjs.map} +1 -1
- package/dist/{chunk-M4SCYPCR.cjs → chunk-QRHAIJMG.cjs} +7 -7
- package/dist/{chunk-M4SCYPCR.cjs.map → chunk-QRHAIJMG.cjs.map} +1 -1
- package/dist/{chunk-4MLOMMDX.cjs → chunk-UCZHGV4E.cjs} +635 -570
- package/dist/chunk-UCZHGV4E.cjs.map +1 -0
- package/dist/{chunk-SZWMJIH3.js → chunk-VCHK2B6W.js} +2 -2
- package/dist/{chunk-5RHJVPL7.js → chunk-VUUIYBIH.js} +4 -4
- package/dist/{chunk-4AWRPTKE.js → chunk-WUM5BTDY.js} +4 -4
- package/dist/{chunk-BTIRYGDT.js → chunk-YQ4B7LCE.js} +4 -4
- package/dist/{chunk-WKWZ7LVC.js → chunk-ZP4QMJX4.js} +2 -2
- package/dist/{chunk-DY6AJNVK.cjs → chunk-ZVCODNW7.cjs} +5 -5
- package/dist/{chunk-DY6AJNVK.cjs.map → chunk-ZVCODNW7.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +132 -130
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +11 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/header-button.d.ts.map +1 -1
- package/dist/components/navigation/header-global-search.d.ts.map +1 -1
- package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
- package/dist/components/navigation/header-organization-filter.d.ts.map +1 -1
- package/dist/components/navigation/header-skeleton.d.ts +6 -0
- package/dist/components/navigation/header-skeleton.d.ts.map +1 -1
- package/dist/components/navigation/header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +4 -2
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +3 -1
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/navigation/mobile-nav-panel.d.ts.map +1 -1
- package/dist/components/navigation/top-navigation.d.ts +55 -0
- package/dist/components/navigation/top-navigation.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/components/features/time-tracker/time-tracker-header-button.tsx +42 -38
- package/src/components/navigation/app-header.tsx +190 -198
- package/src/components/navigation/header-button.tsx +3 -1
- package/src/components/navigation/header-global-search.tsx +3 -1
- package/src/components/navigation/header-mingo-button.tsx +3 -1
- package/src/components/navigation/header-organization-filter.tsx +2 -1
- package/src/components/navigation/header-skeleton.tsx +57 -54
- package/src/components/navigation/header.tsx +231 -208
- package/src/components/navigation/index.ts +3 -0
- package/src/components/navigation/mingo-ai-button.tsx +24 -19
- package/src/components/navigation/mobile-nav-panel.tsx +6 -3
- package/src/components/navigation/top-navigation.tsx +128 -0
- package/src/stories/AppHeader.stories.tsx +148 -0
- package/src/stories/Header.stories.tsx +18 -23
- package/src/stories/TopNavigation.stories.tsx +150 -0
- package/dist/chunk-4MLOMMDX.cjs.map +0 -1
- package/dist/chunk-56J7RLMM.js.map +0 -1
- /package/dist/{chunk-WPFLV4QJ.js.map → chunk-4BGDD6CU.js.map} +0 -0
- /package/dist/{chunk-FG5ALORQ.js.map → chunk-DZU3FCOQ.js.map} +0 -0
- /package/dist/{chunk-QHRNQTW5.js.map → chunk-GANUPKHV.js.map} +0 -0
- /package/dist/{chunk-U5LU56JE.js.map → chunk-KDPQGHQ2.js.map} +0 -0
- /package/dist/{chunk-P3ITKTUE.js.map → chunk-OQFXEWW2.js.map} +0 -0
- /package/dist/{chunk-4J3CB4KO.js.map → chunk-PBAFZETL.js.map} +0 -0
- /package/dist/{chunk-SZWMJIH3.js.map → chunk-VCHK2B6W.js.map} +0 -0
- /package/dist/{chunk-5RHJVPL7.js.map → chunk-VUUIYBIH.js.map} +0 -0
- /package/dist/{chunk-4AWRPTKE.js.map → chunk-WUM5BTDY.js.map} +0 -0
- /package/dist/{chunk-BTIRYGDT.js.map → chunk-YQ4B7LCE.js.map} +0 -0
- /package/dist/{chunk-WKWZ7LVC.js.map → chunk-ZP4QMJX4.js.map} +0 -0
|
@@ -1,147 +1,152 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { HeaderConfig, NavigationItem } from '../../types/navigation'
|
|
6
|
-
import { cn } from '../../utils'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { MingoAiButton } from './mingo-ai-button'
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import Link from '../../embed-shims/next-link';
|
|
5
|
+
import { 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 { TopNavigation } from './top-navigation';
|
|
11
13
|
|
|
12
14
|
export interface HeaderProps {
|
|
13
|
-
config: HeaderConfig
|
|
14
|
-
platform?: string
|
|
15
|
+
config: HeaderConfig;
|
|
16
|
+
platform?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
// Re-export from types for convenience
|
|
18
|
-
export type { HeaderConfig } from '../../types/navigation'
|
|
20
|
+
export type { HeaderConfig } from '../../types/navigation';
|
|
19
21
|
|
|
20
22
|
export function Header({ config, platform }: HeaderProps) {
|
|
21
|
-
const [show, setShow] = useState(true)
|
|
22
|
-
const [lastScrollY, setLastScrollY] = useState(0)
|
|
23
|
-
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({})
|
|
24
|
-
const dropdownRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
|
25
|
-
const triggerRefs = useRef<Record<string, HTMLButtonElement | null>>({})
|
|
26
|
-
|
|
23
|
+
const [show, setShow] = useState(true);
|
|
24
|
+
const [lastScrollY, setLastScrollY] = useState(0);
|
|
25
|
+
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
|
|
26
|
+
const dropdownRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
27
|
+
const triggerRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
|
28
|
+
|
|
27
29
|
// Handle click outside and escape key for custom dropdown
|
|
28
30
|
useEffect(() => {
|
|
29
31
|
const handleClickOutside = (event: MouseEvent) => {
|
|
30
|
-
const target = event.target as HTMLElement
|
|
31
|
-
if (!target) return
|
|
32
|
+
const target = event.target as HTMLElement;
|
|
33
|
+
if (!target) return;
|
|
32
34
|
|
|
33
35
|
// Check if click is outside all dropdowns
|
|
34
36
|
const isOutsideAllDropdowns = Object.keys(openDropdowns).every(id => {
|
|
35
|
-
const dropdown = dropdownRefs.current[id]
|
|
36
|
-
const trigger = triggerRefs.current[id]
|
|
37
|
-
|
|
38
|
-
if (!dropdown || !trigger) return true
|
|
39
|
-
|
|
40
|
-
return !dropdown.contains(target) && !trigger.contains(target)
|
|
41
|
-
})
|
|
37
|
+
const dropdown = dropdownRefs.current[id];
|
|
38
|
+
const trigger = triggerRefs.current[id];
|
|
39
|
+
|
|
40
|
+
if (!dropdown || !trigger) return true;
|
|
41
|
+
|
|
42
|
+
return !dropdown.contains(target) && !trigger.contains(target);
|
|
43
|
+
});
|
|
42
44
|
|
|
43
45
|
if (isOutsideAllDropdowns) {
|
|
44
|
-
setOpenDropdowns({})
|
|
46
|
+
setOpenDropdowns({});
|
|
45
47
|
}
|
|
46
|
-
}
|
|
48
|
+
};
|
|
47
49
|
|
|
48
50
|
const handleEscapeKey = (event: KeyboardEvent) => {
|
|
49
51
|
if (event.key === 'Escape') {
|
|
50
|
-
setOpenDropdowns({})
|
|
52
|
+
setOpenDropdowns({});
|
|
51
53
|
}
|
|
52
|
-
}
|
|
54
|
+
};
|
|
53
55
|
|
|
54
56
|
// Only add listeners if any dropdown is open
|
|
55
|
-
const hasOpenDropdowns = Object.values(openDropdowns).some(Boolean)
|
|
57
|
+
const hasOpenDropdowns = Object.values(openDropdowns).some(Boolean);
|
|
56
58
|
if (hasOpenDropdowns) {
|
|
57
|
-
document.addEventListener('mousedown', handleClickOutside)
|
|
58
|
-
document.addEventListener('keydown', handleEscapeKey)
|
|
59
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
60
|
+
document.addEventListener('keydown', handleEscapeKey);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
return () => {
|
|
62
|
-
document.removeEventListener('mousedown', handleClickOutside)
|
|
63
|
-
document.removeEventListener('keydown', handleEscapeKey)
|
|
64
|
-
}
|
|
65
|
-
}, [openDropdowns])
|
|
64
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
65
|
+
document.removeEventListener('keydown', handleEscapeKey);
|
|
66
|
+
};
|
|
67
|
+
}, [openDropdowns]);
|
|
66
68
|
|
|
67
69
|
// Force close all dropdowns and cleanup on unmount
|
|
68
70
|
useEffect(() => {
|
|
69
71
|
return () => {
|
|
70
72
|
// Close all dropdowns before unmounting to prevent focus errors
|
|
71
|
-
setOpenDropdowns({})
|
|
73
|
+
setOpenDropdowns({});
|
|
72
74
|
// Clear any stored refs
|
|
73
|
-
dropdownRefs.current = {}
|
|
74
|
-
triggerRefs.current = {}
|
|
75
|
-
}
|
|
76
|
-
}, [])
|
|
77
|
-
|
|
75
|
+
dropdownRefs.current = {};
|
|
76
|
+
triggerRefs.current = {};
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
78
80
|
useEffect(() => {
|
|
79
81
|
// Only add scroll listener if autoHide is enabled
|
|
80
82
|
if (!config.autoHide) {
|
|
81
|
-
setShow(true) // Always show header when autoHide is disabled
|
|
82
|
-
return
|
|
83
|
+
setShow(true); // Always show header when autoHide is disabled
|
|
84
|
+
return;
|
|
83
85
|
}
|
|
84
|
-
|
|
86
|
+
|
|
85
87
|
const handleScroll = () => {
|
|
86
|
-
const currentScrollY = window.scrollY
|
|
87
|
-
|
|
88
|
+
const currentScrollY = window.scrollY;
|
|
89
|
+
|
|
88
90
|
setLastScrollY(prevScrollY => {
|
|
89
91
|
// Determine if we should show or hide the header
|
|
90
|
-
const shouldHide = currentScrollY > prevScrollY && currentScrollY > 50
|
|
91
|
-
const shouldShow = currentScrollY < prevScrollY || currentScrollY <= 10
|
|
92
|
-
|
|
92
|
+
const shouldHide = currentScrollY > prevScrollY && currentScrollY > 50;
|
|
93
|
+
const shouldShow = currentScrollY < prevScrollY || currentScrollY <= 10;
|
|
94
|
+
|
|
93
95
|
if (shouldHide) {
|
|
94
|
-
setShow(false)
|
|
96
|
+
setShow(false);
|
|
95
97
|
} else if (shouldShow) {
|
|
96
|
-
setShow(true)
|
|
98
|
+
setShow(true);
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
return currentScrollY
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
}, [config.autoHide])
|
|
101
|
+
return currentScrollY;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
109
106
|
|
|
107
|
+
return () => {
|
|
108
|
+
window.removeEventListener('scroll', handleScroll);
|
|
109
|
+
};
|
|
110
|
+
}, [config.autoHide]);
|
|
110
111
|
|
|
111
112
|
const renderNavigationItem = (item: NavigationItem) => {
|
|
112
113
|
// If custom element provided, use it
|
|
113
114
|
if (item.element) {
|
|
114
|
-
return <React.Fragment key={item.id}>{item.element}</React.Fragment
|
|
115
|
+
return <React.Fragment key={item.id}>{item.element}</React.Fragment>;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
// If it has children, render as custom dropdown
|
|
118
119
|
if (item.children && item.children.length > 0) {
|
|
119
|
-
const isOpen = openDropdowns[item.id] || false
|
|
120
|
-
|
|
120
|
+
const isOpen = openDropdowns[item.id] || false;
|
|
121
|
+
|
|
121
122
|
return (
|
|
122
123
|
<div key={item.id} className="relative">
|
|
123
124
|
<Button
|
|
124
|
-
ref={
|
|
125
|
+
ref={el => {
|
|
126
|
+
triggerRefs.current[item.id] = el;
|
|
127
|
+
}}
|
|
125
128
|
variant="transparent"
|
|
126
129
|
leftIcon={item.icon}
|
|
127
130
|
rightIcon={item.badge}
|
|
128
131
|
onClick={() => {
|
|
129
|
-
setOpenDropdowns(prev => ({
|
|
130
|
-
...prev,
|
|
131
|
-
[item.id]: !prev[item.id]
|
|
132
|
-
}))
|
|
132
|
+
setOpenDropdowns(prev => ({
|
|
133
|
+
...prev,
|
|
134
|
+
[item.id]: !prev[item.id],
|
|
135
|
+
}));
|
|
133
136
|
}}
|
|
134
137
|
size="small-legacy"
|
|
135
138
|
className={cn(
|
|
136
|
-
|
|
139
|
+
// Top-level nav links: h3 bold per the ODS top-navigation spec
|
|
140
|
+
// (Figma 2797-5978); dropdown children stay on the h6 step.
|
|
141
|
+
'text-h3 font-bold tracking-[-0.36px]',
|
|
137
142
|
item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background
|
|
138
143
|
isOpen && 'bg-ods-bg-hover', // Open dropdowns get gray background
|
|
139
|
-
item.className
|
|
144
|
+
item.className,
|
|
140
145
|
)}
|
|
141
146
|
>
|
|
142
147
|
{item.label}
|
|
143
148
|
</Button>
|
|
144
|
-
|
|
149
|
+
|
|
145
150
|
{/* Always render dropdown in DOM so crawlers see child <a> links;
|
|
146
151
|
toggle visibility via CSS + `inert`.
|
|
147
152
|
|
|
@@ -156,18 +161,18 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
156
161
|
removes from a11y tree + blocks focus + prevents click +
|
|
157
162
|
removes from tab order. Native React 19 support. */}
|
|
158
163
|
<div
|
|
159
|
-
ref={
|
|
164
|
+
ref={el => {
|
|
165
|
+
dropdownRefs.current[item.id] = el;
|
|
166
|
+
}}
|
|
160
167
|
inert={!isOpen}
|
|
161
168
|
className={cn(
|
|
162
|
-
|
|
163
|
-
item.dropdownClassName ?
|
|
164
|
-
|
|
165
|
-
item.id === 'community' ?
|
|
166
|
-
|
|
167
|
-
isOpen
|
|
168
|
-
|
|
169
|
-
: "opacity-0 invisible pointer-events-none",
|
|
170
|
-
item.dropdownClassName || ''
|
|
169
|
+
'absolute top-full left-0 mt-1',
|
|
170
|
+
item.dropdownClassName ? '' : 'bg-ods-card border border-ods-border',
|
|
171
|
+
'rounded-lg shadow-xl z-[9999]',
|
|
172
|
+
item.id === 'community' ? 'min-w-[240px]' : 'min-w-[220px]',
|
|
173
|
+
'transition-opacity duration-150',
|
|
174
|
+
isOpen ? 'opacity-100 visible pointer-events-auto' : 'opacity-0 invisible pointer-events-none',
|
|
175
|
+
item.dropdownClassName || '',
|
|
171
176
|
)}
|
|
172
177
|
>
|
|
173
178
|
<div className="p-2">
|
|
@@ -181,18 +186,18 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
181
186
|
rightIcon={child.badge}
|
|
182
187
|
onClick={() => {
|
|
183
188
|
// Always close dropdown when any item is clicked
|
|
184
|
-
setOpenDropdowns(prev => ({ ...prev, [item.id]: false }))
|
|
189
|
+
setOpenDropdowns(prev => ({ ...prev, [item.id]: false }));
|
|
185
190
|
// If there's a custom onClick, call it too
|
|
186
191
|
if (child.onClick) {
|
|
187
|
-
child.onClick()
|
|
192
|
+
child.onClick();
|
|
188
193
|
}
|
|
189
194
|
}}
|
|
190
195
|
className={cn(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
index < (item.children?.length ?? 0) - 1 &&
|
|
194
|
-
|
|
195
|
-
child.isActive && 'bg-ods-bg-hover' // Active dropdown items get gray background
|
|
196
|
+
'flex justify-start w-full',
|
|
197
|
+
'text-h6 font-bold',
|
|
198
|
+
index < (item.children?.length ?? 0) - 1 && 'mb-1',
|
|
199
|
+
'text-ods-text-primary', // All dropdown items use primary text color
|
|
200
|
+
child.isActive && 'bg-ods-bg-hover', // Active dropdown items get gray background
|
|
196
201
|
)}
|
|
197
202
|
{...(child.isExternal && { isExternal: true })}
|
|
198
203
|
>
|
|
@@ -203,14 +208,12 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
203
208
|
{item.dropdownContent && (
|
|
204
209
|
<>
|
|
205
210
|
{item.showDropdownDivider !== false && <div className="h-px my-2 mx-2 bg-ods-border" />}
|
|
206
|
-
<div className="px-2 pb-2">
|
|
207
|
-
{item.dropdownContent}
|
|
208
|
-
</div>
|
|
211
|
+
<div className="px-2 pb-2">{item.dropdownContent}</div>
|
|
209
212
|
</>
|
|
210
213
|
)}
|
|
211
214
|
</div>
|
|
212
215
|
</div>
|
|
213
|
-
)
|
|
216
|
+
);
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
// Regular navigation item
|
|
@@ -225,18 +228,18 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
225
228
|
rightIcon={item.badge}
|
|
226
229
|
size="small-legacy"
|
|
227
230
|
className={cn(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
'text-h3 font-bold tracking-[-0.36px]',
|
|
232
|
+
'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
|
|
233
|
+
'whitespace-nowrap',
|
|
234
|
+
'text-ods-text-primary', // All items use primary text color
|
|
232
235
|
item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background
|
|
233
|
-
item.className
|
|
236
|
+
item.className,
|
|
234
237
|
)}
|
|
235
238
|
{...(item.isExternal && { isExternal: true })}
|
|
236
239
|
>
|
|
237
240
|
{item.label}
|
|
238
241
|
</Button>
|
|
239
|
-
)
|
|
242
|
+
);
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
// Button with onClick
|
|
@@ -249,121 +252,141 @@ export function Header({ config, platform }: HeaderProps) {
|
|
|
249
252
|
rightIcon={item.badge}
|
|
250
253
|
size="small-legacy"
|
|
251
254
|
className={cn(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
'text-h3 font-bold tracking-[-0.36px]',
|
|
256
|
+
'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',
|
|
257
|
+
'whitespace-nowrap',
|
|
258
|
+
'text-ods-text-primary', // All items use primary text color
|
|
256
259
|
item.isActive && 'bg-ods-bg-hover', // Active items get gray background
|
|
257
|
-
item.className
|
|
260
|
+
item.className,
|
|
258
261
|
)}
|
|
259
262
|
>
|
|
260
263
|
{item.label}
|
|
261
264
|
</Button>
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const hasNav = !!config.navigation && config.navigation.items.length > 0;
|
|
269
|
+
// Always-visible leading cells (admin-sidebar toggle). The logo zone's
|
|
270
|
+
// desktop `pl-xxl` (80px) exists to compensate for the ABSENCE of a leading
|
|
271
|
+
// cell — with one present it reads as a stray gap, so collapse it to the
|
|
272
|
+
// regular `pl-l` inset. (The marketing burger doesn't count: it is
|
|
273
|
+
// CSS-hidden on lg, exactly where the 80px inset applies.)
|
|
274
|
+
const hasLeftCells = !!config.actions?.left?.length;
|
|
275
|
+
const hasCta =
|
|
276
|
+
!!config.actions?.right?.length || !!(config.actions?.persistent && config.actions.persistent.length > 0);
|
|
277
|
+
|
|
266
278
|
return (
|
|
267
|
-
<div
|
|
268
|
-
className={cn(
|
|
269
|
-
"sticky top-0 z-[50] w-full transition-transform duration-300 ease-in-out"
|
|
270
|
-
)}
|
|
279
|
+
<div
|
|
280
|
+
className={cn('sticky top-0 z-[50] w-full transition-transform duration-300 ease-in-out')}
|
|
271
281
|
style={{
|
|
272
|
-
transform: !show ? 'translateY(-100%)' : 'translateY(0)'
|
|
282
|
+
transform: !show ? 'translateY(-100%)' : 'translateY(0)',
|
|
273
283
|
}}
|
|
274
284
|
>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// blur together with a translucent `backgroundColor` deliberately.
|
|
291
|
-
"border-b border-ods-border",
|
|
292
|
-
"pl-6",
|
|
293
|
-
!config.mingo?.enabled && "pr-6",
|
|
294
|
-
// Background color (configurable via backgroundColor prop)
|
|
295
|
-
config.backgroundColor || "bg-ods-card",
|
|
296
|
-
config.className
|
|
297
|
-
)}
|
|
285
|
+
{/* Unified ODS top-navigation shell (Figma 2797-5978): 48px mobile /
|
|
286
|
+
56px md+, cell model with per-cell dividers. NOTE: no `backdrop-blur`
|
|
287
|
+
anywhere in this bar. Every platform ships an OPAQUE header
|
|
288
|
+
background (`backgroundColor` resolves to an opaque ODS token), so a
|
|
289
|
+
backdrop-filter would blur a backdrop that is then fully painted
|
|
290
|
+
over: zero visual effect, but the browser still re-rasterizes the
|
|
291
|
+
strip behind this sticky bar every scroll frame and content flickers
|
|
292
|
+
as it passes under the header. If a consumer ever wants a
|
|
293
|
+
translucent "glass" header, add the blur together with a translucent
|
|
294
|
+
`backgroundColor` deliberately. */}
|
|
295
|
+
<TopNavigation
|
|
296
|
+
// `relative` anchors the absolutely-centered nav (position 'center')
|
|
297
|
+
// to the bar itself, so the links sit at the true viewport center
|
|
298
|
+
// regardless of the asymmetric CTA/Mingo cluster on the right.
|
|
299
|
+
className={cn('relative', config.className)}
|
|
298
300
|
style={config.style}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)}
|
|
361
|
-
|
|
362
|
-
{config.mingo?.enabled && (
|
|
363
|
-
<MingoAiButton source={config.mingo.source} icon={config.mingo.icon} label={config.mingo.label} className={config.mingo.className} />
|
|
301
|
+
backgroundClassName={config.backgroundColor}
|
|
302
|
+
centerBreakpoint="lg"
|
|
303
|
+
leading={
|
|
304
|
+
<>
|
|
305
|
+
{/* Length-guarded: platform configs often pass `left: []`, and an
|
|
306
|
+
empty array is truthy — without the guard the cell would render
|
|
307
|
+
as a bare divider + padding. */}
|
|
308
|
+
{!!config.actions?.left?.length && (
|
|
309
|
+
<div className="flex h-full items-center border-r border-ods-border px-[var(--spacing-system-xs)]">
|
|
310
|
+
{config.actions.left}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{/* Mobile/tablet menu toggle — a leading cell per the ODS spec
|
|
314
|
+
(banded with the nav breakpoint `lg` so the desktop nav and the
|
|
315
|
+
toggle never co-show). */}
|
|
316
|
+
{config.mobile?.enabled && (
|
|
317
|
+
<HeaderButton
|
|
318
|
+
className="lg:hidden border-r border-ods-border"
|
|
319
|
+
onClick={() => {
|
|
320
|
+
config.mobile?.onToggle?.();
|
|
321
|
+
}}
|
|
322
|
+
isActive={config.mobile?.isOpen ?? false}
|
|
323
|
+
aria-label={config.mobile?.isOpen ? 'Close menu' : 'Open menu'}
|
|
324
|
+
aria-expanded={config.mobile?.isOpen ?? false}
|
|
325
|
+
// Conditional: the panel unmounts when closed, so an unconditional
|
|
326
|
+
// reference would dangle (axe aria-valid-attr-value).
|
|
327
|
+
aria-controls={config.mobile?.isOpen ? MOBILE_NAV_PANEL_ID : undefined}
|
|
328
|
+
icon={
|
|
329
|
+
config.mobile?.isOpen
|
|
330
|
+
? config.mobile?.closeIcon || <XmarkIcon className="w-4 h-4 md:w-6 md:h-6" />
|
|
331
|
+
: config.mobile?.menuIcon || <Menu01Icon className="w-4 h-4 md:w-6 md:h-6" />
|
|
332
|
+
}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
</>
|
|
336
|
+
}
|
|
337
|
+
logo={
|
|
338
|
+
<Link href={config.logo.href} className="transition-opacity duration-200 hover:opacity-80">
|
|
339
|
+
{config.logo.element}
|
|
340
|
+
</Link>
|
|
341
|
+
}
|
|
342
|
+
logoClassName={hasLeftCells ? 'lg:pl-[var(--spacing-system-l)]' : undefined}
|
|
343
|
+
center={
|
|
344
|
+
hasNav ? (
|
|
345
|
+
<nav
|
|
346
|
+
className={cn(
|
|
347
|
+
'flex items-center gap-2',
|
|
348
|
+
// True centering relative to the bar (not the leftover flex
|
|
349
|
+
// space) — same treatment the pre-unification header had.
|
|
350
|
+
config.navigation?.position === 'center' && 'absolute left-1/2 -translate-x-1/2',
|
|
351
|
+
)}
|
|
352
|
+
role="navigation"
|
|
353
|
+
aria-label="Main navigation"
|
|
354
|
+
>
|
|
355
|
+
{config.navigation?.items.map(renderNavigationItem)}
|
|
356
|
+
</nav>
|
|
357
|
+
) : undefined
|
|
358
|
+
}
|
|
359
|
+
centerClassName={cn(
|
|
360
|
+
config.navigation?.position === 'left' && 'justify-start',
|
|
361
|
+
config.navigation?.position === 'right' && 'justify-end pr-[var(--spacing-system-m)]',
|
|
364
362
|
)}
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
cta={
|
|
364
|
+
hasCta ? (
|
|
365
|
+
<>
|
|
366
|
+
{/* Desktop actions — banded with the nav/burger breakpoint (lg)
|
|
367
|
+
so the desktop right-cluster and the mobile toggle never
|
|
368
|
+
co-show. */}
|
|
369
|
+
{!!config.actions?.right?.length && (
|
|
370
|
+
<div className="hidden lg:flex items-center gap-3">{config.actions.right}</div>
|
|
371
|
+
)}
|
|
372
|
+
{config.actions?.persistent && config.actions.persistent.length > 0 && (
|
|
373
|
+
<div className="flex items-center">{config.actions.persistent}</div>
|
|
374
|
+
)}
|
|
375
|
+
</>
|
|
376
|
+
) : undefined
|
|
377
|
+
}
|
|
378
|
+
ctaClassName="gap-3"
|
|
379
|
+
sideActions={
|
|
380
|
+
config.mingo?.enabled ? (
|
|
381
|
+
<MingoAiButton
|
|
382
|
+
source={config.mingo.source}
|
|
383
|
+
icon={config.mingo.icon}
|
|
384
|
+
label={config.mingo.label}
|
|
385
|
+
className={config.mingo.className}
|
|
386
|
+
/>
|
|
387
|
+
) : undefined
|
|
388
|
+
}
|
|
389
|
+
/>
|
|
367
390
|
</div>
|
|
368
|
-
)
|
|
369
|
-
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -50,6 +50,9 @@ export type { MobileBurgerMenuProps } from './mobile-burger-menu'
|
|
|
50
50
|
export { HeaderButton } from './header-button'
|
|
51
51
|
export type { HeaderButtonProps } from './header-button'
|
|
52
52
|
|
|
53
|
+
export { TopNavigation } from './top-navigation'
|
|
54
|
+
export type { TopNavigationProps, TopNavigationCenterBreakpoint } from './top-navigation'
|
|
55
|
+
|
|
53
56
|
export { HeaderMingoButton } from './header-mingo-button'
|
|
54
57
|
export type { HeaderMingoButtonProps } from './header-mingo-button'
|
|
55
58
|
|