six-degrees 0.3.0 → 0.4.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 (427) 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 +35 -7
  5. package/.next/standalone/.next/required-server-files.json +6 -1
  6. package/.next/standalone/.next/routes-manifest.json +18 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  28. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  51. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +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.nft.json +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/unlock/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  61. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  63. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/import.html +1 -1
  68. package/.next/standalone/.next/server/app/import.rsc +13 -13
  69. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  70. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  71. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.html +1 -1
  73. package/.next/standalone/.next/server/app/index.rsc +13 -13
  74. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  75. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  77. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/launch.html +2 -2
  80. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  81. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  83. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  84. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  86. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/.next/server/app/paths.html +1 -1
  89. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  90. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  91. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  92. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  93. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/app/profile.html +1 -1
  96. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  98. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  99. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  100. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/app/queue.html +1 -1
  103. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  105. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  106. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  107. package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
  108. package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
  109. package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
  110. package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
  111. package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
  112. package/.next/standalone/.next/server/app/scores/page.js +13 -0
  113. package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
  114. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
  115. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
  116. package/.next/standalone/.next/server/app/scores.html +1 -0
  117. package/.next/standalone/.next/server/app/scores.meta +26 -0
  118. package/.next/standalone/.next/server/app/scores.rsc +24 -0
  119. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
  120. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
  121. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
  122. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/standalone/.next/server/app/settings.html +1 -1
  125. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  126. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  127. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  129. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  131. package/.next/standalone/.next/server/app/setup.html +1 -1
  132. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  133. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  134. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  135. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  136. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  159. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  162. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  165. package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
  166. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
  168. package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
  169. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  170. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  171. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  172. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
  173. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  175. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  176. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  178. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  185. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  186. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
  190. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
  201. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  202. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  204. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  205. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  206. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  207. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
  208. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  209. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  210. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
  211. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
  212. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
  213. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  214. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  215. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  216. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  217. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  218. package/.next/standalone/.next/server/pages/404.html +1 -1
  219. package/.next/standalone/.next/server/pages/500.html +1 -1
  220. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  221. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  222. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  223. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  224. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  225. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  226. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  228. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  229. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  230. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  231. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  232. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  233. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  234. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  235. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  236. package/.next/standalone/CHANGELOG.md +286 -0
  237. package/.next/standalone/CONTRIBUTING.md +15 -4
  238. package/.next/standalone/README.md +50 -29
  239. package/.next/standalone/SECURITY.md +10 -0
  240. package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
  241. package/.next/standalone/app/api/company-scores/route.js +1 -1
  242. package/.next/standalone/app/api/ingest/route.js +23 -2
  243. package/.next/standalone/app/api/outreach/route.js +107 -44
  244. package/.next/standalone/app/api/scraper/route.js +146 -17
  245. package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
  246. package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
  247. package/.next/standalone/app/api/update-images/route.js +41 -7
  248. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  249. package/.next/standalone/app/components/Avatar.js +5 -2
  250. package/.next/standalone/app/components/ChainView.js +659 -469
  251. package/.next/standalone/app/components/CompanyScores.js +16 -10
  252. package/.next/standalone/app/components/EmptyState.js +1 -1
  253. package/.next/standalone/app/components/FieldStep.js +94 -0
  254. package/.next/standalone/app/components/ForceGraph.js +183 -768
  255. package/.next/standalone/app/components/GridView.js +9 -7
  256. package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
  257. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  258. package/.next/standalone/app/components/ListView.js +6 -4
  259. package/.next/standalone/app/components/MappingProgress.js +5 -6
  260. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  261. package/.next/standalone/app/components/OrbitGraph.js +33 -1
  262. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  263. package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
  264. package/.next/standalone/app/components/RingsView.js +9 -8
  265. package/.next/standalone/app/components/SeparationView.js +96 -14
  266. package/.next/standalone/app/components/Sidebar.js +366 -287
  267. package/.next/standalone/app/components/TheirCircle.js +300 -0
  268. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  269. package/.next/standalone/app/components/settings/SectorSection.js +21 -195
  270. package/.next/standalone/app/components/settings/TierSection.js +99 -0
  271. package/.next/standalone/app/components/useRequests.js +12 -0
  272. package/.next/standalone/app/components/useScanner.js +13 -0
  273. package/.next/standalone/app/components/views.js +3 -5
  274. package/.next/standalone/app/globals.css +7 -0
  275. package/.next/standalone/app/launch/page.js +6 -3
  276. package/.next/standalone/app/page.js +183 -74
  277. package/.next/standalone/app/paths/page.js +20 -14
  278. package/.next/standalone/app/profile/page.js +3 -3
  279. package/.next/standalone/app/queue/page.js +56 -60
  280. package/.next/standalone/app/scores/page.js +72 -0
  281. package/.next/standalone/app/settings/page.js +15 -3
  282. package/.next/standalone/app/setup/page.js +423 -260
  283. package/.next/standalone/db/schema.js +2 -1
  284. package/.next/standalone/eslint.config.mjs +3 -3
  285. package/.next/standalone/install.sh +3 -3
  286. package/.next/standalone/lib/chain-layout.js +139 -0
  287. package/.next/standalone/lib/circle.js +169 -0
  288. package/.next/standalone/lib/companies.js +1 -1
  289. package/.next/standalone/lib/csv.js +21 -10
  290. package/.next/standalone/lib/data-export.js +2 -1
  291. package/.next/standalone/lib/data-folder.js +4 -2
  292. package/.next/standalone/lib/db-client.js +1 -0
  293. package/.next/standalone/lib/galaxy.js +12 -0
  294. package/.next/standalone/lib/ingest.js +27 -8
  295. package/.next/standalone/lib/legacy-offer.js +7 -6
  296. package/.next/standalone/lib/legacy-scores.js +2 -2
  297. package/.next/standalone/lib/linkedin-limits.js +30 -8
  298. package/.next/standalone/lib/photos.js +47 -0
  299. package/.next/standalone/lib/quest.js +8 -3
  300. package/.next/standalone/lib/rarity.js +80 -0
  301. package/.next/standalone/lib/reach.js +113 -0
  302. package/.next/standalone/lib/requests-client.js +113 -0
  303. package/.next/standalone/lib/rpc.js +26 -15
  304. package/.next/standalone/lib/scan-state.js +1 -0
  305. package/.next/standalone/lib/scanner-setup.js +20 -1
  306. package/.next/standalone/lib/score-guess.js +60 -0
  307. package/.next/standalone/lib/scoring.js +1761 -69
  308. package/.next/standalone/lib/scraper-client.js +235 -17
  309. package/.next/standalone/lib/sector-directory.js +1 -1
  310. package/.next/standalone/lib/sector-focus.js +17 -3
  311. package/.next/standalone/lib/separation.js +4 -2
  312. package/.next/standalone/lib/settings-client.js +21 -0
  313. package/.next/standalone/lib/settings-effects.js +19 -1
  314. package/.next/standalone/lib/settings.js +7 -2
  315. package/.next/standalone/next.config.mjs +25 -3
  316. package/.next/standalone/package-lock.json +2 -2
  317. package/.next/standalone/package.json +2 -2
  318. package/.next/standalone/public/demo-data.json +1 -1
  319. package/.next/standalone/scripts/build-app.mjs +11 -2
  320. package/.next/standalone/scripts/check-node.mjs +1 -1
  321. package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
  322. package/.next/standalone/scripts/image_store.py +77 -21
  323. package/.next/standalone/scripts/scrape.py +305 -29
  324. package/.next/standalone/server.js +1 -1
  325. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  326. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  327. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  328. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  329. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  330. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  331. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  332. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  333. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  334. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  335. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  336. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  337. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  338. package/CHANGELOG.md +286 -0
  339. package/README.md +50 -29
  340. package/SECURITY.md +10 -0
  341. package/db/schema.js +2 -1
  342. package/lib/chain-layout.js +139 -0
  343. package/lib/circle.js +169 -0
  344. package/lib/companies.js +1 -1
  345. package/lib/csv.js +21 -10
  346. package/lib/data-export.js +2 -1
  347. package/lib/data-folder.js +4 -2
  348. package/lib/db-client.js +1 -0
  349. package/lib/galaxy.js +12 -0
  350. package/lib/ingest.js +27 -8
  351. package/lib/legacy-offer.js +7 -6
  352. package/lib/legacy-scores.js +2 -2
  353. package/lib/linkedin-limits.js +30 -8
  354. package/lib/photos.js +47 -0
  355. package/lib/quest.js +8 -3
  356. package/lib/rarity.js +80 -0
  357. package/lib/reach.js +113 -0
  358. package/lib/requests-client.js +113 -0
  359. package/lib/rpc.js +26 -15
  360. package/lib/scan-state.js +1 -0
  361. package/lib/scanner-setup.js +20 -1
  362. package/lib/score-guess.js +60 -0
  363. package/lib/scoring.js +1761 -69
  364. package/lib/scraper-client.js +235 -17
  365. package/lib/sector-directory.js +1 -1
  366. package/lib/sector-focus.js +17 -3
  367. package/lib/separation.js +4 -2
  368. package/lib/settings-client.js +21 -0
  369. package/lib/settings-effects.js +19 -1
  370. package/lib/settings.js +7 -2
  371. package/package.json +2 -2
  372. package/public/demo-data.json +1 -1
  373. package/scripts/check-node.mjs +1 -1
  374. package/scripts/image_store.py +77 -21
  375. package/scripts/scrape.py +305 -29
  376. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  378. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  379. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  380. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
  381. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  382. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  383. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  384. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  385. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  386. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  387. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
  388. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
  390. package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
  392. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  393. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  394. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
  395. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
  396. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
  397. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  398. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
  399. package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  400. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
  401. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
  402. package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
  403. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  404. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
  405. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  406. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
  407. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  408. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  409. package/.next/standalone/app/components/BridgeRing.js +0 -753
  410. package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  411. package/.next/static/chunks/08k1crid-05dt.js +0 -1
  412. package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  413. package/.next/static/chunks/0q-_khndfxubv.js +0 -2
  414. package/.next/static/chunks/1c79veqedvubt.js +0 -1
  415. package/.next/static/chunks/1chp3nx45meg5.js +0 -44
  416. package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  417. package/.next/static/chunks/1uadqylrod5_v.js +0 -1
  418. package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  419. package/.next/static/chunks/289ktj97di6zg.js +0 -1
  420. package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  421. package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  422. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  423. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  424. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  425. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  426. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  427. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
@@ -0,0 +1,300 @@
1
+ 'use client';
2
+
3
+ // Their circle, at the top of a profile card (Blake, 2026-09-28).
4
+ //
5
+ // The person in the middle; their scanned circle round them (D2); the people
6
+ // in it you connected with ringed in green, with their own circles fanning out
7
+ // behind them (D3), and so on out to D6. A request still out is a dotted dot.
8
+ // Each dot is coloured by its tier. Rarity (lib/rarity.js) is a filter beside
9
+ // the tier's rather than a mark on every dot (while it's counted from your
10
+ // scans, most people read as rare, and a ring on nearly everything says
11
+ // nothing): the two work together, so "S" and "Only way in" lights exactly the
12
+ // rare finds in this circle and dims the rest. Tap the map for the large one,
13
+ // where a dot opens that person's card.
14
+
15
+ import { useEffect, useMemo, useState } from 'react';
16
+ import { createPortal } from 'react-dom';
17
+ import { circleIndex, circleRings, circleLayout, requestedByDefault } from '../../lib/circle';
18
+ import { keyFor, routeIndex } from '../../lib/separation';
19
+ import { RARITY, rarityOf, passes, countByRarity, toggle, rarityInfo } from '../../lib/rarity';
20
+ import { scoreGuess } from '../../lib/score-guess';
21
+
22
+ const CLASSIC = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
23
+ const TIERS = ['S', 'A', 'B', 'C', 'D'];
24
+ const GREEN = '#00ff88';
25
+
26
+ export default function TheirCircle({ person, connections = [], degree2 = [], tierColors = CLASSIC, onSelect, isRequested = requestedByDefault, canScan = true }) {
27
+ const index = useMemo(() => circleIndex(connections, degree2), [connections, degree2]);
28
+ const ways = useMemo(() => routeIndex(degree2), [degree2]);
29
+ const { rings, totals } = useMemo(() => circleRings(person, index, { isRequested }), [person, index, isRequested]);
30
+ const [tiers, setTiers] = useState(() => new Set());
31
+ const [rarities, setRarities] = useState(() => new Set());
32
+ const [big, setBig] = useState(false);
33
+
34
+ // Each dot's tier and rarity. Your own connections have no rarity: you're
35
+ // already in.
36
+ const people = useMemo(() => rings.flatMap((ring) => ring.people.map((p) => {
37
+ const yours = p.state === 'connected' || p.row.degree === 1;
38
+ const r = yours ? null : rarityOf(p.row, ways.get(keyFor(p.row))?.size || 1);
39
+ return { key: p.key, tier: p.row.tier, rarity: r?.key ?? null, mutuals: r };
40
+ })), [rings, ways]);
41
+ const facts = useMemo(() => new Map(people.map((p) => [p.key, p])), [people]);
42
+ const filter = useMemo(() => ({ tiers, rarities }), [tiers, rarities]);
43
+ const shown = useMemo(() => people.filter((p) => passes(p, filter)).length, [people, filter]);
44
+
45
+ // How complete the ways-in count is: circles scanned out of your connections.
46
+ const scanned = useMemo(() => connections.filter((c) => index.circles.has(c.id)).length, [connections, index]);
47
+ const fromLinkedIn = people.some((p) => p.mutuals?.from === 'linkedin');
48
+ // A score the headline gave nothing to read for: say so, and point at the
49
+ // scan, which is what can firm it up (lib/score-guess.js).
50
+ const guess = useMemo(() => scoreGuess(person), [person]);
51
+
52
+ if (!person) return null;
53
+ // With the sample or a CSV import open there is no scan box below to point
54
+ // at (Sidebar.js says why in its place), so this only says what is known.
55
+ if (!rings.length) {
56
+ const first = person.name?.split(' ')[0] || 'them';
57
+ return (
58
+ <div style={guess ? { ...box, borderColor: 'rgba(255,215,0,0.28)' } : box}>
59
+ <Label>Their circle</Label>
60
+ {guess ? (
61
+ <>
62
+ <div style={{ fontSize: 11.5, color: '#d8ccb0', lineHeight: 1.5, marginBottom: canScan ? 9 : 0 }}>
63
+ <b style={{ color: '#FFD700' }}>Their score is a guess.</b> {guess.reason}
64
+ {canScan && <> Scanning {first}&apos;s circle shows who you can reach through them, and a strong
65
+ circle adds up to +2 to their score.</>}
66
+ </div>
67
+ {canScan && (
68
+ <button type="button" onClick={toScanBox} style={{
69
+ width: '100%', padding: '8px 10px', borderRadius: 8, cursor: 'pointer', fontSize: 12, fontWeight: 700,
70
+ border: '1px solid rgba(255,215,0,0.4)', background: 'rgba(255,215,0,0.08)', color: '#FFD700',
71
+ }}>
72
+ Scan their circle ↓
73
+ </button>
74
+ )}
75
+ </>
76
+ ) : canScan ? (
77
+ <div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>
78
+ Not scanned yet. Scan {first}&apos;s circle below to see who they know, and
79
+ everyone you reach through them after that.
80
+ </div>
81
+ ) : (
82
+ <div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>Not scanned.</div>
83
+ )}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ const byTier = Object.fromEntries(TIERS.map((t) => [t, people.filter((p) => p.tier === t).length]));
89
+ const byRarity = countByRarity(people);
90
+ const filtering = tiers.size > 0 || rarities.size > 0;
91
+
92
+ const chips = (
93
+ <Filters
94
+ tierColors={tierColors} byTier={byTier} byRarity={byRarity}
95
+ tiers={tiers} rarities={rarities}
96
+ onTier={(t) => setTiers((s) => toggle(s, t))} onRarity={(r) => setRarities((s) => toggle(s, r))}
97
+ onClear={() => { setTiers(new Set()); setRarities(new Set()); }}
98
+ />
99
+ );
100
+ const note = (
101
+ <div style={{ fontSize: 9.5, color: '#777', lineHeight: 1.45, marginTop: 6 }}>
102
+ Rarity is how many mutual connections lead to someone. It never changes a score.{' '}
103
+ {fromLinkedIn
104
+ ? 'LinkedIn’s own count where a scan saved it; otherwise the ways in from your scans.'
105
+ : `Counted from the ${scanned} ${scanned === 1 ? 'circle' : 'circles'} you’ve scanned, so it can only go up as you scan more.`}
106
+ </div>
107
+ );
108
+
109
+ return (
110
+ <div style={box}>
111
+ <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
112
+ <Label>Their circle</Label>
113
+ <button type="button" onClick={() => setBig(true)} style={linkButton} aria-label="Open the large map">
114
+ Enlarge ⤢
115
+ </button>
116
+ </div>
117
+ <button type="button" onClick={() => setBig(true)} aria-label={`${person.name}'s circle: open the large map`} style={{
118
+ display: 'block', width: '100%', padding: 0, border: 'none', background: 'none', cursor: 'zoom-in',
119
+ }}>
120
+ <RingMap person={person} rings={rings} facts={facts} filter={filter} tierColors={tierColors} size={240} />
121
+ </button>
122
+ <RingCounts rings={rings} totals={totals} shown={filtering ? shown : null} />
123
+ {chips}
124
+ {note}
125
+ {big && (
126
+ <Enlarged onClose={() => setBig(false)} title={`${person.name}’s circle`}>
127
+ <RingMap person={person} rings={rings} facts={facts} filter={filter} tierColors={tierColors} size={640} labels
128
+ onPick={(row) => { setBig(false); onSelect?.(row); }} />
129
+ <RingCounts rings={rings} totals={totals} shown={filtering ? shown : null} />
130
+ {chips}
131
+ <Legend />
132
+ {note}
133
+ </Enlarged>
134
+ )}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function RingMap({ person, rings, facts, filter, tierColors, size, labels = false, onPick }) {
140
+ const layout = useMemo(() => circleLayout(rings, size), [rings, size]);
141
+ const c = layout.center;
142
+ const k = size / 240;
143
+ return (
144
+ <svg viewBox={`0 0 ${size} ${size}`} width="100%" style={{ display: 'block', maxWidth: size, margin: '0 auto' }}
145
+ role="img" aria-label={`${person.name}'s circle, ${rings.length} ${rings.length === 1 ? 'ring' : 'rings'}`}>
146
+ {layout.rings.map((ring) => (
147
+ <g key={ring.degree}>
148
+ <circle cx={c.x} cy={c.y} r={ring.radius} fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth={0.6 * k} />
149
+ <text x={c.x} y={c.y - ring.radius - ring.dotR - 3 * k} textAnchor="middle" fontSize={6.5 * k} fill="#666" fontWeight={700}>
150
+ D{ring.degree}
151
+ </text>
152
+ </g>
153
+ ))}
154
+ {layout.spokes.map((s, i) => (
155
+ <line key={i} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="rgba(0,255,136,0.35)" strokeWidth={0.7 * k} />
156
+ ))}
157
+ {layout.rings.map((ring) => ring.items.map((it) => {
158
+ const f = facts.get(it.key);
159
+ const on = passes(f, filter);
160
+ const color = tierColors[it.row.tier] || '#888';
161
+ const r = it.state === 'connected' ? ring.dotR * 1.7 : ring.dotR;
162
+ const tip = [
163
+ `${it.row.name} · ${it.row.tier}-tier${it.row.power_score != null ? ` · ${Number(it.row.power_score).toFixed(1)}` : ''}`,
164
+ it.state === 'connected' ? 'You added them' : it.state === 'requested' ? 'Request sent' : null,
165
+ f?.mutuals ? `${rarityInfo(f.rarity)?.label}: ${f.mutuals.count} mutual${f.mutuals.count === 1 ? '' : 's'}${f.mutuals.from === 'scans' ? ' (from your scans)' : ''}` : null,
166
+ ].filter(Boolean).join('\n');
167
+ return (
168
+ <g key={it.key} opacity={on ? 1 : 0.12} style={onPick ? { cursor: 'pointer' } : undefined}
169
+ onClick={onPick ? (e) => { e.stopPropagation(); onPick(it.row); } : undefined}>
170
+ <title>{tip}</title>
171
+ {it.state === 'requested' ? (
172
+ <circle cx={it.x} cy={it.y} r={r * 1.25} fill="none" stroke={color} strokeWidth={Math.max(0.5, r * 0.45)}
173
+ strokeDasharray={`${Math.max(0.6, r * 0.55)} ${Math.max(0.5, r * 0.45)}`} />
174
+ ) : (
175
+ <circle cx={it.x} cy={it.y} r={r} fill={color}
176
+ stroke={it.state === 'connected' ? GREEN : 'none'} strokeWidth={Math.max(0.6, r * 0.3)} />
177
+ )}
178
+ {labels && it.state === 'connected' && (
179
+ <text x={it.x} y={it.y - r - 3} textAnchor="middle" fontSize={9} fill={GREEN} fontWeight={700}>
180
+ {String(it.row.name || '').split(' ')[0]}
181
+ </text>
182
+ )}
183
+ </g>
184
+ );
185
+ }))}
186
+ <circle cx={c.x} cy={c.y} r={7 * k} fill={tierColors[person.tier] || '#888'} stroke="#fff" strokeWidth={0.8 * k} />
187
+ <text x={c.x} y={c.y + 2.4 * k} textAnchor="middle" fontSize={6.5 * k} fontWeight={800} fill={person.tier === 'S' ? '#000' : '#fff'}>
188
+ {String(person.name || '?').charAt(0)}
189
+ </text>
190
+ </svg>
191
+ );
192
+ }
193
+
194
+ function RingCounts({ rings, totals, shown }) {
195
+ return (
196
+ <div style={{ fontSize: 10.5, color: '#aaa', marginTop: 6, lineHeight: 1.5 }}>
197
+ {rings.map((r) => `D${r.degree} ${r.people.length.toLocaleString('en-US')}`).join(' · ')}
198
+ {totals.connected > 0 && <span style={{ color: GREEN }}> · you added {totals.connected}</span>}
199
+ {totals.requested > 0 && <span style={{ color: '#FFD700' }}> · {totals.requested} {totals.requested === 1 ? 'request' : 'requests'} out</span>}
200
+ {shown != null && <span style={{ color: '#fff' }}> · {shown.toLocaleString('en-US')} shown</span>}
201
+ </div>
202
+ );
203
+ }
204
+
205
+ function Filters({ tierColors, byTier, byRarity, tiers, rarities, onTier, onRarity, onClear }) {
206
+ const chip = (on, color) => ({
207
+ height: 22, padding: '0 7px', borderRadius: 11, cursor: 'pointer', fontSize: 10, fontWeight: 700,
208
+ whiteSpace: 'nowrap', border: `1px solid ${on ? color : 'rgba(255,255,255,0.1)'}`,
209
+ background: on ? `${color}22` : 'rgba(255,255,255,0.03)', color: on ? color : '#999',
210
+ });
211
+ return (
212
+ <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 5 }}>
213
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
214
+ <span style={rowLabel}>Tier</span>
215
+ {TIERS.map((t) => (
216
+ <button key={t} type="button" aria-pressed={tiers.has(t)} onClick={() => onTier(t)} style={chip(tiers.has(t), tierColors[t] || '#888')}>
217
+ {t} <span style={{ fontWeight: 500, opacity: 0.8 }}>{byTier[t]}</span>
218
+ </button>
219
+ ))}
220
+ </div>
221
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
222
+ <span style={rowLabel}>Rarity</span>
223
+ {RARITY.map((r) => (
224
+ <button key={r.key} type="button" aria-pressed={rarities.has(r.key)} onClick={() => onRarity(r.key)}
225
+ title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}`} style={chip(rarities.has(r.key), r.color)}>
226
+ {r.label} <span style={{ fontWeight: 500, opacity: 0.8 }}>{byRarity[r.key]}</span>
227
+ </button>
228
+ ))}
229
+ {(tiers.size > 0 || rarities.size > 0) && (
230
+ <button type="button" onClick={onClear} style={linkButton}>Clear</button>
231
+ )}
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ function Legend() {
238
+ const item = (swatch, text) => (
239
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, marginRight: 12, whiteSpace: 'nowrap' }}>{swatch}{text}</span>
240
+ );
241
+ return (
242
+ <div style={{ fontSize: 10.5, color: '#999', marginTop: 8, lineHeight: 1.8 }}>
243
+ {item(<svg width="10" height="10"><circle cx="5" cy="5" r="4" fill="#FFD700" /></svg>, 'colour = tier')}
244
+ {item(<svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9B59B6" stroke={GREEN} strokeWidth="1.5" /></svg>, 'you added them')}
245
+ {item(<svg width="12" height="12"><circle cx="6" cy="6" r="4.2" fill="none" stroke="#3498DB" strokeWidth="1.4" strokeDasharray="2 1.6" /></svg>, 'request sent')}
246
+ </div>
247
+ );
248
+ }
249
+
250
+ // Rendered into <body>, not the card: the side panel's backdrop blur makes it
251
+ // the containing block for anything position: fixed inside it, so an overlay
252
+ // there only ever covered the panel. Fixed, so it can't change the page's
253
+ // size (TRAPS §29).
254
+ function Enlarged({ title, onClose, children }) {
255
+ useEffect(() => {
256
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
257
+ window.addEventListener('keydown', onKey);
258
+ return () => window.removeEventListener('keydown', onKey);
259
+ }, [onClose]);
260
+ return createPortal(
261
+ <div role="dialog" aria-modal="true" aria-label={title} onClick={onClose} style={{
262
+ position: 'fixed', inset: 0, zIndex: 300, background: 'rgba(0,0,0,0.78)',
263
+ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
264
+ }}>
265
+ <div onClick={(e) => e.stopPropagation()} style={{
266
+ width: 'min(720px, 100%)', maxHeight: '100%', overflowY: 'auto', boxSizing: 'border-box',
267
+ background: '#0f0c05', border: '1px solid rgba(255,215,0,0.18)', borderRadius: 14, padding: 16,
268
+ }}>
269
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
270
+ <div style={{ fontSize: 14, fontWeight: 800, color: '#fff' }}>{title}</div>
271
+ <button type="button" onClick={onClose} style={{ ...linkButton, fontSize: 12 }}>Close ✕</button>
272
+ </div>
273
+ <div style={{ fontSize: 10.5, color: '#888', marginBottom: 8 }}>Click a dot to open that person.</div>
274
+ {children}
275
+ </div>
276
+ </div>,
277
+ document.body,
278
+ );
279
+ }
280
+
281
+ // The card's scan box (Sidebar.js CreateClusterCard, id "scan-box"): bring it
282
+ // into view, flash it, and put the keyboard on its button.
283
+ function toScanBox() {
284
+ const scanBox = document.getElementById('scan-box');
285
+ if (!scanBox) return;
286
+ scanBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
287
+ scanBox.animate?.([{ boxShadow: '0 0 0 3px rgba(255,215,0,0.75)' }, { boxShadow: '0 0 0 0 rgba(255,215,0,0)' }], { duration: 1600 });
288
+ scanBox.querySelector('button:not([disabled])')?.focus({ preventScroll: true });
289
+ }
290
+
291
+ function Label({ children }) {
292
+ return <div style={{ fontSize: 10, fontWeight: 700, color: '#FF6B35', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 6 }}>{children}</div>;
293
+ }
294
+
295
+ const box = {
296
+ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)',
297
+ borderRadius: 10, padding: 10, marginBottom: 16,
298
+ };
299
+ const rowLabel = { fontSize: 9, color: '#666', textTransform: 'uppercase', letterSpacing: 0.5, width: 38, flexShrink: 0 };
300
+ const linkButton = { background: 'none', border: 'none', color: '#888', fontSize: 10.5, fontWeight: 600, cursor: 'pointer', padding: 0 };
@@ -0,0 +1,202 @@
1
+ 'use client';
2
+
3
+ // The sector picks, wherever you choose your field: Scores → Your sector
4
+ // (SectorSection.js) and the Scan page's question before the first scan
5
+ // (app/components/FieldStep.js). Your picks so far, then a search box, or the
6
+ // twelve broad industries, each opening to its narrower sectors from the
7
+ // directory (lib/sector-directory.js). Up to three picks. It only picks:
8
+ // saving is the page's (lib/settings-client.js).
9
+
10
+ import { useMemo, useState } from 'react';
11
+ import { Body, Btn, LINE } from '../ui';
12
+ import { INDUSTRIES } from '../../../lib/companies';
13
+ import { MAX_SECTORS } from '../../../lib/sector-focus';
14
+ import { DIRECTORY, SECTOR_KEYS, sectorByKey } from '../../../lib/sector-directory';
15
+
16
+ const plural = (n, one, many = `${one}s`) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
17
+
18
+ // Each industry's narrower sectors, in the directory's order.
19
+ const SECTORS_OF = new Map(INDUSTRIES.map((g) => [g.key, DIRECTORY.filter((s) => s.group === g.key)]));
20
+ const GROUP_OF = new Map(DIRECTORY.map((s) => [s.key, s.group]));
21
+ // What the search box looks through: each sector's name, its words, jobs and
22
+ // companies, as they read (without the list's "(s)" and "$" marks), so
23
+ // "recruiter" finds HR & Recruiting.
24
+ const SEARCHABLE = DIRECTORY.map((s) => ({
25
+ sector: s,
26
+ label: s.label.toLowerCase(),
27
+ words: [...s.words, ...(s.roles || []), ...(s.names || []), ...s.companies].map((w) => ` ${w.replace(/\(s\)$|\$$/, '').toLowerCase()}`),
28
+ }));
29
+ // A sector of work every company has (HR & Recruiting, Legal…) counts firms in
30
+ // that line, not every company that employs someone doing it.
31
+ const FUNCTION_TITLE = 'Counts firms in this line of work, not every company with someone in the job';
32
+
33
+ /** Sectors (and whole industries) whose name has the query, or a word or company that starts with it. */
34
+ function searchSectors(query) {
35
+ const q = query.trim().toLowerCase().replace(/\s+/g, ' ');
36
+ if (!q) return null;
37
+ return {
38
+ industries: INDUSTRIES.filter((g) => g.label.toLowerCase().includes(q)),
39
+ sectors: SEARCHABLE.filter((s) => s.label.includes(q) || s.words.some((w) => w.includes(` ${q}`))).map((s) => s.sector),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * `sectors` are the picks (keys), `onChange(sectors)` gets the new list.
45
+ * `suggest` is "Suggested from your network" as Scores loads it
46
+ * (/api/settings/sector-suggestions); left out, there are none to show.
47
+ */
48
+ export default function SectorPicker({ sectors, onChange, disabled, suggest }) {
49
+ const [query, setQuery] = useState('');
50
+ // Industries shown open; those holding a pick start open.
51
+ const [open, setOpen] = useState(() => new Set(sectors.map((k) => GROUP_OF.get(k) || k)));
52
+
53
+ const results = useMemo(() => searchSectors(query), [query]);
54
+
55
+ function toggle(key) {
56
+ const on = sectors.includes(key);
57
+ if (!on && sectors.length >= MAX_SECTORS) return;
58
+ const next = on ? sectors.filter((k) => k !== key) : SECTOR_KEYS.filter((k) => k === key || sectors.includes(k));
59
+ // A sector picked from the suggestions or a search shows up in its industry too.
60
+ if (!on && GROUP_OF.has(key)) setOpen((was) => new Set(was).add(GROUP_OF.get(key)));
61
+ onChange(next);
62
+ }
63
+
64
+ function flip(group) {
65
+ setOpen((was) => {
66
+ const next = new Set(was);
67
+ if (next.has(group)) next.delete(group); else next.add(group);
68
+ return next;
69
+ });
70
+ }
71
+
72
+ const full = sectors.length >= MAX_SECTORS;
73
+ // One chip for any pick: an industry or a sector, wherever it's shown.
74
+ const chip = (key, label, { note, title } = {}) => {
75
+ const on = sectors.includes(key);
76
+ return (
77
+ <Chip key={key} color={sectorByKey(key).color} on={on} blocked={!on && full} disabled={disabled} title={title}
78
+ onClick={() => toggle(key)}>
79
+ {label}{note && <span style={{ fontWeight: 400, color: on ? '#dfe7e7' : '#8b9a9a' }}>{note}</span>}
80
+ </Chip>
81
+ );
82
+ };
83
+
84
+ return (
85
+ <>
86
+ <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
87
+ <span style={{ fontSize: 13, color: '#8b9a9a' }}>Your picks ({sectors.length} of {MAX_SECTORS}):</span>
88
+ {!sectors.length && <span style={{ fontSize: 13, color: '#667' }}>none yet</span>}
89
+ {sectors.map((k) => {
90
+ const s = sectorByKey(k);
91
+ return (
92
+ <button key={k} type="button" disabled={disabled} onClick={() => toggle(k)} aria-label={`Remove ${s.label}`}
93
+ style={{
94
+ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 999, fontSize: 12.5,
95
+ fontWeight: 600, color: '#fff', background: `${s.color}33`, border: `1px solid ${s.color}`, cursor: 'pointer',
96
+ }}>
97
+ {s.label} <span aria-hidden="true" style={{ color: '#cfd8d8' }}>×</span>
98
+ </button>
99
+ );
100
+ })}
101
+ </div>
102
+
103
+ {suggest && <Suggestions state={suggest} chip={chip} />}
104
+
105
+ <label htmlFor="sector-search" style={{ display: 'block', fontSize: 13, color: '#8b9a9a', margin: '16px 0 6px' }}>
106
+ Find a sector
107
+ </label>
108
+ <div style={{ display: 'flex', gap: 8 }}>
109
+ <input id="sector-search" type="search" value={query} onChange={(e) => setQuery(e.target.value)}
110
+ placeholder="Try dentist, bakery or Stripe"
111
+ autoComplete="off" spellCheck={false}
112
+ style={{
113
+ flex: 1, minWidth: 0, padding: '8px 12px', borderRadius: 7, fontSize: 14, color: '#fff',
114
+ background: 'rgba(255,255,255,0.05)', border: LINE, outline: 'none',
115
+ }} />
116
+ {query && <Btn onClick={() => setQuery('')}>Clear</Btn>}
117
+ </div>
118
+
119
+ {results ? (
120
+ <div style={{ marginTop: 10 }} aria-live="polite">
121
+ {results.industries.length + results.sectors.length === 0 ? (
122
+ <Body style={{ fontSize: 12.5 }}>
123
+ Nothing in the list matches &ldquo;{query.trim()}&rdquo;. Try a job title, a kind of business or a company, or clear
124
+ the search and pick one of the twelve broad industries.
125
+ </Body>
126
+ ) : (
127
+ <div role="group" aria-label="Sectors that match your search" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
128
+ {results.industries.map((g) => chip(g.key, g.label, { note: ' · the whole industry' }))}
129
+ {results.sectors.map((s) => chip(s.key, s.label, { title: s.kind === 'function' ? FUNCTION_TITLE : `In ${sectorByKey(s.group).label}` }))}
130
+ </div>
131
+ )}
132
+ </div>
133
+ ) : (
134
+ <div style={{ marginTop: 10 }}>
135
+ {INDUSTRIES.map((g) => {
136
+ const inIndustry = SECTORS_OF.get(g.key);
137
+ const isOpen = open.has(g.key);
138
+ const inside = inIndustry.filter((s) => sectors.includes(s.key)).length;
139
+ return (
140
+ <div key={g.key} style={{ borderTop: LINE, padding: '8px 0' }}>
141
+ <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
142
+ {chip(g.key, g.label, { title: 'The whole industry: companies its sectors place, or whose name or the well-known list says so' })}
143
+ <button type="button" aria-expanded={isOpen} aria-controls={`sectors-${g.key}`} onClick={() => flip(g.key)}
144
+ style={{
145
+ padding: '4px 6px', fontSize: 12.5, color: inside ? '#9fd3ff' : '#8b9a9a', background: 'none', border: 'none',
146
+ cursor: 'pointer',
147
+ }}>
148
+ {isOpen ? '▾' : '▸'} {plural(inIndustry.length, 'sector')}{inside ? ` · ${inside} picked` : ''}
149
+ </button>
150
+ </div>
151
+ {isOpen && (
152
+ <div id={`sectors-${g.key}`} role="group" aria-label={`Sectors in ${g.label}`}
153
+ style={{ display: 'flex', flexWrap: 'wrap', gap: 8, margin: '8px 0 2px 14px' }}>
154
+ {inIndustry.map((s) => chip(s.key, s.label, s.kind === 'function' ? { title: FUNCTION_TITLE } : undefined))}
155
+ </div>
156
+ )}
157
+ </div>
158
+ );
159
+ })}
160
+ </div>
161
+ )}
162
+ </>
163
+ );
164
+ }
165
+
166
+ /** A pick you can turn on and off: an industry or a sector, with its industry's colour. */
167
+ function Chip({ color, on, blocked, disabled, title, onClick, children }) {
168
+ return (
169
+ <button type="button" aria-pressed={on} disabled={blocked || disabled} onClick={onClick}
170
+ title={blocked ? `Up to ${MAX_SECTORS} sectors. Turn one off first.` : title}
171
+ style={{
172
+ display: 'flex', alignItems: 'center', gap: 7, padding: '6px 12px', borderRadius: 999, maxWidth: '100%',
173
+ fontSize: 12.5, fontWeight: 600, textAlign: 'left', cursor: blocked ? 'not-allowed' : 'pointer',
174
+ color: on ? '#fff' : blocked ? '#556' : '#b8c4c4',
175
+ background: on ? `${color}33` : 'rgba(255,255,255,0.04)',
176
+ border: on ? `1px solid ${color}` : LINE,
177
+ }}>
178
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, opacity: blocked ? 0.4 : 1, flexShrink: 0 }} />
179
+ <span>{children}</span>
180
+ </button>
181
+ );
182
+ }
183
+
184
+ /** "Suggested from your network": the directory's sectors most of your scanned people work in. */
185
+ function Suggestions({ state, chip }) {
186
+ const note = { fontSize: 12.5, marginTop: 10 };
187
+ if (state.loading) return <Body style={note}>Looking through your network for suggestions…</Body>;
188
+ if (state.error) return <Body style={note}>Couldn&rsquo;t load suggestions ({state.error}). You can still pick below.</Body>;
189
+ if (!state.scored) return <Body style={note}>Suggestions appear here once you&rsquo;ve scanned your network.</Body>;
190
+ if (!state.list.length) return <Body style={note}>None of the list&rsquo;s sectors stands out among the people you&rsquo;ve scanned yet.</Body>;
191
+ return (
192
+ <div style={{ marginTop: 14 }}>
193
+ <div style={{ fontSize: 13, color: '#8b9a9a', marginBottom: 6 }}>Suggested from your network</div>
194
+ <div role="group" aria-label="Suggested from your network" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
195
+ {state.list.map((s) => chip(s.key, s.label, {
196
+ note: `: ${plural(s.people, 'person', 'people')} at ${plural(s.companies, 'company', 'companies')}`,
197
+ title: `In ${sectorByKey(s.group).label}`,
198
+ }))}
199
+ </div>
200
+ </div>
201
+ );
202
+ }