@flamingo-stack/openframe-frontend-core 0.0.296 → 0.0.297

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 +9 -0
  2. package/dist/{chunk-7RIYT7ZH.js → chunk-2QG57XOJ.js} +1067 -205
  3. package/dist/chunk-2QG57XOJ.js.map +1 -0
  4. package/dist/{chunk-WHMATDVP.js → chunk-3JIQVE7T.js} +9 -15
  5. package/dist/{chunk-WHMATDVP.js.map → chunk-3JIQVE7T.js.map} +1 -1
  6. package/dist/{chunk-GLLDTKZK.cjs → chunk-4PSQS3SW.cjs} +7 -9
  7. package/dist/chunk-4PSQS3SW.cjs.map +1 -0
  8. package/dist/{chunk-OY7OF7E7.js → chunk-4TLE6VLU.js} +30 -24
  9. package/dist/chunk-4TLE6VLU.js.map +1 -0
  10. package/dist/{chunk-W6M2FLLT.cjs → chunk-53FUMSZ5.cjs} +40 -46
  11. package/dist/chunk-53FUMSZ5.cjs.map +1 -0
  12. package/dist/{chunk-D3LEFMOA.cjs → chunk-54KNMC2R.cjs} +3 -3
  13. package/dist/{chunk-D3LEFMOA.cjs.map → chunk-54KNMC2R.cjs.map} +1 -1
  14. package/dist/{chunk-EYEW6PTA.cjs → chunk-6C526VNN.cjs} +358 -118
  15. package/dist/chunk-6C526VNN.cjs.map +1 -0
  16. package/dist/{chunk-XREEV72C.cjs → chunk-7OVGB2DQ.cjs} +19 -25
  17. package/dist/chunk-7OVGB2DQ.cjs.map +1 -0
  18. package/dist/{chunk-6GCI7JOE.js → chunk-AD6C23QY.js} +8 -7
  19. package/dist/{chunk-6GCI7JOE.js.map → chunk-AD6C23QY.js.map} +1 -1
  20. package/dist/chunk-F5OB2YAL.cjs +144 -0
  21. package/dist/chunk-F5OB2YAL.cjs.map +1 -0
  22. package/dist/chunk-FBWXMMRB.cjs +2 -0
  23. package/dist/chunk-FBWXMMRB.cjs.map +1 -0
  24. package/dist/{chunk-YIGPRLQY.cjs → chunk-FCDQNTDG.cjs} +21 -20
  25. package/dist/chunk-FCDQNTDG.cjs.map +1 -0
  26. package/dist/{chunk-IE6OU3WQ.cjs → chunk-FQOTC3UU.cjs} +318 -16
  27. package/dist/chunk-FQOTC3UU.cjs.map +1 -0
  28. package/dist/{chunk-QHIXS3W2.cjs → chunk-GUTS7HGA.cjs} +11590 -2105
  29. package/dist/chunk-GUTS7HGA.cjs.map +1 -0
  30. package/dist/chunk-GZ4C3XW6.js +2 -0
  31. package/dist/chunk-GZ4C3XW6.js.map +1 -0
  32. package/dist/{chunk-5P3B2LZW.js → chunk-IL47XWV5.js} +8 -14
  33. package/dist/{chunk-5P3B2LZW.js.map → chunk-IL47XWV5.js.map} +1 -1
  34. package/dist/{chunk-LCNMR277.js → chunk-IZ7JSBFP.js} +1 -1
  35. package/dist/chunk-IZ7JSBFP.js.map +1 -0
  36. package/dist/{chunk-EL6QLAWX.js → chunk-JALO4TAZ.js} +357 -55
  37. package/dist/chunk-JALO4TAZ.js.map +1 -0
  38. package/dist/{chunk-AQOWFSMB.cjs → chunk-L6PSSIUQ.cjs} +1 -1
  39. package/dist/chunk-L6PSSIUQ.cjs.map +1 -0
  40. package/dist/{chunk-MBFWU2EM.js → chunk-L7ULJKG7.js} +6 -10
  41. package/dist/{chunk-MBFWU2EM.js.map → chunk-L7ULJKG7.js.map} +1 -1
  42. package/dist/{chunk-K2PFPBMF.js → chunk-PC746XCO.js} +15050 -5565
  43. package/dist/chunk-PC746XCO.js.map +1 -0
  44. package/dist/{chunk-3ZXUQQL4.js → chunk-PI4WSYQV.js} +2 -2
  45. package/dist/{chunk-E4XABBSU.js → chunk-PWQUAVA3.js} +338 -98
  46. package/dist/chunk-PWQUAVA3.js.map +1 -0
  47. package/dist/chunk-SA2WPJVO.js +144 -0
  48. package/dist/chunk-SA2WPJVO.js.map +1 -0
  49. package/dist/{chunk-X6BV7MB7.cjs → chunk-UNVE2SDJ.cjs} +37 -31
  50. package/dist/chunk-UNVE2SDJ.cjs.map +1 -0
  51. package/dist/{chunk-5E2HOSSH.cjs → chunk-WMSTJAZT.cjs} +913 -51
  52. package/dist/chunk-WMSTJAZT.cjs.map +1 -0
  53. package/dist/{chunk-ZP4AVIZP.js → chunk-X4DOXQRT.js} +4 -6
  54. package/dist/{chunk-ZP4AVIZP.js.map → chunk-X4DOXQRT.js.map} +1 -1
  55. package/dist/{chunk-X647HY3F.cjs → chunk-YBYI62OE.cjs} +33 -37
  56. package/dist/chunk-YBYI62OE.cjs.map +1 -0
  57. package/dist/components/case-studies/index.cjs +126 -0
  58. package/dist/components/case-studies/index.cjs.map +1 -0
  59. package/dist/components/case-studies/index.d.ts +2 -0
  60. package/dist/components/case-studies/index.d.ts.map +1 -0
  61. package/dist/components/case-studies/index.js +126 -0
  62. package/dist/components/case-studies/index.js.map +1 -0
  63. package/dist/components/case-studies/share-experience-section.d.ts +48 -0
  64. package/dist/components/case-studies/share-experience-section.d.ts.map +1 -0
  65. package/dist/components/chat/index.cjs +8 -18
  66. package/dist/components/chat/index.cjs.map +1 -1
  67. package/dist/components/chat/index.js +75 -85
  68. package/dist/components/contact/index.cjs +8 -15
  69. package/dist/components/contact/index.cjs.map +1 -1
  70. package/dist/components/contact/index.js +7 -14
  71. package/dist/components/docs/doc-viewer.d.ts +39 -2
  72. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  73. package/dist/components/docs/docs-hub-page.d.ts +46 -0
  74. package/dist/components/docs/docs-hub-page.d.ts.map +1 -0
  75. package/dist/components/docs/index.cjs +17 -9
  76. package/dist/components/docs/index.cjs.map +1 -1
  77. package/dist/components/docs/index.d.ts +4 -0
  78. package/dist/components/docs/index.d.ts.map +1 -1
  79. package/dist/components/docs/index.js +16 -8
  80. package/dist/components/docs/skeletons.d.ts +32 -0
  81. package/dist/components/docs/skeletons.d.ts.map +1 -0
  82. package/dist/components/docs/use-docs-resolve-link.d.ts +20 -0
  83. package/dist/components/docs/use-docs-resolve-link.d.ts.map +1 -0
  84. package/dist/components/docs/use-document-tree.d.ts.map +1 -1
  85. package/dist/components/embeds/embed-container.d.ts +37 -0
  86. package/dist/components/embeds/embed-container.d.ts.map +1 -0
  87. package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
  88. package/dist/components/embeds/file-download-card.d.ts +18 -0
  89. package/dist/components/embeds/file-download-card.d.ts.map +1 -0
  90. package/dist/components/embeds/index.cjs +38 -15
  91. package/dist/components/embeds/index.cjs.map +1 -1
  92. package/dist/components/embeds/index.d.ts +8 -0
  93. package/dist/components/embeds/index.d.ts.map +1 -1
  94. package/dist/components/embeds/index.js +40 -17
  95. package/dist/components/embeds/linkedin-embed-client.d.ts +8 -0
  96. package/dist/components/embeds/linkedin-embed-client.d.ts.map +1 -0
  97. package/dist/components/embeds/markdown-image.d.ts +5 -0
  98. package/dist/components/embeds/markdown-image.d.ts.map +1 -0
  99. package/dist/components/embeds/reddit-embed-client.d.ts +7 -0
  100. package/dist/components/embeds/reddit-embed-client.d.ts.map +1 -0
  101. package/dist/components/embeds/rich-markdown-runtime.d.ts +46 -0
  102. package/dist/components/embeds/rich-markdown-runtime.d.ts.map +1 -0
  103. package/dist/components/embeds/twitter-embed-client.d.ts +8 -0
  104. package/dist/components/embeds/twitter-embed-client.d.ts.map +1 -0
  105. package/dist/components/faq/index.cjs +9 -16
  106. package/dist/components/faq/index.cjs.map +1 -1
  107. package/dist/components/faq/index.js +8 -15
  108. package/dist/components/features/index.cjs +8 -16
  109. package/dist/components/features/index.cjs.map +1 -1
  110. package/dist/components/features/index.js +24 -32
  111. package/dist/components/index.cjs +257 -452
  112. package/dist/components/index.cjs.map +1 -1
  113. package/dist/components/index.js +781 -976
  114. package/dist/components/index.js.map +1 -1
  115. package/dist/components/layout/page-header.d.ts +78 -0
  116. package/dist/components/layout/page-header.d.ts.map +1 -0
  117. package/dist/components/layout/page-layout.d.ts +10 -1
  118. package/dist/components/layout/page-layout.d.ts.map +1 -1
  119. package/dist/components/layout/page-with-header.d.ts +67 -0
  120. package/dist/components/layout/page-with-header.d.ts.map +1 -0
  121. package/dist/components/layout/title-block.d.ts +17 -1
  122. package/dist/components/layout/title-block.d.ts.map +1 -1
  123. package/dist/components/navigation/index.cjs +7 -15
  124. package/dist/components/navigation/index.cjs.map +1 -1
  125. package/dist/components/navigation/index.js +9 -17
  126. package/dist/components/onboarding-guides/index.cjs +35 -36
  127. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  128. package/dist/components/onboarding-guides/index.js +13 -14
  129. package/dist/components/onboarding-guides/index.js.map +1 -1
  130. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
  131. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  132. package/dist/components/related-content/index.cjs +9 -16
  133. package/dist/components/related-content/index.cjs.map +1 -1
  134. package/dist/components/related-content/index.js +8 -15
  135. package/dist/components/shared/dev-section/dev-section-page.d.ts +9 -0
  136. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  137. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  138. package/dist/components/shared/dev-section/index.d.ts +1 -1
  139. package/dist/components/shared/dev-section/index.d.ts.map +1 -1
  140. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  141. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  142. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  143. package/dist/components/tickets/index.cjs +100 -112
  144. package/dist/components/tickets/index.cjs.map +1 -1
  145. package/dist/components/tickets/index.js +20 -32
  146. package/dist/components/tickets/index.js.map +1 -1
  147. package/dist/components/ui/file-manager/index.cjs +50 -52
  148. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  149. package/dist/components/ui/file-manager/index.js +4 -6
  150. package/dist/components/ui/file-manager/index.js.map +1 -1
  151. package/dist/components/ui/index.cjs +13 -19
  152. package/dist/components/ui/index.cjs.map +1 -1
  153. package/dist/components/ui/index.d.ts +2 -0
  154. package/dist/components/ui/index.d.ts.map +1 -1
  155. package/dist/components/ui/index.js +133 -139
  156. package/dist/components/ui/release-changelog-section.d.ts +6 -2
  157. package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
  158. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  159. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  160. package/dist/components/ui/simple-markdown-renderer.d.ts +2 -8
  161. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  162. package/dist/contexts/chat-runtime-context.d.ts +14 -0
  163. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  164. package/dist/contexts/index.cjs +3 -3
  165. package/dist/contexts/index.js +5 -5
  166. package/dist/embed-shims/index.cjs +3 -3
  167. package/dist/embed-shims/index.cjs.map +1 -1
  168. package/dist/embed-shims/index.js +4 -4
  169. package/dist/hooks/index.cjs +4 -9
  170. package/dist/hooks/index.cjs.map +1 -1
  171. package/dist/hooks/index.js +6 -11
  172. package/dist/index.cjs +14 -20
  173. package/dist/index.cjs.map +1 -1
  174. package/dist/index.js +362 -368
  175. package/dist/types/doc-source.d.ts +31 -1
  176. package/dist/types/doc-source.d.ts.map +1 -1
  177. package/dist/utils/index.cjs +4 -0
  178. package/dist/utils/index.cjs.map +1 -1
  179. package/dist/utils/index.d.ts +1 -0
  180. package/dist/utils/index.d.ts.map +1 -1
  181. package/dist/utils/index.js +4 -1
  182. package/dist/utils/index.js.map +1 -1
  183. package/dist/utils/page-header-constants.d.ts +15 -0
  184. package/dist/utils/page-header-constants.d.ts.map +1 -0
  185. package/dist/utils/social-embed-cache.d.ts +29 -0
  186. package/dist/utils/social-embed-cache.d.ts.map +1 -0
  187. package/package.json +7 -1
  188. package/src/components/case-studies/index.ts +4 -0
  189. package/src/components/case-studies/share-experience-section.tsx +185 -0
  190. package/src/components/chat/embeddable-chat.tsx +1 -1
  191. package/src/components/docs/doc-viewer.tsx +111 -19
  192. package/src/components/docs/docs-hub-page.tsx +149 -0
  193. package/src/components/docs/index.ts +17 -0
  194. package/src/components/docs/skeletons.tsx +138 -0
  195. package/src/components/docs/use-docs-resolve-link.ts +52 -0
  196. package/src/components/docs/use-document-tree.ts +21 -0
  197. package/src/components/embeds/embed-container.tsx +80 -0
  198. package/src/components/embeds/embed-iframe.tsx +7 -9
  199. package/src/components/embeds/file-download-card.tsx +54 -0
  200. package/src/components/embeds/index.ts +30 -0
  201. package/src/components/embeds/linkedin-embed-client.tsx +100 -0
  202. package/src/components/embeds/markdown-image.tsx +88 -0
  203. package/src/components/embeds/og-link-preview.tsx +13 -13
  204. package/src/components/embeds/reddit-embed-client.tsx +550 -0
  205. package/src/components/embeds/rich-markdown-runtime.tsx +79 -0
  206. package/src/components/embeds/twitter-embed-client.tsx +308 -0
  207. package/src/components/layout/page-header.tsx +182 -0
  208. package/src/components/layout/page-layout.tsx +14 -1
  209. package/src/components/layout/page-with-header.tsx +110 -0
  210. package/src/components/layout/title-block.tsx +40 -62
  211. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
  212. package/src/components/shared/dev-section/dev-section-page.tsx +9 -1
  213. package/src/components/shared/dev-section/dev-section-view.tsx +14 -9
  214. package/src/components/shared/dev-section/index.ts +1 -1
  215. package/src/components/shared/doc-search/use-doc-search.ts +7 -3
  216. package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
  217. package/src/components/shared/product-release/release-detail-page.tsx +6 -4
  218. package/src/components/ui/index.ts +2 -0
  219. package/src/components/ui/release-changelog-section.tsx +7 -2
  220. package/src/components/ui/rich-markdown-renderer.tsx +1203 -0
  221. package/src/components/ui/simple-markdown-renderer.tsx +7 -11
  222. package/src/contexts/chat-runtime-context.tsx +14 -0
  223. package/src/types/doc-source.ts +33 -1
  224. package/src/utils/index.ts +1 -0
  225. package/src/utils/page-header-constants.ts +15 -0
  226. package/src/utils/social-embed-cache.ts +391 -0
  227. package/dist/chunk-26PKDALD.js +0 -2379
  228. package/dist/chunk-26PKDALD.js.map +0 -1
  229. package/dist/chunk-3MCHAFHB.js +0 -89
  230. package/dist/chunk-3MCHAFHB.js.map +0 -1
  231. package/dist/chunk-5E2HOSSH.cjs.map +0 -1
  232. package/dist/chunk-66AANIOC.cjs +0 -619
  233. package/dist/chunk-66AANIOC.cjs.map +0 -1
  234. package/dist/chunk-6JINAOI7.cjs +0 -311
  235. package/dist/chunk-6JINAOI7.cjs.map +0 -1
  236. package/dist/chunk-7RIYT7ZH.js.map +0 -1
  237. package/dist/chunk-AQOWFSMB.cjs.map +0 -1
  238. package/dist/chunk-BOCFIKYS.cjs +0 -3009
  239. package/dist/chunk-BOCFIKYS.cjs.map +0 -1
  240. package/dist/chunk-D652TJBQ.js +0 -3009
  241. package/dist/chunk-D652TJBQ.js.map +0 -1
  242. package/dist/chunk-E4XABBSU.js.map +0 -1
  243. package/dist/chunk-EL6QLAWX.js.map +0 -1
  244. package/dist/chunk-EYEW6PTA.cjs.map +0 -1
  245. package/dist/chunk-FQJK446R.js +0 -1606
  246. package/dist/chunk-FQJK446R.js.map +0 -1
  247. package/dist/chunk-GLLDTKZK.cjs.map +0 -1
  248. package/dist/chunk-IE6OU3WQ.cjs.map +0 -1
  249. package/dist/chunk-J54Z3OCR.cjs +0 -1606
  250. package/dist/chunk-J54Z3OCR.cjs.map +0 -1
  251. package/dist/chunk-K2PFPBMF.js.map +0 -1
  252. package/dist/chunk-KXCRGTRN.cjs +0 -2379
  253. package/dist/chunk-KXCRGTRN.cjs.map +0 -1
  254. package/dist/chunk-LCNMR277.js.map +0 -1
  255. package/dist/chunk-LFGGF7OT.cjs +0 -449
  256. package/dist/chunk-LFGGF7OT.cjs.map +0 -1
  257. package/dist/chunk-M2OCXTNT.js +0 -311
  258. package/dist/chunk-M2OCXTNT.js.map +0 -1
  259. package/dist/chunk-ME4EVDFP.js +0 -619
  260. package/dist/chunk-ME4EVDFP.js.map +0 -1
  261. package/dist/chunk-OQ6X7ZOC.js +0 -449
  262. package/dist/chunk-OQ6X7ZOC.js.map +0 -1
  263. package/dist/chunk-OY7OF7E7.js.map +0 -1
  264. package/dist/chunk-POKKCWKF.js +0 -354
  265. package/dist/chunk-POKKCWKF.js.map +0 -1
  266. package/dist/chunk-QHIXS3W2.cjs.map +0 -1
  267. package/dist/chunk-TFSYSWPS.cjs +0 -89
  268. package/dist/chunk-TFSYSWPS.cjs.map +0 -1
  269. package/dist/chunk-W6M2FLLT.cjs.map +0 -1
  270. package/dist/chunk-X647HY3F.cjs.map +0 -1
  271. package/dist/chunk-X6BV7MB7.cjs.map +0 -1
  272. package/dist/chunk-XREEV72C.cjs.map +0 -1
  273. package/dist/chunk-YETA25JW.cjs +0 -354
  274. package/dist/chunk-YETA25JW.cjs.map +0 -1
  275. package/dist/chunk-YIGPRLQY.cjs.map +0 -1
  276. /package/dist/{chunk-3ZXUQQL4.js.map → chunk-PI4WSYQV.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/platform-config.tsx","../src/types/tool.types.ts","../src/utils/access-code-client.ts","../src/utils/scroll-into-view.ts","../src/utils/same-page-hash-nav.ts","../src/utils/humanity-signals.ts"],"sourcesContent":["import React from 'react';\nimport { OpenmspLogo, FlamingoLogo, OpenFrameLogo, MiamiCyberGangLogoFaceOnly } from '../components/icons';\nimport { Globe } from 'lucide-react';\nimport type { SelectableOption } from '../components/features';\nimport type { PlatformConfig } from '../types/platform';\n\n// Platform icons mapping with consistent colors matching app theme\nexport const platformIcons = {\n openframe: <OpenFrameLogo className=\"h-5 w-5\" lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />,\n openmsp: <OpenmspLogo className=\"h-5 w-5\" />,\n flamingo: <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n 'flamingo-teaser': <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n 'marketing-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#F357BB\" />,\n 'product-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EA62E\" />,\n 'revenue-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#FFC008\" />,\n 'people-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EFAF0\" />,\n 'company-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#f36666\" />,\n tmcg: <MiamiCyberGangLogoFaceOnly className=\"h-5 w-5\" />,\n universal: <Globe className=\"h-5 w-5 text-[#10B981]\" />\n};\n\n// Platform colors mapping\nexport const platformColors = {\n openmsp: 'bg-[#3B82F6]',\n openframe: 'bg-[#8B5CF6]',\n flamingo: 'bg-[#EC4899]',\n 'flamingo-teaser': 'bg-[#F59E0B]',\n 'marketing-hub': 'bg-[#F357BB]',\n 'product-hub': 'bg-[#5EA62E]',\n 'revenue-hub': 'bg-[#FFC008]',\n 'people-hub': 'bg-[#5EFAF0]',\n 'company-hub': 'bg-[#f36666]',\n tmcg: 'bg-[#FF6B6B]',\n universal: 'bg-[#10B981]'\n};\n\n// Platform display names for consistent naming across the app\nexport const platformDisplayNames = {\n openmsp: 'OpenMSP',\n openframe: 'OpenFrame',\n flamingo: 'Flamingo',\n 'flamingo-teaser': 'Flamingo Teaser',\n 'marketing-hub': 'Flamingo Marketing Hub',\n 'product-hub': 'Flamingo Product Hub',\n 'revenue-hub': 'Flamingo Revenue Hub',\n 'people-hub': 'Flamingo People Hub',\n 'company-hub': 'Flamingo Company Hub',\n tmcg: 'TMCG',\n universal: 'Universal'\n};\n\n// Platform descriptions for consistent messaging across the app\nexport const platformDescriptions = {\n openmsp: 'Comprehensive directory and comparison platform for managed service providers (MSPs) and technology vendors. Reduce vendor costs and discover open-source alternatives.',\n openframe: 'AI-driven open-source security operations center (SOC) and endpoint detection platform for MSPs.',\n flamingo: 'AI-driven open-source OS for MSPs. Swap bloated vendor tools for open ones. Automate the boring crap. Take your margin back.',\n 'flamingo-teaser': 'Preview of Flamingo - the AI-driven open-source OS for MSPs.',\n tmcg: 'The Miami Cyber Gang - A cybersecurity community focused on education and collaboration.',\n universal: 'Cross-platform universal content.'\n};\n\n// Platform slogans for branding consistency\nexport const platformSlogans = {\n openmsp: 'Find Your Perfect MSP Partner',\n openframe: 'Open-Source Security Operations',\n flamingo: 'Open-Source OS for MSPs',\n 'flamingo-teaser': 'Coming Soon: Open-Source OS for MSPs',\n tmcg: 'Miami Cyber Community',\n universal: 'Universal Platform'\n};\n\n// Platform hex colors for default configuration\nexport const platformHexColors = {\n openmsp: '#FFC008',\n openframe: '#FFC008',\n flamingo: '#FF6B9D',\n universal: '#FFC008',\n 'flamingo-teaser': '#F59E0B',\n 'marketing-hub': '#F357BB',\n 'product-hub': '#5EA62E',\n 'revenue-hub': '#FFC008',\n 'people-hub': '#5EFAF0',\n 'company-hub': '#f36666',\n tmcg: '#FF6B6B'\n};\n\n// Platform icon names for default configuration\nexport const platformIconNames = {\n openmsp: 'openmsp-logo',\n openframe: 'openframe-logo',\n flamingo: 'flamingo-logo',\n universal: 'globe',\n 'flamingo-teaser': 'flamingo-logo',\n 'marketing-hub': 'flamingo-logo',\n 'product-hub': 'flamingo-logo',\n 'revenue-hub': 'flamingo-logo',\n 'people-hub': 'flamingo-logo',\n 'company-hub': 'flamingo-logo',\n tmcg: 'tmcg-logo'\n};\n\n/**\n * Get default color for platform\n */\nexport function getDefaultColorForPlatform(platformName: string): string {\n return platformHexColors[platformName as keyof typeof platformHexColors] || platformHexColors.universal;\n}\n\n/**\n * Get default icon name for platform\n */\nexport function getDefaultIconForPlatform(platformName: string): string {\n return platformIconNames[platformName as keyof typeof platformIconNames] || platformIconNames.universal;\n}\n\nexport function transformPlatformConfigsToOptions(platformConfigs: PlatformConfig[]): SelectableOption[] {\n return platformConfigs.map((platform: PlatformConfig) => ({\n id: platform.id, // Database UUID for matching\n name: platform.name, // Platform name enum\n displayName: platform.display_name, // Human-readable name\n description: platform.description,\n icon: platformIcons[platform.name as keyof typeof platformIcons] || platformIcons.universal,\n color: platformColors[platform.name as keyof typeof platformColors] || platformColors.universal\n }));\n}\n\n/**\n * Get platform icon by name\n */\nexport function getPlatformIcon(platformName: string) {\n return platformIcons[platformName as keyof typeof platformIcons] || platformIcons.universal;\n}\n\n/**\n * Get platform color by name\n */\nexport function getPlatformColor(platformName: string) {\n return platformColors[platformName as keyof typeof platformColors] || platformColors.universal;\n}\n\n/**\n * Get platform display name by name\n */\nexport function getPlatformDisplayName(platformName: string): string {\n return platformDisplayNames[platformName as keyof typeof platformDisplayNames] || platformName;\n}\n\n/**\n * Get platform description by name\n */\nexport function getPlatformDescription(platformName: string): string {\n return platformDescriptions[platformName as keyof typeof platformDescriptions] || platformName;\n}\n\n/**\n * Get platform slogan by name\n */\nexport function getPlatformSlogan(platformName: string): string {\n return platformSlogans[platformName as keyof typeof platformSlogans] || platformName;\n}\n\n/**\n * Get small platform icon for filter buttons with white colors (4x4 size)\n */\nexport function getSmallPlatformIcon(platformName: string): React.ReactNode {\n const className = \"h-4 w-4 flex-shrink-0\";\n\n switch (platformName) {\n case 'openframe':\n return <OpenFrameLogo className={className} lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />;\n case 'openmsp':\n return <OpenmspLogo className={className} frontBubbleColor=\"#f1f1f1\" innerFrontBubbleColor=\"#000000\" backBubbleColor=\"#FFC008\" />;\n case 'flamingo':\n case 'flamingo-teaser':\n return <FlamingoLogo className={`${className}`} fill=\"#EC4899\" />;\n case 'marketing-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n case 'product-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n case 'revenue-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n case 'people-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n case 'company-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n case 'tmcg':\n return <MiamiCyberGangLogoFaceOnly className={className} />;\n case 'universal':\n default:\n return <Globe className={className} />;\n }\n}\n\n/**\n * Get platform icon for admin/selector components (standard 6x6 size)\n */\nexport function getPlatformIconComponent(platformName: string, className: string = \"h-6 w-6\"): React.ReactNode {\n switch (platformName) {\n case 'openframe':\n return <OpenFrameLogo className={className} />;\n case 'openmsp':\n return <OpenmspLogo className={className} color=\"#f1f1f1\" />;\n case 'flamingo':\n case 'flamingo-teaser':\n return <FlamingoLogo className={`${className} text-white`} />;\n case 'marketing-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n case 'product-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n case 'revenue-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n case 'people-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n case 'company-hub':\n return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n case 'tmcg':\n return <MiamiCyberGangLogoFaceOnly size={24} className={className} />;\n case 'universal':\n default:\n return <Globe className={className} />;\n }\n}","/**\n * Centralized Tool Types\n *\n * Single source of truth for all tool-related types across the entire platform.\n * Used by ToolBadge, ToolIcon, and any component that needs tool type information.\n */\n\nexport const ToolTypeValues = {\n TACTICAL_RMM: 'TACTICAL_RMM',\n FLEET_MDM: 'FLEET_MDM',\n MESHCENTRAL: 'MESHCENTRAL',\n AUTHENTIK: 'AUTHENTIK',\n OPENFRAME: 'OPENFRAME',\n OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n OSQUERY: 'OSQUERY',\n SYSTEM: 'SYSTEM'\n} as const\n\nexport type ToolType = (typeof ToolTypeValues)[keyof typeof ToolTypeValues]\n\n/**\n * Maps tool types to display labels\n */\nexport const toolLabels: Record<ToolType, string> = {\n TACTICAL_RMM: 'Tactical',\n FLEET_MDM: 'Fleet',\n MESHCENTRAL: 'MeshCentral',\n AUTHENTIK: 'Authentik',\n OPENFRAME: 'OpenFrame',\n OPENFRAME_CHAT: 'OpenFrame Chat',\n OPENFRAME_CLIENT: 'OpenFrame Client',\n OSQUERY: 'Osquery',\n SYSTEM: 'System'\n}\n","/**\n * Access Code Client Utilities — pure standalone functions.\n *\n * Endpoint paths are NOT hardcoded — every function takes an\n * `endpoints` argument. The React-side wrapper that binds them from\n * `EndpointsRuntimeContext` lives separately at\n * `hooks/use-access-code-integration.ts` (`useAccessCodeIntegration`).\n *\n * Keep this file **free of React imports** — it lives in the\n * server-safe `utils/index` tsup bundle. Any module-top-level call\n * into `createContext()` (which the runtime context file does) would\n * be pulled into the server bundle and crash SSR with\n * `createContext is not a function`.\n */\n\nimport {\n AccessCodeValidation,\n AccessCodeValidationResponse,\n AccessCodeConsumptionResponse\n} from '../types/access-code-cohorts';\n\n/** Endpoints required by the standalone client utilities. The\n * `useAccessCodeIntegration` hook (in `hooks/`) resolves these from\n * `EndpointsRuntimeContext.accessCode` automatically. */\nexport interface AccessCodeEndpoints {\n validateUrl: string\n consumeUrl: string\n}\n\n/**\n * Validate an access code for a given email\n *\n * @param email - User's email address\n * @param code - Access code to validate\n * @returns Promise with validation result\n *\n * @example\n * const result = await validateAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid) {\n * // Allow user to proceed with registration\n * console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n * // Show error message\n * console.error(result.message);\n * }\n */\nexport async function validateAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse> {\n try {\n const response = await fetch(endpoints.validateUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ email, code } as AccessCodeValidation),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error || 'Validation request failed');\n }\n\n return await response.json() as AccessCodeValidationResponse;\n } catch (error) {\n return {\n valid: false,\n message: error instanceof Error ? error.message : 'Validation failed',\n };\n }\n}\n\n/**\n * Consume an access code after successful registration\n *\n * Call this ONLY after the user has successfully completed registration.\n * This marks the code as used and prevents further usage.\n *\n * @param email - User's email address\n * @param code - Access code to consume\n * @returns Promise with consumption result\n *\n * @example\n * // After successful registration\n * const result = await consumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.consumed) {\n * console.log('Access code consumed successfully');\n * } else {\n * console.warn('Failed to consume access code:', result.message);\n * }\n */\nexport async function consumeAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeConsumptionResponse> {\n try {\n const response = await fetch(endpoints.consumeUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ email, code } as AccessCodeValidation),\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error || 'Consumption request failed');\n }\n\n return await response.json() as AccessCodeConsumptionResponse;\n } catch (error) {\n return {\n success: false,\n consumed: false,\n message: error instanceof Error ? error.message : 'Consumption failed',\n };\n }\n}\n\n/**\n * Complete access code flow: validate then consume\n *\n * This is a convenience function that validates an access code and,\n * if valid, immediately consumes it. Use this when you want to\n * validate and consume in one step during registration.\n *\n * @param email - User's email address\n * @param code - Access code to validate and consume\n * @returns Promise with validation and consumption results\n *\n * @example\n * const result = await validateAndConsumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid && result.consumed) {\n * // Registration successful\n * console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n * console.error(result.message);\n * }\n */\nexport async function validateAndConsumeAccessCode(\n email: string,\n code: string,\n endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse & { consumed?: boolean }> {\n // First validate\n const validation = await validateAccessCode(email, code, endpoints);\n\n if (!validation.valid) {\n return validation;\n }\n\n // If valid, consume the code\n const consumption = await consumeAccessCode(email, code, endpoints);\n\n return {\n ...validation,\n consumed: consumption.consumed,\n message: consumption.consumed\n ? `Access granted for ${validation.cohort_name}`\n : consumption.message || validation.message,\n };\n}\n\n// `useAccessCodeIntegration` (the React-side wrapper) lives in\n// `hooks/use-access-code-integration.ts`. It binds the endpoints from\n// `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\n","/**\n * `scrollElementIntoView` — canonical \"scroll an element to the top of the\n * viewport, account for sticky chrome, survive layout shifts\" helper.\n *\n * One shared implementation so every caller (the ticket drawer expand, the\n * hub's `useUnifiedNav` / `use-nav-link` hash scroll, doc-tree, delivery\n * `?focus=`, sticky-section-nav, …) inherits the SAME cancellation-proof\n * motion.\n *\n * WHY A SELF-DRIVEN rAF TWEEN INSTEAD OF `window.scrollTo({behavior:'smooth'})`:\n * the native smooth scroll is CANCELLABLE, and in real pages it gets cancelled\n * constantly:\n *\n * - Browser SCROLL ANCHORING: when content is inserted/removed above or\n * around the target (a collapsible drawer expanding, an async image\n * loading, a list re-rendering) the browser issues a synchronous scrollTop\n * correction to keep the anchored element stable. Per CSSOM-View \"perform a\n * scroll\" step 1 (\"abort any ongoing smooth scroll\"), that correction\n * ABORTS an in-flight native smooth scroll — so it lands as an instant jump.\n * Anchoring is suppressed when the scroll offset is 0, which is exactly why\n * a native smooth scroll appears to work the FIRST time (page at top) and\n * jumps on every repeat (page already scrolled). This was a multi-day\n * \"smooth only works once\" bug on the /tickets drawer.\n * - A second programmatic scroll on the same frame, or a `focus()` without\n * `{preventScroll:true}`, cancels it the same way.\n *\n * A tween that re-asserts the position with INSTANT writes every frame is\n * immune: there is no \"ongoing native smooth scroll\" for anchoring/focus to\n * abort, and any correction that lands between our frames is overwritten on the\n * next frame. We also RECOMPUTE the target each frame, so an element whose\n * final position is still settling (drawer still expanding, images loading)\n * is tracked to its resting place instead of animating to a stale pixel.\n *\n * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user\n * scroll intent (wheel / touch) so we never fight the user.\n *\n * WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window\n * scroller. It walks up from the target to the nearest ancestor that is an\n * actual scroll container (`overflow-y: auto | scroll | overlay` AND\n * `scrollHeight > clientHeight`) and drives THAT element; only when none exists\n * does it fall back to `window`. This is what makes it work inside app shells\n * that put page content in a fixed-height `<main class=\"overflow-y-auto\">`\n * (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the\n * old window-only version was a silent no-op there. Note `overflow: clip` /\n * `hidden` are deliberately NOT treated as scroll containers, so a list wrapper\n * that uses `overflow-clip` only to round its corners still bubbles the scroll\n * up to the real container (matches the `<HelpCenterCard>` list intent).\n */\n\nexport interface ScrollElementIntoViewOptions {\n /** Pixels to subtract from the target element's `top` so it lands BELOW\n * sticky chrome. Defaults to 0. Pass `96` for the standard hub header. */\n headerOffset?: number\n /** `'smooth'` (default) runs the self-driven tween; `'instant'` / `'auto'`\n * jump in one synchronous write (deep-link land, programmatic focus moves). */\n behavior?: ScrollBehavior\n /** Optional adjustment applied to the computed pixel target each frame. The\n * callback receives the \"raw\" Y (`element.top + scrollY - headerOffset`) and\n * returns the FINAL target. Use when the caller knows about a layout shift\n * (e.g. a sibling drawer collapsing) the geometry can't yet reflect. */\n adjustTargetY?: (rawTargetY: number) => number\n /** Tween duration in ms (smooth only). Default 320. */\n durationMs?: number\n}\n\n/** Module-level handle to the in-flight tween so a new call (or a user\n * gesture) cancels the previous one — only ever one page-scroll animation at\n * a time. */\nlet activeRaf = 0\nlet teardownActive: (() => void) | null = null\n\nfunction cancelActiveScroll(): void {\n if (activeRaf) {\n cancelAnimationFrame(activeRaf)\n activeRaf = 0\n }\n if (teardownActive) {\n teardownActive()\n teardownActive = null\n }\n}\n\nconst easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)\n\n/** Nearest ancestor that is a *real* scroll container, or `null` when the\n * window/document is the scroller. Only `auto | scroll | overlay` count —\n * `clip` / `hidden` are intentionally excluded (a wrapper using `overflow-clip`\n * purely to round corners must let the scroll bubble to the page). */\nfunction getScrollableAncestor(el: HTMLElement): HTMLElement | null {\n for (let node = el.parentElement; node; node = node.parentElement) {\n const overflowY = getComputedStyle(node).overflowY\n if (\n (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&\n node.scrollHeight > node.clientHeight\n ) {\n return node\n }\n }\n return null\n}\n\n/**\n * Scroll the page so `target` lands at the top of the viewport (below sticky\n * chrome via `headerOffset`). SSR-safe; `null`/`undefined` target is a no-op so\n * callers can pass refs without defensive branching.\n */\nexport function scrollElementIntoView(\n target: HTMLElement | null | undefined,\n options: ScrollElementIntoViewOptions = {},\n): void {\n if (typeof window === 'undefined' || !target) return\n const { headerOffset = 0, behavior = 'smooth', adjustTargetY, durationMs = 320 } = options\n\n // Pick the scroller ONCE: a fixed-height `<main overflow-y-auto>` shell scrolls\n // the element, a plain document scrolls the window. The choice can't change\n // mid-tween, so resolve it up front and route every read/write through it.\n const container = getScrollableAncestor(target)\n const readCurrent = (): number => (container ? container.scrollTop : window.scrollY)\n const writeTo = (y: number): void => {\n if (container) container.scrollTop = y\n else window.scrollTo(0, y)\n }\n\n // Target is recomputed every frame: the row's absolute position can move as\n // the page reflows (a sibling drawer collapsing) and the reachable max grows\n // as the just-opened drawer expands. Clamp to the LIVE max each frame.\n const computeTarget = (): number => {\n const raw = container\n ? container.scrollTop +\n (target.getBoundingClientRect().top - container.getBoundingClientRect().top) -\n headerOffset\n : target.getBoundingClientRect().top + window.scrollY - headerOffset\n const adjusted = adjustTargetY ? adjustTargetY(raw) : raw\n const maxScroll = container\n ? Math.max(0, container.scrollHeight - container.clientHeight)\n : Math.max(0, document.documentElement.scrollHeight - window.innerHeight)\n return Math.min(Math.max(0, adjusted), maxScroll)\n }\n\n // Any prior animation loses — one page scroll at a time.\n cancelActiveScroll()\n\n const prefersReduced =\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n // Instant paths: a single synchronous write. No tween, no anchoring race.\n if (behavior === 'instant' || behavior === 'auto' || prefersReduced) {\n writeTo(computeTarget())\n return\n }\n\n // Smooth: self-driven tween with instant per-frame writes (anchoring-proof).\n let startY: number | null = null\n let startTime = 0\n\n // Bail the moment the user takes over with a real scroll gesture — we must\n // never fight them. (Not keydown: the ticket composer auto-focuses on open,\n // and typing there should not abort the scroll.)\n const onUserGesture = () => cancelActiveScroll()\n window.addEventListener('wheel', onUserGesture, { passive: true })\n window.addEventListener('touchmove', onUserGesture, { passive: true })\n teardownActive = () => {\n window.removeEventListener('wheel', onUserGesture)\n window.removeEventListener('touchmove', onUserGesture)\n }\n\n const step = (now: number) => {\n if (startY === null) {\n startY = readCurrent()\n startTime = now\n }\n const targetY = computeTarget()\n const t = Math.min(1, (now - startTime) / durationMs)\n const y = startY + (targetY - startY) * easeOutCubic(t)\n writeTo(y)\n if (t < 1) {\n activeRaf = requestAnimationFrame(step)\n } else {\n // Final exact write in case easing left a sub-pixel gap, then teardown.\n writeTo(computeTarget())\n activeRaf = 0\n if (teardownActive) {\n teardownActive()\n teardownActive = null\n }\n }\n }\n activeRaf = requestAnimationFrame(step)\n}\n","import { scrollElementIntoView } from './scroll-into-view'\n\n/** Pages with a section-nav STRIP on top of the global hub header\n * (dev-center roadmap/delivery/tickets, FAQ category-pill nav).\n * Anchor lands BELOW both layers. */\nexport const STICKY_HEADER_OFFSET_PX = 96\n\n/** Pages with only the global hub header (docs, blog, vendor detail).\n * Anchor lands BELOW the header bar. */\nexport const HUB_HEADER_OFFSET_PX = 80\n\n/**\n * Take only the FIRST hash segment from a fragment that may contain extra\n * `#` characters. `'' → ''`, `'#a' → '#a'`, `'#a#b' → '#a'`.\n *\n * No real DOM id contains `#`, so a multi-fragment hash is always a bug at\n * the composer site; `navigateSamePageHash` + `useScrollToHash` both call\n * this so URL bar and `getElementById` stay in sync.\n */\nexport function normalizeHashFragment(hash: string): string {\n if (!hash) return ''\n const second = hash.indexOf('#', 1)\n return second < 0 ? hash : hash.slice(0, second)\n}\n\nexport interface NavigateSamePageHashOptions {\n /** Pixels to subtract for sticky chrome. */\n headerOffset?: number\n /** `'push'` (default) — new history entry; `'replace'` — overwrite\n * current entry (use for TOC-style in-page navigators). */\n history?: 'push' | 'replace'\n}\n\n/**\n * Same-page hash navigation primitive: pushState + synthetic `hashchange`\n * + anchoring-proof smooth scroll. Replaces `router.push` for hash CTAs\n * (Next.js suppresses smooth-scroll during navigation; `router.push` on\n * an exact-URL match is a no-op). Returns `true` when the helper claimed\n * the nav (same pathname + search); `false` for cross-page targets so\n * callers fall through to `router.push`.\n *\n * `target` accepts an origin-stripped path (`/x#anchor`) or a bare hash\n * (`#anchor`); bare-hash callers don't need to reconstruct `pathname +\n * search` themselves.\n */\nexport function navigateSamePageHash(\n target: string,\n options: NavigateSamePageHashOptions = {},\n): boolean {\n if (typeof window === 'undefined') return false\n const { headerOffset = 0, history: historyMode = 'push' } = options\n const normalizedTarget =\n target.startsWith('#')\n ? window.location.pathname + window.location.search + target\n : target\n // `new URL(absoluteUrl, base)` ignores `base` per RFC 3986; an absolute\n // cross-origin target sharing pathname/search would otherwise pass the\n // check below and trip pushState's same-origin enforcement. Parse with\n // an explicit base so malformed inputs cleanly fall through.\n let url: URL\n try {\n url = new URL(normalizedTarget, window.location.href)\n } catch {\n return false\n }\n if (\n url.origin !== window.location.origin ||\n url.pathname !== window.location.pathname ||\n url.search !== window.location.search\n ) {\n return false\n }\n const current = window.location.pathname + window.location.search + window.location.hash\n // Heal a malformed multi-fragment hash so the URL bar is clean and\n // `getElementById` resolves. Dev-warn fingers the upstream composer.\n const normalizedHash = normalizeHashFragment(url.hash)\n if (process.env.NODE_ENV === 'development' && normalizedHash !== url.hash) {\n // eslint-disable-next-line no-console\n console.warn(\n `[navigateSamePageHash] malformed fragment \"${url.hash}\" → normalizing to \"${normalizedHash}\". Fix the upstream composer.`,\n )\n }\n const next = url.pathname + url.search + normalizedHash\n const id = normalizedHash && normalizedHash !== '#' ? normalizedHash.slice(1) : ''\n // Hash-less targets are only ours on an EXACT URL re-click.\n if (!id && next !== current) return false\n if (next !== current) {\n const oldURL = window.location.href\n if (historyMode === 'replace') {\n window.history.replaceState(null, '', next)\n } else {\n window.history.pushState(null, '', next)\n }\n // Synthetic `hashchange` — `pushState` doesn't fire it (HTML spec),\n // so URL-hash-bound listeners (FAQ auto-expand, etc.) wouldn't react.\n window.dispatchEvent(new HashChangeEvent('hashchange', {\n oldURL,\n newURL: window.location.href,\n }))\n }\n const el = id ? document.getElementById(id) : null\n if (id && !el && process.env.NODE_ENV === 'development') {\n // eslint-disable-next-line no-console\n console.warn(\n `[navigateSamePageHash] anchor \"#${id}\" not found — scrolling to top.`,\n )\n }\n // Missing anchor → tween to page top. `documentElement` is at 0 by\n // definition, so one tween covers both branches.\n scrollElementIntoView(el ?? document.documentElement, {\n behavior: 'smooth',\n headerOffset,\n })\n return true\n}\n","/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n */\n\n/** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */\nexport const HONEYPOT_FIELD = 'contact_url_confirm'\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms'\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true } | { ok: false; reason: 'honeypot' | 'too_fast' }\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n const b = (body ?? {}) as Record<string, unknown>\n const rawHp = b[HONEYPOT_FIELD]\n // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n // so ANY present non-string value is a bot filling the decoy with a non-string\n // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n const honeypot = rawHp == null ? '' : String(rawHp)\n const rawMs = b[ELAPSED_MS_FIELD]\n const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null\n return { honeypot, elapsedMs }\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n const { honeypot, elapsedMs } = extractHumanitySignals(body)\n if (honeypot.trim() !== '') return { ok: false, reason: 'honeypot' }\n if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' }\n return { ok: true }\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n s?.split(',').map((t) => t.trim()).filter(Boolean) ?? []\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/unified-pagination.tsx","../src/components/empty-state.tsx","../src/components/shared/dev-section/dev-section-view.tsx","../src/components/shared/dev-section/dev-section-page.tsx","../src/components/shared/dev-section/dev-card-row.tsx","../src/components/shared/delivery/delivery-row.tsx"],"sourcesContent":["\"use client\"\n\nimport { useRouter, useSearchParams, usePathname } from \"../embed-shims/next-navigation\"\nimport { Pagination } from \"./pagination\"\n\ninterface UnifiedPaginationProps {\n currentPage: number\n totalPages: number\n onPageChange?: (page: number) => void\n className?: string\n}\n\nexport function UnifiedPagination({ \n currentPage, \n totalPages, \n onPageChange,\n className = \"mt-8 flex justify-center w-full\"\n}: UnifiedPaginationProps) {\n const router = useRouter()\n const searchParams = useSearchParams()\n const pathname = usePathname()\n\n const handlePageChange = (page: number) => {\n // Preserve current scroll position\n const currentScrollY = window.scrollY\n \n // Call the callback to update local state (prevents reload)\n if (onPageChange) {\n onPageChange(page)\n }\n \n // Update URL for bookmarking without navigation\n const params = new URLSearchParams(searchParams.toString())\n params.set(\"page\", page.toString())\n \n // Update URL without navigation (for bookmarking support)\n const newUrl = `${pathname}?${params.toString()}`\n window.history.replaceState(null, '', newUrl)\n \n // Restore scroll position after a brief delay to allow content to render\n setTimeout(() => {\n window.scrollTo({\n top: currentScrollY,\n behavior: 'instant' // Instant to prevent any scroll animation\n })\n }, 0)\n }\n\n // Don't render pagination if there's only one page\n if (totalPages <= 1) return null\n\n return (\n <div className={className}>\n <Pagination\n currentPage={currentPage}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n )\n} ","\"use client\";\n\nimport { Search, FileText, Package } from \"lucide-react\"\nimport { Button } from \"./ui/button\"\nimport { useRouter } from \"../embed-shims/next-navigation\"\n\nexport interface EmptyStateProps {\n type: 'vendors' | 'posts' | 'search' | 'generic'\n title?: string\n description?: string\n showBackButton?: boolean\n onGoBack?: () => void\n backButtonText?: string\n // New CTA properties\n showCTA?: boolean\n ctaText?: string\n onCtaClick?: () => void\n ctaVariant?: 'primary' | 'secondary'\n}\n\nexport function EmptyState({\n type,\n title,\n description,\n showBackButton = false,\n onGoBack,\n backButtonText = \"Go Back\",\n showCTA = true,\n ctaText,\n onCtaClick,\n ctaVariant = 'primary'\n}: EmptyStateProps) {\n const router = useRouter()\n\n // Default content based on type\n const getDefaultContent = () => {\n switch (type) {\n case 'vendors':\n return {\n icon: <Package className=\"w-full h-full\" />,\n title: \"No vendors found\",\n description: \"We couldn't find any vendors matching your criteria. Try adjusting your filters or search terms.\"\n }\n case 'posts':\n return {\n icon: <FileText className=\"w-full h-full\" />,\n title: \"No articles found\",\n description: \"We couldn't find any articles matching your criteria. Try different categories, tags, or search terms.\"\n }\n case 'search':\n return {\n icon: <Search className=\"w-full h-full\" />,\n title: \"No results found\",\n description: \"Your search didn't return any results. Try different keywords or browse our categories.\"\n }\n default:\n return {\n icon: <Search className=\"w-full h-full\" />,\n title: \"Nothing found\",\n description: \"We couldn't find what you're looking for. Try adjusting your search or filters.\"\n }\n }\n }\n\n // Smart CTA logic based on context\n const getSmartCTA = () => {\n // If custom CTA is provided, use it\n if (ctaText && onCtaClick) {\n return {\n text: ctaText,\n action: onCtaClick\n }\n }\n\n // Check if we're on the client side\n const isClient = typeof window !== 'undefined'\n const currentPath = isClient ? window.location.pathname : ''\n\n // Smart defaults based on type and context\n switch (type) {\n case 'search':\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Try to reset search by clearing URL params and refreshing\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n case 'posts':\n // If we're on blog/community pages, reset blog filters\n if (currentPath.includes('/blog')) {\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Reset blog search and filters by clearing URL params\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n } else if (currentPath.includes('/profile')) {\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n }\n return {\n text: \"View All Posts\",\n action: () => router.push('/blog')\n }\n case 'vendors':\n // If we're in profile or other pages, direct to main content\n if (currentPath.includes('/profile')) {\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n } else if (currentPath.includes('/vendors') || currentPath.includes('/margin-increase/compare')) {\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Reset vendor search and filters by clearing URL params\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n }\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n default:\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n }\n }\n\n const defaultContent = getDefaultContent()\n const displayTitle = title || defaultContent.title\n const displayDescription = description || defaultContent.description\n const smartCTA = getSmartCTA()\n\n return (\n <div className=\"flex flex-col items-center justify-center py-6 md:py-16 px-6 text-center\">\n {/* Icon */}\n <div className=\"mb-3 md:mb-6 flex items-center justify-center\">\n <div className=\"rounded-full bg-ods-card p-3 md:p-6 border border-ods-border\">\n <div className=\"w-8 h-8 md:w-16 md:h-16 text-ods-text-secondary flex items-center justify-center\">\n {defaultContent.icon}\n </div>\n </div>\n </div>\n\n {/* Title */}\n <h2 className=\"mb-2 md:mb-3 text-lg md:text-xl font-semibold font-['DM_Sans'] text-ods-text-primary tracking-[-0.02em]\">\n {displayTitle}\n </h2>\n\n {/* Description */}\n <p className=\"mb-4 md:mb-8 max-w-md text-sm font-medium font-['DM_Sans'] text-ods-text-secondary leading-[1.43em]\">\n {displayDescription}\n </p>\n\n {/* Smart CTA Button */}\n {showCTA && smartCTA && (\n <div className=\"w-full max-w-xs mb-3\">\n <Button\n onClick={smartCTA.action}\n className={ctaVariant === 'primary'\n ? \"w-full bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover transition-all duration-150 font-['DM_Sans'] font-medium\"\n : \"w-full bg-transparent border border-ods-border text-ods-text-primary hover:border-ods-accent hover:text-ods-accent transition-all duration-150 font-['DM_Sans'] font-medium\"\n }\n >\n {smartCTA.text}\n </Button>\n </div>\n )}\n\n {/* Optional Back Button */}\n {showBackButton && onGoBack && (\n <div className=\"w-full max-w-xs\">\n <Button\n onClick={onGoBack}\n variant=\"outline\"\n className=\"w-full transition-all duration-150 font-['DM_Sans'] font-medium\"\n >\n {backButtonText}\n </Button>\n </div>\n )}\n </div>\n )\n} ","'use client';\n\n/**\n * DevSectionView — the canonical chrome for ANY dev-center section\n * (Roadmap / Delivery / Releases). One component, used in BOTH:\n *\n * - tabbed `/roadmap-and-releases` (compact title mode, no `hero`)\n * - full-page `/roadmap`, `/bug-fixes-and-enhancements`, `/releases`\n * (hero mode with icon + description + back link)\n *\n * Owns: title rendering, the inline search input, the filter pill row,\n * and the URL-param wiring that connects both. The list `children`\n * receive a clean URL contract — they read `?<paramKey>=...` via\n * `useSearchParams()` and refetch on change. No duplicated controls.\n */\n\nimport type { ReactNode } from 'react';\nimport { useState, useEffect } from 'react';\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims';\nimport { SearchInput } from '../../ui';\nimport { StatusFilterComponent } from '../../features';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\n\nexport interface DevSectionViewProps {\n /** Which section to render — drives title, search, and filter\n * config via the `OPENFRAME_DEV_SECTIONS` registry. */\n sectionKey: OpenframeDevSectionKey;\n /** When set, renders the rich page-level hero (icon + h1 + description).\n * Omit for the compact tab-context heading. */\n hero?: {\n /** Pre-rendered icon JSX. Server components render the icon themselves\n * and pass the element here — function references can't cross the\n * server→client boundary, but React elements can. */\n icon: ReactNode;\n /** Hero title. Falls back to `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`\n * when omitted, so embedders can override the (OpenFrame-specific) default\n * copy without forking the registry. */\n title?: string;\n description: string;\n };\n /** Optional slot rendered BETWEEN the hero and the search/filter\n * controls. Use this for an entry-action surface that should sit\n * above the list (e.g. the Help Center's \"Open a new ticket\" form).\n * The slot is wrapped in the same `gap-10` flex column so spacing\n * matches the surrounding chrome — callers should NOT add their\n * own top/bottom margin. Renders `null` (no DOM) when omitted. */\n preControls?: ReactNode;\n /** The page-specific list body. Reads URL params written by this\n * component (search input + filter pills). */\n children: ReactNode;\n}\n\nexport function DevSectionView({ sectionKey, hero, preControls, children }: DevSectionViewProps) {\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n const search = section.search;\n const filter = section.filter;\n\n const currentSearch = search ? searchParams.get(search.paramKey) || '' : '';\n const currentFilterValue = filter\n ? searchParams.get(filter.paramKey) || filter.defaultValue\n : '';\n\n // Controlled search-input state — input commits to the URL only on\n // Enter (not on every keystroke), preserving the legacy behavior.\n // Lazy init from URL avoids a brief flash of stale value on first\n // paint after URL-driven re-render (e.g. tab switch).\n const [searchValue, setSearchValue] = useState(() => currentSearch);\n useEffect(() => {\n setSearchValue(currentSearch);\n }, [currentSearch]);\n\n const handleSearchSubmit = (value: string) => {\n if (!search) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value.trim()) params.set(search.paramKey, value.trim());\n else params.delete(search.paramKey);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n const handleFilterChange = (value: string) => {\n if (!filter) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value === filter.defaultValue) params.delete(filter.paramKey);\n else params.set(filter.paramKey, value);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n return (\n <div className=\"w-full flex flex-col gap-10\">\n {hero ? (\n <div className=\"space-y-4\">\n <h1 className=\"text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3\">\n {hero.icon}\n {hero.title ?? section.hero.title}\n </h1>\n <p className=\"font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl\">\n {hero.description}\n </p>\n </div>\n ) : (\n <div className=\"flex items-center justify-between w-full\">\n <h2 className=\"font-['Azeret_Mono'] font-semibold text-[32px] md:text-[40px] lg:text-[48px] leading-[40px] md:leading-[48px] lg:leading-[56px] text-ods-text-primary tracking-[-0.64px] md:tracking-[-0.8px] lg:tracking-[-0.96px]\">\n {section.hero.title}\n <span className=\"text-ods-accent\">:</span>\n </h2>\n </div>\n )}\n\n {preControls}\n\n {(search || filter) && (\n <div className=\"space-y-4\">\n {search && (\n <SearchInput\n showDropdown={false}\n placeholder={search.placeholder}\n value={searchValue}\n onChange={setSearchValue}\n onSubmit={handleSearchSubmit}\n />\n )}\n {filter && (\n <StatusFilterComponent\n selectedStatus={currentFilterValue}\n onStatusChange={handleFilterChange}\n statusOptions={[...filter.options]}\n />\n )}\n </div>\n )}\n\n {children}\n </div>\n );\n}\n","'use client';\n\n/**\n * DevSectionPage — full-page wrapper for a dev-center section\n * (`/roadmap`, `/bug-fixes-and-enhancements`, `/releases`).\n *\n * Mounts the lib's canonical `PageLayout` directly (no in-app wrapper)\n * so the back-button affordance stays in lockstep with whatever the\n * design system ships — any future lib change to BackButton / TitleBlock\n * propagates automatically.\n *\n * Composition: `PageShell` → `PageLayout` (back-to-home wired) →\n * `DevSectionView` (icon hero + search + filter pills) → list body.\n *\n * Adding a new section is one entry in `OPENFRAME_DEV_SECTIONS` plus a\n * single-line page file mounting this factory with the new key.\n */\n\nimport type { ReactNode } from 'react';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { PageShell, PageLayout } from '../../ui';\nimport { DevSectionView } from './dev-section-view';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\n\nconst SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent';\n\nexport interface DevSectionPageProps {\n sectionKey: OpenframeDevSectionKey;\n /** The page-specific list body (e.g. `<RoadmapList />`). */\n children: ReactNode;\n /** Optional slot rendered BETWEEN the hero and search/filter — see\n * `DevSectionView.preControls`. Used by surfaces that want an entry\n * action (e.g. Help Center's \"Open a new ticket\" form) above the\n * controls instead of below them. */\n preControls?: ReactNode;\n /** Back-button config — same shape as `LegalDocumentPage` /\n * `ReleaseDetailPage`. Pass `false` to hide entirely. Default\n * `{ label: 'Back to home', href: '/' }` — embedders whose \"home\" isn't `/`\n * should override `href`, or pass `false` if the embed has no home page. */\n backButton?: { label?: string; href?: string } | false;\n /** Override the hero title. Defaults to the (OpenFrame-specific) copy in\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`. Set this to brand the\n * section for a non-OpenFrame embed. */\n title?: string;\n /** Override the hero subtitle/description. Defaults to\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.description`. */\n subtitle?: string;\n}\n\nexport function DevSectionPage({\n sectionKey,\n children,\n preControls,\n backButton,\n title,\n subtitle,\n}: DevSectionPageProps) {\n const router = useRouter();\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const Icon = section.icon;\n\n // Back-button config — mirrors LegalDocumentPage / ReleaseDetailPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide.\n // After `backButton &&` narrowing, inner type is `{ label?, href? } |\n // undefined`; don't re-compare to `false` (TS2367).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: (backButton ? backButton.label : undefined) ?? 'Back to home',\n onClick: () => router.push((backButton ? backButton.href : undefined) ?? '/'),\n };\n\n return (\n <PageShell>\n <PageLayout backButton={backCfg}>\n <DevSectionView\n sectionKey={sectionKey}\n hero={{\n icon: <Icon className={SECTION_HERO_ICON_CLASS} />,\n title,\n description: subtitle ?? section.hero.description,\n }}\n preControls={preControls}\n >\n {children}\n </DevSectionView>\n </PageLayout>\n </PageShell>\n );\n}\n","'use client';\n\n/**\n * Shared row chrome for any `DevSectionPage` list (delivery, tickets,\n * future sections). One source of truth for the layout that every\n * dev-section card row uses:\n * left column → title (h3) / subtitle (h5 uppercase) / description\n * (h4 line-clamp-3), each in a fixed min-height block\n * so rows align across the grid\n * right column → caller-supplied stacked badges\n *\n * Surface stays small on purpose — `rightBadges` is a `ReactNode` so\n * the caller decides how many badges (delivery: 2, tickets: 1-2,\n * future: anything). No behavior baked in: the caller wraps the row\n * in a `<div>` (static, like delivery) or `<button>` (clickable, like\n * tickets) and renders the row content via this component.\n *\n * Pair with `DevCardRowSkeletonList` for the loading state — the\n * skeleton mirrors the same min-heights so the in-flight UI doesn't\n * shift the layout when real data lands.\n *\n * NOTE: the ticket conversation row is NOT here — it renders the shared\n * `<ChatMessageRow>` (`components/chat/chat-message-row.tsx`), the SAME\n * component the OpenMSP Slack-community feed uses, so the two surfaces stay\n * pixel-identical by construction.\n */\n\nimport type { ReactNode } from 'react';\n\nexport interface DevCardRowContentProps {\n title: string;\n /** Single-line uppercase metadata (e.g. \"UPDATED today, #4271, Code review\"). */\n subtitle: string;\n /** 3-line description block. Empty string renders the fallback. */\n description: string;\n /** Fallback copy when `description` is empty. Defaults to a generic\n * string; ticket / delivery surfaces override. */\n emptyDescription?: string;\n /** Right column — caller renders its own stacked badges. */\n rightBadges: ReactNode;\n}\n\nexport function DevCardRowContent({\n title,\n subtitle,\n description,\n emptyDescription = 'No description provided',\n rightBadges,\n}: DevCardRowContentProps) {\n return (\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n <div className=\"min-h-[24px] flex items-center\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words\">\n {title}\n </h3>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <p className=\"text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate\">\n {subtitle}\n </p>\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <p className=\"text-h4 text-ods-text-secondary line-clamp-3 break-words\">\n {description || emptyDescription}\n </p>\n </div>\n </div>\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n {rightBadges}\n </div>\n </div>\n );\n}\n\n/**\n * Skeleton rendering for a single row — the bars mirror the same\n * min-heights as `DevCardRowContent` so the loading→loaded swap\n * doesn't reflow.\n */\nexport function DevCardRowSkeleton() {\n return (\n <div className=\"border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px]\">\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n <div className=\"min-h-[24px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-1/2\" />\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-2/3\" />\n </div>\n </div>\n </div>\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\" />\n <div className=\"h-[32px] w-[120px] bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * The standard \"5 skeleton rows inside a bordered card\" loading state\n * used by every list shell. Both delivery (`delivery-table.tsx`) and\n * tickets (`tickets-list.tsx`) mount this directly.\n */\nexport function DevCardRowSkeletonList({ rows = 5 }: { rows?: number }) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n {Array.from({ length: rows }, (_, i) => (\n <DevCardRowSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client'\n\n/**\n * `<DeliveryRow />` — canonical single-row presentation for a ClickUp\n * delivery item.\n *\n * Single source of truth: both the `/bug-fixes-and-enhancements` page\n * (via `DeliveryTable`) AND the linked-delivery card on a HubSpot ticket\n * (via `TicketLinkedDeliveryCard`) compose this primitive. Visual parity\n * across those two surfaces is the design goal — the user reads the\n * card on their ticket and recognises it as a row from the public\n * delivery list.\n *\n * Behaviors:\n * - `href` set → outer element is an `<a>`, the whole row becomes\n * clickable (used by the linked-card surface to deep-link into\n * `/bug-fixes-and-enhancements?focus=<id>`).\n * - `id` set → outer element gets that DOM id so the consuming page\n * can `scrollIntoView` to it when the URL carries `?focus=<id>`.\n * - `highlighted` true → brief accent border + background pulse\n * (`animate-flash-focus` keyframe defined in `tailwind.config.ts`).\n * - `caption` set → small uppercase label rendered above the title\n * (\"LINKED DELIVERY\" on the ticket-side variant). Omitted on the\n * standard list rendering.\n */\n\nimport * as React from 'react'\nimport Link from '../../../embed-shims/next-link'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { getStatusColorScheme } from '../../../utils'\nimport {\n type DeliveryItem,\n TASK_TYPE_LABELS,\n TASK_TYPE_TEXT_COLORS,\n} from '../../../types/delivery'\nimport { cn } from '../../../utils/cn'\n\n/** Same heuristic as DeliveryTable's local helper. Inlined so the row\n * primitive owns its complete rendering contract. */\nfunction getRelativeTime(timestamp: number): string {\n const now = Date.now()\n const diff = now - timestamp\n const days = Math.floor(diff / (1000 * 60 * 60 * 24))\n const weeks = Math.floor(days / 7)\n const months = Math.floor(days / 30)\n if (months > 0) return months === 1 ? 'last month' : `${months} months ago`\n if (weeks > 0) return weeks === 1 ? 'last week' : `${weeks} weeks ago`\n if (days > 0) return days === 1 ? 'yesterday' : `${days} days ago`\n return 'today'\n}\n\nexport interface DeliveryRowProps {\n item: DeliveryItem\n /** When set, the row becomes a clickable anchor. The ticket-side\n * linked-card composes this from `buildDevSectionUrl('delivery', id)`\n * which carries `?search=<id>` — the delivery list filters to that\n * exact task on landing (canonical deep-link mechanism, same one\n * the chat-inline delivery card uses). */\n href?: string\n /** Small uppercase caption rendered above the title. Used by the\n * linked-delivery card variant (\"LINKED DELIVERY\"). */\n caption?: string\n /** DOM `id` applied to the row's outer element. `DeliveryTable`\n * always sets `delivery-<external_id>` so chat-card deep-links\n * (`?search=<id>#delivery-<id>`) and the ticket linked-card path\n * both have a target for `useScrollToHash` to scroll to. Always\n * paired with `scroll-mt-24` on the outer element so the row lands\n * BELOW the sticky chrome after the scroll. */\n id?: string\n className?: string\n}\n\nexport function DeliveryRow({\n item,\n href,\n caption,\n id,\n className,\n}: DeliveryRowProps) {\n const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS\n const typeBadgeLabel = TASK_TYPE_LABELS[taskType] || 'TASK'\n const typeBadgeTextColor = TASK_TYPE_TEXT_COLORS[taskType] || ''\n const statusBadgeScheme = getStatusColorScheme(item.status)\n const relativeTime = getRelativeTime(item.dateUpdated)\n const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(', ')}` : ''}, ${item.id}`\n\n const inner = (\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n {/* Left: caption (optional) + title + subtitle + description */}\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n {caption && (\n <p className=\"text-xs font-medium uppercase tracking-wider text-ods-text-secondary\">\n {caption}\n </p>\n )}\n <div className=\"min-h-[24px] md:min-h-[24px] flex items-center\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words\">\n {item.title}\n </h3>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <p className=\"text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate\">\n {subtitle}\n </p>\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <p className=\"text-h4 text-ods-text-secondary line-clamp-3 break-words\">\n {item.description || 'No description provided'}\n </p>\n </div>\n </div>\n\n {/* Right: status + task-type badges */}\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <StatusBadge\n text={item.status.toUpperCase()}\n colorScheme={statusBadgeScheme}\n variant=\"card\"\n className=\"border border-ods-border\"\n />\n <StatusBadge\n text={typeBadgeLabel}\n variant=\"card\"\n className={`border border-ods-border ${typeBadgeTextColor}`}\n />\n </div>\n </div>\n )\n\n const baseClass = cn(\n 'block p-[12px] md:p-[16px] no-underline text-inherit transition-colors duration-150',\n // `scroll-mt-24` is paid for whether `id` is set or not (it's a\n // single Tailwind utility, no runtime cost). Keeping it\n // unconditional means a future caller adding `id` doesn't also\n // have to remember to ask for the offset.\n 'scroll-mt-24',\n href && 'hover:bg-ods-bg-hover cursor-pointer',\n className,\n )\n\n if (href) {\n // `Link` is the env-aware embed-shim — delegates to `next/link` on\n // a Next.js host (soft RSC nav, back-button restores the previous\n // page's React state intact), falls back to a plain `<a>` on\n // non-Next embedders. A raw `<a href>` was hard-navigating +\n // losing TanStack-Query state on back, leaving /tickets stuck on\n // its skeleton.\n return (\n <Link href={href} id={id} className={baseClass} prefetch={false}>\n {inner}\n </Link>\n )\n }\n\n return <div id={id} className={baseClass}>{inner}</div>\n}\n"],"mappings":"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","names":["jsx","jsx","jsxs","jsx","jsx","jsxs","jsx","jsxs"]}
@@ -1,354 +0,0 @@
1
- "use client";
2
-
3
- // src/hooks/ui/use-auto-limit-tags.ts
4
- import { useCallback, useEffect, useRef, useState } from "react";
5
- function useAutoLimitTags({
6
- count,
7
- limitTags = "auto",
8
- placeholder = "",
9
- reserveInputWidth = true
10
- }) {
11
- const middleRef = useRef(null);
12
- const measureRef = useRef(null);
13
- const textMeasureRef = useRef(null);
14
- const badgeRef = useRef(null);
15
- const inputRef = useRef(null);
16
- const [visibleCount, setVisibleCount] = useState(count);
17
- const recalculate = useCallback(() => {
18
- if (limitTags !== "auto") {
19
- setVisibleCount(Math.min(limitTags, count));
20
- return;
21
- }
22
- const middle = middleRef.current;
23
- const measure = measureRef.current;
24
- if (!middle || !measure) {
25
- setVisibleCount(count);
26
- return;
27
- }
28
- if (count === 0) {
29
- setVisibleCount(0);
30
- return;
31
- }
32
- const cs = getComputedStyle(middle);
33
- const padL = parseFloat(cs.paddingLeft) || 0;
34
- const padR = parseFloat(cs.paddingRight) || 0;
35
- const gap = parseFloat(cs.gap) || 0;
36
- const middleW = middle.clientWidth;
37
- let inputReservedW = 0;
38
- let trailingGap = 0;
39
- if (reserveInputWidth) {
40
- const textW = textMeasureRef.current?.offsetWidth ?? 60;
41
- const inputMinW = inputRef.current ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60 : 60;
42
- inputReservedW = Math.max(textW + 8, inputMinW);
43
- trailingGap = gap;
44
- }
45
- const available = middleW - padL - padR - inputReservedW - trailingGap;
46
- const tagEls = Array.from(measure.children);
47
- const widths = tagEls.map((el) => el.offsetWidth);
48
- let total = 0;
49
- for (let i = 0; i < widths.length; i++) {
50
- total += widths[i] + (i > 0 ? gap : 0);
51
- }
52
- if (total <= available) {
53
- setVisibleCount(count);
54
- return;
55
- }
56
- const badgeW = badgeRef.current?.offsetWidth ?? 40;
57
- const spaceWithBadge = available - badgeW - gap;
58
- let used = 0;
59
- let fitCount = 0;
60
- for (let i = 0; i < widths.length; i++) {
61
- const need = widths[i] + (i > 0 ? gap : 0);
62
- if (used + need > spaceWithBadge) break;
63
- used += need;
64
- fitCount++;
65
- }
66
- setVisibleCount(Math.max(0, fitCount));
67
- }, [count, limitTags, placeholder, reserveInputWidth]);
68
- useEffect(() => {
69
- recalculate();
70
- }, [recalculate]);
71
- useEffect(() => {
72
- const el = middleRef.current;
73
- if (!el) return;
74
- const ro = new ResizeObserver(recalculate);
75
- ro.observe(el);
76
- return () => ro.disconnect();
77
- }, [recalculate]);
78
- return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef };
79
- }
80
-
81
- // src/hooks/ui/use-debounce.ts
82
- import { useState as useState2, useEffect as useEffect2 } from "react";
83
- function useDebounce(value, delay = 500) {
84
- const [debouncedValue, setDebouncedValue] = useState2(value);
85
- useEffect2(() => {
86
- const timer = setTimeout(() => {
87
- setDebouncedValue(value);
88
- }, delay);
89
- return () => {
90
- clearTimeout(timer);
91
- };
92
- }, [value, delay]);
93
- return debouncedValue;
94
- }
95
-
96
- // src/utils/local-storage-adapter.ts
97
- function getStorage(backend) {
98
- if (typeof window === "undefined") return null;
99
- try {
100
- return backend === "session" ? window.sessionStorage : window.localStorage;
101
- } catch {
102
- return null;
103
- }
104
- }
105
- function createLocalStorageAdapter(options) {
106
- const tag = options.logTag ?? "[local-storage]";
107
- const backend = options.backend ?? "local";
108
- const resolveKey = () => {
109
- const ns = options.namespace?.();
110
- return ns ? `${ns}.${options.key}` : options.key;
111
- };
112
- return {
113
- resolveKey,
114
- load() {
115
- const storage = getStorage(backend);
116
- if (!storage) return null;
117
- try {
118
- const raw = storage.getItem(resolveKey());
119
- if (!raw) return null;
120
- const parsed = JSON.parse(raw);
121
- if (options.validate && !options.validate(parsed)) return null;
122
- return parsed;
123
- } catch (err) {
124
- console.warn(`${tag} parse failed for key ${resolveKey()}:`, err);
125
- return null;
126
- }
127
- },
128
- save(value) {
129
- const storage = getStorage(backend);
130
- if (!storage) return;
131
- try {
132
- storage.setItem(resolveKey(), JSON.stringify(value));
133
- } catch (err) {
134
- console.warn(`${tag} write failed for key ${resolveKey()}:`, err);
135
- }
136
- },
137
- clear() {
138
- const storage = getStorage(backend);
139
- if (!storage) return;
140
- try {
141
- storage.removeItem(resolveKey());
142
- } catch (err) {
143
- console.warn(`${tag} clear failed for key ${resolveKey()}:`, err);
144
- }
145
- }
146
- };
147
- }
148
-
149
- // src/utils/app-config.ts
150
- function getAppType() {
151
- return process.env.NEXT_PUBLIC_APP_TYPE || "openmsp";
152
- }
153
-
154
- // src/utils/embed-proxy-auth-storage.ts
155
- function isValidPersistedAuth(value) {
156
- if (!value || typeof value !== "object") return false;
157
- const v = value;
158
- if (typeof v.secret !== "string" || v.secret.trim().length === 0 || typeof v.email !== "string" || v.email.trim().length === 0) return false;
159
- if (v.firstName != null && typeof v.firstName !== "string") return false;
160
- if (v.lastName != null && typeof v.lastName !== "string") return false;
161
- if (v.avatarUrl != null && typeof v.avatarUrl !== "string") return false;
162
- return true;
163
- }
164
- var adapter = createLocalStorageAdapter({
165
- // Storage key unchanged from the legacy chat-prefixed helper. Renaming
166
- // it would silently log every existing admin out — the key is a
167
- // storage contract, not a code identifier.
168
- key: "chat.proxy-auth.v1",
169
- namespace: () => getAppType(),
170
- validate: isValidPersistedAuth,
171
- logTag: "[embed-proxy-auth-storage]",
172
- // localStorage — survives tab close, new tabs, and browser restarts.
173
- // Admin re-pasting creds every tab cycle was the dev-experience
174
- // tradeoff prior `sessionStorage` setup demanded — rejected. See
175
- // file-level doc comment for the security tradeoff rationale.
176
- backend: "local"
177
- });
178
- function normalizeOptional(value) {
179
- if (!value) return void 0;
180
- const trimmed = value.trim();
181
- return trimmed.length > 0 ? trimmed : void 0;
182
- }
183
- function getEmbedProxyAuth() {
184
- const persisted = adapter.load();
185
- if (!persisted) return null;
186
- return {
187
- secret: persisted.secret,
188
- email: persisted.email.trim().toLowerCase(),
189
- firstName: normalizeOptional(persisted.firstName),
190
- lastName: normalizeOptional(persisted.lastName),
191
- avatarUrl: normalizeOptional(persisted.avatarUrl)
192
- };
193
- }
194
- function getPersistedProxyEmail() {
195
- const persisted = adapter.load();
196
- return persisted?.email.trim().toLowerCase() ?? null;
197
- }
198
- function setEmbedProxyAuth(value) {
199
- adapter.save({
200
- secret: value.secret,
201
- email: value.email.trim().toLowerCase(),
202
- firstName: normalizeOptional(value.firstName),
203
- lastName: normalizeOptional(value.lastName),
204
- avatarUrl: normalizeOptional(value.avatarUrl)
205
- });
206
- }
207
- function clearEmbedProxyAuth() {
208
- adapter.clear();
209
- }
210
- function applyProxyAuth(url, baseHeaders = { "Content-Type": "application/json" }) {
211
- const auth = getEmbedProxyAuth();
212
- const headers = { ...baseHeaders };
213
- if (auth?.secret) {
214
- headers.Authorization = `Bearer ${auth.secret}`;
215
- }
216
- if (auth?.email) {
217
- headers["X-Chat-Act-As"] = auth.email;
218
- }
219
- if (auth?.firstName) headers["X-Chat-First-Name"] = auth.firstName;
220
- if (auth?.lastName) headers["X-Chat-Last-Name"] = auth.lastName;
221
- if (auth?.avatarUrl) headers["X-Chat-Avatar-Url"] = auth.avatarUrl;
222
- return { url, headers };
223
- }
224
-
225
- // src/utils/embed-authed-fetch.ts
226
- var ADAPTER_GLOBAL_KEY = "__embedAuthedFetchAdapter__";
227
- function getRegisteredAuthAdapter() {
228
- if (typeof globalThis === "undefined") return null;
229
- return globalThis[ADAPTER_GLOBAL_KEY] ?? null;
230
- }
231
- function storeRegisteredAuthAdapter(adapter2) {
232
- if (typeof globalThis === "undefined") return;
233
- globalThis[ADAPTER_GLOBAL_KEY] = adapter2;
234
- }
235
- function setEmbedAuthAdapter(adapter2) {
236
- if (adapter2 && getRegisteredAuthAdapter() && process.env.NODE_ENV !== "production") {
237
- console.warn(
238
- "[setEmbedAuthAdapter] overwriting a previously-registered auth adapter. Two chat-runtime providers should not coexist \u2014 verify mount order and pass `null` from the unmounting provider."
239
- );
240
- }
241
- storeRegisteredAuthAdapter(adapter2);
242
- }
243
- function hasEmbedAuthAdapter() {
244
- return getRegisteredAuthAdapter() !== null;
245
- }
246
- function embedAuthedFetch(url, init = {}) {
247
- assertSameOrigin(url);
248
- let baseHeaders;
249
- if (init.headers === void 0) {
250
- baseHeaders = { "Content-Type": "application/json" };
251
- } else {
252
- baseHeaders = {};
253
- if (init.headers instanceof Headers) {
254
- init.headers.forEach((v, k) => {
255
- baseHeaders[k] = v;
256
- });
257
- } else if (Array.isArray(init.headers)) {
258
- for (const [k, v] of init.headers) baseHeaders[k] = v;
259
- } else {
260
- Object.assign(baseHeaders, init.headers);
261
- }
262
- }
263
- return fetchWithRefresh(url, init, baseHeaders, false);
264
- }
265
- var IN_FLIGHT_REFRESH_GLOBAL_KEY = "__embedAuthedFetchInFlightRefresh__";
266
- function getInFlightRefresh() {
267
- if (typeof globalThis === "undefined") return null;
268
- return globalThis[IN_FLIGHT_REFRESH_GLOBAL_KEY] ?? null;
269
- }
270
- function setInFlightRefresh(refresh) {
271
- if (typeof globalThis === "undefined") return;
272
- globalThis[IN_FLIGHT_REFRESH_GLOBAL_KEY] = refresh;
273
- }
274
- function dedupedRefresh() {
275
- const adapter2 = getRegisteredAuthAdapter();
276
- if (!adapter2?.refresh) return Promise.resolve(false);
277
- let inFlightRefresh = getInFlightRefresh();
278
- if (!inFlightRefresh) {
279
- inFlightRefresh = Promise.resolve().then(() => adapter2.refresh()).catch(() => false).finally(() => {
280
- setInFlightRefresh(null);
281
- });
282
- setInFlightRefresh(inFlightRefresh);
283
- }
284
- return inFlightRefresh;
285
- }
286
- async function fetchWithRefresh(url, init, baseHeaders, isRetry) {
287
- const { url: authedUrl, headers } = applyProxyAuth(url, { ...baseHeaders });
288
- const adapter2 = getRegisteredAuthAdapter();
289
- if (adapter2?.getHeaders) {
290
- for (const [k, v] of Object.entries(adapter2.getHeaders())) {
291
- if (v !== void 0) headers[k] = v;
292
- }
293
- }
294
- const credentials = adapter2?.credentials ?? init.credentials ?? "same-origin";
295
- const response = await fetch(authedUrl, {
296
- ...init,
297
- headers,
298
- // Default `same-origin` carries Supabase cookies for the MPH proxy-
299
- // auth model. Hosts on different origins (openframe-frontend ↔
300
- // openframe gateway) register `credentials: 'include'` via the
301
- // adapter to make their own cookies travel cross-origin (CORS +
302
- // `SameSite=None` must be configured server-side for that to work).
303
- credentials
304
- });
305
- if (response.status === 401 && !isRetry && adapter2?.refresh) {
306
- const refreshed = await dedupedRefresh();
307
- if (refreshed) {
308
- return fetchWithRefresh(url, init, baseHeaders, true);
309
- }
310
- }
311
- return response;
312
- }
313
- function assertSameOrigin(url) {
314
- if (typeof window === "undefined") return;
315
- let target;
316
- let pageOrigin;
317
- try {
318
- target = new URL(url, window.location.href);
319
- pageOrigin = new URL(window.location.href).origin;
320
- } catch {
321
- throw new Error(`embedAuthedFetch: refusing to fetch malformed URL (${JSON.stringify(url)})`);
322
- }
323
- if (target.protocol !== "http:" && target.protocol !== "https:") {
324
- throw new Error(
325
- `embedAuthedFetch: refusing non-http(s) URL (${target.protocol}) \u2014 pass a relative /api/* path instead`
326
- );
327
- }
328
- if (target.origin !== pageOrigin) {
329
- if (process.env.NODE_ENV !== "production") {
330
- console.warn(
331
- `[embedAuthedFetch] cross-origin fetch to ${target.origin} allowed in dev (NODE_ENV !== 'production'). Production builds will reject this \u2014 wire a same-origin proxy before shipping.`
332
- );
333
- return;
334
- }
335
- throw new Error(
336
- `embedAuthedFetch: refusing cross-origin fetch to ${target.origin} \u2014 pass a relative /api/* path instead`
337
- );
338
- }
339
- }
340
-
341
- export {
342
- useAutoLimitTags,
343
- useDebounce,
344
- getAppType,
345
- getEmbedProxyAuth,
346
- getPersistedProxyEmail,
347
- setEmbedProxyAuth,
348
- clearEmbedProxyAuth,
349
- applyProxyAuth,
350
- setEmbedAuthAdapter,
351
- hasEmbedAuthAdapter,
352
- embedAuthedFetch
353
- };
354
- //# sourceMappingURL=chunk-POKKCWKF.js.map