The core screen of `codegraph ui`: who calls a symbol on the left, its verbatim body in the middle with a port on every line that has an outgoing edge, and what it calls on the right — each callee row placed beside the line that makes the call, with a hairline connector between them. The callee rail is the part that is not a list. A row wants to sit at the centre of its first call-site line and is pushed down only when that would collide with the row above, so the rail keeps source order; the connector still runs to the real line, so the displacement is visible rather than silent. Positions come from measuring the laid-out DOM, so they are recomputed on resize, on font load and whenever a fold opens. Honesty is carried in the drawing, not in a footnote: a filled port means the resolver matched something on that line and a hollow one means it only guessed; uncertain connectors are dashed and their targets fold away behind their count; synthesized edges are dashed differently and tagged with the mechanism that made them; references that leave the index are text with a soft underline rather than links to nowhere, and they are counted. Long bodies keep their head plus a window round every call site — windowed on graph edges only, since a function calling `console.log` two hundred times would otherwise window round every line and buy nothing. Containers over 80 lines show a members outline with per-member fan-in/fan-out instead of 700 lines of braces. Two small additions to the read-only API this needed: * `/api/node` gives every outline member its own fanIn/fanOut (two batched queries for the whole outline). A class's own fan-out is nearly always zero because its methods do the calling, so without these the outline cannot say which member carries weight. * `/api/stats` gains `blastScale` — the denominator the blast bar is drawn against, so one symbol's radius reads as wide or narrow *for this repo*. It is measured across the index's 24 most-depended-on symbols (found with a new `getTopDependedOn`, distinct dependents rather than edges), memoised against the index stamp, and reported as sampled; a symbol wider than the sample becomes the scale instead of overflowing the track. Verified against a real index in a real browser: parity with the prototype on `CodeGraph.sync` (259 lines, 27 callee rows, no overlaps), `GraphTraverser` (20-member outline), a 773-line function (26 windows, 78 connectors), light and dark, hover linking in both directions, keyboard-only navigation, and reflow on resize and on fold toggles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
3.4 KiB
Svelte
115 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from 'svelte';
|
|
import TopBar from './components/TopBar.svelte';
|
|
import TrailBar from './components/TrailBar.svelte';
|
|
import HomeView from './views/HomeView.svelte';
|
|
import SymbolView from './views/SymbolView.svelte';
|
|
import FileView from './views/FileView.svelte';
|
|
import MapView from './views/MapView.svelte';
|
|
import FlowView from './views/FlowView.svelte';
|
|
import NotFoundView from './views/NotFoundView.svelte';
|
|
import { router, navigate, back, mapHref, flowHref } from './lib/router.svelte';
|
|
import { trail } from './lib/trail.svelte';
|
|
import { project } from './lib/project.svelte';
|
|
|
|
let query = $state('');
|
|
|
|
// One `/api/stats` for the whole app: the top bar's counts and the Symbol
|
|
// view's blast-radius denominator come out of the same payload.
|
|
$effect(() => {
|
|
void project.ensure();
|
|
});
|
|
|
|
let topbar: TopBar | null = $state(null);
|
|
|
|
let route = $derived(router.route);
|
|
|
|
// Keep the in-memory trail and the `t` param in step. untrack() because the
|
|
// body writes the same store it would otherwise read itself into a loop.
|
|
$effect(() => {
|
|
const current = router.route;
|
|
const encoded = router.params.get('t');
|
|
untrack(() => {
|
|
trail.hydrate(encoded);
|
|
if (current.view === 'symbol' && trail.current?.id !== current.id) {
|
|
trail.push({ id: current.id });
|
|
}
|
|
});
|
|
});
|
|
|
|
function isTypingTarget(target: EventTarget | null): boolean {
|
|
if (!(target instanceof HTMLElement)) return false;
|
|
return (
|
|
target.isContentEditable ||
|
|
target instanceof HTMLInputElement ||
|
|
target instanceof HTMLTextAreaElement ||
|
|
target instanceof HTMLSelectElement
|
|
);
|
|
}
|
|
|
|
function onkeydown(event: KeyboardEvent) {
|
|
if (event.defaultPrevented) return;
|
|
|
|
// Cmd/Ctrl+K reaches the search box even from inside another field.
|
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
event.preventDefault();
|
|
topbar?.focusSearch();
|
|
return;
|
|
}
|
|
|
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
if (isTypingTarget(event.target)) return;
|
|
|
|
switch (event.key) {
|
|
case '/':
|
|
event.preventDefault();
|
|
topbar?.focusSearch();
|
|
break;
|
|
case 'm':
|
|
event.preventDefault();
|
|
navigate(mapHref());
|
|
break;
|
|
case 'f':
|
|
event.preventDefault();
|
|
navigate(flowHref());
|
|
break;
|
|
case 'Backspace':
|
|
case '[':
|
|
event.preventDefault();
|
|
back();
|
|
break;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window {onkeydown} />
|
|
|
|
<TopBar bind:this={topbar} bind:query project={project.name} stats={project.summary} />
|
|
<TrailBar />
|
|
<main>
|
|
{#if route.view === 'symbol'}
|
|
<SymbolView id={route.id} line={route.line} />
|
|
{:else if route.view === 'file'}
|
|
<FileView path={route.path} line={route.line} />
|
|
{:else if route.view === 'map'}
|
|
<MapView />
|
|
{:else if route.view === 'flow'}
|
|
<FlowView flowKey={route.key} />
|
|
{:else if route.view === 'unknown'}
|
|
<NotFoundView path={route.path} />
|
|
{:else}
|
|
<HomeView project={project.name} />
|
|
{/if}
|
|
</main>
|
|
|
|
<style>
|
|
/* The shell grid lives on #app (index.html's mount host) in app.css —
|
|
Svelte's scoped styles cannot reach an element this component does not
|
|
render. Only <main>, which it does render, is styled here. */
|
|
main {
|
|
/* min-height:0 lets the row shrink so the view, not the page, scrolls. */
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|