@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,196 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import { forwardRef, type HTMLAttributes } from 'react';
5
+ import type { NavGroup, NavMenu } from '../../types/navigation';
6
+ import { cn } from '../../utils/cn';
7
+ import { CaseStudyCard } from '../chat/entity-cards/case-study-card';
8
+ import { NavItemRow } from './nav-item-row';
9
+ import { defaultRenderSiteNavLink, type SiteNavLinkRenderer } from './site-nav-link';
10
+
11
+ /** A column with more links than this lays them out in two columns. */
12
+ const TWO_COLUMN_OVER = 4;
13
+
14
+ /** True when a menu opens the full-width mega panel instead of a dropdown list. */
15
+ export function isMegaMenu(menu: Pick<NavMenu, 'columns' | 'features' | 'sideLinks'>): boolean {
16
+ return (menu.columns?.length ?? 0) > 1 || !!menu.features?.length || !!menu.sideLinks?.length;
17
+ }
18
+
19
+ export interface NavPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
20
+ menu: NavMenu;
21
+ open: boolean;
22
+ renderLink?: SiteNavLinkRenderer;
23
+ /** Values for `NavLink.badgeKey`. */
24
+ badges?: Record<string, number>;
25
+ /** Rendered under the links of a menu with `showSocial`. */
26
+ social?: React.ReactNode;
27
+ /** Called when a link is activated, so the owner can close the menu. */
28
+ onNavigate?: () => void;
29
+ /**
30
+ * How an OPEN mega menu arrived: `open` from a closed bar (the panel fades
31
+ * in), `from-start` / `from-end` from the mega menu before or after it in
32
+ * the row (the surface stays put and only the content slides in from that
33
+ * side). Default `open`.
34
+ */
35
+ motion?: NavPanelMotion;
36
+ /**
37
+ * Another mega menu is open: this closed one leaves without a fade, so the
38
+ * two panels never show through each other mid-switch.
39
+ */
40
+ handoff?: boolean;
41
+ }
42
+
43
+ export type NavPanelMotion = 'open' | 'from-start' | 'from-end';
44
+
45
+ const HEADING_CLASS = 'mb-[var(--spacing-system-xsf)] ml-[var(--spacing-system-sf)] text-ods-text-muted text-h5';
46
+
47
+ function Column({
48
+ group,
49
+ wide,
50
+ rowProps,
51
+ }: {
52
+ group: NavGroup;
53
+ wide: boolean;
54
+ rowProps: Pick<NavPanelProps, 'renderLink' | 'badges' | 'onNavigate'>;
55
+ }) {
56
+ const twoColumns = wide && group.links.length > TWO_COLUMN_OVER;
57
+ return (
58
+ <div className={cn('flex min-w-0 flex-col', wide && (twoColumns ? 'flex-[2]' : 'flex-1'))}>
59
+ {group.title && <div className={HEADING_CLASS}>{group.title}</div>}
60
+ <div
61
+ className={
62
+ twoColumns
63
+ ? 'grid grid-cols-2 gap-x-[var(--spacing-system-mf)] gap-y-[var(--spacing-system-xxs)]'
64
+ : 'flex flex-col gap-[var(--spacing-system-xxs)]'
65
+ }
66
+ >
67
+ {group.links.map(link => (
68
+ <NavItemRow
69
+ key={link.id}
70
+ link={link}
71
+ renderLink={rowProps.renderLink}
72
+ onNavigate={rowProps.onNavigate}
73
+ badge={link.badgeKey ? rowProps.badges?.[link.badgeKey] : undefined}
74
+ />
75
+ ))}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ /**
82
+ * The panel a header menu opens, in the one shape its data asks for: a single
83
+ * column is a compact dropdown under its trigger; more than one column, or
84
+ * features, or side links make the full-width mega menu under the bar.
85
+ *
86
+ * Always in the DOM, so crawlers see every link; closed, it is `inert` (out of
87
+ * the tab order and the accessibility tree, which `aria-hidden` alone would
88
+ * not do) and invisible. Plain `<div>`s on purpose: these are disclosures of
89
+ * links, not an ARIA menu, so there is no `role="menu"` and Tab moves through
90
+ * the links in DOM order.
91
+ */
92
+ export const NavPanel = forwardRef<HTMLDivElement, NavPanelProps>(function NavPanelImpl(
93
+ {
94
+ menu,
95
+ open,
96
+ renderLink = defaultRenderSiteNavLink,
97
+ badges,
98
+ social,
99
+ onNavigate,
100
+ motion = 'open',
101
+ handoff = false,
102
+ className,
103
+ ...panelProps
104
+ }: NavPanelProps,
105
+ ref,
106
+ ) {
107
+ const columns = menu.columns ?? [];
108
+ const mega = isMegaMenu(menu);
109
+ const rowProps = { renderLink, badges, onNavigate };
110
+ const socialRow = menu.showSocial && social ? social : null;
111
+
112
+ return (
113
+ <div
114
+ inert={!open}
115
+ data-state={open ? 'open' : 'closed'}
116
+ {...panelProps}
117
+ ref={ref}
118
+ className={cn(
119
+ 'absolute top-full z-[60]',
120
+ // Between two mega menus the surface is handed over in one frame (same
121
+ // place, same size, same colour) and only the content moves; a fade
122
+ // here would let the page show through both for its whole length.
123
+ mega && (open ? motion !== 'open' : handoff) ? 'transition-none' : 'transition-opacity duration-150',
124
+ mega
125
+ ? 'left-0 right-0 border-b border-ods-border bg-ods-card shadow-xl'
126
+ : 'left-0 mt-[var(--spacing-system-xxs)] min-w-[260px] rounded-lg border border-ods-border bg-ods-card p-[var(--spacing-system-xsf)] shadow-xl',
127
+ open ? 'visible opacity-100' : 'pointer-events-none invisible opacity-0',
128
+ className,
129
+ )}
130
+ >
131
+ {mega ? (
132
+ // ONE height for every mega menu, so moving between menus (and a
133
+ // card's cover loading) never resizes the panel under the pointer.
134
+ <div
135
+ data-motion={open ? motion : undefined}
136
+ className="nav-panel-content flex h-[400px] max-h-[calc(100svh-var(--top-nav-height,72px))] flex-col gap-[var(--spacing-system-mf)] overflow-y-auto px-[var(--spacing-system-lf)] pb-[var(--spacing-system-xlf)] pt-8"
137
+ >
138
+ <div className="flex min-h-0 flex-1 gap-[var(--spacing-system-xlf)]">
139
+ {columns.map(group => (
140
+ <Column key={group.id} group={group} wide rowProps={rowProps} />
141
+ ))}
142
+ {!!menu.features?.length && (
143
+ // One row of cards, as many as the bar's width holds: a card that
144
+ // would wrap lands on a zero-height row, where its cell clips it.
145
+ <div className="grid min-w-0 flex-[3] auto-rows-[0px] grid-cols-[repeat(auto-fit,minmax(240px,1fr))] grid-rows-[minmax(0,1fr)] gap-x-[var(--spacing-system-mf)]">
146
+ {menu.features.map(feature => (
147
+ <div key={feature.id} className="min-h-0 min-w-0 overflow-hidden">
148
+ <CaseStudyCard
149
+ size="menu"
150
+ study={feature.study}
151
+ href={feature.href}
152
+ mediaMounted={open}
153
+ onNavigate={onNavigate}
154
+ className="h-full"
155
+ />
156
+ </div>
157
+ ))}
158
+ </div>
159
+ )}
160
+ {!!menu.sideLinks?.length && (
161
+ <div className="flex w-[260px] shrink-0 flex-col gap-0.5 border-l border-ods-border pl-8 pt-5">
162
+ {menu.sideTitle && <div className={HEADING_CLASS}>{menu.sideTitle}</div>}
163
+ {menu.sideLinks.map(link => (
164
+ <NavItemRow
165
+ key={link.id}
166
+ link={link}
167
+ variant="side"
168
+ {...rowProps}
169
+ badge={link.badgeKey ? badges?.[link.badgeKey] : undefined}
170
+ />
171
+ ))}
172
+ </div>
173
+ )}
174
+ </div>
175
+ {socialRow && (
176
+ <div className="border-t border-ods-border px-[var(--spacing-system-sf)] pt-[var(--spacing-system-mf)]">
177
+ {socialRow}
178
+ </div>
179
+ )}
180
+ </div>
181
+ ) : (
182
+ <>
183
+ {columns.map(group => (
184
+ <Column key={group.id} group={group} wide={false} rowProps={rowProps} />
185
+ ))}
186
+ {socialRow && (
187
+ <div className="mt-[var(--spacing-system-xsf)] border-t border-ods-border px-[var(--spacing-system-sf)] pt-[var(--spacing-system-sf)]">
188
+ {socialRow}
189
+ </div>
190
+ )}
191
+ </>
192
+ )}
193
+ </div>
194
+ );
195
+ });
196
+ NavPanel.displayName = 'NavPanel';
@@ -155,8 +155,7 @@ export const NavigationSidebarItemButton = memo(function NavigationSidebarItemBu
155
155
  *
156
156
  * Deliberately in this file, beside `NavigationSidebarItemButton`: the two must
157
157
  * agree on `h-14`, the `p-[var(--spacing-system-m)]` inset, the 24px icon box and
158
- * the label column, or the nav resizes on the handoff. Same reason
159
- * `header-skeleton.tsx` sits beside `header.tsx`.
158
+ * the label column, or the nav resizes on the handoff.
160
159
  *
161
160
  * `showLabel` is the sidebar's own minimized state, so the caller passes what it
162
161
  * already passes the real row — no separate width handling.
@@ -0,0 +1,256 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import { useState } from 'react';
5
+ import type { NavGroup, SiteNav } from '../../types/navigation';
6
+ import { cn } from '../../utils/cn';
7
+ import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
8
+ import { SocialIconRow } from '../social-icon-row';
9
+ import { Button } from '../ui/button';
10
+ import { NavItemRow } from './nav-item-row';
11
+ import { defaultRenderSiteNavLink, NAV_FOCUS_CLASS, navLinkLabel, type SiteNavLinkRenderer } from './site-nav-link';
12
+
13
+ export interface SiteFooterProps {
14
+ nav: SiteNav;
15
+ logo: React.ReactNode;
16
+ /** The platform's name beside the logo: a string is set as the wordmark, a
17
+ * node is drawn as given (a platform with its own styled name). */
18
+ name?: React.ReactNode;
19
+ renderLink?: SiteNavLinkRenderer;
20
+ /** The closing band's primary CTA when `nav.primaryCta` is `'trial'` or `'waitlist'`. */
21
+ cta?: React.ReactNode;
22
+ /** Replaces the default social row built from `nav.brand.social` (each
23
+ * link's `iconName` is its social platform key). */
24
+ social?: React.ReactNode;
25
+ /** Rendered in the brand column, under the tagline and the social links. */
26
+ brandExtra?: React.ReactNode;
27
+ /** Opaque ODS background class. Default `bg-ods-bg`. */
28
+ backgroundClassName?: string;
29
+ /** Generated, never typed. Default: the current year. */
30
+ year?: number;
31
+ }
32
+
33
+ /** Below `md`, more columns than this fold into accordions. */
34
+ const ACCORDION_OVER = 2;
35
+
36
+ // Literal class maps: Tailwind's scanner needs the full class strings.
37
+ const DESKTOP_COLUMNS: Record<number, string> = {
38
+ 1: 'md:grid-cols-1',
39
+ 2: 'md:grid-cols-2',
40
+ 3: 'md:grid-cols-3',
41
+ 4: 'md:grid-cols-3 lg:grid-cols-4',
42
+ 5: 'md:grid-cols-3 lg:grid-cols-5',
43
+ 6: 'md:grid-cols-3 lg:grid-cols-6',
44
+ };
45
+
46
+ const HEADING_CLASS = 'text-ods-text-muted text-h5';
47
+
48
+ function FooterColumn({
49
+ group,
50
+ accordion,
51
+ renderLink,
52
+ }: {
53
+ group: NavGroup;
54
+ accordion: boolean;
55
+ renderLink: SiteNavLinkRenderer;
56
+ }) {
57
+ const [open, setOpen] = useState(false);
58
+ const listId = `site-footer-${group.id}`;
59
+ const links = group.links.map(link => (
60
+ <li key={link.id}>
61
+ <NavItemRow link={link} variant="label" renderLink={renderLink} />
62
+ </li>
63
+ ));
64
+
65
+ return (
66
+ <div
67
+ className={cn(
68
+ 'flex min-w-0 flex-col md:gap-[var(--spacing-system-mf)]',
69
+ accordion ? 'border-b border-ods-border md:border-b-0' : 'gap-[var(--spacing-system-mf)]',
70
+ )}
71
+ >
72
+ {group.title &&
73
+ (accordion ? (
74
+ <>
75
+ {/* Below md the heading is the accordion's button; from md it is a plain heading. */}
76
+ <h3 className="md:hidden">
77
+ <Button
78
+ type="button"
79
+ variant="glyph"
80
+ size="wrap"
81
+ font="regular"
82
+ aria-expanded={open}
83
+ aria-controls={listId}
84
+ onClick={() => setOpen(prev => !prev)}
85
+ className={cn(
86
+ 'flex min-h-14 w-full items-center justify-between gap-[var(--spacing-system-sf)] text-left',
87
+ HEADING_CLASS,
88
+ NAV_FOCUS_CLASS,
89
+ )}
90
+ >
91
+ {group.title}
92
+ <Chevron02DownIcon
93
+ aria-hidden="true"
94
+ className={cn('h-5 w-5 shrink-0 transition-transform duration-150', open && 'rotate-180')}
95
+ />
96
+ </Button>
97
+ </h3>
98
+ <h3 className={cn('hidden md:block', HEADING_CLASS)}>{group.title}</h3>
99
+ </>
100
+ ) : (
101
+ <h3 className={HEADING_CLASS}>{group.title}</h3>
102
+ ))}
103
+ {/* Always in the DOM (crawlers read the footer links); a folded
104
+ accordion only hides its list below md. */}
105
+ <ul
106
+ id={listId}
107
+ className={cn(
108
+ 'flex-col md:flex md:gap-[var(--spacing-system-sf)] md:pb-0',
109
+ accordion
110
+ ? open
111
+ ? 'flex gap-[var(--spacing-system-sf)] pb-[var(--spacing-system-mf)]'
112
+ : 'hidden'
113
+ : 'flex gap-[var(--spacing-system-sf)]',
114
+ )}
115
+ >
116
+ {links}
117
+ </ul>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * The site footer of every platform, drawn from one `SiteNav`: the closing
124
+ * band, the brand column, the link columns and the legal line.
125
+ */
126
+ export function SiteFooter({
127
+ nav,
128
+ logo,
129
+ name,
130
+ renderLink = defaultRenderSiteNavLink,
131
+ cta,
132
+ social,
133
+ brandExtra,
134
+ backgroundClassName,
135
+ year = new Date().getFullYear(),
136
+ }: SiteFooterProps) {
137
+ const band = nav.closingBand;
138
+ const bandPrimary =
139
+ typeof nav.primaryCta === 'object' ? (
140
+ <Button variant="accent" href={nav.primaryCta.href}>
141
+ {nav.primaryCta.label}
142
+ </Button>
143
+ ) : nav.primaryCta === 'none' ? null : (
144
+ (cta ?? null)
145
+ );
146
+
147
+ const columns = nav.footerColumns;
148
+ const accordion = columns.length > ACCORDION_OVER;
149
+ const socialRow =
150
+ social ??
151
+ (nav.brand.social.length > 0 ? (
152
+ <SocialIconRow
153
+ compact
154
+ links={nav.brand.social.map(link => ({
155
+ platform: link.iconName ?? link.id,
156
+ href: link.href,
157
+ label: link.label,
158
+ }))}
159
+ />
160
+ ) : null);
161
+
162
+ return (
163
+ <footer
164
+ className={cn(
165
+ 'relative z-[44] w-full border-t border-ods-border px-[var(--spacing-system-lf)] text-ods-text-primary',
166
+ backgroundClassName ?? 'bg-ods-bg',
167
+ )}
168
+ >
169
+ {band && (
170
+ <div className="flex flex-col gap-[var(--spacing-system-lf)] border-b border-ods-border py-[var(--spacing-system-xlf)] md:flex-row md:items-center md:justify-between md:gap-[var(--spacing-system-xlf)] md:py-14">
171
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xsf)]">
172
+ <h2 className="text-h2">{band.heading}</h2>
173
+ <p className="text-ods-text-secondary text-h4">{band.subheading}</p>
174
+ </div>
175
+ <div className="flex shrink-0 flex-col gap-[var(--spacing-system-sf)] md:flex-row">
176
+ {bandPrimary}
177
+ {band.secondary && (
178
+ <Button variant="outline" href={band.secondary.href} openInNewTab={band.secondary.external}>
179
+ {navLinkLabel(band.secondary)}
180
+ </Button>
181
+ )}
182
+ </div>
183
+ </div>
184
+ )}
185
+
186
+ <div className="flex flex-col gap-8 py-[var(--spacing-system-xlf)] lg:flex-row lg:gap-16 lg:py-14">
187
+ <div className="flex w-full shrink-0 flex-col items-start gap-[var(--spacing-system-mf)] lg:w-60">
188
+ <span className="flex items-center gap-[var(--spacing-system-xsf)]">
189
+ {logo}
190
+ {typeof name === 'string' ? (
191
+ <span className="whitespace-nowrap text-ods-text-primary text-wordmark">{name}</span>
192
+ ) : (
193
+ name
194
+ )}
195
+ </span>
196
+ <p className="text-ods-text-secondary text-h6">{nav.brand.tagline}</p>
197
+ {socialRow}
198
+ {nav.brand.statusUrl &&
199
+ renderLink({
200
+ link: { href: nav.brand.statusUrl, external: true },
201
+ className: cn(
202
+ 'inline-flex items-center gap-[var(--spacing-system-xsf)] text-ods-text-secondary transition-colors text-h6 hover:text-ods-text-primary',
203
+ NAV_FOCUS_CLASS,
204
+ ),
205
+ children: (
206
+ <>
207
+ <span aria-hidden="true" className="h-2 w-2 rounded-full bg-ods-success" />
208
+ System status
209
+ </>
210
+ ),
211
+ })}
212
+ {brandExtra}
213
+ </div>
214
+
215
+ {columns.length > 0 && (
216
+ <nav
217
+ aria-label="Footer"
218
+ className={cn(
219
+ 'grid min-w-0 flex-1 md:gap-7',
220
+ accordion ? 'grid-cols-1' : 'grid-cols-2 gap-7',
221
+ DESKTOP_COLUMNS[Math.min(columns.length, 6)],
222
+ )}
223
+ >
224
+ {columns.map(group => (
225
+ <FooterColumn key={group.id} group={group} accordion={accordion} renderLink={renderLink} />
226
+ ))}
227
+ </nav>
228
+ )}
229
+ </div>
230
+
231
+ {/* The legal line is text, not navigation: muted, with its links in the
232
+ accent so they still read as links (prototype `.nf-legal`). */}
233
+ <div
234
+ className={cn(
235
+ 'flex flex-col gap-[var(--spacing-system-xxs)] border-t border-ods-border pb-[var(--spacing-system-xlf)] pt-[var(--spacing-system-lf)] text-ods-text-muted text-h6 md:flex-row md:items-center md:justify-between md:gap-[var(--spacing-system-lf)]',
236
+ )}
237
+ >
238
+ <span>
239
+ © {year} {nav.legal.company}
240
+ </span>
241
+ <span className="flex flex-wrap items-center gap-x-[var(--spacing-system-mf)] gap-y-[var(--spacing-system-xxs)]">
242
+ {nav.legal.notes.length > 0 && <span>{nav.legal.notes.join(' · ')}</span>}
243
+ {nav.legal.links.map(link => (
244
+ <NavItemRow
245
+ key={link.id}
246
+ link={link}
247
+ variant="label"
248
+ renderLink={renderLink}
249
+ className="text-ods-accent hover:text-ods-accent hover:underline"
250
+ />
251
+ ))}
252
+ </span>
253
+ </div>
254
+ </footer>
255
+ );
256
+ }