nebulosity 0.0.0-stage → 1.0.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/BUILD_ID +1 -0
- package/.next/app-path-routes-manifest.json +18 -0
- package/.next/build/chunks/[root-of-the-server]__05i36w1t4lzaf._.js +502 -0
- package/.next/build/chunks/[root-of-the-server]__1kki86fknkq7y._.js +191 -0
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_1gfkiq9iz8yb8._.js +13 -0
- package/.next/build/chunks/[turbopack]_runtime.js +1056 -0
- package/.next/build/chunks/node_modules_20v-8wlfss9m1._.js +7192 -0
- package/.next/build/chunks/pool_entry-[turbopack-node]_transforms_postcss_ts_0tp-k2vy8nj6i._.js +6 -0
- package/.next/build/package.json +1 -0
- package/.next/build-manifest.json +54 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/diagnostics/route-bundle-stats.json +26 -0
- package/.next/export-marker.json +6 -0
- package/.next/fallback-build-manifest.json +15 -0
- package/.next/images-manifest.json +68 -0
- package/.next/node_modules/@huggingface/transformers-31f28a0eb9b916d1/index.cjs +1 -0
- package/.next/node_modules/@huggingface/transformers-31f28a0eb9b916d1/index.mjs +1 -0
- package/.next/node_modules/@huggingface/transformers-31f28a0eb9b916d1/package.json +9 -0
- package/.next/node_modules/better-sqlite3-90e2652d1716b047/index.cjs +1 -0
- package/.next/node_modules/better-sqlite3-90e2652d1716b047/index.mjs +2 -0
- package/.next/node_modules/better-sqlite3-90e2652d1716b047/package.json +9 -0
- package/.next/node_modules/typescript-279735412610ef8d/index.cjs +1 -0
- package/.next/node_modules/typescript-279735412610ef8d/index.mjs +2 -0
- package/.next/node_modules/typescript-279735412610ef8d/package.json +9 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +161 -0
- package/.next/required-server-files.js +369 -0
- package/.next/required-server-files.json +369 -0
- package/.next/routes-manifest.json +152 -0
- package/.next/server/app/_global-error/page/app-paths-manifest.json +3 -0
- package/.next/server/app/_global-error/page/build-manifest.json +21 -0
- package/.next/server/app/_global-error/page/next-font-manifest.json +6 -0
- package/.next/server/app/_global-error/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/_global-error/page/server-reference-manifest.json +4 -0
- package/.next/server/app/_global-error/page.js +10 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +3 -0
- package/.next/server/app/_global-error.html +1 -0
- package/.next/server/app/_global-error.meta +30 -0
- package/.next/server/app/_global-error.rsc +19 -0
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +24 -0
- package/.next/server/app/_global-error.segments/_full.segment.rsc +19 -0
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +2 -0
- package/.next/server/app/_not-found/page/app-paths-manifest.json +3 -0
- package/.next/server/app/_not-found/page/build-manifest.json +21 -0
- package/.next/server/app/_not-found/page/next-font-manifest.json +12 -0
- package/.next/server/app/_not-found/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/_not-found/page/server-reference-manifest.json +4 -0
- package/.next/server/app/_not-found/page.js +12 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +3 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +35 -0
- package/.next/server/app/_not-found.rsc +21 -0
- package/.next/server/app/_not-found.segments/_full.segment.rsc +21 -0
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +27 -0
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/api/describe/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/describe/route/build-manifest.json +12 -0
- package/.next/server/app/api/describe/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/describe/route.js +9 -0
- package/.next/server/app/api/describe/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/imports/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/imports/route/build-manifest.json +12 -0
- package/.next/server/app/api/imports/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/imports/route.js +9 -0
- package/.next/server/app/api/imports/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/index/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/index/route/build-manifest.json +12 -0
- package/.next/server/app/api/index/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/index/route.js +10 -0
- package/.next/server/app/api/index/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/level/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/level/route/build-manifest.json +12 -0
- package/.next/server/app/api/level/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/level/route.js +9 -0
- package/.next/server/app/api/level/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/live/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/live/route/build-manifest.json +12 -0
- package/.next/server/app/api/live/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/live/route.js +9 -0
- package/.next/server/app/api/live/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/live/stream/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/live/stream/route/build-manifest.json +12 -0
- package/.next/server/app/api/live/stream/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/live/stream/route.js +7 -0
- package/.next/server/app/api/live/stream/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/repos/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/repos/route/build-manifest.json +12 -0
- package/.next/server/app/api/repos/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/repos/route.js +10 -0
- package/.next/server/app/api/repos/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/search/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/search/route/build-manifest.json +12 -0
- package/.next/server/app/api/search/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/search/route.js +10 -0
- package/.next/server/app/api/search/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/source/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/source/route/build-manifest.json +12 -0
- package/.next/server/app/api/source/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/source/route.js +9 -0
- package/.next/server/app/api/source/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/status/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/status/route/build-manifest.json +12 -0
- package/.next/server/app/api/status/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/status/route.js +11 -0
- package/.next/server/app/api/status/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/summarize/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/summarize/route/build-manifest.json +12 -0
- package/.next/server/app/api/summarize/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/summarize/route.js +10 -0
- package/.next/server/app/api/summarize/route_client-reference-manifest.js +3 -0
- package/.next/server/app/favicon.ico/route/app-paths-manifest.json +3 -0
- package/.next/server/app/favicon.ico/route/build-manifest.json +12 -0
- package/.next/server/app/favicon.ico/route.js +8 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/icon.svg/route/app-paths-manifest.json +3 -0
- package/.next/server/app/icon.svg/route/build-manifest.json +12 -0
- package/.next/server/app/icon.svg/route.js +8 -0
- package/.next/server/app/icon.svg.body +31 -0
- package/.next/server/app/icon.svg.meta +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +29 -0
- package/.next/server/app/index.rsc +25 -0
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +27 -0
- package/.next/server/app/index.segments/_full.segment.rsc +25 -0
- package/.next/server/app/index.segments/_tree.segment.rsc +7 -0
- package/.next/server/app/page/app-paths-manifest.json +3 -0
- package/.next/server/app/page/build-manifest.json +21 -0
- package/.next/server/app/page/next-font-manifest.json +12 -0
- package/.next/server/app/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/page/server-reference-manifest.json +4 -0
- package/.next/server/app/page.js +14 -0
- package/.next/server/app/page_client-reference-manifest.js +3 -0
- package/.next/server/app-paths-manifest.json +18 -0
- package/.next/server/chunks/[externals]_@huggingface_transformers_05bd-zziu8167._.js +3 -0
- package/.next/server/chunks/[externals]__0zvqltcvrhqse._.js +3 -0
- package/.next/server/chunks/[externals]__1i28q8c2pqfup._.js +3 -0
- package/.next/server/chunks/[externals]__1xk8lm5my7js6._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__051s9u4mcetdb._.js +6 -0
- package/.next/server/chunks/[root-of-the-server]__06blyow8irfw3._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__0_6g347u9qa57._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0bmqko-d8fd5k._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__0cmmlmvta4i0g._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__0dt_sq7idt1os._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0hgacpyjkqojz._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0jgr1dt1j95ku._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__0m0n163465-zb._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__0p7epsgp2b5m3._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__0vyz_1ns_nua5._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1167paf30_wq8._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__15bjnu5yieigz._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__16ik54iewx9z3._.js +6 -0
- package/.next/server/chunks/[root-of-the-server]__1bhqa26ehjh0e._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1cfurs3fcv85q._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1i5e5sqg45ens._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__1qmpom1yjgoe6._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1vjt0neva-819._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__1z2mv_4lkoix_._.js +68 -0
- package/.next/server/chunks/[root-of-the-server]__1zjq62ps9p2xm._.js +3 -0
- package/.next/server/chunks/[turbopack]_runtime.js +1069 -0
- package/.next/server/chunks/_0btv5i-8jzrkh._.js +3 -0
- package/.next/server/chunks/_0htkitjiqi3b4._.js +17 -0
- package/.next/server/chunks/_0ile-nyzjkyl_._.js +3 -0
- package/.next/server/chunks/_0uqus0ehnijt-._.js +3 -0
- package/.next/server/chunks/_0wemi40xtpagp._.js +3 -0
- package/.next/server/chunks/_143ny4gdo3fhb._.js +3 -0
- package/.next/server/chunks/_17i2yyl2kj1bt._.js +3 -0
- package/.next/server/chunks/_18retujk6-b40._.js +3 -0
- package/.next/server/chunks/_1cl7bj2-5tjc0._.js +3 -0
- package/.next/server/chunks/_1cstru0a1pgds._.js +52 -0
- package/.next/server/chunks/_1mxn_lbuesj3_._.js +3 -0
- package/.next/server/chunks/_1vrfq569dhlwo._.js +3 -0
- package/.next/server/chunks/_1w2kaw7y48w70._.js +3 -0
- package/.next/server/chunks/_1zws2vt6wj4h9._.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_describe_route_actions_0vr6420i-ro3y.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_imports_route_actions_0ky48tq6vuffm.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_index_route_actions_1w8-cjl9-tql7.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_level_route_actions_0tn4in5bloyc8.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_live_route_actions_0dk5m02naz6qd.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_live_stream_route_actions_1bumpsz7v94gh.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_repos_route_actions_1o27l51oy7-gf.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_search_route_actions_0k0mocb1dpixh.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_source_route_actions_18mmajvsmslwc.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_status_route_actions_1qgdetofwsmqf.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_summarize_route_actions_0iw_jzll1h8pz.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_0g2jjlssn1kss.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_1r2h_ubdhjtcp.js +3 -0
- package/.next/server/chunks/lib_background_start_ts_11u_1uu8b9yuy._.js +10 -0
- package/.next/server/chunks/lib_background_start_ts_1uu0hs4_kephi._.js +3 -0
- package/.next/server/chunks/lib_db_store_summary-vectors_ts_019e4mun161yc._.js +3 -0
- package/.next/server/chunks/node_modules_next_0g8xe7c6fvfgl._.js +61 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dyligeswbb47._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0ekw8p2ramybv._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0fwbsqt9e2tr-._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0py1t7q9ntzgc._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__17_rqmx62cz59._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__18ntzywv7t1cq._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1cf6qt9agg_9-._.js +14 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1zfp6sltse4hf._.js +3 -0
- package/.next/server/chunks/ssr/[turbopack]_runtime.js +1069 -0
- package/.next/server/chunks/ssr/_0vfwmvlqr2-ce._.js +3 -0
- package/.next/server/chunks/ssr/_1d2woi6toj_0_._.js +3 -0
- package/.next/server/chunks/ssr/_1wfc1zz26g5ji._.js +9 -0
- package/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0zi5s8-psc_d2.js +3 -0
- package/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0pt47yrisjoev.js +3 -0
- package/.next/server/chunks/ssr/_next-internal_server_app_page_actions_0hhsz1jew5ls7.js +3 -0
- package/.next/server/chunks/ssr/app_0gw6epmxyea3q._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_0gxlxpuufujz9._.js +33 -0
- package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_1ztp13a5szri_._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_0cbzfnmfs57ie._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +6 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_1c5t_vsn6u8fr._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_0wpq8j32_ibz4._.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_1bi5-b2i59sgs._.js +33 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0symwr9mbf-fh.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0l_sp0xky89qu.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13q04th-918ib.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_19fbxi_wfcafr.js +3 -0
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1vmwyi-ropznv.js +3 -0
- package/.next/server/edge/chunks/_0o_tvsklgn-yk._.js +3 -0
- package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_1oiuqfca5iqka.js +3 -0
- package/.next/server/functions-config-manifest.json +25 -0
- package/.next/server/instrumentation/middleware-manifest.json +12 -0
- package/.next/server/instrumentation.js +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware/middleware-manifest.json +6 -0
- package/.next/server/middleware-build-manifest.js +54 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware.js +5 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +17 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages-manifest.json +4 -0
- package/.next/server/prefetch-hints.json +34 -0
- package/.next/server/preview-props.json +5 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +5 -0
- package/.next/static/chunks/02x9pxyl2d8om.css +3 -0
- package/.next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- package/.next/static/chunks/0p-0bv2dzer18.js +7 -0
- package/.next/static/chunks/0umf92h1sb77c.js +1 -0
- package/.next/static/chunks/10u7p2gj02qay.js +1 -0
- package/.next/static/chunks/1ix59xuhq-q14.css +1 -0
- package/.next/static/chunks/1rljem7o1jtdo.js +1 -0
- package/.next/static/chunks/1tty6l8tgntgq.js +22 -0
- package/.next/static/chunks/31qp8jfkw0x2h.js +31 -0
- package/.next/static/chunks/3iyxeb0r4wnp2.js +1 -0
- package/.next/static/chunks/turbopack-2c3mg3r-05n1w.js +1 -0
- package/.next/static/media/123d516c9baeb953-s.1qc6dx89jd2qv.woff2 +0 -0
- package/.next/static/media/41ba59f553543ecf-s.3cb5lpr2l6xud.woff2 +0 -0
- package/.next/static/media/44ed10da57ebfb0e-s.2y65deuc30pmy.woff2 +0 -0
- package/.next/static/media/48eea41d8317494f-s.p.0wiir8udbzjvx.woff2 +0 -0
- package/.next/static/media/49b1d53681a91a99-s.1yd4_i3e35931.woff2 +0 -0
- package/.next/static/media/67bba87670b402d1-s.1u30mmyq6lnsd.woff2 +0 -0
- package/.next/static/media/6fca7e74603dd412-s.02z5rrbg5nt92.woff2 +0 -0
- package/.next/static/media/73263b2957b73100-s.2qt3tdwt-y9x_.woff2 +0 -0
- package/.next/static/media/7ac0b1cd3e695dd0-s.1i-sca43-qwng.woff2 +0 -0
- package/.next/static/media/916cd8a3bde3edf8-s.p.0oace-s_gkfks.woff2 +0 -0
- package/.next/static/media/a45aa1001bf9176f-s.2vemzm0itqzfy.woff2 +0 -0
- package/.next/static/media/a74d2542cc4c775f-s.29yke99gs9qpn.woff2 +0 -0
- package/.next/static/media/ab664c0e419cc841-s.3cvcemwuzoywn.woff2 +0 -0
- package/.next/static/media/c1b11b726b36a5e5-s.2rixgazldzomk.woff2 +0 -0
- package/.next/static/media/d6c1ff7e86974212-s.p.40sczeszzbjw1.woff2 +0 -0
- package/.next/static/media/faab88e4be272d29-s.0rgnxg2b64rzs.woff2 +0 -0
- package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- package/.next/static/media/icon.0absiuod-8x8n.svg +31 -0
- package/.next/static/prDi039MhjlFqDOOwZ4aR/_buildManifest.js +11 -0
- package/.next/static/prDi039MhjlFqDOOwZ4aR/_clientMiddlewareManifest.js +6 -0
- package/.next/static/prDi039MhjlFqDOOwZ4aR/_ssgManifest.js +1 -0
- package/.next/trace +1 -0
- package/.next/trace-build +1 -0
- package/.next/turbopack +0 -0
- package/.next/types/cache-life.d.ts +114 -0
- package/.next/types/root-params.d.ts +3 -0
- package/.next/types/routes.d.ts +83 -0
- package/.next/types/validator.ts +201 -0
- package/LICENSE +21 -0
- package/README.md +104 -3
- package/app/api/_lib/respond.ts +33 -0
- package/app/api/_lib/server.ts +14 -0
- package/app/api/describe/route.ts +22 -0
- package/app/api/imports/route.ts +20 -0
- package/app/api/index/route.ts +38 -0
- package/app/api/level/route.ts +28 -0
- package/app/api/live/route.ts +17 -0
- package/app/api/live/stream/route.ts +47 -0
- package/app/api/repos/route.ts +28 -0
- package/app/api/search/route.ts +23 -0
- package/app/api/source/route.ts +21 -0
- package/app/api/status/route.ts +25 -0
- package/app/api/summarize/route.ts +24 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +158 -0
- package/app/icon.svg +31 -0
- package/app/layout.tsx +39 -0
- package/app/map/ActivityBar.tsx +81 -0
- package/app/map/BackButton.tsx +25 -0
- package/app/map/BoxActions.tsx +55 -0
- package/app/map/Breadcrumb.tsx +83 -0
- package/app/map/DetailPanel.tsx +572 -0
- package/app/map/HelpDialog.tsx +134 -0
- package/app/map/Legend.tsx +82 -0
- package/app/map/MapBox.tsx +88 -0
- package/app/map/MapCanvas.tsx +297 -0
- package/app/map/MapView.tsx +686 -0
- package/app/map/RepoMenu.tsx +97 -0
- package/app/map/RoutedEdge.tsx +39 -0
- package/app/map/SearchPalette.tsx +158 -0
- package/app/map/SourceViewer.tsx +144 -0
- package/app/map/StatusPill.tsx +93 -0
- package/app/map/TopBar.tsx +84 -0
- package/app/map/activity.ts +100 -0
- package/app/map/api.ts +125 -0
- package/app/map/apiError.ts +9 -0
- package/app/map/demoData.ts +35 -0
- package/app/map/embed.ts +120 -0
- package/app/map/emphasis.ts +78 -0
- package/app/map/flowNodes.ts +71 -0
- package/app/map/format.ts +66 -0
- package/app/map/history.ts +61 -0
- package/app/map/labels.ts +34 -0
- package/app/map/levelLayout.ts +199 -0
- package/app/map/links.ts +14 -0
- package/app/map/liveConnection.ts +37 -0
- package/app/map/mode.ts +7 -0
- package/app/map/narrow.ts +168 -0
- package/app/map/paths.ts +60 -0
- package/app/map/perf.ts +31 -0
- package/app/map/popover.ts +36 -0
- package/app/map/portals.ts +69 -0
- package/app/map/pulses.ts +18 -0
- package/app/map/routes.ts +105 -0
- package/app/map/searchHits.ts +19 -0
- package/app/map/shortcuts.ts +43 -0
- package/app/map/staticApi.ts +133 -0
- package/app/map/summaries.ts +38 -0
- package/app/map/syntax.ts +39 -0
- package/app/map/text.ts +82 -0
- package/app/map/useDismiss.ts +34 -0
- package/app/map/useEditorBridge.ts +71 -0
- package/app/map/useIndexStatus.ts +44 -0
- package/app/map/useLive.ts +41 -0
- package/app/map/useScrollLock.ts +28 -0
- package/app/map/useSize.ts +45 -0
- package/app/map/viewport.ts +68 -0
- package/app/page.tsx +14 -0
- package/bin/boot.mjs +38 -0
- package/bin/nebulosity.mjs +9 -0
- package/instrumentation.ts +17 -0
- package/lib/agent/tools.ts +311 -0
- package/lib/background/child.ts +63 -0
- package/lib/background/protocol.ts +20 -0
- package/lib/background/service.ts +495 -0
- package/lib/background/start.ts +77 -0
- package/lib/background/supervisor.ts +130 -0
- package/lib/chunks.ts +432 -0
- package/lib/config.ts +224 -0
- package/lib/contracts.ts +473 -0
- package/lib/db/batch.ts +43 -0
- package/lib/db/client.ts +134 -0
- package/lib/db/migrations/0000_lonely_steve_rogers.sql +29 -0
- package/lib/db/migrations/0001_tidy_karen_page.sql +31 -0
- package/lib/db/migrations/0002_pivot_v1_1.sql +48 -0
- package/lib/db/migrations/0003_v1_2_summaries.sql +40 -0
- package/lib/db/migrations/0004_v1_2_search_index.sql +44 -0
- package/lib/db/migrations/0005_v1_3_drop_chunk_vectors.sql +1 -0
- package/lib/db/migrations/0006_v1_3_deferred_code_search.sql +11 -0
- package/lib/db/migrations/0007_v1_6_entry_points.sql +9 -0
- package/lib/db/migrations/0008_v1_6_docs.sql +21 -0
- package/lib/db/migrations/0009_v1_7_calls.sql +43 -0
- package/lib/db/migrations/0010_e4_declares.sql +1 -0
- package/lib/db/migrations/meta/0000_snapshot.json +213 -0
- package/lib/db/migrations/meta/0001_snapshot.json +422 -0
- package/lib/db/migrations/meta/0002_snapshot.json +747 -0
- package/lib/db/migrations/meta/0003_snapshot.json +1026 -0
- package/lib/db/migrations/meta/0004_snapshot.json +1026 -0
- package/lib/db/migrations/meta/0005_snapshot.json +981 -0
- package/lib/db/migrations/meta/0006_snapshot.json +981 -0
- package/lib/db/migrations/meta/0007_snapshot.json +1042 -0
- package/lib/db/migrations/meta/0008_snapshot.json +1185 -0
- package/lib/db/migrations/meta/0009_snapshot.json +1483 -0
- package/lib/db/migrations/meta/0010_snapshot.json +1491 -0
- package/lib/db/migrations/meta/_journal.json +83 -0
- package/lib/db/schema.ts +343 -0
- package/lib/db/store/calls.ts +73 -0
- package/lib/db/store/docs.ts +32 -0
- package/lib/db/store/entries.ts +34 -0
- package/lib/db/store/lock.ts +122 -0
- package/lib/db/store/map.ts +140 -0
- package/lib/db/store/search.ts +77 -0
- package/lib/db/store/stages.ts +46 -0
- package/lib/db/store/structure.ts +327 -0
- package/lib/db/store/summaries.ts +357 -0
- package/lib/db/store/summary-vectors.ts +73 -0
- package/lib/db/store/version.ts +66 -0
- package/lib/demo/export.ts +214 -0
- package/lib/demo/privacy.ts +111 -0
- package/lib/embed.ts +151 -0
- package/lib/engine/architecture.ts +378 -0
- package/lib/engine/calls.ts +424 -0
- package/lib/engine/describe.ts +89 -0
- package/lib/engine/docs.ts +94 -0
- package/lib/engine/errors.ts +97 -0
- package/lib/engine/freshness.ts +21 -0
- package/lib/engine/git-remote.ts +91 -0
- package/lib/engine/graph.ts +140 -0
- package/lib/engine/impact.ts +98 -0
- package/lib/engine/imports-page.ts +129 -0
- package/lib/engine/level.ts +130 -0
- package/lib/engine/overview.ts +77 -0
- package/lib/engine/pages.ts +196 -0
- package/lib/engine/rank.ts +228 -0
- package/lib/engine/reach.ts +240 -0
- package/lib/engine/repos.ts +43 -0
- package/lib/engine/search.ts +195 -0
- package/lib/engine/source.ts +95 -0
- package/lib/engine/staticSearch.ts +150 -0
- package/lib/engine/status.ts +66 -0
- package/lib/hints.ts +18 -0
- package/lib/home.ts +42 -0
- package/lib/index/aggregate.ts +112 -0
- package/lib/index/discover.ts +289 -0
- package/lib/index/docs.ts +215 -0
- package/lib/index/embed.ts +62 -0
- package/lib/index/identifiers.ts +14 -0
- package/lib/index/languages/adapter.ts +101 -0
- package/lib/index/languages/chunking.ts +67 -0
- package/lib/index/languages/csharp/arity.ts +34 -0
- package/lib/index/languages/csharp/index.ts +17 -0
- package/lib/index/languages/csharp/parse.ts +563 -0
- package/lib/index/languages/csharp/resolve.ts +344 -0
- package/lib/index/languages/csharp/rules.ts +71 -0
- package/lib/index/languages/csharp/scan.ts +86 -0
- package/lib/index/languages/go/index.ts +13 -0
- package/lib/index/languages/go/parse.ts +497 -0
- package/lib/index/languages/go/resolve.ts +260 -0
- package/lib/index/languages/go/rules.ts +39 -0
- package/lib/index/languages/grammars/NOTICE.md +200 -0
- package/lib/index/languages/grammars/grammars.json +37 -0
- package/lib/index/languages/grammars/tree-sitter-c_sharp.wasm.gz +0 -0
- package/lib/index/languages/grammars/tree-sitter-go.wasm.gz +0 -0
- package/lib/index/languages/grammars/tree-sitter-java.wasm.gz +0 -0
- package/lib/index/languages/grammars/tree-sitter-python.wasm.gz +0 -0
- package/lib/index/languages/grammars/tree-sitter-rust.wasm.gz +0 -0
- package/lib/index/languages/java/index.ts +13 -0
- package/lib/index/languages/java/parse.ts +478 -0
- package/lib/index/languages/java/resolve.ts +175 -0
- package/lib/index/languages/java/rules.ts +53 -0
- package/lib/index/languages/python/index.ts +17 -0
- package/lib/index/languages/python/parse.ts +386 -0
- package/lib/index/languages/python/resolve.ts +309 -0
- package/lib/index/languages/python/rules.ts +51 -0
- package/lib/index/languages/python/stdlib.ts +80 -0
- package/lib/index/languages/registry.ts +33 -0
- package/lib/index/languages/resolution.ts +70 -0
- package/lib/index/languages/rules.ts +51 -0
- package/lib/index/languages/rust/cargo.ts +163 -0
- package/lib/index/languages/rust/index.ts +13 -0
- package/lib/index/languages/rust/parse.ts +348 -0
- package/lib/index/languages/rust/resolve.ts +331 -0
- package/lib/index/languages/rust/rules.ts +47 -0
- package/lib/index/languages/rust/scan.ts +36 -0
- package/lib/index/languages/rust/syntax.ts +146 -0
- package/lib/index/languages/treesitter.ts +157 -0
- package/lib/index/languages/typescript-adapter.ts +55 -0
- package/lib/index/languages/typescript-calls.ts +663 -0
- package/lib/index/languages/typescript-rules.ts +24 -0
- package/lib/index/languages/typescript.ts +238 -0
- package/lib/index/paths.ts +54 -0
- package/lib/index/pipeline.ts +404 -0
- package/lib/index/resolve.ts +304 -0
- package/lib/index/summarize/budget.ts +105 -0
- package/lib/index/summarize/claude.ts +366 -0
- package/lib/index/summarize/compare.ts +195 -0
- package/lib/index/summarize/concurrency.ts +19 -0
- package/lib/index/summarize/env.ts +4 -0
- package/lib/index/summarize/estimate.ts +14 -0
- package/lib/index/summarize/models.ts +185 -0
- package/lib/index/summarize/plan.ts +252 -0
- package/lib/index/summarize/preview.ts +442 -0
- package/lib/index/summarize/pricing.ts +52 -0
- package/lib/index/summarize/prompts.ts +253 -0
- package/lib/index/summarize/run.ts +948 -0
- package/lib/index/summarize/snapshot.ts +28 -0
- package/lib/index/summarize/status.ts +106 -0
- package/lib/index/summarize/types.ts +77 -0
- package/lib/index/update.ts +311 -0
- package/lib/index/watch.ts +156 -0
- package/lib/index/workspace.ts +337 -0
- package/lib/live/client.ts +24 -0
- package/lib/live/event.ts +40 -0
- package/lib/live/hub.ts +37 -0
- package/lib/live/local.ts +42 -0
- package/lib/live/sse.ts +13 -0
- package/lib/port.ts +15 -0
- package/lib/tools/answers.ts +530 -0
- package/lib/tools/context.ts +17 -0
- package/lib/tools/registry.ts +230 -0
- package/lib/tools/repo.ts +101 -0
- package/lib/tools/text.ts +78 -0
- package/mcp/launch.mjs +11 -0
- package/mcp/server.ts +106 -0
- package/next.config.ts +31 -0
- package/package.json +148 -6
- package/postcss.config.mjs +7 -0
- package/proxy.ts +19 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/background.ts +43 -0
- package/scripts/cli.ts +446 -0
- package/scripts/commands/index-command.ts +112 -0
- package/scripts/commands/summarize-command.ts +276 -0
- package/scripts/env.ts +19 -0
- package/scripts/index.ts +6 -0
- package/scripts/mcp-smoke.mjs +78 -0
- package/scripts/summarize.ts +2 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Map's place in the address bar: `/?path=src/vs&page=2`. Every move to another folder or page goes into the
|
|
3
|
+
* browser's history, so Back returns to the previous folder (owner review), and a reload or a pasted link opens the
|
|
4
|
+
* same place. The page is counted from 1 for people and from 0 inside the app; the slashes of a path stay readable.
|
|
5
|
+
*/
|
|
6
|
+
export type MapPlace = { path: string; page: number };
|
|
7
|
+
|
|
8
|
+
const encodePath = (folderPath: string): string => encodeURIComponent(folderPath).replaceAll("%2F", "/");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The address for a place. Inside the editor's panel (`embedded`) it ends with `embed=vscode`, so a reload, a restarted
|
|
12
|
+
* server or Next's own reload stays embedded and keeps the bridge (V1.8 plan P8); the browser Map never has the flag.
|
|
13
|
+
*/
|
|
14
|
+
export function mapUrl(folderPath: string, page: number, embedded = false): string {
|
|
15
|
+
const params = [
|
|
16
|
+
...(folderPath === "" ? [] : [`path=${encodePath(folderPath)}`]),
|
|
17
|
+
...(page > 0 ? [`page=${page + 1}`] : []),
|
|
18
|
+
...(embedded ? ["embed=vscode"] : []),
|
|
19
|
+
];
|
|
20
|
+
return params.length === 0 ? "/" : `/?${params.join("&")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The place a query string names; anything it can't read is the first page of the root. */
|
|
24
|
+
export function placeFromSearch(search: string): MapPlace {
|
|
25
|
+
const params = new URLSearchParams(search);
|
|
26
|
+
const page = params.get("page") ?? "";
|
|
27
|
+
return { path: params.get("path") ?? "", page: /^\d+$/.test(page) && Number(page) >= 1 ? Number(page) - 1 : 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the address bar needs when the Map moves to `place`: nothing when it already says so; a rewrite when it names the
|
|
32
|
+
* same place another way (`?path=src%2Fvs`, `?page=1`, `?path=`), so Back doesn't stop at a step that goes nowhere; and
|
|
33
|
+
* a new history entry for a move to another place. `address` is the path and query string, `/?path=src`. `embedded`
|
|
34
|
+
* (see `mapUrl`) makes an address without `embed=vscode` one to rewrite, so the flag is not lost.
|
|
35
|
+
*/
|
|
36
|
+
export function historyStep(address: string, place: MapPlace, embedded = false): "none" | "replace" | "push" {
|
|
37
|
+
if (address === mapUrl(place.path, place.page, embedded)) return "none";
|
|
38
|
+
const query = address.includes("?") ? address.slice(address.indexOf("?")) : "";
|
|
39
|
+
const named = placeFromSearch(query);
|
|
40
|
+
return named.path === place.path && named.page === place.page ? "replace" : "push";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A move the editor asked for with `follow` (the active editor changed) changes the Map's place but must not fill Back
|
|
45
|
+
* with a step per file the person clicked through: a push becomes a rewrite of the current entry. A `show` (a deliberate
|
|
46
|
+
* jump) keeps `mode` "push". Everything else is as `historyStep` said.
|
|
47
|
+
*/
|
|
48
|
+
export function followStep(step: "none" | "replace" | "push", mode: "push" | "replace"): "none" | "replace" | "push" {
|
|
49
|
+
return step === "push" && mode === "replace" ? "replace" : step;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How many of the Map's own steps lie behind a history entry: the `mapDepth` the Map writes into `history.state` with
|
|
54
|
+
* each push (and keeps with each rewrite). VS Code's panel has no browser Back, so the Map's own Back control is enabled
|
|
55
|
+
* only above 0: going back past the Map's first entry would take the webview off the Map (owner review, 2026-10-25).
|
|
56
|
+
* An entry the Map didn't write, a fresh load's or another page's, is 0, and so is anything that isn't a whole number.
|
|
57
|
+
*/
|
|
58
|
+
export function historyDepth(state: unknown): number {
|
|
59
|
+
const depth = typeof state === "object" && state !== null ? (state as { mapDepth?: unknown }).mapDepth : undefined;
|
|
60
|
+
return typeof depth === "number" && Number.isInteger(depth) && depth > 0 ? depth : 0;
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DescribeResponse, MapNode } from "@/lib/contracts";
|
|
2
|
+
import { formatCount } from "./format";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Words for what the Map draws. The owner's three-task test (2026-10-13) found a newcomer reading an arrow backwards
|
|
6
|
+
* ("base depends on editor"), so a line says what it means in a sentence when it's hovered.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** What a box is called inside a sentence: its name, or for a portal the boxes behind it. */
|
|
10
|
+
function subject(box: MapNode): string {
|
|
11
|
+
if (box.kind !== "portal") return box.name;
|
|
12
|
+
return box.pages ? `boxes on ${box.pages.length} other pages` : `boxes on ${box.name}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const capitalized = (text: string): string => text.charAt(0).toUpperCase() + text.slice(1);
|
|
16
|
+
|
|
17
|
+
/** A line's hover label: "editor uses base · 12 imports" (the arrow runs from the user to the used). */
|
|
18
|
+
export function lineLabel(source: MapNode, target: MapNode, count: number): string {
|
|
19
|
+
const imports = `${formatCount(count)} import${count === 1 ? "" : "s"}`;
|
|
20
|
+
const verb = source.kind === "portal" ? "use" : "uses";
|
|
21
|
+
const who = source.kind === "portal" ? capitalized(subject(source)) : subject(source);
|
|
22
|
+
return `${who} ${verb} ${subject(target)} · ${imports}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A box's hover title: its path, and its one-line summary when there is one. */
|
|
26
|
+
export function boxTitle(box: Pick<MapNode, "path" | "name" | "tagline">): string {
|
|
27
|
+
const where = box.path || box.name;
|
|
28
|
+
return box.tagline ? `${where}: ${box.tagline}` : where;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The detail panel's words for a file with no declarations: a file over the size cap had them left unread (ADR-053). */
|
|
32
|
+
export function noDeclarationsText(file: Pick<DescribeResponse, "importsOnly">): string {
|
|
33
|
+
return file.importsOnly ? "Not read (over the 1 MB size cap; imports only)." : "None found.";
|
|
34
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import dagre from "@dagrejs/dagre";
|
|
2
|
+
import type { MapEdge, MapNode } from "@/lib/contracts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every box is drawn the same size, so the layout needs no measuring (VISUAL_REFRESH.md §4). 56 tall, not V1.3.1's
|
|
6
|
+
* first 64, and layers 64 apart, not 72: measured on VS Code's busiest levels, the tighter layout fits them 10–30%
|
|
7
|
+
* larger (src/vs/platform's names from 8.8 to 11.6 px on a 1907 px screen), while GAP stays 40 so the routed lines
|
|
8
|
+
* keep their channels.
|
|
9
|
+
*/
|
|
10
|
+
export const BOX_WIDTH = 232;
|
|
11
|
+
export const BOX_HEIGHT = 56;
|
|
12
|
+
export const GAP = 40;
|
|
13
|
+
/** Room between layers: enough for a line's arrowhead to read before it meets the box below. */
|
|
14
|
+
export const RANK_GAP = 64;
|
|
15
|
+
|
|
16
|
+
export type Position = { x: number; y: number };
|
|
17
|
+
|
|
18
|
+
/** A layer wider than this wraps into rows: 6 boxes span about 1,600 px, a laptop screen at a readable zoom. */
|
|
19
|
+
export const MAX_PER_ROW = 6;
|
|
20
|
+
/** On a wide canvas a layer wraps at 8 instead (about 2,100 px): fewer rows, so a dense level fits larger. */
|
|
21
|
+
export const WIDE_PER_ROW = 8;
|
|
22
|
+
/** A canvas at least this wide counts as wide. Below it, 8 a row would make levels fit smaller, not larger (measured). */
|
|
23
|
+
export const WIDE_CANVAS = 1700;
|
|
24
|
+
|
|
25
|
+
/** How many boxes a layer holds per row on a canvas this wide. */
|
|
26
|
+
export function perRowFor(canvasWidth: number): number {
|
|
27
|
+
return canvasWidth >= WIDE_CANVAS ? WIDE_PER_ROW : MAX_PER_ROW;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Dagre's layers, redrawn as centred tiers (VISUAL_REFRESH.md §4: the level fits the viewport). Dagre decides which
|
|
32
|
+
* layer each box is in and their order within it (the order that crosses fewest lines); its x positions are dropped.
|
|
33
|
+
* Left to them, 49 files that all import one barrel make one layer 13,000 px wide, and fitting that to a screen
|
|
34
|
+
* shrinks every box to a sliver. So each layer is centred on x = 0, and one wider than MAX_PER_ROW wraps into rows,
|
|
35
|
+
* balanced so the last isn't left with one box (13 → 5, 5, 3; at 8 a row, 7 and 6). The tiers stack from y = 0,
|
|
36
|
+
* RANK_GAP apart.
|
|
37
|
+
*/
|
|
38
|
+
function placeTiers(centres: readonly { path: string; x: number; y: number }[], perRowCap: number): Map<string, Position> {
|
|
39
|
+
const layers = new Map<number, { path: string; x: number }[]>();
|
|
40
|
+
for (const { path, x, y } of centres) layers.set(y, [...(layers.get(y) ?? []), { path, x }]);
|
|
41
|
+
|
|
42
|
+
const placed = new Map<string, Position>();
|
|
43
|
+
let top = 0;
|
|
44
|
+
for (const y of [...layers.keys()].sort((a, b) => a - b)){
|
|
45
|
+
const layer = layers.get(y)!.sort((a, b) => a.x - b.x || (a.path < b.path ? -1 : 1));
|
|
46
|
+
const rowCount = Math.ceil(layer.length / perRowCap);
|
|
47
|
+
const perRow = Math.ceil(layer.length / rowCount);
|
|
48
|
+
for (let row = 0; row < rowCount; row++){
|
|
49
|
+
const boxes = layer.slice(row * perRow, (row + 1) * perRow);
|
|
50
|
+
const left = -(boxes.length * BOX_WIDTH + (boxes.length - 1) * GAP) / 2;
|
|
51
|
+
boxes.forEach((box, i) => placed.set(box.path, { x: left + i * (BOX_WIDTH + GAP), y: top + row * (BOX_HEIGHT + GAP) }));
|
|
52
|
+
}
|
|
53
|
+
top += rowCount * (BOX_HEIGHT + GAP) - GAP + RANK_GAP;
|
|
54
|
+
}
|
|
55
|
+
return placed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* At most this many lines shape a level's layout: the heaviest. Dagre's crossing reduction grows with the lines, and
|
|
60
|
+
* VS Code's src/vs/platform level has 390 of them: about 0.6 s to lay out, against 30 ms with the heaviest 100. Every
|
|
61
|
+
* line is still drawn; the lighter ones just don't move the boxes (S3, PIVOT_SPEC.md §2).
|
|
62
|
+
*/
|
|
63
|
+
export const LAYOUT_MAX_LINES = 100;
|
|
64
|
+
|
|
65
|
+
/** The fit's padding (MapCanvas's FIT): React Flow fits a level at most this much smaller than the canvas, each way. */
|
|
66
|
+
export const FIT_PADDING = 0.15;
|
|
67
|
+
/**
|
|
68
|
+
* Room for the divider over the loose boxes ("Not linked to anything on this page", owner's design, 2026-10-13): a grid
|
|
69
|
+
* below the tiers starts this far under them, three gaps instead of two, with the line halfway; a column beside them
|
|
70
|
+
* starts this far down, the line in the band above it.
|
|
71
|
+
*/
|
|
72
|
+
export const BELOW_GAP = 3 * GAP;
|
|
73
|
+
export const DIVIDER_BAND = 64;
|
|
74
|
+
|
|
75
|
+
/** The canvas a level is judged against when choosing where its loose boxes go: a 1440 × 900 screen less the bars. */
|
|
76
|
+
const NOMINAL_CANVAS = { width: 1440, height: 780 };
|
|
77
|
+
|
|
78
|
+
/** The zoom React Flow's fit would pick for a level this big on the nominal canvas: never past 100% (MapCanvas's maxZoom). */
|
|
79
|
+
function fitZoom(width: number, height: number): number {
|
|
80
|
+
return Math.min(1, NOMINAL_CANVAS.width / (width * (1 + FIT_PADDING)), NOMINAL_CANVAS.height / (height * (1 + FIT_PADDING)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A grid of `count` boxes, `columns` wide, from (left, top), in order: the level's order for its loose boxes. */
|
|
84
|
+
function grid(count: number, columns: number, left: number, top: number): Position[] {
|
|
85
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
86
|
+
x: left + (i % columns) * (BOX_WIDTH + GAP),
|
|
87
|
+
y: top + Math.floor(i / columns) * (BOX_HEIGHT + GAP),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** How wide (or tall) `k` boxes side by side (or stacked) are, with the gaps between them. */
|
|
92
|
+
const span = (k: number, size: number): number => k * (size + GAP) - GAP;
|
|
93
|
+
|
|
94
|
+
/** The paths joined by a line to another box at this level; a line from a box to itself joins nothing. */
|
|
95
|
+
function linkedPaths(edges: readonly MapEdge[]): Set<string> {
|
|
96
|
+
return new Set(edges.filter((e) => e.source !== e.target).flatMap((e) => [e.source, e.target]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Where each box of a level goes (ADR-011, PIVOT_SPEC.md §7): dagre's layers, importers above what they import, from
|
|
101
|
+
* the level's `maxLines` heaviest lines, drawn as centred tiers that wrap past `perRow` (placeTiers). A box with no
|
|
102
|
+
* line to any other at this level adds nothing to the layering, so those go in a grid, in the level's order, on the
|
|
103
|
+
* side that lets the whole level fit larger (VISUAL_REFRESH.md §4: readable, fitting the viewport):
|
|
104
|
+
* - beside the tiers, as tall as them: right for a tall chain on a wide screen (VS Code's root: 91% against 68%);
|
|
105
|
+
* - or below them, square-ish and centred: right for a short stack with many loose boxes (2 tiers and 20 loose: 95%
|
|
106
|
+
* against 42%). A tie stays beside.
|
|
107
|
+
* Either way they leave room for the divider that says they're not linked (looseDivider, below).
|
|
108
|
+
* A level with no lines at all is one square-ish grid. Pure and deterministic: the same level always draws the same
|
|
109
|
+
* picture. Positions are top-left corners, as React Flow places nodes.
|
|
110
|
+
*/
|
|
111
|
+
export function layoutLevel(
|
|
112
|
+
nodes: readonly MapNode[],
|
|
113
|
+
edges: readonly MapEdge[],
|
|
114
|
+
maxLines: number = LAYOUT_MAX_LINES,
|
|
115
|
+
perRow: number = MAX_PER_ROW,
|
|
116
|
+
): Map<string, Position> {
|
|
117
|
+
const positions = new Map<string, Position>();
|
|
118
|
+
const lines = edges.filter((e) => e.source !== e.target);
|
|
119
|
+
// a box joined by any line stays in the layers, even when only lighter lines join it
|
|
120
|
+
const linked = linkedPaths(edges);
|
|
121
|
+
const connected = nodes.filter((n) => linked.has(n.path));
|
|
122
|
+
const loose = nodes.filter((n) => !linked.has(n.path));
|
|
123
|
+
// heaviest first; ties by source, then target, so the same level always keeps the same lines
|
|
124
|
+
const shaping = [...lines]
|
|
125
|
+
.sort((a, b) => b.count - a.count || (a.source < b.source ? -1 : a.source > b.source ? 1 : 0) || (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
|
|
126
|
+
.slice(0, maxLines);
|
|
127
|
+
const squareColumns = Math.max(1, Math.ceil(Math.sqrt(loose.length)));
|
|
128
|
+
|
|
129
|
+
if (connected.length === 0){
|
|
130
|
+
grid(loose.length, squareColumns, 0, 0).forEach((at, i) => positions.set(loose[i].path, at));
|
|
131
|
+
return positions;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const graph = new dagre.graphlib.Graph();
|
|
135
|
+
graph.setGraph({ rankdir: "TB", nodesep: GAP, ranksep: RANK_GAP, marginx: 0, marginy: 0 });
|
|
136
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
137
|
+
for (const node of connected) graph.setNode(node.path, { width: BOX_WIDTH, height: BOX_HEIGHT });
|
|
138
|
+
// a heavier line pulls its boxes closer; the square root keeps 20,000 imports from flattening everything else
|
|
139
|
+
for (const edge of shaping) graph.setEdge(edge.source, edge.target, { weight: Math.max(1, Math.round(Math.sqrt(edge.count))) });
|
|
140
|
+
dagre.layout(graph);
|
|
141
|
+
|
|
142
|
+
// the tiers are centred on x = 0 and start at y = 0
|
|
143
|
+
let bottom = 0;
|
|
144
|
+
let right = 0;
|
|
145
|
+
for (const [path, at] of placeTiers(connected.map((node) => ({ path: node.path, ...(graph.node(node.path) as { x: number; y: number }) })), perRow)){
|
|
146
|
+
positions.set(path, at);
|
|
147
|
+
bottom = Math.max(bottom, at.y + BOX_HEIGHT);
|
|
148
|
+
right = Math.max(right, at.x + BOX_WIDTH);
|
|
149
|
+
}
|
|
150
|
+
if (loose.length === 0) return positions;
|
|
151
|
+
|
|
152
|
+
// beside: as many rows as the tiers are tall, starting two gaps right of them and a band down, under the divider
|
|
153
|
+
const besideRows = Math.max(1, Math.floor((bottom + GAP) / (BOX_HEIGHT + GAP)));
|
|
154
|
+
const besideColumns = Math.ceil(loose.length / besideRows);
|
|
155
|
+
const besideWidth = right + 2 * GAP + span(besideColumns, BOX_WIDTH) + right; // the tiers span -right … right
|
|
156
|
+
const besideHeight = Math.max(bottom, DIVIDER_BAND + span(Math.ceil(loose.length / besideColumns), BOX_HEIGHT));
|
|
157
|
+
// below: square-ish, centred, starting BELOW_GAP under the tiers, the divider halfway
|
|
158
|
+
const belowColumns = Math.min(squareColumns, loose.length);
|
|
159
|
+
const belowWidth = Math.max(2 * right, span(belowColumns, BOX_WIDTH));
|
|
160
|
+
const belowHeight = bottom + BELOW_GAP + span(Math.ceil(loose.length / belowColumns), BOX_HEIGHT);
|
|
161
|
+
|
|
162
|
+
const cells = fitZoom(belowWidth, belowHeight) > fitZoom(besideWidth, besideHeight)
|
|
163
|
+
? grid(loose.length, belowColumns, -span(belowColumns, BOX_WIDTH) / 2, bottom + BELOW_GAP)
|
|
164
|
+
: grid(loose.length, besideColumns, right + 2 * GAP, DIVIDER_BAND);
|
|
165
|
+
cells.forEach((at, i) => positions.set(loose[i].path, at));
|
|
166
|
+
return positions;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Where the divider over a level's loose boxes runs: its left end, its centre line, its length. */
|
|
170
|
+
export type Divider = { x: number; y: number; width: number };
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The line with "Not linked to anything on this page" in its middle, between the boxes joined by lines and the loose
|
|
174
|
+
* ones (owner's design, 2026-10-13: without it the loose grid read as randomly placed cards). Below the tiers it runs
|
|
175
|
+
* the whole level's width, halfway down BELOW_GAP; over a column beside them it's as wide as the column, centred in
|
|
176
|
+
* DIVIDER_BAND. No divider when every box is linked or none is: there's nothing to set apart.
|
|
177
|
+
*/
|
|
178
|
+
export function looseDivider(nodes: readonly MapNode[], edges: readonly MapEdge[], positions: ReadonlyMap<string, Position>): Divider | null {
|
|
179
|
+
const linked = linkedPaths(edges);
|
|
180
|
+
const boundsOf = (paths: readonly string[]) => {
|
|
181
|
+
const at = paths.map((p) => positions.get(p)).filter((q): q is Position => q !== undefined);
|
|
182
|
+
return {
|
|
183
|
+
left: Math.min(...at.map((q) => q.x)),
|
|
184
|
+
right: Math.max(...at.map((q) => q.x + BOX_WIDTH)),
|
|
185
|
+
top: Math.min(...at.map((q) => q.y)),
|
|
186
|
+
bottom: Math.max(...at.map((q) => q.y + BOX_HEIGHT)),
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
const joined = nodes.filter((n) => linked.has(n.path)).map((n) => n.path);
|
|
190
|
+
const loose = nodes.filter((n) => !linked.has(n.path)).map((n) => n.path);
|
|
191
|
+
if (joined.length === 0 || loose.length === 0) return null;
|
|
192
|
+
|
|
193
|
+
const tiers = boundsOf(joined);
|
|
194
|
+
const grid = boundsOf(loose);
|
|
195
|
+
// beside: the grid starts right of the tiers (layoutLevel puts it two gaps past them); below, it's centred under them
|
|
196
|
+
if (grid.left >= tiers.right) return { x: grid.left, y: grid.top - DIVIDER_BAND / 2, width: grid.right - grid.left };
|
|
197
|
+
const left = Math.min(tiers.left, grid.left);
|
|
198
|
+
return { x: left, y: (tiers.bottom + grid.top) / 2, width: Math.max(tiers.right, grid.right) - left };
|
|
199
|
+
}
|
package/app/map/links.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GitHubSource } from "./api";
|
|
2
|
+
|
|
3
|
+
export type LinkTarget = { repo: string; path: string };
|
|
4
|
+
|
|
5
|
+
/** A file in the user's editor, from the app (PIVOT_SPEC.md §7): needs the repo's absolute root, so never in the demo. */
|
|
6
|
+
export function editorLink(target: LinkTarget, line?: number): string {
|
|
7
|
+
return `vscode://file/${target.repo}/${target.path}${line ? `:${line}` : ""}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A file on GitHub at the commit the demo's index was built from, at a declaration's lines (ADR-049). */
|
|
11
|
+
export function githubLink(source: GitHubSource, filePath: string, lines?: { start: number; end: number }): string {
|
|
12
|
+
const anchor = lines === undefined ? "" : lines.start === lines.end ? `#L${lines.start}` : `#L${lines.start}-L${lines.end}`;
|
|
13
|
+
return `https://github.com/${source.owner}/${source.name}/blob/${source.commit}/${filePath.split("/").map(encodeURIComponent).join("/")}${anchor}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps one live stream open while the page is on screen (PIVOT_SPEC.md §8).
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: when you leave a page, Chrome may keep it frozen in its back/forward cache so Back is instant, and a
|
|
5
|
+
* frozen page's EventSource stays connected. Over plain HTTP/1.1 a browser allows only 6 connections to one server, so
|
|
6
|
+
* after a handful of full page loads in one tab the frozen pages' streams held them all, and the next page's level,
|
|
7
|
+
* status and repo requests waited forever (measured 2026-10-13: the 7th load stalled with the cache on, never with it
|
|
8
|
+
* off). So the stream closes on `pagehide` and a new one opens on `pageshow` when the page comes back from the cache.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** What a stream must offer: a way to close it. EventSource has it; tests pass a stand-in. */
|
|
12
|
+
export type Closable = { close(): void };
|
|
13
|
+
|
|
14
|
+
/** Where pagehide and pageshow fire: the window, or a stand-in EventTarget in tests. */
|
|
15
|
+
type PageEvents = Pick<EventTarget, "addEventListener" | "removeEventListener">;
|
|
16
|
+
|
|
17
|
+
/** Opens a stream now and keeps one open while the page shows; returns a function that closes it and stops listening. */
|
|
18
|
+
export function keepLiveStream(open: () => Closable, page: PageEvents): () => void {
|
|
19
|
+
let stream: Closable | null = open();
|
|
20
|
+
|
|
21
|
+
const onHide = (): void => {
|
|
22
|
+
stream?.close();
|
|
23
|
+
stream = null;
|
|
24
|
+
};
|
|
25
|
+
// pageshow fires on every load too; only a page restored from the cache (persisted) needs its stream back
|
|
26
|
+
const onShow = (event: Event): void => {
|
|
27
|
+
if ((event as PageTransitionEvent).persisted && stream === null) stream = open();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
page.addEventListener("pagehide", onHide);
|
|
31
|
+
page.addEventListener("pageshow", onShow);
|
|
32
|
+
return () => {
|
|
33
|
+
page.removeEventListener("pagehide", onHide);
|
|
34
|
+
page.removeEventListener("pageshow", onShow);
|
|
35
|
+
onHide();
|
|
36
|
+
};
|
|
37
|
+
}
|
package/app/map/mode.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True in the static demo's build (ADR-049). `npm run build-demo` builds with NEBULOSITY_DEMO=1, which next.config.ts
|
|
3
|
+
* turns into NEXT_PUBLIC_NEBULOSITY_DEMO ("1" in the demo, "0" in the app) for the browser; Next inlines it at build
|
|
4
|
+
* time. For the components. The transport's own switch is written inline in api.ts, so the bundler can drop the demo's
|
|
5
|
+
* code from the app.
|
|
6
|
+
*/
|
|
7
|
+
export const DEMO = process.env.NEXT_PUBLIC_NEBULOSITY_DEMO === "1";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Map at any window size (VISUAL_REFRESH.md, "Narrow windows (2026-10-24)"). At 1100 × 700 and up nothing here
|
|
3
|
+
* changes how the Map looks. Narrower, each surface gives up secondary text first; below the floor the Map stops
|
|
4
|
+
* shrinking and the page scrolls instead, with the theme's thin scrollbars. Pure, so each rule is tested on its own.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TOP_INSET } from "./viewport";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The floor: the smallest the Map lays itself out at. Narrower or shorter, the page scrolls sideways or down.
|
|
11
|
+
* 360 px wide holds the top bar's essentials (logo, repo, search, help, status dot) and a canvas as wide as the floor
|
|
12
|
+
* beside nothing. 320 px tall leaves a 200 px canvas under the bars (48 + 40 + 32 px), which stacks a paged level's
|
|
13
|
+
* pill (16 + 44 px from the top) over the zoom controls (104 px, 15 px from the bottom) with about 20 px between them.
|
|
14
|
+
*/
|
|
15
|
+
export const MIN_WIDTH = 360;
|
|
16
|
+
export const MIN_HEIGHT = 320;
|
|
17
|
+
|
|
18
|
+
/** The details panel's width, when there's room beside it (VISUAL_REFRESH.md §5). */
|
|
19
|
+
export const PANEL_WIDTH = 400;
|
|
20
|
+
/**
|
|
21
|
+
* The least canvas worth keeping beside the panel: the floor's own width. With less left, the panel covers the whole
|
|
22
|
+
* canvas (its × closes it), since a sliver of Map beside it can't be read or used.
|
|
23
|
+
*/
|
|
24
|
+
export const MIN_CANVAS_BESIDE_PANEL = MIN_WIDTH;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the panel covers the whole canvas in a Map area `mainWidth` px wide; beside it, it's PANEL_WIDTH wide.
|
|
28
|
+
* `null`, before the area is measured, is the normal case: beside the canvas.
|
|
29
|
+
*/
|
|
30
|
+
export function panelLayout(mainWidth: number | null): { covers: boolean } {
|
|
31
|
+
return { covers: mainWidth !== null && mainWidth - PANEL_WIDTH < MIN_CANVAS_BESIDE_PANEL };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where a selected box's actions go while the panel would cover the canvas (owner review, 2026-10-25). A click then only
|
|
36
|
+
* selects, so a double click still reaches the box and the Map stays in sight, and the box shows Open and Details beside
|
|
37
|
+
* it. They go under a box whose middle is in the canvas's top half, clear of the page pill's band, and over one lower
|
|
38
|
+
* down, clear of the legend and the zoom controls. `boxTop` and `boxHeight` are the box's on screen, in canvas px.
|
|
39
|
+
*/
|
|
40
|
+
export function actionsSide(boxTop: number, boxHeight: number, canvasHeight: number): "top" | "bottom" {
|
|
41
|
+
return boxTop + boxHeight / 2 < canvasHeight / 2 ? "bottom" : "top";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The gap between a box and its actions, and the least room they keep from the canvas's sides. */
|
|
45
|
+
const ACTIONS_GAP = 8;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where the selected box's actions go on the canvas (their top-left corner, canvas px), given the box and the actions'
|
|
49
|
+
* size on screen: on actionsSide's side, 8 px off the box, centred on it, but kept 8 px inside the canvas's sides, so a
|
|
50
|
+
* box hanging off an edge (a level opened from its top at 75%) never takes them past it. Each axis is its own rule.
|
|
51
|
+
*/
|
|
52
|
+
export function actionsPlace(
|
|
53
|
+
box: { left: number; top: number; width: number; height: number },
|
|
54
|
+
actions: { width: number; height: number },
|
|
55
|
+
canvasWidth: number,
|
|
56
|
+
canvasHeight: number,
|
|
57
|
+
): { left: number; top: number } {
|
|
58
|
+
const top = actionsSide(box.top, box.height, canvasHeight) === "bottom" ? box.top + box.height + ACTIONS_GAP : box.top - ACTIONS_GAP - actions.height;
|
|
59
|
+
const centred = box.left + box.width / 2 - actions.width / 2;
|
|
60
|
+
const left = Math.min(Math.max(centred, ACTIONS_GAP), canvasWidth - ACTIONS_GAP - actions.width);
|
|
61
|
+
return { left, top };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How much the page pill says, by the canvas width it sits in (`null`: not measured yet, the normal case):
|
|
66
|
+
* "Page 2 of 5 · 44 of 182 folders and files", "Page 2 of 5", or "2 / 5". The whole note is about 350 px, 370 at
|
|
67
|
+
* most with four-digit counts; with its 16 px inset on the right and as much again on its left that's about 400 px,
|
|
68
|
+
* and 440 leaves room for a wider font.
|
|
69
|
+
*/
|
|
70
|
+
export type PillForm = "full" | "page" | "numbers";
|
|
71
|
+
const FULL_PILL_ROOM = 440;
|
|
72
|
+
const PAGE_PILL_ROOM = 240;
|
|
73
|
+
|
|
74
|
+
export function pillForm(room: number | null): PillForm {
|
|
75
|
+
if (room === null || room >= FULL_PILL_ROOM) return "full";
|
|
76
|
+
return room >= PAGE_PILL_ROOM ? "page" : "numbers";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The first breadcrumb step to show so the trail fits in `available` px: the steps before it fold into one "…" step
|
|
81
|
+
* (`ellipsis` px), so the trail truncates from its start and the current folder stays. `widths` are the steps' natural
|
|
82
|
+
* widths, `gap` the space between steps. The last step always shows, and truncates itself if it alone is too wide.
|
|
83
|
+
*/
|
|
84
|
+
export function trailStart(widths: readonly number[], available: number, ellipsis: number, gap: number): number {
|
|
85
|
+
const last = widths.length - 1;
|
|
86
|
+
if (last <= 0) return 0;
|
|
87
|
+
let needed = widths.reduce((sum, w) => sum + w, 0) + gap * last;
|
|
88
|
+
if (needed <= available) return 0;
|
|
89
|
+
// drop steps from the start, one at a time; the "…" stands in for all of them
|
|
90
|
+
needed += ellipsis + gap;
|
|
91
|
+
for (let start = 1; start < last; start++){
|
|
92
|
+
needed -= widths[start - 1] + gap;
|
|
93
|
+
if (needed <= available) return start;
|
|
94
|
+
}
|
|
95
|
+
return last;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The space a popover keeps from the Map's edges. */
|
|
99
|
+
const POPOVER_GUTTER = 16;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A popover (the repo list, the status menu) placed to stay inside the Map: aligned with its anchor's start or end
|
|
103
|
+
* edge as at normal sizes, moved in only as far as it takes to keep 16 px from the Map's edges, and narrowed only when
|
|
104
|
+
* the Map is narrower than the popover and both margins. `shift` is how far it moved from that alignment (px, + is
|
|
105
|
+
* right): a start-aligned popover's `left`, an end-aligned one's `right` negated.
|
|
106
|
+
*/
|
|
107
|
+
export function fitPopover(
|
|
108
|
+
anchor: { left: number; right: number },
|
|
109
|
+
container: { left: number; right: number },
|
|
110
|
+
width: number,
|
|
111
|
+
align: "start" | "end",
|
|
112
|
+
): { shift: number; width: number } {
|
|
113
|
+
const fitted = Math.min(width, container.right - container.left - 2 * POPOVER_GUTTER);
|
|
114
|
+
const natural = align === "start" ? anchor.left : anchor.right - fitted;
|
|
115
|
+
const left = Math.min(Math.max(natural, container.left + POPOVER_GUTTER), container.right - POPOVER_GUTTER - fitted);
|
|
116
|
+
return { shift: left - natural, width: fitted };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* The Map's error line (a failed jump, a level that wouldn't load), over the canvas beside the panel. Its place at
|
|
121
|
+
* normal sizes is the one it always had: 24 px down, in the page pill's band, centred, now over the canvas you can
|
|
122
|
+
* see rather than under an open panel, and keeping clear of the pill. Where that band can't hold it, it's a solid card
|
|
123
|
+
* in the canvas's top-left corner instead, clear of the pill and the zoom controls. Over a panel that covers the
|
|
124
|
+
* canvas it's the panel's own last row (DetailPanel). It never takes a click.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/** The overlays' inset from the canvas's edges (the pill's right-4, the legend's left-4 and bottom-4). */
|
|
128
|
+
const OVERLAY_INSET = 16;
|
|
129
|
+
/** The gap the banner keeps from the pill and the legend. */
|
|
130
|
+
const OVERLAY_GAP = 8;
|
|
131
|
+
/** React Flow's zoom controls: 26 px wide, 15 px in from the canvas's right edge. */
|
|
132
|
+
const ZOOM_COLUMN = 15 + 26;
|
|
133
|
+
/** The banner's distance from the canvas's top: as a line in the pill's band (top-6), and as a card (top-4). */
|
|
134
|
+
export const BANNER_TOP = 24;
|
|
135
|
+
export const CARD_TOP = OVERLAY_INSET;
|
|
136
|
+
|
|
137
|
+
export type BannerForm = "band" | "card";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Which form the banner takes, by the height it has as a line in the band (`null`: not measured yet, the normal case):
|
|
141
|
+
* the band while it ends inside the pill's 64 px band, where a paged level's boxes don't open (two lines), else the card.
|
|
142
|
+
*/
|
|
143
|
+
export function bannerForm(bandHeight: number | null): BannerForm {
|
|
144
|
+
return bandHeight === null || BANNER_TOP + bandHeight <= TOP_INSET ? "band" : "card";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The room the line keeps on its right for the pill, past the band's own 16 px padding: the pill's width and a gap,
|
|
149
|
+
* and as much on its left, so it stays centred over the canvas while it can (`null`: no pill, or not measured yet).
|
|
150
|
+
*/
|
|
151
|
+
export function bandClear(pillWidth: number | null): number {
|
|
152
|
+
return pillWidth === null ? 0 : pillWidth + OVERLAY_GAP;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** How far the card's right edge keeps from the canvas's right edge: past the pill, and always past the zoom controls. */
|
|
156
|
+
export function cardClear(pillWidth: number | null): number {
|
|
157
|
+
return Math.max(ZOOM_COLUMN + OVERLAY_GAP, pillWidth === null ? 0 : OVERLAY_INSET + pillWidth + OVERLAY_GAP);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The open legend's greatest height on a canvas `canvasHeight` px tall (`0`: not measured yet, then no cap). It sits
|
|
162
|
+
* 16 px from the bottom and stops short of what's above it: 16 px from the top, a paged level's pill band, or the
|
|
163
|
+
* banner's bottom (`bannerBottom`, 0 with none) and a gap. Its rows scroll inside.
|
|
164
|
+
*/
|
|
165
|
+
export function legendMaxHeight(canvasHeight: number, pillBand: number, bannerBottom: number): number | undefined {
|
|
166
|
+
if (canvasHeight <= 0) return undefined;
|
|
167
|
+
return canvasHeight - OVERLAY_INSET - Math.max(OVERLAY_INSET, pillBand, bannerBottom > 0 ? bannerBottom + OVERLAY_GAP : 0);
|
|
168
|
+
}
|
package/app/map/paths.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LevelResponse } from "@/lib/contracts";
|
|
2
|
+
|
|
3
|
+
// The UI imports from lib/ only types and the few pure, browser-safe modules ARCHITECTURE.md §3 names, and
|
|
4
|
+
// lib/index/paths.ts isn't one of them, so these few lines mirror it.
|
|
5
|
+
|
|
6
|
+
/** The folder that holds a path ("" for something at the root). */
|
|
7
|
+
export function parentOf(targetPath: string): string {
|
|
8
|
+
const slash = targetPath.lastIndexOf("/");
|
|
9
|
+
return slash === -1 ? "" : targetPath.slice(0, slash);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** What a path is called on screen: its last segment, or the repo's folder name for the root. */
|
|
13
|
+
export function nameOf(targetPath: string, repoRoot: string): string {
|
|
14
|
+
if (targetPath === "") return repoRoot.slice(repoRoot.lastIndexOf("/") + 1) || repoRoot;
|
|
15
|
+
return targetPath.slice(targetPath.lastIndexOf("/") + 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A path's last two segments, "src/vs/platform/files/common" → "files/common": enough to tell areas apart in a chip. */
|
|
19
|
+
export function shortPath(targetPath: string): string {
|
|
20
|
+
return targetPath.split("/").slice(-2).join("/");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The extensions of each language the index reads, named as GET /api/source and highlight.js name them (E4, ADR-065).
|
|
25
|
+
* The UI can't import lib/index/languages/rules.ts (ARCHITECTURE.md §3 rule 3), so this mirrors it, one row per
|
|
26
|
+
* language; syntax.test.ts holds the two equal for every extension the rules claim.
|
|
27
|
+
*/
|
|
28
|
+
const LANGUAGES: readonly (readonly [extension: RegExp, language: string])[] = [
|
|
29
|
+
[/\.(ts|tsx|mts|cts)$/, "typescript"],
|
|
30
|
+
[/\.(js|jsx|mjs|cjs)$/, "javascript"],
|
|
31
|
+
[/\.pyi?$/, "python"],
|
|
32
|
+
[/\.go$/, "go"],
|
|
33
|
+
[/\.java$/, "java"],
|
|
34
|
+
[/\.cs$/, "csharp"],
|
|
35
|
+
[/\.rs$/, "rust"],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** The language a path's extension implies, the way GET /api/source names it (the UI can't import lib's copy). */
|
|
39
|
+
export function languageFor(filePath: string): string {
|
|
40
|
+
return LANGUAGES.find(([extension]) => extension.test(filePath))?.[1] ?? "text";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether `targetPath` is strictly inside `folder` ("" holds everything but itself). */
|
|
44
|
+
export function isInside(targetPath: string, folder: string): boolean {
|
|
45
|
+
return folder === "" ? targetPath !== "" : targetPath.startsWith(`${folder}/`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The box on this level that holds `targetPath`: the child on the way down to it, or, when that child is on another
|
|
50
|
+
* page, the portal standing for that page. Null when the path isn't inside this level, is the level itself, or is on a
|
|
51
|
+
* page this one has no portal to. Live pulses and highlights use it.
|
|
52
|
+
*/
|
|
53
|
+
export function boxFor(level: Pick<LevelResponse, "path" | "nodes">, targetPath: string): string | null {
|
|
54
|
+
if (!isInside(targetPath, level.path)) return null;
|
|
55
|
+
const below = level.path === "" ? targetPath : targetPath.slice(level.path.length + 1);
|
|
56
|
+
const slash = below.indexOf("/");
|
|
57
|
+
const child = slash === -1 ? targetPath : `${level.path === "" ? "" : `${level.path}/`}${below.slice(0, slash)}`;
|
|
58
|
+
if (level.nodes.some((n) => n.path === child)) return child;
|
|
59
|
+
return level.nodes.find((n) => n.kind === "portal" && n.members?.includes(child))?.path ?? null;
|
|
60
|
+
}
|
package/app/map/perf.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction timings for S3 (PIVOT_SPEC.md §2: click, zoom or level change → rendered ≤ 100 ms). A start mark goes
|
|
3
|
+
* down when the interaction begins, and the measure is taken after the frame that painted its result. Task 22 reads
|
|
4
|
+
* them from window.__nebulosityPerf.
|
|
5
|
+
*/
|
|
6
|
+
type PerfWindow = Window & { __nebulosityPerf?: { name: string; ms: number }[] };
|
|
7
|
+
|
|
8
|
+
export function markStart(name: string): void {
|
|
9
|
+
performance.clearMarks(`${name}:start`);
|
|
10
|
+
performance.mark(`${name}:start`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Drops a start mark nothing will measure: a jump that ended without a new level (the editor's quiet jump to a path the
|
|
15
|
+
* index doesn't hold, or a failed one). Left down, the next level drawn without a mark of its own (an index update's
|
|
16
|
+
* redraw) would be timed from it, as one long change.
|
|
17
|
+
*/
|
|
18
|
+
export function clearStart(name: string): void {
|
|
19
|
+
performance.clearMarks(`${name}:start`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function measureAfterPaint(name: string): void {
|
|
23
|
+
// the first frame callback runs before the paint that shows the change; the second runs after it
|
|
24
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
25
|
+
if (performance.getEntriesByName(`${name}:start`, "mark").length === 0) return;
|
|
26
|
+
const measure = performance.measure(name, `${name}:start`);
|
|
27
|
+
performance.clearMarks(`${name}:start`);
|
|
28
|
+
const w = window as PerfWindow;
|
|
29
|
+
(w.__nebulosityPerf ??= []).push({ name, ms: Math.round(measure.duration * 10) / 10 });
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, type CSSProperties } from "react";
|
|
2
|
+
import { fitPopover } from "./narrow";
|
|
3
|
+
|
|
4
|
+
/** How far below its anchor's top a popover opens (top-9), and the room it keeps from the Map's bottom edge. */
|
|
5
|
+
const POPOVER_TOP = 36;
|
|
6
|
+
const POPOVER_BOTTOM_ROOM = 8;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Where a popover opened from `anchor` goes, read from the page as it opens (narrow.ts's fitPopover): aligned with the
|
|
10
|
+
* anchor's start or end edge, kept 16 px inside the Map's sides, and no taller than the Map leaves below it, above the
|
|
11
|
+
* activity bar, which is drawn over it (it then scrolls). At normal sizes that's exactly where it always opened. The
|
|
12
|
+
* Map's box is the top bar's parent, MapView's column, which spans the Map down to the floor.
|
|
13
|
+
*/
|
|
14
|
+
export function placePopover(anchor: HTMLElement, width: number, align: "start" | "end"): CSSProperties {
|
|
15
|
+
const column = anchor.closest("header")?.parentElement ?? document.documentElement;
|
|
16
|
+
const map = column.getBoundingClientRect();
|
|
17
|
+
const bottom = column.querySelector(":scope > footer")?.getBoundingClientRect().top ?? map.bottom;
|
|
18
|
+
const box = anchor.getBoundingClientRect();
|
|
19
|
+
const fitted = fitPopover(box, map, width, align);
|
|
20
|
+
const maxHeight = bottom - box.top - POPOVER_TOP - POPOVER_BOTTOM_ROOM;
|
|
21
|
+
return align === "start"
|
|
22
|
+
? { left: fitted.shift, width: fitted.width, maxHeight }
|
|
23
|
+
: { right: -fitted.shift, width: fitted.width, maxHeight };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Places an open popover again whenever the window resizes: VS Code's panel can be dragged narrower with a popover
|
|
28
|
+
* open, and one placed for the old width would run past the Map's edge, or keep a height cap it no longer needs.
|
|
29
|
+
*/
|
|
30
|
+
export function usePlaceOnResize(open: boolean, place: () => void): void {
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open) return;
|
|
33
|
+
window.addEventListener("resize", place);
|
|
34
|
+
return () => window.removeEventListener("resize", place);
|
|
35
|
+
}, [open, place]);
|
|
36
|
+
}
|