@flamingo-stack/openframe-frontend-core 0.0.705-snapshot.20261006160157 → 0.0.705

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 (194) hide show
  1. package/dist/{chunk-WCVZCGK5.cjs → chunk-2QEUFXNS.cjs} +71 -312
  2. package/dist/{chunk-N45D4ZXQ.js → chunk-2YXVWZVF.js} +715 -959
  3. package/dist/{chunk-7OFPU7WE.cjs → chunk-3MX5S4CF.cjs} +2547 -877
  4. package/dist/{chunk-MB4PNY7C.cjs → chunk-4I45Y3DN.cjs} +6 -6
  5. package/dist/{chunk-K6O43WDL.cjs → chunk-4KX6OED2.cjs} +46 -46
  6. package/dist/{chunk-JPG7OHEG.js → chunk-5HBHO5TB.js} +1 -1
  7. package/dist/{chunk-3PLDQDE6.cjs → chunk-6NJKKO73.cjs} +15 -15
  8. package/dist/{chunk-O2RSAA5M.js → chunk-6SEANMOU.js} +11 -11
  9. package/dist/{chunk-WMX26HB5.js → chunk-6XIF2Z7I.js} +3 -3
  10. package/dist/{chunk-TQ4LB2DS.cjs → chunk-7LS22BZU.cjs} +14 -14
  11. package/dist/{chunk-7GEBFJT2.js → chunk-7VVFKJY5.js} +3429 -1759
  12. package/dist/{chunk-DDVBX42E.cjs → chunk-A75QY6CD.cjs} +21 -21
  13. package/dist/{chunk-OF6GEI5A.cjs → chunk-ABU3TGXA.cjs} +3 -3
  14. package/dist/{chunk-4CJMQLB5.js → chunk-AQWZNP6A.js} +4 -4
  15. package/dist/{chunk-FI6GMDO7.js → chunk-CH5232HV.js} +662 -2455
  16. package/dist/{chunk-Y2GSOUCS.cjs → chunk-D7BTSVMS.cjs} +2 -2
  17. package/dist/{chunk-4ILM7W2R.js → chunk-DENB2XJC.js} +2 -2
  18. package/dist/{chunk-6232S532.js → chunk-DW7G4KG3.js} +3 -3
  19. package/dist/{chunk-3NCM4ZQB.js → chunk-F6AFPNDF.js} +15 -15
  20. package/dist/{chunk-PXOQIA5K.js → chunk-F6I6XZMN.js} +5 -5
  21. package/dist/{chunk-G6UMNAVI.js → chunk-FCGC42TS.js} +61 -57
  22. package/dist/{chunk-BHI6GVC4.js → chunk-G5A3PSPV.js} +691 -1080
  23. package/dist/{chunk-HZ7CBXPL.cjs → chunk-HPWWNJE7.cjs} +89 -89
  24. package/dist/{chunk-SW5ZV2BN.cjs → chunk-IHERL5V4.cjs} +34 -34
  25. package/dist/{chunk-UJ33ZXDT.cjs → chunk-IKTQ3ZAG.cjs} +6 -6
  26. package/dist/{chunk-SEKYEWQG.cjs → chunk-JEZKGCZI.cjs} +26 -26
  27. package/dist/{chunk-7SCNMP2B.cjs → chunk-KOWVJD4V.cjs} +56 -56
  28. package/dist/chunk-LMBC5FFP.js +43 -0
  29. package/dist/{chunk-F57ECE2I.js → chunk-MFFCUVWG.js} +1 -1
  30. package/dist/{chunk-KUC673HM.js → chunk-MKXO4Q5P.js} +142 -383
  31. package/dist/{chunk-6NTPY5QA.cjs → chunk-MMOODW57.cjs} +94 -1887
  32. package/dist/{chunk-CUME3VTQ.js → chunk-MP2C3LJ4.js} +1 -1
  33. package/dist/{chunk-VX4E6YSB.cjs → chunk-N3COXDBH.cjs} +5 -41
  34. package/dist/{chunk-FXJ3WWUB.js → chunk-NDESEPW5.js} +0 -68
  35. package/dist/chunk-O43V3EOS.js +252 -0
  36. package/dist/{chunk-PLYEKV3N.cjs → chunk-OHFS7VOU.cjs} +2 -2
  37. package/dist/{chunk-M75XEEGT.cjs → chunk-OUY3HHW6.cjs} +539 -928
  38. package/dist/{chunk-CZ7Y7UG4.js → chunk-PDWVPI3S.js} +72 -4
  39. package/dist/{chunk-TE773HEH.js → chunk-PN2RKPDF.js} +10 -10
  40. package/dist/{chunk-3EZ4HHAP.js → chunk-QFXRPRFT.js} +3 -3
  41. package/dist/chunk-QZVSJF2K.cjs +252 -0
  42. package/dist/chunk-RALRPFDW.cjs +43 -0
  43. package/dist/{chunk-YGJKKDDM.js → chunk-RBDP4WDE.js} +6 -6
  44. package/dist/{chunk-LM7TXMLU.cjs → chunk-RKV5B7EU.cjs} +4 -72
  45. package/dist/{chunk-UVTD52MD.cjs → chunk-TYWLNCNH.cjs} +82 -14
  46. package/dist/{chunk-IKJA3Y7Z.js → chunk-VA5T32XA.js} +2 -2
  47. package/dist/{chunk-GWQEYPYM.cjs → chunk-VESKITRQ.cjs} +732 -728
  48. package/dist/{chunk-RQRX4YC3.cjs → chunk-VHCACDKP.cjs} +39 -39
  49. package/dist/{chunk-4IBPRIQH.js → chunk-VR2ZLFCP.js} +1 -1
  50. package/dist/{chunk-RTIPR53L.cjs → chunk-WA6GUYQ4.cjs} +104 -104
  51. package/dist/{chunk-T5TMCHWM.js → chunk-X5MQ7JRP.js} +9 -9
  52. package/dist/{chunk-LLSUVPQC.cjs → chunk-X72J4ACL.cjs} +8 -252
  53. package/dist/{chunk-UDFRUKLC.cjs → chunk-XOYH2YQE.cjs} +2 -2
  54. package/dist/{chunk-LVSFKENJ.cjs → chunk-YFBM2K6X.cjs} +4 -4
  55. package/dist/{chunk-DWPAAWMR.js → chunk-YHM5AWTT.js} +6 -6
  56. package/dist/{chunk-NCPTID44.js → chunk-Z2VNQQ74.js} +942 -978
  57. package/dist/{chunk-STHZBOQI.cjs → chunk-ZSAGLQOD.cjs} +17 -17
  58. package/dist/{chunk-VZICIQW2.js → chunk-ZSAO2HBW.js} +15 -15
  59. package/dist/components/case-studies/index.cjs +32 -30
  60. package/dist/components/case-studies/index.js +16 -14
  61. package/dist/components/chat/entity-cards/case-study-card.d.ts +4 -11
  62. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  63. package/dist/components/chat/index.cjs +11 -9
  64. package/dist/components/chat/index.js +16 -14
  65. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  66. package/dist/components/contact/index.cjs +14 -12
  67. package/dist/components/contact/index.js +13 -11
  68. package/dist/components/docs/index.cjs +17 -15
  69. package/dist/components/docs/index.js +16 -14
  70. package/dist/components/embeds/index.cjs +14 -12
  71. package/dist/components/embeds/index.js +13 -11
  72. package/dist/components/faq/index.cjs +15 -13
  73. package/dist/components/faq/index.js +14 -12
  74. package/dist/components/features/index.cjs +14 -12
  75. package/dist/components/features/index.js +23 -21
  76. package/dist/components/footer-waitlist-button.d.ts +29 -0
  77. package/dist/components/footer-waitlist-button.d.ts.map +1 -0
  78. package/dist/components/footer.d.ts +47 -0
  79. package/dist/components/footer.d.ts.map +1 -0
  80. package/dist/components/help-center-pages/index.cjs +120 -118
  81. package/dist/components/help-center-pages/index.js +35 -33
  82. package/dist/components/icons/index.cjs +5 -3
  83. package/dist/components/icons/index.js +11 -9
  84. package/dist/components/icons-v2-generated/index.cjs +5 -3
  85. package/dist/components/icons-v2-generated/index.js +4 -2
  86. package/dist/components/index.cjs +159 -86
  87. package/dist/components/index.d.ts +2 -0
  88. package/dist/components/index.d.ts.map +1 -1
  89. package/dist/components/index.js +402 -329
  90. package/dist/components/loading/page-layout-skeleton.d.ts +1 -1
  91. package/dist/components/meeting-scheduler/index.cjs +64 -62
  92. package/dist/components/meeting-scheduler/index.js +14 -12
  93. package/dist/components/navigation/client-only-header.d.ts +8 -0
  94. package/dist/components/navigation/client-only-header.d.ts.map +1 -0
  95. package/dist/components/navigation/header-skeleton.d.ts +12 -0
  96. package/dist/components/navigation/header-skeleton.d.ts.map +1 -0
  97. package/dist/components/navigation/header.d.ts +9 -0
  98. package/dist/components/navigation/header.d.ts.map +1 -0
  99. package/dist/components/navigation/index.cjs +16 -28
  100. package/dist/components/navigation/index.d.ts +10 -14
  101. package/dist/components/navigation/index.d.ts.map +1 -1
  102. package/dist/components/navigation/index.js +26 -38
  103. package/dist/components/navigation/mingo-ai-button.d.ts +3 -38
  104. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  105. package/dist/components/navigation/mobile-nav-panel.d.ts +10 -0
  106. package/dist/components/navigation/mobile-nav-panel.d.ts.map +1 -0
  107. package/dist/components/navigation/navigation-sidebar-item.d.ts +2 -1
  108. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  109. package/dist/components/navigation/top-navigation.d.ts +2 -2
  110. package/dist/components/navigation/top-navigation.d.ts.map +1 -1
  111. package/dist/components/onboarding-guides/index.cjs +17 -15
  112. package/dist/components/onboarding-guides/index.js +16 -14
  113. package/dist/components/related-content/index.cjs +8 -10
  114. package/dist/components/related-content/index.js +7 -9
  115. package/dist/components/tickets/index.cjs +18 -16
  116. package/dist/components/tickets/index.js +17 -15
  117. package/dist/components/ui/index.cjs +14 -12
  118. package/dist/components/ui/index.js +19 -17
  119. package/dist/hooks/index.cjs +11 -19
  120. package/dist/hooks/index.d.ts +0 -1
  121. package/dist/hooks/index.d.ts.map +1 -1
  122. package/dist/hooks/index.js +11 -19
  123. package/dist/hooks/ui/index.d.ts +0 -3
  124. package/dist/hooks/ui/index.d.ts.map +1 -1
  125. package/dist/index.cjs +22 -55
  126. package/dist/index.js +196 -229
  127. package/dist/types/case-study.d.ts +0 -9
  128. package/dist/types/case-study.d.ts.map +1 -1
  129. package/dist/types/navigation.d.ts +65 -95
  130. package/dist/types/navigation.d.ts.map +1 -1
  131. package/dist/utils/index.cjs +0 -55
  132. package/dist/utils/index.d.ts +0 -1
  133. package/dist/utils/index.d.ts.map +1 -1
  134. package/dist/utils/index.js +1 -50
  135. package/dist/utils/source-icons.d.ts.map +1 -1
  136. package/package.json +1 -1
  137. package/src/components/chat/entity-cards/case-study-card.tsx +5 -115
  138. package/src/components/chat/utils/icon-library.tsx +1 -35
  139. package/src/components/footer-waitlist-button.tsx +53 -0
  140. package/src/components/footer.tsx +195 -0
  141. package/src/components/index.ts +2 -0
  142. package/src/components/loading/page-layout-skeleton.tsx +1 -1
  143. package/src/components/navigation/client-only-header.tsx +22 -0
  144. package/src/components/navigation/header-skeleton.tsx +74 -0
  145. package/src/components/navigation/header.tsx +429 -0
  146. package/src/components/navigation/index.ts +14 -21
  147. package/src/components/navigation/mingo-ai-button.tsx +63 -139
  148. package/src/components/navigation/mobile-nav-panel.tsx +155 -0
  149. package/src/components/navigation/navigation-sidebar-item.tsx +2 -1
  150. package/src/components/navigation/ticket-alerts-button.tsx +2 -2
  151. package/src/components/navigation/top-navigation.tsx +2 -2
  152. package/src/hooks/index.ts +0 -1
  153. package/src/hooks/ui/index.ts +0 -3
  154. package/src/styles/chat-animations.css +2 -57
  155. package/src/types/case-study.ts +0 -16
  156. package/src/types/navigation.ts +66 -80
  157. package/src/utils/index.ts +0 -1
  158. package/src/utils/source-icons.ts +0 -13
  159. package/dist/components/navigation/mobile-nav-sheet-id.d.ts +0 -3
  160. package/dist/components/navigation/mobile-nav-sheet-id.d.ts.map +0 -1
  161. package/dist/components/navigation/mobile-nav-sheet.d.ts +0 -48
  162. package/dist/components/navigation/mobile-nav-sheet.d.ts.map +0 -1
  163. package/dist/components/navigation/nav-item-row.d.ts +0 -27
  164. package/dist/components/navigation/nav-item-row.d.ts.map +0 -1
  165. package/dist/components/navigation/nav-panel.d.ts +0 -43
  166. package/dist/components/navigation/nav-panel.d.ts.map +0 -1
  167. package/dist/components/navigation/site-footer.d.ts +0 -28
  168. package/dist/components/navigation/site-footer.d.ts.map +0 -1
  169. package/dist/components/navigation/site-header.d.ts +0 -61
  170. package/dist/components/navigation/site-header.d.ts.map +0 -1
  171. package/dist/components/navigation/site-nav-link.d.ts +0 -26
  172. package/dist/components/navigation/site-nav-link.d.ts.map +0 -1
  173. package/dist/hooks/ui/use-active-section.d.ts +0 -10
  174. package/dist/hooks/ui/use-active-section.d.ts.map +0 -1
  175. package/dist/hooks/ui/use-nav-menus.d.ts +0 -48
  176. package/dist/hooks/ui/use-nav-menus.d.ts.map +0 -1
  177. package/dist/hooks/ui/use-visitor-os.d.ts +0 -15
  178. package/dist/hooks/ui/use-visitor-os.d.ts.map +0 -1
  179. package/dist/hooks/use-location-hash.d.ts +0 -9
  180. package/dist/hooks/use-location-hash.d.ts.map +0 -1
  181. package/dist/utils/visitor-os.d.ts +0 -45
  182. package/dist/utils/visitor-os.d.ts.map +0 -1
  183. package/src/components/navigation/mobile-nav-sheet-id.ts +0 -2
  184. package/src/components/navigation/mobile-nav-sheet.tsx +0 -241
  185. package/src/components/navigation/nav-item-row.tsx +0 -129
  186. package/src/components/navigation/nav-panel.tsx +0 -196
  187. package/src/components/navigation/site-footer.tsx +0 -256
  188. package/src/components/navigation/site-header.tsx +0 -339
  189. package/src/components/navigation/site-nav-link.tsx +0 -41
  190. package/src/hooks/ui/use-active-section.ts +0 -44
  191. package/src/hooks/ui/use-nav-menus.ts +0 -200
  192. package/src/hooks/ui/use-visitor-os.ts +0 -33
  193. package/src/hooks/use-location-hash.ts +0 -34
  194. package/src/utils/visitor-os.ts +0 -84
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * CaseStudyCard (pure presentation). Four densities — `default` (vertical
5
- * detail), `sm` (compact horizontal for chat-inline), `portrait` (rails and
6
- * strips) and `menu` (the site menu's card, hover-plays the highlight cut).
4
+ * CaseStudyCard (pure presentation). Two densities — `default` (vertical
5
+ * detail) and `sm` (compact horizontal for chat-inline).
7
6
  *
8
7
  * The card writes NO click logic — callers wrap with their own anchor
9
8
  * and pass the resolved detail URL via `href`.
@@ -13,15 +12,9 @@
13
12
  * `useOgPlaceholderUrl(...)`) → `bg-ods-bg`.
14
13
  */
15
14
 
16
- import { useState } from 'react';
17
15
  import Image from '../../../embed-shims/next-image';
18
- import Link from '../../../embed-shims/next-link';
19
- import type { CaseStudyCardData } from '../../../types/case-study';
16
+ import type { CaseStudy } from '../../../types/case-study';
20
17
  import { cn } from '../../../utils/cn';
21
- import { sortBitesByFeaturedAtDesc } from '../../features/video-bites-shared';
22
- import { CardHoverPlay } from '../../features/video-center-badge';
23
- import { VideoHoverPreviewSurface } from '../../features/video-hover-preview';
24
- import { EntityIcon } from '../../icon-display';
25
18
  import { Card } from '../../ui/card';
26
19
  import {
27
20
  COMPACT_CARD_IMAGE_SLOT,
@@ -43,7 +36,7 @@ import { useEntityCardLink } from './use-entity-card-link';
43
36
  import { useEntityCardPlaceholder } from './use-entity-card-placeholder';
44
37
 
45
38
  export interface CaseStudyCardProps {
46
- study: CaseStudyCardData;
39
+ study: CaseStudy;
47
40
  /** Detail URL resolved by the caller. */
48
41
  href: string;
49
42
  /** When `_blank`, opens in a new tab. Set by chat dispatch via
@@ -53,14 +46,7 @@ export interface CaseStudyCardProps {
53
46
  targetPlatform?: string | null;
54
47
  /** OG placeholder URL, used when `study.featured_image` is missing. */
55
48
  placeholderUrl?: string | null;
56
- /** `menu` is the site menu's card: the story's video plays muted on hover or
57
- * focus (the video bites' hover grammar), then who it is about and who tells it. */
58
- size?: 'default' | 'sm' | 'portrait' | 'menu';
59
- /** `menu`: mount the hover player (the host passes whether its panel is open,
60
- * so a closed menu holds no player). Omitted: the card's own viewport gate. */
61
- mediaMounted?: boolean;
62
- /** `menu`: runs on click, before the navigation (the host closes its panel). */
63
- onNavigate?: () => void;
49
+ size?: 'default' | 'sm' | 'portrait';
64
50
  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */
65
51
  showTypeBadge?: boolean;
66
52
  className?: string;
@@ -106,22 +92,6 @@ export function CaseStudyCardSkeleton({ size = 'default' }: { size?: 'default' |
106
92
  );
107
93
  }
108
94
 
109
- /**
110
- * A story's HIGHLIGHT cut, the short video a card previews: its highlight
111
- * video, else the bite an admin featured (the newest when several are; featuring
112
- * is independent of the bite's own `published` flag, as on every featured surface). Never
113
- * the full video: that plays on the story's own page. Null when it has neither.
114
- */
115
- function caseStudyHighlight(study: CaseStudyCardData): { url: string; posterUrl: string | null } | null {
116
- if (study.highlight_video_url) {
117
- return { url: study.highlight_video_url, posterUrl: study.highlight_video_thumbnail ?? null };
118
- }
119
- const bite = (study.video_bites ?? [])
120
- .filter(candidate => candidate.featured === true && candidate.url)
121
- .sort(sortBitesByFeaturedAtDesc)[0];
122
- return bite ? { url: bite.url, posterUrl: bite.thumbnail_url ?? null } : null;
123
- }
124
-
125
95
  export function CaseStudyCard({
126
96
  study,
127
97
  href,
@@ -131,11 +101,8 @@ export function CaseStudyCard({
131
101
  placeholderUrl: placeholderUrlProp,
132
102
  size = 'default',
133
103
  showTypeBadge = true,
134
- mediaMounted,
135
- onNavigate,
136
104
  className,
137
105
  }: CaseStudyCardProps) {
138
- const [hovered, setHovered] = useState(false);
139
106
  const { target, rel } = useEntityCardLink({
140
107
  href,
141
108
  targetPlatform,
@@ -182,83 +149,6 @@ export function CaseStudyCard({
182
149
  );
183
150
  }
184
151
 
185
- if (size === 'menu') {
186
- const preview = caseStudyHighlight(study);
187
- const videoUrl = preview?.url ?? null;
188
- const hasAnyVideo = !!(videoUrl || study.main_video_url);
189
- // The cover is the FULL video's poster (wide, like the card); the highlight
190
- // cut's own frame is usually upright and would not fill a 16:9 slot.
191
- const poster = study.main_video_thumbnail || study.featured_image || preview?.posterUrl || null;
192
- return (
193
- <Link
194
- href={href}
195
- target={target}
196
- rel={rel}
197
- onClick={onNavigate}
198
- onPointerEnter={() => setHovered(true)}
199
- onPointerLeave={() => setHovered(false)}
200
- onFocus={() => setHovered(true)}
201
- onBlur={() => setHovered(false)}
202
- className={cn(
203
- // `group/card` is what the shared play glyph accents on.
204
- 'group/card group flex min-w-0 flex-col gap-[var(--spacing-system-sf)] rounded-xl border border-ods-border bg-ods-bg p-[var(--spacing-system-mf)] text-ods-text-primary transition-colors hover:bg-ods-bg-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent',
205
- className,
206
- )}
207
- >
208
- {/* The media never takes the click: the whole card is the link. */}
209
- <span className="pointer-events-none relative block aspect-video w-full shrink-0 overflow-hidden rounded-md bg-ods-bg">
210
- {videoUrl ? (
211
- // The shared hover surface, filled the way the walkthrough card fills
212
- // its own 16:9 slot (`cover`): an upright cut is cropped to the slot,
213
- // never letterboxed. Sound on hover, muted when the browser blocks it.
214
- <VideoHoverPreviewSurface
215
- url={videoUrl}
216
- posterUrl={poster}
217
- active={hovered}
218
- playerMounted={mediaMounted}
219
- fit="cover"
220
- posterSizes="360px"
221
- />
222
- ) : (
223
- <>
224
- {(poster ?? placeholderUrl) && (
225
- <Image
226
- src={poster ?? placeholderUrl ?? ''}
227
- alt=""
228
- className="object-cover"
229
- fill
230
- sizes="360px"
231
- unoptimized
232
- onError={hideOnError}
233
- />
234
- )}
235
- {hasAnyVideo && <CardHoverPlay size="md" />}
236
- </>
237
- )}
238
- </span>
239
- {study.msp?.name && (
240
- <span className="flex items-center gap-[var(--spacing-system-xsf)] text-ods-text-secondary text-h6">
241
- {study.msp.icon_url && (
242
- <EntityIcon
243
- icon={{ url: study.msp.icon_url }}
244
- size={20}
245
- className="size-5 shrink-0 rounded object-contain"
246
- />
247
- )}
248
- <span className="min-w-0 truncate">{study.msp.name}</span>
249
- </span>
250
- )}
251
- <span className="line-clamp-2 text-h6">{study.title}</span>
252
- {study.user?.full_name && (
253
- <span className="truncate text-ods-text-muted text-h6">
254
- {study.user.job_title ? `${study.user.full_name}, ${study.user.job_title}` : study.user.full_name}
255
- </span>
256
- )}
257
- <span className="mt-auto text-ods-accent text-h6">{hasAnyVideo ? 'Watch the story' : 'Read the story'}</span>
258
- </Link>
259
- );
260
- }
261
-
262
152
  if (size === 'portrait') {
263
153
  // Rail/strip density — shared <EntityPortraitCard> shell.
264
154
  return (
@@ -30,7 +30,6 @@
30
30
 
31
31
  import { Megaphone, Bell, Info, Star, Package as PackageGlyph } from 'lucide-react';
32
32
  import type { ComponentType, CSSProperties } from 'react';
33
- import { platformAccentVarName } from '../../../utils/platform-identity';
34
33
  import * as IconsV2 from '../../icons-v2-generated';
35
34
  import { HeadphoneIcon } from '../../icons-v2-generated/audio-and-visual/headphone-icon';
36
35
  import { VideoRecorderIcon } from '../../icons-v2-generated/audio-and-visual/video-recorder-icon';
@@ -53,8 +52,6 @@ import { GraduationCapIcon } from '../../icons-v2-generated/school/graduation-ca
53
52
  import { TicketIcon } from '../../icons-v2-generated/shopping/ticket-icon';
54
53
  import { Rocket02Icon } from '../../icons-v2-generated/vehicles-and-delivery/rocket-02-icon';
55
54
  import { FlamingoLogo } from '../../icons/flamingo-logo';
56
- import { MiamiCyberGangLogoFaceOnly } from '../../icons/miami-cyber-gang-logo-face-only';
57
- import { MlgLogo } from '../../icons/mlg-logo';
58
55
  import { OpenFrameLogo } from '../../icons/openframe-logo';
59
56
  import { OpenmspLogo } from '../../icons/openmsp-logo';
60
57
  import { getIconComponent } from './icon-registry';
@@ -74,37 +71,8 @@ function sizedLogo(Logo: ComponentType<{ className?: string; style?: CSSProperti
74
71
  return <Logo className={className} style={{ width: size, height: size }} />;
75
72
  };
76
73
  }
77
- // A sister platform's mark is drawn in THAT platform's brand colour (the
78
- // platform brand registry), never in the accent of the page showing it: on
79
- // flamingo.run the OpenMSP mark stays OpenMSP yellow, not flamingo pink.
80
- const brandColor = (platform: string) => `var(${platformAccentVarName(platform)})`;
81
- // The OpenMSP mark's own defaults are black and white (made for a light page):
82
- // on the dark surfaces every glyph sits on, the bubbles take the theme's text
83
- // and background tokens and the back bubble OpenMSP's brand colour.
84
- const OpenmspLogoIcon: IconComponent = ({ size = 16, className }) => (
85
- <OpenmspLogo
86
- className={className}
87
- style={{ width: size, height: size }}
88
- frontBubbleColor="var(--color-text-primary)"
89
- innerFrontBubbleColor="var(--color-bg)"
90
- backBubbleColor={brandColor('openmsp')}
91
- />
92
- );
74
+ const OpenmspLogoIcon = sizedLogo(OpenmspLogo);
93
75
  const FlamingoLogoIcon = sizedLogo(FlamingoLogo);
94
- // The two community marks size themselves through a `size` prop, the resolver's own contract.
95
- // Major League GitHub draws its mark in the theme's text colour on its own site
96
- // (its header, hero and footer), never in an accent: so does every glyph of it.
97
- const MlgLogoIcon: IconComponent = ({ size = 16, className, color }) => (
98
- <MlgLogo size={size} className={className} color={color ?? 'var(--color-text-primary)'} />
99
- );
100
- const TmcgLogoIcon: IconComponent = ({ size = 16, className }) => (
101
- <MiamiCyberGangLogoFaceOnly
102
- size={size}
103
- className={className}
104
- originalPink={brandColor('tmcg')}
105
- originalWhite="var(--color-text-primary)"
106
- />
107
- );
108
76
 
109
77
  /**
110
78
  * `OpenFrameLogo` from `components/icons/` predates the v2 set and:
@@ -170,8 +138,6 @@ export const ICON_ALIASES: Record<string, IconComponent> = {
170
138
  // (was the separate `renderSvgIcon` map; now ONE resolver everywhere).
171
139
  'openframe-logo': LogoOpenframeIcon,
172
140
  'openmsp-logo': OpenmspLogoIcon,
173
- 'mlg-logo': MlgLogoIcon,
174
- 'tmcg-logo': TmcgLogoIcon,
175
141
  flamingo: FlamingoLogoIcon,
176
142
  'flamingo-logo': FlamingoLogoIcon,
177
143
  megaphone: Megaphone,
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { useChatRuntime } from '../contexts/chat-runtime-context';
5
+ import { useRouter } from '../embed-shims/next-navigation';
6
+ import { executeNavigationImperative } from './chat/utils/execute-navigation';
7
+ import { OpenFrameLogo } from './icons';
8
+ import { Button } from './ui/button';
9
+
10
+ export interface FooterWaitlistButtonProps {
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * Small wrapper around JoinWaitlistButton for use inside the footer.
16
+ *
17
+ * Routes through the host's unified-navigation hook
18
+ * (`runtime.navigation.navigate`) when a `ChatRuntimeContext` is
19
+ * mounted — that's the same path EVERY other in-app navigation
20
+ * surface uses (source chips, inline cards, search-autocomplete,
21
+ * action cards). One rule, one decision tree across the whole app.
22
+ * The hub's `HubRuntimeProvider` wires `navigate` to its `useUnifiedNav`
23
+ * helper, so this button picks up cross-platform new-tab decisions,
24
+ * same-URL re-scroll handling, embed-mode short-circuiting, and any
25
+ * future host-side nav rules for free.
26
+ *
27
+ * Falls back to the embed-shim's `router.push` when no runtime is
28
+ * mounted (third-party embedders who haven't set up
29
+ * `ChatRuntimeContext` — the lib stays usable without forcing them
30
+ * to wire the full chat-runtime).
31
+ *
32
+ * Target URL: `/waitlist#top`. `#top` is the canonical "scroll to
33
+ * page top" anchor — the destination page has an explicit
34
+ * `<div id="top">` at the top of `<main>` so native browser anchor
35
+ * scroll works in every browser regardless of the HTML5 magic-anchor
36
+ * behavior.
37
+ */
38
+ export function FooterWaitlistButton({ className }: FooterWaitlistButtonProps) {
39
+ const router = useRouter();
40
+ const runtime = useChatRuntime();
41
+
42
+ const handleClick = useCallback(() => {
43
+ // The unified nav primitive: host `navigation.navigate` if wired, else the
44
+ // embed-shim router; new-tab/embed decision handled internally.
45
+ executeNavigationImperative({ runtime, href: '/waitlist#top', fallbackNavigate: router.push });
46
+ }, [router, runtime]);
47
+
48
+ return (
49
+ <Button onClick={handleClick} className={className} leftIcon={<OpenFrameLogo />}>
50
+ Join Waitlist
51
+ </Button>
52
+ );
53
+ }
@@ -0,0 +1,195 @@
1
+ 'use client';
2
+
3
+ import { Suspense } from 'react';
4
+ import type { ReactNode } from 'react';
5
+ import { copyrightLine } from '../utils/copyright-line';
6
+ import { SocialIconRow } from './social-icon-row';
7
+ import { Skeleton } from './ui/skeleton';
8
+
9
+ interface FooterLink {
10
+ href: string;
11
+ label: string;
12
+ }
13
+
14
+ interface FooterSection {
15
+ title: string;
16
+ links: FooterLink[];
17
+ }
18
+
19
+ interface FooterConfig {
20
+ name: string;
21
+ legalName: string;
22
+ description: string;
23
+ logo?: ReactNode;
24
+ sections: FooterSection[];
25
+ customComponent?: ReactNode; // Inject any custom component here
26
+ nameElement?: ReactNode; // Custom element for platform name with specific font
27
+ hideSocialRow?: boolean; // Hide the default social row
28
+ rightColumnContent?: ReactNode; // Custom content for right column
29
+ belowDescriptionContent?: ReactNode; // Custom content below description
30
+ moveDescriptionToRight?: boolean; // Move description and belowDescriptionContent to right column
31
+ keepBelowDescriptionLeft?: boolean; // Keep belowDescriptionContent on left even when moveDescriptionToRight is true
32
+ backgroundColor?: string; // ODS background color (e.g., 'bg-ods-card', 'bg-ods-bg')
33
+ social?: {
34
+ github?: string;
35
+ twitter?: string;
36
+ linkedin?: string;
37
+ reddit?: string;
38
+ youtube?: string;
39
+ instagram?: string;
40
+ facebook?: string;
41
+ discord?: string;
42
+ telegram?: string;
43
+ whatsapp?: string;
44
+ };
45
+ }
46
+
47
+ interface FooterProps {
48
+ config?: FooterConfig;
49
+ renderLink?: (link: FooterLink) => ReactNode;
50
+ }
51
+
52
+ function NavLinkSkeleton() {
53
+ return <Skeleton className="h-5 w-20 md:h-6 md:w-24" />;
54
+ }
55
+
56
+ /**
57
+ * Platform-Aware Footer Component
58
+ * Accepts configuration from app-config.ts
59
+ */
60
+ export function Footer({ config, renderLink }: FooterProps) {
61
+ // Config is required - no hardcoded fallbacks
62
+ if (!config) {
63
+ console.warn('Footer: No config provided');
64
+ return null;
65
+ }
66
+
67
+ return <UniversalFooter config={config} renderLink={renderLink} />;
68
+ }
69
+
70
+ /**
71
+ * Universal Footer Component
72
+ * Renders footer based on provided config
73
+ */
74
+ function UniversalFooter({
75
+ config,
76
+ renderLink,
77
+ }: {
78
+ config: FooterConfig;
79
+ renderLink?: (link: FooterLink) => ReactNode;
80
+ }) {
81
+ const defaultRenderLink = (link: FooterLink) => (
82
+ <a
83
+ href={link.href}
84
+ className="text-md md:text-md font-body font-medium leading-[1.33] text-ods-text-primary transition-colors hover:text-ods-accent"
85
+ >
86
+ {link.label}
87
+ </a>
88
+ );
89
+
90
+ const linkRenderer = renderLink || defaultRenderLink;
91
+
92
+ return (
93
+ <footer
94
+ className={`flex w-full flex-col items-center justify-center ${config.backgroundColor || 'bg-ods-card'} relative z-[44] min-h-[auto] gap-6 border-t border-ods-border px-6 py-10 md:min-h-[248px] md:gap-6`}
95
+ >
96
+ <div className="grid w-full grid-cols-2 items-start gap-6 md:gap-8 lg:grid-cols-4">
97
+ {/* Column 1: Logo and optionally description */}
98
+ <div className="col-span-2 flex flex-col items-start gap-4 text-left md:col-span-1 md:gap-6 lg:col-span-1">
99
+ {/* Logo and name */}
100
+ <div className="flex items-center gap-2">
101
+ {config.logo && <Suspense fallback={<Skeleton className="h-8 w-8" />}>{config.logo}</Suspense>}
102
+ {config.nameElement || (
103
+ <span className="whitespace-nowrap font-heading text-heading-5 font-bold text-ods-text-primary">
104
+ {config.name}
105
+ </span>
106
+ )}
107
+ </div>
108
+
109
+ {/* Only show description here if NOT moving to right */}
110
+ {!config.moveDescriptionToRight && (
111
+ <>
112
+ <p className="font-body text-sm font-medium leading-[1.43] text-ods-text-primary md:text-sm">
113
+ {config.description}
114
+ </p>
115
+
116
+ {/* Custom content below description */}
117
+ {config.belowDescriptionContent && (
118
+ <Suspense fallback={<Skeleton className="h-8 w-full" />}>{config.belowDescriptionContent}</Suspense>
119
+ )}
120
+
121
+ {/* Conditional social row - show by default unless hideSocialRow is true */}
122
+ {!config.hideSocialRow && (
123
+ <SocialIconRow
124
+ className="pt-2"
125
+ links={
126
+ config.social
127
+ ? Object.entries(config.social)
128
+ .filter(([_, href]) => href)
129
+ .map(([platform, href]) => ({ platform, href: href }))
130
+ : undefined
131
+ }
132
+ />
133
+ )}
134
+ </>
135
+ )}
136
+
137
+ {/* Show belowDescriptionContent on left even when description is moved to right */}
138
+ {config.moveDescriptionToRight && config.keepBelowDescriptionLeft && config.belowDescriptionContent && (
139
+ <Suspense fallback={<Skeleton className="h-8 w-full" />}>{config.belowDescriptionContent}</Suspense>
140
+ )}
141
+ </div>
142
+
143
+ {/* Dynamic sections - 1 column each on all screens */}
144
+ {config.sections.map((section, index) => (
145
+ <div key={index} className="col-span-1 flex flex-col items-start gap-3 text-left">
146
+ <h3 className="tracking-[-0.02em] text-ods-text-muted text-h5">{section.title}</h3>
147
+ <div className="flex flex-col gap-3">
148
+ {section.links.map((link, linkIndex) => (
149
+ <Suspense key={linkIndex} fallback={<NavLinkSkeleton />}>
150
+ {linkRenderer(link)}
151
+ </Suspense>
152
+ ))}
153
+ </div>
154
+ </div>
155
+ ))}
156
+
157
+ {/* Custom component column - full width on mobile and medium, 1 column on large */}
158
+ {config.customComponent && (
159
+ <div className="col-span-2 flex flex-col justify-center md:col-span-1 lg:col-span-1">
160
+ <Suspense fallback={<Skeleton className="h-32 w-full" />}>{config.customComponent}</Suspense>
161
+ </div>
162
+ )}
163
+
164
+ {/* Right column content - shows if rightColumnContent is provided OR if moving description to right */}
165
+ {(config.rightColumnContent || config.moveDescriptionToRight) && (
166
+ <div className="col-span-2 flex flex-col justify-start gap-4 md:col-span-1 md:gap-6 lg:col-span-1">
167
+ {/* Show description in right column if moveDescriptionToRight is true */}
168
+ {config.moveDescriptionToRight && (
169
+ <>
170
+ <p className="font-body text-sm font-medium leading-[1.43] text-ods-text-primary md:text-sm">
171
+ {config.description}
172
+ </p>
173
+
174
+ {/* Custom content below description - only if NOT keeping it on left */}
175
+ {config.belowDescriptionContent && !config.keepBelowDescriptionLeft && (
176
+ <Suspense fallback={<Skeleton className="h-8 w-full" />}>{config.belowDescriptionContent}</Suspense>
177
+ )}
178
+ </>
179
+ )}
180
+
181
+ {/* Regular right column content */}
182
+ {config.rightColumnContent && (
183
+ <Suspense fallback={<Skeleton className="h-32 w-full" />}>{config.rightColumnContent}</Suspense>
184
+ )}
185
+ </div>
186
+ )}
187
+ </div>
188
+
189
+ {/* Copyright */}
190
+ <p className="text-md md:text-md w-full pt-4 text-center font-body font-medium leading-[1.33] text-ods-text-muted md:pt-0">
191
+ {copyrightLine(config.legalName)}
192
+ </p>
193
+ </footer>
194
+ );
195
+ }
@@ -33,8 +33,10 @@ export * from './faq-accordion';
33
33
  export * from './faq';
34
34
  export * from './related-content';
35
35
  export * from './filter-chip';
36
+ export * from './footer';
36
37
  export * from './unified-filter-logic';
37
38
  export * from './unified-pagination';
39
+ export * from './footer-waitlist-button';
38
40
  export * from './hero-image-uploader';
39
41
  export * from './icons-block';
40
42
  export * from './image-cropper';
@@ -33,7 +33,7 @@ export function AnnouncementBarSkeleton() {
33
33
  }
34
34
 
35
35
  /**
36
- * Header skeleton for a page that is still loading: logo, navigation and action placeholders
36
+ * Header skeleton that matches the ClientOnlyHeader placeholder but with proper animations
37
37
  */
38
38
  export function HeaderSkeleton() {
39
39
  return (
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { useIsHydrated } from '../../hooks/ui/use-is-hydrated';
5
+ import { Header, type HeaderConfig } from './header';
6
+ import { HeaderSkeleton } from './header-skeleton';
7
+
8
+ export interface ClientOnlyHeaderProps {
9
+ config: HeaderConfig;
10
+ skeleton?: ReactNode;
11
+ }
12
+
13
+ export function ClientOnlyHeader({ config, skeleton }: ClientOnlyHeaderProps) {
14
+ const isClient = useIsHydrated();
15
+
16
+ if (!isClient) {
17
+ // Return custom skeleton or default skeleton while client-side JavaScript loads
18
+ return skeleton || <HeaderSkeleton config={config} />;
19
+ }
20
+
21
+ return <Header config={config} />;
22
+ }
@@ -0,0 +1,74 @@
1
+ import type { HeaderConfig } from '../../types/navigation';
2
+ import { cn } from '../../utils';
3
+
4
+ export interface HeaderSkeletonProps {
5
+ config?: HeaderConfig;
6
+ }
7
+
8
+ /**
9
+ * Loading skeleton mirroring the unified ODS top-navigation geometry
10
+ * (`TopNavigation`, Figma 2797-5978): 48px mobile / 56px md+, cell model.
11
+ * Keep in sync with `header.tsx` — a diverging skeleton makes the
12
+ * skeleton→live handoff jump.
13
+ */
14
+ export function HeaderSkeleton({ config }: HeaderSkeletonProps) {
15
+ const showNavigation = config?.navigation && config.navigation.items.length > 0;
16
+ const showActions = config?.actions?.right && config.actions.right.length > 0;
17
+ const showMobileMenu = config?.mobile?.enabled;
18
+ const showLeftActions = !!config?.actions?.left;
19
+
20
+ return (
21
+ <div className="sticky top-0 z-[50] w-full">
22
+ <header
23
+ className={cn(
24
+ 'flex h-[72px] w-full items-center border-b border-t border-ods-border md:border-t-0',
25
+ config?.backgroundColor || 'bg-ods-card',
26
+ config?.className,
27
+ )}
28
+ >
29
+ {/* Leading cells: admin toggle / burger */}
30
+ {showLeftActions && (
31
+ <div className="flex h-full w-[72px] items-center justify-center border-r border-ods-border">
32
+ <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
33
+ </div>
34
+ )}
35
+ {showMobileMenu && (
36
+ <div className="flex h-full w-[72px] items-center justify-center border-r border-ods-border lg:hidden">
37
+ <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
38
+ </div>
39
+ )}
40
+
41
+ {/* Logo zone */}
42
+ <div className="flex h-full flex-1 items-center gap-2 p-[var(--spacing-system-m)] md:pl-[var(--spacing-system-l)] lg:flex-none lg:pl-[var(--spacing-system-xxl)]">
43
+ <div className="h-6 w-6 animate-pulse rounded bg-ods-border" />
44
+ <div className="h-5 w-24 animate-pulse rounded bg-ods-border" />
45
+ </div>
46
+
47
+ {/* Center: navigation links (desktop only) */}
48
+ <div className="hidden h-full min-w-0 flex-1 items-center justify-center gap-2 lg:flex">
49
+ {showNavigation && (
50
+ <>
51
+ <div className="h-8 w-20 animate-pulse rounded bg-ods-border" />
52
+ <div className="h-8 w-28 animate-pulse rounded bg-ods-border" />
53
+ <div className="h-8 w-24 animate-pulse rounded bg-ods-border" />
54
+ </>
55
+ )}
56
+ </div>
57
+
58
+ {/* CTA zone */}
59
+ {showActions && (
60
+ <div className="hidden h-full items-center px-[var(--spacing-system-m)] lg:flex">
61
+ <div className="h-8 w-24 animate-pulse rounded bg-ods-border" />
62
+ </div>
63
+ )}
64
+
65
+ {/* Mingo cell */}
66
+ {config?.mingo?.enabled && (
67
+ <div className="flex h-full items-center border-l border-ods-border px-[var(--spacing-system-l)]">
68
+ <div className="h-8 w-8 animate-pulse rounded bg-ods-border md:w-28" />
69
+ </div>
70
+ )}
71
+ </header>
72
+ </div>
73
+ );
74
+ }