feat(ui): implement Map grouping, dependents, and weight bars; symbol tab address

Adds a new grouping system for the Map with a new grouping depth control, exposes per-module dependents (files and modules) to drive a weight bar, and renders it on each module. Introduces a MapKey to explain visuals, collapses lone root-file buckets for clearer labeling, and supports a nullable depth value to let the provider pick grouping. The Symbol tab now has its own address (#/s) when nothing is selected, and routing/top-bar logic is updated accordingly. Also updates export SVG rendering to include weight-based bars, and extends tests and docs to cover the new visuals and behavior.
This commit is contained in:
Colby McHenry
2026-08-31 23:49:39 -05:00
parent b9ca4b7981
commit 7ec9ef1818
21 changed files with 905 additions and 41 deletions
+32
View File
@@ -139,6 +139,7 @@ function mod(id: string, over: Partial<WireMapModule> = {}): WireMapModule {
test: over.test ?? false,
facade: over.facade ?? false,
fileList: over.fileList ?? { total: 3, shown: 3, truncated: false, items: [] },
dependents: over.dependents ?? { files: 0, modules: 0 },
};
}
@@ -449,6 +450,37 @@ describe('mapSvg', () => {
expect(svg).not.toContain('>__tests__</text>');
});
it('exports the weight bar the canvas draws, scaled the same way', () => {
const weighted = buildMapLayout(
{
modules: [
mod('src/types', { dependents: { files: 80, modules: 4 } }),
mod('src/db', { dependents: { files: 20, modules: 2 } }),
mod('src/bin'),
],
links: [link('src/db', 'src/types', 30), link('src/bin', 'src/db', 30)],
},
{ includeTests: false }
);
const svg = mapSvg(weighted);
const nodeOf = (id: string) => weighted.nodes.find((n) => n.id === id)!;
// Full bar for the heaviest, a quarter for the module a quarter as leaned
// on, and NO rect at all for the one nothing depends on.
// The export rounds to a tenth, as every coordinate in this file does.
const tenth = (n: number) => Math.round(n * 10) / 10;
const full = nodeOf('src/types');
const quarter = nodeOf('src/db');
expect(quarter.weight).toBeCloseTo(0.25, 5);
expect(svg).toContain(`width="${tenth(full.width)}" height="4" fill="${EXPORT_COLORS.ink}"`);
expect(svg).toContain(
`width="${tenth(quarter.width * 0.25)}" height="4" fill="${EXPORT_COLORS.ink}"`
);
expect(nodeOf('src/bin').weight).toBe(0);
expect(svg.match(/height="4" fill=/g)?.length).toBe(2);
// …and the count rides in the meta line, as on screen.
expect(svg).toContain('· 80 depend on it</text>');
});
it('names the top and bottom bands', () => {
const svg = mapSvg(layout);
expect(svg).toContain('>entry points</text>');