six-degrees 0.4.10 → 0.5.0

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 (414) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +9 -9
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  50. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
  60. package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
  61. package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
  62. package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
  63. package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
  64. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
  65. package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
  66. package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
  67. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/api/social/route.js +4 -3
  69. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  71. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  74. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  76. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/import.html +1 -1
  81. package/.next/standalone/.next/server/app/import.rsc +15 -15
  82. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  83. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  84. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
  85. package/.next/standalone/.next/server/app/index.html +1 -1
  86. package/.next/standalone/.next/server/app/index.rsc +15 -15
  87. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  88. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  89. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  90. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/launch.html +2 -2
  93. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  94. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  95. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
  97. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/paths.html +1 -1
  102. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  103. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  104. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  105. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
  106. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/profile.html +1 -1
  109. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  110. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  111. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
  113. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/queue.html +1 -1
  116. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  117. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  118. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  119. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
  120. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/scores.html +1 -1
  123. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  124. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  125. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
  127. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  128. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  129. package/.next/standalone/.next/server/app/settings.html +1 -1
  130. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  131. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  132. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  133. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
  134. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  135. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  136. package/.next/standalone/.next/server/app/setup.html +1 -1
  137. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  138. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  139. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  140. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
  141. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  143. package/.next/standalone/.next/server/app/social.html +1 -1
  144. package/.next/standalone/.next/server/app/social.rsc +15 -15
  145. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  146. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  147. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
  148. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  149. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  156. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  159. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
  162. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  165. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  166. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  167. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  168. package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  176. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  178. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  196. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  198. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  199. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  200. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  201. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  202. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  203. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
  204. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  205. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  206. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
  207. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  208. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  209. package/.next/standalone/.next/server/pages/404.html +1 -1
  210. package/.next/standalone/.next/server/pages/500.html +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  212. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  213. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  214. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  215. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  216. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  217. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  218. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  219. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  220. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  221. package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
  222. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  223. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  224. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  225. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  226. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  228. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  229. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  230. package/.next/standalone/CHANGELOG.md +235 -0
  231. package/.next/standalone/app/api/data/export/route.js +2 -1
  232. package/.next/standalone/app/api/ingest/route.js +40 -1
  233. package/.next/standalone/app/api/scraper/route.js +12 -2
  234. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  235. package/.next/standalone/app/api/social/crm/route.js +37 -0
  236. package/.next/standalone/app/api/social/messages/route.js +44 -8
  237. package/.next/standalone/app/api/social/route.js +80 -22
  238. package/.next/standalone/app/components/AppTabs.js +59 -0
  239. package/.next/standalone/app/components/ChainView.js +303 -89
  240. package/.next/standalone/app/components/CirclePanel.js +162 -0
  241. package/.next/standalone/app/components/CompanyScores.js +2 -2
  242. package/.next/standalone/app/components/FieldStep.js +4 -4
  243. package/.next/standalone/app/components/FilterPanel.js +121 -183
  244. package/.next/standalone/app/components/ForceGraph.js +41 -3
  245. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  246. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  247. package/.next/standalone/app/components/MapControls.js +154 -0
  248. package/.next/standalone/app/components/NoteDetail.js +118 -0
  249. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  250. package/.next/standalone/app/components/PeopleMap.js +308 -0
  251. package/.next/standalone/app/components/ScanRadar.js +129 -0
  252. package/.next/standalone/app/components/ScanStatusBar.js +72 -27
  253. package/.next/standalone/app/components/SeparationView.js +626 -118
  254. package/.next/standalone/app/components/Sidebar.js +28 -2
  255. package/.next/standalone/app/components/settings/DataSection.js +12 -3
  256. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  257. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  258. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  259. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  260. package/.next/standalone/app/components/views.js +15 -14
  261. package/.next/standalone/app/globals.css +11 -0
  262. package/.next/standalone/app/page.js +89 -156
  263. package/.next/standalone/app/paths/page.js +39 -27
  264. package/.next/standalone/app/profile/page.js +33 -5
  265. package/.next/standalone/app/queue/page.js +56 -34
  266. package/.next/standalone/app/scores/page.js +16 -66
  267. package/.next/standalone/app/settings/page.js +27 -13
  268. package/.next/standalone/app/setup/page.js +298 -205
  269. package/.next/standalone/app/social/Crm.js +683 -0
  270. package/.next/standalone/app/social/SocialHub.js +374 -0
  271. package/.next/standalone/app/social/Thread.js +90 -0
  272. package/.next/standalone/app/social/page.js +10 -351
  273. package/.next/standalone/lib/chain-layout.js +105 -35
  274. package/.next/standalone/lib/data-export.js +10 -4
  275. package/.next/standalone/lib/data-folder.js +9 -0
  276. package/.next/standalone/lib/data-import.js +13 -3
  277. package/.next/standalone/lib/degrees.js +0 -6
  278. package/.next/standalone/lib/dot-rings.js +25 -0
  279. package/.next/standalone/lib/galaxy-lab.js +65 -6
  280. package/.next/standalone/lib/ingest.js +2 -0
  281. package/.next/standalone/lib/island.js +40 -0
  282. package/.next/standalone/lib/linkedin-export.js +267 -46
  283. package/.next/standalone/lib/linkedin-limits.js +6 -2
  284. package/.next/standalone/lib/map-heat.js +20 -0
  285. package/.next/standalone/lib/network-health.js +1 -1
  286. package/.next/standalone/lib/notifications.js +121 -0
  287. package/.next/standalone/lib/people-map.js +82 -0
  288. package/.next/standalone/lib/rarity.js +45 -3
  289. package/.next/standalone/lib/reach.js +7 -6
  290. package/.next/standalone/lib/rpc.js +15 -4
  291. package/.next/standalone/lib/scan-pace.js +46 -0
  292. package/.next/standalone/lib/scoring.js +39 -8
  293. package/.next/standalone/lib/scraper-client.js +19 -6
  294. package/.next/standalone/lib/sector-focus.js +4 -4
  295. package/.next/standalone/lib/separation.js +137 -10
  296. package/.next/standalone/lib/settings-effects.js +20 -11
  297. package/.next/standalone/lib/settings.js +3 -0
  298. package/.next/standalone/lib/social-crm.js +415 -0
  299. package/.next/standalone/lib/social-store.js +13 -6
  300. package/.next/standalone/lib/tier-grid.js +99 -0
  301. package/.next/standalone/lib/title-ranking.js +83 -0
  302. package/.next/standalone/package-lock.json +2 -2
  303. package/.next/standalone/package.json +1 -1
  304. package/.next/standalone/scripts/scrape.py +177 -35
  305. package/.next/static/chunks/03bziijw5srol.js +1 -0
  306. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  307. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  308. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  309. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  310. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  311. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  312. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  313. package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
  314. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  315. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  316. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  317. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  318. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  319. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  320. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  321. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  322. package/CHANGELOG.md +235 -0
  323. package/lib/chain-layout.js +105 -35
  324. package/lib/data-export.js +10 -4
  325. package/lib/data-folder.js +9 -0
  326. package/lib/data-import.js +13 -3
  327. package/lib/degrees.js +0 -6
  328. package/lib/dot-rings.js +25 -0
  329. package/lib/galaxy-lab.js +65 -6
  330. package/lib/ingest.js +2 -0
  331. package/lib/island.js +40 -0
  332. package/lib/linkedin-export.js +267 -46
  333. package/lib/linkedin-limits.js +6 -2
  334. package/lib/map-heat.js +20 -0
  335. package/lib/network-health.js +1 -1
  336. package/lib/notifications.js +121 -0
  337. package/lib/people-map.js +82 -0
  338. package/lib/rarity.js +45 -3
  339. package/lib/reach.js +7 -6
  340. package/lib/rpc.js +15 -4
  341. package/lib/scan-pace.js +46 -0
  342. package/lib/scoring.js +39 -8
  343. package/lib/scraper-client.js +19 -6
  344. package/lib/sector-focus.js +4 -4
  345. package/lib/separation.js +137 -10
  346. package/lib/settings-effects.js +20 -11
  347. package/lib/settings.js +3 -0
  348. package/lib/social-crm.js +415 -0
  349. package/lib/social-store.js +13 -6
  350. package/lib/tier-grid.js +99 -0
  351. package/lib/title-ranking.js +83 -0
  352. package/package.json +1 -1
  353. package/scripts/scrape.py +177 -35
  354. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
  355. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  356. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  357. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
  358. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  359. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  360. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  361. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  362. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  363. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  364. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  365. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  366. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  367. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  368. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  369. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  370. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  371. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  372. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  375. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  376. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  378. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  379. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
  380. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  381. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  382. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  383. package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  384. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  385. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  386. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  387. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  388. package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
  389. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  390. package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  391. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  392. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  393. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  394. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  395. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  396. package/.next/standalone/app/social/Conversations.js +0 -246
  397. package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  398. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  399. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  400. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  401. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  402. package/.next/static/chunks/206rm6j42gl_a.js +0 -1
  403. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  404. package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  405. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  406. package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  407. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  408. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  409. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  410. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  411. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  412. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  413. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  414. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
@@ -0,0 +1,308 @@
1
+ 'use client';
2
+
3
+ // Paths → People: the Paths map with people in place of companies (Blake,
4
+ // 2026-10-02, mock-up 3b). One bubble per connection of yours, sized by the
5
+ // cluster behind them, coloured by their sector and grouped like the company
6
+ // map; a white centre for the share of their cluster you can only reach
7
+ // through them, a gold ring for the S tier inside, and a line between two
8
+ // connections whose clusters share people. Point at one for their numbers;
9
+ // click to open their cluster in Bridge Chains.
10
+ //
11
+ // Laid out once per filter change and drawn as plain SVG, as the company map
12
+ // is (TRAPS §29). The numbers come from lib/people-map.js.
13
+
14
+ import { useMemo, useRef, useState } from 'react';
15
+ import { useRouter } from 'next/navigation';
16
+ import { forceCollide, forceLink, forceManyBody, forceSimulation, forceX, forceY } from 'd3';
17
+ import { peopleMap } from '../../lib/people-map';
18
+ import { companyOf, industryOf, industryByKey, INDUSTRIES, UNKNOWN_INDUSTRY } from '../../lib/companies';
19
+ import { jitter, Seg, useSize, SidePanel, panelHeading } from './PathsAnalyzer';
20
+ import { TierGrid } from './FilterPanel';
21
+ import { useLivePhysics, HeatDefs, HeatGlow, PHYSICS_OFF } from './MapControls';
22
+ import { makeGrid, shows, gridCounts } from '../../lib/tier-grid';
23
+ import { heatBy, heatColour } from '../../lib/galaxy-lab';
24
+ import { clusterStrength } from '../../lib/map-heat';
25
+
26
+ const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
27
+ const LINE = '1px solid rgba(255,255,255,0.1)';
28
+ const UNSCANNED = '#3a3f55';
29
+ const MAX_BUBBLES = 400;
30
+ const ALL_INDUSTRIES = [...INDUSTRIES, UNKNOWN_INDUSTRY];
31
+ const fmt = (n) => Number(n || 0).toLocaleString('en-US');
32
+
33
+ // What a bubble's size follows.
34
+ const SIZE_BY = {
35
+ value: { label: 'Cluster value', of: (p) => p.value, k: 2 },
36
+ size: { label: 'Cluster size', of: (p) => p.size, k: 2.6 },
37
+ s: { label: 'S tier inside', of: (p) => p.S, k: 6 },
38
+ };
39
+
40
+ /**
41
+ * `top` (Paths' "Bubbles are" switch), `look` (its Colour and Physics) and the
42
+ * `colourBy` and `physics` they set come from PathsAnalyzer, shared with the company map.
43
+ */
44
+ export default function PeopleMap({ d1 = [], d2 = [], top = null, look = null, colourBy = 'sector', physics = PHYSICS_OFF }) {
45
+ const router = useRouter();
46
+ const [sizeBy, setSizeBy] = useState('value');
47
+ const [who, setWho] = useState('scanned'); // 'scanned' | 'all'
48
+ // Their own tier, on the same grid as Network Circle; everyone here is 1st degree.
49
+ const [grid, setGrid] = useState(() => makeGrid([1]));
50
+ const [hidden, setHidden] = useState(() => new Set());
51
+ const [query, setQuery] = useState('');
52
+ const [panelOpen, setPanelOpen] = useState(true);
53
+ const [labels, setLabels] = useState('top'); // 'top' | 'all' | 'none'
54
+
55
+ const model = useMemo(() => peopleMap(d1, d2), [d1, d2]);
56
+ const counts = useMemo(() => gridCounts({ 1: model.people.filter((p) => who === 'all' || p.size > 0).map((p) => p.person) }), [model, who]);
57
+ // Each connection's sector, from where they work (inferred, as on the company map).
58
+ const sectorOf = useMemo(() => {
59
+ const m = new Map();
60
+ for (const p of model.people) m.set(p.id, industryOf(companyOf(p.person), p.person.headline));
61
+ return m;
62
+ }, [model]);
63
+ const scannedCount = useMemo(() => model.people.filter((p) => p.size > 0).length, [model]);
64
+
65
+ const shown = useMemo(() => {
66
+ const q = query.trim().toLowerCase();
67
+ return model.people.filter((p) => (who === 'all' || p.size > 0)
68
+ && shows(grid, p.person.tier, 1)
69
+ && !hidden.has(sectorOf.get(p.id).key)
70
+ && (!q || String(p.person.name || '').toLowerCase().includes(q)))
71
+ .sort((a, b) => b.value - a.value || b.size - a.size);
72
+ }, [model, who, grid, hidden, query, sectorOf]);
73
+
74
+ const sectors = useMemo(() => {
75
+ const counts = new Map();
76
+ for (const p of model.people) {
77
+ if (who !== 'all' && !p.size) continue;
78
+ const k = sectorOf.get(p.id).key;
79
+ counts.set(k, (counts.get(k) || 0) + 1);
80
+ }
81
+ return ALL_INDUSTRIES.filter((i) => counts.get(i.key)).map((i) => ({ ...i, count: counts.get(i.key) })).sort((a, b) => b.count - a.count);
82
+ }, [model, who, sectorOf]);
83
+
84
+ return (
85
+ <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
86
+ <SidePanel open={panelOpen} onToggle={() => setPanelOpen((o) => !o)}>
87
+ {top}
88
+ <div style={panelHeading}>Filter</div>
89
+ <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Find a person"
90
+ style={{ width: '100%', boxSizing: 'border-box', padding: '6px 10px', borderRadius: 7, border: LINE, background: 'rgba(255,255,255,0.05)', color: '#fff', fontSize: 13, marginBottom: 8 }} />
91
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
92
+ <Seg value={who} onChange={setWho} options={[['scanned', `Scanned (${fmt(scannedCount)})`], ['all', `All (${fmt(model.people.length)})`]]} />
93
+ </div>
94
+ <div style={{ marginTop: 12 }}>
95
+ <TierGrid grid={grid} counts={counts} onChange={setGrid} mode="paths" />
96
+ </div>
97
+ <div style={panelHeading}>Sectors</div>
98
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
99
+ {sectors.map((i) => {
100
+ const off = hidden.has(i.key);
101
+ return (
102
+ <button key={i.key} title={off ? 'Show on the map' : 'Hide from the map'}
103
+ onClick={() => setHidden((h) => { const n = new Set(h); if (n.has(i.key)) n.delete(i.key); else n.add(i.key); return n; })}
104
+ style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '3px 10px', borderRadius: 14, cursor: 'pointer',
105
+ border: `1px solid ${i.color}55`, background: off ? 'transparent' : `${i.color}14`, color: '#cfd8d8', fontSize: 11.5, opacity: off ? 0.4 : 1 }}>
106
+ <span style={{ width: 7, height: 7, borderRadius: '50%', background: i.color }} />
107
+ {i.label} <span style={{ color: '#778' }}>{fmt(i.count)}</span>
108
+ </button>
109
+ );
110
+ })}
111
+ </div>
112
+ <div style={{ fontSize: 10.5, color: '#667', marginTop: 6 }}>Inferred from where each connection works.</div>
113
+ <div style={panelHeading}>Read the map</div>
114
+ <div style={{ fontSize: 11, color: '#8b9a9a', marginBottom: 4 }}>Bubble size</div>
115
+ <Seg value={sizeBy} onChange={setSizeBy} options={Object.entries(SIZE_BY).map(([k, v]) => [k, v.label])} />
116
+ <div style={{ fontSize: 11, color: '#8b9a9a', margin: '10px 0 4px' }}>Names</div>
117
+ <Seg value={labels} onChange={setLabels} options={[['top', 'Biggest'], ['all', 'All'], ['none', 'None']]} />
118
+ {look}
119
+ </SidePanel>
120
+ {/* Room at the top for the notch's Map / Companies, so it never covers a label */}
121
+ <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', paddingTop: 40, boxSizing: 'border-box' }}>
122
+ <Bubbles people={shown} links={model.links} sizeBy={SIZE_BY[sizeBy]} sectorOf={sectorOf} rank={model.rank} names={labels}
123
+ colourBy={colourBy} physics={physics}
124
+ onOpen={(id) => router.push(`/?chain=${encodeURIComponent(id)}`)} />
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ function Bubbles({ people, links, sizeBy, sectorOf, rank, onOpen, names = 'top', colourBy = 'sector', physics = PHYSICS_OFF }) {
131
+ const wrap = useRef(null);
132
+ const { w, h } = useSize(wrap);
133
+ const [hover, setHover] = useState(null);
134
+
135
+ // Sectors on a ring, each person pulled to their sector and to the people
136
+ // whose clusters overlap theirs, then frozen: the same as the company map.
137
+ const layout = useMemo(() => {
138
+ const list = people.slice(0, MAX_BUBBLES);
139
+ const ids = new Set(list.map((p) => p.id));
140
+ const keys = [...new Set(list.map((p) => sectorOf.get(p.id).key))];
141
+ const R = 260;
142
+ const anchor = new Map(keys.map((k, i) => {
143
+ const a = (i / Math.max(1, keys.length)) * Math.PI * 2 - Math.PI / 2;
144
+ return [k, { x: Math.cos(a) * R, y: Math.sin(a) * R }];
145
+ }));
146
+ const nodes = list.map((p) => {
147
+ const at = anchor.get(sectorOf.get(p.id).key);
148
+ return {
149
+ id: p.id, p, sector: sectorOf.get(p.id),
150
+ r: p.size ? 4 + Math.sqrt(Math.max(0, sizeBy.of(p))) * sizeBy.k : 3,
151
+ x: at.x + (jitter(String(p.id)) - 0.5) * 40, y: at.y + (jitter(`${p.id}#`) - 0.5) * 40,
152
+ ax: at.x, ay: at.y, // where their sector sits, for live physics
153
+ };
154
+ });
155
+ const edges = links.filter((l) => ids.has(l.a) && ids.has(l.b)).map((l) => ({ source: l.a, target: l.b, weight: l.shared, k: Math.min(0.25, 0.02 * l.shared) }));
156
+ const sim = forceSimulation(nodes)
157
+ .force('x', forceX((d) => anchor.get(d.sector.key).x).strength(0.12))
158
+ .force('y', forceY((d) => anchor.get(d.sector.key).y).strength(0.12))
159
+ .force('collide', forceCollide((d) => d.r + 3))
160
+ .force('charge', forceManyBody().strength(-25))
161
+ .force('link', forceLink(edges).id((d) => d.id).strength((e) => e.k).distance(60))
162
+ .stop();
163
+ for (let i = 0; i < 320; i++) sim.tick();
164
+ const xs = nodes.flatMap((n) => [n.x - n.r, n.x + n.r]);
165
+ const ys = nodes.flatMap((n) => [n.y - n.r, n.y + n.r]);
166
+ const box = nodes.length
167
+ ? { x: Math.min(...xs) - 60, y: Math.min(...ys) - 40, w: Math.max(...xs) - Math.min(...xs) + 120, h: Math.max(...ys) - Math.min(...ys) + 80 }
168
+ : { x: -300, y: -300, w: 600, h: 600 };
169
+ const labels = keys.map((k) => {
170
+ const ns = nodes.filter((n) => n.sector.key === k);
171
+ const cx = ns.reduce((sum, n) => sum + n.x, 0) / ns.length;
172
+ const cy = ns.reduce((sum, n) => sum + n.y, 0) / ns.length;
173
+ const len = Math.hypot(cx, cy) || 1;
174
+ const ux = cx / len, uy = cy / len;
175
+ const reach = Math.max(...ns.map((n) => (n.x - cx) * ux + (n.y - cy) * uy + n.r));
176
+ return { key: k, x: cx + ux * (reach + 16), y: cy + uy * (reach + 16) + 4, anchor: ux > 0.35 ? 'start' : ux < -0.35 ? 'end' : 'middle', ind: industryByKey(k) };
177
+ });
178
+ for (const l of labels) {
179
+ const wText = l.ind.label.length * 7.5;
180
+ const x0 = l.anchor === 'start' ? l.x : l.anchor === 'end' ? l.x - wText : l.x - wText / 2;
181
+ const right = Math.max(box.x + box.w, x0 + wText + 10);
182
+ box.x = Math.min(box.x, x0 - 10); box.w = right - box.x;
183
+ const bottom = Math.max(box.y + box.h, l.y + 12);
184
+ box.y = Math.min(box.y, l.y - 18); box.h = bottom - box.y;
185
+ }
186
+ box.h += Math.max(60, box.h * 0.1);
187
+ return { nodes, edges, box, labels, cut: Math.max(0, people.length - list.length) };
188
+ }, [people, links, sizeBy, sectorOf]);
189
+ const { drag, clicked } = useLivePhysics(layout, physics);
190
+ // Heat: how strong each cluster is, person for person; not scanned is the coldest.
191
+ const heat = useMemo(() => heatBy(layout.nodes, (n) => clusterStrength(n.p)), [layout]);
192
+ const hot = colourBy === 'heat';
193
+
194
+ const hovered = hover ? layout.nodes.find((n) => n.id === hover) : null;
195
+ const neighbours = useMemo(() => {
196
+ if (!hover) return null;
197
+ const s = new Set([hover]);
198
+ for (const e of layout.edges) {
199
+ if (e.source.id === hover) s.add(e.target.id);
200
+ if (e.target.id === hover) s.add(e.source.id);
201
+ }
202
+ return s;
203
+ }, [hover, layout]);
204
+ const labelled = useMemo(() => new Set(names === 'none' ? [] : layout.nodes.filter((n) => n.p.size).slice(0, names === 'all' ? Infinity : 28).map((n) => n.id)), [layout, names]);
205
+
206
+ // Where a node is on screen (the SVG fits its box to the window, centred).
207
+ const scale = w && h ? Math.min(w / layout.box.w, h / layout.box.h) : 1;
208
+ const ox = (w - layout.box.w * scale) / 2;
209
+ const oy = (h - layout.box.h * scale) / 2;
210
+ const onScreen = (n) => ({ x: ox + (n.x - layout.box.x) * scale, y: oy + (n.y - layout.box.y) * scale, r: n.r * scale });
211
+
212
+ return (
213
+ <div ref={wrap} style={{ flex: 1, minHeight: 360, position: 'relative' }}>
214
+ {w > 0 && (
215
+ <svg width={w} height={h} viewBox={`${layout.box.x} ${layout.box.y} ${layout.box.w} ${layout.box.h}`} style={{ display: 'block', touchAction: drag ? 'none' : undefined }}>
216
+ {hot && <HeatDefs id="pplheat" />}
217
+ {hot && <HeatGlow id="pplheat" nodes={layout.nodes.filter((n) => n.p.size)} heat={heat} />}
218
+ <g>
219
+ {layout.edges.map((e, i) => {
220
+ const on = hover && (e.source.id === hover || e.target.id === hover);
221
+ return (
222
+ <line key={i} x1={e.source.x} y1={e.source.y} x2={e.target.x} y2={e.target.y}
223
+ stroke={on ? '#FF6B35' : '#8fa6c0'} strokeOpacity={on ? 0.8 : hover ? 0.03 : 0.09}
224
+ strokeWidth={on ? 1 + Math.log2(1 + e.weight) : 0.4 + Math.log2(1 + e.weight) * 0.4} />
225
+ );
226
+ })}
227
+ </g>
228
+ {layout.labels.map((l) => (
229
+ <text key={l.key} x={l.x} y={l.y} textAnchor={l.anchor} fontSize={13} fontWeight={800} fill={l.ind.color} opacity={0.85}>
230
+ {l.ind.label}
231
+ </text>
232
+ ))}
233
+ {layout.nodes.map((n) => {
234
+ const { p } = n;
235
+ const dim = neighbours && !neighbours.has(n.id);
236
+ return (
237
+ <g key={n.id} transform={`translate(${n.x},${n.y})`} style={{ cursor: drag ? 'grab' : p.size ? 'pointer' : 'default' }} opacity={dim ? 0.18 : 1}
238
+ onMouseEnter={() => setHover(n.id)} onMouseLeave={() => setHover(null)}
239
+ onPointerDown={drag ? (ev) => drag(ev, n) : undefined}
240
+ onClick={() => { if (clicked() && p.size) onOpen(n.id); }}>
241
+ <circle r={n.r} fill={!p.size ? UNSCANNED : hot ? heatColour(heat(n)) : n.sector.color} fillOpacity={p.size ? 0.92 : 0.6}
242
+ stroke={hover === n.id ? '#fff' : p.S ? TIER.S : 'rgba(0,0,0,0.35)'}
243
+ strokeWidth={hover === n.id ? 2.5 : p.S ? Math.min(4, 1 + p.S / 40) : 0.6} />
244
+ {/* The share of their cluster only they can open, as an inner disc */}
245
+ {p.size > 0 && p.only > 0 && <circle r={n.r * Math.sqrt(p.share) * 0.85} fill="#fff" fillOpacity={0.3} />}
246
+ {(labelled.has(n.id) || hover === n.id) && (
247
+ <text y={n.r + 11} textAnchor="middle" fontSize={10} fill="#e6edf0" style={{ pointerEvents: 'none' }}
248
+ paintOrder="stroke" stroke="rgba(0,0,0,0.6)" strokeWidth={3}>{p.person.name}</text>
249
+ )}
250
+ </g>
251
+ );
252
+ })}
253
+ </svg>
254
+ )}
255
+ {hovered && <Card n={hovered} at={onScreen(hovered)} w={w} rank={rank(hovered.id)} />}
256
+ <div style={{ position: 'absolute', left: 12, bottom: 10, fontSize: 11, color: '#778', lineHeight: 1.6, pointerEvents: 'none' }}>
257
+ Bubble = one of your connections, sized by {sizeBy.label.toLowerCase()} (value: S tier counts 3, A tier 2, everyone else 1){hot ? '; colour = how strong their cluster is, person for person, cold to white hot' : ''};
258
+ white centre = the share of their cluster only they can open; gold ring = S tier inside.<br />
259
+ A line = two of your connections whose clusters share people. Grey = not scanned yet.
260
+ {layout.cut ? ` Showing the ${MAX_BUBBLES} largest of ${fmt(layout.cut + MAX_BUBBLES)}.` : ''}
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ /** Someone's numbers, beside their bubble. */
267
+ function Card({ n, at, w, rank }) {
268
+ const { p } = n;
269
+ const width = 280;
270
+ const left = at.x + at.r + 14 + width > w ? Math.max(8, at.x - at.r - 14 - width) : at.x + at.r + 14;
271
+ const role = [p.person.role, companyOf(p.person)].filter(Boolean).join(' @ ') || p.person.headline || '';
272
+ return (
273
+ <div style={{
274
+ position: 'absolute', left, top: Math.max(8, at.y - 70), width, pointerEvents: 'none', zIndex: 5,
275
+ padding: '12px 14px', borderRadius: 12, background: 'rgba(8,10,22,0.96)', border: `1px solid ${n.sector.color}99`,
276
+ boxShadow: '0 12px 40px rgba(0,0,0,0.6)', fontSize: 12, lineHeight: 1.45, color: '#ddd',
277
+ }}>
278
+ <div style={{ fontSize: 15, fontWeight: 800, color: '#fff' }}>{p.person.name}</div>
279
+ <div style={{ fontSize: 11.5, color: '#8a8fa8' }}>
280
+ {role}{p.person.tier ? ` · ${p.person.tier}-tier` : ''} · {n.sector.label}
281
+ </div>
282
+ {p.size ? (
283
+ <>
284
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px 10px', margin: '10px 0 8px' }}>
285
+ <Stat value={fmt(p.value)} label="cluster value" color="#FFD700" />
286
+ <Stat value={fmt(p.size)} label="people in their cluster" />
287
+ <Stat value={`${fmt(p.S)} S · ${fmt(p.A)} A`} label="the strongest among them" color="#FFD700" />
288
+ <Stat value={`${fmt(p.only)} (${Math.round(p.share * 100)}%)`} label="only through them" color="#00E5FF" />
289
+ </div>
290
+ <div style={{ fontSize: 11.5, color: '#aab' }}>
291
+ {rank === 1 ? 'The most valuable cluster you’ve scanned. ' : rank != null ? `Worth more than ${Math.round(rank * 100)}% of the clusters you’ve scanned. ` : ''}Click to open it in Bridge Chains.
292
+ </div>
293
+ </>
294
+ ) : (
295
+ <div style={{ fontSize: 11.5, color: '#aab', marginTop: 8 }}>Their circle isn’t scanned yet, so there’s no cluster to size.</div>
296
+ )}
297
+ </div>
298
+ );
299
+ }
300
+
301
+ function Stat({ value, label, color = '#fff' }) {
302
+ return (
303
+ <div>
304
+ <div style={{ fontSize: 15, fontWeight: 800, color }}>{value}</div>
305
+ <div style={{ fontSize: 10.5, color: '#8a8fa8' }}>{label}</div>
306
+ </div>
307
+ );
308
+ }
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ // The Scan page's radar (Blake, 2026-10-02: "making the scan button more
4
+ // gamified, having the slow medium high scan option"). A round Scan button in
5
+ // the middle of a radar: the sweep turns while a scan runs, the outer arc is
6
+ // today's searches against the budget, and beside it the three speeds with
7
+ // what each means for the rest of today's budget. Speed only changes searches
8
+ // an hour (lib/scan-pace.js); the daily budget is still the cap.
9
+
10
+ import { PACES, PACE_NAMES, paceOf, paceSeconds, searchesPerHour, durationText } from '../../lib/scan-pace';
11
+
12
+ const SIZE = 236;
13
+ const C = SIZE / 2;
14
+ const CSS = `
15
+ @keyframes radarSweep { to { transform: rotate(360deg); } }
16
+ @keyframes radarPing { 0% { opacity: .9; r: 3; } 100% { opacity: 0; r: 11; } }
17
+ .radar-sweep { transform-origin: ${C}px ${C}px; animation: radarSweep 2.6s linear infinite; }
18
+ .radar-ping { animation: radarPing 2.6s ease-out infinite; }
19
+ .radar-go { transition: transform .15s ease, box-shadow .2s ease; }
20
+ .radar-go:not(:disabled):hover { transform: scale(1.04); box-shadow: 0 0 34px rgba(0,255,136,.45); }
21
+ .radar-go:not(:disabled):active { transform: scale(.97); }
22
+ @media (prefers-reduced-motion: reduce) { .radar-sweep, .radar-ping { animation: none; } }
23
+ `;
24
+
25
+ // A few fixed blips, so the radar looks alive without inventing anything about anyone.
26
+ const BLIPS = [[0.62, 0.7], [0.38, 2.1], [0.8, 3.3], [0.5, 4.4], [0.72, 5.6]];
27
+
28
+ function arc(r, from, to) {
29
+ const p = (a) => `${(C + r * Math.cos(a)).toFixed(2)} ${(C + r * Math.sin(a)).toFixed(2)}`;
30
+ const large = to - from > Math.PI ? 1 : 0;
31
+ return `M${p(from)} A${r} ${r} 0 ${large} 1 ${p(to)}`;
32
+ }
33
+
34
+ export default function ScanRadar({ li, running, scanning, disabled, label, sublabel, onScan, onPace }) {
35
+ const pace = li?.limits?.pace || 'fast';
36
+ const daily = li?.limits?.daily || 0;
37
+ const used = li?.unreadable ? daily : li?.searchesToday || 0;
38
+ const left = daily ? Math.max(0, daily - used) : null;
39
+ const share = daily ? Math.min(1, used / daily) : 0;
40
+ const tone = share >= 0.9 ? '#ff6b6b' : share >= 0.6 ? '#FFD700' : '#00ff88';
41
+ const top = -Math.PI / 2;
42
+ const R = C - 10;
43
+
44
+ return (
45
+ <div style={{ display: 'flex', gap: 22, alignItems: 'center', flexWrap: 'wrap', padding: '14px 16px', borderRadius: 14,
46
+ border: '1px solid rgba(0,255,136,0.18)', background: 'radial-gradient(circle at 20% 50%, rgba(0,255,136,0.06), rgba(255,255,255,0.02) 60%)' }}>
47
+ <style>{CSS}</style>
48
+ <div style={{ position: 'relative', width: SIZE, height: SIZE, flexShrink: 0 }}>
49
+ <svg width={SIZE} height={SIZE} aria-hidden="true" style={{ position: 'absolute', inset: 0 }}>
50
+ <defs>
51
+ <radialGradient id="radarBg"><stop offset="0" stopColor="#0f2a22" /><stop offset="1" stopColor="#0a0a1a" /></radialGradient>
52
+ <linearGradient id="radarBeam" x1="0" y1="0" x2="1" y2="0">
53
+ <stop offset="0" stopColor="#00ff88" stopOpacity="0" /><stop offset="1" stopColor="#00ff88" stopOpacity="0.45" />
54
+ </linearGradient>
55
+ </defs>
56
+ <circle cx={C} cy={C} r={R} fill="url(#radarBg)" stroke="rgba(0,255,136,0.25)" />
57
+ {[0.33, 0.66].map((k) => <circle key={k} cx={C} cy={C} r={R * k} fill="none" stroke="rgba(0,255,136,0.14)" strokeDasharray="2 5" />)}
58
+ <line x1={C - R} y1={C} x2={C + R} y2={C} stroke="rgba(0,255,136,0.1)" />
59
+ <line x1={C} y1={C - R} x2={C} y2={C + R} stroke="rgba(0,255,136,0.1)" />
60
+ {/* The sweep: turning while a scan runs, resting otherwise */}
61
+ <g className={running ? 'radar-sweep' : undefined} opacity={running ? 1 : 0.35}>
62
+ <path d={`M${C} ${C} L${C + R} ${C} A${R} ${R} 0 0 0 ${C + R * Math.cos(-0.55)} ${C + R * Math.sin(-0.55)} Z`} fill="url(#radarBeam)" />
63
+ <line x1={C} y1={C} x2={C + R} y2={C} stroke="#00ff88" strokeOpacity={0.8} strokeWidth={1.5} />
64
+ </g>
65
+ {BLIPS.map(([k, a], i) => (
66
+ <g key={i}>
67
+ <circle cx={C + R * k * Math.cos(a)} cy={C + R * k * Math.sin(a)} r={2.4} fill="#00ff88" opacity={running ? 0.9 : 0.35} />
68
+ {running && <circle className="radar-ping" style={{ animationDelay: `${(a / (2 * Math.PI)) * 2.6}s` }}
69
+ cx={C + R * k * Math.cos(a)} cy={C + R * k * Math.sin(a)} r={3} fill="none" stroke="#00ff88" />}
70
+ </g>
71
+ ))}
72
+ {/* Today's searches against the budget, round the edge */}
73
+ <path d={arc(R + 5, top, top + 2 * Math.PI - 0.001)} fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth={5} />
74
+ {share > 0 && <path d={arc(R + 5, top, top + 2 * Math.PI * share - 0.001)} fill="none" stroke={tone} strokeWidth={5} strokeLinecap="round" />}
75
+ </svg>
76
+ <button type="button" className="radar-go" onClick={onScan} disabled={disabled}
77
+ style={{
78
+ position: 'absolute', left: C - 62, top: C - 62, width: 124, height: 124, borderRadius: '50%', cursor: disabled ? 'not-allowed' : 'pointer',
79
+ border: `2px solid ${disabled ? 'rgba(255,255,255,0.12)' : '#00ff88'}`,
80
+ background: disabled ? 'rgba(20,24,40,0.92)' : 'radial-gradient(circle at 50% 35%, #134d3a, #0b2a22 70%)',
81
+ color: disabled ? '#667' : '#eafff5', boxShadow: disabled ? 'none' : '0 0 22px rgba(0,255,136,0.25)',
82
+ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2, padding: 8,
83
+ }}>
84
+ <span style={{ fontSize: 22, fontWeight: 900, letterSpacing: 2 }}>{scanning ? '•••' : 'SCAN'}</span>
85
+ <span style={{ fontSize: 11, fontWeight: 700, opacity: 0.85, lineHeight: 1.25, textAlign: 'center' }}>{label}</span>
86
+ {sublabel && <span style={{ fontSize: 10, opacity: 0.6, textAlign: 'center' }}>{sublabel}</span>}
87
+ </button>
88
+ </div>
89
+
90
+ <div style={{ flex: 1, minWidth: 260 }}>
91
+ <div style={{ fontSize: 10, fontWeight: 800, letterSpacing: 1, color: '#6b7090', textTransform: 'uppercase', marginBottom: 8 }}>Speed</div>
92
+ <div role="radiogroup" aria-label="Scan speed" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
93
+ {PACE_NAMES.map((name) => {
94
+ const p = PACES[name];
95
+ const on = pace === name;
96
+ const color = name === 'slow' ? '#00E5FF' : name === 'medium' ? '#FFD700' : '#FF7043';
97
+ return (
98
+ <button key={name} type="button" role="radio" aria-checked={on} disabled={running || !onPace}
99
+ onClick={() => onPace?.(name)}
100
+ title={`${p.pagePause} s before each page, ${p.chunkCooldown / 60} min more after every 10, profiles ${p.profileGap} s apart`}
101
+ style={{
102
+ padding: '10px 8px', borderRadius: 10, cursor: running ? 'not-allowed' : 'pointer', textAlign: 'left',
103
+ border: `1.5px solid ${on ? color : 'rgba(255,255,255,0.1)'}`, background: on ? `${color}1a` : 'rgba(255,255,255,0.03)',
104
+ color: '#ddd',
105
+ }}>
106
+ <div style={{ fontSize: 14, fontWeight: 800, color: on ? color : '#ccd' }}>{p.label}</div>
107
+ <div style={{ fontSize: 11, color: '#8a8fa8', marginTop: 2 }}>~{searchesPerHour(name)} searches an hour</div>
108
+ <div style={{ fontSize: 10.5, color: '#6b7090', marginTop: 1 }}>{p.pagePause} s between pages</div>
109
+ </button>
110
+ );
111
+ })}
112
+ </div>
113
+ <div style={{ marginTop: 10, fontSize: 12.5, color: '#b8c4c4', lineHeight: 1.6 }}>
114
+ {!li
115
+ ? <>Today&rsquo;s searches show here once the scanner can read them.</>
116
+ : left == null
117
+ ? <>No daily cap is set.</>
118
+ : <><b style={{ color: tone }}>{left}</b> of {daily} searches left today
119
+ {left > 0 && <> · at {paceOf(pace).label}, {durationText(paceSeconds(pace, left))} to use them</>}.</>}
120
+ </div>
121
+ <div style={{ marginTop: 4, fontSize: 11.5, color: '#778', lineHeight: 1.55 }}>
122
+ Fast is how the scanner has always run; Medium and Slow only add waiting, so fewer searches an hour reach
123
+ LinkedIn. Your daily budget stays the cap at every speed: slower spreads it out, it doesn&rsquo;t shrink it.
124
+ Slower lowers the odds of a check from LinkedIn; it can&rsquo;t promise there won&rsquo;t be one.
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
- // The notch: a small pill hanging from the bottom of the header, centred, on
3
+ // The notch: a small bar hanging from the bottom of the header, under its tab
4
+ // buttons. It holds the page's own buttons (its views or sub-tabs, set through
5
+ // lib/island.js setNotchTabs) and, beside them, what the app is doing. On
4
6
  // every page (Blake, 2026-09-29: "a notch ui of the thing scanning"). It stays
5
7
  // out of the way: nothing at all while nothing runs; a tiny dimmed "Auto scan"
6
8
  // when the all-day mode is on but idle; while a scan runs, what it's doing and
@@ -15,7 +17,7 @@ import { usePathname } from 'next/navigation';
15
17
  import Link from 'next/link';
16
18
  import { watchScanner, scannerNow, stopScrape } from '../../lib/scraper-client';
17
19
  import { watchAllDay, allDayNow } from '../../lib/experimental-client';
18
- import { watchActivities, activitiesNow, noActivities } from '../../lib/island';
20
+ import { watchActivities, activitiesNow, noActivities, watchNotchTabs, notchTabsNow, noNotchTabs, setNotchShown } from '../../lib/island';
19
21
 
20
22
  const WHAT = {
21
23
  full: 'Scanning your network',
@@ -31,6 +33,7 @@ const WHAT = {
31
33
  company: 'Scanning a company',
32
34
  photos: 'Saving photos',
33
35
  messages: 'Reading your messages list',
36
+ 'messages-full': 'Reading your whole messages history',
34
37
  };
35
38
 
36
39
  // Green while there's plenty left, amber past 60%, red past 90%.
@@ -43,6 +46,8 @@ export default function ScanStatusBar() {
43
46
  const [pinned, setPinned] = useState(false);
44
47
  const allDay = useSyncExternalStore(watchAllDay, allDayNow, () => false);
45
48
  const others = useSyncExternalStore(watchActivities, activitiesNow, noActivities);
49
+ // The page's own buttons (its views or sub-tabs), when it has any.
50
+ const tabs = useSyncExternalStore(watchNotchTabs, notchTabsNow, noNotchTabs);
46
51
  useEffect(() => watchScanner(() => {
47
52
  const now = scannerNow();
48
53
  setJob(now);
@@ -53,6 +58,7 @@ export default function ScanStatusBar() {
53
58
  // be swapped for a new one, or wrap to two rows.
54
59
  const pathname = usePathname();
55
60
  const [top, setTop] = useState(null);
61
+ const [centre, setCentre] = useState(null);
56
62
  useEffect(() => {
57
63
  let header = null;
58
64
  let ro = null;
@@ -68,6 +74,10 @@ export default function ScanStatusBar() {
68
74
  }
69
75
  const at = header ? Math.round(header.getBoundingClientRect().bottom) : null;
70
76
  setTop((was) => (was === at ? was : at));
77
+ // Under the header's tab buttons when the page has them, else the window's middle.
78
+ const group = document.getElementById('main-tabs')?.getBoundingClientRect();
79
+ const mid = group && group.width ? Math.round(group.left + group.width / 2) : null;
80
+ setCentre((was) => (was === mid ? was : mid));
71
81
  };
72
82
  const later = () => { if (!queued) queued = requestAnimationFrame(measure); };
73
83
  later();
@@ -79,7 +89,14 @@ export default function ScanStatusBar() {
79
89
 
80
90
  const running = !!job?.running;
81
91
  const other = others[others.length - 1] || null;
82
- if (!running && !other && !allDay) return null;
92
+ const status = running || other || allDay;
93
+ // Pages with a heading at the top leave room for the notch while it's showing.
94
+ const showing = Boolean(status || tabs);
95
+ useEffect(() => {
96
+ setNotchShown(showing);
97
+ return () => setNotchShown(false);
98
+ }, [showing]);
99
+ if (!status && !tabs) return null;
83
100
 
84
101
  const p = job?.progress;
85
102
  const step = p?.kind === 'batch' && p.total ? `${p.current || p.done || 0} of ${p.total}`
@@ -95,42 +112,70 @@ export default function ScanStatusBar() {
95
112
  const label = running ? (WHAT[job.action] || 'Scanning') : other ? other.label : 'Auto scan';
96
113
  const short = running ? step : other ? other.detail : null;
97
114
 
115
+ // Kept inside the window: centred under the tab buttons, but never off an edge.
116
+ const left = centre != null ? `clamp(170px, ${centre}px, calc(100vw - 170px))` : '50%';
117
+
98
118
  return (
99
119
  <div
100
- role="status"
101
- aria-live="polite"
102
- onMouseEnter={() => setOpen(true)}
103
120
  onMouseLeave={() => setOpen(false)}
104
121
  style={{
105
- position: 'fixed', left: '50%', top: top ?? 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
106
- maxWidth: 'calc(100vw - 32px)', minWidth: running || other ? 180 : 0,
107
- padding: expanded ? '8px 14px 10px' : '4px 12px 6px',
108
- borderRadius: '0 0 16px 16px', borderTop: 'none',
109
- border: `1px solid ${running ? 'rgba(0,255,136,0.25)' : 'rgba(255,255,255,0.1)'}`,
122
+ position: 'fixed', left, top: top ?? 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
123
+ maxWidth: 'calc(100vw - 16px)',
124
+ padding: expanded && status ? '4px 6px 10px' : '4px 6px 5px',
125
+ borderRadius: '0 0 14px 14px', border: `1px solid ${running ? 'rgba(0,255,136,0.25)' : 'rgba(255,255,255,0.1)'}`, borderTop: 'none',
110
126
  background: 'rgba(8,10,22,0.96)', color: '#cfd8d8', fontSize: 12,
111
- opacity: running || other || expanded ? 1 : 0.55,
127
+ opacity: tabs || running || other || expanded ? 1 : 0.55,
112
128
  boxShadow: running ? '0 6px 20px rgba(0,0,0,0.35)' : 'none',
113
129
  transition: 'padding 0.18s ease, opacity 0.18s ease',
114
130
  }}
115
131
  >
116
- <button
117
- onClick={() => setPinned((v) => !v)}
118
- aria-expanded={expanded}
119
- title={expanded ? 'Close' : 'Open'}
120
- style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', background: 'none', border: 'none', color: 'inherit', font: 'inherit', cursor: 'pointer', padding: 0, whiteSpace: 'nowrap' }}
121
- >
122
- <span className={running ? 'notch-pulse' : undefined} style={{ width: 7, height: 7, borderRadius: '50%', background: dot, flexShrink: 0 }} />
123
- <b style={{ color: running || other ? '#fff' : '#b8c4c4', fontWeight: 600 }}>{label}</b>
124
- {short && <span style={{ color: '#8b9a9a' }}>{short}</span>}
125
- {!running && !other && expanded && <span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>}
126
- </button>
127
- {fraction != null && (
128
- <div style={{ height: 2, marginTop: 5, borderRadius: 2, background: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
132
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, overflowX: 'auto' }}>
133
+ {tabs && (
134
+ <div role="tablist" aria-label="View" style={{ display: 'flex', gap: 2 }}>
135
+ {tabs.items.map((t) => {
136
+ const on = t.key === tabs.current;
137
+ return (
138
+ <button
139
+ key={t.key} role="tab" aria-selected={on} title={t.title}
140
+ onClick={() => tabs.onPick(t.key)}
141
+ style={{
142
+ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 11px', borderRadius: 8, border: 'none',
143
+ cursor: 'pointer', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 600,
144
+ background: on ? 'rgba(52,152,219,0.22)' : 'transparent', color: on ? '#cfe6f7' : '#8b9aa8',
145
+ }}
146
+ >
147
+ {t.icon && <span aria-hidden="true">{t.icon}</span>}
148
+ {t.label}
149
+ </button>
150
+ );
151
+ })}
152
+ </div>
153
+ )}
154
+ {tabs && status && <span aria-hidden="true" style={{ width: 1, alignSelf: 'stretch', margin: '3px 4px', background: 'rgba(255,255,255,0.12)' }} />}
155
+ {status && (
156
+ <button
157
+ onMouseEnter={() => setOpen(true)}
158
+ onClick={() => setPinned((v) => !v)}
159
+ aria-expanded={expanded}
160
+ title={expanded ? 'Close' : 'Open'}
161
+ style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'none', border: 'none', color: 'inherit', font: 'inherit', cursor: 'pointer', padding: '5px 8px', whiteSpace: 'nowrap' }}
162
+ >
163
+ <span className={running ? 'notch-pulse' : undefined} style={{ width: 7, height: 7, borderRadius: '50%', background: dot, flexShrink: 0 }} />
164
+ <span role="status" aria-live="polite" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
165
+ <b style={{ color: running || other ? '#fff' : '#b8c4c4', fontWeight: 600 }}>{label}</b>
166
+ {short && <span style={{ color: '#8b9a9a' }}>{short}</span>}
167
+ </span>
168
+ {!running && !other && expanded && <span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>}
169
+ </button>
170
+ )}
171
+ </div>
172
+ {status && fraction != null && (
173
+ <div style={{ height: 2, margin: '3px 6px 0', borderRadius: 2, background: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
129
174
  <div style={{ width: `${Math.round(fraction * 100)}%`, height: '100%', background: running ? '#00ff88' : '#3498DB', transition: 'width 0.4s ease' }} />
130
175
  </div>
131
176
  )}
132
177
  {expanded && running && (
133
- <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8, flexWrap: 'wrap' }}>
178
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '8px 8px 0', flexWrap: 'wrap' }}>
134
179
  {job.target?.name && <span>{job.target.name}</span>}
135
180
  {b && (
136
181
  <span style={{ color: tone(b.searches, b.cap) }} title="Searches on your LinkedIn account in the last 24 hours, and your daily budget">
@@ -152,7 +197,7 @@ export default function ScanStatusBar() {
152
197
  >{stopping ? 'Stopping…' : 'Stop'}</button>
153
198
  </div>
154
199
  )}
155
- {expanded && !running && other?.detail && <div style={{ marginTop: 6, color: '#8b9a9a' }}>{other.detail}</div>}
200
+ {expanded && !running && other?.detail && <div style={{ margin: '6px 8px 0', color: '#8b9a9a' }}>{other.detail}</div>}
156
201
  </div>
157
202
  );
158
203
  }