@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.
Files changed (212) hide show
  1. package/dist/{chunk-7LFIKJX2.js → chunk-3EZ4HHAP.js} +3 -3
  2. package/dist/{chunk-I7DFUQMN.js → chunk-3NCM4ZQB.js} +17 -18
  3. package/dist/{chunk-4DQMTBMK.cjs → chunk-3PLDQDE6.cjs} +15 -15
  4. package/dist/{chunk-VTASHWER.js → chunk-4CJMQLB5.js} +4 -4
  5. package/dist/{chunk-VR2ZLFCP.js → chunk-4IBPRIQH.js} +1 -1
  6. package/dist/{chunk-ZQFZSFQI.js → chunk-4ILM7W2R.js} +2 -2
  7. package/dist/{chunk-FXENTYE5.js → chunk-6232S532.js} +3 -3
  8. package/dist/{chunk-MMOODW57.cjs → chunk-6NTPY5QA.cjs} +1887 -94
  9. package/dist/{chunk-2JDVL7UV.js → chunk-7GEBFJT2.js} +1829 -3447
  10. package/dist/{chunk-LDPEQGKT.cjs → chunk-7OFPU7WE.cjs} +942 -2560
  11. package/dist/{chunk-L3IC42IF.cjs → chunk-7SCNMP2B.cjs} +57 -58
  12. package/dist/{chunk-2G546OND.js → chunk-BHI6GVC4.js} +1094 -693
  13. package/dist/{chunk-MP2C3LJ4.js → chunk-CUME3VTQ.js} +1 -1
  14. package/dist/{chunk-WUJNYGO2.js → chunk-CZ7Y7UG4.js} +4 -76
  15. package/dist/{chunk-7JIL3WOH.cjs → chunk-DDVBX42E.cjs} +21 -21
  16. package/dist/{chunk-6WQIUQUC.js → chunk-DWPAAWMR.js} +6 -6
  17. package/dist/{chunk-MFFCUVWG.js → chunk-F57ECE2I.js} +1 -1
  18. package/dist/{chunk-CH5232HV.js → chunk-FI6GMDO7.js} +2455 -662
  19. package/dist/{chunk-NDESEPW5.js → chunk-FXJ3WWUB.js} +68 -0
  20. package/dist/{chunk-4KGXLVCF.js → chunk-G6UMNAVI.js} +57 -61
  21. package/dist/{chunk-LTSTC3N5.cjs → chunk-GWQEYPYM.cjs} +728 -732
  22. package/dist/{chunk-N3TO7JV3.cjs → chunk-HZ7CBXPL.cjs} +89 -89
  23. package/dist/{chunk-XATTBO5X.js → chunk-IKJA3Y7Z.js} +2 -2
  24. package/dist/{chunk-5HBHO5TB.js → chunk-JPG7OHEG.js} +1 -1
  25. package/dist/{chunk-AMYY3PNL.cjs → chunk-K6O43WDL.cjs} +46 -46
  26. package/dist/{chunk-IHU5ANUY.js → chunk-KUC673HM.js} +383 -142
  27. package/dist/{chunk-X72J4ACL.cjs → chunk-LLSUVPQC.cjs} +252 -8
  28. package/dist/{chunk-RKV5B7EU.cjs → chunk-LM7TXMLU.cjs} +72 -4
  29. package/dist/{chunk-XJKZ4IKT.cjs → chunk-LVSFKENJ.cjs} +4 -4
  30. package/dist/{chunk-IZGDG5X6.cjs → chunk-M75XEEGT.cjs} +940 -539
  31. package/dist/{chunk-UK62YTH6.cjs → chunk-MB4PNY7C.cjs} +6 -6
  32. package/dist/{chunk-2YXVWZVF.js → chunk-N45D4ZXQ.js} +959 -715
  33. package/dist/{chunk-Z2VNQQ74.js → chunk-NCPTID44.js} +978 -942
  34. package/dist/{chunk-JEYRXT5L.js → chunk-O2RSAA5M.js} +11 -11
  35. package/dist/{chunk-ABU3TGXA.cjs → chunk-OF6GEI5A.cjs} +3 -3
  36. package/dist/{chunk-OHFS7VOU.cjs → chunk-PLYEKV3N.cjs} +2 -2
  37. package/dist/{chunk-5QJDM6UV.js → chunk-PXOQIA5K.js} +5 -5
  38. package/dist/{chunk-VHCACDKP.cjs → chunk-RQRX4YC3.cjs} +39 -39
  39. package/dist/{chunk-HFJW24JJ.cjs → chunk-RTIPR53L.cjs} +104 -104
  40. package/dist/{chunk-262YQFOS.cjs → chunk-SEKYEWQG.cjs} +26 -26
  41. package/dist/{chunk-SYXQLETR.cjs → chunk-STHZBOQI.cjs} +17 -17
  42. package/dist/{chunk-MI3JUJ75.cjs → chunk-SW5ZV2BN.cjs} +34 -34
  43. package/dist/{chunk-QVSFBSWV.js → chunk-T5TMCHWM.js} +9 -9
  44. package/dist/{chunk-ERCV6HHJ.js → chunk-TE773HEH.js} +10 -10
  45. package/dist/{chunk-C7XGRLT5.cjs → chunk-TQ4LB2DS.cjs} +14 -14
  46. package/dist/{chunk-XOYH2YQE.cjs → chunk-UDFRUKLC.cjs} +2 -2
  47. package/dist/{chunk-XIFOBQRQ.cjs → chunk-UJ33ZXDT.cjs} +6 -6
  48. package/dist/{chunk-C3BWIU7R.cjs → chunk-UVTD52MD.cjs} +14 -86
  49. package/dist/{chunk-N3COXDBH.cjs → chunk-VX4E6YSB.cjs} +41 -5
  50. package/dist/{chunk-HJAYVKWQ.js → chunk-VZICIQW2.js} +15 -15
  51. package/dist/{chunk-MGGGBHYD.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-NRLJUBCS.js → chunk-YGJKKDDM.js} +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/chat-input.d.ts.map +1 -1
  58. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  59. package/dist/components/chat/embeddable-chat.d.ts +8 -0
  60. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  61. package/dist/components/chat/entity-cards/case-study-card.d.ts +11 -4
  62. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  63. package/dist/components/chat/index.cjs +11 -11
  64. package/dist/components/chat/index.d.ts +1 -1
  65. package/dist/components/chat/index.d.ts.map +1 -1
  66. package/dist/components/chat/index.js +16 -16
  67. package/dist/components/chat/mingo-chat-rail.d.ts +3 -1
  68. package/dist/components/chat/mingo-chat-rail.d.ts.map +1 -1
  69. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  70. package/dist/components/contact/index.cjs +12 -14
  71. package/dist/components/contact/index.js +11 -13
  72. package/dist/components/docs/index.cjs +15 -17
  73. package/dist/components/docs/index.js +14 -16
  74. package/dist/components/embeds/index.cjs +12 -14
  75. package/dist/components/embeds/index.js +11 -13
  76. package/dist/components/faq/index.cjs +13 -15
  77. package/dist/components/faq/index.js +12 -14
  78. package/dist/components/features/index.cjs +12 -14
  79. package/dist/components/features/index.js +21 -23
  80. package/dist/components/help-center-pages/index.cjs +118 -120
  81. package/dist/components/help-center-pages/index.js +33 -35
  82. package/dist/components/icons/index.cjs +3 -5
  83. package/dist/components/icons/index.js +9 -11
  84. package/dist/components/icons-v2-generated/index.cjs +3 -5
  85. package/dist/components/icons-v2-generated/index.js +2 -4
  86. package/dist/components/index.cjs +89 -158
  87. package/dist/components/index.d.ts +0 -2
  88. package/dist/components/index.d.ts.map +1 -1
  89. package/dist/components/index.js +333 -402
  90. package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
  91. package/dist/components/meeting-scheduler/index.cjs +62 -64
  92. package/dist/components/meeting-scheduler/index.js +12 -14
  93. package/dist/components/navigation/app-layout-side-panel.d.ts +18 -1
  94. package/dist/components/navigation/app-layout-side-panel.d.ts.map +1 -1
  95. package/dist/components/navigation/index.cjs +30 -16
  96. package/dist/components/navigation/index.d.ts +15 -10
  97. package/dist/components/navigation/index.d.ts.map +1 -1
  98. package/dist/components/navigation/index.js +40 -26
  99. package/dist/components/navigation/mingo-ai-button.d.ts +38 -3
  100. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  101. package/dist/components/navigation/mobile-nav-sheet-id.d.ts +3 -0
  102. package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +1 -0
  103. package/dist/components/navigation/mobile-nav-sheet.d.ts +48 -0
  104. package/dist/components/navigation/mobile-nav-sheet.d.ts.map +1 -0
  105. package/dist/components/navigation/nav-item-row.d.ts +27 -0
  106. package/dist/components/navigation/nav-item-row.d.ts.map +1 -0
  107. package/dist/components/navigation/nav-panel.d.ts +43 -0
  108. package/dist/components/navigation/nav-panel.d.ts.map +1 -0
  109. package/dist/components/navigation/navigation-sidebar-item.d.ts +1 -2
  110. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  111. package/dist/components/navigation/site-footer.d.ts +28 -0
  112. package/dist/components/navigation/site-footer.d.ts.map +1 -0
  113. package/dist/components/navigation/site-header.d.ts +61 -0
  114. package/dist/components/navigation/site-header.d.ts.map +1 -0
  115. package/dist/components/navigation/site-nav-link.d.ts +26 -0
  116. package/dist/components/navigation/site-nav-link.d.ts.map +1 -0
  117. package/dist/components/navigation/top-navigation.d.ts +2 -2
  118. package/dist/components/navigation/top-navigation.d.ts.map +1 -1
  119. package/dist/components/onboarding-guides/index.cjs +15 -17
  120. package/dist/components/onboarding-guides/index.js +14 -16
  121. package/dist/components/related-content/index.cjs +10 -8
  122. package/dist/components/related-content/index.js +9 -7
  123. package/dist/components/tickets/index.cjs +16 -18
  124. package/dist/components/tickets/index.js +15 -17
  125. package/dist/components/ui/index.cjs +14 -14
  126. package/dist/components/ui/index.js +19 -19
  127. package/dist/hooks/index.cjs +19 -11
  128. package/dist/hooks/index.d.ts +1 -0
  129. package/dist/hooks/index.d.ts.map +1 -1
  130. package/dist/hooks/index.js +19 -11
  131. package/dist/hooks/ui/index.d.ts +3 -0
  132. package/dist/hooks/ui/index.d.ts.map +1 -1
  133. package/dist/hooks/ui/use-active-section.d.ts +10 -0
  134. package/dist/hooks/ui/use-active-section.d.ts.map +1 -0
  135. package/dist/hooks/ui/use-nav-menus.d.ts +48 -0
  136. package/dist/hooks/ui/use-nav-menus.d.ts.map +1 -0
  137. package/dist/hooks/ui/use-visitor-os.d.ts +15 -0
  138. package/dist/hooks/ui/use-visitor-os.d.ts.map +1 -0
  139. package/dist/hooks/use-location-hash.d.ts +9 -0
  140. package/dist/hooks/use-location-hash.d.ts.map +1 -0
  141. package/dist/index.cjs +58 -21
  142. package/dist/index.js +233 -196
  143. package/dist/types/case-study.d.ts +9 -0
  144. package/dist/types/case-study.d.ts.map +1 -1
  145. package/dist/types/navigation.d.ts +95 -65
  146. package/dist/types/navigation.d.ts.map +1 -1
  147. package/dist/utils/index.cjs +55 -0
  148. package/dist/utils/index.d.ts +1 -0
  149. package/dist/utils/index.d.ts.map +1 -1
  150. package/dist/utils/index.js +50 -1
  151. package/dist/utils/source-icons.d.ts.map +1 -1
  152. package/dist/utils/visitor-os.d.ts +45 -0
  153. package/dist/utils/visitor-os.d.ts.map +1 -0
  154. package/package.json +1 -1
  155. package/src/components/chat/chat-input.tsx +5 -1
  156. package/src/components/chat/chat-message-enhanced.tsx +4 -3
  157. package/src/components/chat/embeddable-chat.tsx +64 -6
  158. package/src/components/chat/entity-cards/case-study-card.tsx +115 -5
  159. package/src/components/chat/index.ts +1 -0
  160. package/src/components/chat/mingo-chat-rail.tsx +24 -7
  161. package/src/components/chat/utils/icon-library.tsx +35 -1
  162. package/src/components/index.ts +0 -2
  163. package/src/components/loading/page-layout-skeleton.tsx +1 -1
  164. package/src/components/navigation/app-layout-side-panel.tsx +30 -2
  165. package/src/components/navigation/index.ts +22 -14
  166. package/src/components/navigation/mingo-ai-button.tsx +139 -63
  167. package/src/components/navigation/mobile-nav-sheet-id.ts +2 -0
  168. package/src/components/navigation/mobile-nav-sheet.tsx +241 -0
  169. package/src/components/navigation/nav-item-row.tsx +129 -0
  170. package/src/components/navigation/nav-panel.tsx +196 -0
  171. package/src/components/navigation/navigation-sidebar-item.tsx +1 -2
  172. package/src/components/navigation/site-footer.tsx +256 -0
  173. package/src/components/navigation/site-header.tsx +339 -0
  174. package/src/components/navigation/site-nav-link.tsx +41 -0
  175. package/src/components/navigation/ticket-alerts-button.tsx +2 -2
  176. package/src/components/navigation/top-navigation.tsx +2 -2
  177. package/src/components/platform/SoftwareSourceBadge.tsx +1 -1
  178. package/src/hooks/index.ts +1 -0
  179. package/src/hooks/ui/index.ts +3 -0
  180. package/src/hooks/ui/use-active-section.ts +44 -0
  181. package/src/hooks/ui/use-nav-menus.ts +200 -0
  182. package/src/hooks/ui/use-visitor-os.ts +33 -0
  183. package/src/hooks/use-location-hash.ts +34 -0
  184. package/src/styles/app-globals.css +13 -0
  185. package/src/styles/chat-animations.css +57 -2
  186. package/src/types/case-study.ts +16 -0
  187. package/src/types/navigation.ts +80 -66
  188. package/src/utils/index.ts +1 -0
  189. package/src/utils/source-icons.ts +13 -0
  190. package/src/utils/visitor-os.ts +84 -0
  191. package/dist/chunk-LMBC5FFP.js +0 -43
  192. package/dist/chunk-O43V3EOS.js +0 -252
  193. package/dist/chunk-QZVSJF2K.cjs +0 -252
  194. package/dist/chunk-RALRPFDW.cjs +0 -43
  195. package/dist/components/footer-waitlist-button.d.ts +0 -29
  196. package/dist/components/footer-waitlist-button.d.ts.map +0 -1
  197. package/dist/components/footer.d.ts +0 -47
  198. package/dist/components/footer.d.ts.map +0 -1
  199. package/dist/components/navigation/client-only-header.d.ts +0 -8
  200. package/dist/components/navigation/client-only-header.d.ts.map +0 -1
  201. package/dist/components/navigation/header-skeleton.d.ts +0 -12
  202. package/dist/components/navigation/header-skeleton.d.ts.map +0 -1
  203. package/dist/components/navigation/header.d.ts +0 -9
  204. package/dist/components/navigation/header.d.ts.map +0 -1
  205. package/dist/components/navigation/mobile-nav-panel.d.ts +0 -10
  206. package/dist/components/navigation/mobile-nav-panel.d.ts.map +0 -1
  207. package/src/components/footer-waitlist-button.tsx +0 -53
  208. package/src/components/footer.tsx +0 -195
  209. package/src/components/navigation/client-only-header.tsx +0 -22
  210. package/src/components/navigation/header-skeleton.tsx +0 -74
  211. package/src/components/navigation/header.tsx +0 -429
  212. package/src/components/navigation/mobile-nav-panel.tsx +0 -155
@@ -0,0 +1,339 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import Link from '../../embed-shims/next-link';
6
+ import { useActiveSection } from '../../hooks/ui/use-active-section';
7
+ import { useNavMenus } from '../../hooks/ui/use-nav-menus';
8
+ import type { NavMenu, SiteNav } from '../../types/navigation';
9
+ import { cn } from '../../utils/cn';
10
+ import { EntityIcon } from '../icon-display';
11
+ import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
12
+ import { Menu01Icon } from '../icons-v2-generated/interface/menu-01-icon';
13
+ import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';
14
+ import { Button } from '../ui/button';
15
+ import { HeaderButton } from './header-button';
16
+ import { MingoAiButton } from './mingo-ai-button';
17
+ import { MOBILE_NAV_SHEET_ID } from './mobile-nav-sheet-id';
18
+ import { isMegaMenu, NavPanel, type NavPanelMotion } from './nav-panel';
19
+ import { defaultRenderSiteNavLink, NAV_FOCUS_CLASS, type SiteNavLinkRenderer } from './site-nav-link';
20
+ import { TopNavigation } from './top-navigation';
21
+
22
+ export interface SiteHeaderProps {
23
+ nav: SiteNav;
24
+ /** The host router's pathname: marks the active section and closes menus on navigation. */
25
+ pathname: string;
26
+ logo: React.ReactNode;
27
+ /** Omitted: the logo is not a link. */
28
+ logoHref?: string;
29
+ /** The host's link (Next `Link`, unified navigation). Default: `<a>`. */
30
+ renderLink?: SiteNavLinkRenderer;
31
+ /** Leading cells (admin sidebar toggle). Each cell owns its divider. */
32
+ leading?: React.ReactNode;
33
+ /** Host actions at the right (profile button, custom buttons). Shown at every width. */
34
+ actions?: React.ReactNode;
35
+ /** The primary CTA node when `nav.primaryCta` is `'trial'` or `'waitlist'`. */
36
+ cta?: React.ReactNode;
37
+ /** Trailing cells (ticket alerts). Each cell owns its divider. */
38
+ sideActions?: React.ReactNode;
39
+ /**
40
+ * The Mingo AI launcher, first in the right cluster (before Sign in).
41
+ * Omitted: no launcher. `icon` and `label` are the server-configured
42
+ * assistant identity (the same ones the chat panel shows); `source` scopes
43
+ * the `ask-ai:open` event the launcher dispatches.
44
+ */
45
+ mingo?: { source?: string; icon?: React.ReactNode; label?: string; shortcutHint?: boolean };
46
+ /** Omitted: no burger. */
47
+ mobile?: { isOpen: boolean; onToggle: () => void; controlsId?: string };
48
+ /** Values for `NavLink.badgeKey`. */
49
+ badges?: Record<string, number>;
50
+ /** Rendered under a menu with `showSocial`. */
51
+ social?: React.ReactNode;
52
+ autoHide?: boolean;
53
+ /** Opaque ODS background class. */
54
+ backgroundClassName?: string;
55
+ className?: string;
56
+ style?: React.CSSProperties;
57
+ /** The id the "Skip to content" link targets. Default `main-content`. */
58
+ skipToContentId?: string;
59
+ }
60
+
61
+ // A top-level item (prototype `.nv-l`): 40px tall, quiet until hovered, open
62
+ // or current.
63
+ const TOP_ITEM_CLASS =
64
+ 'relative inline-flex h-10 items-center gap-1.5 whitespace-nowrap rounded-md px-3.5 text-ods-text-secondary transition-colors text-h6 hover:bg-ods-bg-hover hover:text-ods-text-primary';
65
+ // The current section: a 2px accent underline and an accent wash.
66
+ const TOP_ITEM_CURRENT_CLASS =
67
+ 'rounded-b-none bg-gradient-to-b from-transparent to-ods-accent/10 text-ods-text-primary after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-ods-accent';
68
+
69
+ /** A top-level item's optional glyph (`NavMenu.iconName`), before its label. */
70
+ const menuIcon = (menu: NavMenu) =>
71
+ menu.iconName ? <EntityIcon icon={{ name: menu.iconName }} size={20} className="size-5 shrink-0" /> : null;
72
+
73
+ /**
74
+ * The site header of every platform, drawn from one `SiteNav`: logo, the
75
+ * menus left-aligned after it, then the Mingo AI launcher, Sign in, the host's
76
+ * actions, the primary CTA (the only button) and the trailing cells. A
77
+ * platform differs only in the data and the slots it passes.
78
+ *
79
+ * Below `lg` the menus and Sign in leave the bar (they live in
80
+ * `MobileNavSheet`); the host's actions, the CTA and the launcher stay.
81
+ */
82
+ export function SiteHeader({
83
+ nav,
84
+ pathname,
85
+ logo,
86
+ logoHref,
87
+ renderLink = defaultRenderSiteNavLink,
88
+ leading,
89
+ actions,
90
+ cta,
91
+ sideActions,
92
+ mingo,
93
+ mobile,
94
+ badges,
95
+ social,
96
+ autoHide = false,
97
+ backgroundClassName,
98
+ className,
99
+ style,
100
+ skipToContentId = 'main-content',
101
+ }: SiteHeaderProps) {
102
+ const [show, setShow] = useState(true);
103
+ // Not state: nothing renders the previous offset, and as `useState` it
104
+ // re-rendered the whole header on every scroll event just to store a number.
105
+ const lastScrollY = useRef(0);
106
+
107
+ const activeId = useActiveSection(nav.menus, pathname);
108
+ const { openId, close, getTriggerProps, getPanelProps } = useNavMenus({ pathname });
109
+
110
+ // The menu that was open before this one, kept during render (no effect, no
111
+ // late frame): it tells an opening mega menu which side its content comes from.
112
+ const [trail, setTrail] = useState<{ id: string | null; from: string | null }>({ id: null, from: null });
113
+ if (trail.id !== openId) setTrail({ id: openId, from: trail.id });
114
+ const previousId = trail.id === openId ? trail.from : trail.id;
115
+ const megaIds = nav.menus.filter(isMegaMenu).map(menu => menu.id);
116
+ const openMegaIndex = openId === null ? -1 : megaIds.indexOf(openId);
117
+ const previousMegaIndex = previousId === null ? -1 : megaIds.indexOf(previousId);
118
+ const megaMotion: NavPanelMotion =
119
+ openMegaIndex < 0 || previousMegaIndex < 0 ? 'open' : openMegaIndex > previousMegaIndex ? 'from-end' : 'from-start';
120
+
121
+ useEffect(() => {
122
+ // Only add the scroll listener while auto-hide is on. Nothing to reset
123
+ // here: `show` is masked by `autoHide` where it is read (`headerShown`), so
124
+ // turning auto-hide off can never leave the header stuck off-screen.
125
+ if (!autoHide) return undefined;
126
+
127
+ const handleScroll = () => {
128
+ const currentScrollY = window.scrollY;
129
+ const prevScrollY = lastScrollY.current;
130
+ lastScrollY.current = currentScrollY;
131
+
132
+ if (currentScrollY > prevScrollY && currentScrollY > 50) setShow(false);
133
+ else if (currentScrollY < prevScrollY || currentScrollY <= 10) setShow(true);
134
+ };
135
+
136
+ window.addEventListener('scroll', handleScroll, { passive: true });
137
+ return () => window.removeEventListener('scroll', handleScroll);
138
+ }, [autoHide]);
139
+
140
+ // An open menu holds the header in place: hiding the bar would take the
141
+ // panel the visitor is reading with it.
142
+ const headerShown = !autoHide || show || openId !== null;
143
+
144
+ const renderMenu = (menu: NavMenu) => {
145
+ const current = menu.id === activeId;
146
+ const hasPanel = !!menu.columns?.length || !!menu.features?.length || !!menu.sideLinks?.length;
147
+
148
+ if (!hasPanel) {
149
+ if (!menu.href) return null;
150
+ return (
151
+ <div key={menu.id} className="flex">
152
+ <Button
153
+ asChild
154
+ variant="transparent"
155
+ size="wrap"
156
+ font="regular"
157
+ className={cn(TOP_ITEM_CLASS, NAV_FOCUS_CLASS, current && TOP_ITEM_CURRENT_CLASS)}
158
+ >
159
+ {renderLink({
160
+ link: { href: menu.href },
161
+ 'aria-current': current ? 'page' : undefined,
162
+ children: (
163
+ <>
164
+ {menuIcon(menu)}
165
+ {menu.label}
166
+ </>
167
+ ),
168
+ })}
169
+ </Button>
170
+ </div>
171
+ );
172
+ }
173
+
174
+ const open = openId === menu.id;
175
+ return (
176
+ // A dropdown is anchored to its trigger (`relative`); a mega menu to the
177
+ // bar itself, so its wrapper stays unpositioned.
178
+ <div key={menu.id} className={cn('flex', !isMegaMenu(menu) && 'relative')}>
179
+ <Button
180
+ type="button"
181
+ variant="transparent"
182
+ size="wrap"
183
+ font="regular"
184
+ {...getTriggerProps(menu.id)}
185
+ aria-current={current ? 'page' : undefined}
186
+ className={cn(
187
+ TOP_ITEM_CLASS,
188
+ NAV_FOCUS_CLASS,
189
+ open && 'bg-ods-bg-hover text-ods-text-primary',
190
+ current && TOP_ITEM_CURRENT_CLASS,
191
+ )}
192
+ >
193
+ {menuIcon(menu)}
194
+ {menu.label}
195
+ <Chevron02DownIcon
196
+ aria-hidden="true"
197
+ // `!`: the Button sizes every glyph inside it at 20px; the chevron is 16.
198
+ className={cn('!h-4 !w-4 transition-transform duration-150', open && 'rotate-180')}
199
+ />
200
+ </Button>
201
+ <NavPanel
202
+ {...getPanelProps(menu.id)}
203
+ menu={menu}
204
+ open={open}
205
+ motion={open ? megaMotion : undefined}
206
+ handoff={openMegaIndex >= 0}
207
+ renderLink={renderLink}
208
+ badges={badges}
209
+ social={social}
210
+ onNavigate={close}
211
+ />
212
+ </div>
213
+ );
214
+ };
215
+
216
+ const primaryCta =
217
+ typeof nav.primaryCta === 'object' ? (
218
+ <Button variant="accent" href={nav.primaryCta.href}>
219
+ {nav.primaryCta.label}
220
+ </Button>
221
+ ) : nav.primaryCta === 'none' ? null : (
222
+ (cta ?? null)
223
+ );
224
+ const hasRight = !!mingo || !!nav.signIn || !!actions || !!primaryCta;
225
+
226
+ return (
227
+ <div
228
+ // Hook point for page-scoped header CSS (a full-screen deck hides the bar).
229
+ data-site-header=""
230
+ className="sticky top-0 z-[50] w-full transition-transform duration-300 ease-in-out"
231
+ style={{ transform: headerShown ? 'translateY(0)' : 'translateY(-100%)' }}
232
+ >
233
+ {/* First tab stop of the page; visible only while focused. */}
234
+ <Link
235
+ href={`#${skipToContentId}`}
236
+ className={cn(
237
+ 'sr-only left-[var(--spacing-system-lf)] top-3 z-[70] rounded-md bg-ods-text-primary px-[var(--spacing-system-mf)] py-[var(--spacing-system-xsf)] text-ods-bg text-h6 focus:not-sr-only focus:absolute',
238
+ NAV_FOCUS_CLASS,
239
+ )}
240
+ >
241
+ Skip to content
242
+ </Link>
243
+ {/* Unified ODS top-navigation shell (Figma 2797-5978), cell model with
244
+ per-cell dividers. NOTE: no `backdrop-blur` anywhere in this bar.
245
+ Every platform ships an OPAQUE header background, so a
246
+ backdrop-filter would blur a backdrop that is then fully painted
247
+ over: zero visual effect, but the browser still re-rasterizes the
248
+ strip behind this sticky bar every scroll frame and content flickers
249
+ as it passes under the header. A translucent "glass" header needs
250
+ the blur and a translucent `backgroundClassName` together. */}
251
+ <TopNavigation
252
+ // `relative` anchors a mega menu to the bar: it opens full width
253
+ // right under it.
254
+ className={cn('relative', className)}
255
+ style={style}
256
+ backgroundClassName={backgroundClassName}
257
+ centerBreakpoint="lg"
258
+ size="big"
259
+ leading={
260
+ <>
261
+ {leading}
262
+ {/* Menu toggle: a leading cell, banded with the menus' breakpoint
263
+ (`lg`) so the desktop menus and the toggle never co-show. */}
264
+ {mobile && (
265
+ <HeaderButton
266
+ className="border-r border-ods-border lg:hidden"
267
+ onClick={mobile.onToggle}
268
+ isActive={mobile.isOpen}
269
+ aria-label={mobile.isOpen ? 'Close menu' : 'Open menu'}
270
+ aria-expanded={mobile.isOpen}
271
+ // Conditional: the sheet unmounts when closed, so an
272
+ // unconditional reference would dangle (axe aria-valid-attr-value).
273
+ aria-controls={mobile.isOpen ? (mobile.controlsId ?? MOBILE_NAV_SHEET_ID) : undefined}
274
+ icon={mobile.isOpen ? <XmarkIcon className="h-6 w-6" /> : <Menu01Icon className="h-6 w-6" />}
275
+ />
276
+ )}
277
+ </>
278
+ }
279
+ logo={
280
+ logoHref
281
+ ? renderLink({
282
+ link: { href: logoHref },
283
+ className: cn(
284
+ 'flex min-w-0 items-center rounded-md transition-opacity duration-200 hover:opacity-80',
285
+ NAV_FOCUS_CLASS,
286
+ ),
287
+ 'aria-label': nav.brand.name,
288
+ children: logo,
289
+ })
290
+ : logo
291
+ }
292
+ // Big-bar rule (Figma 2936-6812): a fixed 24px left inset on the logo
293
+ // zone at every breakpoint, which also reads as the gap between a
294
+ // leading cell and the logo. `shrink` lets the logo, never the CTA,
295
+ // give way on the narrowest phones.
296
+ logoClassName="shrink pl-[var(--spacing-system-lf)] md:pl-[var(--spacing-system-lf)] lg:pl-[var(--spacing-system-lf)]"
297
+ center={
298
+ nav.menus.length > 0 ? (
299
+ <nav aria-label="Main" className="flex items-center gap-[var(--spacing-system-xxs)]">
300
+ {nav.menus.map(renderMenu)}
301
+ </nav>
302
+ ) : undefined
303
+ }
304
+ centerClassName="justify-start pl-[var(--spacing-system-xlf)]"
305
+ cta={
306
+ hasRight ? (
307
+ <>
308
+ {mingo && (
309
+ <MingoAiButton
310
+ source={mingo.source}
311
+ icon={mingo.icon}
312
+ label={mingo.label}
313
+ shortcutHint={mingo.shortcutHint}
314
+ />
315
+ )}
316
+ {nav.signIn && (
317
+ <div className="hidden lg:flex">
318
+ <Button
319
+ asChild
320
+ variant="transparent"
321
+ size="wrap"
322
+ font="regular"
323
+ className={cn(TOP_ITEM_CLASS, NAV_FOCUS_CLASS)}
324
+ >
325
+ {renderLink({ link: nav.signIn, children: nav.signIn.label })}
326
+ </Button>
327
+ </div>
328
+ )}
329
+ {actions}
330
+ {primaryCta}
331
+ </>
332
+ ) : undefined
333
+ }
334
+ ctaClassName="gap-[var(--spacing-system-xsf)]"
335
+ sideActions={sideActions}
336
+ />
337
+ </div>
338
+ );
339
+ }
@@ -0,0 +1,41 @@
1
+ import type React from 'react';
2
+ import Link from '../../embed-shims/next-link';
3
+ import type { NavLink } from '../../types/navigation';
4
+
5
+ /**
6
+ * How a host renders one navigation link (Next `Link`, a unified-navigation
7
+ * anchor). Every site navigation component takes one; omitted, links go
8
+ * through {@link defaultRenderSiteNavLink}.
9
+ */
10
+ export type SiteNavLinkRenderer = (props: {
11
+ link: Pick<NavLink, 'href' | 'external'>;
12
+ className?: string;
13
+ children: React.ReactNode;
14
+ onClick?: (e: React.MouseEvent) => void;
15
+ 'aria-current'?: 'page';
16
+ 'aria-label'?: string;
17
+ }) => React.ReactNode;
18
+
19
+ /**
20
+ * The default link: the library's `Link` shim (a plain `<a>`, or the host's
21
+ * registered router link, so internal links soft-navigate), opened in a new
22
+ * tab for a link that leaves the site.
23
+ */
24
+ export const defaultRenderSiteNavLink: SiteNavLinkRenderer = ({ link, children, ...rest }) =>
25
+ link.external ? (
26
+ <Link href={link.href} target="_blank" rel="noopener noreferrer" {...rest}>
27
+ {children}
28
+ </Link>
29
+ ) : (
30
+ <Link href={link.href} {...rest}>
31
+ {children}
32
+ </Link>
33
+ );
34
+
35
+ /** A link's label, with its `{count}` token filled from `link.count`. */
36
+ export function navLinkLabel(link: Pick<NavLink, 'label' | 'count'>): string {
37
+ return link.count === undefined ? link.label : link.label.replace('{count}', String(link.count));
38
+ }
39
+
40
+ /** Focus ring shared by every navigation link and trigger. */
41
+ export const NAV_FOCUS_CLASS = 'outline-none focus-visible:ring-2 focus-visible:ring-ods-accent';
@@ -2,8 +2,8 @@
2
2
 
3
3
  /**
4
4
  * TicketAlertsButton — the unified header affordance for support-ticket
5
- * unread indication. Mounted by BOTH header shells (hub `Header` via
6
- * `HeaderConfig.tickets`, console `AppHeader` via `showTicketAlerts`).
5
+ * unread indication. Mounted by BOTH header shells (the `SiteHeader` through its
6
+ * `sideActions` slot, console `AppHeader` via `showTicketAlerts`).
7
7
  *
8
8
  * ATTENTION-ONLY element, deliberately unlike the always-on header cells:
9
9
  * - renders NOTHING unless there are unread support replies (and a
@@ -59,7 +59,7 @@ export interface TopNavigationProps extends React.HTMLAttributes<HTMLElement> {
59
59
  sideActions?: React.ReactNode;
60
60
  /** Opaque ODS background class. Defaults to `bg-ods-card`. Keep it opaque —
61
61
  * translucent backgrounds under a sticky bar need a deliberate
62
- * backdrop-blur treatment (see the note in `header.tsx`). */
62
+ * backdrop-blur treatment (see the note in `site-header.tsx`). */
63
63
  backgroundClassName?: string;
64
64
  /** Top border on mobile (the ODS spec bar shows it below md). Default true. */
65
65
  mobileTopBorder?: boolean;
@@ -73,7 +73,7 @@ const hasContent = (node: React.ReactNode): boolean => node !== null && node !==
73
73
  /**
74
74
  * Unified ODS top-navigation bar (Figma `[UPD] top-navigation`, node
75
75
  * 2797-5978): the single 48px/56px (mobile / md+) cell-based header shell
76
- * shared by the console `AppHeader` and the marketing `Header` across all
76
+ * shared by the console `AppHeader` and the `SiteHeader` across all
77
77
  * platforms.
78
78
  *
79
79
  * The shell owns the bar geometry only — height, borders, background, and the
@@ -33,7 +33,7 @@
33
33
  */
34
34
 
35
35
  import type React from 'react';
36
- import { cn } from '../../utils/common';
36
+ import { cn } from '../../utils/cn';
37
37
  import { PackageIcon, WindowsIcon, MacOSIcon, LinuxIcon } from '../icons';
38
38
 
39
39
  export type SoftwareSource =
@@ -39,6 +39,7 @@ export * from './use-claude-mirror-src';
39
39
  // Deep-link "scroll to URL hash" after data loads. Pairs with URL
40
40
  // composers that emit `?<filter>=<id>#<prefix>-<id>` — the filter
41
41
  // narrows the list, the hash scrolls the matching DOM id.
42
+ export * from './use-location-hash';
42
43
  export * from './use-scroll-to-hash';
43
44
 
44
45
  // Invisible bot-protection client primitive (honeypot ref + submit-timing).
@@ -31,3 +31,6 @@ export { usePreventScroll } from '@react-aria/overlays';
31
31
  export * from './use-prefers-reduced-motion';
32
32
  export * from './use-in-view';
33
33
  export * from './use-autoplay';
34
+ export * from './use-active-section';
35
+ export * from './use-nav-menus';
36
+ export * from './use-visitor-os';
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import type { NavMenu } from '../../types/navigation';
5
+
6
+ /** The path of an internal href, without its query or `#anchor`. `null` for an
7
+ * absolute URL: another site is never the current section. */
8
+ function pathOf(href: string | undefined): string | null {
9
+ if (!href || !href.startsWith('/') || href.startsWith('//')) return null;
10
+ const path = href.split(/[?#]/)[0];
11
+ return path.length > 1 ? path.replace(/\/+$/, '') : '/';
12
+ }
13
+
14
+ /** Boundary-aware prefix test: `/pricing` holds `/pricing` and `/pricing/x`,
15
+ * never `/pricing-old`; `/` holds only `/`. */
16
+ function prefixHolds(prefix: string, pathname: string): boolean {
17
+ if (prefix === '/') return pathname === '/';
18
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
19
+ }
20
+
21
+ /**
22
+ * Pure rule behind {@link useActiveSection}: the id of the menu whose `match`
23
+ * prefixes (else its `href`) hold the pathname. The longest prefix wins, the
24
+ * first menu on a tie; `null` when none holds it.
25
+ */
26
+ export function resolveActiveSection(menus: readonly NavMenu[], pathname: string | null | undefined): string | null {
27
+ const current = pathOf(pathname ?? undefined);
28
+ if (!current) return null;
29
+ let best: { id: string; length: number } | null = null;
30
+ for (const menu of menus) {
31
+ const prefixes = menu.match?.length ? menu.match : [menu.href];
32
+ for (const raw of prefixes) {
33
+ const prefix = pathOf(raw);
34
+ if (!prefix || !prefixHolds(prefix, current)) continue;
35
+ if (!best || prefix.length > best.length) best = { id: menu.id, length: prefix.length };
36
+ }
37
+ }
38
+ return best?.id ?? null;
39
+ }
40
+
41
+ /** The id of the top-level menu the current page belongs to, or `null`. */
42
+ export function useActiveSection(menus: readonly NavMenu[], pathname: string | null | undefined): string | null {
43
+ return useMemo(() => resolveActiveSection(menus, pathname), [menus, pathname]);
44
+ }
@@ -0,0 +1,200 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useRef,
8
+ useState,
9
+ type MouseEvent as ReactMouseEvent,
10
+ type RefCallback,
11
+ } from 'react';
12
+
13
+ export interface UseNavMenusOptions {
14
+ /** The host router's pathname: any change closes the open menu. */
15
+ pathname: string;
16
+ /** Hover intent: how long the pointer rests on a trigger before it opens. */
17
+ hoverOpenMs?: number;
18
+ /** How long after the pointer left the trigger and the panel the menu closes. */
19
+ hoverCloseMs?: number;
20
+ }
21
+
22
+ export interface NavMenuTriggerProps {
23
+ ref: RefCallback<HTMLElement>;
24
+ 'aria-expanded': boolean;
25
+ 'aria-controls': string;
26
+ onClick: (event: ReactMouseEvent) => void;
27
+ onMouseEnter: () => void;
28
+ onMouseLeave: () => void;
29
+ }
30
+
31
+ export interface NavMenuPanelProps {
32
+ ref: RefCallback<HTMLElement>;
33
+ id: string;
34
+ inert: boolean;
35
+ 'data-state': 'open' | 'closed';
36
+ onMouseEnter: () => void;
37
+ onMouseLeave: () => void;
38
+ }
39
+
40
+ export interface UseNavMenusResult {
41
+ /** The id of the open menu, or `null`. One menu is open at a time. */
42
+ openId: string | null;
43
+ close: () => void;
44
+ getTriggerProps: (id: string) => NavMenuTriggerProps;
45
+ getPanelProps: (id: string) => NavMenuPanelProps;
46
+ }
47
+
48
+ /**
49
+ * Open state for a row of disclosure menus (the site header's dropdowns and
50
+ * mega menus).
51
+ *
52
+ * - Click toggles; the trigger is a `<button>`, so Enter and Space arrive as
53
+ * that same click.
54
+ * - Hover on a trigger opens after `hoverOpenMs`; with a menu already open,
55
+ * hovering another trigger switches to it at once. Leaving the trigger and the
56
+ * panel closes after `hoverCloseMs`; entering the panel or any trigger
57
+ * cancels that close, so the pointer can cross the gap between them.
58
+ * - Escape closes and returns focus to the trigger. A pointer-down outside
59
+ * every trigger and panel closes. A `pathname` change closes.
60
+ * - No arrow-key roving: these are plain disclosures, Tab moves in DOM order.
61
+ */
62
+ export function useNavMenus({
63
+ pathname,
64
+ hoverOpenMs = 150,
65
+ hoverCloseMs = 300,
66
+ }: UseNavMenusOptions): UseNavMenusResult {
67
+ const baseId = useId();
68
+ // The open menu is remembered WITH the pathname it was opened on, so a
69
+ // navigation closes it during render: no effect, no frame of a stale panel.
70
+ // The stale entry is dropped in the same render, or coming back to that
71
+ // pathname would reopen it.
72
+ const [opened, setOpened] = useState<{ id: string; pathname: string } | null>(null);
73
+ const stale = opened !== null && opened.pathname !== pathname;
74
+ if (stale) setOpened(null);
75
+ const openId = opened && !stale ? opened.id : null;
76
+
77
+ const triggers = useRef(new Map<string, HTMLElement>());
78
+ const panels = useRef(new Map<string, HTMLElement>());
79
+ const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
80
+ const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
81
+ // The menu a HOVER opened, until a click claims it (see `onClick`).
82
+ const openedByHover = useRef<string | null>(null);
83
+
84
+ const clearTimers = useCallback(() => {
85
+ if (openTimer.current) clearTimeout(openTimer.current);
86
+ if (closeTimer.current) clearTimeout(closeTimer.current);
87
+ openTimer.current = null;
88
+ closeTimer.current = null;
89
+ }, []);
90
+
91
+ useEffect(() => clearTimers, [clearTimers]);
92
+
93
+ const close = useCallback(() => {
94
+ clearTimers();
95
+ setOpened(null);
96
+ }, [clearTimers]);
97
+
98
+ const cancelClose = useCallback(() => {
99
+ if (closeTimer.current) clearTimeout(closeTimer.current);
100
+ closeTimer.current = null;
101
+ }, []);
102
+
103
+ const scheduleClose = useCallback(() => {
104
+ if (openTimer.current) clearTimeout(openTimer.current);
105
+ openTimer.current = null;
106
+ cancelClose();
107
+ closeTimer.current = setTimeout(() => {
108
+ closeTimer.current = null;
109
+ setOpened(null);
110
+ }, hoverCloseMs);
111
+ }, [cancelClose, hoverCloseMs]);
112
+
113
+ useEffect(() => {
114
+ if (!openId) return undefined;
115
+
116
+ const handleKeyDown = (event: KeyboardEvent) => {
117
+ if (event.key !== 'Escape') return;
118
+ const trigger = triggers.current.get(openId);
119
+ close();
120
+ trigger?.focus({ preventScroll: true });
121
+ };
122
+ const handlePointerDown = (event: PointerEvent) => {
123
+ const target = event.target;
124
+ if (!(target instanceof Node)) return;
125
+ for (const el of triggers.current.values()) if (el.contains(target)) return;
126
+ for (const el of panels.current.values()) if (el.contains(target)) return;
127
+ close();
128
+ };
129
+
130
+ document.addEventListener('keydown', handleKeyDown);
131
+ document.addEventListener('pointerdown', handlePointerDown);
132
+ return () => {
133
+ document.removeEventListener('keydown', handleKeyDown);
134
+ document.removeEventListener('pointerdown', handlePointerDown);
135
+ };
136
+ }, [openId, close]);
137
+
138
+ const getTriggerProps = useCallback(
139
+ (id: string): NavMenuTriggerProps => ({
140
+ ref: el => {
141
+ if (el) triggers.current.set(id, el);
142
+ else triggers.current.delete(id);
143
+ },
144
+ 'aria-expanded': openId === id,
145
+ 'aria-controls': `${baseId}-panel-${id}`,
146
+ onClick: () => {
147
+ // A touch tap fires mouseenter first: drop its pending open, or it
148
+ // would reopen the menu this click just closed.
149
+ clearTimers();
150
+ // A click on a menu the pointer's hover already opened means "open
151
+ // it": it keeps the menu open instead of toggling it shut (a mouse
152
+ // user hovers, then clicks; a touch tap fires mouseenter, then click).
153
+ // The click claims the menu, so the next click on it closes it.
154
+ const hovered = openedByHover.current === id;
155
+ openedByHover.current = null;
156
+ if (hovered) {
157
+ setOpened({ id, pathname });
158
+ return;
159
+ }
160
+ setOpened(prev => (prev && prev.id === id && prev.pathname === pathname ? null : { id, pathname }));
161
+ },
162
+ onMouseEnter: () => {
163
+ clearTimers();
164
+ if (openId === id) return;
165
+ // A menu is already open: the visitor is browsing the row, so the next
166
+ // one opens at once. The hover-intent wait is only for the FIRST open
167
+ // (it filters a pointer merely crossing the bar).
168
+ if (openId !== null) {
169
+ openedByHover.current = id;
170
+ setOpened({ id, pathname });
171
+ return;
172
+ }
173
+ openTimer.current = setTimeout(() => {
174
+ openTimer.current = null;
175
+ openedByHover.current = id;
176
+ setOpened({ id, pathname });
177
+ }, hoverOpenMs);
178
+ },
179
+ onMouseLeave: scheduleClose,
180
+ }),
181
+ [baseId, openId, pathname, hoverOpenMs, clearTimers, scheduleClose],
182
+ );
183
+
184
+ const getPanelProps = useCallback(
185
+ (id: string): NavMenuPanelProps => ({
186
+ ref: el => {
187
+ if (el) panels.current.set(id, el);
188
+ else panels.current.delete(id);
189
+ },
190
+ id: `${baseId}-panel-${id}`,
191
+ inert: openId !== id,
192
+ 'data-state': openId === id ? 'open' : 'closed',
193
+ onMouseEnter: cancelClose,
194
+ onMouseLeave: scheduleClose,
195
+ }),
196
+ [baseId, openId, cancelClose, scheduleClose],
197
+ );
198
+
199
+ return { openId, close, getTriggerProps, getPanelProps };
200
+ }