@flamingo-stack/openframe-frontend-core 0.0.296-snapshot.20260621021605 → 0.0.296

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 (276) hide show
  1. package/README.md +0 -9
  2. package/dist/chunk-26PKDALD.js +2379 -0
  3. package/dist/chunk-26PKDALD.js.map +1 -0
  4. package/dist/chunk-3MCHAFHB.js +89 -0
  5. package/dist/chunk-3MCHAFHB.js.map +1 -0
  6. package/dist/{chunk-PI4WSYQV.js → chunk-3ZXUQQL4.js} +2 -2
  7. package/dist/{chunk-WMSTJAZT.cjs → chunk-5E2HOSSH.cjs} +51 -913
  8. package/dist/chunk-5E2HOSSH.cjs.map +1 -0
  9. package/dist/{chunk-IL47XWV5.js → chunk-5P3B2LZW.js} +14 -8
  10. package/dist/{chunk-IL47XWV5.js.map → chunk-5P3B2LZW.js.map} +1 -1
  11. package/dist/chunk-66AANIOC.cjs +619 -0
  12. package/dist/chunk-66AANIOC.cjs.map +1 -0
  13. package/dist/{chunk-AD6C23QY.js → chunk-6GCI7JOE.js} +7 -8
  14. package/dist/{chunk-AD6C23QY.js.map → chunk-6GCI7JOE.js.map} +1 -1
  15. package/dist/chunk-6JINAOI7.cjs +311 -0
  16. package/dist/chunk-6JINAOI7.cjs.map +1 -0
  17. package/dist/{chunk-2QG57XOJ.js → chunk-7RIYT7ZH.js} +205 -1067
  18. package/dist/chunk-7RIYT7ZH.js.map +1 -0
  19. package/dist/{chunk-L6PSSIUQ.cjs → chunk-AQOWFSMB.cjs} +1 -1
  20. package/dist/chunk-AQOWFSMB.cjs.map +1 -0
  21. package/dist/chunk-BOCFIKYS.cjs +3009 -0
  22. package/dist/chunk-BOCFIKYS.cjs.map +1 -0
  23. package/dist/{chunk-54KNMC2R.cjs → chunk-D3LEFMOA.cjs} +3 -3
  24. package/dist/{chunk-54KNMC2R.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
  25. package/dist/chunk-D652TJBQ.js +3009 -0
  26. package/dist/chunk-D652TJBQ.js.map +1 -0
  27. package/dist/{chunk-PWQUAVA3.js → chunk-E4XABBSU.js} +98 -338
  28. package/dist/chunk-E4XABBSU.js.map +1 -0
  29. package/dist/{chunk-JALO4TAZ.js → chunk-EL6QLAWX.js} +55 -357
  30. package/dist/chunk-EL6QLAWX.js.map +1 -0
  31. package/dist/{chunk-6C526VNN.cjs → chunk-EYEW6PTA.cjs} +118 -358
  32. package/dist/chunk-EYEW6PTA.cjs.map +1 -0
  33. package/dist/chunk-FQJK446R.js +1606 -0
  34. package/dist/chunk-FQJK446R.js.map +1 -0
  35. package/dist/{chunk-4PSQS3SW.cjs → chunk-GLLDTKZK.cjs} +9 -7
  36. package/dist/chunk-GLLDTKZK.cjs.map +1 -0
  37. package/dist/{chunk-FQOTC3UU.cjs → chunk-IE6OU3WQ.cjs} +16 -318
  38. package/dist/chunk-IE6OU3WQ.cjs.map +1 -0
  39. package/dist/chunk-J54Z3OCR.cjs +1606 -0
  40. package/dist/chunk-J54Z3OCR.cjs.map +1 -0
  41. package/dist/{chunk-PC746XCO.js → chunk-K2PFPBMF.js} +5563 -15048
  42. package/dist/chunk-K2PFPBMF.js.map +1 -0
  43. package/dist/chunk-KXCRGTRN.cjs +2379 -0
  44. package/dist/chunk-KXCRGTRN.cjs.map +1 -0
  45. package/dist/{chunk-IZ7JSBFP.js → chunk-LCNMR277.js} +1 -1
  46. package/dist/chunk-LCNMR277.js.map +1 -0
  47. package/dist/chunk-LFGGF7OT.cjs +449 -0
  48. package/dist/chunk-LFGGF7OT.cjs.map +1 -0
  49. package/dist/chunk-M2OCXTNT.js +311 -0
  50. package/dist/chunk-M2OCXTNT.js.map +1 -0
  51. package/dist/{chunk-L7ULJKG7.js → chunk-MBFWU2EM.js} +10 -6
  52. package/dist/{chunk-L7ULJKG7.js.map → chunk-MBFWU2EM.js.map} +1 -1
  53. package/dist/chunk-ME4EVDFP.js +619 -0
  54. package/dist/chunk-ME4EVDFP.js.map +1 -0
  55. package/dist/chunk-OQ6X7ZOC.js +449 -0
  56. package/dist/chunk-OQ6X7ZOC.js.map +1 -0
  57. package/dist/{chunk-4TLE6VLU.js → chunk-OY7OF7E7.js} +24 -30
  58. package/dist/chunk-OY7OF7E7.js.map +1 -0
  59. package/dist/chunk-POKKCWKF.js +354 -0
  60. package/dist/chunk-POKKCWKF.js.map +1 -0
  61. package/dist/{chunk-GUTS7HGA.cjs → chunk-QHIXS3W2.cjs} +2514 -11999
  62. package/dist/chunk-QHIXS3W2.cjs.map +1 -0
  63. package/dist/chunk-TFSYSWPS.cjs +89 -0
  64. package/dist/chunk-TFSYSWPS.cjs.map +1 -0
  65. package/dist/{chunk-53FUMSZ5.cjs → chunk-W6M2FLLT.cjs} +46 -40
  66. package/dist/chunk-W6M2FLLT.cjs.map +1 -0
  67. package/dist/{chunk-3JIQVE7T.js → chunk-WHMATDVP.js} +15 -9
  68. package/dist/{chunk-3JIQVE7T.js.map → chunk-WHMATDVP.js.map} +1 -1
  69. package/dist/{chunk-YBYI62OE.cjs → chunk-X647HY3F.cjs} +37 -33
  70. package/dist/chunk-X647HY3F.cjs.map +1 -0
  71. package/dist/{chunk-UNVE2SDJ.cjs → chunk-X6BV7MB7.cjs} +31 -37
  72. package/dist/chunk-X6BV7MB7.cjs.map +1 -0
  73. package/dist/{chunk-7OVGB2DQ.cjs → chunk-XREEV72C.cjs} +25 -19
  74. package/dist/chunk-XREEV72C.cjs.map +1 -0
  75. package/dist/chunk-YETA25JW.cjs +354 -0
  76. package/dist/chunk-YETA25JW.cjs.map +1 -0
  77. package/dist/{chunk-FCDQNTDG.cjs → chunk-YIGPRLQY.cjs} +20 -21
  78. package/dist/chunk-YIGPRLQY.cjs.map +1 -0
  79. package/dist/{chunk-X4DOXQRT.js → chunk-ZP4AVIZP.js} +6 -4
  80. package/dist/{chunk-X4DOXQRT.js.map → chunk-ZP4AVIZP.js.map} +1 -1
  81. package/dist/components/chat/index.cjs +18 -8
  82. package/dist/components/chat/index.cjs.map +1 -1
  83. package/dist/components/chat/index.js +85 -75
  84. package/dist/components/contact/index.cjs +15 -8
  85. package/dist/components/contact/index.cjs.map +1 -1
  86. package/dist/components/contact/index.js +14 -7
  87. package/dist/components/docs/doc-viewer.d.ts +2 -39
  88. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  89. package/dist/components/docs/index.cjs +9 -17
  90. package/dist/components/docs/index.cjs.map +1 -1
  91. package/dist/components/docs/index.d.ts +0 -4
  92. package/dist/components/docs/index.d.ts.map +1 -1
  93. package/dist/components/docs/index.js +8 -16
  94. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  95. package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
  96. package/dist/components/embeds/index.cjs +15 -38
  97. package/dist/components/embeds/index.cjs.map +1 -1
  98. package/dist/components/embeds/index.d.ts +0 -8
  99. package/dist/components/embeds/index.d.ts.map +1 -1
  100. package/dist/components/embeds/index.js +17 -40
  101. package/dist/components/faq/index.cjs +16 -9
  102. package/dist/components/faq/index.cjs.map +1 -1
  103. package/dist/components/faq/index.js +15 -8
  104. package/dist/components/features/index.cjs +16 -8
  105. package/dist/components/features/index.cjs.map +1 -1
  106. package/dist/components/features/index.js +32 -24
  107. package/dist/components/index.cjs +452 -257
  108. package/dist/components/index.cjs.map +1 -1
  109. package/dist/components/index.js +976 -781
  110. package/dist/components/index.js.map +1 -1
  111. package/dist/components/layout/page-layout.d.ts +1 -10
  112. package/dist/components/layout/page-layout.d.ts.map +1 -1
  113. package/dist/components/layout/title-block.d.ts +1 -17
  114. package/dist/components/layout/title-block.d.ts.map +1 -1
  115. package/dist/components/navigation/index.cjs +15 -7
  116. package/dist/components/navigation/index.cjs.map +1 -1
  117. package/dist/components/navigation/index.js +17 -9
  118. package/dist/components/onboarding-guides/index.cjs +36 -35
  119. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  120. package/dist/components/onboarding-guides/index.js +14 -13
  121. package/dist/components/onboarding-guides/index.js.map +1 -1
  122. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
  123. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  124. package/dist/components/related-content/index.cjs +16 -9
  125. package/dist/components/related-content/index.cjs.map +1 -1
  126. package/dist/components/related-content/index.js +15 -8
  127. package/dist/components/shared/dev-section/dev-section-page.d.ts +0 -9
  128. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  129. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  130. package/dist/components/shared/dev-section/index.d.ts +1 -1
  131. package/dist/components/shared/dev-section/index.d.ts.map +1 -1
  132. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  133. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  134. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  135. package/dist/components/tickets/index.cjs +112 -100
  136. package/dist/components/tickets/index.cjs.map +1 -1
  137. package/dist/components/tickets/index.js +32 -20
  138. package/dist/components/tickets/index.js.map +1 -1
  139. package/dist/components/ui/file-manager/index.cjs +52 -50
  140. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  141. package/dist/components/ui/file-manager/index.js +6 -4
  142. package/dist/components/ui/file-manager/index.js.map +1 -1
  143. package/dist/components/ui/index.cjs +19 -13
  144. package/dist/components/ui/index.cjs.map +1 -1
  145. package/dist/components/ui/index.d.ts +0 -2
  146. package/dist/components/ui/index.d.ts.map +1 -1
  147. package/dist/components/ui/index.js +139 -133
  148. package/dist/components/ui/release-changelog-section.d.ts +2 -6
  149. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  150. package/dist/components/ui/simple-markdown-renderer.d.ts +8 -2
  151. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  152. package/dist/contexts/chat-runtime-context.d.ts +0 -14
  153. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  154. package/dist/contexts/index.cjs +3 -3
  155. package/dist/contexts/index.js +5 -5
  156. package/dist/embed-shims/index.cjs +3 -3
  157. package/dist/embed-shims/index.cjs.map +1 -1
  158. package/dist/embed-shims/index.js +4 -4
  159. package/dist/hooks/index.cjs +9 -4
  160. package/dist/hooks/index.cjs.map +1 -1
  161. package/dist/hooks/index.js +11 -6
  162. package/dist/index.cjs +20 -14
  163. package/dist/index.cjs.map +1 -1
  164. package/dist/index.js +364 -358
  165. package/dist/types/doc-source.d.ts +1 -31
  166. package/dist/types/doc-source.d.ts.map +1 -1
  167. package/dist/utils/index.cjs +0 -4
  168. package/dist/utils/index.cjs.map +1 -1
  169. package/dist/utils/index.d.ts +0 -1
  170. package/dist/utils/index.d.ts.map +1 -1
  171. package/dist/utils/index.js +1 -4
  172. package/dist/utils/index.js.map +1 -1
  173. package/package.json +1 -7
  174. package/src/components/chat/embeddable-chat.tsx +1 -1
  175. package/src/components/docs/doc-viewer.tsx +19 -111
  176. package/src/components/docs/index.ts +0 -17
  177. package/src/components/docs/use-document-tree.ts +0 -21
  178. package/src/components/embeds/embed-iframe.tsx +9 -7
  179. package/src/components/embeds/index.ts +0 -30
  180. package/src/components/embeds/og-link-preview.tsx +13 -13
  181. package/src/components/layout/page-layout.tsx +1 -14
  182. package/src/components/layout/title-block.tsx +62 -40
  183. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
  184. package/src/components/shared/dev-section/dev-section-page.tsx +1 -9
  185. package/src/components/shared/dev-section/dev-section-view.tsx +9 -14
  186. package/src/components/shared/dev-section/index.ts +1 -1
  187. package/src/components/shared/doc-search/use-doc-search.ts +3 -7
  188. package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
  189. package/src/components/shared/product-release/release-detail-page.tsx +4 -6
  190. package/src/components/ui/index.ts +0 -2
  191. package/src/components/ui/release-changelog-section.tsx +2 -7
  192. package/src/components/ui/simple-markdown-renderer.tsx +11 -7
  193. package/src/contexts/chat-runtime-context.tsx +0 -14
  194. package/src/types/doc-source.ts +1 -33
  195. package/src/utils/index.ts +0 -1
  196. package/dist/chunk-2QG57XOJ.js.map +0 -1
  197. package/dist/chunk-4PSQS3SW.cjs.map +0 -1
  198. package/dist/chunk-4TLE6VLU.js.map +0 -1
  199. package/dist/chunk-53FUMSZ5.cjs.map +0 -1
  200. package/dist/chunk-6C526VNN.cjs.map +0 -1
  201. package/dist/chunk-7OVGB2DQ.cjs.map +0 -1
  202. package/dist/chunk-F5OB2YAL.cjs +0 -144
  203. package/dist/chunk-F5OB2YAL.cjs.map +0 -1
  204. package/dist/chunk-FBWXMMRB.cjs +0 -2
  205. package/dist/chunk-FBWXMMRB.cjs.map +0 -1
  206. package/dist/chunk-FCDQNTDG.cjs.map +0 -1
  207. package/dist/chunk-FQOTC3UU.cjs.map +0 -1
  208. package/dist/chunk-GUTS7HGA.cjs.map +0 -1
  209. package/dist/chunk-GZ4C3XW6.js +0 -2
  210. package/dist/chunk-GZ4C3XW6.js.map +0 -1
  211. package/dist/chunk-IZ7JSBFP.js.map +0 -1
  212. package/dist/chunk-JALO4TAZ.js.map +0 -1
  213. package/dist/chunk-L6PSSIUQ.cjs.map +0 -1
  214. package/dist/chunk-PC746XCO.js.map +0 -1
  215. package/dist/chunk-PWQUAVA3.js.map +0 -1
  216. package/dist/chunk-SA2WPJVO.js +0 -144
  217. package/dist/chunk-SA2WPJVO.js.map +0 -1
  218. package/dist/chunk-UNVE2SDJ.cjs.map +0 -1
  219. package/dist/chunk-WMSTJAZT.cjs.map +0 -1
  220. package/dist/chunk-YBYI62OE.cjs.map +0 -1
  221. package/dist/components/case-studies/index.cjs +0 -126
  222. package/dist/components/case-studies/index.cjs.map +0 -1
  223. package/dist/components/case-studies/index.d.ts +0 -2
  224. package/dist/components/case-studies/index.d.ts.map +0 -1
  225. package/dist/components/case-studies/index.js +0 -126
  226. package/dist/components/case-studies/index.js.map +0 -1
  227. package/dist/components/case-studies/share-experience-section.d.ts +0 -48
  228. package/dist/components/case-studies/share-experience-section.d.ts.map +0 -1
  229. package/dist/components/docs/docs-hub-page.d.ts +0 -46
  230. package/dist/components/docs/docs-hub-page.d.ts.map +0 -1
  231. package/dist/components/docs/skeletons.d.ts +0 -32
  232. package/dist/components/docs/skeletons.d.ts.map +0 -1
  233. package/dist/components/docs/use-docs-resolve-link.d.ts +0 -20
  234. package/dist/components/docs/use-docs-resolve-link.d.ts.map +0 -1
  235. package/dist/components/embeds/embed-container.d.ts +0 -37
  236. package/dist/components/embeds/embed-container.d.ts.map +0 -1
  237. package/dist/components/embeds/file-download-card.d.ts +0 -18
  238. package/dist/components/embeds/file-download-card.d.ts.map +0 -1
  239. package/dist/components/embeds/linkedin-embed-client.d.ts +0 -8
  240. package/dist/components/embeds/linkedin-embed-client.d.ts.map +0 -1
  241. package/dist/components/embeds/markdown-image.d.ts +0 -5
  242. package/dist/components/embeds/markdown-image.d.ts.map +0 -1
  243. package/dist/components/embeds/reddit-embed-client.d.ts +0 -7
  244. package/dist/components/embeds/reddit-embed-client.d.ts.map +0 -1
  245. package/dist/components/embeds/rich-markdown-runtime.d.ts +0 -46
  246. package/dist/components/embeds/rich-markdown-runtime.d.ts.map +0 -1
  247. package/dist/components/embeds/twitter-embed-client.d.ts +0 -8
  248. package/dist/components/embeds/twitter-embed-client.d.ts.map +0 -1
  249. package/dist/components/layout/page-header.d.ts +0 -78
  250. package/dist/components/layout/page-header.d.ts.map +0 -1
  251. package/dist/components/layout/page-with-header.d.ts +0 -67
  252. package/dist/components/layout/page-with-header.d.ts.map +0 -1
  253. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  254. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  255. package/dist/utils/page-header-constants.d.ts +0 -15
  256. package/dist/utils/page-header-constants.d.ts.map +0 -1
  257. package/dist/utils/social-embed-cache.d.ts +0 -29
  258. package/dist/utils/social-embed-cache.d.ts.map +0 -1
  259. package/src/components/case-studies/index.ts +0 -4
  260. package/src/components/case-studies/share-experience-section.tsx +0 -185
  261. package/src/components/docs/docs-hub-page.tsx +0 -149
  262. package/src/components/docs/skeletons.tsx +0 -138
  263. package/src/components/docs/use-docs-resolve-link.ts +0 -52
  264. package/src/components/embeds/embed-container.tsx +0 -80
  265. package/src/components/embeds/file-download-card.tsx +0 -54
  266. package/src/components/embeds/linkedin-embed-client.tsx +0 -100
  267. package/src/components/embeds/markdown-image.tsx +0 -88
  268. package/src/components/embeds/reddit-embed-client.tsx +0 -550
  269. package/src/components/embeds/rich-markdown-runtime.tsx +0 -79
  270. package/src/components/embeds/twitter-embed-client.tsx +0 -308
  271. package/src/components/layout/page-header.tsx +0 -182
  272. package/src/components/layout/page-with-header.tsx +0 -110
  273. package/src/components/ui/rich-markdown-renderer.tsx +0 -1203
  274. package/src/utils/page-header-constants.ts +0 -15
  275. package/src/utils/social-embed-cache.ts +0 -391
  276. /package/dist/{chunk-PI4WSYQV.js.map → chunk-3ZXUQQL4.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/related-content/related-content-section.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * RelatedContentSection\n *\n * Renders content references grouped by type using the canonical card\n * components. MOVED from the hub (`components/shared/related-content-card.tsx`)\n * so any consuming app can embed it; the hub keeps a thin wrapper that\n * pre-binds its host-specific injections (nav hook, URL recomposition,\n * program configs, admin campaign card).\n *\n * THREE data modes (precedence top-down):\n * 1. CONTROLLED — `contentRefs` provided (even `[]`): render exactly those\n * refs, no suggestion fetch (the original investor-update behavior).\n * 2. SUGGESTION — `entityType` + `entityId` provided: self-fetch\n * `GET {apiBaseUrl}/api/related-content?entityType&entityId[&count][&excludeTypes]`\n * (the generic 5-tier engine's second web service). `minResults` maps to\n * `count`; absent → param not sent (server default applies). Each ref\n * carries a `reason` (data-only — never rendered, matching the\n * FaqSection/FaqWithReason precedent).\n * 3. SSR-HYDRATED suggestion — also pass `initialItems` (the server page\n * called the engine directly); the first client fetch is skipped per the\n * `useSelfFetch` initialData contract.\n *\n * Group layout (list vs grid) + card size (lg vs default) come from\n * `CONTENT_REF_GROUPS` in `../../utils/content-ref-groups` — single source of\n * truth, no per-type logic in this file. Skeletons come from\n * `renderSkeletonForType` so the placeholder height matches the loaded card\n * exactly (zero layout shift on resolve).\n *\n * One API call per content type via the shared list-URL builder\n * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested\n * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain\n * fetch, NO react-query) so third-party embedders need no QueryClientProvider;\n * cards are imported via DEEP module paths (not the chat barrel) so this\n * chunk never reaches `@tanstack/react-query`.\n *\n * LOCKSTEP NOTE: this file's per-type card/skeleton dispatch is the SIZED\n * sibling of the chat-side `CHAT_CARD_REGISTRY` (`../chat/entity-cards/\n * dispatch.tsx`), which renders compact `size='sm'` cards wired to the chat\n * runtime. Two dispatchers by design — when registering a new fetch-mode\n * content type, add it BOTH there and here (cards + skeleton + list URL).\n */\n\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n CONTENT_REF_GROUPS,\n getContentRefLabelOrTitleCase,\n orderContentRefTypes,\n type ContentRefGroupConfig,\n} from '../../utils/content-ref-groups';\nimport type { ContentRef, ContentRefWithReason } from '../../types/content-ref';\nimport { useSelfFetch } from '../../hooks/use-self-fetch';\nimport { Pagination } from '../pagination';\nimport { extractItems, extractItemId } from '../../utils/extract-items';\nimport { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';\nimport { buildSuggestionUrl } from '../../utils/suggestion-url';\nimport { decideNewTab } from '../chat/utils/decide-new-tab';\n// DEEP card imports — NOT the `../chat` barrel (the barrel statically reaches\n// @tanstack/react-query via embeddable-chat + its hooks). Deep paths keep this\n// component's SOURCE graph react-query-free. Note: tsup's shared-chunk\n// splitting may still colocate the cards with chat hooks in one dist chunk\n// (react-query is a required peerDep, so resolution always succeeds) — the\n// guarantee that matters here is the RUNTIME one: nothing on this path ever\n// instantiates a QueryClient, so embedders need NO QueryClientProvider.\nimport { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';\nimport { WhatIShippedCard, WhatIShippedCardSkeleton } from '../chat/entity-cards/what-i-shipped-card';\nimport { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';\nimport { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';\nimport { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';\nimport { buildProductReleaseCardProps } from '../chat/entity-cards/product-release-card-defaults';\nimport { ProgramCard, ProgramCardSkeleton } from '../chat/entity-cards/program-card';\nimport { InvestorUpdateCard, InvestorUpdateCardSkeleton } from '../chat/entity-cards/investor-update-card';\nimport { OnboardingGuideCard, OnboardingGuideCardSkeleton } from '../chat/entity-cards/onboarding-guide-card';\nimport { RoadmapCard, RoadmapCardSkeleton } from '../chat/entity-cards/roadmap-card';\n// Type-only — erased at build, no runtime dependency on the dispatch module.\nimport type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch';\n\ntype CardSize = 'lg' | 'default' | 'sm';\n\n/** Anchor prop bundle the per-card link surface receives — same shape the\n * hub's `useNavLink` returns and the chat dispatcher's anchor builders\n * produce. `null` = non-anchor mode (no URL). */\nexport interface CardLinkAnchorProps {\n href: string;\n target?: '_blank';\n rel?: 'noopener noreferrer';\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** Render-prop component injection for the navigation decision — keeps hook\n * calls legal (hooks live INSIDE the injected component; `CardForType`\n * itself calls zero hooks). The hub injects a `useNavLink`-backed provider;\n * the default is hook-free (pure `decideNewTab`). MUST be defined at module\n * scope by hosts — an inline arrow would remount every card each render. */\nexport interface CardLinkProviderProps {\n href: string | null;\n targetPlatform: string | null;\n children: (linkProps: CardLinkAnchorProps | null) => React.ReactElement | null;\n}\nexport type CardLinkProvider = React.ComponentType<CardLinkProviderProps>;\n\n/** Default link provider for standalone embeds: relative/-same-origin hrefs\n * stay same-tab, cross-origin pops a new tab (pure `decideNewTab` with no\n * platform context — `currentSource: ''` falls through to the origin\n * check). No router integration, no hooks. */\nfunction DefaultLinkPropsProvider({ href, targetPlatform, children }: CardLinkProviderProps): React.ReactElement | null {\n if (!href) return children(null);\n const newTab = decideNewTab({ href, targetPlatform, currentSource: '' });\n return children(\n newTab\n ? { href, target: '_blank', rel: 'noopener noreferrer' }\n : { href },\n );\n}\n\n/** Default href resolution: trust the ref's stored url/targetPlatform as the\n * API composed them. The hub overrides this with its `buildContentURL`\n * re-composition so dev gets localhost and prod gets platform domains. */\nfunction defaultResolveHref(ref: ContentRef): { href: string | null; targetPlatform: string | null } {\n return { href: ref.url || null, targetPlatform: ref.targetPlatform ?? null };\n}\n\n/** Host-injected renderer pair for the admin-only `marketing_campaign` type.\n * Absent (every non-hub embed) → the type renders nothing (its list URL\n * hits `/api/admin`, unreachable outside the hub anyway). */\nexport interface AdminCampaignCardSlot {\n Card: React.ComponentType<{ campaign: any }>;\n Skeleton: React.ComponentType<{ size?: 'default' | 'sm' }>;\n}\n\n/**\n * Per-type skeleton dispatch — returns the SAME colocated skeleton the\n * resolved card renders, sized to match (zero layout shift on resolve).\n * The chat-side `CHAT_CARD_REGISTRY` already does this via\n * `entry.skeleton()`; this surface exposes the same discipline to the\n * related-content rail.\n */\nfunction renderSkeletonForType(\n type: string,\n size: CardSize,\n adminCampaignCard?: AdminCampaignCardSlot,\n): React.ReactNode {\n // Most card skeletons accept only `{default, sm}`. `'lg'` collapses to\n // `'default'`. ProductReleaseCardSkeleton uses lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n switch (type) {\n case 'blog_post_existing':\n return <BlogCardSkeleton size={legacySize} />;\n case 'case_study':\n return <CaseStudyCardSkeleton size={legacySize} />;\n case 'customer_interview':\n return <CustomerInterviewCardSkeleton size={legacySize} />;\n case 'product_release':\n return <ProductReleaseCardSkeleton size={size === 'sm' ? 'sm' : 'lg'} />;\n case 'podcast':\n case 'webinar':\n case 'event':\n return <ProgramCardSkeleton size={legacySize} />;\n case 'investor_update':\n return <InvestorUpdateCardSkeleton size={legacySize} />;\n case 'onboarding_guide':\n // The rich catalog variant (hero + author grid, clamped description) —\n // the step-numbered 'default' variant is for the guide detail page's\n // \"More in section\" rail, not this full-width row.\n return <OnboardingGuideCardSkeleton size={size === 'sm' ? 'sm' : 'catalog'} />;\n case 'what_i_shipped':\n // Matches the WhatIShippedCard (AdminContentCard 3:2) shape.\n return <WhatIShippedCardSkeleton />;\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return <RoadmapCardSkeleton size={legacySize} />;\n default:\n return null;\n }\n}\n\n/**\n * Per-type card dispatch — renders the right card with the right size.\n * Sized cards (`'lg'` / `'default'`) are unique to this rail — the chat\n * dispatcher only renders `'sm'`, so we go directly through the per-type\n * cards here.\n *\n * PURE FUNCTION COMPONENT WITH ZERO HOOK CALLS: the placeholder comes from a\n * plain `extras.buildOgPlaceholderUrl` call (the chat `dispatch.tsx`\n * pattern) and the anchor-prop bundle arrives via the `LinkProvider`\n * render-prop from the parent — so per-card hook legality is owned by the\n * injected provider component, not by this switch.\n *\n * `href` comes from the host's `resolveHref(ref)` (hub: live\n * `buildContentURL` recomposition; default: the ref's stored url).\n */\nfunction CardForType({\n type,\n item,\n size,\n href,\n targetPlatform,\n linkProps,\n extras,\n adminCampaignCard,\n}: {\n type: string;\n item: any;\n size: CardSize;\n href: string;\n targetPlatform: string | null;\n linkProps: CardLinkAnchorProps | null;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n}): React.ReactNode {\n // Most card variants accept only `{default, sm}`. `'lg'` collapses to\n // `'default'` for those. ProductReleaseCard uses its own lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n // OG placeholder URL — injected into the pure-presentation cards so they\n // render a branded fallback when the row's featured image is null. Plain\n // function call (NOT a hook). Title is the universal field across all card\n // item shapes used here.\n const placeholderUrl =\n extras?.buildOgPlaceholderUrl?.((item?.title as string | undefined) ?? '') ?? undefined;\n\n // Top-level target/rel for cards that take them as separate props\n // (BlogCard, CaseStudyCard, …). ProductReleaseCard takes the bundle as a\n // single `anchorProps={...}` and uses `linkProps` directly. When the host\n // didn't surface a URL, `linkProps` is null and the card stays in\n // non-anchor mode.\n const anchorAttrs: Pick<CardLinkAnchorProps, 'target' | 'rel'> = linkProps\n ? { target: linkProps.target, rel: linkProps.rel }\n : {};\n\n switch (type) {\n case 'blog_post_existing':\n return <BlogCard post={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'case_study':\n return <CaseStudyCard study={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'customer_interview':\n return <CustomerInterviewCard interview={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'product_release': {\n // Anchor-prop pattern: build product-release lg-variant props from the\n // shared `buildProductReleaseCardProps` so this rail and the /releases\n // catalog page render byte-identically. The card wraps in\n // `<a {...anchorProps}>` ONLY when `anchorProps.href` is set — pass\n // `undefined` (not an empty object) when href is empty so the card\n // stays in non-anchor mode without rendering a dead <a> tag.\n const releaseSize = size === 'sm' ? 'sm' : 'lg';\n const buildReleaseProps = extras?.buildProductReleaseCardProps ?? buildProductReleaseCardProps;\n const releaseProps = buildReleaseProps(item);\n return (\n <ProductReleaseCard\n size={releaseSize}\n title={item.title}\n summary={item.summary}\n version={item.version}\n {...releaseProps}\n anchorProps={linkProps ?? undefined}\n />\n );\n }\n case 'podcast':\n return extras?.programConfigs?.podcast\n ? <ProgramCard config={extras.programConfigs.podcast} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'webinar':\n return extras?.programConfigs?.webinar\n ? <ProgramCard config={extras.programConfigs.webinar} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'event':\n return extras?.programConfigs?.event\n ? <ProgramCard config={extras.programConfigs.event} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'investor_update':\n return <InvestorUpdateCard update={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'onboarding_guide':\n // Catalog variant (see skeleton note) — full-width rich card with a\n // line-clamped description instead of the step-numbered rail card.\n return <OnboardingGuideCard guide={item} size={size === 'sm' ? 'sm' : 'catalog'} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'what_i_shipped':\n // THE single What I Shipped card — same lib component the people-hub\n // dashboard renders, so the card is identical in the rail and the\n // dashboard. `anchorProps` makes the whole card a click-through link\n // (rail is read-only — no owner actions).\n return (\n <WhatIShippedCard\n entry={item}\n placeholderUrl={placeholderUrl}\n // Only pass anchorProps when there's a REAL href — a fallback object\n // with `href: undefined` is still truthy and would make WhatIShippedCard\n // wrap the card in a dead <a> (no URL). Mirrors the ProductReleaseCard\n // `linkProps ?? undefined` pattern above.\n anchorProps={\n linkProps ??\n (href ? ({ href, ...anchorAttrs } as React.AnchorHTMLAttributes<HTMLAnchorElement>) : undefined)\n }\n />\n );\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return (\n <RoadmapCard\n item={item}\n href={href ?? ''}\n targetPlatform={targetPlatform}\n userVote={null}\n onVote={() => {}}\n size={legacySize}\n cardType={type as 'roadmap_item' | 'delivery_item' | 'internal_task'}\n {...anchorAttrs}\n />\n );\n default:\n return null;\n }\n}\n\n// =============================================================================\n// Fetch all items for a type in ONE server-sorted call, via the injectable\n// list-URL builder. `useSelfFetch` (URL = cache key) replaces the hub's old\n// react-query usage: `enabled` ≙ `url === null`, `!res.ok`/network error ≙\n// `error → items null → group renders nothing`. Accepted deltas vs\n// react-query: no retry/backoff, no focus refetch, no cross-mount cache.\n// =============================================================================\n\nfunction useGroupItems(\n type: string,\n refs: ContentRef[],\n buildUrl: (type: string, ids: string[]) => string | null,\n) {\n const ids = refs.map((r) => r.id);\n const url = ids.length > 0 ? buildUrl(type, ids) : null;\n const { data, isLoading } = useSelfFetch<unknown>(url);\n const items = data != null ? extractItems(data) : null;\n return { items, isLoading };\n}\n\n// =============================================================================\n// Per-group renderer — one API call, server-sorted, then render cards via the\n// dispatcher with per-type skeletons + per-type layout from CONTENT_REF_GROUPS.\n// =============================================================================\n\n/** Map columns prop → tailwind grid class. Only consulted for grid-layout\n * groups; list-layout groups stack vertically. */\nfunction gridClassFor(columns: 2 | 3): string {\n return columns === 3\n ? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'\n : 'grid grid-cols-1 sm:grid-cols-2 gap-6';\n}\n\n/** Resolve the group config for a type, falling back to a grid layout with\n * the default card size for unregistered types so the section still renders\n * rather than silently dropping them. The `label` field on the fallback is\n * intentionally a placeholder — the section heading goes through\n * `getContentRefLabelOrTitleCase(type)` instead so cross-surface labels\n * stay consistent between this rail and the investor-email builder. */\nfunction resolveGroupConfig(type: string): ContentRefGroupConfig {\n return CONTENT_REF_GROUPS[type] ?? {\n label: type,\n order: 999,\n layout: 'grid',\n gridSize: 'default',\n };\n}\n\n/** Items per page within one type group. Groups larger than this paginate\n * with the standard Pagination control (NO nested scrolling — a bounded\n * scrollbox inside the page traps wheel events and hides the sections\n * below it). MUST stay at or above the largest suggestion fill\n * (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so\n * current rails never paginate — only genuinely big groups (author pages)\n * do. Exported through the subpath barrel for the hub's module-load\n * assertion of that relation (entity-suggestion-sections.tsx). */\nexport const GROUP_PAGE_SIZE = 12;\n\nfunction ContentGroup({\n type,\n refs,\n columns,\n buildUrl,\n resolveHref,\n LinkProvider,\n extras,\n adminCampaignCard,\n heading,\n}: {\n type: string;\n refs: ContentRef[];\n columns: 2 | 3;\n buildUrl: (type: string, ids: string[]) => string | null;\n resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n LinkProvider: CardLinkProvider;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n /** Group heading, rendered INSIDE the group so a group that resolves to\n * nothing (fetch miss / unsupported type / missing program config) drops\n * its heading too — no orphaned titles. */\n heading: React.ReactNode;\n}) {\n const { items, isLoading } = useGroupItems(type, refs, buildUrl);\n const config = resolveGroupConfig(type);\n const isListLayout = config.layout === 'list';\n const cardSize = config.gridSize;\n\n // Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items\n // per page with the standard Pagination control below the group. Client-side\n // slicing — useGroupItems already fetched every row in one batched call, so\n // page flips are instant. Hooks live above every early return (file\n // convention). Page is clamped so a shrinking refs array (suggestion\n // refetch) can never strand the view past the last page, and RESET when the\n // ref set actually changes (shrink→grow must not return to a stale page).\n const [page, setPage] = useState(1);\n const refsKey = refs.map((r) => r.id).join('|');\n const prevRefsKeyRef = useRef(refsKey);\n useEffect(() => {\n if (prevRefsKeyRef.current !== refsKey) {\n prevRefsKeyRef.current = refsKey;\n setPage(1);\n }\n }, [refsKey]);\n const totalGroupPages = Math.max(1, Math.ceil(refs.length / GROUP_PAGE_SIZE));\n const safePage = Math.min(page, totalGroupPages);\n const visibleGroupRefs =\n refs.length > GROUP_PAGE_SIZE\n ? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE)\n : refs;\n const groupPagination =\n totalGroupPages > 1 ? (\n <Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />\n ) : null;\n\n // Skeleton gate: `isLoading && !items` — SSR HTML and the client's first\n // paint render identical skeletons (useSelfFetch starts isLoading=true on\n // both sides), and once items exist they are never replaced by skeletons.\n if (isLoading && !items) {\n const skeletons = visibleGroupRefs.map((r) => (\n <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>\n ));\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{skeletons}</div>\n ) : (\n <div className={gridClassFor(columns)}>{skeletons}</div>\n )}\n </div>\n );\n }\n\n if (!items || items.length === 0) return null;\n\n // Index fetched rows by id, then render in REF order — refs carry the\n // intended sequence (suggestion mode: the engine's tier order, so\n // same-platform/tag-matched items lead; controlled mode: the curated\n // display_order). The list APIs return rows date-sorted, which would\n // otherwise scramble that ordering (same-platform items sinking below\n // newer cross-platform ones).\n // Shared extractor (NOT raw `.id`) — some API shapes key items differently\n // (e.g. external_id types); raw access would silently drop valid items.\n const itemById = new Map(\n (items as any[]).map((it) => [extractItemId(type, it) ?? String((it as any)?.id), it]),\n );\n\n const cards = visibleGroupRefs\n .map((contentRef) => {\n const itemId = String(contentRef.id);\n const item = itemById.get(itemId);\n if (!item) return null;\n // Re-compose the URL via the host's resolver (hub: buildContentURL so\n // dev gets localhost and prod the right platform domain; default: the\n // ref's stored url as the API composed it).\n const resolved = resolveHref(contentRef);\n const href = resolved.href ?? '';\n const targetPlatform = resolved.targetPlatform ?? contentRef.targetPlatform ?? null;\n return (\n <div key={itemId}>\n <LinkProvider href={href || null} targetPlatform={targetPlatform}>\n {(linkProps) => (\n <CardForType\n type={type}\n item={item}\n size={cardSize}\n href={href}\n targetPlatform={targetPlatform}\n linkProps={linkProps}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n />\n )}\n </LinkProvider>\n </div>\n );\n })\n .filter(Boolean);\n\n if (cards.length === 0) {\n // Current PAGE resolved zero cards (rows deleted between the ref fetch\n // and the group fetch, or a stricter list-API gate dropped them). When a\n // pager exists the user must keep the controls to navigate back —\n // dropping the whole group would strand them. A genuinely empty group\n // (no pager) still vanishes with its heading.\n if (groupPagination) {\n return (\n <div className=\"space-y-4\">\n {heading}\n {groupPagination}\n </div>\n );\n }\n return null;\n }\n\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{cards}</div>\n ) : (\n <div className={gridClassFor(columns)}>{cards}</div>\n )}\n {groupPagination}\n </div>\n );\n}\n\n// =============================================================================\n// Main component\n// =============================================================================\n\ninterface RelatedContentResponse {\n refs: ContentRefWithReason[];\n}\n\nexport interface RelatedContentSectionProps {\n /** CONTROLLED mode (the original behavior). When defined — even `[]` — no\n * suggestion fetch runs and exactly these refs render. */\n contentRefs?: ContentRef[];\n /** SUGGESTION mode (with `entityId`): self-fetch suggestions for this host\n * entity from `{apiBaseUrl}/api/related-content`. Ignored when\n * `contentRefs` is provided. */\n entityType?: string;\n entityId?: number | string;\n /** AUTHOR mode: self-fetch ALL published content authored by this profile\n * from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type,\n * endless within each group). Ignored when `contentRefs` is provided;\n * takes precedence over the entityType/entityId suggestion scope.\n * SSR-hydrate via `initialItems`, same as suggestion mode. */\n authorId?: string;\n /** Maps to the suggestion API's `count` param — the PER-TYPE fill target\n * for every candidate type EXCEPT the host's own. Absent → param not sent\n * (server default applies). */\n minResults?: number;\n /** Maps to the suggestion API's `sameTypeCount` param — the budget for the\n * candidate type MATCHING the host's own `entityType` (same-type boost:\n * a blog post's rail leads with more blog posts). Absent → param not\n * sent (host's type uses the server's `count`). */\n sameTypeMinResults?: number;\n /** SSR hydrate for suggestion mode — the server page ran the engine and\n * drills the refs here; the first client fetch is skipped (useSelfFetch\n * initialData contract). */\n initialItems?: ContentRefWithReason[];\n /** Section title (default: \"Related Content\") */\n title?: string;\n /**\n * Grid columns at desktop. 2 = denser cards / wider summary (original\n * investor-update layout); 3 = more cards per row for dashboards.\n * Only consulted for grid-layout groups. Default: 2.\n */\n columns?: 2 | 3;\n /**\n * ContentRef.type values to exclude. Honored in ALL modes — controlled\n * mode post-filters (original behavior); suggestion mode ALSO forwards the\n * list verbatim as the API's `excludeTypes=` param so excluded types never\n * consume engine fill slots (`minResults` stays honored). The subtraction\n * happens SERVER-side — this component never mirrors the hub's candidate\n * list.\n */\n excludeTypes?: string[];\n /**\n * SUGGESTION-mode allow-list (rail vocabulary): which content types\n * participate in this rail. Sent verbatim as the API's `types=` param —\n * the SERVER intersects it with its own allowed candidate set, and\n * platform policy gates (e.g. internal-only types) ALWAYS win: the client\n * cannot request its way past them. Absent → all server-side candidates.\n */\n includeTypes?: string[];\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin). Applied to BOTH the suggestion fetch and the\n * default per-group list fetches. */\n apiBaseUrl?: string;\n /** Host injection bundle — REUSES the chat dispatcher's\n * `ChatCardDispatchExtras` (programConfigs, buildOgPlaceholderUrl,\n * buildProductReleaseCardProps override). Program groups render nothing\n * when their config is absent. */\n extras?: ChatCardDispatchExtras;\n /** Hub injects its `buildContentURL` recomposition; default uses the\n * ref's stored `url`/`targetPlatform` as the API composed them. */\n resolveHref?: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n /** Hub injects its registry-driven entity-list-api builder; default = the\n * lib's `buildListUrl(type, ids, apiBaseUrl)`. */\n buildListUrl?: (type: string, ids: string[]) => string | null;\n /** Hub injects a `useNavLink`-backed render-prop provider; default = pure\n * anchor via `decideNewTab`. MUST be a module-scope component. */\n LinkProvider?: CardLinkProvider;\n /** Renderer pair for the admin-only `marketing_campaign` type. Absent →\n * the type renders nothing. */\n adminCampaignCard?: AdminCampaignCardSlot;\n /** When true, render the section shell (title + an empty-state line) even\n * with ZERO refs, instead of returning null. Default false (the original\n * behavior — empty rail = no shell). Opt-in per host page (e.g. people-hub's\n * \"What I Shipped\", where the section should always be present). */\n showWhenEmpty?: boolean;\n /** Empty-state copy shown under the title when `showWhenEmpty` and no refs.\n * Default: \"No related content yet.\" */\n emptyStateText?: string;\n /** Custom empty-state node (e.g. a hub `<EmptyState/>`) rendered under the\n * title when `showWhenEmpty` and there are no refs — overrides\n * `emptyStateText`. Lets a host match its canonical empty state. */\n emptyState?: React.ReactNode;\n}\n\nexport function RelatedContentSection({\n contentRefs,\n entityType,\n entityId,\n authorId,\n minResults,\n sameTypeMinResults,\n includeTypes,\n initialItems,\n title = 'Related Content',\n columns = 2,\n excludeTypes,\n apiBaseUrl = '',\n extras,\n resolveHref = defaultResolveHref,\n buildListUrl,\n LinkProvider = DefaultLinkPropsProvider,\n adminCampaignCard,\n showWhenEmpty = false,\n emptyStateText = 'No related content yet.',\n emptyState,\n}: RelatedContentSectionProps) {\n // ── Hooks above EVERY early return (the original `if (!contentRefs.length)\n // return null` guard moved below them). ──\n\n // Suggestion-mode fetch URL — null in controlled mode (contentRefs defined,\n // even []) or when the entity scope is incomplete.\n // `includeTypes: []` is an explicit \"nothing participates\" — skip the fetch\n // entirely (an empty-string `types=` param would be dropped by the URL\n // builder and read server-side as \"all candidates\") AND ignore SSR refs.\n const suggestionsDisabled = includeTypes?.length === 0;\n // Shared type-filter params — one spelling for both fetch modes so a future\n // normalization (trim/dedupe) can't diverge between them.\n const typeFilterParams = {\n types: includeTypes !== undefined ? includeTypes.join(',') : undefined,\n excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,\n };\n // AUTHOR mode beats suggestion mode: when `authorId` is set the rail lists\n // everything that profile authored (the server returns ALL, no count).\n const authorUrl =\n contentRefs === undefined && authorId && !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n extraParams: { authorId, ...typeFilterParams },\n })\n : null;\n const suggestUrl =\n authorUrl ??\n (contentRefs === undefined &&\n entityType &&\n entityId !== undefined &&\n entityId !== null &&\n entityId !== '' &&\n !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n entityType,\n entityId,\n count: minResults,\n extraParams: {\n sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,\n ...typeFilterParams,\n },\n })\n : null);\n // Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],\n // and a fresh per-render object would loop setState under re-rendering\n // parents (the latent FaqSection bug, fixed there in the same change).\n const initialData = useMemo<RelatedContentResponse | undefined>(\n // An explicitly disabled rail (includeTypes: []) must ignore SSR-hydrated\n // refs too — otherwise useSelfFetch(null, {initialData}) keeps serving\n // initialItems and the \"nothing participates\" contract silently breaks.\n () => (!suggestionsDisabled && initialItems ? { refs: initialItems } : undefined),\n [initialItems, suggestionsDisabled],\n );\n const { data, isLoading } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });\n\n // Default group fetcher: the lib's byte-parity-tested builder, prefixed for\n // embeds. Memoized so group-fetch URLs stay value-stable across renders.\n const effectiveBuildListUrl = useMemo(\n () => buildListUrl ?? ((type: string, ids: string[]) => libBuildListUrl(type, ids, apiBaseUrl)),\n [buildListUrl, apiBaseUrl],\n );\n\n const refs: ContentRef[] = contentRefs ?? data?.refs ?? [];\n\n // Per-consumer type gating — drops refs whose `type` is in the exclude\n // list. In suggestion mode the server already subtracted these (the param\n // is forwarded above); the client filter stays as an idempotent guard and\n // IS the mechanism in controlled mode (original behavior).\n const exclude = new Set(excludeTypes || []);\n const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;\n // Zero refs (still loading in suggestion mode, or genuinely empty). Default:\n // no empty shell. Opt-in (`showWhenEmpty`): render the title + an empty-state\n // line so the section is always present (e.g. people-hub \"What I Shipped\").\n if (!visibleRefs.length) {\n if (!showWhenEmpty) return null; // non-showWhenEmpty consumers stay blank (unchanged)\n // Client-fetch loading (author/suggestion mode, no SSR initialItems): render a\n // SKELETON grid — reserves height + matches the rest of the app's loading, so\n // there's no blank-then-pop jump (the prior `return null` collapsed the tab to\n // zero height during the fetch). SSR controlled mode has isLoading=false → it\n // skips straight to the empty state below. Skeleton type = the requested rail\n // type (author mode passes a single `includeTypes`).\n if (isLoading) {\n const skeletonType = includeTypes?.[0] ?? entityType ?? 'blog_post_existing';\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n <div className={gridClassFor(columns)}>\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i}>{renderSkeletonForType(skeletonType, 'default', adminCampaignCard)}</div>\n ))}\n </div>\n </div>\n );\n }\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {emptyState ?? <p className=\"text-ods-text-secondary\">{emptyStateText}</p>}\n </div>\n );\n }\n\n const grouped: Record<string, ContentRef[]> = {};\n for (const ref of visibleRefs) {\n if (!grouped[ref.type]) grouped[ref.type] = [];\n grouped[ref.type].push(ref);\n }\n\n // Registered types in CONTENT_REF_GROUPS order, then any unregistered\n // types appended (same shape the investor-email builder uses — both\n // consume `orderContentRefTypes` so cross-surface ordering matches).\n // SAME-TYPE FIRST: when a host entityType is known (suggestion / SSR\n // modes), its own content-type group is hoisted to the top — a blog\n // post's rail leads with blog posts. Rail group keys are compared via\n // the shared alias canonicalizer (blog_post_existing ↔ blog_post).\n let orderedTypes = orderContentRefTypes(Object.keys(grouped));\n if (entityType) {\n // Canonicalize BOTH sides — hosts pass registry vocab ('blog_post') but\n // rail-vocab aliases ('blog_post_existing') are also legal inputs; a raw\n // comparison would silently lose the same-type-first hoist for aliases.\n const canonicalEntityType = canonicalContentRefType(entityType);\n const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === canonicalEntityType);\n if (sameType.length > 0) {\n orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== canonicalEntityType)];\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {orderedTypes.map((type) => (\n <ContentGroup\n key={type}\n type={type}\n refs={grouped[type]}\n columns={columns}\n buildUrl={effectiveBuildListUrl}\n resolveHref={resolveHref}\n LinkProvider={LinkProvider}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n heading={\n <h3 className=\"font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider\">\n {getContentRefLabelOrTitleCase(type)}\n </h3>\n }\n />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["buildListUrl"]}
1
+ {"version":3,"sources":["../src/components/related-content/related-content-section.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * RelatedContentSection\n *\n * Renders content references grouped by type using the canonical card\n * components. MOVED from the hub (`components/shared/related-content-card.tsx`)\n * so any consuming app can embed it; the hub keeps a thin wrapper that\n * pre-binds its host-specific injections (nav hook, URL recomposition,\n * program configs, admin campaign card).\n *\n * THREE data modes (precedence top-down):\n * 1. CONTROLLED — `contentRefs` provided (even `[]`): render exactly those\n * refs, no suggestion fetch (the original investor-update behavior).\n * 2. SUGGESTION — `entityType` + `entityId` provided: self-fetch\n * `GET {apiBaseUrl}/api/related-content?entityType&entityId[&count][&excludeTypes]`\n * (the generic 5-tier engine's second web service). `minResults` maps to\n * `count`; absent → param not sent (server default applies). Each ref\n * carries a `reason` (data-only — never rendered, matching the\n * FaqSection/FaqWithReason precedent).\n * 3. SSR-HYDRATED suggestion — also pass `initialItems` (the server page\n * called the engine directly); the first client fetch is skipped per the\n * `useSelfFetch` initialData contract.\n *\n * Group layout (list vs grid) + card size (lg vs default) come from\n * `CONTENT_REF_GROUPS` in `../../utils/content-ref-groups` — single source of\n * truth, no per-type logic in this file. Skeletons come from\n * `renderSkeletonForType` so the placeholder height matches the loaded card\n * exactly (zero layout shift on resolve).\n *\n * One API call per content type via the shared list-URL builder\n * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested\n * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain\n * fetch, NO react-query) so third-party embedders need no QueryClientProvider;\n * cards are imported via DEEP module paths (not the chat barrel) so this\n * chunk never reaches `@tanstack/react-query`.\n *\n * LOCKSTEP NOTE: this file's per-type card/skeleton dispatch is the SIZED\n * sibling of the chat-side `CHAT_CARD_REGISTRY` (`../chat/entity-cards/\n * dispatch.tsx`), which renders compact `size='sm'` cards wired to the chat\n * runtime. Two dispatchers by design — when registering a new fetch-mode\n * content type, add it BOTH there and here (cards + skeleton + list URL).\n */\n\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n CONTENT_REF_GROUPS,\n getContentRefLabelOrTitleCase,\n orderContentRefTypes,\n type ContentRefGroupConfig,\n} from '../../utils/content-ref-groups';\nimport type { ContentRef, ContentRefWithReason } from '../../types/content-ref';\nimport { useSelfFetch } from '../../hooks/use-self-fetch';\nimport { Pagination } from '../pagination';\nimport { extractItems, extractItemId } from '../../utils/extract-items';\nimport { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';\nimport { buildSuggestionUrl } from '../../utils/suggestion-url';\nimport { decideNewTab } from '../chat/utils/decide-new-tab';\n// DEEP card imports — NOT the `../chat` barrel (the barrel statically reaches\n// @tanstack/react-query via embeddable-chat + its hooks). Deep paths keep this\n// component's SOURCE graph react-query-free. Note: tsup's shared-chunk\n// splitting may still colocate the cards with chat hooks in one dist chunk\n// (react-query is a required peerDep, so resolution always succeeds) — the\n// guarantee that matters here is the RUNTIME one: nothing on this path ever\n// instantiates a QueryClient, so embedders need NO QueryClientProvider.\nimport { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';\nimport { WhatIShippedCard, WhatIShippedCardSkeleton } from '../chat/entity-cards/what-i-shipped-card';\nimport { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';\nimport { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';\nimport { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';\nimport { buildProductReleaseCardProps } from '../chat/entity-cards/product-release-card-defaults';\nimport { ProgramCard, ProgramCardSkeleton } from '../chat/entity-cards/program-card';\nimport { InvestorUpdateCard, InvestorUpdateCardSkeleton } from '../chat/entity-cards/investor-update-card';\nimport { OnboardingGuideCard, OnboardingGuideCardSkeleton } from '../chat/entity-cards/onboarding-guide-card';\nimport { RoadmapCard, RoadmapCardSkeleton } from '../chat/entity-cards/roadmap-card';\n// Type-only — erased at build, no runtime dependency on the dispatch module.\nimport type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch';\n\ntype CardSize = 'lg' | 'default' | 'sm';\n\n/** Anchor prop bundle the per-card link surface receives — same shape the\n * hub's `useNavLink` returns and the chat dispatcher's anchor builders\n * produce. `null` = non-anchor mode (no URL). */\nexport interface CardLinkAnchorProps {\n href: string;\n target?: '_blank';\n rel?: 'noopener noreferrer';\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** Render-prop component injection for the navigation decision — keeps hook\n * calls legal (hooks live INSIDE the injected component; `CardForType`\n * itself calls zero hooks). The hub injects a `useNavLink`-backed provider;\n * the default is hook-free (pure `decideNewTab`). MUST be defined at module\n * scope by hosts — an inline arrow would remount every card each render. */\nexport interface CardLinkProviderProps {\n href: string | null;\n targetPlatform: string | null;\n children: (linkProps: CardLinkAnchorProps | null) => React.ReactElement | null;\n}\nexport type CardLinkProvider = React.ComponentType<CardLinkProviderProps>;\n\n/** Default link provider for standalone embeds: relative/-same-origin hrefs\n * stay same-tab, cross-origin pops a new tab (pure `decideNewTab` with no\n * platform context — `currentSource: ''` falls through to the origin\n * check). No router integration, no hooks. */\nfunction DefaultLinkPropsProvider({ href, targetPlatform, children }: CardLinkProviderProps): React.ReactElement | null {\n if (!href) return children(null);\n const newTab = decideNewTab({ href, targetPlatform, currentSource: '' });\n return children(\n newTab\n ? { href, target: '_blank', rel: 'noopener noreferrer' }\n : { href },\n );\n}\n\n/** Default href resolution: trust the ref's stored url/targetPlatform as the\n * API composed them. The hub overrides this with its `buildContentURL`\n * re-composition so dev gets localhost and prod gets platform domains. */\nfunction defaultResolveHref(ref: ContentRef): { href: string | null; targetPlatform: string | null } {\n return { href: ref.url || null, targetPlatform: ref.targetPlatform ?? null };\n}\n\n/** Host-injected renderer pair for the admin-only `marketing_campaign` type.\n * Absent (every non-hub embed) → the type renders nothing (its list URL\n * hits `/api/admin`, unreachable outside the hub anyway). */\nexport interface AdminCampaignCardSlot {\n Card: React.ComponentType<{ campaign: any }>;\n Skeleton: React.ComponentType<{ size?: 'default' | 'sm' }>;\n}\n\n/**\n * Per-type skeleton dispatch — returns the SAME colocated skeleton the\n * resolved card renders, sized to match (zero layout shift on resolve).\n * The chat-side `CHAT_CARD_REGISTRY` already does this via\n * `entry.skeleton()`; this surface exposes the same discipline to the\n * related-content rail.\n */\nfunction renderSkeletonForType(\n type: string,\n size: CardSize,\n adminCampaignCard?: AdminCampaignCardSlot,\n): React.ReactNode {\n // Most card skeletons accept only `{default, sm}`. `'lg'` collapses to\n // `'default'`. ProductReleaseCardSkeleton uses lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n switch (type) {\n case 'blog_post_existing':\n return <BlogCardSkeleton size={legacySize} />;\n case 'case_study':\n return <CaseStudyCardSkeleton size={legacySize} />;\n case 'customer_interview':\n return <CustomerInterviewCardSkeleton size={legacySize} />;\n case 'product_release':\n return <ProductReleaseCardSkeleton size={size === 'sm' ? 'sm' : 'lg'} />;\n case 'podcast':\n case 'webinar':\n case 'event':\n return <ProgramCardSkeleton size={legacySize} />;\n case 'investor_update':\n return <InvestorUpdateCardSkeleton size={legacySize} />;\n case 'onboarding_guide':\n // The rich catalog variant (hero + author grid, clamped description) —\n // the step-numbered 'default' variant is for the guide detail page's\n // \"More in section\" rail, not this full-width row.\n return <OnboardingGuideCardSkeleton size={size === 'sm' ? 'sm' : 'catalog'} />;\n case 'what_i_shipped':\n // Matches the WhatIShippedCard (AdminContentCard 3:2) shape.\n return <WhatIShippedCardSkeleton />;\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return <RoadmapCardSkeleton size={legacySize} />;\n default:\n return null;\n }\n}\n\n/**\n * Per-type card dispatch — renders the right card with the right size.\n * Sized cards (`'lg'` / `'default'`) are unique to this rail — the chat\n * dispatcher only renders `'sm'`, so we go directly through the per-type\n * cards here.\n *\n * PURE FUNCTION COMPONENT WITH ZERO HOOK CALLS: the placeholder comes from a\n * plain `extras.buildOgPlaceholderUrl` call (the chat `dispatch.tsx`\n * pattern) and the anchor-prop bundle arrives via the `LinkProvider`\n * render-prop from the parent — so per-card hook legality is owned by the\n * injected provider component, not by this switch.\n *\n * `href` comes from the host's `resolveHref(ref)` (hub: live\n * `buildContentURL` recomposition; default: the ref's stored url).\n */\nfunction CardForType({\n type,\n item,\n size,\n href,\n targetPlatform,\n linkProps,\n extras,\n adminCampaignCard,\n}: {\n type: string;\n item: any;\n size: CardSize;\n href: string;\n targetPlatform: string | null;\n linkProps: CardLinkAnchorProps | null;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n}): React.ReactNode {\n // Most card variants accept only `{default, sm}`. `'lg'` collapses to\n // `'default'` for those. ProductReleaseCard uses its own lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n // OG placeholder URL — injected into the pure-presentation cards so they\n // render a branded fallback when the row's featured image is null. Plain\n // function call (NOT a hook). Title is the universal field across all card\n // item shapes used here.\n const placeholderUrl =\n extras?.buildOgPlaceholderUrl?.((item?.title as string | undefined) ?? '') ?? undefined;\n\n // Top-level target/rel for cards that take them as separate props\n // (BlogCard, CaseStudyCard, …). ProductReleaseCard takes the bundle as a\n // single `anchorProps={...}` and uses `linkProps` directly. When the host\n // didn't surface a URL, `linkProps` is null and the card stays in\n // non-anchor mode.\n const anchorAttrs: Pick<CardLinkAnchorProps, 'target' | 'rel'> = linkProps\n ? { target: linkProps.target, rel: linkProps.rel }\n : {};\n\n switch (type) {\n case 'blog_post_existing':\n return <BlogCard post={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'case_study':\n return <CaseStudyCard study={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'customer_interview':\n return <CustomerInterviewCard interview={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'product_release': {\n // Anchor-prop pattern: build product-release lg-variant props from the\n // shared `buildProductReleaseCardProps` so this rail and the /releases\n // catalog page render byte-identically. The card wraps in\n // `<a {...anchorProps}>` ONLY when `anchorProps.href` is set — pass\n // `undefined` (not an empty object) when href is empty so the card\n // stays in non-anchor mode without rendering a dead <a> tag.\n const releaseSize = size === 'sm' ? 'sm' : 'lg';\n const buildReleaseProps = extras?.buildProductReleaseCardProps ?? buildProductReleaseCardProps;\n const releaseProps = buildReleaseProps(item);\n return (\n <ProductReleaseCard\n size={releaseSize}\n title={item.title}\n summary={item.summary}\n version={item.version}\n {...releaseProps}\n anchorProps={linkProps ?? undefined}\n />\n );\n }\n case 'podcast':\n return extras?.programConfigs?.podcast\n ? <ProgramCard config={extras.programConfigs.podcast} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'webinar':\n return extras?.programConfigs?.webinar\n ? <ProgramCard config={extras.programConfigs.webinar} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'event':\n return extras?.programConfigs?.event\n ? <ProgramCard config={extras.programConfigs.event} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'investor_update':\n return <InvestorUpdateCard update={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'onboarding_guide':\n // Catalog variant (see skeleton note) — full-width rich card with a\n // line-clamped description instead of the step-numbered rail card.\n return <OnboardingGuideCard guide={item} size={size === 'sm' ? 'sm' : 'catalog'} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'what_i_shipped':\n // THE single What I Shipped card — same lib component the people-hub\n // dashboard renders, so the card is identical in the rail and the\n // dashboard. `anchorProps` makes the whole card a click-through link\n // (rail is read-only — no owner actions).\n return (\n <WhatIShippedCard\n entry={item}\n placeholderUrl={placeholderUrl}\n // Only pass anchorProps when there's a REAL href — a fallback object\n // with `href: undefined` is still truthy and would make WhatIShippedCard\n // wrap the card in a dead <a> (no URL). Mirrors the ProductReleaseCard\n // `linkProps ?? undefined` pattern above.\n anchorProps={\n linkProps ??\n (href ? ({ href, ...anchorAttrs } as React.AnchorHTMLAttributes<HTMLAnchorElement>) : undefined)\n }\n />\n );\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return (\n <RoadmapCard\n item={item}\n href={href ?? ''}\n targetPlatform={targetPlatform}\n userVote={null}\n onVote={() => {}}\n size={legacySize}\n cardType={type as 'roadmap_item' | 'delivery_item' | 'internal_task'}\n {...anchorAttrs}\n />\n );\n default:\n return null;\n }\n}\n\n// =============================================================================\n// Fetch all items for a type in ONE server-sorted call, via the injectable\n// list-URL builder. `useSelfFetch` (URL = cache key) replaces the hub's old\n// react-query usage: `enabled` ≙ `url === null`, `!res.ok`/network error ≙\n// `error → items null → group renders nothing`. Accepted deltas vs\n// react-query: no retry/backoff, no focus refetch, no cross-mount cache.\n// =============================================================================\n\nfunction useGroupItems(\n type: string,\n refs: ContentRef[],\n buildUrl: (type: string, ids: string[]) => string | null,\n) {\n const ids = refs.map((r) => r.id);\n const url = ids.length > 0 ? buildUrl(type, ids) : null;\n const { data, isLoading } = useSelfFetch<unknown>(url);\n const items = data != null ? extractItems(data) : null;\n return { items, isLoading };\n}\n\n// =============================================================================\n// Per-group renderer — one API call, server-sorted, then render cards via the\n// dispatcher with per-type skeletons + per-type layout from CONTENT_REF_GROUPS.\n// =============================================================================\n\n/** Map columns prop → tailwind grid class. Only consulted for grid-layout\n * groups; list-layout groups stack vertically. */\nfunction gridClassFor(columns: 2 | 3): string {\n return columns === 3\n ? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'\n : 'grid grid-cols-1 sm:grid-cols-2 gap-6';\n}\n\n/** Resolve the group config for a type, falling back to a grid layout with\n * the default card size for unregistered types so the section still renders\n * rather than silently dropping them. The `label` field on the fallback is\n * intentionally a placeholder — the section heading goes through\n * `getContentRefLabelOrTitleCase(type)` instead so cross-surface labels\n * stay consistent between this rail and the investor-email builder. */\nfunction resolveGroupConfig(type: string): ContentRefGroupConfig {\n return CONTENT_REF_GROUPS[type] ?? {\n label: type,\n order: 999,\n layout: 'grid',\n gridSize: 'default',\n };\n}\n\n/** Items per page within one type group. Groups larger than this paginate\n * with the standard Pagination control (NO nested scrolling — a bounded\n * scrollbox inside the page traps wheel events and hides the sections\n * below it). MUST stay at or above the largest suggestion fill\n * (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so\n * current rails never paginate — only genuinely big groups (author pages)\n * do. Exported through the subpath barrel for the hub's module-load\n * assertion of that relation (entity-suggestion-sections.tsx). */\nexport const GROUP_PAGE_SIZE = 12;\n\nfunction ContentGroup({\n type,\n refs,\n columns,\n buildUrl,\n resolveHref,\n LinkProvider,\n extras,\n adminCampaignCard,\n heading,\n}: {\n type: string;\n refs: ContentRef[];\n columns: 2 | 3;\n buildUrl: (type: string, ids: string[]) => string | null;\n resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n LinkProvider: CardLinkProvider;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n /** Group heading, rendered INSIDE the group so a group that resolves to\n * nothing (fetch miss / unsupported type / missing program config) drops\n * its heading too — no orphaned titles. */\n heading: React.ReactNode;\n}) {\n const { items, isLoading } = useGroupItems(type, refs, buildUrl);\n const config = resolveGroupConfig(type);\n const isListLayout = config.layout === 'list';\n const cardSize = config.gridSize;\n\n // Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items\n // per page with the standard Pagination control below the group. Client-side\n // slicing — useGroupItems already fetched every row in one batched call, so\n // page flips are instant. Hooks live above every early return (file\n // convention). Page is clamped so a shrinking refs array (suggestion\n // refetch) can never strand the view past the last page, and RESET when the\n // ref set actually changes (shrink→grow must not return to a stale page).\n const [page, setPage] = useState(1);\n const refsKey = refs.map((r) => r.id).join('|');\n const prevRefsKeyRef = useRef(refsKey);\n useEffect(() => {\n if (prevRefsKeyRef.current !== refsKey) {\n prevRefsKeyRef.current = refsKey;\n setPage(1);\n }\n }, [refsKey]);\n const totalGroupPages = Math.max(1, Math.ceil(refs.length / GROUP_PAGE_SIZE));\n const safePage = Math.min(page, totalGroupPages);\n const visibleGroupRefs =\n refs.length > GROUP_PAGE_SIZE\n ? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE)\n : refs;\n const groupPagination =\n totalGroupPages > 1 ? (\n <Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />\n ) : null;\n\n // Skeleton gate: `isLoading && !items` — SSR HTML and the client's first\n // paint render identical skeletons (useSelfFetch starts isLoading=true on\n // both sides), and once items exist they are never replaced by skeletons.\n if (isLoading && !items) {\n const skeletons = visibleGroupRefs.map((r) => (\n <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>\n ));\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{skeletons}</div>\n ) : (\n <div className={gridClassFor(columns)}>{skeletons}</div>\n )}\n </div>\n );\n }\n\n if (!items || items.length === 0) return null;\n\n // Index fetched rows by id, then render in REF order — refs carry the\n // intended sequence (suggestion mode: the engine's tier order, so\n // same-platform/tag-matched items lead; controlled mode: the curated\n // display_order). The list APIs return rows date-sorted, which would\n // otherwise scramble that ordering (same-platform items sinking below\n // newer cross-platform ones).\n // Shared extractor (NOT raw `.id`) — some API shapes key items differently\n // (e.g. external_id types); raw access would silently drop valid items.\n const itemById = new Map(\n (items as any[]).map((it) => [extractItemId(type, it) ?? String((it as any)?.id), it]),\n );\n\n const cards = visibleGroupRefs\n .map((contentRef) => {\n const itemId = String(contentRef.id);\n const item = itemById.get(itemId);\n if (!item) return null;\n // Re-compose the URL via the host's resolver (hub: buildContentURL so\n // dev gets localhost and prod the right platform domain; default: the\n // ref's stored url as the API composed it).\n const resolved = resolveHref(contentRef);\n const href = resolved.href ?? '';\n const targetPlatform = resolved.targetPlatform ?? contentRef.targetPlatform ?? null;\n return (\n <div key={itemId}>\n <LinkProvider href={href || null} targetPlatform={targetPlatform}>\n {(linkProps) => (\n <CardForType\n type={type}\n item={item}\n size={cardSize}\n href={href}\n targetPlatform={targetPlatform}\n linkProps={linkProps}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n />\n )}\n </LinkProvider>\n </div>\n );\n })\n .filter(Boolean);\n\n if (cards.length === 0) {\n // Current PAGE resolved zero cards (rows deleted between the ref fetch\n // and the group fetch, or a stricter list-API gate dropped them). When a\n // pager exists the user must keep the controls to navigate back —\n // dropping the whole group would strand them. A genuinely empty group\n // (no pager) still vanishes with its heading.\n if (groupPagination) {\n return (\n <div className=\"space-y-4\">\n {heading}\n {groupPagination}\n </div>\n );\n }\n return null;\n }\n\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{cards}</div>\n ) : (\n <div className={gridClassFor(columns)}>{cards}</div>\n )}\n {groupPagination}\n </div>\n );\n}\n\n// =============================================================================\n// Main component\n// =============================================================================\n\ninterface RelatedContentResponse {\n refs: ContentRefWithReason[];\n}\n\nexport interface RelatedContentSectionProps {\n /** CONTROLLED mode (the original behavior). When defined — even `[]` — no\n * suggestion fetch runs and exactly these refs render. */\n contentRefs?: ContentRef[];\n /** SUGGESTION mode (with `entityId`): self-fetch suggestions for this host\n * entity from `{apiBaseUrl}/api/related-content`. Ignored when\n * `contentRefs` is provided. */\n entityType?: string;\n entityId?: number | string;\n /** AUTHOR mode: self-fetch ALL published content authored by this profile\n * from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type,\n * endless within each group). Ignored when `contentRefs` is provided;\n * takes precedence over the entityType/entityId suggestion scope.\n * SSR-hydrate via `initialItems`, same as suggestion mode. */\n authorId?: string;\n /** Maps to the suggestion API's `count` param — the PER-TYPE fill target\n * for every candidate type EXCEPT the host's own. Absent → param not sent\n * (server default applies). */\n minResults?: number;\n /** Maps to the suggestion API's `sameTypeCount` param — the budget for the\n * candidate type MATCHING the host's own `entityType` (same-type boost:\n * a blog post's rail leads with more blog posts). Absent → param not\n * sent (host's type uses the server's `count`). */\n sameTypeMinResults?: number;\n /** SSR hydrate for suggestion mode — the server page ran the engine and\n * drills the refs here; the first client fetch is skipped (useSelfFetch\n * initialData contract). */\n initialItems?: ContentRefWithReason[];\n /** Section title (default: \"Related Content\") */\n title?: string;\n /**\n * Grid columns at desktop. 2 = denser cards / wider summary (original\n * investor-update layout); 3 = more cards per row for dashboards.\n * Only consulted for grid-layout groups. Default: 2.\n */\n columns?: 2 | 3;\n /**\n * ContentRef.type values to exclude. Honored in ALL modes — controlled\n * mode post-filters (original behavior); suggestion mode ALSO forwards the\n * list verbatim as the API's `excludeTypes=` param so excluded types never\n * consume engine fill slots (`minResults` stays honored). The subtraction\n * happens SERVER-side — this component never mirrors the hub's candidate\n * list.\n */\n excludeTypes?: string[];\n /**\n * SUGGESTION-mode allow-list (rail vocabulary): which content types\n * participate in this rail. Sent verbatim as the API's `types=` param —\n * the SERVER intersects it with its own allowed candidate set, and\n * platform policy gates (e.g. internal-only types) ALWAYS win: the client\n * cannot request its way past them. Absent → all server-side candidates.\n */\n includeTypes?: string[];\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin). Applied to BOTH the suggestion fetch and the\n * default per-group list fetches. */\n apiBaseUrl?: string;\n /** Host injection bundle — REUSES the chat dispatcher's\n * `ChatCardDispatchExtras` (programConfigs, buildOgPlaceholderUrl,\n * buildProductReleaseCardProps override). Program groups render nothing\n * when their config is absent. */\n extras?: ChatCardDispatchExtras;\n /** Hub injects its `buildContentURL` recomposition; default uses the\n * ref's stored `url`/`targetPlatform` as the API composed them. */\n resolveHref?: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n /** Hub injects its registry-driven entity-list-api builder; default = the\n * lib's `buildListUrl(type, ids, apiBaseUrl)`. */\n buildListUrl?: (type: string, ids: string[]) => string | null;\n /** Hub injects a `useNavLink`-backed render-prop provider; default = pure\n * anchor via `decideNewTab`. MUST be a module-scope component. */\n LinkProvider?: CardLinkProvider;\n /** Renderer pair for the admin-only `marketing_campaign` type. Absent →\n * the type renders nothing. */\n adminCampaignCard?: AdminCampaignCardSlot;\n /** When true, render the section shell (title + an empty-state line) even\n * with ZERO refs, instead of returning null. Default false (the original\n * behavior — empty rail = no shell). Opt-in per host page (e.g. people-hub's\n * \"What I Shipped\", where the section should always be present). */\n showWhenEmpty?: boolean;\n /** Empty-state copy shown under the title when `showWhenEmpty` and no refs.\n * Default: \"No related content yet.\" */\n emptyStateText?: string;\n /** Custom empty-state node (e.g. a hub `<EmptyState/>`) rendered under the\n * title when `showWhenEmpty` and there are no refs — overrides\n * `emptyStateText`. Lets a host match its canonical empty state. */\n emptyState?: React.ReactNode;\n}\n\nexport function RelatedContentSection({\n contentRefs,\n entityType,\n entityId,\n authorId,\n minResults,\n sameTypeMinResults,\n includeTypes,\n initialItems,\n title = 'Related Content',\n columns = 2,\n excludeTypes,\n apiBaseUrl = '',\n extras,\n resolveHref = defaultResolveHref,\n buildListUrl,\n LinkProvider = DefaultLinkPropsProvider,\n adminCampaignCard,\n showWhenEmpty = false,\n emptyStateText = 'No related content yet.',\n emptyState,\n}: RelatedContentSectionProps) {\n // ── Hooks above EVERY early return (the original `if (!contentRefs.length)\n // return null` guard moved below them). ──\n\n // Suggestion-mode fetch URL — null in controlled mode (contentRefs defined,\n // even []) or when the entity scope is incomplete.\n // `includeTypes: []` is an explicit \"nothing participates\" — skip the fetch\n // entirely (an empty-string `types=` param would be dropped by the URL\n // builder and read server-side as \"all candidates\") AND ignore SSR refs.\n const suggestionsDisabled = includeTypes?.length === 0;\n // Shared type-filter params — one spelling for both fetch modes so a future\n // normalization (trim/dedupe) can't diverge between them.\n const typeFilterParams = {\n types: includeTypes !== undefined ? includeTypes.join(',') : undefined,\n excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,\n };\n // AUTHOR mode beats suggestion mode: when `authorId` is set the rail lists\n // everything that profile authored (the server returns ALL, no count).\n const authorUrl =\n contentRefs === undefined && authorId && !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n extraParams: { authorId, ...typeFilterParams },\n })\n : null;\n const suggestUrl =\n authorUrl ??\n (contentRefs === undefined &&\n entityType &&\n entityId !== undefined &&\n entityId !== null &&\n entityId !== '' &&\n !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n entityType,\n entityId,\n count: minResults,\n extraParams: {\n sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,\n ...typeFilterParams,\n },\n })\n : null);\n // Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],\n // and a fresh per-render object would loop setState under re-rendering\n // parents (the latent FaqSection bug, fixed there in the same change).\n const initialData = useMemo<RelatedContentResponse | undefined>(\n // An explicitly disabled rail (includeTypes: []) must ignore SSR-hydrated\n // refs too — otherwise useSelfFetch(null, {initialData}) keeps serving\n // initialItems and the \"nothing participates\" contract silently breaks.\n () => (!suggestionsDisabled && initialItems ? { refs: initialItems } : undefined),\n [initialItems, suggestionsDisabled],\n );\n const { data, isLoading } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });\n\n // Default group fetcher: the lib's byte-parity-tested builder, prefixed for\n // embeds. Memoized so group-fetch URLs stay value-stable across renders.\n const effectiveBuildListUrl = useMemo(\n () => buildListUrl ?? ((type: string, ids: string[]) => libBuildListUrl(type, ids, apiBaseUrl)),\n [buildListUrl, apiBaseUrl],\n );\n\n const refs: ContentRef[] = contentRefs ?? data?.refs ?? [];\n\n // Per-consumer type gating — drops refs whose `type` is in the exclude\n // list. In suggestion mode the server already subtracted these (the param\n // is forwarded above); the client filter stays as an idempotent guard and\n // IS the mechanism in controlled mode (original behavior).\n const exclude = new Set(excludeTypes || []);\n const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;\n // Zero refs (still loading in suggestion mode, or genuinely empty). Default:\n // no empty shell. Opt-in (`showWhenEmpty`): render the title + an empty-state\n // line so the section is always present (e.g. people-hub \"What I Shipped\").\n if (!visibleRefs.length) {\n if (!showWhenEmpty) return null; // non-showWhenEmpty consumers stay blank (unchanged)\n // Client-fetch loading (author/suggestion mode, no SSR initialItems): render a\n // SKELETON grid — reserves height + matches the rest of the app's loading, so\n // there's no blank-then-pop jump (the prior `return null` collapsed the tab to\n // zero height during the fetch). SSR controlled mode has isLoading=false → it\n // skips straight to the empty state below. Skeleton type = the requested rail\n // type (author mode passes a single `includeTypes`).\n if (isLoading) {\n const skeletonType = includeTypes?.[0] ?? entityType ?? 'blog_post_existing';\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n <div className={gridClassFor(columns)}>\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i}>{renderSkeletonForType(skeletonType, 'default', adminCampaignCard)}</div>\n ))}\n </div>\n </div>\n );\n }\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {emptyState ?? <p className=\"text-ods-text-secondary\">{emptyStateText}</p>}\n </div>\n );\n }\n\n const grouped: Record<string, ContentRef[]> = {};\n for (const ref of visibleRefs) {\n if (!grouped[ref.type]) grouped[ref.type] = [];\n grouped[ref.type].push(ref);\n }\n\n // Registered types in CONTENT_REF_GROUPS order, then any unregistered\n // types appended (same shape the investor-email builder uses — both\n // consume `orderContentRefTypes` so cross-surface ordering matches).\n // SAME-TYPE FIRST: when a host entityType is known (suggestion / SSR\n // modes), its own content-type group is hoisted to the top — a blog\n // post's rail leads with blog posts. Rail group keys are compared via\n // the shared alias canonicalizer (blog_post_existing ↔ blog_post).\n let orderedTypes = orderContentRefTypes(Object.keys(grouped));\n if (entityType) {\n // Canonicalize BOTH sides — hosts pass registry vocab ('blog_post') but\n // rail-vocab aliases ('blog_post_existing') are also legal inputs; a raw\n // comparison would silently lose the same-type-first hoist for aliases.\n const canonicalEntityType = canonicalContentRefType(entityType);\n const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === canonicalEntityType);\n if (sameType.length > 0) {\n orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== canonicalEntityType)];\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {orderedTypes.map((type) => (\n <ContentGroup\n key={type}\n type={type}\n refs={grouped[type]}\n columns={columns}\n buildUrl={effectiveBuildListUrl}\n resolveHref={resolveHref}\n LinkProvider={LinkProvider}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n heading={\n <h3 className=\"font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider\">\n {getContentRefLabelOrTitleCase(type)}\n </h3>\n }\n />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["buildListUrl"]}
@@ -11,16 +11,20 @@
11
11
 
12
12
 
13
13
 
14
+ var _chunkQHIXS3W2cjs = require('./chunk-QHIXS3W2.cjs');
14
15
 
15
- var _chunkGUTS7HGAcjs = require('./chunk-GUTS7HGA.cjs');
16
16
 
17
17
 
18
+ var _chunk5E2HOSSHcjs = require('./chunk-5E2HOSSH.cjs');
18
19
 
19
- var _chunkWMSTJAZTcjs = require('./chunk-WMSTJAZT.cjs');
20
20
 
21
+ var _chunk6JINAOI7cjs = require('./chunk-6JINAOI7.cjs');
21
22
 
22
23
 
23
- var _chunkFQOTC3UUcjs = require('./chunk-FQOTC3UU.cjs');
24
+ var _chunkJ54Z3OCRcjs = require('./chunk-J54Z3OCR.cjs');
25
+
26
+
27
+ var _chunkIE6OU3WQcjs = require('./chunk-IE6OU3WQ.cjs');
24
28
 
25
29
  // src/components/contact/contact-form.tsx
26
30
  var _react = require('react');
@@ -109,14 +113,14 @@ function ContactForm({
109
113
  successRedirectUrl = "/blog#community",
110
114
  successToastMessage = "Redirecting you to join our community..."
111
115
  } = {}) {
112
- const attachments = _chunkGUTS7HGAcjs.useChatAttachments.call(void 0, );
113
- const builtInSubmission = _chunkWMSTJAZTcjs.useContactSubmission.call(void 0, {
116
+ const attachments = _chunkQHIXS3W2cjs.useChatAttachments.call(void 0, );
117
+ const builtInSubmission = _chunk5E2HOSSHcjs.useContactSubmission.call(void 0, {
114
118
  userId,
115
119
  successRedirectUrl,
116
120
  successToastMessage
117
121
  });
118
122
  const [customSubmitting, setCustomSubmitting] = _react.useState.call(void 0, false);
119
- const { honeypotInputProps, getSignals, resetSignals } = _chunkWMSTJAZTcjs.useHumanitySignals.call(void 0, );
123
+ const { honeypotInputProps, getSignals, resetSignals } = _chunk5E2HOSSHcjs.useHumanitySignals.call(void 0, );
120
124
  const isSubmitting = onCustomSubmit ? customSubmitting : builtInSubmission.isSubmitting;
121
125
  const isSuccess = onCustomSubmit ? false : builtInSubmission.isSuccess;
122
126
  const {
@@ -171,7 +175,7 @@ function ContactForm({
171
175
  className: `h-full flex flex-col ${!noBorder ? "border border-ods-border rounded-2xl md:rounded-3xl" : ""} ${!noPadding ? "p-6 md:p-8 lg:p-10" : ""}`,
172
176
  children: [
173
177
  (title || subtitle) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mb-6 md:mb-8", children: [
174
- title && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: `${_chunkGUTS7HGAcjs.SECTION_HEADING_CLASS} mb-3 md:mb-4`, children: title }),
178
+ title && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: `${_chunkJ54Z3OCRcjs.SECTION_HEADING_CLASS} mb-3 md:mb-4`, children: title }),
175
179
  subtitle && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "font-['DM_Sans'] font-medium text-[16px] md:text-[18px] leading-[24px] text-ods-text-primary", children: subtitle })
176
180
  ] }),
177
181
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
@@ -191,16 +195,16 @@ function ContactForm({
191
195
  !showEmail && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "input", { type: "hidden", ...register("email") }),
192
196
  !showHelpCategory && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "input", { type: "hidden", ...register("helpCategory") }),
193
197
  !showMessage && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "input", { type: "hidden", ...register("message") }),
194
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.HoneypotField, { ...honeypotInputProps }),
198
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.HoneypotField, { ...honeypotInputProps }),
195
199
  extraTopField,
196
200
  showNameEmailRow && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6", children: [
197
201
  showName && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col", children: [
198
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Label, { htmlFor: "name", children: [
202
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Label, { htmlFor: "name", children: [
199
203
  "Your Name",
200
204
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-ods-accent", children: "*" })
201
205
  ] }),
202
206
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
203
- _chunkFQOTC3UUcjs.Input,
207
+ _chunk6JINAOI7cjs.Input,
204
208
  {
205
209
  id: "name",
206
210
  type: "text",
@@ -214,12 +218,12 @@ function ContactForm({
214
218
  errors.name && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { id: "name-error", className: "text-ods-error text-xs font-['DM_Sans'] mt-1", children: errors.name.message })
215
219
  ] }),
216
220
  showEmail && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col", children: [
217
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Label, { htmlFor: "email", children: [
221
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Label, { htmlFor: "email", children: [
218
222
  "Email",
219
223
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-ods-accent", children: "*" })
220
224
  ] }),
221
225
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
222
- _chunkFQOTC3UUcjs.Input,
226
+ _chunk6JINAOI7cjs.Input,
223
227
  {
224
228
  id: "email",
225
229
  type: "email",
@@ -235,46 +239,46 @@ function ContactForm({
235
239
  ] }),
236
240
  (showCompanySize || showReferralSource) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6", children: [
237
241
  showCompanySize && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col", children: [
238
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.Label, { htmlFor: "companySize", children: "Company Size" }),
242
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.Label, { htmlFor: "companySize", children: "Company Size" }),
239
243
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
240
244
  _reacthookform.Controller,
241
245
  {
242
246
  control,
243
247
  name: "companySize",
244
- render: ({ field }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Select, { onValueChange: field.onChange, defaultValue: field.value, children: [
248
+ render: ({ field }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Select, { onValueChange: field.onChange, defaultValue: field.value, children: [
245
249
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
246
- _chunkGUTS7HGAcjs.SelectTrigger,
250
+ _chunkQHIXS3W2cjs.SelectTrigger,
247
251
  {
248
252
  id: "companySize",
249
253
  "aria-label": "Company Size",
250
254
  className: "bg-ods-card border-ods-border text-ods-text-primary h-12 px-3",
251
- children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectValue, { placeholder: "Select company size" })
255
+ children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectValue, { placeholder: "Select company size" })
252
256
  }
253
257
  ),
254
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectContent, { children: companySizeOptions.map((opt) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectItem, { value: opt, children: opt }, opt)) })
258
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectContent, { children: companySizeOptions.map((opt) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectItem, { value: opt, children: opt }, opt)) })
255
259
  ] })
256
260
  }
257
261
  ),
258
262
  errors.companySize && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { id: "companySize-error", className: "text-ods-error text-xs font-['DM_Sans'] mt-1", children: errors.companySize.message })
259
263
  ] }),
260
264
  showReferralSource && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col", children: [
261
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.Label, { htmlFor: "referralSource", children: "How did you hear about us?" }),
265
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.Label, { htmlFor: "referralSource", children: "How did you hear about us?" }),
262
266
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
263
267
  _reacthookform.Controller,
264
268
  {
265
269
  control,
266
270
  name: "referralSource",
267
- render: ({ field }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Select, { onValueChange: field.onChange, defaultValue: field.value, children: [
271
+ render: ({ field }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Select, { onValueChange: field.onChange, defaultValue: field.value, children: [
268
272
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
269
- _chunkGUTS7HGAcjs.SelectTrigger,
273
+ _chunkQHIXS3W2cjs.SelectTrigger,
270
274
  {
271
275
  id: "referralSource",
272
276
  "aria-label": "Referral Source",
273
277
  className: "bg-ods-card border-ods-border text-ods-text-primary h-12 px-3",
274
- children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectValue, { placeholder: "Select an option" })
278
+ children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectValue, { placeholder: "Select an option" })
275
279
  }
276
280
  ),
277
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectContent, { children: referralSourceOptions.map((opt) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectItem, { value: opt, children: opt }, opt)) })
281
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectContent, { children: referralSourceOptions.map((opt) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectItem, { value: opt, children: opt }, opt)) })
278
282
  ] })
279
283
  }
280
284
  ),
@@ -282,7 +286,7 @@ function ContactForm({
282
286
  ] })
283
287
  ] }),
284
288
  showHelpCategory && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col", children: [
285
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Label, { htmlFor: "helpCategory", children: [
289
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Label, { htmlFor: "helpCategory", children: [
286
290
  "Choose your main interest",
287
291
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-ods-accent", children: "*" })
288
292
  ] }),
@@ -291,29 +295,29 @@ function ContactForm({
291
295
  {
292
296
  control,
293
297
  name: "helpCategory",
294
- render: ({ field }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Select, { onValueChange: field.onChange, defaultValue: field.value, children: [
298
+ render: ({ field }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Select, { onValueChange: field.onChange, defaultValue: field.value, children: [
295
299
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
296
- _chunkGUTS7HGAcjs.SelectTrigger,
300
+ _chunkQHIXS3W2cjs.SelectTrigger,
297
301
  {
298
302
  id: "helpCategory",
299
303
  "aria-label": "Help Category",
300
304
  className: "bg-ods-card border-ods-border text-ods-text-primary h-12 px-3",
301
- children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectValue, { placeholder: "Choose your main interest" })
305
+ children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectValue, { placeholder: "Choose your main interest" })
302
306
  }
303
307
  ),
304
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectContent, { children: helpCategoryOptions.map((opt) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkGUTS7HGAcjs.SelectItem, { value: opt, children: opt }, opt)) })
308
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectContent, { children: helpCategoryOptions.map((opt) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkQHIXS3W2cjs.SelectItem, { value: opt, children: opt }, opt)) })
305
309
  ] })
306
310
  }
307
311
  ),
308
312
  errors.helpCategory && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { id: "helpCategory-error", className: "text-ods-error text-xs font-['DM_Sans'] mt-1", children: errors.helpCategory.message })
309
313
  ] }),
310
314
  showMessage && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col flex-grow", children: [
311
- /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkGUTS7HGAcjs.Label, { htmlFor: "message", children: [
315
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkQHIXS3W2cjs.Label, { htmlFor: "message", children: [
312
316
  "Your Message",
313
317
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-ods-accent", children: "*" })
314
318
  ] }),
315
319
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
316
- _chunkGUTS7HGAcjs.Textarea,
320
+ _chunkQHIXS3W2cjs.Textarea,
317
321
  {
318
322
  id: "message",
319
323
  ...register("message"),
@@ -327,7 +331,7 @@ function ContactForm({
327
331
  ] }),
328
332
  attachmentsEnabled && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col gap-2", children: [
329
333
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
330
- _chunkGUTS7HGAcjs.ChatAttachmentChipStrip,
334
+ _chunkQHIXS3W2cjs.ChatAttachmentChipStrip,
331
335
  {
332
336
  attachments: attachments.attachments,
333
337
  onRemove: attachments.removeAttachment,
@@ -336,7 +340,7 @@ function ContactForm({
336
340
  ),
337
341
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
338
342
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
339
- _chunkGUTS7HGAcjs.ChatAttachmentAddButton,
343
+ _chunkQHIXS3W2cjs.ChatAttachmentAddButton,
340
344
  {
341
345
  attachmentsEnabled: true,
342
346
  attachmentsCount: attachments.attachments.length,
@@ -350,7 +354,7 @@ function ContactForm({
350
354
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-end w-full pt-2 mt-auto", children: [
351
355
  footerText && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "font-['DM_Sans'] text-ods-text-secondary text-xs md:text-sm leading-relaxed text-center md:text-left", children: footerText }),
352
356
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
353
- _chunkFQOTC3UUcjs.Button,
357
+ _chunkIE6OU3WQcjs.Button,
354
358
  {
355
359
  type: "submit",
356
360
  loading: isSubmitting,
@@ -372,4 +376,4 @@ function ContactForm({
372
376
 
373
377
 
374
378
  exports.ContactForm = ContactForm;
375
- //# sourceMappingURL=chunk-YBYI62OE.cjs.map
379
+ //# sourceMappingURL=chunk-X647HY3F.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-X647HY3F.cjs","../src/components/contact/contact-form.tsx","../src/schemas/contact-schema.ts"],"names":[],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-X647HY3F.cjs","sourcesContent":[null,"'use client'\n\n/**\n * `<ContactForm />` — the canonical contact form used by every public\n * surface (TMCG join, case-study pitch, generic /contact, Help Center\n * ticket creation, etc.).\n *\n * Self-contained inside the lib — host-specific values (user id for\n * tracking, platform-specific contact reasons, reddit-click attribution\n * id) flow IN via props. The hub passes them via a thin\n * `<ContactForm>` wrapper that resolves them from `useAuth` /\n * `getAppConfig` / `getStoredRedditClickId`. Other embedders pass\n * whatever they have (or omit).\n *\n * Field-hide + custom-submit + extra-top-field knobs let one form\n * serve both contact and ticket-creation flows without forking:\n * - Contact page: rendered with all fields visible, built-in submit\n * flow to `/api/contact` via `useContactSubmission`.\n * - Ticket page: hides name/email/companySize/referralSource/\n * helpCategory; supplies `extraTopField` (a Subject input) +\n * `onCustomSubmit` wired to `useTicketActions.submitTicket`.\n */\n\nimport { useState, type ReactNode } from 'react'\nimport { useForm, Controller } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n ContactSchema,\n type ContactFormData,\n companySizeOptions,\n referralSourceOptions,\n defaultHelpCategoryOptions,\n} from '../../schemas/contact-schema'\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading'\nimport {\n Button,\n type ButtonProps,\n Input,\n Textarea,\n Select,\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n Label,\n} from '../ui'\nimport { useContactSubmission } from '../../hooks/use-contact-submission'\nimport { useHumanitySignals } from '../../hooks/use-humanity-signals'\nimport { HoneypotField } from '../ui/honeypot-field'\nimport {\n ChatAttachmentAddButton,\n ChatAttachmentChipStrip,\n} from '../chat/chat-attachment-bar'\nimport { useChatAttachments } from '../chat/hooks/use-chat-attachments'\nimport type { ChatAttachment } from '../chat/utils/chat-attachment-markdown'\n\n/**\n * Fields the caller can suppress. Six values — every primary form\n * field plus `name` and `email` (newly hideable so ticket-creation\n * surfaces can hide them; they still need to validate, so the caller\n * MUST supply pre-filled values via `defaultValues` when hiding them).\n */\nexport type ContactFormHideableField =\n | 'name'\n | 'email'\n | 'companySize'\n | 'referralSource'\n | 'helpCategory'\n | 'message'\n\nexport interface ContactFormProps {\n /** Host-side user id passed to `useContactSubmission` for attribution.\n * Hub wrapper passes `useAuth().user?.id`; lib's Help Center surface\n * passes `useChatIdentity().user?.id`. Omit for anon flows. */\n userId?: string\n /** Platform-specific help-category dropdown options. Hub wrapper\n * passes `getAppConfig().contact.contactReasons`. Defaults to the\n * lib's `defaultHelpCategoryOptions`. */\n helpCategoryOptions?: readonly string[]\n /** Reddit click attribution id. Caller resolves from wherever they\n * stash it (hub: sessionStorage via `getStoredRedditClickId`). When\n * set, it's spread into the submission payload. */\n rdtCid?: string\n /** Called after a successful submit so the caller can clear their\n * attribution storage (hub wrapper calls `clearStoredRedditClickId`).\n * Fires for BOTH the built-in and custom submit paths. */\n onSubmitSuccess?: () => void\n\n prefilledReason?: string\n prefilledMessage?: string\n hideFields?: ContactFormHideableField[]\n /** Authoritative pre-fill for any field the caller hides. Merged\n * into react-hook-form's `defaultValues` AFTER the legacy\n * `prefilledReason` / `prefilledMessage` props (caller-supplied\n * wins). REQUIRED when hiding `name` / `email` / `helpCategory` —\n * those fields are still validated by Zod even when not rendered. */\n defaultValues?: Partial<ContactFormData>\n /** Optional custom submit handler. When provided, the form bypasses\n * the built-in `useContactSubmission` flow (no /api/contact call,\n * no success-redirect, no built-in toast) — the caller owns the\n * entire side-effect chain. Reset + `onSubmitSuccess` still fire\n * on a successful await.\n *\n * Receives the schema-validated form payload PLUS the ready\n * attachments array (empty when `attachmentsEnabled === false` or\n * the user hasn't picked any). Caller forwards `attachments` to\n * whichever sink owns the upload (e.g. `actions.submitTicket`'s\n * `attachments` field for HubSpot Note engagements). */\n onCustomSubmit?: (data: ContactFormData, attachments: ChatAttachment[]) => Promise<void>\n /** Turn on the attachments bar (file `+` button + chip strip) using\n * the same lib primitives the chat composer uses\n * (`<ChatAttachmentAddButton>` + `<ChatAttachmentChipStrip>` +\n * `useChatAttachments`). When `false` (the default), the form\n * doesn't render the bar AND the attachments array passed to\n * `onCustomSubmit` is always empty. */\n attachmentsEnabled?: boolean\n /** Render slot for an EXTRA field at the very top of the form,\n * ABOVE the name/email row. Use this for ticket surfaces that need\n * a Subject input — the field is NOT part of `ContactSchema`, so\n * the caller manages its own state + validation and reads the\n * value back inside `onCustomSubmit`. */\n extraTopField?: ReactNode\n\n title?: string\n subtitle?: string\n footerText?: string\n noBorder?: boolean\n noPadding?: boolean\n buttonVariant?: ButtonProps['variant']\n buttonClassName?: string\n /** Submit-button label. Defaults to \"Send Message\". Override for\n * ticket surfaces (e.g. \"Open ticket\"). */\n submitLabel?: string\n /** Success-state submit-button label (shown briefly after submit on\n * the built-in flow). Defaults to \"Message Sent!\". Has no effect\n * when `onCustomSubmit` is provided — the caller owns success UX. */\n submitSuccessLabel?: string\n successRedirectUrl?: string\n successToastMessage?: string\n}\n\nexport function ContactForm({\n userId,\n helpCategoryOptions = defaultHelpCategoryOptions,\n rdtCid,\n onSubmitSuccess,\n prefilledReason,\n prefilledMessage,\n hideFields = [],\n defaultValues: defaultValuesProp,\n onCustomSubmit,\n extraTopField,\n attachmentsEnabled = false,\n title = 'Hit Us Up',\n subtitle,\n footerText = 'We typically respond within 24 hours. We respect your privacy – no spam, ever.',\n noBorder = false,\n noPadding = false,\n buttonVariant = 'accent',\n buttonClassName = '',\n submitLabel = 'Send Message',\n submitSuccessLabel = 'Message Sent!',\n successRedirectUrl = '/blog#community',\n successToastMessage = 'Redirecting you to join our community...',\n}: ContactFormProps = {}) {\n // Attachments staging — same hook the chat composer + ticket\n // detail-drawer composer use. Files upload to Supabase as soon as\n // the user picks them; `readyAttachments` is the wire-shape array\n // ready for the next submit. `hasInflightUploads` disables Send\n // until every upload settles.\n const attachments = useChatAttachments()\n // Built-in contact-API flow. Hook is called unconditionally (rules\n // of hooks); we just don't dispatch its `submit` when the caller\n // passes `onCustomSubmit`. The hook owns its own toast + redirect\n // chain so bypassing it cleanly hands all side-effects to the caller.\n const builtInSubmission = useContactSubmission({\n userId,\n successRedirectUrl,\n successToastMessage,\n })\n // Independent in-flight tracker for the custom path — we can't reuse\n // `builtInSubmission.isSubmitting` because that hook never sees a\n // request when `onCustomSubmit` is active.\n const [customSubmitting, setCustomSubmitting] = useState(false)\n\n // Invisible bot-protection signals (honeypot + timing). Spread into the\n // submit payload for BOTH the built-in and custom paths; reset on success.\n const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()\n\n const isSubmitting = onCustomSubmit ? customSubmitting : builtInSubmission.isSubmitting\n // `isSuccess` only ever fires on the built-in path; custom callers\n // own their own UX (no \"Message Sent!\" button-label flicker).\n const isSuccess = onCustomSubmit ? false : builtInSubmission.isSuccess\n\n const {\n register,\n handleSubmit,\n control,\n formState: { errors },\n reset,\n } = useForm<ContactFormData>({\n resolver: zodResolver(ContactSchema),\n defaultValues: {\n ...(prefilledReason && { helpCategory: prefilledReason }),\n ...(prefilledMessage && { message: prefilledMessage }),\n // Caller-supplied defaults win over the legacy `prefilled*` props\n // (they're the authoritative seed for hidden fields).\n ...defaultValuesProp,\n },\n })\n\n const handleFormSubmit = async (data: ContactFormData) => {\n if (isSubmitting) return\n if (attachmentsEnabled && attachments.hasInflightUploads) return\n try {\n const payload = { ...data, ...(rdtCid && { rdt_cid: rdtCid }), ...getSignals() }\n const readyAttachments = attachmentsEnabled ? attachments.readyAttachments : []\n if (onCustomSubmit) {\n setCustomSubmitting(true)\n try {\n await onCustomSubmit(payload, readyAttachments)\n } finally {\n setCustomSubmitting(false)\n }\n } else {\n await builtInSubmission.submit(payload)\n }\n onSubmitSuccess?.()\n reset()\n resetSignals()\n if (attachmentsEnabled) attachments.clear()\n } catch {\n // Error toast is owned by the active flow:\n // - built-in: `useContactSubmission` toasts inside `submit()`.\n // - custom: the caller toasts inside `onCustomSubmit`.\n // Either way we swallow here so a thrown error doesn't crash the\n // form tree (react-hook-form's onSubmit handler rejects upward).\n }\n }\n\n const showName = !hideFields.includes('name')\n const showEmail = !hideFields.includes('email')\n const showNameEmailRow = showName || showEmail\n const showCompanySize = !hideFields.includes('companySize')\n const showReferralSource = !hideFields.includes('referralSource')\n const showHelpCategory = !hideFields.includes('helpCategory')\n const showMessage = !hideFields.includes('message')\n\n return (\n <div\n className={`h-full flex flex-col ${!noBorder ? 'border border-ods-border rounded-2xl md:rounded-3xl' : ''} ${!noPadding ? 'p-6 md:p-8 lg:p-10' : ''}`}\n >\n {(title || subtitle) && (\n <div className=\"mb-6 md:mb-8\">\n {title && (\n <h2 className={`${SECTION_HEADING_CLASS} mb-3 md:mb-4`}>\n {title}\n </h2>\n )}\n {subtitle && (\n <p className=\"font-['DM_Sans'] font-medium text-[16px] md:text-[18px] leading-[24px] text-ods-text-primary\">\n {subtitle}\n </p>\n )}\n </div>\n )}\n\n <form\n onSubmit={handleSubmit(handleFormSubmit, (validationErrors) => {\n // When validation fails on a HIDDEN field (e.g. ticket form\n // hides name/email/helpCategory and seeds them via\n // `defaultValues`), there's no visible error UI for the user\n // — the submit button just appears dead. Log so the broken\n // defaultValues wiring is at least discoverable in DevTools.\n // eslint-disable-next-line no-console\n console.warn(\n '[ContactForm] submit blocked by validation:',\n Object.fromEntries(\n Object.entries(validationErrors).map(([k, v]) => [k, v?.message ?? v]),\n ),\n )\n })}\n className=\"flex flex-col flex-grow space-y-4 md:space-y-6\"\n >\n {/* Hidden inputs for fields that are required by `ContactSchema`\n but suppressed from the visible UI via `hideFields`. Without\n these, `register('name')` never runs, react-hook-form skips\n the field at submit time, and Zod's required-string check\n fails silently — the user clicks Submit and NOTHING visible\n happens (no error, no network call). The caller-supplied\n `defaultValues` seed the values; the hidden inputs just tell\n RHF to include them in the submit payload. */}\n {!showName && <input type=\"hidden\" {...register('name')} />}\n {!showEmail && <input type=\"hidden\" {...register('email')} />}\n {!showHelpCategory && <input type=\"hidden\" {...register('helpCategory')} />}\n {!showMessage && <input type=\"hidden\" {...register('message')} />}\n\n {/* Invisible honeypot — real users never fill it; bots that fill every field trip it. */}\n <HoneypotField {...honeypotInputProps} />\n\n {/* Extra top field (e.g. Subject for ticket forms). Rendered\n outside the schema-driven layout so the caller fully owns\n label / placeholder / state. */}\n {extraTopField}\n\n {showNameEmailRow && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6\">\n {showName && (\n <div className=\"flex flex-col\">\n <Label htmlFor=\"name\">\n Your Name<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"name\"\n type=\"text\"\n {...register('name')}\n placeholder=\"Jane Doe\"\n aria-invalid={!!errors.name}\n aria-describedby=\"name-error\"\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12\"\n />\n {errors.name && (\n <span id=\"name-error\" className=\"text-ods-error text-xs font-['DM_Sans'] mt-1\">\n {errors.name.message}\n </span>\n )}\n </div>\n )}\n {showEmail && (\n <div className=\"flex flex-col\">\n <Label htmlFor=\"email\">\n Email<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"email\"\n type=\"email\"\n {...register('email')}\n placeholder=\"jane@company.com\"\n aria-invalid={!!errors.email}\n aria-describedby=\"email-error\"\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12\"\n />\n {errors.email && (\n <span id=\"email-error\" className=\"text-ods-error text-xs font-['DM_Sans'] mt-1\">\n {errors.email.message}\n </span>\n )}\n </div>\n )}\n </div>\n )}\n\n {(showCompanySize || showReferralSource) && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6\">\n {showCompanySize && (\n <div className=\"flex flex-col\">\n <Label htmlFor=\"companySize\">Company Size</Label>\n <Controller\n control={control}\n name=\"companySize\"\n render={({ field }) => (\n <Select onValueChange={field.onChange} defaultValue={field.value}>\n <SelectTrigger\n id=\"companySize\"\n aria-label=\"Company Size\"\n className=\"bg-ods-card border-ods-border text-ods-text-primary h-12 px-3\"\n >\n <SelectValue placeholder=\"Select company size\" />\n </SelectTrigger>\n <SelectContent>\n {companySizeOptions.map((opt) => (\n <SelectItem key={opt} value={opt}>\n {opt}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n />\n {errors.companySize && (\n <span id=\"companySize-error\" className=\"text-ods-error text-xs font-['DM_Sans'] mt-1\">\n {errors.companySize.message}\n </span>\n )}\n </div>\n )}\n {showReferralSource && (\n <div className=\"flex flex-col\">\n <Label htmlFor=\"referralSource\">How did you hear about us?</Label>\n <Controller\n control={control}\n name=\"referralSource\"\n render={({ field }) => (\n <Select onValueChange={field.onChange} defaultValue={field.value}>\n <SelectTrigger\n id=\"referralSource\"\n aria-label=\"Referral Source\"\n className=\"bg-ods-card border-ods-border text-ods-text-primary h-12 px-3\"\n >\n <SelectValue placeholder=\"Select an option\" />\n </SelectTrigger>\n <SelectContent>\n {referralSourceOptions.map((opt) => (\n <SelectItem key={opt} value={opt}>\n {opt}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n />\n {errors.referralSource && (\n <span id=\"referralSource-error\" className=\"text-ods-error text-xs font-['DM_Sans'] mt-1\">\n {errors.referralSource.message}\n </span>\n )}\n </div>\n )}\n </div>\n )}\n\n {showHelpCategory && (\n <div className=\"flex flex-col\">\n <Label htmlFor=\"helpCategory\">\n Choose your main interest<span className=\"text-ods-accent\">*</span>\n </Label>\n <Controller\n control={control}\n name=\"helpCategory\"\n render={({ field }) => (\n <Select onValueChange={field.onChange} defaultValue={field.value}>\n <SelectTrigger\n id=\"helpCategory\"\n aria-label=\"Help Category\"\n className=\"bg-ods-card border-ods-border text-ods-text-primary h-12 px-3\"\n >\n <SelectValue placeholder=\"Choose your main interest\" />\n </SelectTrigger>\n <SelectContent>\n {helpCategoryOptions.map((opt) => (\n <SelectItem key={opt} value={opt}>\n {opt}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n />\n {errors.helpCategory && (\n <span id=\"helpCategory-error\" className=\"text-ods-error text-xs font-['DM_Sans'] mt-1\">\n {errors.helpCategory.message}\n </span>\n )}\n </div>\n )}\n\n {showMessage && (\n <div className=\"flex flex-col flex-grow\">\n <Label htmlFor=\"message\">\n Your Message<span className=\"text-ods-accent\">*</span>\n </Label>\n <Textarea\n id=\"message\"\n {...register('message')}\n placeholder=\"Share your current challenges or questions about open-source alternatives...\"\n aria-invalid={!!errors.message}\n aria-describedby=\"message-error\"\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary h-full flex-grow\"\n />\n {errors.message && (\n <span id=\"message-error\" className=\"text-ods-error text-xs font-['DM_Sans'] mt-1\">\n {errors.message.message}\n </span>\n )}\n </div>\n )}\n\n {/* Attachments — only renders when `attachmentsEnabled` is on.\n Uses the SAME chip strip + add button + staging hook the\n chat composer and ticket-drawer composer use, so the visual\n chip styling + upload-progress UX are identical everywhere\n attachments appear. */}\n {attachmentsEnabled && (\n <div className=\"flex flex-col gap-2\">\n <ChatAttachmentChipStrip\n attachments={attachments.attachments}\n onRemove={attachments.removeAttachment}\n disabled={isSubmitting}\n />\n <div className=\"flex items-center gap-2\">\n <ChatAttachmentAddButton\n attachmentsEnabled\n attachmentsCount={attachments.attachments.length}\n onAddFiles={attachments.addFiles}\n disabled={isSubmitting}\n />\n <span className=\"text-xs text-ods-text-secondary\">\n Attach files (optional)\n </span>\n </div>\n </div>\n )}\n\n <div className=\"flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-end w-full pt-2 mt-auto\">\n {footerText && (\n <p className=\"font-['DM_Sans'] text-ods-text-secondary text-xs md:text-sm leading-relaxed text-center md:text-left\">\n {footerText}\n </p>\n )}\n <Button\n type=\"submit\"\n loading={isSubmitting}\n disabled={\n isSubmitting ||\n isSuccess ||\n (attachmentsEnabled && attachments.hasInflightUploads)\n }\n variant={buttonVariant}\n className={`w-full md:w-auto ${buttonClassName}`}\n >\n {isSuccess ? submitSuccessLabel : submitLabel}\n </Button>\n </div>\n </form>\n </div>\n )\n}\n","import { z } from 'zod';\n\n// Dropdown option constants — re-exported by `<ContactForm>` consumers\n// that want to surface their own custom Select widgets keyed on the\n// same allowed-value set.\nexport const companySizeOptions = [\n '1-10',\n '11-50',\n '51-200',\n '201-500',\n '501-1000',\n '1001+',\n] as const;\n\nexport const referralSourceOptions = [\n 'Google',\n 'LinkedIn',\n 'Twitter/X',\n 'Reddit',\n 'Friend / Colleague',\n 'Other',\n] as const;\n\n// Default fallback options — used when the embedder doesn't supply\n// platform-specific help-category options via the `helpCategoryOptions`\n// prop on `<ContactForm>`.\nexport const defaultHelpCategoryOptions = [\n 'Open-Source Alternatives',\n 'Vendor Cost Reduction',\n 'MSP Best Practices',\n 'Partnerships',\n 'Press',\n 'Other',\n] as const;\n\n// Reusable LinkedIn URL validator — the single source of truth. Every\n// public form schema, every admin update schema, every HubSpot push\n// validator MUST reference this so validation rules cannot drift\n// across boundaries.\n//\n// Host validation parses the URL and checks the hostname suffix so an\n// adversarial input like `https://evil.com/linkedin.com/x` is rejected\n// (substring match would have accepted it — CodeQL alert\n// \"Incomplete URL substring sanitization\").\nexport const LinkedInUrlSchema = z\n .string()\n .url({ message: 'Please enter a valid LinkedIn URL' })\n .refine(\n (url) => {\n try {\n const host = new URL(url).hostname.toLowerCase()\n return host === 'linkedin.com' || host.endsWith('.linkedin.com')\n } catch {\n return false\n }\n },\n {\n message: 'Please enter a valid LinkedIn profile URL',\n },\n )\n .optional()\n .or(z.literal(''));\n\n/**\n * Base schema — fields shared by every contact-style form (main contact\n * form, TMCG join, data-room request, case-study pitch, etc.). Any\n * field that exists on a form but NOT on this schema is silently\n * stripped by `safeParse` — that's exactly the bug the LinkedIn field\n * hit historically.\n */\nexport const ContactBaseSchema = z.object({\n name: z\n .string()\n .min(2, { message: 'Name must be at least 2 characters' })\n .max(255, { message: 'Name is too long' }),\n email: z\n .string()\n .email({ message: 'Please enter a valid email address' })\n .max(255),\n linkedin_url: LinkedInUrlSchema,\n helpCategory: z\n .string()\n .min(1, { message: 'Please select what we can help you with' })\n .max(255, { message: 'Help category is too long' }),\n message: z\n .string()\n .min(10, { message: 'Message must be at least 10 characters' })\n .max(5000, { message: 'Message is too long (5,000 character limit)' }),\n rdt_cid: z.string().optional(),\n});\n\n// Public POST /api/contact validator — base + dropdown fields used by\n// the generic contact form. Other form-specific schemas extend\n// `ContactBaseSchema`.\nexport const ContactSchema = ContactBaseSchema.extend({\n companySize: z\n .string()\n .optional()\n .refine((val) => !val || companySizeOptions.includes(val as (typeof companySizeOptions)[number]), {\n message: 'Please select a valid company size',\n }),\n referralSource: z\n .string()\n .optional()\n .refine((val) => !val || referralSourceOptions.includes(val as (typeof referralSourceOptions)[number]), {\n message: 'Please select a valid referral source',\n }),\n});\n\nexport type ContactFormData = z.infer<typeof ContactSchema>;\n\nexport interface ContactApiResponse {\n success: boolean;\n error?: string;\n}\n"]}