six-degrees 0.4.11 → 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 (375) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +9 -9
  4. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +3 -3
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +14 -14
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +14 -14
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  81. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/launch.html +2 -2
  84. package/.next/standalone/.next/server/app/launch.rsc +14 -14
  85. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  86. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  87. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/paths.html +1 -1
  93. package/.next/standalone/.next/server/app/paths.rsc +14 -14
  94. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  95. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/profile.html +1 -1
  100. package/.next/standalone/.next/server/app/profile.rsc +14 -14
  101. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  102. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/queue.html +1 -1
  107. package/.next/standalone/.next/server/app/queue.rsc +14 -14
  108. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  109. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/scores.html +1 -1
  114. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  115. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  116. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/settings.html +1 -1
  121. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  122. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  123. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  125. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/setup.html +1 -1
  128. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  129. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  130. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  131. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  132. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/app/social.html +1 -1
  135. package/.next/standalone/.next/server/app/social.rsc +14 -14
  136. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
  137. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  138. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__13dx4sd._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  143. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  145. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  148. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  149. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  150. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  151. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  152. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  155. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  156. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  157. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  158. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  159. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  160. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  161. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  162. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  163. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  170. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  171. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  173. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  178. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  181. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  184. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  186. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -2
  187. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
  190. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  191. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  192. package/.next/standalone/.next/server/pages/404.html +1 -1
  193. package/.next/standalone/.next/server/pages/500.html +1 -1
  194. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  196. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  197. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  198. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  200. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  201. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  202. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  203. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  204. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  205. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  206. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  207. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  208. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  209. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  210. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  211. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  212. package/.next/standalone/CHANGELOG.md +187 -0
  213. package/.next/standalone/app/api/ingest/route.js +40 -1
  214. package/.next/standalone/app/api/scraper/route.js +7 -1
  215. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  216. package/.next/standalone/app/components/AppTabs.js +59 -0
  217. package/.next/standalone/app/components/ChainView.js +303 -89
  218. package/.next/standalone/app/components/CirclePanel.js +162 -0
  219. package/.next/standalone/app/components/CompanyScores.js +2 -2
  220. package/.next/standalone/app/components/FieldStep.js +4 -4
  221. package/.next/standalone/app/components/FilterPanel.js +121 -183
  222. package/.next/standalone/app/components/ForceGraph.js +41 -3
  223. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  224. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  225. package/.next/standalone/app/components/MapControls.js +154 -0
  226. package/.next/standalone/app/components/NoteDetail.js +118 -0
  227. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  228. package/.next/standalone/app/components/PeopleMap.js +308 -0
  229. package/.next/standalone/app/components/ScanRadar.js +129 -0
  230. package/.next/standalone/app/components/ScanStatusBar.js +71 -27
  231. package/.next/standalone/app/components/SeparationView.js +626 -118
  232. package/.next/standalone/app/components/Sidebar.js +28 -2
  233. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  234. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  235. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  236. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  237. package/.next/standalone/app/components/views.js +15 -14
  238. package/.next/standalone/app/page.js +89 -156
  239. package/.next/standalone/app/paths/page.js +39 -27
  240. package/.next/standalone/app/profile/page.js +33 -5
  241. package/.next/standalone/app/queue/page.js +56 -34
  242. package/.next/standalone/app/scores/page.js +16 -66
  243. package/.next/standalone/app/settings/page.js +27 -13
  244. package/.next/standalone/app/setup/page.js +298 -205
  245. package/.next/standalone/app/social/SocialHub.js +374 -0
  246. package/.next/standalone/app/social/page.js +10 -362
  247. package/.next/standalone/lib/chain-layout.js +105 -35
  248. package/.next/standalone/lib/degrees.js +0 -6
  249. package/.next/standalone/lib/dot-rings.js +25 -0
  250. package/.next/standalone/lib/galaxy-lab.js +65 -6
  251. package/.next/standalone/lib/ingest.js +2 -0
  252. package/.next/standalone/lib/island.js +40 -0
  253. package/.next/standalone/lib/linkedin-limits.js +6 -2
  254. package/.next/standalone/lib/map-heat.js +20 -0
  255. package/.next/standalone/lib/network-health.js +1 -1
  256. package/.next/standalone/lib/notifications.js +121 -0
  257. package/.next/standalone/lib/people-map.js +82 -0
  258. package/.next/standalone/lib/rarity.js +45 -3
  259. package/.next/standalone/lib/reach.js +7 -6
  260. package/.next/standalone/lib/rpc.js +15 -4
  261. package/.next/standalone/lib/scan-pace.js +46 -0
  262. package/.next/standalone/lib/scoring.js +39 -8
  263. package/.next/standalone/lib/scraper-client.js +19 -6
  264. package/.next/standalone/lib/sector-focus.js +4 -4
  265. package/.next/standalone/lib/separation.js +137 -10
  266. package/.next/standalone/lib/settings-effects.js +20 -11
  267. package/.next/standalone/lib/settings.js +3 -0
  268. package/.next/standalone/lib/tier-grid.js +99 -0
  269. package/.next/standalone/lib/title-ranking.js +83 -0
  270. package/.next/standalone/package-lock.json +2 -2
  271. package/.next/standalone/package.json +1 -1
  272. package/.next/standalone/scripts/scrape.py +51 -13
  273. package/.next/static/chunks/03bziijw5srol.js +1 -0
  274. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  275. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  276. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  277. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  278. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  279. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  280. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  281. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  282. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  283. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  284. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  285. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  286. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  287. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  288. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  289. package/CHANGELOG.md +187 -0
  290. package/lib/chain-layout.js +105 -35
  291. package/lib/degrees.js +0 -6
  292. package/lib/dot-rings.js +25 -0
  293. package/lib/galaxy-lab.js +65 -6
  294. package/lib/ingest.js +2 -0
  295. package/lib/island.js +40 -0
  296. package/lib/linkedin-limits.js +6 -2
  297. package/lib/map-heat.js +20 -0
  298. package/lib/network-health.js +1 -1
  299. package/lib/notifications.js +121 -0
  300. package/lib/people-map.js +82 -0
  301. package/lib/rarity.js +45 -3
  302. package/lib/reach.js +7 -6
  303. package/lib/rpc.js +15 -4
  304. package/lib/scan-pace.js +46 -0
  305. package/lib/scoring.js +39 -8
  306. package/lib/scraper-client.js +19 -6
  307. package/lib/sector-focus.js +4 -4
  308. package/lib/separation.js +137 -10
  309. package/lib/settings-effects.js +20 -11
  310. package/lib/settings.js +3 -0
  311. package/lib/tier-grid.js +99 -0
  312. package/lib/title-ranking.js +83 -0
  313. package/package.json +1 -1
  314. package/scripts/scrape.py +51 -13
  315. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  316. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  320. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  321. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  322. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  323. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  324. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  325. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  326. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  327. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  328. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  329. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  330. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  331. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +0 -3
  333. package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +0 -3
  334. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  335. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  336. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  337. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  338. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  339. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  340. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  341. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  342. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  343. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  344. package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  345. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  346. package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +0 -1
  347. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  348. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  349. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  350. package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +0 -1
  351. package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  352. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  353. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  354. package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  355. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  356. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  357. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  358. package/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  359. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  360. package/.next/static/chunks/1a74dky22dxd6.js +0 -1
  361. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  362. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  363. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  364. package/.next/static/chunks/297e1z34yi--z.js +0 -1
  365. package/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  366. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  367. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  368. package/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  369. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  370. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  371. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  372. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  373. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  374. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  375. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ // The right panel while a circle is open in Bridge Chains (Blake, 2026-10-02:
4
+ // "when im in a d2 cluster in the bridges and i open up the people panel it
5
+ // should show the cluster of whoever im in"). Who is in their circle, strongest
6
+ // first, with the people you added from it at the top: the ones whose own
7
+ // circle is the next link in the chain.
8
+
9
+ import { useMemo, useState } from 'react';
10
+ import Link from 'next/link';
11
+ import Avatar from './Avatar';
12
+ import { circleIndex } from '../../lib/circle';
13
+ import { reachIndex, reachState } from '../../lib/reach';
14
+ import { score1 } from '../../lib/separation';
15
+ import { TIER_ORDER } from '../../lib/tiers';
16
+
17
+ const PAGE = 60;
18
+ const rank = (a, b) => TIER_ORDER.indexOf(a.tier) - TIER_ORDER.indexOf(b.tier)
19
+ || score1(b) - score1(a)
20
+ || String(a.name || '').localeCompare(String(b.name || ''));
21
+ const first = (name) => String(name || '').trim().split(/\s+/)[0] || 'Their';
22
+
23
+ const rowStyle = {
24
+ display: 'flex', alignItems: 'center', gap: 9, width: '100%', padding: '7px 8px', borderRadius: 8, boxSizing: 'border-box',
25
+ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)',
26
+ color: '#fff', cursor: 'pointer', textAlign: 'left', font: 'inherit', textDecoration: 'none',
27
+ };
28
+
29
+ /** One person: opens their card, or (with `href`) goes there instead. */
30
+ function Row({ person, tierColors, note, noteColor, onSelect, href, label }) {
31
+ const body = (
32
+ <>
33
+ <Avatar person={person} size={26} tierColors={tierColors} />
34
+ <span style={{ flex: 1, minWidth: 0 }}>
35
+ <span style={{ display: 'block', fontSize: 12, fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
36
+ {person.name}
37
+ <span style={{ fontSize: 10, color: tierColors[person.tier] || '#888', fontWeight: 800, marginLeft: 6 }}>{person.tier}</span>
38
+ </span>
39
+ <span style={{ display: 'block', fontSize: 10.5, color: '#888', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
40
+ {[person.title, person.company].filter(Boolean).join(' · ') || ' '}
41
+ </span>
42
+ </span>
43
+ {note && <span style={{ fontSize: 10, fontWeight: 700, color: noteColor || '#888', whiteSpace: 'nowrap' }}>{note}</span>}
44
+ </>
45
+ );
46
+ return href
47
+ ? <Link href={href} aria-label={label} style={{ ...rowStyle, borderColor: 'rgba(0,255,136,0.2)' }}>{body}</Link>
48
+ : <button type="button" onClick={() => onSelect?.(person)} style={rowStyle}>{body}</button>;
49
+ }
50
+
51
+ export default function CirclePanel({ person, connections = [], degree2 = [], scanNotes, tierColors, onSelect, onToggle, canScan = true }) {
52
+ const [tier, setTier] = useState(null);
53
+ const [query, setQuery] = useState('');
54
+ const [shown, setShown] = useState(PAGE);
55
+ const index = useMemo(() => circleIndex(connections, degree2), [connections, degree2]);
56
+ const reach = useMemo(() => reachIndex(connections, degree2, scanNotes), [connections, degree2, scanNotes]);
57
+ const members = useMemo(() => [...(index.circles.get(person.id) || [])].sort(rank), [index, person.id]);
58
+ const added = useMemo(() => [...(index.introduced.get(person.id) || [])].sort(rank), [index, person.id]);
59
+ const counts = useMemo(() => {
60
+ const c = Object.fromEntries(TIER_ORDER.map((t) => [t, 0]));
61
+ for (const m of members) if (c[m.tier] != null) c[m.tier] += 1;
62
+ return c;
63
+ }, [members]);
64
+ const q = query.trim().toLowerCase();
65
+ const list = members.filter((m) => (!tier || m.tier === tier)
66
+ && (q.length < 2 || [m.name, m.title, m.company].some((v) => String(v || '').toLowerCase().includes(q))));
67
+ const color = tierColors[person.tier] || '#888';
68
+ const stateNote = (p) => {
69
+ const s = reachState(p, reach);
70
+ return s === 'ready' ? ['ready to scan', '#00ff88'] : s === 'scanned' ? ['circle scanned', '#74B9FF'] : s === 'hidden' ? ['list hidden', '#888'] : [null];
71
+ };
72
+
73
+ return (
74
+ <div>
75
+ <button onClick={onToggle} style={{
76
+ display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none',
77
+ color: '#888', fontSize: 12, cursor: 'pointer', padding: '0 0 10px', fontWeight: 600,
78
+ }}>
79
+ <span style={{ fontSize: 16 }}>&larr;</span> Close
80
+ </button>
81
+
82
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
83
+ <Avatar person={person} size={38} tierColors={tierColors} />
84
+ <div style={{ minWidth: 0 }}>
85
+ <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: 1, color: '#888', textTransform: 'uppercase' }}>You&rsquo;re in</div>
86
+ <h3 style={{ fontSize: 16, fontWeight: 800, margin: 0, color }}>{first(person.name)}&rsquo;s circle</h3>
87
+ </div>
88
+ </div>
89
+ <div style={{ fontSize: 11.5, color: '#999', marginBottom: 12 }}>
90
+ {[person.title, person.company].filter(Boolean).join(' · ')}
91
+ <button type="button" onClick={() => onSelect?.(person)} style={{
92
+ display: 'block', marginTop: 6, padding: 0, background: 'none', border: 'none', color: '#74B9FF',
93
+ fontSize: 11, fontWeight: 700, cursor: 'pointer',
94
+ }}>Open {first(person.name)}&rsquo;s card →</button>
95
+ </div>
96
+
97
+ {/* The same count as the map: the people you added from here were in it too. */}
98
+ <div style={{ fontSize: 22, fontWeight: 800, lineHeight: 1 }}>{(members.length + added.length).toLocaleString('en-US')}</div>
99
+ <div style={{ fontSize: 11, color: '#888', marginBottom: 10 }}>
100
+ {members.length + added.length === 1 ? 'person' : 'people'} in their circle{added.length ? ` · you added ${added.length} of them` : ''}
101
+ </div>
102
+
103
+ {/* Tier chips: tap one to see just them. */}
104
+ <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', marginBottom: 10 }}>
105
+ {TIER_ORDER.map((t) => (
106
+ <button key={t} type="button" aria-pressed={tier === t} disabled={!counts[t]}
107
+ onClick={() => { setTier(tier === t ? null : t); setShown(PAGE); }}
108
+ style={{
109
+ padding: '4px 9px', borderRadius: 12, fontSize: 11, fontWeight: 700, cursor: counts[t] ? 'pointer' : 'default',
110
+ border: `1px solid ${tier === t ? tierColors[t] : 'rgba(255,255,255,0.1)'}`,
111
+ background: tier === t ? `${tierColors[t]}26` : 'rgba(255,255,255,0.03)',
112
+ color: counts[t] ? tierColors[t] : '#444',
113
+ }}>
114
+ {t} <span style={{ color: '#aaa', fontWeight: 600 }}>{counts[t]}</span>
115
+ </button>
116
+ ))}
117
+ </div>
118
+
119
+ <input type="search" value={query} placeholder={`Search ${first(person.name)}’s circle`}
120
+ onChange={(e) => { setQuery(e.target.value); setShown(PAGE); }}
121
+ style={{
122
+ width: '100%', padding: '8px 12px', borderRadius: 8, fontSize: 12, marginBottom: 14, boxSizing: 'border-box',
123
+ border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)', color: '#fff', outline: 'none',
124
+ }} />
125
+
126
+ {added.length > 0 && !tier && q.length < 2 && (
127
+ <div style={{ marginBottom: 14 }}>
128
+ <div style={{ fontSize: 10, fontWeight: 700, color: '#00ff88', letterSpacing: 1, marginBottom: 6, textTransform: 'uppercase' }}>
129
+ You added from here ({added.length})
130
+ </div>
131
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
132
+ {added.map((p) => {
133
+ const [note, noteColor] = stateNote(p);
134
+ return note === 'ready to scan' && canScan
135
+ ? <Row key={p.id} person={p} tierColors={tierColors} note="Scan… →" noteColor="#00ff88"
136
+ href={`/setup?scan=${encodeURIComponent(p.id)}`} label={`Scan ${p.name}’s circle: opens the Scan page to confirm`} />
137
+ : <Row key={p.id} person={p} tierColors={tierColors} note={note} noteColor={noteColor} onSelect={onSelect} />;
138
+ })}
139
+ </div>
140
+ </div>
141
+ )}
142
+
143
+ <div style={{ fontSize: 10, fontWeight: 700, color: '#888', letterSpacing: 1, marginBottom: 6, textTransform: 'uppercase' }}>
144
+ {tier ? `${tier} tier in their circle` : added.length ? 'Everyone else in their circle' : 'Everyone in their circle'} ({list.length.toLocaleString('en-US')})
145
+ </div>
146
+ {list.length === 0 ? (
147
+ <div style={{ fontSize: 11.5, color: '#888', lineHeight: 1.5 }}>
148
+ {members.length === 0 ? 'Their circle isn’t scanned yet.' : 'No one here matches.'}
149
+ </div>
150
+ ) : (
151
+ <div role="list" style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
152
+ {list.slice(0, shown).map((m) => <Row key={m.id} person={m} tierColors={tierColors} onSelect={onSelect} />)}
153
+ {list.length > shown && (
154
+ <button type="button" onClick={() => setShown((n) => n + PAGE * 3)} style={{
155
+ background: 'none', border: 'none', color: '#888', fontSize: 11, fontWeight: 600, cursor: 'pointer', padding: '6px 0', textAlign: 'left',
156
+ }}>Show more ({(list.length - shown).toLocaleString('en-US')} left)</button>
157
+ )}
158
+ </div>
159
+ )}
160
+ </div>
161
+ );
162
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- // The Scores tab's company scores (app/scores): every company in your network, the score it carries into
3
+ // Settings → Scores → company scores (app/settings): every company in your network, the score it carries into
4
4
  // people's power, where that score comes from, and a control to set your own.
5
5
  // Setting a score rescores everyone (app/api/company-scores → lib/scoring.js).
6
6
 
@@ -61,7 +61,7 @@ export function ScorePicker({ company, onSet, compact }) {
61
61
 
62
62
  export default function CompanyScores({ onRescored, version = 0 }) {
63
63
  const { companies, error, setScore, reload } = useCompanyScores();
64
- // A save beside the list (your sector or tiers, on the Scores tab) rescores
64
+ // A save beside the list (your sector or tiers, in Settings) rescores
65
65
  // everyone: `version` changes, and the list is read again, keeping what you
66
66
  // searched and filtered.
67
67
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  // The Scan page's one question before the first scan: what field you're in.
4
- // The same picks as Scores → Your sector (SectorPicker), saved the same way
4
+ // The same picks as Settings → Scores → Your field (SectorPicker), saved the same way
5
5
  // (lib/settings-client.js), so the first scan is already scored with them.
6
6
  // Optional: Skip for now carries on without one. Either answer is kept
7
7
  // (`fieldAsked`, lib/sector-focus.js), so it's asked once; when it's asked at
@@ -53,7 +53,7 @@ export default function FieldStep({ onDone }) {
53
53
  <h2 id="field-question" style={{ fontSize: 18, fontWeight: 700, margin: '6px 0 4px' }}>What field are you in?</h2>
54
54
  <Body style={{ marginTop: 0, marginBottom: 14 }}>
55
55
  Pick up to three. Companies in your field count for more when your network is scored, so the people there rank
56
- higher. You can change it anytime on the Scores tab.
56
+ higher. You can change it anytime in Settings.
57
57
  </Body>
58
58
 
59
59
  <SectorPicker sectors={sectors} onChange={setSectors} disabled={Boolean(saving)} />
@@ -82,13 +82,13 @@ export default function FieldStep({ onDone }) {
82
82
  /** What was answered, above the scan's steps: where it went, and where to change it. */
83
83
  export function FieldAnswer({ sectors }) {
84
84
  if (!sectors.length) {
85
- return <Status>No field for now. You can pick one anytime on <Link href="/scores#sector" style={link}>Scores</Link>.</Status>;
85
+ return <Status>No field for now. You can pick one anytime in <Link href="/settings#sector" style={link}>Settings</Link>.</Status>;
86
86
  }
87
87
  // The labels have commas of their own ("Tech, Software & AI"), so a dot separates them.
88
88
  return (
89
89
  <Status tone="ok">
90
90
  Your field: {sectors.map((k) => sectorByKey(k).label).join(' · ')}. Your first scan is scored with it. Change it
91
- anytime on <Link href="/scores#sector" style={link}>Scores</Link>.
91
+ anytime in <Link href="/settings#sector" style={link}>Settings</Link>.
92
92
  </Status>
93
93
  );
94
94
  }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
- import { viewsForMode } from './views';
5
4
  import GalaxyLab, { NamesSwitch } from './GalaxyLab';
5
+ import { TIERS, GRID_DEGREES, showing, showingIn, toggleCell, toggleTier, toggleDegree, showAllTiers } from '../../lib/tier-grid';
6
6
 
7
7
  function useIsMobile() {
8
8
  const [m, setM] = useState(false);
@@ -13,19 +13,124 @@ function useIsMobile() {
13
13
  const TIER_COLORS = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
14
14
 
15
15
 
16
- export default function FilterPanel({ collapsed, onToggle, mode, filter, onFilterChange, visualMode, onVisualModeChange, tierCounts, bridgeTierCounts, degrees = [1], onDegreesChange, degreeCounts = {}, hiddenTiers = [], onHiddenTiersChange }) {
16
+ const DEGREE_NAMES = ['', '1st', '2nd', '3rd', '4th', '5th', '6th'];
17
+ // What each degree is, per tab: Network Circle draws people; Degrees draws bridges and their circles.
18
+ const DEGREE_IS = {
19
+ network: { 1: 'your connections', 2: 'people in a scanned circle', 3: 'people found by a company scan' },
20
+ degrees: { 1: 'your bridges', 2: 'people in their circles' },
21
+ paths: { 1: 'your connections', 2: 'people in a scanned circle', 3: 'people found by a company scan' },
22
+ };
23
+
24
+ /**
25
+ * Tiers down, six degrees across: tap a tier to hide or show it, a dot to show
26
+ * that degree for that tier, a number at the top for that degree in every tier.
27
+ * The same in Network Circle, Degrees and Paths (lib/tier-grid.js).
28
+ */
29
+ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
30
+ const total = showing(grid, counts);
31
+ const dot = isMobile ? 21 : 23;
32
+ const anyAt = (d) => TIERS.some((t) => (counts[t]?.[d] || 0) > 0);
33
+ const what = (d) => DEGREE_IS[mode]?.[d];
34
+ return (
35
+ <div style={{ marginBottom: 18 }}>
36
+ <div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
37
+ <div style={{ flex: 1, fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, textTransform: 'uppercase' }}>
38
+ Tiers
39
+ </div>
40
+ {/* A degree, for every tier at once */}
41
+ <div style={{ display: 'flex', gap: 3 }}>
42
+ {GRID_DEGREES.map((d) => {
43
+ const any = anyAt(d);
44
+ const on = any && TIERS.every((t) => grid.hidden.includes(t) || !(counts[t]?.[d] > 0) || grid.degrees[t].includes(d));
45
+ return (
46
+ <button key={d} type="button" disabled={!any} aria-pressed={on}
47
+ title={any ? `${DEGREE_NAMES[d]} degree${what(d) ? `: ${what(d)}` : ''}. Tap to ${on ? 'hide' : 'show'} it for every tier` : `Nobody at ${DEGREE_NAMES[d]} degree yet`}
48
+ onClick={() => onChange(toggleDegree(grid, d, counts))}
49
+ style={{
50
+ width: dot, height: 18, padding: 0, border: 'none', background: 'none', cursor: any ? 'pointer' : 'default',
51
+ fontSize: 10, fontWeight: 800, color: !any ? '#333' : on ? '#cfe6f7' : '#778',
52
+ }}>
53
+ {d}
54
+ </button>
55
+ );
56
+ })}
57
+ </div>
58
+ <div style={{ width: 44 }} />
59
+ </div>
60
+
61
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
62
+ {TIERS.map((t) => {
63
+ const off = grid.hidden.includes(t);
64
+ const n = showingIn(grid, counts, t);
65
+ const color = TIER_COLORS[t];
66
+ return (
67
+ <div key={t} style={{
68
+ display: 'flex', alignItems: 'center', borderRadius: 8,
69
+ background: off ? 'rgba(255,255,255,0.02)' : `${color}14`,
70
+ borderLeft: `3px solid ${off ? 'transparent' : color}`,
71
+ }}>
72
+ <button type="button" aria-pressed={!off} title={off ? `Show ${t}-Tier` : `Hide ${t}-Tier`}
73
+ onClick={() => onChange(toggleTier(grid, t, counts))}
74
+ style={{
75
+ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 8, padding: '12px 6px 12px 10px',
76
+ border: 'none', background: 'none', cursor: 'pointer', color: off ? '#555' : color, textAlign: 'left',
77
+ }}>
78
+ <span style={{ width: 11, height: 11, borderRadius: '50%', background: color, opacity: off ? 0.2 : 1, flexShrink: 0 }} />
79
+ <span style={{ fontSize: 13.5, fontWeight: 700, whiteSpace: 'nowrap', textDecoration: off ? 'line-through' : 'none' }}>{t}-Tier</span>
80
+ </button>
81
+ <div style={{ display: 'flex', gap: 3 }}>
82
+ {GRID_DEGREES.map((d) => {
83
+ const people = counts[t]?.[d] || 0;
84
+ const on = !off && people > 0 && grid.degrees[t].includes(d);
85
+ return (
86
+ <button key={d} type="button" disabled={!people} aria-pressed={on}
87
+ aria-label={`${t}-Tier at ${DEGREE_NAMES[d]} degree`}
88
+ title={people
89
+ ? `${people.toLocaleString('en-US')} ${t}-Tier at ${DEGREE_NAMES[d]} degree. Tap to ${on ? 'hide' : 'show'} them`
90
+ : `No ${t}-Tier at ${DEGREE_NAMES[d]} degree yet`}
91
+ onClick={() => onChange(toggleCell(grid, t, d, counts))}
92
+ style={{
93
+ width: dot, height: dot, padding: 0, borderRadius: '50%', boxSizing: 'border-box',
94
+ cursor: people ? 'pointer' : 'default', fontSize: 10, fontWeight: 800, lineHeight: 1,
95
+ border: !people ? '1px dashed rgba(255,255,255,0.1)' : on ? `1.5px solid ${color}` : `1.5px solid ${color}70`,
96
+ background: on ? color : 'transparent',
97
+ color: !people ? 'rgba(255,255,255,0.12)' : on ? '#0a0a1a' : `${color}b0`,
98
+ opacity: off && people ? 0.45 : 1,
99
+ }}>
100
+ {d}
101
+ </button>
102
+ );
103
+ })}
104
+ </div>
105
+ <div style={{ width: 44, textAlign: 'right', paddingRight: 8, fontSize: 10.5, color: off || !n ? '#445' : '#889', fontVariantNumeric: 'tabular-nums' }}>
106
+ {n.toLocaleString('en-US')}
107
+ </div>
108
+ </div>
109
+ );
110
+ })}
111
+ </div>
112
+
113
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 8 }}>
114
+ <div style={{ fontSize: 10, color: '#667', lineHeight: 1.4 }}>
115
+ Tap a tier to hide it, or a dot to show that degree.
116
+ </div>
117
+ <div style={{ fontSize: 10.5, color: '#889', whiteSpace: 'nowrap', marginLeft: 8 }}>
118
+ {total.toLocaleString('en-US')} showing
119
+ {grid.hidden.length > 0 && (
120
+ <button type="button" onClick={() => onChange(showAllTiers(grid))} style={{
121
+ marginLeft: 8, padding: '2px 8px', borderRadius: 10, fontSize: 10, cursor: 'pointer',
122
+ border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.05)', color: '#aab',
123
+ }}>All tiers</button>
124
+ )}
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ export default function FilterPanel({ collapsed, onToggle, mode, visualMode, grid, gridCounts = {}, onGridChange }) {
17
132
  const isMobile = useIsMobile();
18
133
  const isDegreesMode = mode === 'degrees';
19
- // Network Circle: each tier switches on and off, so the noisy ones (C, D) can go
20
- // while the rest stay. Degrees keeps one tier at a time.
21
- const toggles = !isDegreesMode && !!onHiddenTiersChange;
22
- // Same source of truth the renderer uses, so the menu can never offer a
23
- // view that does not exist or miss one that does.
24
- const modes = viewsForMode(isDegreesMode ? 'degrees' : 'network');
25
- const counts = isDegreesMode ? (bridgeTierCounts || {}) : (tierCounts || {});
26
- const allCount = isDegreesMode
27
- ? Object.values(bridgeTierCounts || {}).reduce((s, v) => s + v, 0)
28
- : Object.values(tierCounts || {}).reduce((s, v) => s + v, 0);
29
134
 
30
135
  if (collapsed) {
31
136
  return (
@@ -80,178 +185,11 @@ export default function FilterPanel({ collapsed, onToggle, mode, filter, onFilte
80
185
  <span style={{ fontSize: 16 }}>›</span> Close
81
186
  </button>
82
187
 
83
- {/* Visual Mode */}
84
- <div style={{ marginBottom: 18 }}>
85
- <div style={{ fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, marginBottom: 8, textTransform: 'uppercase' }}>
86
- Visual
87
- </div>
88
- <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
89
- {modes.map(m => (
90
- <button
91
- key={m.key}
92
- onClick={() => onVisualModeChange && onVisualModeChange(m.key)}
93
- style={{
94
- display: 'flex', alignItems: 'center', gap: 8, padding: '7px 10px',
95
- borderRadius: 6, border: 'none', cursor: 'pointer', width: '100%', textAlign: 'left',
96
- background: visualMode === m.key ? 'rgba(52,152,219,0.15)' : 'rgba(255,255,255,0.03)',
97
- color: visualMode === m.key ? '#3498DB' : '#888',
98
- transition: 'all 0.15s',
99
- }}
100
- >
101
- <span style={{ fontSize: 14, width: 20, textAlign: 'center' }}>{m.icon}</span>
102
- <div>
103
- <div style={{ fontSize: 11, fontWeight: 600 }}>{m.label}</div>
104
- <div style={{ fontSize: 8, color: '#555', marginTop: 1 }}>{m.desc}</div>
105
- </div>
106
- </button>
107
- ))}
108
- </div>
109
- </div>
110
-
111
- {/* Tier Filters */}
112
- {/* Network Circle: which degrees to draw. Any mix, never none: 2nd and
113
- 3rd without your own connections shows who's valuable outside them. */}
114
- {!isDegreesMode && onDegreesChange && (
115
- <div style={{ marginBottom: 18 }}>
116
- <div style={{ fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, marginBottom: 8, textTransform: 'uppercase' }}>
117
- Degree
118
- </div>
119
- <div style={{ display: 'flex', gap: 6 }}>
120
- {[[1, '1st', 'Your connections'], [2, '2nd', 'In a scanned circle'], [3, '3rd', 'Found by a company scan']].map(([d, label, what]) => {
121
- const on = degrees.includes(d);
122
- const count = degreeCounts[d] || 0;
123
- const last = on && degrees.length === 1;
124
- return (
125
- <button
126
- key={d}
127
- title={`${what}: ${count.toLocaleString()}${last ? ' (keep at least one)' : ''}`}
128
- aria-pressed={on}
129
- disabled={last || (!on && !count)}
130
- onClick={() => onDegreesChange(on ? degrees.filter((x) => x !== d) : [...degrees, d].sort())}
131
- style={{
132
- flex: 1, padding: '7px 4px', borderRadius: 8, fontSize: 11.5, fontWeight: 700,
133
- cursor: last || (!on && !count) ? 'default' : 'pointer',
134
- border: on ? '1px solid rgba(52,152,219,0.6)' : '1px solid rgba(255,255,255,0.08)',
135
- background: on ? 'rgba(52,152,219,0.18)' : 'rgba(255,255,255,0.03)',
136
- color: on ? '#cfe6f7' : count ? '#888' : '#444',
137
- }}
138
- >
139
- {label}
140
- <div style={{ fontSize: 9.5, fontWeight: 500, opacity: 0.8, marginTop: 2 }}>{count.toLocaleString()}</div>
141
- </button>
142
- );
143
- })}
144
- </div>
145
- </div>
146
- )}
147
-
148
- {!isDegreesMode && visualMode === 'galaxy' && <NamesSwitch />}
188
+ {/* Which tiers, at which degrees: the same grid in Network Circle and in Degrees */}
189
+ <TierGrid grid={grid} counts={gridCounts} onChange={onGridChange} mode={isDegreesMode ? 'degrees' : 'network'} isMobile={isMobile} />
149
190
 
150
- <div style={{ marginBottom: 18 }}>
151
- {/* In Degrees these chips filter by the tier of the connection who
152
- introduces you, not the person reached — Separation has its own
153
- "Their tier" chips for that, so the two must not read alike. */}
154
- <div style={{ fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, marginBottom: 8, textTransform: 'uppercase' }}>
155
- {isDegreesMode ? 'Filter by bridge tier' : 'Filter'}
156
- </div>
157
- <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
158
- {/* All button */}
159
- <button
160
- onClick={() => (toggles ? onHiddenTiersChange([]) : onFilterChange && onFilterChange('all'))}
161
- style={{
162
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
163
- padding: '7px 10px', borderRadius: 6, border: 'none', cursor: 'pointer', width: '100%',
164
- background: (toggles ? !hiddenTiers.length : filter === 'all') ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.03)',
165
- color: (toggles ? !hiddenTiers.length : filter === 'all') ? '#fff' : '#888',
166
- }}
167
- >
168
- <span style={{ fontSize: 11, fontWeight: 600 }}>All</span>
169
- <span style={{ fontSize: 10, color: '#555' }}>{allCount}</span>
170
- </button>
171
- {/* Tier buttons */}
172
- {toggles && (
173
- <div style={{ fontSize: 10, color: '#667', margin: '2px 2px 4px' }}>Click a tier to hide or show it</div>
174
- )}
175
- {toggles && ['S', 'A', 'B', 'C', 'D'].map(t => {
176
- const on = !hiddenTiers.includes(t);
177
- const last = on && hiddenTiers.length >= 4;
178
- return (
179
- <button
180
- key={t}
181
- aria-pressed={on}
182
- title={last ? 'Keep at least one tier showing' : on ? `Hide ${t}-Tier` : `Show ${t}-Tier`}
183
- onClick={() => !last && onHiddenTiersChange(on ? [...hiddenTiers, t] : hiddenTiers.filter(x => x !== t))}
184
- style={{
185
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
186
- padding: '7px 10px', borderRadius: 6, border: 'none', cursor: last ? 'default' : 'pointer', width: '100%',
187
- background: on ? `${TIER_COLORS[t]}14` : 'rgba(255,255,255,0.02)',
188
- color: on ? TIER_COLORS[t] : '#555',
189
- borderLeft: on ? `3px solid ${TIER_COLORS[t]}` : '3px solid transparent',
190
- }}
191
- >
192
- <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
193
- <div style={{ width: 8, height: 8, borderRadius: '50%', background: TIER_COLORS[t], opacity: on ? 1 : 0.2 }} />
194
- <span style={{ fontSize: 11, fontWeight: 600, textDecoration: on ? 'none' : 'line-through' }}>{t}-Tier</span>
195
- {!on && <span style={{ fontSize: 9.5, color: '#667' }}>hidden</span>}
196
- </div>
197
- <span style={{ fontSize: 10, color: '#555' }}>{counts[t] || 0}</span>
198
- </button>
199
- );
200
- })}
201
- {!toggles && ['S', 'A', 'B', 'C', 'D'].map(t => (
202
- <button
203
- key={t}
204
- onClick={() => onFilterChange && onFilterChange(t)}
205
- style={{
206
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
207
- padding: '7px 10px', borderRadius: 6, border: 'none', cursor: 'pointer', width: '100%',
208
- background: filter === t ? `${TIER_COLORS[t]}20` : 'rgba(255,255,255,0.03)',
209
- color: filter === t ? TIER_COLORS[t] : '#888',
210
- borderLeft: filter === t ? `3px solid ${TIER_COLORS[t]}` : '3px solid transparent',
211
- }}
212
- >
213
- <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
214
- <div style={{
215
- width: 8, height: 8, borderRadius: '50%',
216
- background: TIER_COLORS[t], opacity: filter === t ? 1 : 0.4,
217
- }} />
218
- <span style={{ fontSize: 11, fontWeight: 600 }}>{t}-Tier</span>
219
- </div>
220
- <span style={{ fontSize: 10, color: '#555' }}>{counts[t] || 0}</span>
221
- </button>
222
- ))}
223
- </div>
224
- </div>
225
-
226
- {/* The Galaxy's physics lab (experimental): sliders and a replay, on a computer. */}
227
- {!isDegreesMode && visualMode === 'galaxy' && !isMobile && <GalaxyLab />}
228
-
229
- {/* Stats */}
230
- <div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: 12 }}>
231
- <div style={{ fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, marginBottom: 8, textTransform: 'uppercase' }}>
232
- Stats
233
- </div>
234
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
235
- {Object.entries(counts).filter(([k]) => ['S', 'A', 'B'].includes(k)).map(([tier, count]) => (
236
- <div key={tier} style={{
237
- padding: '6px 8px', borderRadius: 6,
238
- background: `${TIER_COLORS[tier]}10`,
239
- textAlign: 'center',
240
- }}>
241
- <div style={{ fontSize: 16, fontWeight: 800, color: TIER_COLORS[tier] }}>{count}</div>
242
- <div style={{ fontSize: 8, color: '#555' }}>{tier}-Tier</div>
243
- </div>
244
- ))}
245
- <div style={{
246
- padding: '6px 8px', borderRadius: 6,
247
- background: 'rgba(255,255,255,0.04)',
248
- textAlign: 'center',
249
- }}>
250
- <div style={{ fontSize: 16, fontWeight: 800, color: '#fff' }}>{allCount}</div>
251
- <div style={{ fontSize: 8, color: '#555' }}>Total</div>
252
- </div>
253
- </div>
254
- </div>
191
+ {/* Network Circle's Galaxy: the physics that lays it out is what this panel is for. */}
192
+ {!isDegreesMode && visualMode === 'galaxy' && (isMobile ? <NamesSwitch /> : <GalaxyLab />)}
255
193
  </div>
256
194
  </div>
257
195
  );
@@ -6,7 +6,7 @@ import { localPhoto } from '../../lib/photos';
6
6
  import { recentre } from '../../lib/galaxy';
7
7
  import { reachIndex, readyByCircle, scanBars } from '../../lib/reach';
8
8
  import { ringSegments, RING } from '../../lib/dot-rings';
9
- import { LAB_DEFAULTS, labNow, watchLab, effectiveLab, clockNow, watchClock, setClock, stopReplay, bornTimes, reachCounts, colourScheme, findMatches, loadSocial, chapterAt } from '../../lib/galaxy-lab';
9
+ import { LAB_DEFAULTS, labNow, watchLab, effectiveLab, clockNow, watchClock, setClock, stopReplay, bornTimes, reachCounts, colourScheme, findMatches, loadSocial, chapterAt, heatColour } from '../../lib/galaxy-lab';
10
10
  import { registerGalaxy } from '../../lib/galaxy-export';
11
11
 
12
12
  // Connection fields are attacker-reachable: /api/ingest and /api/update-images
@@ -23,6 +23,7 @@ function esc(value) {
23
23
  // The id of the node at the centre, you. Not a connection's id (those come from
24
24
  // the database), and never shown.
25
25
  const CENTER_ID = '__center__';
26
+ const HEAT_GLOWS = 700; // glows drawn under Colour by → Heat, hottest first
26
27
 
27
28
  // Builds the avatar tooltip through the DOM rather than a string, so no value
28
29
  // can break out of the attribute it is written into. Only a photo saved on this
@@ -510,6 +511,30 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
510
511
  }
511
512
  });
512
513
 
514
+ // Heat (Colour by → Heat): a soft glow behind the hotter dots, screen-blended
515
+ // so the glows add up where powerful people cluster, like a thermal camera.
516
+ // The hottest HEAT_GLOWS only, so a big network stays smooth.
517
+ let heatFn = scheme?.heat || null;
518
+ const heatDefs = svg.select('defs').size() ? svg.select('defs') : svg.append('defs');
519
+ for (let k = 0; k <= 5; k++) {
520
+ if (heatDefs.select(`#heat-glow-${k}`).size()) continue;
521
+ const grad = heatDefs.append('radialGradient').attr('id', `heat-glow-${k}`);
522
+ grad.append('stop').attr('offset', '0').attr('stop-color', heatColour(k / 5)).attr('stop-opacity', 0.14 + 0.06 * k);
523
+ grad.append('stop').attr('offset', '1').attr('stop-color', heatColour(k / 5)).attr('stop-opacity', 0);
524
+ }
525
+ const heatG = g.append('g').attr('class', 'heat-glow').style('pointer-events', 'none').style('mix-blend-mode', 'screen');
526
+ let heatDots = heatG.selectAll('circle');
527
+ const drawHeat = () => {
528
+ const hot = heatFn
529
+ ? nodes.filter(n => n.id !== CENTER_ID && heatFn(n) >= 0.5).sort((a, b) => heatFn(b) - heatFn(a)).slice(0, HEAT_GLOWS)
530
+ : [];
531
+ heatDots = heatG.selectAll('circle').data(hot, d => d.id).join('circle')
532
+ .attr('r', d => nodeRadius(d) * 2.5 + 12 + 30 * heatFn(d))
533
+ .attr('fill', d => `url(#heat-glow-${Math.round(heatFn(d) * 5)})`)
534
+ .attr('cx', d => d.x || 0).attr('cy', d => d.y || 0)
535
+ .style('display', d => (clockAt == null || bornById.get(d.id) <= clockAt ? null : 'none'));
536
+ };
537
+
513
538
  // forceLink has already swapped each link's ids for the nodes themselves, so
514
539
  // the colour is one step away. It used to search every node for every link:
515
540
  // about 1.8 s at 30,000 people.
@@ -612,8 +637,12 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
612
637
  g.classed('lab-focus', true);
613
638
  };
614
639
 
640
+ // Moving from one dot to the next shouldn't flash the whole Galaxy back on in
641
+ // between: the dimming lifts a moment after the pointer leaves, unless it lands on another.
642
+ let unlight = null;
615
643
  node.on('mouseover', function (event, d) {
616
644
  easeRadius(this, nodeRadius(d) * 1.5);
645
+ clearTimeout(unlight);
617
646
  if (L.on && L.branch && d.id !== CENTER_ID) lightBranch(d);
618
647
  if (d.profile_image_url && d.id !== CENTER_ID) {
619
648
  const size = Math.max(48, nodeRadius(d) * 5);
@@ -642,7 +671,8 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
642
671
  }
643
672
  }).on('mouseout', function (event, d) {
644
673
  easeRadius(this, nodeRadius(d));
645
- g.classed('lab-focus', false);
674
+ clearTimeout(unlight);
675
+ unlight = setTimeout(() => g.classed('lab-focus', false), 120);
646
676
  tooltip.style('opacity', 0);
647
677
  photoTooltip.style('opacity', 0);
648
678
  });
@@ -686,6 +716,7 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
686
716
  catalystRings.attr('cx', d => d.x).attr('cy', d => d.y);
687
717
  dotRings.attr('transform', d => `translate(${d.x},${d.y})`);
688
718
  labels.attr('x', d => d.x).attr('y', d => d.y);
719
+ if (heatFn) heatDots.attr('cx', d => d.x).attr('cy', d => d.y);
689
720
  placeRing();
690
721
  });
691
722
 
@@ -735,7 +766,12 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
735
766
  const dated = d1.map(n => bornById.get(n.id)).filter(Number.isFinite);
736
767
  const range = { min: dated.length ? dated.reduce((m, t) => Math.min(m, t)) : null, max: dated.length ? dated.reduce((m, t) => Math.max(m, t)) : null, of: d1.length };
737
768
  let clockAt = null;
738
- const showBorn = () => labels.style('display', d => (clockAt == null || bornById.get(d.id) <= clockAt ? null : 'none'));
769
+ const showBorn = () => {
770
+ const born = d => (clockAt == null || bornById.get(d.id) <= clockAt ? null : 'none');
771
+ labels.style('display', born);
772
+ if (heatFn) heatDots.style('display', born);
773
+ };
774
+ drawHeat();
739
775
  const setTime = (at) => {
740
776
  if (!L.on) at = null;
741
777
  clockAt = at;
@@ -763,6 +799,8 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
763
799
  // A new colour scheme (Colour by): recolour the dots, lines and names in place.
764
800
  const setColours = (next) => {
765
801
  colourOf = next.of;
802
+ heatFn = next.heat || null;
803
+ drawHeat();
766
804
  node.attr('fill', d => d.id === CENTER_ID ? '#fff' : colourOf(d));
767
805
  link.attr('stroke', d => colourOf(d.target) || '#333');
768
806
  drawLabels();