six-degrees 0.4.10 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +9 -9
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/.next/standalone/CHANGELOG.md +235 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +12 -2
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/AppTabs.js +59 -0
- package/.next/standalone/app/components/ChainView.js +303 -89
- package/.next/standalone/app/components/CirclePanel.js +162 -0
- package/.next/standalone/app/components/CompanyScores.js +2 -2
- package/.next/standalone/app/components/FieldStep.js +4 -4
- package/.next/standalone/app/components/FilterPanel.js +121 -183
- package/.next/standalone/app/components/ForceGraph.js +41 -3
- package/.next/standalone/app/components/GalaxyLab.js +26 -31
- package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
- package/.next/standalone/app/components/MapControls.js +154 -0
- package/.next/standalone/app/components/NoteDetail.js +118 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
- package/.next/standalone/app/components/PeopleMap.js +308 -0
- package/.next/standalone/app/components/ScanRadar.js +129 -0
- package/.next/standalone/app/components/ScanStatusBar.js +72 -27
- package/.next/standalone/app/components/SeparationView.js +626 -118
- package/.next/standalone/app/components/Sidebar.js +28 -2
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
- package/.next/standalone/app/components/settings/SectorSection.js +2 -2
- package/.next/standalone/app/components/settings/TierSection.js +1 -1
- package/.next/standalone/app/components/settings/TitleSection.js +149 -0
- package/.next/standalone/app/components/views.js +15 -14
- package/.next/standalone/app/globals.css +11 -0
- package/.next/standalone/app/page.js +89 -156
- package/.next/standalone/app/paths/page.js +39 -27
- package/.next/standalone/app/profile/page.js +33 -5
- package/.next/standalone/app/queue/page.js +56 -34
- package/.next/standalone/app/scores/page.js +16 -66
- package/.next/standalone/app/settings/page.js +27 -13
- package/.next/standalone/app/setup/page.js +298 -205
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +10 -351
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/degrees.js +0 -6
- package/.next/standalone/lib/dot-rings.js +25 -0
- package/.next/standalone/lib/galaxy-lab.js +65 -6
- package/.next/standalone/lib/ingest.js +2 -0
- package/.next/standalone/lib/island.js +40 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/linkedin-limits.js +6 -2
- package/.next/standalone/lib/map-heat.js +20 -0
- package/.next/standalone/lib/network-health.js +1 -1
- package/.next/standalone/lib/notifications.js +121 -0
- package/.next/standalone/lib/people-map.js +82 -0
- package/.next/standalone/lib/rarity.js +45 -3
- package/.next/standalone/lib/reach.js +7 -6
- package/.next/standalone/lib/rpc.js +15 -4
- package/.next/standalone/lib/scan-pace.js +46 -0
- package/.next/standalone/lib/scoring.js +39 -8
- package/.next/standalone/lib/scraper-client.js +19 -6
- package/.next/standalone/lib/sector-focus.js +4 -4
- package/.next/standalone/lib/separation.js +137 -10
- package/.next/standalone/lib/settings-effects.js +20 -11
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/lib/tier-grid.js +99 -0
- package/.next/standalone/lib/title-ranking.js +83 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +177 -35
- package/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/CHANGELOG.md +235 -0
- package/lib/chain-layout.js +105 -35
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/degrees.js +0 -6
- package/lib/dot-rings.js +25 -0
- package/lib/galaxy-lab.js +65 -6
- package/lib/ingest.js +2 -0
- package/lib/island.js +40 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/linkedin-limits.js +6 -2
- package/lib/map-heat.js +20 -0
- package/lib/network-health.js +1 -1
- package/lib/notifications.js +121 -0
- package/lib/people-map.js +82 -0
- package/lib/rarity.js +45 -3
- package/lib/reach.js +7 -6
- package/lib/rpc.js +15 -4
- package/lib/scan-pace.js +46 -0
- package/lib/scoring.js +39 -8
- package/lib/scraper-client.js +19 -6
- package/lib/sector-focus.js +4 -4
- package/lib/separation.js +137 -10
- package/lib/settings-effects.js +20 -11
- package/lib/settings.js +3 -0
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +177 -35
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
- package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
// is organised by the PERSON. Someone three of your connections know is one row
|
|
9
9
|
// with three routes, not three rows (lib/separation.js does that merge).
|
|
10
10
|
//
|
|
11
|
+
// The slider (Blake, 2026-10-02: "i want it to be slider ajusted and can be
|
|
12
|
+
// dynmaic") reorders everything as it moves: the rarest ways in at one end,
|
|
13
|
+
// the easiest at the other, each person's own score alone in the middle
|
|
14
|
+
// (lib/rarity.js slideValue). It never changes a score, a tier or a rank.
|
|
15
|
+
//
|
|
16
|
+
// The map follows the slider too (Blake, 2026-10-02): from the rare end to the
|
|
17
|
+
// middle it fans out to ten people, each with the one or two doors that lead
|
|
18
|
+
// to them; towards the easy end it closes in on fewer, and at the end on the
|
|
19
|
+
// one person you're aiming at, with every connection of yours who leads to them.
|
|
20
|
+
//
|
|
21
|
+
// "Path" beside the tier chips narrows everyone to one sector or one company.
|
|
22
|
+
// The picker opens under the button and goes away once you've picked.
|
|
23
|
+
//
|
|
11
24
|
// Two parts, one scroll: a small "summit map" of the top of the list, drawn
|
|
12
25
|
// from You through each bridge to each person, and under it the full ranked
|
|
13
26
|
// list. The list is windowed — about thirty rows are mounted whether there are
|
|
@@ -19,12 +32,14 @@
|
|
|
19
32
|
// when it changes. The map's height never depends on its width, so a scrollbar
|
|
20
33
|
// appearing cannot feed a resize loop.
|
|
21
34
|
|
|
22
|
-
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
|
|
23
|
-
import { separationPeople,
|
|
24
|
-
import {
|
|
35
|
+
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
36
|
+
import { separationPeople, comparePower, summitLayout, convergeLayout, mapCount, mostMutuals, shortName, TIERS, keyFor, routeIndex } from '../../lib/separation';
|
|
37
|
+
import { companyOf, industryOf, INDUSTRIES } from '../../lib/companies';
|
|
38
|
+
import { RARITY, rarityOf, rarityInfo, toggle, SLIDER_MIDDLE, slideValue, slideLabel, easeOf } from '../../lib/rarity';
|
|
25
39
|
import { hasRequest } from '../../lib/requests-client';
|
|
26
40
|
import useRequests from './useRequests';
|
|
27
41
|
import { initialsFor } from '../../lib/tiers';
|
|
42
|
+
import { watchNotchShown, notchShownNow, noNotch } from '../../lib/island';
|
|
28
43
|
import Avatar from './Avatar';
|
|
29
44
|
|
|
30
45
|
const ORANGE = '#FF6B35';
|
|
@@ -35,6 +50,9 @@ const CANT_NAME = 'a connection we can’t name';
|
|
|
35
50
|
// phone. This much side padding keeps them off the text at every width.
|
|
36
51
|
const SIDE = 56;
|
|
37
52
|
const LABEL_ROW = 28;
|
|
53
|
+
const TITLE_ROW = 44; // the ledger's title bar, on a computer (Blake, 2026-10-02: mock-up 2)
|
|
54
|
+
// The ledger's columns: rank, face, person, way in, how rare the way in, power.
|
|
55
|
+
const COLUMNS = '64px 36px minmax(0,1.3fr) minmax(0,1fr) 190px 132px';
|
|
38
56
|
const BOTTOM_PAD = 72; // clear of the last row's reach, and the corner toggles
|
|
39
57
|
const OVERSCAN = 8;
|
|
40
58
|
const SCROLL_STEP = 16; // scroll is published to state in 16px steps, not every pixel
|
|
@@ -68,16 +86,26 @@ function subline(person) {
|
|
|
68
86
|
|
|
69
87
|
export default function SeparationView({ connections = [], degree2 = [], fullDegree1 = connections, fullDegree2 = degree2, onSelect, userName, selectedId, tierColors = CLASSIC, preset = null }) {
|
|
70
88
|
const isMobile = useIsMobile();
|
|
89
|
+
// Room at the top for the notch, only while it's on screen (a scan, or another tab's views).
|
|
90
|
+
const notch = useSyncExternalStore(watchNotchShown, notchShownNow, noNotch);
|
|
71
91
|
const ROW_H = isMobile ? 68 : 56;
|
|
72
92
|
const K = isMobile ? 5 : 10;
|
|
73
93
|
const sidePad = isMobile ? 8 : 16;
|
|
74
94
|
|
|
75
95
|
const [query, setQuery] = useState('');
|
|
76
96
|
const q = useDeferredValue(query.trim().toLowerCase());
|
|
77
|
-
|
|
97
|
+
// The slider, 0 (rarest ways in first) to 100 (easiest first); 50 is power alone.
|
|
98
|
+
// The list follows a deferred copy, so the thumb never waits for a sort.
|
|
99
|
+
const [at, setAt] = useState(SLIDER_MIDDLE);
|
|
100
|
+
const atSlow = useDeferredValue(at);
|
|
78
101
|
const [tier, setTier] = useState('all');
|
|
79
102
|
// Rarity beside the tier (lib/rarity.js): any bands switched on; none is everyone.
|
|
80
103
|
const [rarities, setRarities] = useState(() => new Set());
|
|
104
|
+
// Path: one sector or one company, or none. The picker is only on screen while it's open.
|
|
105
|
+
const [path, setPath] = useState(null); // { kind: 'sector' | 'company', key, label, color }
|
|
106
|
+
const [pathOpen, setPathOpen] = useState(false);
|
|
107
|
+
const [pathText, setPathText] = useState('');
|
|
108
|
+
const [pathUsed, setPathUsed] = useState(false); // companies are only read once Path is opened
|
|
81
109
|
// Filters a card's Insights asked for ("Show them", "S only"): applied once each.
|
|
82
110
|
const [presetFor, setPresetFor] = useState(null);
|
|
83
111
|
if (preset && preset.id !== presetFor) {
|
|
@@ -100,10 +128,6 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
100
128
|
|
|
101
129
|
// ── Data: one merge per population, then order, then what's showing ──────
|
|
102
130
|
const model = useMemo(() => separationPeople(degree2, connections), [degree2, connections]);
|
|
103
|
-
const ordered = useMemo(
|
|
104
|
-
() => (sort === 'ways' ? [...model.people].sort(compareWaysIn) : model.people),
|
|
105
|
-
[model, sort],
|
|
106
|
-
);
|
|
107
131
|
// How rare the way in to each person is, counted across every circle you've
|
|
108
132
|
// scanned (not only the ones a bridge-tier filter leaves): never a score.
|
|
109
133
|
const waysAll = useMemo(() => routeIndex(fullDegree2), [fullDegree2]);
|
|
@@ -112,18 +136,50 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
112
136
|
for (const p of model.people) m.set(p.key, rarityOf(p.person, waysAll.get(p.key)?.size || p.waysIn));
|
|
113
137
|
return m;
|
|
114
138
|
}, [model, waysAll]);
|
|
139
|
+
const ordered = useMemo(() => {
|
|
140
|
+
if (atSlow === SLIDER_MIDDLE) return model.people;
|
|
141
|
+
const value = new Map();
|
|
142
|
+
for (const p of model.people) value.set(p.key, slideValue(p.score, rarityBy.get(p.key)?.count || p.waysIn, atSlow));
|
|
143
|
+
return [...model.people].sort((a, b) => value.get(b.key) - value.get(a.key) || comparePower(a, b));
|
|
144
|
+
}, [model, rarityBy, atSlow]);
|
|
115
145
|
const byRarity = useMemo(() => {
|
|
116
146
|
const out = Object.fromEntries(RARITY.map((r) => [r.key, 0]));
|
|
117
147
|
for (const r of rarityBy.values()) out[r.key]++;
|
|
118
148
|
return out;
|
|
119
149
|
}, [rarityBy]);
|
|
150
|
+
// Where everyone works, and the sector that suggests (inferred, as in Paths):
|
|
151
|
+
// read once, the first time Path is opened.
|
|
152
|
+
const facets = useMemo(() => {
|
|
153
|
+
if (!pathUsed) return null;
|
|
154
|
+
const by = new Map();
|
|
155
|
+
const sectors = new Map();
|
|
156
|
+
const companies = new Map();
|
|
157
|
+
for (const p of model.people) {
|
|
158
|
+
const company = companyOf(p.person) || null;
|
|
159
|
+
const sector = industryOf(company, p.person.headline).key;
|
|
160
|
+
by.set(p.key, { company, sector });
|
|
161
|
+
sectors.set(sector, (sectors.get(sector) || 0) + 1);
|
|
162
|
+
if (company) companies.set(company, (companies.get(company) || 0) + 1);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
by,
|
|
166
|
+
sectors: INDUSTRIES.map((i) => ({ ...i, count: sectors.get(i.key) || 0 })).filter((i) => i.count > 0).sort((a, b) => b.count - a.count),
|
|
167
|
+
companies: [...companies].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
|
|
168
|
+
};
|
|
169
|
+
}, [model, pathUsed]);
|
|
170
|
+
const onPath = useCallback((p) => {
|
|
171
|
+
if (!path || !facets) return true;
|
|
172
|
+
const f = facets.by.get(p.key);
|
|
173
|
+
return path.kind === 'sector' ? f?.sector === path.key : f?.company === path.key;
|
|
174
|
+
}, [path, facets]);
|
|
120
175
|
const matches = useCallback((p, text) => (tier === 'all' || p.tier === tier)
|
|
121
176
|
&& (!rarities.size || rarities.has(rarityBy.get(p.key)?.key))
|
|
122
|
-
&& (
|
|
177
|
+
&& onPath(p)
|
|
178
|
+
&& (!text || p.haystack.includes(text)), [tier, rarities, rarityBy, onPath]);
|
|
123
179
|
const visible = useMemo(() => {
|
|
124
|
-
if (!q && tier === 'all' && !rarities.size) return ordered;
|
|
180
|
+
if (!q && tier === 'all' && !rarities.size && !path) return ordered;
|
|
125
181
|
return ordered.filter((p) => matches(p, q));
|
|
126
|
-
}, [ordered, q, tier, rarities, matches]);
|
|
182
|
+
}, [ordered, q, tier, rarities, path, matches]);
|
|
127
183
|
|
|
128
184
|
// Who you've already asked, or already know. The map is "who to ask next",
|
|
129
185
|
// so it moves on past them: send requests to its ten and the next ten come
|
|
@@ -131,9 +187,27 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
131
187
|
const d1Keys = useMemo(() => new Set(fullDegree1.map(keyFor)), [fullDegree1]);
|
|
132
188
|
const statusOf = useCallback((p) => (d1Keys.has(p.key) ? 'connected' : hasRequest(p.person, requests) ? 'asked' : null),
|
|
133
189
|
[d1Keys, requests]);
|
|
134
|
-
|
|
190
|
+
// How many the map draws follows the slider: ten, then fewer, then the one you're aiming at.
|
|
191
|
+
const shown = mapCount(atSlow, K);
|
|
192
|
+
const unasked = useMemo(() => visible.filter((p) => !statusOf(p)).slice(0, K + 1), [visible, K, statusOf]);
|
|
193
|
+
const top = useMemo(() => unasked.slice(0, shown), [unasked, shown]);
|
|
135
194
|
const askedShown = useMemo(() => visible.reduce((n, p) => n + (statusOf(p) ? 1 : 0), 0), [visible, statusOf]);
|
|
136
195
|
const selectedKey = selectedId != null ? model.rowToKey.get(selectedId) ?? null : null;
|
|
196
|
+
// Down to one: the person picked, if they're in what's showing, else the top of the list.
|
|
197
|
+
const single = shown === 1;
|
|
198
|
+
// Down to one, it aims at whoever you share the most mutual connections with,
|
|
199
|
+
// scanned or not (the ones not scanned are grey dots), unless you picked
|
|
200
|
+
// someone; "Next" are the runners-up.
|
|
201
|
+
const aimable = useMemo(() => {
|
|
202
|
+
if (!single) return [];
|
|
203
|
+
return mostMutuals(visible.filter((p) => !statusOf(p)), (p) => rarityBy.get(p.key)?.count || 0, (isMobile ? 3 : 6) + 1);
|
|
204
|
+
}, [single, visible, statusOf, rarityBy, isMobile]);
|
|
205
|
+
const target = useMemo(() => {
|
|
206
|
+
if (!single) return null;
|
|
207
|
+
return (selectedKey && visible.find((p) => p.key === selectedKey)) || aimable[0] || null;
|
|
208
|
+
}, [single, selectedKey, visible, aimable]);
|
|
209
|
+
const nextUp = useMemo(() => (single && target ? aimable.filter((p) => p.key !== target.key).slice(0, isMobile ? 3 : 6) : []),
|
|
210
|
+
[single, target, aimable, isMobile]);
|
|
137
211
|
|
|
138
212
|
// onSelect is read through a ref so the row and map callbacks never change
|
|
139
213
|
// identity, and memoised rows never re-render because a parent re-rendered.
|
|
@@ -159,10 +233,22 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
159
233
|
const cw = Math.max(0, Math.min(980, box.w - 2 * sidePad));
|
|
160
234
|
// The map's height comes from the layout, never the DOM, so the list below
|
|
161
235
|
// it can be positioned by arithmetic alone.
|
|
162
|
-
|
|
236
|
+
// One map for both: ten people fanned out, or the one you're aiming at with
|
|
237
|
+
// everyone who leads to them. The same shape, so it glides from one to the other.
|
|
238
|
+
// Wide enough: connections as pills and people as cards (Blake, 2026-10-02); a phone keeps the dots.
|
|
239
|
+
const cards = !isMobile && cw >= 760;
|
|
240
|
+
const layout = useMemo(
|
|
241
|
+
() => (single
|
|
242
|
+
? convergeLayout(target, cw || 800, isMobile, { cards, mutuals: target ? rarityBy.get(target.key)?.count || 0 : 0 })
|
|
243
|
+
: summitLayout(top, cw || 800, isMobile, { cards })),
|
|
244
|
+
[single, target, top, cw, isMobile, cards, rarityBy],
|
|
245
|
+
);
|
|
163
246
|
const captionH = isMobile ? 58 : 40; // fixed, so HEAD is arithmetic; the phone legend takes two lines
|
|
164
|
-
const
|
|
165
|
-
const
|
|
247
|
+
const NEXT_H = nextUp.length ? 38 : 0;
|
|
248
|
+
const mapH = single ? (target ? layout.height + NEXT_H : 0) : layout.height;
|
|
249
|
+
const mapBlockH = mapH ? captionH + mapH + 12 : 0;
|
|
250
|
+
const titleH = isMobile ? 0 : TITLE_ROW;
|
|
251
|
+
const HEAD = mapBlockH + titleH + LABEL_ROW;
|
|
166
252
|
const total = HEAD + visible.length * ROW_H + BOTTOM_PAD;
|
|
167
253
|
|
|
168
254
|
// ── Windowing ────────────────────────────────────────────────────────────
|
|
@@ -196,8 +282,10 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
196
282
|
const changeQuery = (v) => { setQuery(v); toTop(); };
|
|
197
283
|
const changeTier = (t) => { setTier(t); toTop(); };
|
|
198
284
|
const changeRarity = (r) => { setRarities((set) => toggle(set, r)); toTop(); };
|
|
199
|
-
const clearAll = () => { setQuery(''); setTier('all'); setRarities(new Set()); toTop(); };
|
|
200
|
-
const
|
|
285
|
+
const clearAll = () => { setQuery(''); setTier('all'); setRarities(new Set()); setPath(null); toTop(); };
|
|
286
|
+
const openPath = () => { setPathUsed(true); setPathText(''); setPathOpen(true); };
|
|
287
|
+
const pickPath = (next) => { setPath(next); setPathOpen(false); toTop(); };
|
|
288
|
+
const changeAt = (v) => { setAt(Math.max(0, Math.min(100, Math.round(Number(v) || 0)))); toTop(); };
|
|
201
289
|
const onSearchKey = (e) => {
|
|
202
290
|
if (e.key === 'Enter') {
|
|
203
291
|
// Read the live text, not the deferred copy the list is still catching up to.
|
|
@@ -211,7 +299,9 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
211
299
|
};
|
|
212
300
|
|
|
213
301
|
const { summary } = model;
|
|
214
|
-
const filtered = !!q || tier !== 'all' || rarities.size > 0;
|
|
302
|
+
const filtered = !!q || tier !== 'all' || rarities.size > 0 || !!path;
|
|
303
|
+
const slide = slideLabel(at);
|
|
304
|
+
const slideColor = at < 45 ? RARE : at > 55 ? EASY : '#FFD700';
|
|
215
305
|
const youLabel = !userName || /^you$/i.test(String(userName).trim()) ? 'You' : initialsFor(userName);
|
|
216
306
|
const rows = visible.slice(start, end);
|
|
217
307
|
|
|
@@ -221,7 +311,7 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
221
311
|
background: '#0a0a1a', color: '#fff', overflow: 'hidden',
|
|
222
312
|
}}>
|
|
223
313
|
{/* ── Header: what this is, how many, and what it can't say ── */}
|
|
224
|
-
<div style={{ padding:
|
|
314
|
+
<div style={{ padding: `${notch ? 48 : 14}px ${SIDE}px 8px`, flexShrink: 0 }}>
|
|
225
315
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
|
|
226
316
|
<h2 style={{
|
|
227
317
|
margin: 0, fontSize: 18, fontWeight: 800,
|
|
@@ -261,13 +351,9 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
261
351
|
)}
|
|
262
352
|
</div>
|
|
263
353
|
|
|
264
|
-
{/* ── Controls: search,
|
|
265
|
-
<div style={{
|
|
266
|
-
|
|
267
|
-
display: 'flex', gap: 8, flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'stretch' : 'center',
|
|
268
|
-
flexWrap: isMobile ? 'nowrap' : 'wrap', // with the Sidebar open, the chips take a second line
|
|
269
|
-
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
|
270
|
-
}}>
|
|
354
|
+
{/* ── Controls: search, then the slider with their tier and rarity ── */}
|
|
355
|
+
<div style={{ padding: `0 ${SIDE}px 10px`, flexShrink: 0, borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
|
356
|
+
<style>{SLIDER_CSS}</style>
|
|
271
357
|
<input
|
|
272
358
|
ref={searchRef}
|
|
273
359
|
type="search"
|
|
@@ -277,62 +363,116 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
277
363
|
placeholder="Search people, companies, or who knows them"
|
|
278
364
|
aria-label="Search people, companies, or who knows them"
|
|
279
365
|
style={{
|
|
280
|
-
|
|
366
|
+
width: '100%', minWidth: 0, height: isMobile ? 44 : 34, boxSizing: 'border-box',
|
|
281
367
|
padding: '0 12px', borderRadius: 8, outline: 'none', color: '#fff',
|
|
282
368
|
fontSize: isMobile ? 16 : 13, // under 16px, iOS zooms the page on focus
|
|
283
369
|
border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.05)',
|
|
284
370
|
}}
|
|
285
371
|
/>
|
|
286
372
|
<div style={{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
overflowX: isMobile ? 'auto' : 'visible', scrollbarWidth: 'thin', flexShrink: isMobile ? 0 : 1,
|
|
373
|
+
marginTop: 8, padding: isMobile ? '8px 10px' : '8px 14px', borderRadius: 12, position: 'relative',
|
|
374
|
+
border: '1px solid rgba(255,255,255,0.08)', background: 'rgba(255,255,255,0.025)',
|
|
290
375
|
}}>
|
|
291
|
-
|
|
292
|
-
{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const n = t === 'all' ? summary.people : summary.byTier[t];
|
|
306
|
-
return (
|
|
307
|
-
<button key={t} type="button" aria-pressed={on} onClick={() => changeTier(t)} style={{
|
|
308
|
-
height: isMobile ? 40 : 30, padding: '0 10px', borderRadius: 15, cursor: 'pointer', flexShrink: 0,
|
|
309
|
-
fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
|
|
310
|
-
border: `1px solid ${on ? c : 'rgba(255,255,255,0.1)'}`,
|
|
311
|
-
background: on ? `${c === '#fff' ? '#ffffff' : c}22` : 'rgba(255,255,255,0.03)',
|
|
312
|
-
color: on ? c : '#999',
|
|
313
|
-
}}>
|
|
314
|
-
{t === 'all' ? 'All' : t}
|
|
315
|
-
<span style={{ marginLeft: 5, fontWeight: 500, color: on ? c : '#666', opacity: 0.85 }}>{fmt(n)}</span>
|
|
316
|
-
</button>
|
|
317
|
-
);
|
|
318
|
-
})}
|
|
319
|
-
<span title={RARITY_NOTE} style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5, marginLeft: 6, cursor: 'help' }}>Rarity</span>
|
|
320
|
-
{RARITY.map((r) => {
|
|
321
|
-
const on = rarities.has(r.key);
|
|
322
|
-
return (
|
|
323
|
-
<button key={r.key} type="button" aria-pressed={on} onClick={() => changeRarity(r.key)}
|
|
324
|
-
title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}. ${RARITY_NOTE}`} style={{
|
|
376
|
+
{pathOpen && (
|
|
377
|
+
<PathPicker facets={facets} text={pathText} onText={setPathText} isMobile={isMobile}
|
|
378
|
+
onPick={pickPath} onClose={() => setPathOpen(false)} />
|
|
379
|
+
)}
|
|
380
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: isMobile ? 'nowrap' : 'wrap', minWidth: 0,
|
|
381
|
+
// A phone scrolls this strip sideways, so the page itself never does.
|
|
382
|
+
overflowX: isMobile ? 'auto' : 'visible', scrollbarWidth: 'thin' }}>
|
|
383
|
+
<span style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5 }}>Their tier</span>
|
|
384
|
+
{['all', ...TIERS].map((t) => {
|
|
385
|
+
const on = tier === t;
|
|
386
|
+
const c = t === 'all' ? '#fff' : tierColors[t] || '#888';
|
|
387
|
+
const n = t === 'all' ? summary.people : summary.byTier[t];
|
|
388
|
+
return (
|
|
389
|
+
<button key={t} type="button" aria-pressed={on} onClick={() => changeTier(t)} style={{
|
|
325
390
|
height: isMobile ? 40 : 30, padding: '0 10px', borderRadius: 15, cursor: 'pointer', flexShrink: 0,
|
|
326
391
|
fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
|
|
327
|
-
border: `1px solid ${on ?
|
|
328
|
-
background: on ? `${
|
|
329
|
-
color: on ?
|
|
392
|
+
border: `1px solid ${on ? c : 'rgba(255,255,255,0.1)'}`,
|
|
393
|
+
background: on ? `${c === '#fff' ? '#ffffff' : c}22` : 'rgba(255,255,255,0.03)',
|
|
394
|
+
color: on ? c : '#999',
|
|
395
|
+
}}>
|
|
396
|
+
{t === 'all' ? 'All' : t}
|
|
397
|
+
<span style={{ marginLeft: 5, fontWeight: 500, color: on ? c : '#666', opacity: 0.85 }}>{fmt(n)}</span>
|
|
398
|
+
</button>
|
|
399
|
+
);
|
|
400
|
+
})}
|
|
401
|
+
{/* Path: one sector or one company. The picker opens under here and goes away once you pick. */}
|
|
402
|
+
<span style={{ display: 'inline-flex', flexShrink: 0, height: isMobile ? 40 : 30, borderRadius: 15, overflow: 'hidden',
|
|
403
|
+
border: `1px solid ${path ? PATH : 'rgba(0,255,136,0.35)'}`, background: path ? 'rgba(0,255,136,0.12)' : 'rgba(0,255,136,0.04)' }}>
|
|
404
|
+
<button type="button" aria-haspopup="dialog" aria-expanded={pathOpen} onClick={() => (pathOpen ? setPathOpen(false) : openPath())}
|
|
405
|
+
title="Narrow everyone to one sector or one company" style={{
|
|
406
|
+
padding: path ? '0 6px 0 12px' : '0 12px', border: 'none', background: 'none', cursor: 'pointer',
|
|
407
|
+
fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap', color: PATH, maxWidth: isMobile ? 200 : 300, overflow: 'hidden', textOverflow: 'ellipsis',
|
|
330
408
|
}}>
|
|
331
|
-
{
|
|
332
|
-
<span style={{ marginLeft: 5, fontWeight: 500, color: on ? r.color : '#666', opacity: 0.85 }}>{fmt(byRarity[r.key])}</span>
|
|
409
|
+
{path ? <>Path<span style={{ fontWeight: 500, color: '#cfe' }}> · {path.label}</span></> : 'Path ▾'}
|
|
333
410
|
</button>
|
|
334
|
-
|
|
335
|
-
|
|
411
|
+
{path && (
|
|
412
|
+
<button type="button" aria-label={`Clear the path: ${path.label}`} onClick={() => pickPath(null)} style={{
|
|
413
|
+
padding: '0 10px 0 4px', border: 'none', background: 'none', cursor: 'pointer', fontSize: 14, color: PATH,
|
|
414
|
+
}}>×</button>
|
|
415
|
+
)}
|
|
416
|
+
</span>
|
|
417
|
+
{/* Where the slider is, in words */}
|
|
418
|
+
<span aria-live="polite" style={{
|
|
419
|
+
marginLeft: 'auto', flexShrink: 0, height: isMobile ? 40 : 30, padding: '0 12px', borderRadius: 15, boxSizing: 'border-box',
|
|
420
|
+
display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 12, fontWeight: 700, whiteSpace: 'nowrap',
|
|
421
|
+
border: `1px solid ${slideColor}66`, background: `${slideColor}14`, color: slideColor,
|
|
422
|
+
}}>
|
|
423
|
+
<span style={{ width: 7, height: 7, borderRadius: '50%', background: slideColor }} />
|
|
424
|
+
<span style={{ fontVariantNumeric: 'tabular-nums' }}>{at}</span>
|
|
425
|
+
{slide.name}
|
|
426
|
+
</span>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
{/* The slider: the rarest ways in at one end, the easiest at the other, power alone in the middle */}
|
|
430
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginTop: 8, fontSize: 10.5, fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase' }}>
|
|
431
|
+
<span style={{ color: RARE }}>Rare{isMobile ? '' : ' · one way in'}</span>
|
|
432
|
+
<span style={{ color: '#FFD700' }}>Power</span>
|
|
433
|
+
<span style={{ color: EASY }}>Easy{isMobile ? '' : ' · many mutual connections'}</span>
|
|
434
|
+
</div>
|
|
435
|
+
<input
|
|
436
|
+
className="sepslide" type="range" min={0} max={100} step={1} value={at}
|
|
437
|
+
onChange={(e) => changeAt(e.target.value)}
|
|
438
|
+
onDoubleClick={() => changeAt(SLIDER_MIDDLE)}
|
|
439
|
+
aria-label="Order: the rarest ways in, to the easiest"
|
|
440
|
+
aria-valuetext={`${at}: ${slide.name}`}
|
|
441
|
+
title="Drag to reorder everyone. Double-click to go back to the middle."
|
|
442
|
+
/>
|
|
443
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: -2 }}>
|
|
444
|
+
{[[0, 'Rarest'], [25, ''], [SLIDER_MIDDLE, 'By power'], [75, ''], [100, 'Easiest']].map(([v, name]) => (
|
|
445
|
+
<button key={v} type="button" onClick={() => changeAt(v)} aria-label={`Set the slider to ${v}${name ? `: ${name}` : ''}`} style={{
|
|
446
|
+
padding: '2px 4px', border: 'none', background: 'none', cursor: 'pointer', fontSize: 10,
|
|
447
|
+
color: at === v ? '#fff' : '#667', fontWeight: at === v ? 700 : 500, fontVariantNumeric: 'tabular-nums',
|
|
448
|
+
}}>{v}{name && !isMobile ? ` · ${name}` : ''}</button>
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
<div style={{ marginTop: 4, fontSize: 11.5, lineHeight: 1.45, color: '#bbb' }}>
|
|
452
|
+
<span style={{ color: slideColor, fontWeight: 700 }}>{slide.name}.</span>{' '}{slide.detail}
|
|
453
|
+
{!isMobile && <span style={{ color: '#667' }}>{' '}It only reorders: scores, tiers and ranks stay as they are.</span>}
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8, minWidth: 0, flexWrap: isMobile ? 'nowrap' : 'wrap',
|
|
457
|
+
overflowX: isMobile ? 'auto' : 'visible', scrollbarWidth: 'thin' }}>
|
|
458
|
+
<span title={RARITY_NOTE} style={{ fontSize: 10, color: '#666', whiteSpace: 'nowrap', flexShrink: 0, textTransform: 'uppercase', letterSpacing: 0.5, cursor: 'help' }}>Only show</span>
|
|
459
|
+
{RARITY.map((r) => {
|
|
460
|
+
const on = rarities.has(r.key);
|
|
461
|
+
return (
|
|
462
|
+
<button key={r.key} type="button" aria-pressed={on} onClick={() => changeRarity(r.key)}
|
|
463
|
+
title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}. ${RARITY_NOTE}`} style={{
|
|
464
|
+
height: isMobile ? 40 : 28, padding: '0 10px', borderRadius: 14, cursor: 'pointer', flexShrink: 0,
|
|
465
|
+
fontSize: 11.5, fontWeight: 700, whiteSpace: 'nowrap',
|
|
466
|
+
border: `1px solid ${on ? r.color : 'rgba(255,255,255,0.1)'}`,
|
|
467
|
+
background: on ? `${r.color}22` : 'rgba(255,255,255,0.03)',
|
|
468
|
+
color: on ? r.color : '#999',
|
|
469
|
+
}}>
|
|
470
|
+
{r.label}
|
|
471
|
+
<span style={{ marginLeft: 5, fontWeight: 500, color: on ? r.color : '#666', opacity: 0.85 }}>{fmt(byRarity[r.key])}</span>
|
|
472
|
+
</button>
|
|
473
|
+
);
|
|
474
|
+
})}
|
|
475
|
+
</div>
|
|
336
476
|
</div>
|
|
337
477
|
</div>
|
|
338
478
|
|
|
@@ -343,15 +483,20 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
343
483
|
style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', position: 'relative', padding: `0 ${sidePad}px` }}
|
|
344
484
|
>
|
|
345
485
|
<div style={{ position: 'relative', maxWidth: 980, margin: '0 auto', height: total }}>
|
|
346
|
-
{
|
|
486
|
+
{mapBlockH > 0 && (
|
|
347
487
|
<div style={{ height: mapBlockH, boxSizing: 'border-box', paddingTop: 10 }}>
|
|
348
488
|
<div style={{ height: captionH - 10, overflow: 'hidden' }}>
|
|
349
489
|
<div style={{ fontSize: 12, fontWeight: 700, color: '#ddd', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
350
|
-
|
|
490
|
+
{single
|
|
491
|
+
? <>Aiming at {target.person.name} · {layout.unscanned
|
|
492
|
+
? `${fmt(target.waysIn + layout.unscanned)} mutual connections: ${fmt(target.waysIn)} drawn, ${fmt(layout.unscanned)} in circles not scanned yet`
|
|
493
|
+
: target.waysIn === 1 ? 'the one connection of yours who leads to them' : `all ${fmt(target.waysIn)} connections of yours who lead to them`}</>
|
|
494
|
+
: <>Top {fmt(top.length)} you haven’t asked, of {fmt(visible.length)}{filtered ? ' shown' : ''}{at !== SLIDER_MIDDLE ? ` · ${slide.name.toLowerCase()}` : ''} · every way in drawn</>}
|
|
351
495
|
</div>
|
|
352
496
|
<div style={{ fontSize: 10, color: '#777', marginTop: 2, whiteSpace: isMobile ? 'normal' : 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: 1.35 }}>
|
|
353
|
-
{askedShown > 0 && <span style={{ color: '#FFD700' }}>{fmt(askedShown)} asked or connected, so it moved on · </span>}
|
|
354
|
-
<span style={{ color: ORANGE }}>solid orange</span> = top-scored bridge · dashed = other routes
|
|
497
|
+
{!single && askedShown > 0 && <span style={{ color: '#FFD700' }}>{fmt(askedShown)} asked or connected, so it moved on · </span>}
|
|
498
|
+
<span style={{ color: ORANGE }}>solid orange</span> = top-scored bridge · dashed = other routes
|
|
499
|
+
{single ? ' · pick anyone in the list to aim at them instead' : cards ? ' · a lit pill is someone’s best way in' : ' · dot size = ways in'}
|
|
355
500
|
</div>
|
|
356
501
|
</div>
|
|
357
502
|
{cw > 0 && (
|
|
@@ -363,22 +508,63 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
363
508
|
isMobile={isMobile}
|
|
364
509
|
onPick={onPick}
|
|
365
510
|
onPickBridge={onPickBridge}
|
|
511
|
+
mutualsBy={rarityBy}
|
|
512
|
+
doors={!isMobile && cw >= 640}
|
|
513
|
+
cards={cards}
|
|
366
514
|
/>
|
|
367
515
|
)}
|
|
516
|
+
{/* Aiming at one person: the next few, to aim at instead */}
|
|
517
|
+
{cw > 0 && single && (
|
|
518
|
+
<>
|
|
519
|
+
{nextUp.length > 0 && (
|
|
520
|
+
<div style={{ height: NEXT_H, display: 'flex', alignItems: 'center', gap: 6, overflow: 'hidden' }}>
|
|
521
|
+
<span style={{ fontSize: 10, color: '#666', textTransform: 'uppercase', letterSpacing: 0.5, flexShrink: 0 }}>Next</span>
|
|
522
|
+
{nextUp.map((p) => (
|
|
523
|
+
<button key={p.key} type="button" onClick={() => onPick(p)} title={`Aim at ${p.person.name}`} style={{
|
|
524
|
+
height: 26, padding: '0 10px', borderRadius: 13, cursor: 'pointer', flexShrink: 1, minWidth: 0,
|
|
525
|
+
fontSize: 11, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
526
|
+
border: `1px solid ${(tierColors[p.tier] || '#888')}55`, background: 'rgba(255,255,255,0.03)', color: '#ccc',
|
|
527
|
+
}}>
|
|
528
|
+
{shortName(p.person.name)}
|
|
529
|
+
<span style={{ marginLeft: 5, color: tierColors[p.tier] || '#888', fontWeight: 700 }}>{p.score.toFixed(1)}</span>
|
|
530
|
+
{/* The count they're ordered by: every mutual, scanned or not */}
|
|
531
|
+
<span style={{ marginLeft: 5, color: '#667', fontWeight: 500 }}>
|
|
532
|
+
{(() => { const n = Math.max(rarityBy.get(p.key)?.count || 0, p.waysIn); return n === 1 ? '1 mutual' : `${fmt(n)} mutuals`; })()}
|
|
533
|
+
</span>
|
|
534
|
+
</button>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</>
|
|
539
|
+
)}
|
|
368
540
|
</div>
|
|
369
541
|
)}
|
|
370
542
|
|
|
543
|
+
{visible.length > 0 && !isMobile && (
|
|
544
|
+
<div style={{
|
|
545
|
+
height: TITLE_ROW, display: 'flex', alignItems: 'center', gap: 10, padding: '0 12px 0 15px', boxSizing: 'border-box',
|
|
546
|
+
borderTop: '1px solid rgba(255,255,255,0.06)',
|
|
547
|
+
}}>
|
|
548
|
+
<span style={{ fontSize: 15, fontWeight: 800 }}>Ranked by reach</span>
|
|
549
|
+
<span style={{ fontSize: 11, fontWeight: 800, color: '#000', background: '#FFD700', borderRadius: 10, padding: '2px 8px' }}>
|
|
550
|
+
{fmt(visible.length)}{filtered ? ` of ${fmt(summary.people)}` : ''}
|
|
551
|
+
</span>
|
|
552
|
+
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#777' }}>
|
|
553
|
+
order follows the slider · <span style={{ color: slideColor }}>{slide.name.toLowerCase()}</span>
|
|
554
|
+
</span>
|
|
555
|
+
</div>
|
|
556
|
+
)}
|
|
371
557
|
{visible.length > 0 && (
|
|
372
558
|
<div style={{
|
|
373
559
|
height: LABEL_ROW, display: 'grid', alignItems: 'center', gap: 10, padding: '0 12px 0 15px',
|
|
374
|
-
gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' :
|
|
560
|
+
gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' : COLUMNS,
|
|
375
561
|
fontSize: 9, fontWeight: 700, color: '#555', textTransform: 'uppercase', letterSpacing: 0.5,
|
|
376
562
|
borderBottom: '1px solid rgba(255,255,255,0.08)', boxSizing: 'border-box',
|
|
377
563
|
}}>
|
|
378
564
|
<span title="Rank by score, then ways in. = means tied">#</span>
|
|
379
565
|
<span />
|
|
380
|
-
{isMobile ? <span>Person · way in</span> : <><span>Person</span><span>Way in</span><span>Power</span></>}
|
|
381
|
-
<span style={{ textAlign: 'right' }}>
|
|
566
|
+
{isMobile ? <span>Person · way in</span> : <><span>Person</span><span>Way in</span><span title={RARITY_NOTE}>How rare the way in</span><span style={{ textAlign: 'right' }}>Power</span></>}
|
|
567
|
+
{isMobile && <span style={{ textAlign: 'right' }}>Score</span>}
|
|
382
568
|
</div>
|
|
383
569
|
)}
|
|
384
570
|
|
|
@@ -422,6 +608,7 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
422
608
|
// whatever the network's size. Hover is pure CSS from the scoped <style>, so a
|
|
423
609
|
// still mouse can never re-render anything. aria-hidden because the same people
|
|
424
610
|
// are the first rows of the list, and the list is the accessible path.
|
|
611
|
+
const MOVE = '.38s cubic-bezier(.2,.8,.2,1)';
|
|
425
612
|
const MAP_CSS = `
|
|
426
613
|
.sepmap .sp { cursor: pointer; transition: opacity .12s; }
|
|
427
614
|
.sepmap .ppl:hover .sp { opacity: .28; }
|
|
@@ -430,11 +617,34 @@ const MAP_CSS = `
|
|
|
430
617
|
.sepmap .br { cursor: pointer; }
|
|
431
618
|
.sepmap .br:hover text { text-decoration: underline; }
|
|
432
619
|
.sepmap .rt { pointer-events: none; }
|
|
620
|
+
/* Everything glides to its new place as the slider moves: dots and names by
|
|
621
|
+
their group's transform, lines by their path. Newcomers fade in. */
|
|
622
|
+
.sepmap .mv { transition: transform ${MOVE}; }
|
|
623
|
+
.sepmap .rt, .sepmap .spk { transition: d ${MOVE}, stroke-opacity .12s; }
|
|
624
|
+
.sepmap .in { animation: sepIn .3s ease-out both; }
|
|
625
|
+
@keyframes sepIn { from { opacity: 0; } to { opacity: 1; } }
|
|
626
|
+
@media (prefers-reduced-motion: reduce) {
|
|
627
|
+
.sepmap .mv, .sepmap .rt, .sepmap .spk { transition: none; }
|
|
628
|
+
.sepmap .in { animation: none; }
|
|
629
|
+
}
|
|
433
630
|
`;
|
|
434
631
|
const HALO = { paintOrder: 'stroke', stroke: '#0a0a1a', strokeWidth: 3, strokeLinejoin: 'round' };
|
|
632
|
+
const at = (x, y) => ({ transform: `translate(${x}px, ${y}px)` });
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* How many mutual connections lead to someone, said the way the data has it:
|
|
636
|
+
* the ones drawn are the connections of yours whose circles you've scanned;
|
|
637
|
+
* LinkedIn's own count, when a scan saved one, can be higher.
|
|
638
|
+
*/
|
|
639
|
+
function waysTag(p, mutuals) {
|
|
640
|
+
if (mutuals?.from === 'linkedin' && mutuals.count > p.waysIn) {
|
|
641
|
+
return { text: `${fmt(mutuals.count)} mutuals · ${fmt(p.waysIn)} mapped`, rare: false, more: mutuals.count };
|
|
642
|
+
}
|
|
643
|
+
return p.waysIn === 1 ? { text: 'only way in', rare: true, more: 0 } : { text: `${fmt(p.waysIn)} ways in`, rare: false, more: 0 };
|
|
644
|
+
}
|
|
435
645
|
|
|
436
|
-
const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, youLabel, isMobile, onPick, onPickBridge }) {
|
|
437
|
-
const { width, height, you, people, bridges, links, spokes } = layout;
|
|
646
|
+
const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, youLabel, isMobile, onPick, onPickBridge, mutualsBy, doors = false, cards = false }) {
|
|
647
|
+
const { width, height, you, people, bridges, links, spokes, ghosts = [], unscanned = 0, ghostLabel = null, drawn = 1 } = layout;
|
|
438
648
|
const linksBy = useMemo(() => {
|
|
439
649
|
const m = new Map();
|
|
440
650
|
for (const l of links) { if (!m.has(l.key)) m.set(l.key, []); m.get(l.key).push(l); }
|
|
@@ -453,42 +663,122 @@ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, you
|
|
|
453
663
|
</linearGradient>
|
|
454
664
|
</defs>
|
|
455
665
|
|
|
666
|
+
<defs>
|
|
667
|
+
<radialGradient id="sepAimHalo"><stop offset="0" stopColor="#FFD700" stopOpacity="0.2" /><stop offset="1" stopColor="#FFD700" stopOpacity="0" /></radialGradient>
|
|
668
|
+
</defs>
|
|
456
669
|
{spokes.map((s) => (
|
|
457
|
-
<
|
|
670
|
+
<path key={`s-${s.id ?? 'unresolved'}`} className="spk in" d={`M${s.x1},${s.y1} L${cards ? s.x2 - PILL_W : s.x2},${s.y2}`} fill="none" stroke="#fff" strokeOpacity={0.12} strokeWidth={1} />
|
|
458
671
|
))}
|
|
459
672
|
|
|
673
|
+
{/* Not as bright when most of the way in is still unscanned: the glow follows the share drawn. */}
|
|
674
|
+
{cards && people.length === 1 && people[0].big && (
|
|
675
|
+
<circle className="mv" style={{ ...at(people[0].x + Math.min(200, (width - people[0].x) / 2), people[0].y), opacity: 0.3 + 0.7 * drawn }} r={170} fill="url(#sepAimHalo)" />
|
|
676
|
+
)}
|
|
677
|
+
|
|
678
|
+
{/* Mutual connections in circles you haven't scanned: grey, each with a faint line in. */}
|
|
679
|
+
{people.length === 1 && people[0].big && ghosts.length > 0 && (
|
|
680
|
+
<g className="in" pointerEvents="none">
|
|
681
|
+
{ghosts.map((g, i) => {
|
|
682
|
+
const t = people[0];
|
|
683
|
+
const x2 = cards ? t.x : t.x - t.r - 2;
|
|
684
|
+
const mx = (g.x + x2) / 2;
|
|
685
|
+
return <path key={`gl-${i}`} d={`M${g.x},${g.y} C${mx},${g.y} ${mx},${t.y} ${x2},${t.y}`} fill="none" stroke="#8a8fa8" strokeOpacity={0.09} strokeWidth={0.8} />;
|
|
686
|
+
})}
|
|
687
|
+
{ghosts.map((g, i) => <circle key={`gd-${i}`} cx={g.x} cy={g.y} r={3.3} fill="#4a4f68" stroke="#6b7090" strokeWidth={0.6} strokeOpacity={0.6} />)}
|
|
688
|
+
{ghostLabel && (
|
|
689
|
+
<text x={ghostLabel.x} y={ghostLabel.y} fontSize={10.5} fontWeight={700} fill="#8a8fa8" style={HALO}>
|
|
690
|
+
{`+${fmt(unscanned)} more mutual${unscanned === 1 ? '' : 's'}, in circles not scanned yet`}
|
|
691
|
+
{unscanned > ghosts.length ? ` (${fmt(ghosts.length)} shown)` : ''}
|
|
692
|
+
</text>
|
|
693
|
+
)}
|
|
694
|
+
</g>
|
|
695
|
+
)}
|
|
460
696
|
<g className="ppl">
|
|
461
697
|
{people.map((pp) => {
|
|
462
698
|
const { p } = pp;
|
|
463
699
|
const c = tierColors[p.tier] || '#888';
|
|
464
700
|
const sel = pp.key === selectedKey;
|
|
465
701
|
const via = p.routes.map((r) => (r.bridge ? r.bridge.name : CANT_NAME)).join(', ');
|
|
702
|
+
const tag = waysTag(p, mutualsBy?.get(p.key));
|
|
703
|
+
const lx = pp.r + 14;
|
|
704
|
+
const room = Math.max(8, Math.floor((width - pp.x - lx) / 6));
|
|
705
|
+
const clip = (t) => (String(t || '').length > room ? `${String(t).slice(0, room - 1)}…` : String(t || ''));
|
|
466
706
|
return (
|
|
467
707
|
<g key={pp.key} className={sel ? 'sp sel' : 'sp'} onClick={() => onPick(p)}>
|
|
468
|
-
<title>{`${p.person.name} · #${p.rank}${p.tied ? '=' : ''} · ${p.tier} · ${p.score.toFixed(1)} · via ${via}`}</title>
|
|
708
|
+
<title>{`${p.person.name} · #${p.rank}${p.tied ? '=' : ''} · ${p.tier} · ${p.score.toFixed(1)} · ${tag.text} · via ${via}`}</title>
|
|
469
709
|
{(linksBy.get(pp.key) || []).map((l) => {
|
|
470
710
|
const mx = (l.x1 + l.x2) / 2;
|
|
471
|
-
|
|
711
|
+
// Aiming at one person: their other routes are gold; among ten, each bridge's tier colour.
|
|
712
|
+
const stroke = l.unresolved ? '#777' : l.primary ? ORANGE : pp.big ? '#FFD700' : tierColors[l.tier] || '#888';
|
|
472
713
|
return (
|
|
473
714
|
<path
|
|
474
715
|
key={`${l.key}-${l.bridgeId ?? 'u'}`}
|
|
475
|
-
className="rt"
|
|
476
|
-
d={
|
|
716
|
+
className="rt in"
|
|
717
|
+
d={cards
|
|
718
|
+
? `M${l.x1},${l.y1} C${mx},${l.y1} ${mx},${l.y2} ${l.x2},${l.y2}`
|
|
719
|
+
: `M${l.x1 + 5},${l.y1} C${mx},${l.y1} ${mx},${l.y2} ${l.x2 - pp.r - (pp.big ? 2 : 0)},${l.y2}`}
|
|
477
720
|
fill="none"
|
|
478
721
|
stroke={stroke}
|
|
479
|
-
strokeWidth={l.primary ? 1.5 : 1}
|
|
480
|
-
strokeOpacity={l.primary ? 0.8 : 0.35}
|
|
481
|
-
strokeDasharray={l.primary ? undefined : '3 3'}
|
|
722
|
+
strokeWidth={pp.big ? (l.primary ? 2.2 : 1.4) : l.primary ? 1.5 : 1}
|
|
723
|
+
strokeOpacity={pp.big ? (l.primary ? 0.95 : 0.6) : l.primary ? 0.8 : 0.35}
|
|
724
|
+
strokeDasharray={l.primary ? undefined : pp.big ? '5 4' : '3 3'}
|
|
482
725
|
/>
|
|
483
726
|
);
|
|
484
727
|
})}
|
|
485
|
-
|
|
486
|
-
{
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
728
|
+
{/* As cards, each line plugs into both boxes: a small dot at each end */}
|
|
729
|
+
{cards && (linksBy.get(pp.key) || []).map((l) => {
|
|
730
|
+
const stroke = l.unresolved ? '#777' : l.primary ? ORANGE : pp.big ? '#FFD700' : tierColors[l.tier] || '#888';
|
|
731
|
+
return (
|
|
732
|
+
<g key={`e-${l.bridgeId ?? 'u'}`} className="in" pointerEvents="none">
|
|
733
|
+
<circle className="mv" style={at(l.x1, l.y1)} r={2.6} fill={stroke} />
|
|
734
|
+
<circle className="mv" style={at(l.x2, l.y2)} r={2.6} fill={stroke} />
|
|
735
|
+
</g>
|
|
736
|
+
);
|
|
737
|
+
})}
|
|
738
|
+
<g className="mv in" style={at(pp.x, pp.y)}>
|
|
739
|
+
{(pp.rings || []).map((r, i) => (
|
|
740
|
+
<circle key={r} r={r} fill="none" stroke={c} strokeOpacity={0.28 - i * 0.08} strokeWidth={1} strokeDasharray={i ? '2 5' : undefined} />
|
|
741
|
+
))}
|
|
742
|
+
{!cards && <rect x={-12} y={-gap / 2} width={Math.max(0, width - pp.x + 12)} height={gap} fill="transparent" />}
|
|
743
|
+
{cards && pp.big ? (
|
|
744
|
+
<BigCard p={p} c={c} tag={tag} width={Math.max(260, Math.min(400, width - pp.x - 4))} fs={fs} />
|
|
745
|
+
) : cards ? (
|
|
746
|
+
<SmallCard p={p} c={c} tag={tag} width={Math.max(240, Math.min(340, width - pp.x - 4))} sel={sel} />
|
|
747
|
+
) : pp.big ? (
|
|
748
|
+
<>
|
|
749
|
+
<circle r={pp.r} fill="#0a0a1a" stroke={c} strokeWidth={3} strokeOpacity={0.35 + 0.65 * drawn} />
|
|
750
|
+
<text dy="0.35em" textAnchor="middle" fontSize={11} fontWeight={800} fill={c}>{initialsFor(p.person.name)}</text>
|
|
751
|
+
<text x={lx} y={-20} fontSize={fs + 3} fontWeight={800} fill="#fff" style={HALO}>{clip(p.person.name)}</text>
|
|
752
|
+
<text x={lx} y={-4} fontSize={fs} fill="#aaa" style={HALO}>{clip(subline(p.person))}</text>
|
|
753
|
+
<text x={lx} y={13} fontSize={fs} fontWeight={700} fill={c} style={HALO}>
|
|
754
|
+
#{p.rank}{p.tied ? '=' : ''} · {p.tier}-tier · {p.score.toFixed(1)}
|
|
755
|
+
</text>
|
|
756
|
+
<text x={lx} y={29} fontSize={fs} fill="#ccc" style={HALO}>
|
|
757
|
+
{clip(tag.more
|
|
758
|
+
? `${fmt(tag.more)} mutual connections · ${fmt(p.waysIn)} drawn here`
|
|
759
|
+
: p.waysIn === 1 ? '1 mutual connection: the only way in' : `${fmt(p.waysIn)} mutual connections, all drawn`)}
|
|
760
|
+
</text>
|
|
761
|
+
{tag.more > 0 && (
|
|
762
|
+
<text x={lx} y={44} fontSize={fs - 1} fill="#778" style={HALO}>
|
|
763
|
+
{clip('The count is LinkedIn’s; the rest are in circles not scanned yet')}
|
|
764
|
+
</text>
|
|
765
|
+
)}
|
|
766
|
+
</>
|
|
767
|
+
) : (
|
|
768
|
+
<>
|
|
769
|
+
{sel && <circle r={pp.r + 3.5} fill="none" stroke="#fff" strokeWidth={1.5} />}
|
|
770
|
+
<circle r={pp.r} fill={c} />
|
|
771
|
+
<text x={14} dy="0.35em" fontSize={fs} fill="#ddd" style={HALO}>
|
|
772
|
+
{pp.label}
|
|
773
|
+
<tspan dx={6} fill={c} fontWeight={700}>{p.score.toFixed(1)}</tspan>
|
|
774
|
+
{/* How many mutual connections lead to them: one is the rare kind */}
|
|
775
|
+
{doors && (
|
|
776
|
+
<tspan dx={8} fontSize={fs - 2} fontWeight={700} fill={tag.rare ? RARE : '#778'}>{tag.text}</tspan>
|
|
777
|
+
)}
|
|
778
|
+
</text>
|
|
779
|
+
</>
|
|
780
|
+
)}
|
|
781
|
+
</g>
|
|
492
782
|
</g>
|
|
493
783
|
);
|
|
494
784
|
})}
|
|
@@ -504,25 +794,29 @@ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, you
|
|
|
504
794
|
>
|
|
505
795
|
<title>{b.unresolved
|
|
506
796
|
? 'Routes whose connection record doesn’t match anyone in your 1st-degree list, so who introduces you can’t be named'
|
|
507
|
-
: `${b.bridge.name} · ${b.bridge.tier}-tier · a way in to ${b.count} of these · click to open`}</title>
|
|
508
|
-
<
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
{
|
|
518
|
-
|
|
797
|
+
: `${b.bridge.name} · ${b.bridge.tier}-tier${b.count > 1 ? ` · a way in to ${b.count} of these` : ''} · click to open`}</title>
|
|
798
|
+
<g className="mv in" style={at(b.x, b.y)}>
|
|
799
|
+
{cards ? <Pill b={b} c={c} big={people.length === 1 && people[0].big} /> : (<>
|
|
800
|
+
<circle
|
|
801
|
+
r={b.r}
|
|
802
|
+
fill={b.unresolved ? 'none' : c}
|
|
803
|
+
stroke={b.unresolved ? '#777' : '#0a0a1a'}
|
|
804
|
+
strokeWidth={b.unresolved ? 1 : 1.5}
|
|
805
|
+
strokeDasharray={b.unresolved ? '2 2' : undefined}
|
|
806
|
+
/>
|
|
807
|
+
<text x={-9} dy="0.35em" textAnchor="end" fontSize={fs - 1} fill={c}
|
|
808
|
+
fontStyle={b.unresolved ? 'italic' : undefined} fontWeight={b.unresolved ? 400 : b.primary ? 800 : 600} style={HALO}>
|
|
809
|
+
{b.label}
|
|
810
|
+
</text>
|
|
811
|
+
</>)}
|
|
812
|
+
</g>
|
|
519
813
|
</g>
|
|
520
814
|
);
|
|
521
815
|
})}
|
|
522
816
|
|
|
523
|
-
<g>
|
|
524
|
-
<circle
|
|
525
|
-
<text
|
|
817
|
+
<g className="mv" style={at(you.x, you.y)}>
|
|
818
|
+
<circle r={you.r} fill="url(#sepYouGrad)" />
|
|
819
|
+
<text dy="0.35em" textAnchor="middle" fontSize={youLabel.length > 2 ? 9 : 10} fontWeight={800} fill="#000">
|
|
526
820
|
{youLabel}
|
|
527
821
|
</text>
|
|
528
822
|
</g>
|
|
@@ -530,11 +824,102 @@ const SummitMap = memo(function SummitMap({ layout, selectedKey, tierColors, you
|
|
|
530
824
|
);
|
|
531
825
|
});
|
|
532
826
|
|
|
827
|
+
// ── Cards (Blake, 2026-10-02: mock-ups 1 and 1b) ───────────────────────────
|
|
828
|
+
// Connections as pills in the middle, people as cards on the right; at the
|
|
829
|
+
// easy end, the one person as a big card. Plain SVG, placed by the same
|
|
830
|
+
// gliding groups as the dots, so the slider still moves everything smoothly.
|
|
831
|
+
const PILL_W = 236;
|
|
832
|
+
const clipText = (t, n) => (String(t || '').length > n ? `${String(t).slice(0, Math.max(1, n - 1))}…` : String(t || ''));
|
|
833
|
+
const chipW = (text, size = 9.5) => Math.round(String(text).length * size * 0.6 + 14);
|
|
834
|
+
|
|
835
|
+
/** A connection: their initials, name, tier and what they are to the people on the map. */
|
|
836
|
+
function Pill({ b, c, big }) {
|
|
837
|
+
if (b.unresolved) {
|
|
838
|
+
return (
|
|
839
|
+
<g>
|
|
840
|
+
<rect x={-PILL_W} y={-14} width={PILL_W} height={28} rx={14} fill="#11132a" stroke="#555" strokeDasharray="3 3" />
|
|
841
|
+
<text x={-PILL_W + 14} dy="0.35em" fontSize={11.5} fontStyle="italic" fill="#888">{clipText(b.label, 30)}</text>
|
|
842
|
+
</g>
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const tier = b.bridge.tier;
|
|
846
|
+
const tag = big
|
|
847
|
+
? (b.primary ? `${tier} · best way in` : tier)
|
|
848
|
+
: b.onlyFor ? `${tier} · only door${b.onlyFor > 1 ? ` ×${b.onlyFor}` : ''}`
|
|
849
|
+
: b.bestFor ? `${tier} · door for ${b.bestFor}` : tier;
|
|
850
|
+
const hot = big ? b.primary : b.bestFor > 0;
|
|
851
|
+
const tw = chipW(tag);
|
|
852
|
+
return (
|
|
853
|
+
<g>
|
|
854
|
+
<rect x={-PILL_W} y={-14} width={PILL_W} height={28} rx={14} fill="#14172e"
|
|
855
|
+
stroke={hot ? 'rgba(255,215,0,0.55)' : 'rgba(255,255,255,0.09)'} strokeWidth={hot ? 1.3 : 1} />
|
|
856
|
+
<circle cx={-PILL_W + 15} r={10} fill={c} />
|
|
857
|
+
<text x={-PILL_W + 15} dy="0.35em" textAnchor="middle" fontSize={8} fontWeight={800} fill="#000">{initialsFor(b.bridge.name)}</text>
|
|
858
|
+
<text x={-PILL_W + 31} dy="0.35em" fontSize={12} fontWeight={600} fill="#e8e8f0">
|
|
859
|
+
{clipText(b.bridge.name, Math.max(6, Math.floor((PILL_W - 31 - tw - 10) / 6.4)))}
|
|
860
|
+
</text>
|
|
861
|
+
<rect x={-tw - 7} y={-9} width={tw} height={18} rx={9} fill={`${c}22`} />
|
|
862
|
+
<text x={-7 - tw / 2} dy="0.35em" textAnchor="middle" fontSize={9.5} fontWeight={800} fill={c}>{tag}</text>
|
|
863
|
+
</g>
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Someone on the map: rank, name, role, score, and how many ways in. */
|
|
868
|
+
function SmallCard({ p, c, tag, width, sel }) {
|
|
869
|
+
const tw = chipW(tag.text);
|
|
870
|
+
return (
|
|
871
|
+
<g>
|
|
872
|
+
<rect x={0} y={-20} width={width} height={40} rx={10} fill="#151830" stroke={sel ? '#fff' : 'rgba(255,255,255,0.09)'} strokeWidth={sel ? 1.5 : 1} />
|
|
873
|
+
<text x={12} dy="0.35em" fontSize={11} fontWeight={800} fill="#FFD700">#{p.rank}{p.tied ? '=' : ''}</text>
|
|
874
|
+
<text x={60} y={-3} fontSize={13} fontWeight={700} fill="#fff">{clipText(p.person.name, Math.floor((width - 60 - 70) / 7))}</text>
|
|
875
|
+
<text x={60} y={12} fontSize={10.5} fill="#8a8fa8">{clipText(subline(p.person), Math.floor((width - 60 - tw - 20) / 5.6))}</text>
|
|
876
|
+
<text x={width - 12} y={-2} textAnchor="end" fontSize={14} fontWeight={800} fill={c}>{p.score.toFixed(1)}</text>
|
|
877
|
+
<rect x={width - 12 - tw} y={4} width={tw} height={14} rx={7}
|
|
878
|
+
fill={tag.rare ? 'rgba(0,229,255,0.1)' : 'rgba(255,255,255,0.06)'} stroke={tag.rare ? 'rgba(0,229,255,0.35)' : 'rgba(255,255,255,0.14)'} />
|
|
879
|
+
<text x={width - 12 - tw / 2} y={11} dy="0.35em" textAnchor="middle" fontSize={9.5} fontWeight={800} fill={tag.rare ? RARE : '#cfd3e6'}>{tag.text}</text>
|
|
880
|
+
</g>
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** The one person you're aiming at: a large card with both numbers that matter. */
|
|
885
|
+
function BigCard({ p, c, tag, width, fs }) {
|
|
886
|
+
const named = p.routes.filter((r) => r.bridge).length;
|
|
887
|
+
const first = tag.more ? `${fmt(tag.more)} mutual connections` : p.waysIn === 1 ? '1 mutual connection' : `${fmt(p.waysIn)} mutual connections`;
|
|
888
|
+
const second = tag.more ? `${fmt(p.waysIn)} drawn here` : p.waysIn === 1 ? 'the only way in' : 'all drawn here';
|
|
889
|
+
const w1 = chipW(first, 10.5);
|
|
890
|
+
const w2 = chipW(second, 10.5);
|
|
891
|
+
const tx = 96;
|
|
892
|
+
const room = Math.floor((width - tx - 14) / 6.6);
|
|
893
|
+
return (
|
|
894
|
+
<g>
|
|
895
|
+
<rect x={0} y={-76} width={width} height={152} rx={16} fill="#151830" stroke="rgba(255,215,0,0.45)" />
|
|
896
|
+
<circle cx={48} r={32} fill="#0a0a1a" stroke={c} strokeWidth={3} />
|
|
897
|
+
<text x={48} dy="0.35em" textAnchor="middle" fontSize={18} fontWeight={800} fill={c}>{initialsFor(p.person.name)}</text>
|
|
898
|
+
<text x={tx} y={-40} fontSize={11} fontWeight={800} fill="#FFD700">#{p.rank}{p.tied ? '=' : ''} · {p.tier}-tier · {p.score.toFixed(1)}</text>
|
|
899
|
+
<text x={tx} y={-17} fontSize={fs + 8} fontWeight={800} fill="#fff">{clipText(p.person.name, Math.floor(room * 0.72))}</text>
|
|
900
|
+
<text x={tx} y={1} fontSize={12} fill="#8a8fa8">{clipText(subline(p.person), room)}</text>
|
|
901
|
+
<rect x={tx} y={12} width={w1} height={20} rx={10} fill="rgba(255,112,67,0.12)" stroke="rgba(255,112,67,0.4)" />
|
|
902
|
+
<text x={tx + w1 / 2} y={22} dy="0.35em" textAnchor="middle" fontSize={10.5} fontWeight={800} fill={EASY}>{first}</text>
|
|
903
|
+
<rect x={tx + w1 + 6} y={12} width={w2} height={20} rx={10} fill="rgba(255,255,255,0.06)" stroke="rgba(255,255,255,0.14)" />
|
|
904
|
+
<text x={tx + w1 + 6 + w2 / 2} y={22} dy="0.35em" textAnchor="middle" fontSize={10.5} fontWeight={800} fill="#cfd3e6">{second}</text>
|
|
905
|
+
<text x={tx} y={50} fontSize={10.5} fill="#6b7090">
|
|
906
|
+
{tag.more ? `The ${fmt(tag.more)} is LinkedIn’s count. The other ${fmt(tag.more - p.waysIn)}` : named === 1 ? 'Only one of your connections knows them.' : `${fmt(named)} of your connections know them.`}
|
|
907
|
+
</text>
|
|
908
|
+
{tag.more > 0 && (
|
|
909
|
+
<text x={tx} y={64} fontSize={10.5} fill="#6b7090">
|
|
910
|
+
{clipText('are in circles not scanned yet: the grey dots.', Math.floor((width - tx - 14) / 5.4))}
|
|
911
|
+
</text>
|
|
912
|
+
)}
|
|
913
|
+
</g>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
533
917
|
// ── Rows ───────────────────────────────────────────────────────────────────
|
|
534
918
|
|
|
535
919
|
const ellipsis = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
|
536
920
|
|
|
537
921
|
function Via({ p, route, tierColors, compact }) {
|
|
922
|
+
if (!compact) return <WayIn p={p} route={route} tierColors={tierColors} />;
|
|
538
923
|
if (!route?.bridge) {
|
|
539
924
|
return <span style={{ ...ellipsis, color: '#777', fontStyle: 'italic', fontSize: compact ? 11 : 12 }}>via {CANT_NAME}</span>;
|
|
540
925
|
}
|
|
@@ -555,6 +940,44 @@ function Via({ p, route, tierColors, compact }) {
|
|
|
555
940
|
);
|
|
556
941
|
}
|
|
557
942
|
|
|
943
|
+
/** The ledger's way in: the connection's face and name, and under it "only way in" or how many more. */
|
|
944
|
+
function WayIn({ p, route, tierColors }) {
|
|
945
|
+
const b = route?.bridge;
|
|
946
|
+
const extra = p.waysIn - 1;
|
|
947
|
+
return (
|
|
948
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }} title={`Every way in:\n${everyRoute(p)}`}>
|
|
949
|
+
{b ? <Avatar person={b} size={26} tierColors={tierColors} /> : <span style={{ width: 26, height: 26, borderRadius: '50%', border: '1px dashed #666', flexShrink: 0 }} />}
|
|
950
|
+
<span style={{ minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
|
951
|
+
<span style={{ ...ellipsis, fontSize: 13, fontWeight: 700, color: b ? tierColors[b.tier] || '#ddd' : '#777', fontStyle: b ? 'normal' : 'italic' }}>
|
|
952
|
+
{b ? b.name : CANT_NAME}
|
|
953
|
+
</span>
|
|
954
|
+
<span style={{ ...ellipsis, fontSize: 10.5, marginTop: 1, color: extra > 0 ? '#99a' : RARE, fontWeight: extra > 0 ? 500 : 700 }}>
|
|
955
|
+
{extra > 0 ? `+${extra} more way${extra > 1 ? 's' : ''} in` : 'only way in'}
|
|
956
|
+
</span>
|
|
957
|
+
</span>
|
|
958
|
+
</span>
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** How rare the way in is, as a bar: full for one door, nearly empty for the warmest. */
|
|
963
|
+
function RarityBar({ p, rarity }) {
|
|
964
|
+
const info = rarity ? rarityInfo(rarity.key) : null;
|
|
965
|
+
if (!info) return <span />;
|
|
966
|
+
const fill = Math.max(0.06, 1 - easeOf(rarity.count));
|
|
967
|
+
const mapped = rarity.from === 'linkedin' && rarity.count > p.waysIn ? `, ${fmt(p.waysIn)} mapped` : '';
|
|
968
|
+
return (
|
|
969
|
+
<span style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}
|
|
970
|
+
title={`${rarity.count} mutual connection${rarity.count === 1 ? '' : 's'}${rarity.from === 'scans' ? ' (from your scans, so it can only go up)' : ' (LinkedIn’s count)'}. ${RARITY_NOTE}`}>
|
|
971
|
+
<span style={{ height: 6, borderRadius: 3, background: 'rgba(255,255,255,0.07)', overflow: 'hidden' }}>
|
|
972
|
+
<span style={{ display: 'block', height: '100%', width: `${fill * 100}%`, borderRadius: 3, background: info.color }} />
|
|
973
|
+
</span>
|
|
974
|
+
<span style={{ ...ellipsis, fontSize: 10.5, fontWeight: 700, color: info.color }}>
|
|
975
|
+
{info.label} · {fmt(rarity.count)} mutual{rarity.count === 1 ? '' : 's'}{mapped}
|
|
976
|
+
</span>
|
|
977
|
+
</span>
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
558
981
|
/** "Request sent" or "Connected", and how rare the way in is. */
|
|
559
982
|
function Tags({ status, rarity }) {
|
|
560
983
|
const info = rarity ? rarityInfo(rarity.key) : null;
|
|
@@ -579,6 +1002,84 @@ function Tags({ status, rarity }) {
|
|
|
579
1002
|
);
|
|
580
1003
|
}
|
|
581
1004
|
|
|
1005
|
+
// ── Path: pick one sector or one company ───────────────────────────────────
|
|
1006
|
+
// Opens under the Path button, over the slider, and closes on a pick, on Esc
|
|
1007
|
+
// or on a click anywhere else. Sectors are inferred (lib/companies.js), so it says so.
|
|
1008
|
+
function PathPicker({ facets, text, onText, onPick, onClose, isMobile }) {
|
|
1009
|
+
const needle = text.trim().toLowerCase();
|
|
1010
|
+
const sectors = (facets?.sectors || []).filter((i) => !needle || i.label.toLowerCase().includes(needle));
|
|
1011
|
+
const companies = (facets?.companies || []).filter((c) => !needle || c.name.toLowerCase().includes(needle)).slice(0, needle ? 60 : 24);
|
|
1012
|
+
const onKey = (e) => {
|
|
1013
|
+
if (e.key === 'Escape') { e.stopPropagation(); onClose(); }
|
|
1014
|
+
else if (e.key === 'Enter') {
|
|
1015
|
+
if (sectors.length === 1 && !companies.length) onPick({ kind: 'sector', key: sectors[0].key, label: sectors[0].label, color: sectors[0].color });
|
|
1016
|
+
else if (companies.length === 1 && !sectors.length) onPick({ kind: 'company', key: companies[0].name, label: companies[0].name });
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
const sub = { fontSize: 9, fontWeight: 700, color: '#667', letterSpacing: 1, textTransform: 'uppercase', margin: '10px 0 6px' };
|
|
1020
|
+
return (
|
|
1021
|
+
<>
|
|
1022
|
+
<div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 40 }} />
|
|
1023
|
+
<div role="dialog" aria-label="Pick a sector or a company" style={{
|
|
1024
|
+
position: 'absolute', zIndex: 41, top: isMobile ? 52 : 44, left: isMobile ? 6 : 14, width: isMobile ? 'calc(100% - 12px)' : 'min(560px, calc(100% - 28px))',
|
|
1025
|
+
maxHeight: 340, overflowY: 'auto', boxSizing: 'border-box', padding: 12, borderRadius: 12,
|
|
1026
|
+
background: '#0c101e', border: `1px solid ${PATH}55`, boxShadow: '0 12px 40px rgba(0,0,0,0.6)',
|
|
1027
|
+
}}>
|
|
1028
|
+
<input
|
|
1029
|
+
autoFocus type="search" value={text} onChange={(e) => onText(e.target.value)} onKeyDown={onKey}
|
|
1030
|
+
placeholder="Type a sector or a company" aria-label="Type a sector or a company"
|
|
1031
|
+
style={{
|
|
1032
|
+
width: '100%', height: isMobile ? 44 : 34, boxSizing: 'border-box', padding: '0 12px', borderRadius: 8, outline: 'none',
|
|
1033
|
+
color: '#fff', fontSize: isMobile ? 16 : 13, border: '1px solid rgba(255,255,255,0.14)', background: 'rgba(255,255,255,0.06)',
|
|
1034
|
+
}}
|
|
1035
|
+
/>
|
|
1036
|
+
{sectors.length > 0 && <div style={sub}>Sectors · inferred from company names and headlines</div>}
|
|
1037
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
1038
|
+
{sectors.map((i) => (
|
|
1039
|
+
<button key={i.key} type="button" onClick={() => onPick({ kind: 'sector', key: i.key, label: i.label, color: i.color })} style={{
|
|
1040
|
+
height: 28, padding: '0 10px', borderRadius: 14, cursor: 'pointer', fontSize: 11.5, fontWeight: 700, whiteSpace: 'nowrap',
|
|
1041
|
+
border: `1px solid ${i.color}66`, background: `${i.color}14`, color: '#ddd', display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
1042
|
+
}}>
|
|
1043
|
+
<span style={{ width: 7, height: 7, borderRadius: '50%', background: i.color }} />
|
|
1044
|
+
{i.label}
|
|
1045
|
+
<span style={{ fontWeight: 500, color: '#889' }}>{fmt(i.count)}</span>
|
|
1046
|
+
</button>
|
|
1047
|
+
))}
|
|
1048
|
+
</div>
|
|
1049
|
+
{companies.length > 0 && <div style={sub}>Companies{needle ? '' : ' · the biggest; type to find any'}</div>}
|
|
1050
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
1051
|
+
{companies.map((c) => (
|
|
1052
|
+
<button key={c.name} type="button" onClick={() => onPick({ kind: 'company', key: c.name, label: c.name })} style={{
|
|
1053
|
+
height: 28, padding: '0 10px', borderRadius: 14, cursor: 'pointer', fontSize: 11.5, fontWeight: 600, whiteSpace: 'nowrap',
|
|
1054
|
+
border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.04)', color: '#ccc',
|
|
1055
|
+
}}>
|
|
1056
|
+
{c.name}
|
|
1057
|
+
<span style={{ marginLeft: 6, fontWeight: 500, color: '#889' }}>{fmt(c.count)}</span>
|
|
1058
|
+
</button>
|
|
1059
|
+
))}
|
|
1060
|
+
</div>
|
|
1061
|
+
{!sectors.length && !companies.length && (
|
|
1062
|
+
<div style={{ fontSize: 12, color: '#888', padding: '12px 2px 4px' }}>
|
|
1063
|
+
{facets ? `Nothing matches ‘${text.trim()}’.` : 'Reading where everyone works…'}
|
|
1064
|
+
</div>
|
|
1065
|
+
)}
|
|
1066
|
+
</div>
|
|
1067
|
+
</>
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const PATH = '#00ff88';
|
|
1072
|
+
// The slider's two ends, in the colours rarity already uses for them.
|
|
1073
|
+
const RARE = '#00E5FF';
|
|
1074
|
+
const EASY = '#FF7043';
|
|
1075
|
+
const SLIDER_CSS = `
|
|
1076
|
+
.sepslide { -webkit-appearance: none; appearance: none; display: block; width: 100%; height: 24px; margin: 4px 0 0; background: transparent; cursor: pointer; }
|
|
1077
|
+
.sepslide::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, ${RARE} 0%, #FFD700 50%, ${EASY} 100%); }
|
|
1078
|
+
.sepslide::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; margin-top: -7px; border-radius: 50%; background: #0a0a1a; border: 3px solid #fff; box-shadow: 0 0 10px rgba(255,255,255,0.45); }
|
|
1079
|
+
.sepslide::-moz-range-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, ${RARE} 0%, #FFD700 50%, ${EASY} 100%); }
|
|
1080
|
+
.sepslide::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: #0a0a1a; border: 3px solid #fff; box-shadow: 0 0 10px rgba(255,255,255,0.45); }
|
|
1081
|
+
.sepslide:focus-visible { outline: 2px solid rgba(255,255,255,0.7); outline-offset: 3px; border-radius: 6px; }
|
|
1082
|
+
`;
|
|
582
1083
|
const RARITY_NOTE = 'Rarity is how many mutual connections lead to them: few is a rare way in, many is warm (likely to accept). It never changes a score or a tier.';
|
|
583
1084
|
|
|
584
1085
|
const Row = memo(function Row({ p, top, height, isMobile, selected, tierColors, onPick, q, status, rarity }) {
|
|
@@ -607,7 +1108,7 @@ const Row = memo(function Row({ p, top, height, isMobile, selected, tierColors,
|
|
|
607
1108
|
style={{
|
|
608
1109
|
position: 'absolute', top, left: 0, right: 0, height, boxSizing: 'border-box',
|
|
609
1110
|
display: 'grid', alignItems: 'center', gap: 10, padding: '0 12px',
|
|
610
|
-
gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' :
|
|
1111
|
+
gridTemplateColumns: isMobile ? '44px 36px 1fr 44px' : COLUMNS,
|
|
611
1112
|
borderLeft: `3px solid ${selected ? ORANGE : 'transparent'}`,
|
|
612
1113
|
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
|
613
1114
|
background: base, cursor: 'pointer',
|
|
@@ -633,22 +1134,29 @@ const Row = memo(function Row({ p, top, height, isMobile, selected, tierColors,
|
|
|
633
1134
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
634
1135
|
<span style={{ ...ellipsis, fontSize: 13, fontWeight: 600, color: member ? '#888' : '#fff' }}>{person.name}</span>
|
|
635
1136
|
<span style={{ flexShrink: 0, fontSize: 9, fontWeight: 800, color: c, border: `1px solid ${c}55`, borderRadius: 4, padding: '0 4px' }}>{p.tier}</span>
|
|
636
|
-
|
|
1137
|
+
{/* The rarity has its own column here */}
|
|
1138
|
+
<Tags status={status} rarity={null} />
|
|
637
1139
|
{member && <span style={{ flexShrink: 0, fontSize: 10, color: '#666' }}>out of network</span>}
|
|
638
1140
|
</div>
|
|
639
1141
|
<div style={{ ...ellipsis, fontSize: 10, color: '#777', marginTop: 2 }}>{subline(person)}</div>
|
|
640
1142
|
</div>
|
|
641
1143
|
<Via p={p} route={route} tierColors={tierColors} />
|
|
642
|
-
<
|
|
643
|
-
|
|
1144
|
+
<RarityBar p={p} rarity={rarity} />
|
|
1145
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
1146
|
+
<div style={{ flex: 1, height: 6, background: 'rgba(255,255,255,0.06)', borderRadius: 3 }}>
|
|
1147
|
+
<div style={{ height: '100%', borderRadius: 3, background: c, width: `${Math.max(0, Math.min(1, p.score / 10)) * 100}%` }} />
|
|
1148
|
+
</div>
|
|
1149
|
+
<span style={{ fontSize: 15, fontWeight: 800, color: c, width: 30, textAlign: 'right' }}>{p.score.toFixed(1)}</span>
|
|
644
1150
|
</div>
|
|
645
1151
|
</>
|
|
646
1152
|
)}
|
|
647
1153
|
|
|
648
|
-
|
|
649
|
-
<div style={{
|
|
650
|
-
|
|
651
|
-
|
|
1154
|
+
{isMobile && (
|
|
1155
|
+
<div style={{ textAlign: 'right' }}>
|
|
1156
|
+
<div style={{ fontSize: 13, fontWeight: 800, color: c }}>{p.score.toFixed(1)}</div>
|
|
1157
|
+
<div style={{ fontSize: 9, color: '#666', fontWeight: 700 }}>{p.tier}</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
)}
|
|
652
1160
|
</div>
|
|
653
1161
|
);
|
|
654
1162
|
});
|