@flamingo-stack/openframe-frontend-core 0.0.692 → 0.0.693-snapshot.20261002120603

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 (205) hide show
  1. package/dist/chat-protocol/events.d.ts +3 -0
  2. package/dist/chat-protocol/events.d.ts.map +1 -1
  3. package/dist/chat-protocol/frames.d.ts +6 -0
  4. package/dist/chat-protocol/frames.d.ts.map +1 -1
  5. package/dist/chat-protocol/index.cjs +26 -1
  6. package/dist/chat-protocol/index.cjs.map +1 -1
  7. package/dist/chat-protocol/index.d.ts +1 -0
  8. package/dist/chat-protocol/index.d.ts.map +1 -1
  9. package/dist/chat-protocol/index.js +23 -2
  10. package/dist/chat-protocol/index.js.map +1 -1
  11. package/dist/chat-protocol/leading-frames.d.ts.map +1 -1
  12. package/dist/chat-protocol/progress.d.ts +35 -0
  13. package/dist/chat-protocol/progress.d.ts.map +1 -0
  14. package/dist/{chunk-GDZ7IC5B.js → chunk-234IIQIS.js} +2 -2
  15. package/dist/{chunk-KDCIYRZV.cjs → chunk-2EQED5SZ.cjs} +34 -34
  16. package/dist/{chunk-KDCIYRZV.cjs.map → chunk-2EQED5SZ.cjs.map} +1 -1
  17. package/dist/{chunk-JUNOY5HK.js → chunk-2HX6AADM.js} +15 -17
  18. package/dist/chunk-2HX6AADM.js.map +1 -0
  19. package/dist/{chunk-X5XFKI6N.js → chunk-33L2IR62.js} +3 -3
  20. package/dist/{chunk-A5NHAHV6.js → chunk-42LIHMRA.js} +4 -4
  21. package/dist/{chunk-MIZU2OTO.cjs → chunk-63BLVGXZ.cjs} +4 -4
  22. package/dist/{chunk-MIZU2OTO.cjs.map → chunk-63BLVGXZ.cjs.map} +1 -1
  23. package/dist/{chunk-TEIHEFXP.js → chunk-6ZSDKDOV.js} +8 -8
  24. package/dist/{chunk-UVUNBXS6.js → chunk-7IINE3MK.js} +48 -5
  25. package/dist/chunk-7IINE3MK.js.map +1 -0
  26. package/dist/{chunk-N3526CTH.cjs → chunk-7TO3CWPM.cjs} +3 -3
  27. package/dist/{chunk-N3526CTH.cjs.map → chunk-7TO3CWPM.cjs.map} +1 -1
  28. package/dist/{chunk-TXFGG47A.js → chunk-7VO6NC67.js} +4 -4
  29. package/dist/{chunk-SC44YJCF.js → chunk-A7RKT5SO.js} +2 -2
  30. package/dist/{chunk-Y7ZEVTLC.js → chunk-BW6YBATM.js} +141 -71
  31. package/dist/chunk-BW6YBATM.js.map +1 -0
  32. package/dist/{chunk-GZRU2A4L.cjs → chunk-C27F6GBZ.cjs} +790 -720
  33. package/dist/chunk-C27F6GBZ.cjs.map +1 -0
  34. package/dist/{chunk-DVSPRY2V.cjs → chunk-ESMDIJIN.cjs} +3 -3
  35. package/dist/{chunk-DVSPRY2V.cjs.map → chunk-ESMDIJIN.cjs.map} +1 -1
  36. package/dist/{chunk-EXWS2QSD.cjs → chunk-FNGX5WZM.cjs} +497 -474
  37. package/dist/chunk-FNGX5WZM.cjs.map +1 -0
  38. package/dist/{chunk-RK47E3PP.js → chunk-GWQCUC53.js} +4 -4
  39. package/dist/{chunk-AVAZ6BD6.js → chunk-HBMGSGZY.js} +6 -6
  40. package/dist/{chunk-HM7ZGABI.js → chunk-HEBLLLLD.js} +2 -2
  41. package/dist/{chunk-7BNWMOPM.cjs → chunk-HJZ3UTUE.cjs} +83 -83
  42. package/dist/{chunk-7BNWMOPM.cjs.map → chunk-HJZ3UTUE.cjs.map} +1 -1
  43. package/dist/{chunk-ATREVNIN.cjs → chunk-JB25CCOB.cjs} +47 -47
  44. package/dist/{chunk-ATREVNIN.cjs.map → chunk-JB25CCOB.cjs.map} +1 -1
  45. package/dist/{chunk-Q25VYAEL.cjs → chunk-JISKS7RJ.cjs} +26 -26
  46. package/dist/{chunk-Q25VYAEL.cjs.map → chunk-JISKS7RJ.cjs.map} +1 -1
  47. package/dist/{chunk-WTYQCT4A.cjs → chunk-K5GSLGV2.cjs} +18 -18
  48. package/dist/{chunk-WTYQCT4A.cjs.map → chunk-K5GSLGV2.cjs.map} +1 -1
  49. package/dist/{chunk-CG7OWW7X.cjs → chunk-K5K2FRQK.cjs} +19 -19
  50. package/dist/{chunk-CG7OWW7X.cjs.map → chunk-K5K2FRQK.cjs.map} +1 -1
  51. package/dist/{chunk-CKUPANVU.cjs → chunk-KBSL4FCL.cjs} +9 -9
  52. package/dist/{chunk-CKUPANVU.cjs.map → chunk-KBSL4FCL.cjs.map} +1 -1
  53. package/dist/{chunk-SDORFSFK.cjs → chunk-KDUDL4PB.cjs} +37 -37
  54. package/dist/{chunk-SDORFSFK.cjs.map → chunk-KDUDL4PB.cjs.map} +1 -1
  55. package/dist/{chunk-5XPK2QGU.cjs → chunk-KJHIFF6H.cjs} +16 -16
  56. package/dist/{chunk-5XPK2QGU.cjs.map → chunk-KJHIFF6H.cjs.map} +1 -1
  57. package/dist/{chunk-MCNQKS6I.js → chunk-LHYOESIQ.js} +4 -4
  58. package/dist/{chunk-DUURFYGS.cjs → chunk-LPFU6U7O.cjs} +5 -5
  59. package/dist/{chunk-DUURFYGS.cjs.map → chunk-LPFU6U7O.cjs.map} +1 -1
  60. package/dist/{chunk-ZAV2F4VX.js → chunk-N7HFUBOV.js} +3 -3
  61. package/dist/{chunk-53O6PQ3F.cjs → chunk-NDLFK57D.cjs} +7 -7
  62. package/dist/{chunk-53O6PQ3F.cjs.map → chunk-NDLFK57D.cjs.map} +1 -1
  63. package/dist/{chunk-Z7QCNCBN.js → chunk-O4RDXDVS.js} +12 -12
  64. package/dist/{chunk-F6LXGP6S.cjs → chunk-Q2B2MGFU.cjs} +7 -7
  65. package/dist/{chunk-F6LXGP6S.cjs.map → chunk-Q2B2MGFU.cjs.map} +1 -1
  66. package/dist/{chunk-CKO6NKNE.js → chunk-QP54LMXJ.js} +38 -15
  67. package/dist/{chunk-CKO6NKNE.js.map → chunk-QP54LMXJ.js.map} +1 -1
  68. package/dist/{chunk-QRA2JXE4.cjs → chunk-RE6PGOSS.cjs} +6 -8
  69. package/dist/chunk-RE6PGOSS.cjs.map +1 -0
  70. package/dist/{chunk-C2SVJVEB.js → chunk-SH5GPYSV.js} +2 -2
  71. package/dist/{chunk-KQZMM3QJ.cjs → chunk-SJFB4Z4L.cjs} +105 -105
  72. package/dist/{chunk-KQZMM3QJ.cjs.map → chunk-SJFB4Z4L.cjs.map} +1 -1
  73. package/dist/{chunk-7EPGJOKN.js → chunk-TA3JJFEO.js} +4 -4
  74. package/dist/{chunk-LYJJH4D6.js → chunk-TBR3UTFY.js} +6 -6
  75. package/dist/{chunk-USKQURNF.cjs → chunk-TG3VBMDG.cjs} +3 -3
  76. package/dist/{chunk-USKQURNF.cjs.map → chunk-TG3VBMDG.cjs.map} +1 -1
  77. package/dist/{chunk-IQTQIBZ3.js → chunk-TV5SZJ3N.js} +12 -12
  78. package/dist/{chunk-RLNNV46K.js → chunk-TXSQLORA.js} +6 -6
  79. package/dist/{chunk-LYQULSK4.js → chunk-UCXZRJIE.js} +7 -7
  80. package/dist/{chunk-I27PWVWE.cjs → chunk-WT4HURQN.cjs} +46 -3
  81. package/dist/chunk-WT4HURQN.cjs.map +1 -0
  82. package/dist/{chunk-XAOAYDGR.js → chunk-WZJWIZVH.js} +4 -4
  83. package/dist/{chunk-PKZGXRU2.js → chunk-XKNKGBUX.js} +10 -8
  84. package/dist/{chunk-PKZGXRU2.js.map → chunk-XKNKGBUX.js.map} +1 -1
  85. package/dist/{chunk-LNKLB7GM.cjs → chunk-XNTQHC26.cjs} +28 -28
  86. package/dist/{chunk-LNKLB7GM.cjs.map → chunk-XNTQHC26.cjs.map} +1 -1
  87. package/dist/{chunk-2YVTGPM7.js → chunk-XZILGCXJ.js} +2 -2
  88. package/dist/{chunk-DKJDO7YM.cjs → chunk-Y5AFMYYN.cjs} +46 -44
  89. package/dist/chunk-Y5AFMYYN.cjs.map +1 -0
  90. package/dist/{chunk-FTK3S57R.cjs → chunk-ZCPMWXIB.cjs} +25 -25
  91. package/dist/{chunk-FTK3S57R.cjs.map → chunk-ZCPMWXIB.cjs.map} +1 -1
  92. package/dist/{chunk-RTKF5AYA.cjs → chunk-ZUGAHQDB.cjs} +7 -7
  93. package/dist/{chunk-RTKF5AYA.cjs.map → chunk-ZUGAHQDB.cjs.map} +1 -1
  94. package/dist/components/case-studies/index.cjs +21 -21
  95. package/dist/components/case-studies/index.js +8 -8
  96. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  97. package/dist/components/chat/chat-typing-indicator.d.ts.map +1 -1
  98. package/dist/components/chat/cycling-phrase.d.ts.map +1 -1
  99. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  100. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  101. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  102. package/dist/components/chat/index.cjs +5 -5
  103. package/dist/components/chat/index.js +4 -4
  104. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -1
  105. package/dist/components/chat/stream/chat-stream-reducer.d.ts +4 -0
  106. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  107. package/dist/components/chat/types/component.types.d.ts +2 -0
  108. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  109. package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
  110. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  111. package/dist/components/contact/index.cjs +9 -9
  112. package/dist/components/contact/index.js +8 -8
  113. package/dist/components/docs/index.cjs +12 -12
  114. package/dist/components/docs/index.js +11 -11
  115. package/dist/components/embeds/index.cjs +9 -9
  116. package/dist/components/embeds/index.js +8 -8
  117. package/dist/components/faq/index.cjs +10 -10
  118. package/dist/components/faq/index.js +9 -9
  119. package/dist/components/features/index.cjs +8 -8
  120. package/dist/components/features/index.js +7 -7
  121. package/dist/components/help-center-pages/index.cjs +107 -107
  122. package/dist/components/help-center-pages/index.js +22 -22
  123. package/dist/components/index.cjs +69 -69
  124. package/dist/components/index.js +22 -22
  125. package/dist/components/meeting-scheduler/index.cjs +59 -59
  126. package/dist/components/meeting-scheduler/index.js +9 -9
  127. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  128. package/dist/components/navigation/index.cjs +11 -11
  129. package/dist/components/navigation/index.js +10 -10
  130. package/dist/components/onboarding-guides/index.cjs +12 -12
  131. package/dist/components/onboarding-guides/index.js +11 -11
  132. package/dist/components/related-content/index.cjs +6 -6
  133. package/dist/components/related-content/index.js +5 -5
  134. package/dist/components/tickets/index.cjs +13 -13
  135. package/dist/components/tickets/index.js +12 -12
  136. package/dist/components/ui/index.cjs +8 -8
  137. package/dist/components/ui/index.js +7 -7
  138. package/dist/components/ui/markdown/engine.d.ts +0 -21
  139. package/dist/components/ui/markdown/engine.d.ts.map +1 -1
  140. package/dist/hooks/index.cjs +6 -6
  141. package/dist/hooks/index.cjs.map +1 -1
  142. package/dist/hooks/index.js +5 -5
  143. package/dist/hooks/use-authed-asset-src.d.ts.map +1 -1
  144. package/dist/index.cjs +15 -15
  145. package/dist/index.cjs.map +1 -1
  146. package/dist/index.js +12 -12
  147. package/dist/tailwind.config.cjs +14 -1
  148. package/dist/tailwind.config.cjs.map +1 -1
  149. package/dist/tailwind.config.js +14 -1
  150. package/dist/tailwind.config.js.map +1 -1
  151. package/eslint-config/README.md +13 -0
  152. package/eslint-config/base.js +15 -0
  153. package/eslint-config/webview-performance.js +75 -0
  154. package/package.json +1 -1
  155. package/src/chat-protocol/__tests__/progress.test.ts +39 -0
  156. package/src/chat-protocol/events.ts +3 -0
  157. package/src/chat-protocol/frames.ts +7 -0
  158. package/src/chat-protocol/index.ts +2 -0
  159. package/src/chat-protocol/leading-frames.ts +4 -1
  160. package/src/chat-protocol/progress.ts +55 -0
  161. package/src/components/chat/chat-message-enhanced.tsx +1 -1
  162. package/src/components/chat/chat-message-list.tsx +65 -15
  163. package/src/components/chat/chat-typing-indicator.tsx +18 -29
  164. package/src/components/chat/cycling-phrase.tsx +1 -11
  165. package/src/components/chat/embeddable-chat.tsx +14 -6
  166. package/src/components/chat/hooks/use-sse-chat-adapter.ts +2 -0
  167. package/src/components/chat/hooks/use-unified-chat.ts +1 -0
  168. package/src/components/chat/stream/__tests__/chat-stream-reducer-progress.test.ts +40 -0
  169. package/src/components/chat/stream/chat-dialog-store.ts +1 -0
  170. package/src/components/chat/stream/chat-stream-reducer.ts +13 -0
  171. package/src/components/chat/types/component.types.ts +2 -0
  172. package/src/components/chat/types/unified-chat-state.types.ts +6 -0
  173. package/src/components/navigation/app-layout-drawer.tsx +53 -7
  174. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +29 -29
  175. package/src/components/ui/markdown/engine.tsx +56 -12
  176. package/src/hooks/use-authed-asset-src.ts +58 -6
  177. package/tailwind.config.ts +13 -0
  178. package/dist/chunk-DKJDO7YM.cjs.map +0 -1
  179. package/dist/chunk-EXWS2QSD.cjs.map +0 -1
  180. package/dist/chunk-GZRU2A4L.cjs.map +0 -1
  181. package/dist/chunk-I27PWVWE.cjs.map +0 -1
  182. package/dist/chunk-JUNOY5HK.js.map +0 -1
  183. package/dist/chunk-QRA2JXE4.cjs.map +0 -1
  184. package/dist/chunk-UVUNBXS6.js.map +0 -1
  185. package/dist/chunk-Y7ZEVTLC.js.map +0 -1
  186. /package/dist/{chunk-GDZ7IC5B.js.map → chunk-234IIQIS.js.map} +0 -0
  187. /package/dist/{chunk-X5XFKI6N.js.map → chunk-33L2IR62.js.map} +0 -0
  188. /package/dist/{chunk-A5NHAHV6.js.map → chunk-42LIHMRA.js.map} +0 -0
  189. /package/dist/{chunk-TEIHEFXP.js.map → chunk-6ZSDKDOV.js.map} +0 -0
  190. /package/dist/{chunk-TXFGG47A.js.map → chunk-7VO6NC67.js.map} +0 -0
  191. /package/dist/{chunk-SC44YJCF.js.map → chunk-A7RKT5SO.js.map} +0 -0
  192. /package/dist/{chunk-RK47E3PP.js.map → chunk-GWQCUC53.js.map} +0 -0
  193. /package/dist/{chunk-AVAZ6BD6.js.map → chunk-HBMGSGZY.js.map} +0 -0
  194. /package/dist/{chunk-HM7ZGABI.js.map → chunk-HEBLLLLD.js.map} +0 -0
  195. /package/dist/{chunk-MCNQKS6I.js.map → chunk-LHYOESIQ.js.map} +0 -0
  196. /package/dist/{chunk-ZAV2F4VX.js.map → chunk-N7HFUBOV.js.map} +0 -0
  197. /package/dist/{chunk-Z7QCNCBN.js.map → chunk-O4RDXDVS.js.map} +0 -0
  198. /package/dist/{chunk-C2SVJVEB.js.map → chunk-SH5GPYSV.js.map} +0 -0
  199. /package/dist/{chunk-7EPGJOKN.js.map → chunk-TA3JJFEO.js.map} +0 -0
  200. /package/dist/{chunk-LYJJH4D6.js.map → chunk-TBR3UTFY.js.map} +0 -0
  201. /package/dist/{chunk-IQTQIBZ3.js.map → chunk-TV5SZJ3N.js.map} +0 -0
  202. /package/dist/{chunk-RLNNV46K.js.map → chunk-TXSQLORA.js.map} +0 -0
  203. /package/dist/{chunk-LYQULSK4.js.map → chunk-UCXZRJIE.js.map} +0 -0
  204. /package/dist/{chunk-XAOAYDGR.js.map → chunk-WZJWIZVH.js.map} +0 -0
  205. /package/dist/{chunk-2YVTGPM7.js.map → chunk-XZILGCXJ.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-QRA2JXE4.cjs","../src/hooks/ui/use-is-truncated.ts","../src/hooks/ui/use-debounce.ts","../src/hooks/ui/use-isomorphic-layout-effect.ts","../src/hooks/ui/use-media-query.ts","../src/hooks/ui/use-header-height.ts","../src/hooks/ui/use-marquee-engine.ts","../src/hooks/ui/use-reset-on-page-hidden.ts","../src/hooks/ui/use-suppress-clone-focus.ts","../src/types/tool.types.ts","../src/components/tool-icon.tsx","../src/utils/common.ts","../src/hooks/use-near-viewport.ts","../src/utils/markdown-fences.ts","../src/utils/markdown-heading-id.ts","../src/utils/anchor-id.ts","../src/utils/scroll-into-view.ts","../src/utils/same-page-hash-nav.ts"],"names":["delay","useState","useEffect","header","bar","useCallback","useRef"],"mappings":"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;AAEA,EAAA;AACM,IAAA;AACN,EAAA;AAIO,EAAA;AACT;AfqeU;AACA;AgB1gBN;AACA;AAEK;AACH,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEM;AAWU;AACd,EAAA;AACQ,IAAA;AAEH,IAAA;AAGD,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAOgB;AAIV,EAAA;AACI,EAAA;AAKF,EAAA;AACA,EAAA;AACA,EAAA;AACA,IAAA;AACC,IAAA;AACP,EAAA;AAKM,EAAA;AACE,IAAA;AAKA,IAAA;AACA,IAAA;AAGN,IAAA;AACF,EAAA;AAGA,EAAA;AAEM,EAAA;AAIF,EAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AAGI,EAAA;AACA,EAAA;AAKE,EAAA;AACC,EAAA;AACA,EAAA;AACP,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEM,EAAA;AACA,IAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AACM,IAAA;AACA,IAAA;AACA,IAAA;AACN,IAAA;AACI,IAAA;AACF,MAAA;AACF,IAAA;AAEE,MAAA;AACA,MAAA;AACI,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AhBwdU;AACA;AiBppBG;AAIA;AAUG;AACT,EAAA;AACC,EAAA;AACC,EAAA;AACT;AAqBgB;AACL,EAAA;AACH,EAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACF,IAAA;AACI,IAAA;AACF,MAAA;AACI,MAAA;AACN,IAAA;AACM,EAAA;AAEN,IAAA;AACF,EAAA;AAEO,EAAA;AACT;AAsBgB;AACV,EAAA;AACI,EAAA;AACF,EAAA;AAKF,EAAA;AACA,EAAA;AACI,IAAA;AACA,EAAA;AACN,IAAA;AACF,EAAA;AAEM,EAAA;AAIJ,IAAA;AACF,EAAA;AACM,EAAA;AAGA,EAAA;AACF,EAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACM,EAAA;AACA,EAAA;AAED,EAAA;AACD,EAAA;AACI,IAAA;AACF,IAAA;AACF,MAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AAGA,IAAA;AACM,MAAA;AACF,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACF,IAAA;AACF,EAAA;AAGA,EAAA;AACE,IAAA;AACA,IAAA;AACD,EAAA;AACM,EAAA;AACT;AjB+kBU;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-QRA2JXE4.cjs","sourcesContent":[null,"'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\n\nexport interface UseIsTruncatedOptions {\n /** Measure vertical clipping (`line-clamp-N`) instead of the single-line ellipsis. */\n multiline?: boolean;\n}\n\n/**\n * Tracks whether an element's text is ACTUALLY clipped — the condition for\n * offering a \"full value\" tooltip. Text that fits needs no tooltip, and showing\n * one anyway just repeats what the user is already reading.\n *\n * Three things can change the answer, and all three are watched: the box\n * resizing (`ResizeObserver`), the text itself changing, and the measured\n * ELEMENT changing. None of them implies the others — new text inside a\n * width-constrained box leaves the box exactly as it was, so the observer never\n * fires; and a caller that swaps between render branches (adornment vs plain)\n * mounts a different node under the same text, which a `useRef` would leave\n * measured against the old, detached node. Pass the rendered value as `value`.\n *\n * `ref` is a CALLBACK ref, not a `RefObject` — that is what makes the element\n * itself a dependency. Attach it exactly like a normal ref; there is no\n * `.current` to read.\n *\n * ```tsx\n * const { ref, isTruncated } = useIsTruncated<HTMLHeadingElement>(title)\n * <FloatingTooltip content={title} disabled={!isTruncated}>\n * <h1 ref={ref} className=\"truncate\">{title}</h1>\n * </FloatingTooltip>\n * ```\n */\nexport function useIsTruncated<T extends HTMLElement>(value: unknown, options?: UseIsTruncatedOptions) {\n const multiline = options?.multiline ?? false;\n const [element, setElement] = useState<T | null>(null);\n const [isTruncated, setIsTruncated] = useState(false);\n\n // Wrapped so the identity stays stable across renders: passing the raw\n // `useState` setter as a ref would be stable too, but this keeps the public\n // shape a plain `RefCallback<T>` instead of a state dispatcher.\n const ref = useCallback((node: T | null) => setElement(node), []);\n\n useEffect(() => {\n if (!element) return undefined;\n // The 1px tolerance absorbs sub-pixel layout rounding, which otherwise\n // reports a permanent fractional \"overflow\" at some zoom levels / DPRs and\n // leaves a tooltip armed on text that is fully visible.\n const check = () => {\n setIsTruncated(\n multiline ? element.scrollHeight > element.clientHeight + 1 : element.scrollWidth > element.clientWidth + 1,\n );\n };\n check();\n const observer = new ResizeObserver(check);\n observer.observe(element);\n return () => observer.disconnect();\n }, [element, value, multiline]);\n\n // Masked rather than reset from the effect: with no element there is nothing\n // to clip, and deriving that during render avoids a second render pass every\n // time the measured node unmounts.\n return { ref, isTruncated: element ? isTruncated : false };\n}\n","'use client';\n\nimport { useState, useEffect } from 'react';\n\n/**\n * Hook to debounce a value\n * @param value - Value to debounce\n * @param delay - Delay in milliseconds\n * @returns Debounced value\n */\nexport function useDebounce<T>(value: T, delay = 500): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n useEffect(() => {\n // Set up timeout to update debounced value\n const timer = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n // Clean up timeout on value change or unmount\n return () => {\n clearTimeout(timer);\n };\n }, [value, delay]);\n\n return debouncedValue;\n}\n","'use client';\n\nimport { useEffect, useLayoutEffect } from 'react';\n\n/**\n * `useLayoutEffect` in the browser, `useEffect` on the server.\n *\n * `useLayoutEffect` never runs during server rendering — React cannot measure or\n * mutate a layout that has not been laid out — so calling it in a component that\n * SSRs logs a warning on every render pass. Client components still render on the\n * server in Next.js, so `'use client'` alone does not avoid it.\n *\n * Use this wherever the effect writes something the FIRST paint depends on (a CSS\n * custom property, a scroll position, a measured size). A plain `useEffect` there\n * runs after paint, which shows one frame of the pre-effect value; a plain\n * `useLayoutEffect` gets the timing right and pays for it with the warning. This\n * gets both: paint-blocking where there is a paint, a no-op where there is not.\n */\nexport const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n","'use client';\n\nimport { useState } from 'react';\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\n\n/**\n * Hook to check if a media query matches\n * @param query - Media query to check\n * @returns Whether the media query matches, or undefined during SSR/initial render\n */\nexport function useMediaQuery(query: string): boolean | undefined {\n const [matches, setMatches] = useState<boolean | undefined>(undefined);\n\n useIsomorphicLayoutEffect(() => {\n const matchMedia = window.matchMedia(query);\n\n const handleChange = () => {\n setMatches(matchMedia.matches);\n };\n\n // Set initial value\n handleChange();\n\n matchMedia.addEventListener('change', handleChange);\n\n return () => {\n matchMedia.removeEventListener('change', handleChange);\n };\n }, [query]);\n\n return matches;\n}\n\n/**\n * Predefined breakpoints for common screen sizes\n */\nexport const breakpoints = {\n md: '(min-width: 800px)', // Tablet: 50rem\n lg: '(min-width: 1280px)', // Desktop: 80rem\n};\n\n/** @deprecated Use useMdUp instead */\nexport function useSmUp(): boolean | undefined {\n return useMdUp();\n}\n\nexport function useMdUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.md);\n return matches === undefined ? undefined : matches;\n}\n\nexport function useLgUp(): boolean | undefined {\n const matches = useMediaQuery(breakpoints.lg);\n return matches === undefined ? undefined : matches;\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\n/**\n * Returns the combined height (px) of the sticky page header and announcement\n * bar (if present), updated live via ResizeObserver / MutationObserver.\n * Useful for offsetting fixed/absolute-positioned panels so they don't\n * overlap the header.\n */\nexport function useHeaderHeight(\n // First-paint fallback = the unified top-navigation `big` bar (72px), the\n // size every hub/marketing site renders; replaced by the live measurement.\n defaultHeight = 72,\n options: {\n /**\n * When false, no observers are attached and the default height is\n * returned — for consumers that stay mounted on every page but only need\n * the measurement while an overlay is open (avoids an always-on\n * document-wide MutationObserver + forced reflow).\n * @default true\n */\n enabled?: boolean;\n } = {},\n): number {\n const { enabled = true } = options;\n const [height, setHeight] = useState(defaultHeight);\n\n useEffect(() => {\n if (!enabled) return undefined;\n\n const measure = () => {\n let total = 0;\n const header = document.querySelector('header');\n if (header) total += header.offsetHeight;\n const bar = document.querySelector('[data-announcement-bar]');\n if (bar instanceof HTMLElement) total += bar.offsetHeight;\n setHeight(total > 0 ? total : defaultHeight);\n };\n\n measure();\n\n const resizeObserver = new ResizeObserver(measure);\n const header = document.querySelector('header');\n if (header) resizeObserver.observe(header);\n const bar = document.querySelector('[data-announcement-bar]');\n if (bar) resizeObserver.observe(bar);\n\n const mutationObserver = new MutationObserver(mutations => {\n for (const mutation of mutations) {\n if (mutation.type === 'childList' || mutation.type === 'attributes') {\n measure();\n const newBar = document.querySelector('[data-announcement-bar]');\n if (newBar) resizeObserver.observe(newBar);\n }\n }\n });\n mutationObserver.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['data-announcement-bar'],\n });\n\n return () => {\n resizeObserver.disconnect();\n mutationObserver.disconnect();\n };\n }, [defaultHeight, enabled]);\n\n return height;\n}\n","'use client';\n\nimport { useCallback, useEffect, useRef, type MutableRefObject } from 'react';\n\n/**\n * THE marquee animation core (single source of truth) — extracted verbatim\n * from <CardsStrip> so every endless-scroll surface (card strips, logo strips,\n * quick-action walls, chip marquees) shares ONE rAF position engine (\"unify\n * means delete variations\").\n *\n * The engine is axis- and driver-agnostic: it owns a FLOAT position ref, a\n * velocity envelope, seam wrapping, and an optional chevron-style glide; the\n * caller decides what a position \"means\" via `apply` — writing\n * `scroller.scrollLeft` (CardsStrip: a real scroller the user can also drag)\n * or a `translate` transform (MarqueeWall: decorative tracks, incl. vertical —\n * mandatory inside surfaces that forbid inner scrollers, e.g. the deck's\n * mobile single-fold panels).\n *\n * Engine semantics (do not \"simplify\"):\n * - Position lives in a FLOAT ref, never read-modify-write from the DOM:\n * scrollLeft reads back rounded values, so sub-pixel increments\n * (speed/fps < 1px) get eaten by rounding — stutter + measurably slower\n * than the configured speed.\n * - Velocity envelope (~250ms time constant) eases between 0 and `speed`\n * instead of binary stop/start — pause decelerates, resume accelerates from\n * the current position (GSAP-marquee behavior; hard cuts read as jank).\n * - Glide consumes a signed remaining-distance, wrapping modulo the copy size\n * each frame, so seam warps are invisible to it (browser smooth-scroll\n * animates toward an ABSOLUTE target and lunges after a warp).\n * - `isPaused` is evaluated every frame — the caller owns the pause-reason\n * set (hover, viewport, tab visibility, suppress windows); the engine only\n * turns it into a smooth velocity target.\n */\nexport interface MarqueeEngineOptions {\n /** Master switch — the rAF runs only while true. On (re)activation the\n * position resyncs from `readBack` when provided. */\n active: boolean;\n /** Cruise speed in px/s. */\n speed: number;\n /** Per-frame pause verdict (caller-owned reason set). `now` is the rAF\n * timestamp, for suppress-until comparisons. */\n isPaused: (now: number) => boolean;\n /** Single-copy size in px (the wrap modulus). Return <= 0 to disable\n * wrapping (position advances unwrapped — callers should size tracks so\n * this never happens while active). */\n getWrapSize: () => number;\n /** Lower bound of the wrap range: positions stay in\n * [wrapMin, wrapMin + wrapSize). Default 0. Scroller drivers with USER\n * scrolling pass their seam-warp buffer here so engine-written positions\n * never enter the re-center zones — an exact-edge warp check is racy\n * against the rAF (the engine nudges the scroller off the edge before the\n * scroll handler samples it → \"stuck at the left edge\"). */\n getWrapMin?: () => number;\n /** Write the position to the DOM (scrollLeft assignment or transform). */\n apply: (pos: number) => void;\n /** Read the externally-mutated position back (real scrollers: user drag /\n * native smooth scroll move scrollLeft under the engine). Omit for\n * transform drivers — nothing external can move them. */\n readBack?: () => number;\n /** Called at the end of every frame (e.g. hover re-resolution after the\n * track moved under a stationary pointer). */\n onAfterFrame?: () => void;\n}\n\nexport interface MarqueeEngineHandle {\n /** The engine's float position (px along the axis, 0 ≤ pos < wrap while\n * wrapping). Callers may write it together with their own DOM write (seam\n * warp on manual scroll). */\n posRef: MutableRefObject<number>;\n /** Accumulate a signed glide distance (chevron navigation). Runs through\n * the engine's own tween — never native smooth scroll while wrapping. */\n glideBy: (px: number) => void;\n /** Wrap an arbitrary position into [0, wrapSize). */\n wrap: (pos: number) => number;\n}\n\n// Resync threshold (px): external movement beyond this adopts the DOM value.\nconst RESYNC_EPSILON = 1.5;\n\nexport function useMarqueeEngine(options: MarqueeEngineOptions): MarqueeEngineHandle {\n const posRef = useRef(0);\n const glideRemainingRef = useRef(0);\n const speedEnvRef = useRef(0);\n\n // Latest-options ref: the rAF loop always reads current values without the\n // effect restarting on every render. Refreshed in an unconditional effect,\n // declared before every effect that starts the loop, rather than in the\n // render body — a render attempt React discards must not be able to hand the\n // loop options that were never committed.\n const optsRef = useRef(options);\n useEffect(() => {\n optsRef.current = options;\n });\n\n const wrap = useCallback((pos: number) => {\n const half = optsRef.current.getWrapSize();\n if (half <= 0) return pos;\n const min = optsRef.current.getWrapMin?.() ?? 0;\n let wrapped = pos;\n while (wrapped >= min + half) wrapped -= half;\n while (wrapped < min) wrapped += half;\n return wrapped;\n }, []);\n\n const glideBy = useCallback((px: number) => {\n glideRemainingRef.current += px;\n }, []);\n\n const active = options.active;\n useEffect(() => {\n if (!active) return undefined;\n let raf = 0;\n let last = performance.now();\n const readBack = optsRef.current.readBack;\n if (readBack) posRef.current = readBack();\n\n const tick = (now: number) => {\n const opts = optsRef.current;\n const dt = Math.min(now - last, 100) / 1000; // clamp tab-wake jumps\n last = now;\n\n const paused = opts.isPaused(now);\n // Ease the velocity toward its target (0 when paused) — smooth decel on\n // pause, smooth accel on resume, always from the CURRENT position.\n const targetSpeed = paused ? 0 : opts.speed;\n speedEnvRef.current += (targetSpeed - speedEnvRef.current) * Math.min(1, dt / 0.25);\n if (targetSpeed === 0 && speedEnvRef.current < 0.5) speedEnvRef.current = 0;\n\n const external = opts.readBack ? opts.readBack() : null;\n const resync = () => {\n if (external !== null && Math.abs(external - posRef.current) > RESYNC_EPSILON) {\n posRef.current = external;\n }\n };\n\n const glide = glideRemainingRef.current;\n if (glide !== 0) {\n // Ease-out toward the glide target; runs even while \"paused\" (the\n // interaction that queued it sets its own suppress window).\n resync();\n const speed = Math.max(Math.abs(glide) * 6, 240); // px/s, proportional\n const step = Math.sign(glide) * Math.min(Math.abs(glide), speed * dt);\n glideRemainingRef.current = Math.abs(glide - step) < 0.5 ? 0 : glide - step;\n posRef.current = wrap(posRef.current + step);\n opts.apply(posRef.current);\n } else if (speedEnvRef.current > 0) {\n // Resync after external movement (user scroll, seam warp).\n resync();\n posRef.current = wrap(posRef.current + speedEnvRef.current * dt);\n opts.apply(posRef.current);\n } else if (external !== null) {\n // Idle: follow external movement so resumes start from where the\n // user left the scroller.\n posRef.current = external;\n }\n opts.onAfterFrame?.();\n raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [active, wrap]);\n\n return { posRef, glideBy, wrap };\n}\n","import { useEffect } from 'react';\n\n/**\n * Runs `reset` whenever the tab is hidden or the window loses focus — exactly the\n * moments a `pointerleave` / `pointercancel` can be SWALLOWED (tab blur, an\n * overlay mounting under the cursor, an interrupted pointer), which otherwise\n * leaves a hover/\"inside\" flag stuck `true` and freezes a marquee FOREVER. The\n * next real pointer/hover re-arms the flag.\n *\n * SSOT self-heal for every marquee surface (MarqueeWall, CardsStrip) so the\n * blur + `visibilitychange` wiring lives in one place. `reset` almost always\n * only UN-sticks state. The one edge: on `blur` WITHOUT the tab hiding (a second\n * window focused beside this one), clearing the hover-pause can let the marquee\n * resume under a still-stationary cursor in the now-unfocused window — it\n * re-pauses on the next `pointermove`/`pointerenter`. We keep the `blur` reset\n * anyway because it's the only signal that catches a tab-blur that swallows the\n * `pointerleave` without a `visibilitychange`. Pass a STABLE `reset` (memoize\n * with `useCallback`) — the listeners re-attach whenever its identity changes.\n */\nexport function useResetOnPageHidden(reset: () => void): void {\n useEffect(() => {\n const onVisibility = () => {\n if (document.visibilityState === 'hidden') reset();\n };\n window.addEventListener('blur', reset);\n document.addEventListener('visibilitychange', onVisibility);\n return () => {\n window.removeEventListener('blur', reset);\n document.removeEventListener('visibilitychange', onVisibility);\n };\n }, [reset]);\n}\n","'use client';\n\nimport { useEffect, useRef } from 'react';\n\n// Every focusable a clone might contain — matched so it can be pulled out of the\n// keyboard tab order (see useSuppressCloneFocus). `[contenteditable]:not(\n// [contenteditable=\"false\"])` catches the empty/`\"true\"`/`\"plaintext-only\"`\n// editable forms; `summary` is focusable inside a <details>.\nconst CLONE_FOCUSABLE_SELECTOR =\n 'a[href],button,input,select,textarea,iframe,summary,audio[controls],video[controls],[tabindex],[contenteditable]:not([contenteditable=\"false\"])';\n\n/**\n * Keep a marquee clone's content pointer-CLICKABLE while taking it out of the\n * accessibility tree and the keyboard tab order. Shared by every endless-loop\n * surface that renders a second track copy (<CardsStrip> managed cells,\n * <MarqueeWall> clone copies).\n *\n * `inert` (the previous approach) does all three at once: a11y-hidden,\n * un-focusable AND un-clickable. That third effect was the \"news marquee stops\n * being clickable after you scroll it for a while\" bug — the 2-copy endless\n * loop ALWAYS paints clone cards inside the viewport near the copy seam, so a\n * user who manually scrolls and parks there is looking at real-looking cards\n * that silently swallow every click. Clones must therefore stay clickable (a\n * click on ANY visible card must open its link — mouse, ⌘-click, middle-click\n * and context-menu all handled natively by the clone's own anchor).\n *\n * The a11y contract is still met without `inert`: the caller keeps\n * `aria-hidden` on the clone wrapper (out of the a11y tree) and this hook\n * forces every focusable descendant to `tabindex=\"-1\"` (out of the tab order →\n * no duplicate tab stops, no aria-hidden-focus violation). Re-applied on\n * subtree mutation. This is the standard loop-clone treatment used by\n * Swiper/Splide.\n */\nexport function useSuppressCloneFocus(active: boolean) {\n const ref = useRef<HTMLDivElement | null>(null);\n useEffect(() => {\n const root = ref.current;\n if (!root || !active) return undefined;\n const suppress = () => {\n root.querySelectorAll<HTMLElement>(CLONE_FOCUSABLE_SELECTOR).forEach(el => {\n // Only stamp the ones not already stamped — avoids touching the DOM (and\n // clobbering an intentional tabindex) on every mutation.\n if (el.getAttribute('tabindex') !== '-1') el.setAttribute('tabindex', '-1');\n });\n };\n suppress();\n // Watch structure AND the attributes that can turn a descendant tabbable\n // (an anchor gaining `href`, React restoring `tabindex=\"0\"`, an element\n // becoming editable, media gaining `controls`). The `!== '-1'` guard above\n // makes our own tabindex writes a no-op, so the observer never loops.\n const mo = new MutationObserver(suppress);\n mo.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['href', 'tabindex', 'contenteditable', 'controls'],\n });\n return () => mo.disconnect();\n }, [active]);\n return ref;\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 FLEET_MDM: 'FLEET_MDM',\n MESHCENTRAL: 'MESHCENTRAL',\n AUTHENTIK: 'AUTHENTIK',\n OPENFRAME: 'OPENFRAME',\n OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n OPENFRAME_RMM: 'OPENFRAME_RMM',\n OSQUERY: 'OSQUERY',\n SYSTEM: 'SYSTEM',\n // Directory tenants (Tenant Management): the same identifiers the gateway's\n // DirectoryProvider enum uses, so a directory sync's log rows resolve here.\n MICROSOFT_365: 'MICROSOFT_365',\n GOOGLE_WORKSPACE: 'GOOGLE_WORKSPACE',\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 FLEET_MDM: 'Fleet',\n MESHCENTRAL: 'MeshCentral',\n AUTHENTIK: 'Authentik',\n OPENFRAME: 'OpenFrame',\n OPENFRAME_CHAT: 'OpenFrame Chat',\n OPENFRAME_CLIENT: 'OpenFrame Client',\n OPENFRAME_RMM: 'RMM',\n OSQUERY: 'Osquery',\n SYSTEM: 'System',\n MICROSOFT_365: 'Microsoft 365',\n GOOGLE_WORKSPACE: 'Google Workspace',\n};\n","import type { FC, ReactNode } from 'react';\nimport { type ToolType, ToolTypeValues } from '../types/tool.types';\nimport {\n OsqueryLogoGreyIcon,\n MeshcentralLogoGreyIcon,\n FleetMdmLogoGreyIcon,\n AuthentikLogoGreyIcon,\n Office365LogoGreyIcon,\n OpenframeLogoGreyIcon,\n GoogleLogoGreyIcon,\n} from './icons-v2-generated';\n\ntype ToolIconRenderer = (size: number, className?: string) => ReactNode;\n\n// Every mark is the grey (`currentColor`) cut of the brand logo, the OpenFrame one included, so a\n// row of tool icons reads as one set and takes the text colour of wherever it sits.\nconst renderOpenFrameLogo: ToolIconRenderer = (size, className) => (\n <OpenframeLogoGreyIcon size={size} className={className} />\n);\n\nconst toolIconMap: Record<ToolType, ToolIconRenderer> = {\n [ToolTypeValues.FLEET_MDM]: (size, className) => <FleetMdmLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.MESHCENTRAL]: (size, className) => <MeshcentralLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.OPENFRAME]: renderOpenFrameLogo,\n [ToolTypeValues.OPENFRAME_CHAT]: renderOpenFrameLogo,\n [ToolTypeValues.OPENFRAME_CLIENT]: renderOpenFrameLogo,\n [ToolTypeValues.OPENFRAME_RMM]: renderOpenFrameLogo,\n [ToolTypeValues.AUTHENTIK]: (size, className) => <AuthentikLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.OSQUERY]: (size, className) => <OsqueryLogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.SYSTEM]: () => null,\n [ToolTypeValues.MICROSOFT_365]: (size, className) => <Office365LogoGreyIcon size={size} className={className} />,\n [ToolTypeValues.GOOGLE_WORKSPACE]: (size, className) => <GoogleLogoGreyIcon size={size} className={className} />,\n};\n\nexport interface ToolIconProps {\n toolType: ToolType;\n size?: number;\n className?: string;\n}\n\nexport const ToolIcon: FC<ToolIconProps> = ({ toolType, size = 16, className }) => (\n <>{toolIconMap[toolType]?.(size, className) ?? null}</>\n);\n\nToolIcon.displayName = 'ToolIcon';\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Utility functions for common operations\n */\n\n/**\n * Merge class names with Tailwind CSS\n * @param inputs - Class names to merge\n * @returns Merged class names\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * Human-readable message for a caught value.\n *\n * `catch (e)` binds `unknown` under this tsconfig, and a thrown value is not\n * guaranteed to be an `Error`. Use this instead of `catch (e: any)` +\n * `e.message`, which crashes on a thrown string/object with no `message`.\n *\n * @param error - The caught value\n * @param fallback - Returned when the value carries no usable message\n */\nexport function errorMessage(error: unknown, fallback = 'Something went wrong'): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n if (typeof error === 'object' && error !== null) {\n const message = (error as { message?: unknown }).message;\n if (typeof message === 'string' && message) return message;\n }\n return fallback;\n}\n\n/**\n * Human-readable message for a FAILED API response body.\n *\n * `Response.json()` is typed `any`, so `body.error` used to be read off an\n * `any` at every call site. The platform's route helpers answer errors with\n * `{ error, code, details }` (see `errorResponse` in the hub's\n * `lib/api/route-base.ts`); a few endpoints answer `{ message }` instead, so\n * both are accepted before giving up.\n *\n * @param payload - The parsed response body (or whatever came back)\n * @param fallback - Returned when the body carries no usable message\n */\nexport function apiErrorMessage(payload: unknown, fallback: string): string {\n if (typeof payload === 'object' && payload !== null && 'error' in payload) {\n const { error } = payload;\n if (typeof error === 'string' && error) return error;\n }\n return errorMessage(payload, fallback);\n}\n\n/**\n * Delay execution for a specified time\n * @param ms - Milliseconds to delay\n * @returns Promise that resolves after the delay\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Generate a random string of specified length\n * @param length - Length of the string\n * @returns Random string\n */\nexport function generateRandomString(length = 8): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n let result = '';\n for (let i = 0; i < length; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n\n/**\n * Truncate a string to a specified length\n * @param str - String to truncate\n * @param maxLength - Maximum length\n * @param suffix - Suffix to add to truncated string\n * @returns Truncated string\n */\nexport function truncateString(str: string, maxLength: number, suffix = '...'): string {\n if (str.length <= maxLength) return str;\n return str.substring(0, maxLength - suffix.length) + suffix;\n}\n\n/**\n * Serialize a JSON-LD schema for a `<script type=\"application/ld+json\">`\n * block — THE one home of the escape rule (the hub re-exports this from\n * lib/utils/breadcrumbs). JSON.stringify does NOT HTML-escape: a stored\n * \"</script>\" inside any admin/user-entered field would terminate the tag\n * early (stored XSS). Every \"<\" becomes the JSON escape sequence\n * backslash-u003c — still valid JSON, inert in HTML.\n */\nexport function serializeJsonLd(schema: unknown): string {\n return JSON.stringify(schema).replace(/</g, '\\\\u003c');\n}\n\n/**\n * Deep clone an object through a JSON round-trip.\n *\n * Lossy by construction, and deliberately so — this is the JSON projection of\n * `T`: `undefined` members and functions are dropped, `Date` becomes an ISO\n * string, `Map`/`Set` become `{}`, and a cyclic graph throws. Reach for\n * `structuredClone` when any of that matters.\n *\n * `JSON.parse` is typed `any`; the assertion states the contract above, which\n * is the closest a JSON clone can get to \"still a `T`\".\n *\n * @param obj - Object to clone (must be JSON-serializable)\n * @returns Cloned object\n */\nexport function deepClone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj)) as T;\n}\n\n/**\n * Get the Slack community join URL from environment variables\n * @returns Slack community join URL or fallback URL\n */\nexport function getSlackCommunityJoinUrl(): string {\n const url = process.env.NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL;\n if (!url) {\n console.warn('NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL is not defined in environment variables');\n return 'https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA';\n }\n return url;\n}\n\n/**\n * Clamp a number into an inclusive range. THE one clamp for the lib and the hub\n * (step/page/contrast math all bottom out here).\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Typed key-subset of an object. Keys whose value is `undefined` on the source\n * are still copied when present, so `pick(params, dropped)` writes explicit\n * `null`s (the URL-state writer's \"remove this key\" signal).\n */\nexport function pick<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Pick<T, K> {\n const out = {} as Pick<T, K>;\n for (const key of keys) {\n if (key in source) out[key] = source[key];\n }\n return out;\n}\n\n/**\n * React Query options for data that MUST NOT be served from the client cache.\n *\n * The rule (hub memory `feedback_no_client_query_cache_for_mutable_entities`):\n * anything a user can mutate, or a cron can rewrite under them, is refetched on\n * mount. Spread this instead of retyping `staleTime: 0` — it is also the hub\n * QueryClient's default, so a hook that spreads it is explicit rather than\n * silently inheriting.\n */\nexport const NO_CLIENT_CACHE = { staleTime: 0 } as const;\n","/**\n * useNearViewport — module-level shared IntersectionObserver in hook form.\n *\n * Single IO instance per `rootMargin` value, shared across every component\n * that mounts the hook. Reduces overhead vs. one IO per component on\n * grid/list pages where many subscribers observe the viewport with the same\n * margin. Promoted from the inline singleton at\n * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,\n * which is the only IO pattern in either repo today.\n *\n * Usage:\n * ```tsx\n * function MyCard() {\n * const { ref, isNear } = useNearViewport('500px');\n * return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;\n * }\n * ```\n *\n * StrictMode safety: cleanup uses an identity check on the registered\n * callback so React's dev double-mount (mount → cleanup → re-mount) does\n * not drop the second mount's freshly-set subscription. The IO callback\n * also checks `subscribers.get(target)` before invoking so a fire that\n * races with unmount cannot crash on a torn-down component.\n *\n * The hook fires once — on first intersection it sets `isNear=true` and\n * unobserves the element. Callers that need re-observation should\n * unmount and remount (or fork the hook for two-way behavior).\n */\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\n\n// Per-(rootMargin, threshold) IO map. Multiple call sites with different\n// margins/thresholds each get their own singleton observer.\nconst observers = new Map<string, IntersectionObserver>();\nconst subscribers = new WeakMap<Element, () => void>();\n\n// Slack for the explicit ratio check: an element sized to exactly meet the\n// threshold can report a ratio a hair under it (sub-pixel / float rounding), so\n// accept `ratio >= threshold - EPSILON`. With `threshold: 0` this is a no-op\n// (ratio is always ≥ 0 > -EPSILON) — the `isIntersecting` check alone gates.\nconst THRESHOLD_EPSILON = 0.01;\n\n/** Stable map key for an observer config. */\nfunction observerKey(rootMargin: string, threshold: number): string {\n return `${rootMargin}|${threshold}`;\n}\n\nfunction getObserverFor(rootMargin: string, threshold: number): IntersectionObserver {\n const key = observerKey(rootMargin, threshold);\n const existing = observers.get(key);\n if (existing) return existing;\n\n const io = new IntersectionObserver(\n entries => {\n entries.forEach(entry => {\n // Enforce the threshold EXPLICITLY. `isIntersecting` is spec-defined as\n // `intersectionRatio > 0` (any overlap), so the initial `observe()`\n // firing can report `true` at, say, 20% visible even with\n // `threshold: 0.5` — which would prematurely pass the gate. Require both\n // an intersection AND the ratio to actually meet the threshold (minus a\n // small epsilon for the float rounding where the ratio lands a hair\n // under the exact value).\n if (!entry.isIntersecting) return;\n if (entry.intersectionRatio < threshold - THRESHOLD_EPSILON) return;\n // Race-safe: re-read the callback at fire time. A late IO firing\n // after cleanup must not invoke a stale callback.\n const cb = subscribers.get(entry.target);\n if (cb) {\n cb();\n io.unobserve(entry.target);\n subscribers.delete(entry.target);\n }\n });\n },\n { rootMargin, threshold },\n );\n observers.set(key, io);\n return io;\n}\n\n/**\n * Default near-viewport lookahead — the SSOT for \"mount media on approach\".\n * Shared by this hook's default AND the raw two-way IntersectionObservers in\n * `cards-strip.tsx` / `video-bites-strip.tsx` (which need mount+unmount, not\n * this hook's fire-once semantics, but must agree on the distance).\n */\nexport const NEAR_VIEWPORT_ROOT_MARGIN = '500px';\n\nexport interface UseNearViewportResult<T extends Element = HTMLElement> {\n /** Ref to attach to the element you want to gate on visibility. */\n ref: (node: T | null) => void;\n /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */\n isNear: boolean;\n}\n\n/**\n * @param rootMargin Margin around the viewport (CSS-style string).\n * '500px' = element starts mounting 500px before scroll-in.\n * '1000px' = a full viewport's worth of lookahead.\n * '0px' = strict on-screen detection.\n * @param threshold Fraction of the element that must be visible before firing\n * (0 = any pixel — the default; 0.5 = at least half on-screen).\n */\nexport function useNearViewport<T extends Element = HTMLElement>(\n rootMargin: string = NEAR_VIEWPORT_ROOT_MARGIN,\n threshold: number = 0,\n): UseNearViewportResult<T> {\n const [isNear, setIsNear] = useState(false);\n const elRef = useRef<T | null>(null);\n const key = observerKey(rootMargin, threshold);\n\n // Subscribe/unsubscribe on element change.\n const ref = useCallback(\n (node: T | null) => {\n const prev = elRef.current;\n\n // Unsubscribe previous, if any. Identity-check the callback so a\n // StrictMode re-mount that has already re-registered keeps its sub.\n if (prev) {\n const stillOurs = subscribers.get(prev);\n if (stillOurs) {\n subscribers.delete(prev);\n observers.get(key)?.unobserve(prev);\n }\n }\n\n elRef.current = node;\n if (!node) return;\n\n const cb = () => setIsNear(true);\n subscribers.set(node, cb);\n getObserverFor(rootMargin, threshold).observe(node);\n },\n [key, rootMargin, threshold],\n );\n\n // Unsubscribe on unmount. Identity check guards the StrictMode race.\n useEffect(() => {\n return () => {\n const el = elRef.current;\n if (!el) return;\n if (subscribers.get(el)) {\n subscribers.delete(el);\n observers.get(key)?.unobserve(el);\n }\n };\n }, [key]);\n\n return { ref, isNear };\n}\n","/**\n * Fenced-code tracker — THE definition of \"am I inside a code fence\".\n * Server-safe: no React, no DOM.\n *\n * Lives in its own module because it is a GENERAL CommonMark fence machine\n * with two unrelated consumers: the heading scanner\n * (`utils/markdown-heading-id`) and the streaming block splitter\n * (`components/ui/markdown/streaming`). It used to live inside\n * `markdown-heading-id`, which meant a streaming renderer imported its fence\n * state machine from a module named after heading slugs. `markdown-heading-id`\n * re-exports these names so the public `utils` surface is unchanged.\n */\n\n/**\n * CommonMark fenced-code opener/closer. Fence indent is capped at 3 spaces\n * (4+ is an indented code block, whose backticks are literal content), and the\n * run length is captured so a closer can be required to be at least as long as\n * its opener. Matched on the RAW line — `trimStart()` would let a fence inside\n * 4-space-indented code toggle the state.\n *\n * `\\r`-TOLERANT BY CONSTRUCTION. The previous spelling ended `(.*)$`, and `.`\n * excludes `\\r` while `$` (no `m` flag) anchors at end of input — so on a CRLF\n * document, split on `\\n`, NO line ever matched and the tracker was completely\n * fence-blind: `['```js\\r','const a = 1\\r','```\\r','after\\r']` classified as\n * four `text` lines where the LF spelling gives `open/inside/close/text`. The\n * sanitizer's closer haystack then never blanked the fenced region, so a\n * `</textarea>` written as a CRLF code sample satisfied `hasLaterCloser` and a\n * prose `<textarea>` above it stayed LIVE (reproduced in the DOM; the LF twin\n * of the same fixture escapes correctly). The `\\r` is absorbed by `\\r?$` rather\n * than normalized out of the text, because the sanitizer's mask is\n * LENGTH-PRESERVING and every offset in it is an index into the original\n * string — stripping `\\r` would shift them all.\n *\n * THE ONLY COPY. This constant plus its open/close state machine used to be\n * duplicated verbatim in `components/ui/markdown/streaming.ts`, kept in sync\n * by a code comment — which is exactly how the `~~~`-blind extractor drifted\n * in the first place. Both consumers now instantiate `createFenceTracker()`.\n */\nconst FENCE_RE = /^ {0,3}(`{3,}|~{3,})([^\\n]*?)\\r?$/;\n\n/**\n * CommonMark's BLANK LINE — spaces and tabs only. The single definition, shared\n * by every consumer of this module (the sanitizer's mask passes, the heading\n * scanner, the HTML-block range walk).\n *\n * NOT `String.prototype.trim()`. JS `trim()` strips the full Unicode\n * White_Space set — U+00A0 NBSP, U+000B VT, U+000C FF, U+2028/9, U+FEFF BOM —\n * none of which CommonMark (or remark) treats as blank. Every place that meant\n * \"is this a CommonMark blank line?\" and asked `trim() === ''` therefore\n * TERMINATED a construct one line early while remark kept it open. Verified\n * end-to-end: `<div>\\n\\u00A0\\n\\`<textarea>\\`\\n\\nrest\\n` left a LIVE `<textarea>`\n * in the DOM (`escapeUnknownHtmlTags` returned the input byte-identical),\n * because the tracked HTML-block range ended at the NBSP filler line and the\n * inline-code shelter below it stopped being inside a tracked block. Same for\n * VT / FF / BOM, for `<pre>` / `<details>` wrappers, and for a live third-party\n * `<iframe>`.\n *\n * `\\r` IS accepted: a CRLF document split on `\\n` spells its blank lines\n * `\"\\r\"`, and the mask is length-preserving so stripping `\\r` from the source\n * is not an option (see `FENCE_RE`'s `\\r?$`).\n */\nconst BLANK_LINE_RE = /^[ \\t\\r]*$/;\n\n/** True when `line` is a CommonMark blank line (spaces/tabs only, `\\r`-tolerant). */\nexport function isBlankLine(line: string): boolean {\n return BLANK_LINE_RE.test(line);\n}\n\n/**\n * What one source line is, relative to fenced code:\n * - `open` — this line opened a fence\n * - `close` — this line closed the open fence\n * - `inside`— this line is fence content (including a would-be delimiter that\n * does NOT close the open fence: wrong marker char, too short, or\n * carrying an info string)\n * - `text` — this line is ordinary markdown outside any fence\n *\n * NOTE: no consumer discriminates the three non-`text` roles today — both the\n * heading scanner and the streaming splitter only test `!== 'text'`, and the\n * closer-haystack mask below only tests `=== 'open'` / `=== 'close'`. The\n * four-value union is kept because the state machine already computes the\n * distinction for free and a future consumer (e.g. fence-aware syntax\n * post-processing) would otherwise have to re-derive it.\n */\nexport type FenceLineRole = 'open' | 'close' | 'inside' | 'text';\n\nexport interface FenceTracker {\n /** Advance across one raw source line (mutates) and classify it. */\n push(line: string): FenceLineRole;\n /** Marker CHARACTER of the currently open fence (`` ` `` / `~`), or null. */\n openChar(): string | null;\n /** Run length of the currently open fence (a closer must be ≥ this). */\n openLength(): number;\n}\n\n/** A fresh fenced-code state machine. Server-safe: no React, no DOM. */\nexport function createFenceTracker(): FenceTracker {\n let fenceChar: string | null = null;\n let fenceLength = 0;\n return {\n push(line: string): FenceLineRole {\n const fence = FENCE_RE.exec(line);\n if (fence) {\n const run = fence[1];\n if (fenceChar === null) {\n fenceChar = run[0];\n fenceLength = run.length;\n return 'open';\n }\n // A closer must use the SAME marker char, be at least as long as the\n // opener, and (per CommonMark) carry no info string. \"No info string\"\n // means spaces/tabs only — `trim()` would accept an NBSP tail and close\n // a fence CommonMark keeps open, i.e. blank LESS of the haystack\n // (fail-OPEN). Hence `isBlankLine`, not `trim()`.\n if (run[0] === fenceChar && run.length >= fenceLength && isBlankLine(fence[2])) {\n fenceChar = null;\n fenceLength = 0;\n return 'close';\n }\n return 'inside';\n }\n return fenceChar === null ? 'text' : 'inside';\n },\n openChar: () => fenceChar,\n openLength: () => fenceLength,\n };\n}\n","/**\n * Markdown heading-id slug SSOT (server-safe: no React, no DOM).\n *\n * This algorithm previously existed in THREE byte-identical copies:\n * - `components/ui/simple-markdown-renderer.tsx` (generateHeadingId)\n * - `components/ui/rich-markdown-renderer.tsx` (generateHeadingId)\n * - `utils/markdown-section-extractor.ts` (extractSections)\n *\n * The extractor is the PRODUCER of `sectionIds` and the renderers are the\n * CONSUMERS — if the two ever drift, deep-link anchors and scroll-spy\n * targets silently diverge. All three now call these helpers; the parity\n * test `components/ui/__tests__/markdown-parity.test.tsx` asserts\n * extractor-vs-renderer ID agreement over the fixture corpus.\n */\n\n/** Emoji ranges stripped from heading text before slugification. */\nexport const HEADING_EMOJI_RE = /[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu;\n\n/** Remove emoji characters and trim. */\nexport function stripHeadingEmojis(text: string): string {\n return text.replace(HEADING_EMOJI_RE, '').trim();\n}\n\n/**\n * Core slug chain WITHOUT the emoji strip (the extractor exposes emoji\n * stripping as an option, so the two steps are kept separable):\n * lowercase → drop non-word/space/hyphen chars → spaces to hyphens →\n * trim leading/trailing hyphens. May return `''` for symbol-only input —\n * callers apply their own fallback (`section-N`).\n */\nexport function slugifyHeadingBase(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** The full default chain used by the renderers: emoji strip + slugify. */\nexport function slugifyHeadingText(text: string): string {\n return slugifyHeadingBase(stripHeadingEmojis(text));\n}\n\n// ---------------------------------------------------------------------------\n// Heading SCANNER + DEDUPER (the other half of the producer/consumer contract)\n// ---------------------------------------------------------------------------\n/**\n * Sharing the slug chain alone was not enough. The PRODUCER\n * (`utils/markdown-section-extractor`) and the CONSUMER\n * (`components/ui/markdown/heading-ids`) each carried their own copy of\n * \"which lines are headings\" and \"how do duplicates get suffixed\", and the\n * copies drifted:\n * - the extractor toggled its code-block state on a bare\n * `line.startsWith('```')`, so a `~~~`-fenced (or wider-backtick, or\n * indented) block containing `## Setup` produced a SECTION from the\n * extractor and NO id from the renderer — `sectionIdMap` then missed\n * silently and the deep-link anchor pointed nowhere;\n * - the extractor anchored ATX at column 0 while the renderer allowed the\n * CommonMark 0..3-space indent;\n * - the dedupe counter was hand-copied in both files, each with a comment\n * saying \"the two must agree\".\n * Both now call `scanHeadings` + `createHeadingIdDeduper`. Server-safe: no\n * React, no DOM.\n */\n\n// The CommonMark fence machine used to live in THIS file, which meant a\n// streaming renderer imported its fence state from a module named after\n// heading slugs. It now lives in `./markdown-fences`; re-exported here so the\n// public `utils` surface (and every existing import) is unchanged.\nexport { createFenceTracker, isBlankLine, type FenceTracker, type FenceLineRole } from './markdown-fences';\nimport { createFenceTracker, isBlankLine } from './markdown-fences';\n\n/** A heading the renderer will emit, located in the source. */\nexport interface ScannedHeading {\n /** 1-based line of the heading's FIRST line (setext: the title line). */\n line: number;\n level: number;\n /** Raw title text, before `stripInlineMarkdown` / slugification. */\n text: string;\n}\n\nexport interface ScanHeadingsOptions {\n /**\n * Skip a leading YAML frontmatter block (`---` on line 1 through the next\n * `---`). Only a DOCUMENT-LEADING block counts: a bare `---` mid-document\n * is a thematic break or a setext underline, and treating it as a\n * frontmatter toggle (what the extractor used to do) silently swallowed\n * every heading until the next one.\n */\n skipFrontmatter?: boolean;\n /** Include raw-HTML headings (`<h2>Title</h2>`). Default true. */\n includeRawHtml?: boolean;\n /**\n * Skip fenced-code blocks. Default true — a `##` inside a code fence is\n * code, not a heading, and the renderer emits no id for it.\n */\n skipFences?: boolean;\n}\n\n/**\n * EVERY line-anchored regex in this module ends `\\r?$`, not `$`.\n *\n * Lines here come from `content.split('\\n')`, so on a CRLF document each one\n * carries a trailing `\\r`. `$` (no `m` flag) anchors at end of string and `\\r`\n * is not in `[ \\t]`, so a `$`-anchored pattern matches NOTHING on such a\n * document. That was a live REGRESSION: `main`'s looser `^(#{1,6})\\s+(.+)`\n * had no end anchor at all and tolerated the `\\r` (the title was `.trim()`ed\n * afterwards), so `'# Real\\r\\n\\r\\nbody\\r\\n\\r\\n## Second\\r\\n'` yielded\n * `[\"Real\",\"Second\"]` on `main` and `[]` here — every CRLF-stored doc / blog /\n * release body silently lost its TOC, its in-page anchors and its doc-SEO\n * heading links.\n *\n * The `\\r` is ABSORBED, not normalized away: `scanHeadings` reports 1-based\n * LINE numbers that the renderer's id map is keyed by, and the sanitizer's mask\n * (which shares `markdown-fences`) is length-preserving — neither may be fed a\n * rewritten source.\n */\n/** CommonMark ATX heading (closing `#` run is not part of the title). */\nconst ATX_HEADING_RE = /^ {0,3}(#{1,6})[ \\t]+(.*?)(?:[ \\t]+#+)?[ \\t]*\\r?$/;\n/** ATX with no title at all (`###`, `## ###`) — still a heading, empty text. */\nconst ATX_EMPTY_RE = /^ {0,3}(#{1,6})[ \\t]*#*[ \\t]*\\r?$/;\n/**\n * Raw-HTML heading (`<h2 id=\"x\">Title</h2>`) — rehype-raw renders these too.\n * GLOBAL: two `<h3>`s on one line are two headings, and a non-global scan\n * silently dropped the second (it then fell through to the renderer's\n * suffix-free fallback and emitted a DUPLICATE DOM id).\n */\nconst RAW_HEADING_RE = /<h([1-6])\\b[^>]*>([\\s\\S]*?)(?:<\\/h\\1>|$)/gi;\n/**\n * Blockquote markers and list-item markers preceding an ATX heading\n * (`> ## Setup`, `- ## Setup`, `1. ## Setup`). mdast emits a REAL `<h2>` for\n * these; scanning only column-0..3 missed them entirely, so two identical\n * `> ## Setup` headings both hit the suffix-free fallback and emitted the\n * duplicate DOM ids this whole module exists to prevent.\n */\nconst CONTAINER_PREFIX_RE = /^ {0,3}(?:(?:>[ \\t]?)+|(?:[-+*]|\\d{1,9}[.)])[ \\t]+)+/;\n/** Setext underline (`===` → h1, `---` → h2), AFTER container-prefix strip. */\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(=+|-+)[ \\t]*\\r?$/;\n/** Thematic break — ends a paragraph run, never underlines it. */\nconst THEMATIC_BREAK_RE = /^ {0,3}(\\*|_){3,}[ \\t]*\\r?$/;\n/**\n * CommonMark HTML-block type 6 tag names — these open a raw HTML block no\n * matter what follows them on the line.\n */\nconst HTML_BLOCK_TAGS =\n 'address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul';\n\n/**\n * A line that OPENS a raw HTML block. mdast hands the whole block to the HTML\n * parser, so a `---` inside it is not a setext underline and the lines above\n * it are not a paragraph: `<div>\\nText\\n---\\n</div>` emits NO heading. The\n * scanner used to publish a phantom `Text` h2 there.\n *\n * The naive `^ {0,3}<` this replaced disqualified any run whose first line\n * merely STARTED with a tag, so `<span>x</span> more text` + `---` (a plain\n * paragraph in mdast, yielding an h2) was missed. The alternatives are spelled\n * out per CommonMark:\n * - types 1-5: `<script|pre|style|textarea`, `<!--`, `<?`, `<!X`, `<![CDATA[`\n * - type 6: a `HTML_BLOCK_TAGS` name, whatever follows it\n * - type 7: ANY complete open/closing tag ALONE on the line (trailing\n * whitespace only) — this is the clause the old regex got wrong\n * Attribute soup inside a type-7 tag is approximated with `[^>]*` rather than\n * a full attribute grammar; the error bias is unchanged (a missed run costs a\n * TOC entry, never a wrong id — the same direction as the documented\n * list-item-indent gap in `SetextRun.prefix`).\n */\nconst HTML_BLOCK_OPENER_RE = new RegExp(\n '^ {0,3}(?:' +\n // types 1-5\n '<(?:script|pre|style|textarea)(?:[\\\\s>]|$)|<!--|<\\\\?|<![a-zA-Z]|<!\\\\[CDATA\\\\[' +\n // type 6\n `|</?(?:${HTML_BLOCK_TAGS})(?:[\\\\s/>]|$)` +\n // type 7 — a complete tag standing alone on the line\n '|<[a-zA-Z][a-zA-Z0-9-]*(?:\\\\s[^>]*)?/?>[ \\\\t]*\\\\r?$' +\n '|</[a-zA-Z][a-zA-Z0-9-]*[ \\\\t]*>[ \\\\t]*\\\\r?$' +\n ')',\n 'i',\n);\n\n/**\n * The paragraph RUN that a setext underline would convert into a heading.\n *\n * mdast positions a setext heading at the FIRST line of the paragraph and its\n * text is the WHOLE run (`Foo\\nbar\\n===` → `{line:1, text:\"Foo\\nbar\"}`). The\n * scanner used to remember only the LAST line, so the renderer's line-keyed\n * lookup missed the heading entirely (falling back to a slug of the rendered\n * children, `foo-bar`) while the extractor published `bar` — a dead deep\n * link, plus a phantom TOC entry at the wrong line.\n */\ninterface SetextRun {\n /** 1-based line of the run's FIRST line — where mdast puts the heading. */\n line: number;\n /**\n * Container prefix (`> `, `- `, …) every line of the run shares. The\n * underline must carry the IDENTICAL prefix, which is why `> Quote` +\n * `> ---` scans as an h2 while `> Quote` + a bare `---` does not.\n *\n * KNOWN, DELIBERATE GAP: a setext underline indented to a LIST ITEM's\n * content column (`- item` / ` ---`) is a heading in mdast but has no\n * container prefix at all, so this equality test misses it. Closing that\n * would mean re-deriving list-item content columns here — the scanner\n * simply emits nothing (the pre-existing behavior), which costs a TOC entry\n * and never produces a WRONG id.\n */\n prefix: string;\n /** Run lines with the container prefix stripped, in order. */\n lines: string[];\n /** Run opened with a raw-HTML block line — mdast emits no heading for it. */\n disqualified: boolean;\n}\n\n/**\n * Collect every heading the markdown renderer will emit, in document order.\n * THE single definition of \"which lines are headings\" for both the extractor\n * and the renderer's id map.\n */\nexport function scanHeadings(content: string, options: ScanHeadingsOptions = {}): ScannedHeading[] {\n const { skipFrontmatter = true, includeRawHtml = true, skipFences = true } = options;\n const out: ScannedHeading[] = [];\n const lines = content.split('\\n');\n // ONE fenced-code state machine (utils/markdown-fences), shared with\n // markdown/streaming.ts.\n const fences = skipFences ? createFenceTracker() : null;\n\n let start = 0;\n if (skipFrontmatter && lines[0]?.trim() === '---') {\n for (let i = 1; i < lines.length; i++) {\n if (lines[i].trim() === '---' || lines[i].trim() === '...') {\n start = i + 1;\n break;\n }\n }\n }\n\n // The paragraph run a setext underline would convert (see SetextRun).\n let setextRun: SetextRun | null = null;\n\n for (let i = start; i < lines.length; i++) {\n const line = lines[i];\n if (fences) {\n const role = fences.push(line);\n if (role !== 'text') {\n setextRun = null;\n continue;\n }\n }\n\n // Container prefix (`> `, `- `, `1. `) is stripped from EVERY construct\n // below, so a blockquote/list-nested heading is scanned in both its ATX\n // (`> ## Setup`) and setext (`> Quote title` / `> ---`) spellings.\n const prefix = CONTAINER_PREFIX_RE.exec(line)?.[0] ?? '';\n const body = prefix ? line.slice(prefix.length) : line;\n\n // --- setext (`Title` / `===`): a REAL h1/h2 in mdast, positioned at the\n // run's FIRST line — which is the line the heading-id map is keyed by.\n // The underline must carry the SAME container prefix as the run it\n // closes; `> Quote` followed by a bare `---` is a quote and a thematic\n // break, not a heading.\n const setext = SETEXT_UNDERLINE_RE.exec(body);\n if (setext) {\n if (setextRun && !setextRun.disqualified && setextRun.prefix === prefix) {\n out.push({\n line: setextRun.line,\n level: setext[1][0] === '=' ? 1 : 2,\n text: setextRun.lines.join('\\n'),\n });\n }\n setextRun = null;\n continue;\n }\n\n // --- ATX, including inside a blockquote / list item.\n const atx = ATX_HEADING_RE.exec(body) ?? ATX_EMPTY_RE.exec(body);\n if (atx) {\n out.push({ line: i + 1, level: atx[1].length, text: (atx[2] ?? '').trim() });\n setextRun = null;\n continue;\n }\n\n if (includeRawHtml && line.indexOf('<') !== -1) {\n RAW_HEADING_RE.lastIndex = 0;\n let raw: RegExpExecArray | null;\n let matched = false;\n while ((raw = RAW_HEADING_RE.exec(line)) !== null) {\n matched = true;\n out.push({\n line: i + 1,\n level: Number(raw[1]),\n text: raw[2].replace(/<[^>]*>/g, '').trim(),\n });\n }\n if (matched) {\n setextRun = null;\n continue;\n }\n }\n\n // Paragraph accumulation. A blank line or a thematic break ends the run;\n // a change of container prefix starts a new block (a list or blockquote\n // interrupts a paragraph in CommonMark).\n // CommonMark's blank line (spaces/tabs), NOT `trim()` — an NBSP-only line\n // is paragraph CONTENT to remark, so ending the run there would move a\n // setext heading's reported line off the run's first line.\n if (isBlankLine(body) || THEMATIC_BREAK_RE.test(body)) {\n setextRun = null;\n continue;\n }\n if (setextRun && setextRun.prefix === prefix) {\n setextRun.lines.push(body.trim());\n } else {\n setextRun = {\n line: i + 1,\n prefix,\n lines: [body.trim()],\n disqualified: HTML_BLOCK_OPENER_RE.test(body),\n };\n }\n }\n\n return out;\n}\n\n/**\n * The duplicate-id suffix counter, as ONE closure both sides instantiate.\n * First occurrence keeps the bare slug; the Nth gets `-N`. Extractor and\n * renderer MUST use the same counter or `#anchor` deep links diverge.\n */\nexport function createHeadingIdDeduper(): (cleanId: string) => string {\n const counts: Record<string, number> = {};\n return (cleanId: string): string => {\n if (counts[cleanId]) {\n counts[cleanId]++;\n return `${cleanId}-${counts[cleanId]}`;\n }\n counts[cleanId] = 1;\n return cleanId;\n };\n}\n","/**\n * GitHub-compatible anchor resolution for in-page `#hash` links.\n *\n * ## The mismatch this exists to bridge\n *\n * Doc authors write their table of contents against GitHub's slugger, which\n * keeps the separator left behind by a stripped emoji:\n *\n * `## 📚 Table of Contents` → GitHub anchor `#-table-of-contents`\n *\n * Our heading ids come from the shared slug SSOT (`markdown-heading-id`),\n * whose `slugifyHeadingBase` trims leading and trailing hyphens — so the same\n * heading renders as `id=\"table-of-contents\"`. The \"On this page\" rail\n * navigates by those ids and works; the in-body TOC link points at an id that\n * does not exist and the browser silently does nothing.\n *\n * Rather than change the id shape (it is already baked into shared URLs and\n * the rail), resolution is made tolerant: compare a *normalized* form of both\n * sides. This module owns ONLY that tolerance — the slug chain itself is\n * imported, never re-implemented, so a change to the id shape lands in one\n * place and both sides move together.\n *\n * Callers do not reach for this directly: `getHashTargetElement`\n * (`utils/same-page-hash-nav`) is the ONE resolver every hash consumer goes\n * through, and it applies this as its last pass.\n */\nimport { slugifyHeadingText } from './markdown-heading-id';\n\n/**\n * Reduce an anchor id (ours or GitHub's) to a comparable form.\n *\n * Everything but the final hyphen-run collapse is the shared heading-slug\n * chain: emoji strip → lowercase → drop non-word chars → spaces to hyphens →\n * trim edge hyphens. The collapse is this module's own addition, covering\n * GitHub's *interior* divergence — `## 💬 Community & Support` slugs to\n * `#-community--support` (a double hyphen around the dropped `&`) against our\n * `community-support`.\n *\n * `'-getting-started'` → `'getting-started'`\n * `'-community--support'` → `'community-support'`\n * `'Getting Started'` → `'getting-started'`\n */\nexport function normalizeAnchorId(raw: string): string {\n return slugifyHeadingText(raw)\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/** Heading elements carrying an id — the candidate set for the fuzzy pass. */\nconst HEADING_SELECTOR = 'h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]';\n\n/** A `base-N` id, split into its base and ordinal. */\nconst DUPLICATE_SUFFIX_RE = /^(.+)-(\\d+)$/;\n\n/**\n * The two sides also disagree on where duplicate-heading numbering STARTS.\n * For a document with two `## Setup` headings GitHub emits `setup` and\n * `setup-1`, while `createHeadingIdDeduper` emits `setup` and `setup-2` (its\n * suffix is the occurrence COUNT, not the offset). So a GitHub `#setup-1`\n * needs our `setup-2` — every duplicate is off by exactly one.\n *\n * Runs LAST, after the exact lookup and the heading scan have both missed: a\n * heading that genuinely slugs to `step-2` is resolved long before this, and\n * the shift is the one pass that can match an id it was not asked for.\n */\nfunction findDuplicateShifted(normalized: string, doc: Document): HTMLElement | null {\n const parts = DUPLICATE_SUFFIX_RE.exec(normalized);\n if (!parts) return null;\n const ordinal = Number(parts[2]);\n if (!Number.isSafeInteger(ordinal)) return null;\n return doc.getElementById(`${parts[1]}-${ordinal + 1}`);\n}\n\n/**\n * Resolve a `#hash` id that no exact lookup matched, by comparing normalized\n * forms. Returns `null` when nothing in the document matches, so the caller\n * can leave the browser's default behavior alone rather than hijacking a\n * click that was never ours to handle.\n *\n * Three passes, in order of how much they assume:\n * 1. a single `getElementById` on the normalized id — catches the whole\n * emoji-hyphen family without walking the DOM;\n * 2. a heading scan — catches ids whose own raw form normalizes differently\n * (e.g. `a---b`, from a title containing a literal ` - `);\n * 3. the duplicate-heading shift (see `findDuplicateShifted`) — last,\n * because it is the only pass that resolves to an id the fragment did\n * not name.\n */\nexport function findAnchorElementByNormalizedId(rawId: string, doc: Document): HTMLElement | null {\n const normalized = normalizeAnchorId(rawId);\n if (!normalized) return null;\n\n if (normalized !== rawId) {\n const direct = doc.getElementById(normalized);\n if (direct) return direct;\n }\n\n for (const heading of Array.from(doc.querySelectorAll<HTMLElement>(HEADING_SELECTOR))) {\n if (normalizeAnchorId(heading.id) === normalized) return heading;\n }\n\n // Last: the shift would happily match an unrelated `x-3` for a `x-2` that\n // has no heading at all, so it runs only once the scan has come up empty.\n return findDuplicateShifted(normalized, doc);\n}\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).\n *\n * THE one rule for \"what scrolls this element\": `useScrollSpy` resolves the\n * scroller it listens to with it, so the section a spy highlights is the\n * section this helper scrolls to — inside a `<main overflow-y-auto>` shell as\n * much as on a window-scrolled page. */\nexport function 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' && 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 { findAnchorElementByNormalizedId } from './anchor-id';\nimport { 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\n/**\n * Resolve the DOM element a hash fragment id points at — THE hash resolver;\n * `navigateSamePageHash`, `useScrollToHash` and the markdown link renderer\n * all go through this one, so a fragment that resolves for a deep link\n * resolves for an in-body click too.\n *\n * Three passes, cheapest first:\n * 1. the raw id (ordinary anchors — the overwhelming common case);\n * 2. the percent-decoded form — href composers like `buildTicketOpenHref`\n * encode the id into the fragment while rows render the raw id, so\n * `#ticket-a%2Fb` must still find `id=\"ticket-a/b\"`. A malformed escape\n * sequence just falls through;\n * 3. a NORMALIZED match against the document's headings — doc bodies are\n * authored on GitHub, whose slugger keeps the hyphen left behind by a\n * stripped emoji (`## 📚 Table of Contents` → `#-table-of-contents`)\n * while our ids trim it. Without this every in-body TOC link in every\n * GitHub-authored doc resolves to nothing and clicking one does nothing.\n * See `utils/anchor-id`.\n */\nexport function getHashTargetElement(id: string): HTMLElement | null {\n if (!id) return null;\n const direct = document.getElementById(id);\n if (direct) return direct;\n\n let decoded = id;\n try {\n decoded = decodeURIComponent(id);\n if (decoded !== id) {\n const decodedEl = document.getElementById(decoded);\n if (decodedEl) return decodedEl;\n }\n } catch {\n // malformed escape — the raw lookup above already covered it\n decoded = id;\n }\n\n return findAnchorElementByNormalizedId(decoded, document);\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(target: string, options: NavigateSamePageHashOptions = {}): boolean {\n if (typeof window === 'undefined') return false;\n const { headerOffset = 0, history: historyMode = 'push' } = options;\n const normalizedTarget = target.startsWith('#') ? window.location.pathname + window.location.search + target : 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 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(\n new HashChangeEvent('hashchange', {\n oldURL,\n newURL: window.location.href,\n }),\n );\n }\n const el = id ? getHashTargetElement(id) : null;\n if (id && !el && process.env.NODE_ENV === 'development') {\n console.warn(`[navigateSamePageHash] anchor \"#${id}\" not found — scrolling to top.`);\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"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/ui/use-image-edge-color.ts","../src/utils/platform-identity.ts","../src/utils/app-config.ts","../src/utils/local-storage-adapter.ts","../src/utils/embed-proxy-auth-storage.ts","../src/utils/embed-authed-fetch.ts","../src/hooks/use-authed-asset-src.ts","../src/hooks/use-authed-image-src.ts","../src/utils/og-placeholder.ts","../src/hooks/use-og-placeholder-url.ts"],"sourcesContent":["'use client';\n\nimport { useState, useEffect } from 'react';\n\n/**\n * Extract the dominant edge color from an image using color bucketing.\n * Samples pixels along left and right edges (the visible letterbox areas),\n * groups them into color buckets, and returns the most common bucket.\n * This avoids muddy averages when edges have mixed colors (e.g., sky + sand).\n */\nfunction extractEdgeColor(img: HTMLImageElement): string {\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return '#000000';\n\n const maxSize = 100;\n const scale = Math.min(maxSize / img.naturalWidth, maxSize / img.naturalHeight);\n const w = Math.round(img.naturalWidth * scale);\n const h = Math.round(img.naturalHeight * scale);\n canvas.width = w;\n canvas.height = h;\n\n ctx.drawImage(img, 0, 0, w, h);\n\n const data = ctx.getImageData(0, 0, w, h).data;\n\n // Sample left edge, right edge, top edge, bottom edge (15% band)\n const edgeW = Math.max(2, Math.round(w * 0.15));\n const edgeH = Math.max(2, Math.round(h * 0.15));\n\n // Color bucketing: quantize to 32-step buckets for grouping similar colors\n const bucketSize = 32;\n const buckets = new Map<string, { r: number; g: number; b: number; count: number }>();\n\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const isEdge = x < edgeW || x >= w - edgeW || y < edgeH || y >= h - edgeH;\n\n if (!isEdge) continue;\n\n const i = (y * w + x) * 4;\n const a = data[i + 3];\n if (a < 128) continue;\n\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n\n // Quantize to bucket\n const br = Math.floor(r / bucketSize) * bucketSize;\n const bg = Math.floor(g / bucketSize) * bucketSize;\n const bb = Math.floor(b / bucketSize) * bucketSize;\n const key = `${br},${bg},${bb}`;\n\n const existing = buckets.get(key);\n if (existing) {\n existing.r += r;\n existing.g += g;\n existing.b += b;\n existing.count++;\n } else {\n buckets.set(key, { r, g, b, count: 1 });\n }\n }\n }\n\n if (buckets.size === 0) return '#000000';\n\n // Find the most common color bucket\n let bestBucket: { r: number; g: number; b: number; count: number } | null = null;\n for (const bucket of buckets.values()) {\n if (!bestBucket || bucket.count > bestBucket.count) {\n bestBucket = bucket;\n }\n }\n\n if (!bestBucket || bestBucket.count === 0) return '#000000';\n\n // Return average color within the winning bucket\n const r = Math.round(bestBucket.r / bestBucket.count);\n const g = Math.round(bestBucket.g / bestBucket.count);\n const b = Math.round(bestBucket.b / bestBucket.count);\n\n return `rgb(${r}, ${g}, ${b})`;\n}\n\n/**\n * Hook that extracts the dominant edge color from an image URL.\n * Returns a CSS color string for use as a background behind object-contain images.\n *\n * Always sets crossOrigin='anonymous' — required for canvas pixel access.\n * Most CDNs (Supabase, Cloudflare, our image proxy) return CORS headers.\n * If the server doesn't support CORS, onerror fires and we use the fallback.\n *\n * @param imageUrl - URL of the image to analyze\n * @param fallback - Fallback color if extraction fails (default: '#000000')\n * @returns CSS color string (e.g., 'rgb(34, 28, 22)')\n */\nexport function useImageEdgeColor(imageUrl: string | undefined | null, fallback = '#000000'): string {\n const [color, setColor] = useState(fallback);\n\n // Losing the url (or being given a different fallback) is a prop-driven\n // reset, so it happens while rendering — React's documented alternative to a\n // sync-from-props effect. From the effect it would commit one frame with the\n // departed image's colour still painted behind an image that is already gone.\n const [resetFor, setResetFor] = useState({ imageUrl, fallback });\n if (resetFor.imageUrl !== imageUrl || resetFor.fallback !== fallback) {\n setResetFor({ imageUrl, fallback });\n if (!imageUrl) setColor(fallback);\n }\n\n useEffect(() => {\n if (!imageUrl) return undefined;\n\n let cancelled = false;\n const img = new Image();\n img.crossOrigin = 'anonymous';\n\n img.onload = () => {\n if (cancelled) return;\n try {\n setColor(extractEdgeColor(img));\n } catch {\n setColor(fallback);\n }\n };\n\n img.onerror = () => {\n if (cancelled) return;\n setColor(fallback);\n };\n\n img.src = imageUrl;\n\n return () => {\n cancelled = true;\n };\n }, [imageUrl, fallback]);\n\n return color;\n}\n","/**\n * Platform identity + brand SSOT (single source of truth).\n *\n * ONE module owns every per-platform STRING (display/short/long name, description,\n * slogan, icon name) and every per-platform COLOUR DECISION (the accent/secondary\n * \"stem\", the theme, the surface). Everything downstream DERIVES from here:\n * `platform-config.tsx`'s icon + colour maps, `PlatformBadge`, `getPlatformConfig`,\n * the hub's `AppConfig.brandColors`, the hub's manifest generator and `theme-color`.\n *\n * JSX-FREE + zero-dependency ON PURPOSE: it carries its own tsup entry and\n * `exports` subpath (`@flamingo-stack/openframe-frontend-core/utils/platform-identity`)\n * so hub SCRIPTS and `server-only` modules can import it without dragging React,\n * the icon components, or the utils barrel into their graph.\n *\n * TWO accepted by-construction copies of the colour decision remain, both LOCKED\n * by `src/__tests__/mlg-platform-leaves.test.ts` in this package:\n * 1. `platformHexColors` below (hand-typed hex mirror of each accent token).\n * 2. The `[data-app-type='…']` blocks in `src/styles/ods-colors.css`.\n */\n\nimport type { PlatformName } from '../types/platform';\n\n// ── Strings ─────────────────────────────────────────────────────────────────\n\n/** Human-readable platform names. THE naming source for lib-side presentation. */\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 mlg: 'Major League GitHub',\n universal: 'Universal',\n};\n\n/**\n * Short-name OVERRIDES only. A platform without a row falls back to its display\n * name, so this map never lists a name that equals `platformDisplayNames`.\n */\nexport const platformShortNames: Partial<Record<PlatformName, string>> = {\n mlg: 'MLG',\n};\n\nexport const platformDescriptions = {\n openmsp:\n '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:\n '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 mlg: 'Scouting report for US open-source talent. Rank GitHub contributors by programming language, city, state, region, and nearest Major League Soccer club.',\n universal: 'Cross-platform universal content.',\n};\n\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 mlg: 'GitHub Scouting Report: Major League Edition',\n universal: 'Universal Platform',\n};\n\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 mlg: 'mlg-logo',\n};\n\nexport function getPlatformDisplayName(platformName: string): string {\n return platformDisplayNames[platformName as keyof typeof platformDisplayNames] || platformName;\n}\n\n/** Compact label (nav chips, manifest `short_name`, JSON-LD `alternateName`). */\nexport function getPlatformShortName(platformName: string): string {\n return platformShortNames[platformName as PlatformName] ?? getPlatformDisplayName(platformName);\n}\n\nexport function getPlatformDescription(platformName: string): string {\n return platformDescriptions[platformName as keyof typeof platformDescriptions] || platformName;\n}\n\nexport function getPlatformSlogan(platformName: string): string {\n return platformSlogans[platformName as keyof typeof platformSlogans] || platformName;\n}\n\nexport function getDefaultIconForPlatform(platformName: string): string {\n return platformIconNames[platformName as keyof typeof platformIconNames] || platformIconNames.universal;\n}\n\n// ── Colour stems ────────────────────────────────────────────────────────────\n\n/**\n * The six brand/attention colours (plus the neutral) a platform may adopt, named\n * by STEM rather than by token so the token spelling has exactly one home below.\n */\nexport type OdsColorStem =\n 'flamingo-pink' | 'flamingo-cyan' | 'open-yellow' | 'success' | 'warning' | 'error' | 'text-secondary';\n\n/** Stem → ODS token name (without the `--ods-` prefix). THE token spelling. */\nexport const ODS_STEM_TOKENS: Record<OdsColorStem, string> = {\n 'flamingo-pink': 'flamingo-pink-base',\n 'flamingo-cyan': 'flamingo-cyan-base',\n 'open-yellow': 'open-yellow-base',\n success: 'attention-green-success',\n warning: 'attention-yellow-warning',\n error: 'attention-red-error',\n 'text-secondary': 'system-greys-grey',\n};\n\n/**\n * Stem → Tailwind class strings. STATIC LITERALS on purpose: the Tailwind\n * scanner reads source text, so a template-built class would be purged.\n * This is the ONLY place a stem-to-class literal exists.\n */\nexport const ODS_STEM_CLASSES: Record<OdsColorStem, { text: string; bg: string; bgSoft: string; borderSoft: string }> =\n {\n 'flamingo-pink': {\n text: 'text-ods-flamingo-pink',\n bg: 'bg-ods-flamingo-pink',\n bgSoft: 'bg-ods-flamingo-pink/10',\n borderSoft: 'border-ods-flamingo-pink/30',\n },\n 'flamingo-cyan': {\n text: 'text-ods-flamingo-cyan',\n bg: 'bg-ods-flamingo-cyan',\n bgSoft: 'bg-ods-flamingo-cyan/10',\n borderSoft: 'border-ods-flamingo-cyan/30',\n },\n 'open-yellow': {\n text: 'text-ods-open-yellow',\n bg: 'bg-ods-open-yellow',\n bgSoft: 'bg-ods-open-yellow/10',\n borderSoft: 'border-ods-open-yellow/30',\n },\n success: {\n text: 'text-ods-success',\n bg: 'bg-ods-success',\n bgSoft: 'bg-ods-success/10',\n borderSoft: 'border-ods-success/30',\n },\n warning: {\n text: 'text-ods-warning',\n bg: 'bg-ods-warning',\n bgSoft: 'bg-ods-warning/10',\n borderSoft: 'border-ods-warning/30',\n },\n error: {\n text: 'text-ods-error',\n bg: 'bg-ods-error',\n bgSoft: 'bg-ods-error/10',\n borderSoft: 'border-ods-error/30',\n },\n 'text-secondary': {\n text: 'text-ods-text-secondary',\n bg: 'bg-ods-text-secondary',\n bgSoft: 'bg-ods-text-secondary/10',\n borderSoft: 'border-ods-text-secondary/30',\n },\n };\n\n// ── Theme + surface ─────────────────────────────────────────────────────────\n\n/**\n * Every platform ships the dark theme today (the hub's flamingo config says so\n * explicitly, and every `[data-app-type]` block inherits the dark `:root`).\n * ONE constant instead of a per-platform field: reintroduce the field only when\n * a platform actually differs.\n */\nexport const PLATFORM_THEME = 'dark' as const;\n\n/** The dark surface's tokens, consumed by config `brandColors` and the manifest. */\nexport const PLATFORM_SURFACE = {\n background: 'system-greys-background',\n text: 'system-greys-white',\n} as const;\n\nexport const getPlatformBackgroundVarName = (): string => `--ods-${PLATFORM_SURFACE.background}`;\nexport const getPlatformTextVarName = (): string => `--ods-${PLATFORM_SURFACE.text}`;\n\n// ── Brand record ────────────────────────────────────────────────────────────\n\nexport interface PlatformBrand {\n /** Primary brand colour (`--color-accent-primary` in the platform's CSS block). */\n accentStem: OdsColorStem;\n /** Link colour (`--color-link`). Equals `accentStem` for most platforms. */\n secondaryStem: OdsColorStem;\n}\n\n/**\n * THE accent/link table. Every row is verified against that platform's\n * `[data-app-type='…']` block in `ods-colors.css` by the hub's parity test.\n */\nexport const PLATFORM_BRAND: Record<PlatformName, PlatformBrand> = {\n openframe: { accentStem: 'open-yellow', secondaryStem: 'flamingo-cyan' },\n openmsp: { accentStem: 'open-yellow', secondaryStem: 'open-yellow' },\n flamingo: { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n 'flamingo-teaser': { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n tmcg: { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n 'marketing-hub': { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-pink' },\n 'product-hub': { accentStem: 'success', secondaryStem: 'success' },\n 'revenue-hub': { accentStem: 'warning', secondaryStem: 'warning' },\n 'people-hub': { accentStem: 'flamingo-cyan', secondaryStem: 'flamingo-cyan' },\n 'company-hub': { accentStem: 'error', secondaryStem: 'error' },\n mlg: { accentStem: 'flamingo-pink', secondaryStem: 'flamingo-cyan' },\n universal: { accentStem: 'text-secondary', secondaryStem: 'text-secondary' },\n};\n\n/** The brand row for a platform, falling back to `universal` for anything unknown. */\nexport function getPlatformBrand(platformName: string): PlatformBrand {\n return PLATFORM_BRAND[platformName as PlatformName] ?? PLATFORM_BRAND.universal;\n}\n\nexport function platformAccentToken(platformName: string): string {\n return ODS_STEM_TOKENS[getPlatformBrand(platformName).accentStem];\n}\n\nexport function platformSecondaryToken(platformName: string): string {\n return ODS_STEM_TOKENS[getPlatformBrand(platformName).secondaryStem];\n}\n\n/** `--ods-…` CSS variable NAME for a platform's accent (input to `resolveOdsColor`). */\nexport function platformAccentVarName(platformName: string): string {\n return `--ods-${platformAccentToken(platformName)}`;\n}\n\nexport function platformSecondaryVarName(platformName: string): string {\n return `--ods-${platformSecondaryToken(platformName)}`;\n}\n\n/** `var(--ods-…)` VALUE for a platform's accent (config `brandColors` data). */\nexport function getPlatformAccentColor(platformName: string): string {\n return `var(${platformAccentVarName(platformName)})`;\n}\n\nexport function getPlatformSecondaryColor(platformName: string): string {\n return `var(${platformSecondaryVarName(platformName)})`;\n}\n\n/** Tailwind classes for a platform, DERIVED from its stems. */\nexport function getPlatformBrandClasses(platformName: string): {\n accentText: string;\n accentBg: string;\n accentBgSoft: string;\n accentBorderSoft: string;\n secondaryText: string;\n} {\n const brand = getPlatformBrand(platformName);\n const accent = ODS_STEM_CLASSES[brand.accentStem];\n const secondary = ODS_STEM_CLASSES[brand.secondaryStem];\n return {\n accentText: accent.text,\n accentBg: accent.bg,\n accentBgSoft: accent.bgSoft,\n accentBorderSoft: accent.borderSoft,\n secondaryText: secondary.text,\n };\n}\n\n/** Platform background classes, DERIVED (was a hand-typed hex map). */\nexport const platformColors: Record<PlatformName, string> = Object.fromEntries(\n (Object.keys(PLATFORM_BRAND) as PlatformName[]).map(p => [p, getPlatformBrandClasses(p).accentBg]),\n) as Record<PlatformName, string>;\n\nexport function getPlatformColor(platformName: string) {\n return platformColors[platformName as PlatformName] || platformColors.universal;\n}\n\n/**\n * Hand-typed hex mirror of each platform's accent token (the DB `default_color`\n * and other JS-value consumers). Accepted copy #1 — locked by\n * `src/__tests__/mlg-platform-leaves.test.ts` in this package, which asserts\n * every row against the `[data-app-type]` blocks in `ods-colors.css`.\n * (The hub has its OWN parity test, but it holds different copies: the asset\n * generator's map and the generated manifests.)\n */\nexport const platformHexColors: Record<PlatformName, string> = {\n openmsp: '#ffc008',\n openframe: '#ffc008',\n flamingo: '#f357bb',\n universal: '#888888',\n 'flamingo-teaser': '#f357bb',\n 'marketing-hub': '#f357bb',\n 'product-hub': '#5ea62e',\n 'revenue-hub': '#e1b32f',\n 'people-hub': '#5efaf0',\n 'company-hub': '#f36666',\n tmcg: '#f357bb',\n mlg: '#f357bb',\n};\n\nexport function getDefaultColorForPlatform(platformName: string): string {\n return platformHexColors[platformName as PlatformName] || platformHexColors.universal;\n}\n","// Stub app config\nexport const APP_CONFIG = {\n app: {\n type: 'openmsp',\n name: 'OpenMSP',\n domain: 'openmsp.ai',\n },\n features: {\n announcements: true,\n notifications: true,\n },\n} as const;\n\nexport function getAppConfig() {\n return APP_CONFIG;\n}\n\nexport function getAppType() {\n // The exact `process.env.NEXT_PUBLIC_APP_TYPE` member expression is kept so\n // Next/webpack can statically inline it. The try/catch makes the call safe\n // in non-Next React hosts (Vite/CRA embeds) where `process` does not exist\n // at runtime — those hosts get the 'openmsp' fallback, which is fine for\n // embeds: dismissal cookies are domain-scoped and an embed domain serves\n // one platform's announcements.\n try {\n return process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp';\n } catch {\n return 'openmsp';\n }\n}\n","/**\n * Thin JSON-typed Web-Storage adapter (localStorage or sessionStorage).\n *\n * Centralizes the SSR-guard + try/catch + silent quota-failure pattern\n * that every per-feature storage util would otherwise re-implement.\n *\n * Optional `namespace` prefix supports platform / user partitioning —\n * the resolver runs lazily at call time so the namespace can vary across\n * the lifetime of the page (e.g. proxy-auth switches user, which switches\n * the key suffix).\n *\n * Backend selection (`backend: 'local' | 'session'`):\n * - `'local'` (default): persists across browser sessions. Use for\n * UI state, chat history metadata, feature-flag opt-ins.\n * - `'session'`: cleared when the tab closes. Use for ANY auth-adjacent\n * value (bearer tokens, act-as identity, proxy credentials). Reduces\n * the XSS-exfiltration attack window from \"indefinite\" to \"until tab\n * close\" without losing per-session ergonomics.\n */\n\nexport type WebStorageBackend = 'local' | 'session';\n\nexport interface LocalStorageAdapter<T> {\n load(): T | null;\n save(value: T): void;\n clear(): void;\n /** Resolved storage key for the current call. Useful for tests. */\n resolveKey(): string;\n}\n\nexport interface LocalStorageAdapterOptions<T> {\n /** Base storage key. Combined with `namespace()` when provided. */\n key: string;\n /** Optional dynamic namespace prefix appended via `.` separator.\n * Called on EVERY read/write so the key can vary across the page\n * lifetime (e.g. when the platform or user identity changes). */\n namespace?: () => string | null | undefined;\n /** Runtime shape check. Falsey return → `load()` yields null. */\n validate?: (parsed: unknown) => parsed is T;\n /** Diagnostic prefix written to `console.warn` on parse / write\n * failures. Defaults to `'[local-storage]'`. */\n logTag?: string;\n /** Which Web-Storage backend to use. Defaults to `'local'`. Pass\n * `'session'` for anything auth-adjacent so the value evaporates\n * when the tab closes. */\n backend?: WebStorageBackend;\n}\n\nfunction getStorage(backend: WebStorageBackend): Storage | null {\n if (typeof window === 'undefined') return null;\n try {\n return backend === 'session' ? window.sessionStorage : window.localStorage;\n } catch {\n // Some sandboxed contexts (Safari private mode older versions,\n // strict CSP) throw on storage access — treat as unavailable.\n return null;\n }\n}\n\nexport function createLocalStorageAdapter<T>(options: LocalStorageAdapterOptions<T>): LocalStorageAdapter<T> {\n const tag = options.logTag ?? '[local-storage]';\n const backend: WebStorageBackend = options.backend ?? 'local';\n const resolveKey = (): string => {\n const ns = options.namespace?.();\n return ns ? `${ns}.${options.key}` : options.key;\n };\n\n return {\n resolveKey,\n load() {\n const storage = getStorage(backend);\n if (!storage) return null;\n try {\n const raw = storage.getItem(resolveKey());\n if (!raw) return null;\n const parsed = JSON.parse(raw) as unknown;\n if (options.validate && !options.validate(parsed)) return null;\n return parsed as T;\n } catch (err) {\n console.warn(`${tag} parse failed for key ${resolveKey()}:`, err);\n return null;\n }\n },\n save(value: T) {\n const storage = getStorage(backend);\n if (!storage) return;\n try {\n storage.setItem(resolveKey(), JSON.stringify(value));\n } catch (err) {\n console.warn(`${tag} write failed for key ${resolveKey()}:`, err);\n }\n },\n clear() {\n const storage = getStorage(backend);\n if (!storage) return;\n try {\n storage.removeItem(resolveKey());\n } catch (err) {\n console.warn(`${tag} clear failed for key ${resolveKey()}:`, err);\n }\n },\n };\n}\n","'use client';\n\n/**\n * Client-side persistence for embed-surface proxy credentials (a\n * PLATFORM API KEY + impersonation email). Used by every embedded\n * surface — the chat widget AND the ticket center AND any future\n * embedded React component that needs to identify itself as the\n * impersonated customer.\n *\n * When set, the surface attaches the creds as\n * `Authorization: Bearer <platform API key>` + `X-Chat-Act-As: <email>`\n * on every call to `/api/docs/chat`, `/api/chat/*`, and any other route\n * gated by `requireChatAuth` — proving to the server that this session\n * is acting on behalf of <email>. The key is minted in the hub's\n * `/admin/api-keys` and is platform-bound (`fpk_<platform>_…`): a\n * SERVICE key may act as any email; a PERSONAL key only as its owner.\n *\n * **Naming history:** the stored field is still named `secret` and the\n * wire-side header names are `X-Chat-*`. Those are client/server\n * contracts predating the per-platform key migration (the old shared\n * `CHAT_PROXY_SECRET` env is removed hub-side); renaming them would\n * break persisted state + the wire shape for zero behavior change. The\n * CLIENT-side helpers were renamed `Embed*` so non-chat surfaces (e.g.\n * ticket center) don't have to import a chat-prefixed symbol just to\n * send the same headers.\n *\n * Persists to **`localStorage`** so the bearer token + act-as identity\n * survive tab close, new-tab opens, and browser restarts — the\n * `/debug` paste-creds UI is an admin tool and re-pasting every tab\n * cycle was rejected as a dev-experience tradeoff that wasn't worth\n * the security gain. An XSS sink on this origin can read the value\n * indefinitely (vs only-this-tab with sessionStorage), but `/debug`\n * is admin-gated behind the platform's `askAI.enabled` flag, the key\n * is server-verified (hash-at-rest, platform-bound, revocable in\n * `/admin/api-keys`), and the act-as reach is bounded by the key's\n * tier. Explicit \"Clear\" button on the creds bar is the supported\n * logout path; closing the tab is no longer.\n *\n * Namespaced under `<platform>.chat.proxy-auth.v1` (the storage key is\n * unchanged from the old chat-prefixed helper — that's a storage\n * contract; renaming it would log everyone out).\n */\n\nimport { getAppType } from './app-config';\nimport { createLocalStorageAdapter } from './local-storage-adapter';\n\n/**\n * Every OPTIONAL header of the chat-proxy contract, with the field it is stored\n * in: the ONE list storage, validation and `applyProxyAuth` all loop over, so a\n * new header is one row. `identity` rows are the acting user's display identity\n * (the hub threads them through `resolveChatProxyIdentity`); `visitor` rows are\n * the visitor context a gateway forwards (honoured hub-side only from a SERVICE\n * key). Empty or omitted = not sent.\n */\nexport const EMBED_PROXY_OPTIONAL_HEADERS = [\n { field: 'firstName', group: 'identity', header: 'X-Chat-First-Name' },\n { field: 'lastName', group: 'identity', header: 'X-Chat-Last-Name' },\n { field: 'avatarUrl', group: 'identity', header: 'X-Chat-Avatar-Url' },\n { field: 'originUrl', group: 'visitor', header: 'X-Chat-Origin-Url' },\n { field: 'ip', group: 'visitor', header: 'X-Chat-Ip' },\n { field: 'country', group: 'visitor', header: 'X-Chat-Country' },\n { field: 'visitorId', group: 'visitor', header: 'X-Chat-User-Id' },\n] as const;\n\nexport type EmbedProxyOptionalField = (typeof EMBED_PROXY_OPTIONAL_HEADERS)[number]['field'];\n\nexport type EmbedProxyAuth = {\n secret: string;\n email: string;\n} & Partial<Record<EmbedProxyOptionalField, string>>;\n\nfunction isValidPersistedAuth(value: unknown): value is EmbedProxyAuth {\n if (!value || typeof value !== 'object') return false;\n const v = value as Record<string, unknown>;\n if (\n typeof v.secret !== 'string' ||\n v.secret.trim().length === 0 ||\n typeof v.email !== 'string' ||\n v.email.trim().length === 0\n )\n return false;\n // Optional fields: when present must be strings. Empty string is treated\n // as absent later (in `getEmbedProxyAuth`).\n return EMBED_PROXY_OPTIONAL_HEADERS.every(({ field }) => v[field] == null || typeof v[field] === 'string');\n}\n\nconst adapter = createLocalStorageAdapter<EmbedProxyAuth>({\n // Storage key unchanged from the legacy chat-prefixed helper. Renaming\n // it would silently log every existing admin out — the key is a\n // storage contract, not a code identifier.\n key: 'chat.proxy-auth.v1',\n namespace: () => getAppType(),\n validate: isValidPersistedAuth,\n logTag: '[embed-proxy-auth-storage]',\n // localStorage — survives tab close, new tabs, and browser restarts.\n // Admin re-pasting creds every tab cycle was the dev-experience\n // tradeoff prior `sessionStorage` setup demanded — rejected. See\n // file-level doc comment for the security tradeoff rationale.\n backend: 'local',\n});\n\n/** Trim + null-coerce an optional identity field so consumers can do\n * `auth.firstName ?? ''` without worrying about whitespace-only strings. */\nfunction normalizeOptional(value: string | undefined): string | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n/** Every optional field of `value`, trimmed, blank ones dropped. */\nfunction normalizedOptionalFields(value: Partial<Record<EmbedProxyOptionalField, string>>) {\n const out: Partial<Record<EmbedProxyOptionalField, string>> = {};\n for (const { field } of EMBED_PROXY_OPTIONAL_HEADERS) {\n const normalized = normalizeOptional(value[field]);\n if (normalized) out[field] = normalized;\n }\n return out;\n}\n\n/**\n * Returns full credentials (secret + email + optional identity passthrough)\n * when secret + email are available. Returns `null` when nothing is saved —\n * callers treat that as \"fall back to cookie auth\".\n */\nexport function getEmbedProxyAuth(): EmbedProxyAuth | null {\n const persisted = adapter.load();\n if (!persisted) return null;\n return {\n secret: persisted.secret,\n email: persisted.email.trim().toLowerCase(),\n ...normalizedOptionalFields(persisted),\n };\n}\n\n/**\n * Returns the LAST email the admin saved. The proxy creds bar reads\n * this to pre-fill the email field on mount.\n */\nexport function getPersistedProxyEmail(): string | null {\n const persisted = adapter.load();\n return persisted?.email.trim().toLowerCase() ?? null;\n}\n\n/** Save the proxy creds. Secret + email are required; identity-passthrough\n * fields are persisted only when non-empty. */\nexport function setEmbedProxyAuth(value: EmbedProxyAuth): void {\n adapter.save({\n secret: value.secret,\n email: value.email.trim().toLowerCase(),\n ...normalizedOptionalFields(value),\n });\n}\n\n/** Drop the persisted creds. */\nexport function clearEmbedProxyAuth(): void {\n adapter.clear();\n}\n\n/**\n * Apply the embed-proxy auth (Bearer + X-Chat-Act-As) to a fetch call's\n * URL + headers. Used by every embedded-surface route that needs to\n * identify itself as the proxied customer (chat stream, agent-* routes,\n * ticket-center actions). When proxy auth is absent (regular\n * cookie-session users), returns the inputs unchanged so the cookie-auth\n * path still works.\n *\n * `X-Chat-Act-As` header (vs a URL query param) keeps PII out of access\n * logs, Sentry breadcrumbs, browser history, and CDN analytics.\n */\nexport function applyProxyAuth(\n url: string,\n baseHeaders: Record<string, string> = { 'Content-Type': 'application/json' },\n): { url: string; headers: Record<string, string> } {\n const auth = getEmbedProxyAuth();\n const headers = { ...baseHeaders };\n if (auth?.secret) {\n headers.Authorization = `Bearer ${auth.secret}`;\n }\n if (auth?.email) {\n headers['X-Chat-Act-As'] = auth.email;\n }\n // Optional headers — only attached when present so the server's\n // \"required vs optional\" header shape stays exact.\n for (const { field, header } of EMBED_PROXY_OPTIONAL_HEADERS) {\n const value = auth?.[field];\n if (value) headers[header] = value;\n }\n return { url, headers };\n}\n","'use client';\n\n/**\n * Shared `fetch` wrapper for any embedded surface (chat, ticket center,\n * future widgets) that needs to carry the bearer-act-as identity\n * (proxy `Authorization` + `X-Chat-Act-As` headers from\n * `embed-proxy-auth-storage.ts`).\n *\n * Wire header names are `X-Chat-*` for historical reasons — that's a\n * server contract, not a UI namespace. The wrapper itself is generic.\n *\n * Drop-in replacement for `fetch()` — `Authorization` / `X-Chat-Act-As`\n * are merged into `init.headers` when proxy creds are stashed in\n * sessionStorage, otherwise the call falls through to the cookie-auth\n * path unchanged.\n *\n * Use this for any client-side fetch hitting `/api/chat/*`, `/api/docs/chat/*`,\n * or `/api/storage/generate-upload-url` (chat-attachment surface — shared\n * with the ticket center). Routes that do NOT need bearer-act-as\n * (e.g. `/api/profile/me`) keep using vanilla `fetch`.\n */\n\nimport { applyProxyAuth } from './embed-proxy-auth-storage';\n\n// =============================================================================\n// Host-supplied auth adapter (opt-in)\n// =============================================================================\n\n/**\n * Hosts that have their own auth model (cookie sessions, app-specific\n * JWT in localStorage, OAuth access tokens, …) can register an adapter\n * to override the lib's default `embedProxyAuth` flow. When set, the\n * adapter's `getHeaders()` result is merged onto every `embedAuthedFetch`\n * call AFTER the default proxy-auth header step (so adapter headers\n * win over both caller and proxy values), and `credentials` overrides\n * the default `'same-origin'` behaviour.\n *\n * Default (no adapter): MPH-style proxy-impersonation — bearer + act-as\n * read from localStorage, `credentials: 'same-origin'`. No consumer\n * needs to touch this unless they want a different auth model.\n *\n * Use cases:\n * - openframe-frontend has its own JWT in `localStorage.of_access_token`\n * and cookie-based session; register an adapter to attach the JWT\n * and request `credentials: 'include'` so cookies travel cross-origin\n * to the openframe gateway.\n * - Future embed hosts with OAuth access tokens, signed URLs, etc.\n *\n * Lifetime: setter is module-level (intentionally — `embedAuthedFetch`\n * is a plain utility, not a hook, so it can't read React context). Host\n * runtime providers should call `setEmbedAuthAdapter(...)` on mount and\n * `setEmbedAuthAdapter(null)` on unmount. Multiple hosts registering at\n * once is a programming error (one chat panel per app).\n */\nexport interface EmbedAuthAdapter {\n /** Headers merged onto every embedded-fetch call. Return `{}` to add\n * nothing. Called per-request so reactive token refresh sees the latest\n * value from your auth store / storage. Values typed as\n * `string | undefined` so the common narrowed shape\n * `{ Authorization: token ? 'Bearer …' : undefined }` (or a conditional\n * `token ? { Authorization: … } : {}`) assigns cleanly — `undefined`\n * values are filtered before being merged into the request headers. */\n getHeaders?: () => Record<string, string | undefined>;\n /** `RequestInit.credentials` mode. Default when no adapter: callers'\n * `init.credentials` or `'same-origin'`. Use `'include'` for cookie\n * auth against a different origin (CORS + `SameSite=None` required). */\n credentials?: RequestCredentials;\n /**\n * Optional 401 self-heal. When a request comes back `401`,\n * `embedAuthedFetch` calls this once, and — if it resolves `true` —\n * retries the SAME request exactly once with freshly-recomputed\n * headers (so a rotated bearer from `getHeaders()` is picked up).\n * Resolve `false` to surface the 401 to the caller unchanged.\n *\n * This is the capability the openframe `apiClient` has had all along\n * (refresh-the-access-token-then-retry); registering it here gives the\n * embedded chat/ticket surfaces the same self-healing auth instead of\n * dying on an expired token. Concurrent 401s are de-duplicated by the\n * wrapper, so this fires at most once per refresh cycle even when a\n * stampede of chat requests all expire together — your implementation\n * does NOT need its own in-flight guard (though a token-refresh manager\n * that already dedups is harmless).\n *\n * Keep it idempotent and side-effect-light: on failure the wrapper just\n * returns the original 401 — logout/redirect decisions belong to the\n * host's own auth layer, not to this fetch wrapper.\n */\n refresh?: () => Promise<boolean>;\n /**\n * Exact extra origins (`scheme://host[:port]`, as produced by `URL.origin`)\n * the same-origin guard additionally accepts. The guard normally rejects\n * every cross-origin URL in production — but native-shell hosts serve the\n * page from a local pseudo-origin (`capacitor://localhost`,\n * `tauri://localhost`) with NO server behind it, so the gateway their\n * bearer already belongs to is unavoidably cross-origin. Listing it here\n * is the host's explicit sanction to send the adapter's credentials there.\n * Compared AFTER the http(s)-protocol check, so non-http(s) schemes can\n * never be allowlisted. Omit (or leave empty) everywhere else — same-origin\n * remains the rule, this is the narrow exception.\n */\n allowedOrigins?: string[];\n /**\n * Optional: call `listener` whenever `getHeaders()` starts returning a NEW\n * credential (a rotation the host ran on its own — e.g. a native shell's\n * background refresh). Returns an unsubscribe. Lets a long-lived surface\n * that stopped after a 401 retry as soon as a usable credential exists,\n * instead of on its next backoff step. Fire only on a real change: a\n * listener may respond with a request, and through `refresh` that request\n * can lead the host to notify again.\n */\n subscribe?: (listener: () => void) => () => void;\n}\n\n/**\n * The registered adapter is parked on `globalThis`, NOT in a module-private\n * `let`. Reason: this lib ships multiple entry points (`/utils`,\n * `/components/chat`, …) and a consumer's bundler can inline this file into\n * more than one chunk — giving each chunk its OWN module scope. If the host\n * calls `setEmbedAuthAdapter` from the `/utils` copy while the chat's\n * `embedAuthedFetch` runs from the `/components/chat` copy, a module-local\n * `let` would be set on one copy and read as `null` on the other (the exact\n * \"credentials: same-origin, no Bearer, no refresh\" symptom). A single\n * `globalThis` slot is shared across every copy, so registration always\n * reaches the fetch path.\n */\nconst ADAPTER_GLOBAL_KEY = '__embedAuthedFetchAdapter__';\n\nfunction getRegisteredAuthAdapter(): EmbedAuthAdapter | null {\n if (typeof globalThis === 'undefined') return null;\n return ((globalThis as Record<string, unknown>)[ADAPTER_GLOBAL_KEY] as EmbedAuthAdapter | null) ?? null;\n}\n\nfunction storeRegisteredAuthAdapter(adapter: EmbedAuthAdapter | null): void {\n if (typeof globalThis === 'undefined') return;\n (globalThis as Record<string, unknown>)[ADAPTER_GLOBAL_KEY] = adapter;\n}\n\n/**\n * Register a host-owned auth adapter for `embedAuthedFetch`. Pass `null`\n * to clear (typically on provider unmount).\n *\n * Module-level state — there is one chat panel per app, so a single\n * registration is sufficient. Calling this twice with different non-null\n * adapters replaces the previous one (the most recent registration wins);\n * a `console.warn` flags the overwrite so duplicate-provider mounts get\n * caught in dev.\n */\nexport function setEmbedAuthAdapter(adapter: EmbedAuthAdapter | null): void {\n if (adapter && getRegisteredAuthAdapter() && process.env.NODE_ENV !== 'production') {\n console.warn(\n '[setEmbedAuthAdapter] overwriting a previously-registered auth ' +\n 'adapter. Two chat-runtime providers should not coexist — verify ' +\n 'mount order and pass `null` from the unmounting provider.',\n );\n }\n storeRegisteredAuthAdapter(adapter);\n}\n\n/**\n * Whether a host auth adapter is currently registered. Lets sibling helpers\n * (e.g. `contentFetch`) route through `embedAuthedFetch` ONLY when a host has\n * opted into embedded auth, and stay a plain `fetch` otherwise — so there's a\n * single auth knob (the adapter), not a second content-fetch registration.\n */\nexport function hasEmbedAuthAdapter(): boolean {\n return getRegisteredAuthAdapter() !== null;\n}\n\n/**\n * Subscribe to the registered adapter's credential changes\n * (`EmbedAuthAdapter.subscribe`). A no-op when no adapter is registered, or\n * the adapter doesn't report changes. Reads the adapter once, at call time:\n * an adapter registered (or replaced) after a subscriber starts is not\n * picked up — register it before the surfaces that need it mount.\n */\nexport function subscribeEmbedCredentialChange(listener: () => void): () => void {\n return getRegisteredAuthAdapter()?.subscribe?.(listener) ?? (() => {});\n}\n\n/**\n * Whether `url` is an asset the browser CANNOT load natively because its\n * auth rides in request headers.\n *\n * Native asset loads (`<img src>`, `<track src>`, CSS `background-image`)\n * can't carry custom headers, so a URL this returns `true` for must go\n * through `embedAuthedFetch` → blob object-URL instead (see\n * `useAuthedAssetSrc` / `useAuthedImageSrc`).\n *\n * The single signal is the registered adapter: when it supplies an\n * `Authorization` header, the host authenticates by HEADER, so nothing the\n * browser fetches on its own is authenticated. That holds for BOTH shapes a\n * header-auth host takes:\n *\n * - a cross-origin gateway URL — sanctioned via `allowedOrigins` (native\n * shells, whose page origin is `capacitor://` / `tauri://`);\n * - a same-origin / relative reverse-proxy path (`/content/api/...`) —\n * dev-ticket web, where the token lives in localStorage and there is NO\n * session cookie to fall back on. This case used to be excluded on the\n * assumption that same-origin implies \"cookies work\"; true for a cookie\n * session, false for every header-auth host, which is exactly when this\n * predicate is consulted at all.\n *\n * Everything else loads natively, unchanged: cookie-auth web (the adapter\n * supplies no `Authorization`), no adapter at all (the hub), and third-party\n * origins the bearer does NOT belong to (public images) — those stay out of\n * reach of the token by the same origin rules `embedAuthedFetch` enforces.\n */\nexport function needsBearerAssetFetch(url: string): boolean {\n if (typeof window === 'undefined') return false;\n const adapter = getRegisteredAuthAdapter();\n if (!adapter || adapter.getHeaders?.().Authorization === undefined) return false;\n let target: URL;\n let pageOrigin: string;\n try {\n target = new URL(url, window.location.href);\n // Same reason `assertSameOrigin` derives it this way: test environments\n // mock `window.location` as a plain object with no `origin` field.\n pageOrigin = new URL(window.location.href).origin;\n } catch {\n return false;\n }\n if (target.protocol !== 'http:' && target.protocol !== 'https:') return false;\n if (target.origin === pageOrigin) return true;\n return adapter.allowedOrigins?.includes(target.origin) ?? false;\n}\n\n/**\n * `fetch` wrapper that attaches embed-proxy bearer headers (when\n * present in sessionStorage) and forces `credentials: 'same-origin'`\n * so Supabase auth cookies travel too.\n *\n * **Header merge direction (proxy WINS over caller):** the implementation\n * spreads `baseHeaders` first inside `applyProxyAuth`, then sets the\n * `Authorization` / `X-Chat-*` keys — so the proxy values take precedence\n * over anything the caller passed. The motivation is that the bearer +\n * act-as identity is the source of truth for embedded auth; a caller\n * accidentally passing a stale `Authorization` header should NOT override\n * the live proxy creds.\n *\n * **Cross-origin defense:** the wrapper assumes a same-origin `/api/…`\n * relative URL. Absolute URLs are accepted only when their origin matches\n * the current window's origin — or appears in the registered adapter's\n * `allowedOrigins` (the native-shell hatch; see that field's doc) — and\n * cross-origin URLs otherwise throw before the bearer leaves the page.\n * This is a defense-in-depth guard for future call sites — outside the\n * allowlisted-shell case there is no legitimate cross-origin use of this\n * fetch wrapper.\n *\n * **401 self-heal:** when a registered adapter supplies `refresh`, a `401`\n * response triggers a single token refresh + retry of the same request\n * (see `EmbedAuthAdapter.refresh`). This is the openframe `apiClient`'s\n * refresh-then-retry behaviour, lifted into the lib so embedded surfaces\n * no longer need a host-side `window.fetch` monkey-patch to survive an\n * expired access token mid-chat. With no adapter (or no `refresh`), the\n * 401 passes straight through unchanged.\n */\nexport function embedAuthedFetch(url: string, init: RequestInit = {}): Promise<Response> {\n // Same-origin guard runs SYNCHRONOUSLY (not awaited inside the async\n // helper below) so a bearer-leaking cross-origin URL throws before any\n // promise is created — callers and tests rely on the synchronous throw.\n assertSameOrigin(url);\n\n // `applyProxyAuth` accepts `Record<string, string>`; normalize the\n // caller's headers to that shape ONCE, up front. RequestInit accepts\n // `HeadersInit` which is broader (Headers instance OR array of tuples).\n // We re-derive the per-request headers from this base on every attempt\n // (initial + post-refresh retry) so a rotated bearer is picked up.\n //\n // When the caller passes no headers, fall back to the same default\n // `applyProxyAuth` uses internally — `Content-Type: application/json` —\n // so JSON POSTs keep their content-type when only `embedAuthedFetch(url)`\n // is used at the call site. GET callers that explicitly want no body\n // headers can pass `init.headers = {}` to opt out.\n let baseHeaders: Record<string, string>;\n if (init.headers === undefined) {\n baseHeaders = { 'Content-Type': 'application/json' };\n } else {\n baseHeaders = {};\n if (init.headers instanceof Headers) {\n init.headers.forEach((v, k) => {\n baseHeaders[k] = v;\n });\n } else if (Array.isArray(init.headers)) {\n for (const [k, v] of init.headers) baseHeaders[k] = v;\n } else {\n Object.assign(baseHeaders, init.headers);\n }\n }\n\n return fetchWithRefresh(url, init, baseHeaders, false);\n}\n\n/**\n * Single in-flight refresh shared across all concurrent `embedAuthedFetch`\n * callers. A stampede of chat requests that all 401 at the same moment must\n * trigger the adapter's `refresh()` ONCE, not N times — otherwise an\n * expiring session fires a thundering herd of refresh calls at the auth\n * server. Resets to `null` once settled so the next genuine expiry can\n * refresh again.\n */\n// Stored on `globalThis` rather than a module-local so the \"single refresh\"\n// guarantee survives module duplication. Bundlers can ship more than one copy\n// of this module (e.g. across chunks or a host + embedded build); a per-module\n// variable would let each copy run its own refresh cycle, re-creating the\n// thundering-herd this dedupe exists to prevent.\nconst IN_FLIGHT_REFRESH_GLOBAL_KEY = '__embedAuthedFetchInFlightRefresh__';\n\nfunction getInFlightRefresh(): Promise<boolean> | null {\n if (typeof globalThis === 'undefined') return null;\n return (\n ((globalThis as Record<string, unknown>)[IN_FLIGHT_REFRESH_GLOBAL_KEY] as Promise<boolean> | null | undefined) ??\n null\n );\n}\n\nfunction setInFlightRefresh(refresh: Promise<boolean> | null): void {\n if (typeof globalThis === 'undefined') return;\n (globalThis as Record<string, unknown>)[IN_FLIGHT_REFRESH_GLOBAL_KEY] = refresh;\n}\n\nfunction dedupedRefresh(): Promise<boolean> {\n const adapter = getRegisteredAuthAdapter();\n // Bound and captured BEFORE the closure below. `refresh` is an optional\n // property, so TypeScript drops the `if (!adapter?.refresh)` narrowing inside\n // the `.then()` callback — the property could in principle be reassigned\n // between the guard and the call. `bind` keeps the adapter as the receiver.\n const refresh = adapter?.refresh?.bind(adapter);\n if (!refresh) return Promise.resolve(false);\n let inFlightRefresh = getInFlightRefresh();\n if (!inFlightRefresh) {\n // Wrap in `Promise.resolve` so an adapter that throws synchronously\n // (rather than rejecting) still funnels through the shared slot and\n // clears it. A rejected refresh is treated as \"could not refresh\".\n inFlightRefresh = Promise.resolve()\n .then(() => refresh())\n .catch(() => false)\n .finally(() => {\n setInFlightRefresh(null);\n });\n setInFlightRefresh(inFlightRefresh);\n }\n return inFlightRefresh;\n}\n\n/**\n * Core fetch path: merge proxy-auth + adapter headers, issue the request,\n * and — on a `401` with a refresh-capable adapter — refresh once and retry\n * the identical request a single time. Mirrors the openframe `apiClient`'s\n * refresh-then-retry contract (`isRetry` guards against infinite loops).\n */\nasync function fetchWithRefresh(\n url: string,\n init: RequestInit,\n baseHeaders: Record<string, string>,\n isRetry: boolean,\n): Promise<Response> {\n // Re-run the merge each attempt: `applyProxyAuth` reads the latest stored\n // proxy creds and `getHeaders()` reads the latest bearer, so a retry after\n // refresh carries the rotated token rather than the stale one. `{...baseHeaders}`\n // keeps the caller's normalized headers immutable across attempts.\n const { url: authedUrl, headers } = applyProxyAuth(url, { ...baseHeaders });\n\n // Host-supplied auth adapter layer. Runs AFTER the proxy-auth merge so\n // adapter headers override both caller and proxy values — the adapter\n // is the host's explicit \"this is my auth model\" override, intentionally\n // last-writer-wins. When no adapter is registered, this is a zero-cost\n // no-op (object spread of `{}`).\n const adapter = getRegisteredAuthAdapter();\n if (adapter?.getHeaders) {\n // Filter `undefined` values — the adapter type allows them so consumers\n // don't have to narrow `{ Authorization: token ? '…' : undefined }`-shaped\n // returns, but `fetch` headers must be strings.\n for (const [k, v] of Object.entries(adapter.getHeaders())) {\n if (v !== undefined) headers[k] = v;\n }\n }\n const credentials = adapter?.credentials ?? init.credentials ?? 'same-origin';\n\n const response = await fetch(authedUrl, {\n ...init,\n headers,\n // Default `same-origin` carries Supabase cookies for the MPH proxy-\n // auth model. Hosts on different origins (openframe-frontend ↔\n // openframe gateway) register `credentials: 'include'` via the\n // adapter to make their own cookies travel cross-origin (CORS +\n // `SameSite=None` must be configured server-side for that to work).\n credentials,\n });\n\n // 401 self-heal: refresh the token once and retry. Only when an adapter\n // opted into `refresh`, and only on the first attempt — a 401 on the\n // retry means the fresh token is also unauthorized, so surface it.\n if (response.status === 401 && !isRetry && adapter?.refresh) {\n const refreshed = await dedupedRefresh();\n if (refreshed) {\n return fetchWithRefresh(url, init, baseHeaders, true);\n }\n }\n\n return response;\n}\n\n/**\n * Reject any URL that resolves to a cross-origin destination or to a\n * non-http(s) scheme. Every input is resolved against\n * `window.location.href` so the same rule covers path-only\n * (`/api/...`), absolute (`https://...`), protocol-relative\n * (`//host/...`), AND whitespace-prefixed forms (`\\t//evil.com/...`) —\n * the WHATWG fetch spec strips leading ASCII whitespace before\n * parsing, so any regex-based \"skip relative\" shortcut is bypassable\n * with a leading `\\t`/`\\n`/`\\r`/space. We resolve unconditionally\n * instead and compare origins.\n *\n * Also blocks `javascript:` / `data:` / `blob:` etc. — only `http(s):`\n * is allowed. This is explicit allowlisting rather than relying on\n * `origin === 'null'` to fall out wrong.\n *\n * Server-side rendering: when `typeof window === 'undefined'` we skip\n * the check — the bearer comes from sessionStorage which doesn't exist\n * on the server, so there's nothing to leak anyway.\n */\nfunction assertSameOrigin(url: string): void {\n if (typeof window === 'undefined') return;\n let target: URL;\n let pageOrigin: string;\n try {\n target = new URL(url, window.location.href);\n // Derive the page origin from `href` rather than reading\n // `window.location.origin` directly so the check works in test\n // environments that mock `window.location` to a plain object\n // without an `origin` field (jsdom setups do this).\n pageOrigin = new URL(window.location.href).origin;\n } catch {\n throw new Error(`embedAuthedFetch: refusing to fetch malformed URL (${JSON.stringify(url)})`);\n }\n if (target.protocol !== 'http:' && target.protocol !== 'https:') {\n throw new Error(\n `embedAuthedFetch: refusing non-http(s) URL (${target.protocol}) — pass a relative /api/* path instead`,\n );\n }\n if (target.origin !== pageOrigin) {\n // Host-sanctioned cross-origin target (`EmbedAuthAdapter.allowedOrigins`).\n // Native shells serve the page from a local pseudo-origin with no server\n // behind it, so their gateway — the same backend the host already trusts\n // with this bearer — is unavoidably cross-origin. Exact `URL.origin`\n // match, and only reachable for http(s) targets (checked above).\n if (getRegisteredAuthAdapter()?.allowedOrigins?.includes(target.origin)) {\n return;\n }\n // Dev-mode escape hatch — embedded apps (e.g. openframe-frontend)\n // run on a different origin from their gateway during local dev,\n // and forcing a Next.js `rewrites()` workaround is more error-prone\n // than relaxing the guard for the dev build. In production\n // (`NODE_ENV === 'production'`) the guard stays absolute — same\n // defense-in-depth bearer-leak protection as before. The check is\n // baked at build time by Next/webpack/Turbopack so prod bundles\n // contain only the throwing branch (no dev string in the artifact).\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[embedAuthedFetch] cross-origin fetch to ${target.origin} ` +\n `allowed in dev (NODE_ENV !== 'production'). Production builds ` +\n `will reject this — wire a same-origin proxy before shipping.`,\n );\n return;\n }\n throw new Error(\n `embedAuthedFetch: refusing cross-origin fetch to ${target.origin} — pass a relative /api/* path instead`,\n );\n }\n}\n","'use client';\n\nimport { useEffect, useReducer } from 'react';\nimport { embedAuthedFetch, needsBearerAssetFetch } from '../utils/embed-authed-fetch';\n\n/**\n * Resolve ANY asset URL the browser would load natively — `<img src>`,\n * `<track src>`, a font, a poster — for hosts whose auth rides in request\n * HEADERS instead of cookies: native shells (`capacitor://`, `tauri://`) and\n * dev-ticket web, both in bearer mode. A native subresource load can't carry\n * an `Authorization` header, so a gateway URL that works on the cookie-auth\n * web 401s there.\n *\n * When `needsBearerAssetFetch` says the URL is bearer-authed, this hook\n * fetches it through `embedAuthedFetch` (bearer + deduped 401-refresh-retry +\n * the adapter's cross-origin guard — the SAME single auth knob every embedded\n * `fetch` in the lib rides) and returns a blob object-URL. Every other URL is\n * returned unchanged, so on the cookie-auth web the hook is a pass-through.\n *\n * Returns `undefined` while the fetch is in flight OR after it fails, so\n * callers show their existing placeholder / \"no captions\" branch instead of a\n * broken asset; a failed fetch retries on the next mount.\n *\n * Cache: module-level, session-lifetime, keyed by full URL — so the two\n * `<track>` URLs of one player, or the same avatar in twenty rows, cost one\n * request. Gateway URLs carry a `?v=<content-hash>` cache-buster, so changed\n * content is a new key; entries are never revoked (a VTT / avatar is a few KB\n * — refcount churn isn't worth it) and are dropped wholesale at session end\n * via `clearAuthedAssetCache`.\n */\n\nconst resolvedCache = new Map<string, string>();\nconst inFlight = new Map<string, Promise<void>>();\nlet cacheGeneration = 0;\n\n/**\n * Drop every cached blob and revoke its object URL. Hosts call this at\n * session end (logout / forced re-login) so a follow-on login as a\n * different identity cannot be served blobs fetched under the previous\n * one's bearer. Fetches still in flight when the clear happens are fenced\n * by a generation counter — their results are revoked instead of cached.\n */\nexport function clearAuthedAssetCache(): void {\n cacheGeneration++;\n for (const url of resolvedCache.values()) URL.revokeObjectURL(url);\n resolvedCache.clear();\n inFlight.clear();\n}\n\nfunction fetchAsBlobUrl(src: string, accept: string): Promise<void> {\n let pending = inFlight.get(src);\n if (!pending) {\n const startedGeneration = cacheGeneration;\n pending = embedAuthedFetch(src, { headers: { Accept: accept } })\n .then(async response => {\n if (!response.ok) throw new Error(`asset fetch failed: ${response.status}`);\n const blobUrl = URL.createObjectURL(await response.blob());\n if (startedGeneration === cacheGeneration) {\n resolvedCache.set(src, blobUrl);\n } else {\n URL.revokeObjectURL(blobUrl);\n }\n })\n .finally(() => {\n if (inFlight.get(src) === pending) inFlight.delete(src);\n });\n inFlight.set(src, pending);\n }\n return pending;\n}\n\n/**\n * @param src the asset URL (relative or absolute), or null/undefined\n * @param accept `Accept` header for the authed fetch — narrow it to what the\n * consumer renders (`image/*`, `text/vtt`); it also replaces\n * `embedAuthedFetch`'s default JSON `Content-Type` on the GET.\n */\nexport function useAuthedAssetSrc(src?: string | null, accept = '*/*'): string | undefined {\n const bearerSrc = src && needsBearerAssetFetch(src) ? src : null;\n const [, rerender] = useReducer((c: number) => c + 1, 0);\n\n useEffect(() => {\n if (!bearerSrc || resolvedCache.has(bearerSrc)) return undefined;\n let cancelled = false;\n fetchAsBlobUrl(bearerSrc, accept)\n .catch(() => {})\n .finally(() => {\n if (!cancelled) rerender();\n });\n return () => {\n cancelled = true;\n };\n }, [bearerSrc, accept]);\n\n if (!src) return undefined;\n if (!bearerSrc) return src;\n return resolvedCache.get(bearerSrc);\n}\n","'use client';\n\nimport { clearAuthedAssetCache, useAuthedAssetSrc } from './use-authed-asset-src';\n\n/**\n * Image flavor of `useAuthedAssetSrc` — see that hook for the full contract.\n * Kept as its own export because it is the lib's most-used consumer (avatars,\n * entity images, markdown `<img>`) and because hosts already import this name.\n *\n * Distinct from the legacy `useAuthenticatedImage`, which ALWAYS blob-fetches\n * with `credentials: 'include'` and its own global config. This hook is\n * pass-through-first and keys off the single auth knob the lib already has:\n * the registered `EmbedAuthAdapter`.\n */\nexport function useAuthedImageSrc(src?: string | null): string | undefined {\n return useAuthedAssetSrc(src, 'image/*');\n}\n\n/**\n * Session-end cache drop. Now an alias of `clearAuthedAssetCache` — one cache\n * backs every authed asset (images AND caption tracks), so hosts keep calling\n * this single function on logout / forced re-login.\n */\nexport const clearAuthedImageCache = clearAuthedAssetCache;\n","/**\n * Branded OG-placeholder URL construction — the DEFAULT cover-image fallback\n * for entity cards (onboarding guides, blog/case-study/release/etc.) that have\n * no image.\n *\n * ALL of the logic lives here, in the lib. A consumer hands over its runtime\n * `endpoints` object and NOTHING else — base resolution AND the `?title=…`\n * concatenation happen inside `buildOgPlaceholderUrl`. For the entity-card +\n * onboarding-detail surfaces this is the single entry point — no card builds an\n * og-placeholder URL itself (that was the bug this replaced: each embedder\n * wired a per-surface callback that concatenated the URL, and a host that\n * forgot it rendered a blank slot with no request). (Other server-side OG paths\n * such as the hub's blog og:image generator construct their own URLs and are\n * out of this module's scope.)\n *\n * The base API URL is taken from the endpoints the host already configures:\n * 1. explicit `endpoints.ogPlaceholderUrl`\n * 2. derived from the sibling `endpoints.imageProxyUrlPrefix` (same API base)\n * 3. same-origin relative `/api/og-placeholder`\n * The base may already carry pre-existing query params; they're preserved and\n * `title` (+ dimensions) are layered on top. Per-platform brand colors are NOT\n * baked into this URL — the `/api/og-placeholder` route resolves them\n * server-side from the platform. Most hosts leave `ogPlaceholderUrl` unset and\n * let the base derive from `imageProxyUrlPrefix`.\n */\n\n/** The slice of `ChatRuntime.endpoints` this module needs. */\nexport interface OgPlaceholderEndpoints {\n /** Explicit base URL for the og-placeholder route. May already carry query\n * params — they're preserved. Per-platform colors are NOT baked here; the\n * route resolves them server-side from the platform. */\n ogPlaceholderUrl?: string;\n /** Sibling image route under the SAME API base. When `ogPlaceholderUrl` is\n * unset, the base is derived from this by swapping the trailing\n * `/image-proxy` segment for `/og-placeholder` — so a host that already\n * proxies images gets the placeholder for free, with zero extra wiring. */\n imageProxyUrlPrefix?: string;\n}\n\nexport interface BuildOgPlaceholderOptions {\n /** Site name shown under the title. Skipped when empty. */\n site?: string;\n /** `'wide'` (1200×630, the route default — no `w`/`h` emitted) or\n * `'square'` (1024×1024, for compact 56×56 chat-inline slots). */\n aspect?: 'wide' | 'square';\n /** Explicit pixel overrides (win over `aspect`). */\n width?: number;\n height?: number;\n}\n\n/** Same-origin default — for hosts that serve the route themselves (the hub).\n * Cross-origin embedders override via `ogPlaceholderUrl` or inherit it from\n * `imageProxyUrlPrefix`. */\nconst DEFAULT_OG_PLACEHOLDER_PATH = '/api/og-placeholder';\n\n/** Resolve the og-placeholder route base from the host's endpoints.\n * Internal — callers go through `buildOgPlaceholderUrl(endpoints, …)`. */\nfunction resolveOgPlaceholderBase(endpoints?: OgPlaceholderEndpoints | null): string {\n if (endpoints?.ogPlaceholderUrl) return endpoints.ogPlaceholderUrl;\n const imageProxy = endpoints?.imageProxyUrlPrefix;\n if (imageProxy) {\n // `/image-proxy` and `/og-placeholder` are sibling API routes under one\n // base. Anchor to a path-segment boundary so we only rewrite the route\n // name, never an incidental substring.\n const derived = imageProxy.replace(/\\/image-proxy(?=$|[?/])/, '/og-placeholder');\n if (derived !== imageProxy) return derived;\n }\n return DEFAULT_OG_PLACEHOLDER_PATH;\n}\n\n/**\n * Build the branded og-placeholder image URL from the host's `endpoints` + a\n * title. This is the single entry point for entity-card + onboarding-detail\n * cover fallbacks: it resolves the route base from `endpoints` AND concatenates\n * `title` (+ dimensions), so those consumers never construct a URL themselves.\n *\n * Pure string construction — SSR- and browser-safe. Always returns a usable\n * URL (relative default at worst), so a missing/unknown image degrades\n * gracefully via the `<img onError>` recovery in the card components.\n */\nexport function buildOgPlaceholderUrl(\n endpoints: OgPlaceholderEndpoints | null | undefined,\n title: string,\n options: BuildOgPlaceholderOptions = {},\n): string {\n const base = resolveOgPlaceholderBase(endpoints);\n const qIndex = base.indexOf('?');\n const path = qIndex === -1 ? base : base.slice(0, qIndex);\n const params = new URLSearchParams(qIndex === -1 ? '' : base.slice(qIndex + 1));\n\n params.set('title', title);\n if (options.site) params.set('site', options.site);\n\n // Square aspect → request a 1024×1024 image so `object-cover` doesn't crop\n // the title off in compact slots. Wide leaves dimensions to the route\n // default (1200×630). Explicit width/height always win.\n const width = options.width ?? (options.aspect === 'square' ? 1024 : undefined);\n const height = options.height ?? (options.aspect === 'square' ? 1024 : undefined);\n // `Number.isFinite` does not coerce, so it already rejects `undefined` — no\n // separate `typeof === 'number'` guard needed.\n if (Number.isFinite(width)) params.set('w', String(width));\n if (Number.isFinite(height)) params.set('h', String(height));\n\n return `${path}?${params.toString()}`;\n}\n","'use client';\n\nimport { useMemo } from 'react';\n\nimport { useChatRuntime } from '../contexts/chat-runtime-context';\nimport { buildOgPlaceholderUrl } from '../utils/og-placeholder';\n\n/**\n * Resolve a branded og-placeholder image URL for a title, driven entirely by\n * the runtime `endpoints` (no injected builder).\n *\n * THE one og-placeholder hook. It reads the host's `endpoints` from\n * `ChatRuntime` and hands them to `buildOgPlaceholderUrl`, which resolves the\n * route base (explicit `ogPlaceholderUrl` → derived from `imageProxyUrlPrefix`\n * → same-origin `/api/og-placeholder`) and appends `?title=…`. Per-platform\n * brand colors are resolved SERVER-SIDE by the route — nothing is baked here.\n *\n * Replaces the old builder-injection `useOgPlaceholder(buildUrl, …)`: callers\n * no longer pass a URL builder. `useEntityCardPlaceholder` delegates here too,\n * so every surface shares one memo + one code path.\n */\nexport interface UseOgPlaceholderUrlArgs {\n /** Text to display on the placeholder. */\n title: string | undefined | null;\n /** Site name shown below the title (optional). */\n siteName?: string;\n /** `'wide'` (1200×630 social-card; default) or `'square'` (1024×1024 — for\n * compact chat-inline slots so `object-cover` doesn't crop the title off). */\n aspect?: 'wide' | 'square';\n /** When `false`, returns `null` instead of a URL. */\n enabled?: boolean;\n}\n\nexport function useOgPlaceholderUrl({\n title,\n siteName = '',\n aspect = 'wide',\n enabled = true,\n}: UseOgPlaceholderUrlArgs): string | null {\n const endpoints = useChatRuntime()?.endpoints;\n\n return useMemo(() => {\n if (!enabled || !title) return null;\n return buildOgPlaceholderUrl(endpoints, title, { site: siteName || undefined, aspect });\n }, [endpoints, title, siteName, aspect, enabled]);\n}\n"],"mappings":"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","names":["r","g","b","adapter","useEffect","useEffect"]}