@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
@@ -131,6 +131,8 @@ const HISTORY_RAIL_WIDTH = 320;
131
131
  /** The v2 chat list (`MingoChatRail`, Figma `chat-sidebar`): the narrowest the v2 chat draws. */
132
132
  export const MINGO_V2_RAIL_WIDTH = 296;
133
133
  const CHAT_BLOCK_MIN_WIDTH = 400;
134
+ /** Mingo v2: the list and the chat side by side at their narrowest. */
135
+ export const MINGO_V2_SPLIT_WIDTH = MINGO_V2_RAIL_WIDTH + CHAT_BLOCK_MIN_WIDTH;
134
136
  const SPLIT_MIN_WIDTH = HISTORY_RAIL_WIDTH + CHAT_BLOCK_MIN_WIDTH;
135
137
 
136
138
  // Desktop drawer is this fraction of the viewport width (clamped to the
@@ -364,6 +366,12 @@ export interface EmbeddableChatProps {
364
366
  * the open chat, which stays open.
365
367
  */
366
368
  collapseTo?: 'list' | 'column';
369
+ /**
370
+ * v2: the |← control on the chat list while the panel shows the list alone.
371
+ * The host widens its panel; the chat opens the conversation open last (or
372
+ * the newest in the list), or a new chat when there is none.
373
+ */
374
+ onExpand?: () => void;
367
375
 
368
376
  /** v2: the status glyph at the end of a chat's row (working / unread / approval). */
369
377
  dialogStatusOf?: (dialog: DialogItem) => MingoDialogStatus | undefined;
@@ -1024,6 +1032,7 @@ function EmbeddableChatInner({
1024
1032
  appearance = CHAT_APPEARANCE.CLASSIC,
1025
1033
  onCollapse,
1026
1034
  collapseTo = 'list',
1035
+ onExpand,
1027
1036
  dialogStatusOf,
1028
1037
  userDisplayName,
1029
1038
  userAvatarUrl,
@@ -1601,18 +1610,25 @@ function EmbeddableChatInner({
1601
1610
  [handleNavigationClose],
1602
1611
  );
1603
1612
 
1613
+ // "Ask Mingo" about a row (a card or source chip in the thread, or a page's
1614
+ // `ask-ai:open-with-ref`): asks in the open conversation. Forwarded through a
1615
+ // ref because what it does depends on dialog state declared further down
1616
+ // (`askAboutRef.current` is set next to it: an archived chat is read-only).
1617
+ const askAboutRef = useRef<(reference: ChatRef) => void>(discussRef);
1618
+ const askAbout = useCallback((reference: ChatRef) => askAboutRef.current(reference), []);
1619
+
1604
1620
  // Host-provided renderer for inline entity cards — routes through the
1605
1621
  // shared dispatcher in lib's `entity-cards/dispatch.tsx`.
1606
1622
  const renderEntityCard = useCallback(
1607
1623
  (reference: ChatRef): React.ReactNode =>
1608
1624
  renderChatInlineEntityCard(reference, {
1609
- onDiscuss: discussRef,
1625
+ onDiscuss: askAbout,
1610
1626
  onDisplay: displayRef,
1611
1627
  baseRoute: resolvedBaseRoute,
1612
1628
  chipBasePlatform,
1613
1629
  extras,
1614
1630
  }),
1615
- [discussRef, displayRef, resolvedBaseRoute, chipBasePlatform, extras],
1631
+ [askAbout, displayRef, resolvedBaseRoute, chipBasePlatform, extras],
1616
1632
  );
1617
1633
 
1618
1634
  // Stable assistant-icon element. `<ChatMessageList>` forwards this prop to
@@ -1832,11 +1848,11 @@ function EmbeddableChatInner({
1832
1848
  ).detail;
1833
1849
  if (!detail || detail.source !== source) return;
1834
1850
  setIsOpen(true);
1835
- setTimeout(() => discussRef(detail.ref as ChatRef), 0);
1851
+ setTimeout(() => askAbout(detail.ref as ChatRef), 0);
1836
1852
  };
1837
1853
  window.addEventListener('ask-ai:open-with-ref', handler);
1838
1854
  return () => window.removeEventListener('ask-ai:open-with-ref', handler);
1839
- }, [source, discussRef, setIsOpen]);
1855
+ }, [source, askAbout, setIsOpen]);
1840
1856
 
1841
1857
  // Listen for plain "open chat" events (no row context). Fired by the
1842
1858
  // header MingoAiButton. Same strict source filter as `ask-ai:open-with-ref`
@@ -1960,12 +1976,12 @@ function EmbeddableChatInner({
1960
1976
  baseRoute={resolvedBaseRoute}
1961
1977
  chipBasePlatform={chipBasePlatform}
1962
1978
  onClose={handleNavigationClose}
1963
- onDiscuss={discussRef}
1979
+ onDiscuss={askAbout}
1964
1980
  />
1965
1981
  </div>
1966
1982
  );
1967
1983
  },
1968
- [chatLoading, messages.length, resolvedBaseRoute, chipBasePlatform, handleNavigationClose, discussRef],
1984
+ [chatLoading, messages.length, resolvedBaseRoute, chipBasePlatform, handleNavigationClose, askAbout],
1969
1985
  );
1970
1986
 
1971
1987
  // Host node for in-panel Radix portals (see the body wrapper below).
@@ -2176,6 +2192,37 @@ function EmbeddableChatInner({
2176
2192
  resetToNewChat();
2177
2193
  }, [resetToNewChat]);
2178
2194
 
2195
+ // "Ask Mingo" from an archived chat: the conversation is read-only (the
2196
+ // backend refuses writes), so the question starts a new chat instead. It is
2197
+ // queued until the host has let go of the archived dialog: until then its
2198
+ // `discussRef` still writes into the dialog it has open.
2199
+ const queuedAskRef = useRef<ChatRef | null>(null);
2200
+ useEffect(() => {
2201
+ askAboutRef.current = reference => {
2202
+ if (!isViewingArchived) {
2203
+ discussRef(reference);
2204
+ return;
2205
+ }
2206
+ queuedAskRef.current = reference;
2207
+ if (archiveOpen) closeArchive();
2208
+ resetToNewChat();
2209
+ setComposeOpen(true);
2210
+ };
2211
+ });
2212
+ useEffect(() => {
2213
+ const reference = queuedAskRef.current;
2214
+ if (!reference || activeDialogId != null) return;
2215
+ queuedAskRef.current = null;
2216
+ discussRef(reference);
2217
+ }, [activeDialogId, discussRef]);
2218
+
2219
+ // The conversation open last, for the |← control: going back to the list
2220
+ // closes it, and widening the panel again should bring it back.
2221
+ const lastDialogIdRef = useRef<string | null>(null);
2222
+ useEffect(() => {
2223
+ if (activeDialogId && !isViewingArchived) lastDialogIdRef.current = activeDialogId;
2224
+ }, [activeDialogId, isViewingArchived]);
2225
+
2179
2226
  // Host prefill (`prefillDraft`) — staged, not written at once. The composer
2180
2227
  // it goes into may only mount on the commit `resetToNewChat` +
2181
2228
  // `setComposeOpen(true)` produce (narrow layout: list → compose; or the
@@ -2654,6 +2701,17 @@ function EmbeddableChatInner({
2654
2701
  <MingoChatRail
2655
2702
  {...mingoRailProps}
2656
2703
  onNewChat={() => setComposeOpen(true)}
2704
+ onExpand={
2705
+ onExpand &&
2706
+ (() => {
2707
+ const last = lastDialogIdRef.current;
2708
+ const target = last && dialogs.some(d => d.id === last) ? last : dialogs[0]?.id;
2709
+ if (archiveOpen) closeArchive();
2710
+ if (target) handleSelectDialog(target);
2711
+ else handleNewChat();
2712
+ onExpand();
2713
+ })
2714
+ }
2657
2715
  className="w-full border-r-0 duration-200 animate-in fade-in-0"
2658
2716
  />
2659
2717
  ) : stackedListView ? (
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * CaseStudyCard (pure presentation). Two densities — `default` (vertical
5
- * detail) and `sm` (compact horizontal for chat-inline).
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).
6
7
  *
7
8
  * The card writes NO click logic — callers wrap with their own anchor
8
9
  * and pass the resolved detail URL via `href`.
@@ -12,9 +13,15 @@
12
13
  * `useOgPlaceholderUrl(...)`) → `bg-ods-bg`.
13
14
  */
14
15
 
16
+ import { useState } from 'react';
15
17
  import Image from '../../../embed-shims/next-image';
16
- import type { CaseStudy } from '../../../types/case-study';
18
+ import Link from '../../../embed-shims/next-link';
19
+ import type { CaseStudyCardData } from '../../../types/case-study';
17
20
  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';
18
25
  import { Card } from '../../ui/card';
19
26
  import {
20
27
  COMPACT_CARD_IMAGE_SLOT,
@@ -36,7 +43,7 @@ import { useEntityCardLink } from './use-entity-card-link';
36
43
  import { useEntityCardPlaceholder } from './use-entity-card-placeholder';
37
44
 
38
45
  export interface CaseStudyCardProps {
39
- study: CaseStudy;
46
+ study: CaseStudyCardData;
40
47
  /** Detail URL resolved by the caller. */
41
48
  href: string;
42
49
  /** When `_blank`, opens in a new tab. Set by chat dispatch via
@@ -46,7 +53,14 @@ export interface CaseStudyCardProps {
46
53
  targetPlatform?: string | null;
47
54
  /** OG placeholder URL, used when `study.featured_image` is missing. */
48
55
  placeholderUrl?: string | null;
49
- size?: 'default' | 'sm' | 'portrait';
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;
50
64
  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */
51
65
  showTypeBadge?: boolean;
52
66
  className?: string;
@@ -92,6 +106,22 @@ export function CaseStudyCardSkeleton({ size = 'default' }: { size?: 'default' |
92
106
  );
93
107
  }
94
108
 
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
+
95
125
  export function CaseStudyCard({
96
126
  study,
97
127
  href,
@@ -101,8 +131,11 @@ export function CaseStudyCard({
101
131
  placeholderUrl: placeholderUrlProp,
102
132
  size = 'default',
103
133
  showTypeBadge = true,
134
+ mediaMounted,
135
+ onNavigate,
104
136
  className,
105
137
  }: CaseStudyCardProps) {
138
+ const [hovered, setHovered] = useState(false);
106
139
  const { target, rel } = useEntityCardLink({
107
140
  href,
108
141
  targetPlatform,
@@ -149,6 +182,83 @@ export function CaseStudyCard({
149
182
  );
150
183
  }
151
184
 
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
+
152
262
  if (size === 'portrait') {
153
263
  // Rail/strip density — shared <EntityPortraitCard> shell.
154
264
  return (
@@ -99,5 +99,6 @@ export {
99
99
  type EmbeddableChatHandle,
100
100
  type EmbeddableChatProps,
101
101
  MINGO_V2_RAIL_WIDTH,
102
+ MINGO_V2_SPLIT_WIDTH,
102
103
  } from './embeddable-chat';
103
104
  export * from './proxy-credentials-panel';
@@ -4,6 +4,7 @@ import { type ReactNode, useEffect, useRef, useState } from 'react';
4
4
  import { cn } from '../../utils/cn';
5
5
  import {
6
6
  AlertCircleIcon,
7
+ Arrow02LeftIcon,
7
8
  ChatPlusIcon,
8
9
  ChatQuestionIcon,
9
10
  ChatsIcon,
@@ -69,6 +70,8 @@ export interface MingoChatRailProps {
69
70
  /** The list failed to load with nothing cached. */
70
71
  loadError?: boolean;
71
72
  onRetry?: () => void;
73
+ /** The |← control beside the logo: widen the panel to the list and the chat. */
74
+ onExpand?: () => void;
72
75
  className?: string;
73
76
  }
74
77
 
@@ -113,6 +116,7 @@ export function MingoChatRail({
113
116
  isLoadingHistory = false,
114
117
  loadError = false,
115
118
  onRetry,
119
+ onExpand,
116
120
  className,
117
121
  }: MingoChatRailProps) {
118
122
  const [searchOpen, setSearchOpen] = useState(!!searchQuery?.trim());
@@ -251,13 +255,26 @@ export function MingoChatRail({
251
255
  data-accent="mingo"
252
256
  aria-label="Mingo chats"
253
257
  logo={
254
- <MingoIcon
255
- color="currentColor"
256
- eyesColor="var(--ods-flamingo-cyan-base)"
257
- cornerColor="var(--ods-flamingo-cyan-base)"
258
- className="size-6 text-ods-text-primary"
259
- aria-hidden
260
- />
258
+ <div className="flex flex-1 items-center justify-between">
259
+ <MingoIcon
260
+ color="currentColor"
261
+ eyesColor="var(--ods-flamingo-cyan-base)"
262
+ cornerColor="var(--ods-flamingo-cyan-base)"
263
+ className="size-6 text-ods-text-primary"
264
+ aria-hidden
265
+ />
266
+ {onExpand && (
267
+ <Button
268
+ variant="transparent"
269
+ size="icon"
270
+ aria-label="Expand chat"
271
+ onClick={onExpand}
272
+ className="size-6 p-0 text-ods-text-secondary hover:text-ods-text-primary md:size-6"
273
+ >
274
+ <Arrow02LeftIcon size={24} />
275
+ </Button>
276
+ )}
277
+ </div>
261
278
  }
262
279
  items={items}
263
280
  sectionLabel="Your Current Chats"
@@ -30,6 +30,7 @@
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';
33
34
  import * as IconsV2 from '../../icons-v2-generated';
34
35
  import { HeadphoneIcon } from '../../icons-v2-generated/audio-and-visual/headphone-icon';
35
36
  import { VideoRecorderIcon } from '../../icons-v2-generated/audio-and-visual/video-recorder-icon';
@@ -52,6 +53,8 @@ import { GraduationCapIcon } from '../../icons-v2-generated/school/graduation-ca
52
53
  import { TicketIcon } from '../../icons-v2-generated/shopping/ticket-icon';
53
54
  import { Rocket02Icon } from '../../icons-v2-generated/vehicles-and-delivery/rocket-02-icon';
54
55
  import { FlamingoLogo } from '../../icons/flamingo-logo';
56
+ import { MiamiCyberGangLogoFaceOnly } from '../../icons/miami-cyber-gang-logo-face-only';
57
+ import { MlgLogo } from '../../icons/mlg-logo';
55
58
  import { OpenFrameLogo } from '../../icons/openframe-logo';
56
59
  import { OpenmspLogo } from '../../icons/openmsp-logo';
57
60
  import { getIconComponent } from './icon-registry';
@@ -71,8 +74,37 @@ function sizedLogo(Logo: ComponentType<{ className?: string; style?: CSSProperti
71
74
  return <Logo className={className} style={{ width: size, height: size }} />;
72
75
  };
73
76
  }
74
- const OpenmspLogoIcon = sizedLogo(OpenmspLogo);
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
+ );
75
93
  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
+ );
76
108
 
77
109
  /**
78
110
  * `OpenFrameLogo` from `components/icons/` predates the v2 set and:
@@ -138,6 +170,8 @@ export const ICON_ALIASES: Record<string, IconComponent> = {
138
170
  // (was the separate `renderSvgIcon` map; now ONE resolver everywhere).
139
171
  'openframe-logo': LogoOpenframeIcon,
140
172
  'openmsp-logo': OpenmspLogoIcon,
173
+ 'mlg-logo': MlgLogoIcon,
174
+ 'tmcg-logo': TmcgLogoIcon,
141
175
  flamingo: FlamingoLogoIcon,
142
176
  'flamingo-logo': FlamingoLogoIcon,
143
177
  megaphone: Megaphone,
@@ -33,10 +33,8 @@ 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';
37
36
  export * from './unified-filter-logic';
38
37
  export * from './unified-pagination';
39
- export * from './footer-waitlist-button';
40
38
  export * from './hero-image-uploader';
41
39
  export * from './icons-block';
42
40
  export * from './image-cropper';
@@ -33,7 +33,7 @@ export function AnnouncementBarSkeleton() {
33
33
  }
34
34
 
35
35
  /**
36
- * Header skeleton that matches the ClientOnlyHeader placeholder but with proper animations
36
+ * Header skeleton for a page that is still loading: logo, navigation and action placeholders
37
37
  */
38
38
  export function HeaderSkeleton() {
39
39
  return (
@@ -44,13 +44,23 @@ export interface AppLayoutSidePanelRenderState {
44
44
  collapsesTo: 'column' | 'minimum' | null;
45
45
  /** Step the panel back (see `collapsesTo`), animated. */
46
46
  collapse: () => void;
47
+ /** Docked at its minimum with room for more: `expand` widens it. */
48
+ canExpand: boolean;
49
+ /** Widen the panel from its minimum to `defaultWidth` (as much as fits), animated. */
50
+ expand: () => void;
47
51
  }
48
52
 
49
53
  export interface AppLayoutSidePanelConfig {
50
54
  /** Panel body. Told its width so it can choose what fits. */
51
55
  children: (state: AppLayoutSidePanelRenderState) => ReactNode;
52
- /** Narrowest docked width, and the width it starts at: the narrowest its content draws. */
56
+ /** Narrowest docked width: the narrowest its content draws. */
53
57
  minWidth: number;
58
+ /**
59
+ * Width it starts at until the user picks one (a first launch), and the
60
+ * width `expand` takes it back to from its minimum. As much of it as fits:
61
+ * with no room the panel stays narrower. Default `minWidth`.
62
+ */
63
+ defaultWidth?: number;
54
64
  /** Narrowest the content may get before the panel takes over. Default 400. */
55
65
  minContentWidth?: number;
56
66
  /** localStorage key for the chosen width. Without it the width lasts the session. */
@@ -75,6 +85,11 @@ export interface AppLayoutSidePanelConfig {
75
85
 
76
86
  /** Inset of the docked card from the window edge and the header. */
77
87
  export const SIDE_PANEL_INSET = 16;
88
+ /**
89
+ * The docked card's 1px border on both sides: its body is this much narrower
90
+ * than the panel's width. Add it to a content width to size the panel by it.
91
+ */
92
+ export const SIDE_PANEL_FRAME_WIDTH = 2;
78
93
  const MOBILE_QUERY = `not all and ${breakpoints.md}`;
79
94
  const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
80
95
  /** Length of the docked <-> full morph; keep in step with `duration-300` below. */
@@ -228,6 +243,8 @@ export interface AppLayoutSidePanelState extends SidePanelLayout {
228
243
  endMorph: () => void;
229
244
  collapsesTo: AppLayoutSidePanelRenderState['collapsesTo'];
230
245
  collapse: () => void;
246
+ canExpand: boolean;
247
+ expand: () => void;
231
248
  toggle: () => void;
232
249
  close: () => void;
233
250
  resize: (next: number) => void;
@@ -256,6 +273,7 @@ export function useAppLayoutSidePanel(
256
273
  const minContentWidth = config?.minContentWidth ?? 400;
257
274
  const collapsed = config?.collapsed ?? false;
258
275
  const minimum: SidePanelSize = { width: minWidth, expanded: false };
276
+ const defaultWidth = Math.max(minWidth, config?.defaultWidth ?? minWidth);
259
277
 
260
278
  const [rowWidth, setRowWidth] = useState(0);
261
279
  useIsomorphicLayoutEffect(() => {
@@ -271,7 +289,7 @@ export function useAppLayoutSidePanel(
271
289
  const reduceMotion = useMediaQuery(REDUCED_MOTION_QUERY) === true;
272
290
  const [morph, setMorph] = useState<SidePanelMorph | null>(null);
273
291
  const [liveWidth, setLiveWidth] = useState<number | null>(null);
274
- const [storedSize, setStoredSize] = useStoredSize(config?.storageKey, minimum);
292
+ const [storedSize, setStoredSize] = useStoredSize(config?.storageKey, { width: defaultWidth, expanded: false });
275
293
  const [ownIsOpen, setOwnIsOpen] = useState(false);
276
294
  const isOpen = config?.open ?? ownIsOpen;
277
295
  const onOpenChange = config?.onOpenChange;
@@ -340,6 +358,8 @@ export function useAppLayoutSidePanel(
340
358
  let collapsesTo: AppLayoutSidePanelRenderState['collapsesTo'] = null;
341
359
  if (layout.mode === 'full' && layout.canDock) collapsesTo = layout.dockedWidth > minWidth ? 'column' : 'minimum';
342
360
  else if (layout.mode === 'docked' && layout.width > minWidth) collapsesTo = 'minimum';
361
+ const canExpand =
362
+ layout.mode === 'docked' && layout.width <= minWidth && defaultWidth > minWidth && layout.maxDockedWidth > minWidth;
343
363
 
344
364
  return {
345
365
  ...layout,
@@ -362,6 +382,12 @@ export function useAppLayoutSidePanel(
362
382
  setSize({ width: minWidth, expanded: false });
363
383
  }
364
384
  },
385
+ canExpand,
386
+ expand: () => {
387
+ if (!canExpand) return;
388
+ animate('narrow');
389
+ dock(defaultWidth);
390
+ },
365
391
  toggle: () => setIsOpen(!isOpen),
366
392
  close: () => setIsOpen(false),
367
393
  resize,
@@ -586,6 +612,8 @@ export function AppLayoutSidePanel({ config, state }: AppLayoutSidePanelProps) {
586
612
  close,
587
613
  collapsesTo: state.collapsesTo,
588
614
  collapse: state.collapse,
615
+ canExpand: state.canExpand,
616
+ expand: state.expand,
589
617
  })}
590
618
  </div>
591
619
  </div>
@@ -1,20 +1,22 @@
1
1
  'use client';
2
2
 
3
3
  // Navigation component exports
4
- export { Header } from './header';
5
- export type { HeaderConfig, HeaderProps } from './header';
6
-
7
- export { MingoAiButton } from './mingo-ai-button';
4
+ // Site navigation: one data model (`SiteNav`), rendered by the header, its
5
+ // menu panels, the mobile sheet and the footer.
6
+ export { SiteHeader } from './site-header';
7
+ export type { SiteHeaderProps } from './site-header';
8
+ export { NavPanel, isMegaMenu } from './nav-panel';
9
+ export type { NavPanelProps } from './nav-panel';
10
+ export { NavItemRow } from './nav-item-row';
11
+ export type { NavItemRowProps, NavItemRowVariant } from './nav-item-row';
12
+ export { ASK_AI_OPEN_EVENT, MingoAiButton, openAskAi } from './mingo-ai-button';
8
13
  export type { MingoAiButtonProps } from './mingo-ai-button';
9
-
10
- export { ClientOnlyHeader } from './client-only-header';
11
- export type { ClientOnlyHeaderProps } from './client-only-header';
12
-
13
- export { HeaderSkeleton } from './header-skeleton';
14
- export type { HeaderSkeletonProps } from './header-skeleton';
15
-
16
- export { MobileNavPanel, MOBILE_NAV_PANEL_ID } from './mobile-nav-panel';
17
- export type { MobileNavPanelProps } from './mobile-nav-panel';
14
+ export { MobileNavSheet, MOBILE_NAV_SHEET_ID, siteNavHasMobileMenus } from './mobile-nav-sheet';
15
+ export type { MobileNavSheetProps } from './mobile-nav-sheet';
16
+ export { SiteFooter } from './site-footer';
17
+ export type { SiteFooterProps } from './site-footer';
18
+ export { defaultRenderSiteNavLink, navLinkLabel } from './site-nav-link';
19
+ export type { SiteNavLinkRenderer } from './site-nav-link';
18
20
 
19
21
  export { SlidingSidebar } from './sliding-sidebar';
20
22
  export type { SlidingSidebarProps } from './sliding-sidebar';
@@ -35,6 +37,7 @@ export type {
35
37
  AppLayoutSidePanelMode,
36
38
  AppLayoutSidePanelRenderState,
37
39
  } from './app-layout-side-panel';
40
+ export { SIDE_PANEL_FRAME_WIDTH } from './app-layout-side-panel';
38
41
 
39
42
  export {
40
43
  AppLayoutDrawer,
@@ -80,10 +83,15 @@ export type { NavigationNode } from './multi-level-navigation';
80
83
 
81
84
  // Re-export types from navigation types
82
85
  export type {
83
- MobileNavConfig,
86
+ NavFeature,
87
+ NavGroup,
88
+ NavLink,
89
+ NavMenu,
84
90
  NavigationItem,
85
91
  NavigationSidebarConfig,
86
92
  NavigationSidebarItem,
93
+ SiteNav,
94
+ SiteNavPrimaryCta,
87
95
  SlidingSidebarConfig,
88
96
  UnifiedSidebarUser,
89
97
  } from '../../types/navigation';