@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.
Files changed (206) hide show
  1. package/dist/{chunk-4KX6OED2.cjs → chunk-2G7UYK35.cjs} +46 -46
  2. package/dist/{chunk-KOWVJD4V.cjs → chunk-2KXTOP6G.cjs} +70 -57
  3. package/dist/{chunk-A75QY6CD.cjs → chunk-2RNB5DTN.cjs} +21 -21
  4. package/dist/{chunk-PN2RKPDF.js → chunk-36BCTE7D.js} +10 -10
  5. package/dist/{chunk-ZSAGLQOD.cjs → chunk-3AHTOSXJ.cjs} +17 -17
  6. package/dist/{chunk-VR2ZLFCP.js → chunk-4IBPRIQH.js} +1 -1
  7. package/dist/{chunk-FCGC42TS.js → chunk-4L6XCMOU.js} +66 -62
  8. package/dist/{chunk-X5MQ7JRP.js → chunk-4UM2ETLM.js} +9 -9
  9. package/dist/{chunk-6NJKKO73.cjs → chunk-6I4ZAFO2.cjs} +15 -15
  10. package/dist/{chunk-RBDP4WDE.js → chunk-6IW7TWPV.js} +6 -6
  11. package/dist/{chunk-MMOODW57.cjs → chunk-6NTPY5QA.cjs} +1887 -94
  12. package/dist/{chunk-QFXRPRFT.js → chunk-6YKAT7NV.js} +3 -3
  13. package/dist/{chunk-HPWWNJE7.cjs → chunk-AOWDNSTM.cjs} +89 -89
  14. package/dist/{chunk-IHERL5V4.cjs → chunk-B73373AG.cjs} +34 -34
  15. package/dist/{chunk-DENB2XJC.js → chunk-CNWOMQQJ.js} +2 -2
  16. package/dist/{chunk-MP2C3LJ4.js → chunk-CUME3VTQ.js} +1 -1
  17. package/dist/{chunk-PDWVPI3S.js → chunk-CZ7Y7UG4.js} +4 -72
  18. package/dist/{chunk-YHM5AWTT.js → chunk-CZSZSDD4.js} +6 -6
  19. package/dist/{chunk-F6I6XZMN.js → chunk-DA5UDTLG.js} +5 -5
  20. package/dist/{chunk-MFFCUVWG.js → chunk-F57ECE2I.js} +1 -1
  21. package/dist/{chunk-CH5232HV.js → chunk-FI6GMDO7.js} +2455 -662
  22. package/dist/{chunk-NDESEPW5.js → chunk-FXJ3WWUB.js} +68 -0
  23. package/dist/{chunk-G5A3PSPV.js → chunk-HDOYSEOJ.js} +1080 -691
  24. package/dist/{chunk-VA5T32XA.js → chunk-I3ST4PLM.js} +2 -2
  25. package/dist/{chunk-5HBHO5TB.js → chunk-JPG7OHEG.js} +1 -1
  26. package/dist/{chunk-YFBM2K6X.cjs → chunk-JRU2CXLB.cjs} +4 -4
  27. package/dist/{chunk-MKXO4Q5P.js → chunk-KUC673HM.js} +383 -142
  28. package/dist/{chunk-JEZKGCZI.cjs → chunk-L6Y6OOKG.cjs} +26 -26
  29. package/dist/{chunk-F6AFPNDF.js → chunk-LFFB6EMB.js} +29 -16
  30. package/dist/{chunk-X72J4ACL.cjs → chunk-LLSUVPQC.cjs} +252 -8
  31. package/dist/{chunk-RKV5B7EU.cjs → chunk-LM7TXMLU.cjs} +72 -4
  32. package/dist/{chunk-6SEANMOU.js → chunk-LNW45N2C.js} +11 -11
  33. package/dist/{chunk-OUY3HHW6.cjs → chunk-LRQTJOF2.cjs} +928 -539
  34. package/dist/{chunk-DW7G4KG3.js → chunk-MR3A4MZV.js} +3 -3
  35. package/dist/{chunk-2YXVWZVF.js → chunk-N45D4ZXQ.js} +959 -715
  36. package/dist/{chunk-Z2VNQQ74.js → chunk-NCPTID44.js} +978 -942
  37. package/dist/{chunk-WA6GUYQ4.cjs → chunk-NSGXARGJ.cjs} +104 -104
  38. package/dist/{chunk-ABU3TGXA.cjs → chunk-OF6GEI5A.cjs} +3 -3
  39. package/dist/{chunk-IKTQ3ZAG.cjs → chunk-OJLR5MEO.cjs} +6 -6
  40. package/dist/{chunk-VESKITRQ.cjs → chunk-P42V6N34.cjs} +736 -732
  41. package/dist/{chunk-OHFS7VOU.cjs → chunk-PLYEKV3N.cjs} +2 -2
  42. package/dist/{chunk-7VVFKJY5.js → chunk-PO33GZVY.js} +1799 -3449
  43. package/dist/{chunk-7LS22BZU.cjs → chunk-QAOFMK2A.cjs} +14 -14
  44. package/dist/{chunk-VHCACDKP.cjs → chunk-RQRX4YC3.cjs} +39 -39
  45. package/dist/{chunk-3MX5S4CF.cjs → chunk-SEXWHIMS.cjs} +899 -2549
  46. package/dist/{chunk-AQWZNP6A.js → chunk-SS5MBHDP.js} +4 -4
  47. package/dist/{chunk-ZSAO2HBW.js → chunk-TDYIREDJ.js} +15 -15
  48. package/dist/{chunk-XOYH2YQE.cjs → chunk-UDFRUKLC.cjs} +2 -2
  49. package/dist/{chunk-TYWLNCNH.cjs → chunk-UVTD52MD.cjs} +14 -82
  50. package/dist/{chunk-N3COXDBH.cjs → chunk-VX4E6YSB.cjs} +41 -5
  51. package/dist/{chunk-2QEUFXNS.cjs → chunk-WCVZCGK5.cjs} +312 -71
  52. package/dist/{chunk-6XIF2Z7I.js → chunk-WMX26HB5.js} +3 -3
  53. package/dist/{chunk-D7BTSVMS.cjs → chunk-Y2GSOUCS.cjs} +2 -2
  54. package/dist/{chunk-4I45Y3DN.cjs → chunk-YBUT4AMG.cjs} +6 -6
  55. package/dist/components/case-studies/index.cjs +30 -32
  56. package/dist/components/case-studies/index.js +14 -16
  57. package/dist/components/chat/entity-cards/case-study-card.d.ts +11 -4
  58. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  59. package/dist/components/chat/index.cjs +11 -11
  60. package/dist/components/chat/index.d.ts +1 -1
  61. package/dist/components/chat/index.d.ts.map +1 -1
  62. package/dist/components/chat/index.js +16 -16
  63. package/dist/components/chat/nav-link-anchor-via-runtime.d.ts +34 -5
  64. package/dist/components/chat/nav-link-anchor-via-runtime.d.ts.map +1 -1
  65. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  66. package/dist/components/contact/index.cjs +12 -14
  67. package/dist/components/contact/index.js +11 -13
  68. package/dist/components/docs/index.cjs +15 -17
  69. package/dist/components/docs/index.js +14 -16
  70. package/dist/components/embeds/index.cjs +12 -14
  71. package/dist/components/embeds/index.js +11 -13
  72. package/dist/components/faq/index.cjs +13 -15
  73. package/dist/components/faq/index.js +12 -14
  74. package/dist/components/features/index.cjs +12 -14
  75. package/dist/components/features/index.js +21 -23
  76. package/dist/components/help-center-pages/index.cjs +118 -120
  77. package/dist/components/help-center-pages/index.js +33 -35
  78. package/dist/components/icons/index.cjs +3 -5
  79. package/dist/components/icons/index.js +9 -11
  80. package/dist/components/icons-v2-generated/index.cjs +3 -5
  81. package/dist/components/icons-v2-generated/index.js +2 -4
  82. package/dist/components/index.cjs +87 -158
  83. package/dist/components/index.d.ts +0 -2
  84. package/dist/components/index.d.ts.map +1 -1
  85. package/dist/components/index.js +331 -402
  86. package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
  87. package/dist/components/meeting-scheduler/index.cjs +62 -64
  88. package/dist/components/meeting-scheduler/index.js +12 -14
  89. package/dist/components/navigation/index.cjs +28 -16
  90. package/dist/components/navigation/index.d.ts +14 -10
  91. package/dist/components/navigation/index.d.ts.map +1 -1
  92. package/dist/components/navigation/index.js +38 -26
  93. package/dist/components/navigation/mingo-ai-button.d.ts +38 -3
  94. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  95. package/dist/components/navigation/mobile-nav-sheet-id.d.ts +3 -0
  96. package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +1 -0
  97. package/dist/components/navigation/mobile-nav-sheet.d.ts +48 -0
  98. package/dist/components/navigation/mobile-nav-sheet.d.ts.map +1 -0
  99. package/dist/components/navigation/nav-item-row.d.ts +27 -0
  100. package/dist/components/navigation/nav-item-row.d.ts.map +1 -0
  101. package/dist/components/navigation/nav-panel.d.ts +43 -0
  102. package/dist/components/navigation/nav-panel.d.ts.map +1 -0
  103. package/dist/components/navigation/navigation-sidebar-item.d.ts +1 -2
  104. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  105. package/dist/components/navigation/site-footer.d.ts +28 -0
  106. package/dist/components/navigation/site-footer.d.ts.map +1 -0
  107. package/dist/components/navigation/site-header.d.ts +61 -0
  108. package/dist/components/navigation/site-header.d.ts.map +1 -0
  109. package/dist/components/navigation/site-nav-link.d.ts +26 -0
  110. package/dist/components/navigation/site-nav-link.d.ts.map +1 -0
  111. package/dist/components/navigation/top-navigation.d.ts +2 -2
  112. package/dist/components/navigation/top-navigation.d.ts.map +1 -1
  113. package/dist/components/onboarding-guides/index.cjs +15 -17
  114. package/dist/components/onboarding-guides/index.js +14 -16
  115. package/dist/components/platform/ToolBadge.d.ts +6 -0
  116. package/dist/components/platform/ToolBadge.d.ts.map +1 -1
  117. package/dist/components/related-content/index.cjs +10 -8
  118. package/dist/components/related-content/index.js +9 -7
  119. package/dist/components/tickets/index.cjs +16 -18
  120. package/dist/components/tickets/index.js +15 -17
  121. package/dist/components/ui/brand-association-card.d.ts +7 -0
  122. package/dist/components/ui/brand-association-card.d.ts.map +1 -1
  123. package/dist/components/ui/index.cjs +14 -14
  124. package/dist/components/ui/index.js +19 -19
  125. package/dist/hooks/index.cjs +19 -11
  126. package/dist/hooks/index.d.ts +1 -0
  127. package/dist/hooks/index.d.ts.map +1 -1
  128. package/dist/hooks/index.js +19 -11
  129. package/dist/hooks/ui/index.d.ts +3 -0
  130. package/dist/hooks/ui/index.d.ts.map +1 -1
  131. package/dist/hooks/ui/use-active-section.d.ts +10 -0
  132. package/dist/hooks/ui/use-active-section.d.ts.map +1 -0
  133. package/dist/hooks/ui/use-nav-menus.d.ts +48 -0
  134. package/dist/hooks/ui/use-nav-menus.d.ts.map +1 -0
  135. package/dist/hooks/ui/use-visitor-os.d.ts +15 -0
  136. package/dist/hooks/ui/use-visitor-os.d.ts.map +1 -0
  137. package/dist/hooks/use-location-hash.d.ts +9 -0
  138. package/dist/hooks/use-location-hash.d.ts.map +1 -0
  139. package/dist/index.cjs +56 -21
  140. package/dist/index.js +231 -196
  141. package/dist/types/case-study.d.ts +9 -0
  142. package/dist/types/case-study.d.ts.map +1 -1
  143. package/dist/types/navigation.d.ts +95 -65
  144. package/dist/types/navigation.d.ts.map +1 -1
  145. package/dist/utils/index.cjs +55 -0
  146. package/dist/utils/index.d.ts +1 -0
  147. package/dist/utils/index.d.ts.map +1 -1
  148. package/dist/utils/index.js +50 -1
  149. package/dist/utils/source-icons.d.ts.map +1 -1
  150. package/dist/utils/visitor-os.d.ts +45 -0
  151. package/dist/utils/visitor-os.d.ts.map +1 -0
  152. package/package.json +1 -1
  153. package/src/components/chat/entity-cards/case-study-card.tsx +115 -5
  154. package/src/components/chat/index.ts +7 -1
  155. package/src/components/chat/nav-link-anchor-via-runtime.tsx +62 -9
  156. package/src/components/chat/utils/icon-library.tsx +35 -1
  157. package/src/components/index.ts +0 -2
  158. package/src/components/loading/page-layout-skeleton.tsx +1 -1
  159. package/src/components/navigation/index.ts +21 -14
  160. package/src/components/navigation/mingo-ai-button.tsx +139 -63
  161. package/src/components/navigation/mobile-nav-sheet-id.ts +2 -0
  162. package/src/components/navigation/mobile-nav-sheet.tsx +241 -0
  163. package/src/components/navigation/nav-item-row.tsx +129 -0
  164. package/src/components/navigation/nav-panel.tsx +196 -0
  165. package/src/components/navigation/navigation-sidebar-item.tsx +1 -2
  166. package/src/components/navigation/site-footer.tsx +256 -0
  167. package/src/components/navigation/site-header.tsx +339 -0
  168. package/src/components/navigation/site-nav-link.tsx +41 -0
  169. package/src/components/navigation/ticket-alerts-button.tsx +2 -2
  170. package/src/components/navigation/top-navigation.tsx +2 -2
  171. package/src/components/platform/ToolBadge.tsx +18 -1
  172. package/src/components/ui/brand-association-card.tsx +25 -3
  173. package/src/hooks/index.ts +1 -0
  174. package/src/hooks/ui/index.ts +3 -0
  175. package/src/hooks/ui/use-active-section.ts +44 -0
  176. package/src/hooks/ui/use-nav-menus.ts +200 -0
  177. package/src/hooks/ui/use-visitor-os.ts +33 -0
  178. package/src/hooks/use-location-hash.ts +34 -0
  179. package/src/styles/chat-animations.css +57 -2
  180. package/src/types/case-study.ts +16 -0
  181. package/src/types/navigation.ts +80 -66
  182. package/src/utils/index.ts +1 -0
  183. package/src/utils/source-icons.ts +13 -0
  184. package/src/utils/visitor-os.ts +84 -0
  185. package/dist/chunk-LMBC5FFP.js +0 -43
  186. package/dist/chunk-O43V3EOS.js +0 -252
  187. package/dist/chunk-QZVSJF2K.cjs +0 -252
  188. package/dist/chunk-RALRPFDW.cjs +0 -43
  189. package/dist/components/footer-waitlist-button.d.ts +0 -29
  190. package/dist/components/footer-waitlist-button.d.ts.map +0 -1
  191. package/dist/components/footer.d.ts +0 -47
  192. package/dist/components/footer.d.ts.map +0 -1
  193. package/dist/components/navigation/client-only-header.d.ts +0 -8
  194. package/dist/components/navigation/client-only-header.d.ts.map +0 -1
  195. package/dist/components/navigation/header-skeleton.d.ts +0 -12
  196. package/dist/components/navigation/header-skeleton.d.ts.map +0 -1
  197. package/dist/components/navigation/header.d.ts +0 -9
  198. package/dist/components/navigation/header.d.ts.map +0 -1
  199. package/dist/components/navigation/mobile-nav-panel.d.ts +0 -10
  200. package/dist/components/navigation/mobile-nav-panel.d.ts.map +0 -1
  201. package/src/components/footer-waitlist-button.tsx +0 -53
  202. package/src/components/footer.tsx +0 -195
  203. package/src/components/navigation/client-only-header.tsx +0 -22
  204. package/src/components/navigation/header-skeleton.tsx +0 -74
  205. package/src/components/navigation/header.tsx +0 -429
  206. 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
- }