@psnext/lscg 0.1.2 → 0.1.3
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/README.md +69 -3
- package/dist/src/cli.js +68 -13
- package/dist/src/graph/repository.d.ts +4 -1
- package/dist/src/graph/repository.js +106 -6
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +2 -0
- package/dist/src/mcp/server.js +2 -2
- package/dist/src/scanner/discover.js +0 -2
- package/dist/src/scanner/packagePlugin.d.ts +3 -0
- package/dist/src/scanner/packagePlugin.js +125 -0
- package/dist/src/scanner/plugins.d.ts +86 -0
- package/dist/src/scanner/plugins.js +503 -0
- package/dist/src/storage/connection.d.ts +6 -0
- package/dist/src/storage/connection.js +133 -0
- package/dist/src/storage/context-queries.d.ts +20 -0
- package/dist/src/storage/context-queries.js +137 -0
- package/dist/src/storage/database.d.ts +12 -71
- package/dist/src/storage/database.js +12 -562
- package/dist/src/storage/graph-writes.d.ts +17 -0
- package/dist/src/storage/graph-writes.js +126 -0
- package/dist/src/storage/plugin-contributions.d.ts +15 -0
- package/dist/src/storage/plugin-contributions.js +28 -0
- package/dist/src/storage/plugin-graph.d.ts +6 -0
- package/dist/src/storage/plugin-graph.js +81 -0
- package/dist/src/storage/queries.d.ts +25 -0
- package/dist/src/storage/queries.js +129 -0
- package/dist/src/storage/row-decoders.d.ts +8 -0
- package/dist/src/storage/row-decoders.js +37 -0
- package/dist/src/storage/schema.d.ts +2 -2
- package/dist/src/storage/schema.js +13 -2
- package/dist/src/storage/traversal-queries.d.ts +15 -0
- package/dist/src/storage/traversal-queries.js +87 -0
- package/dist/src/types.d.ts +10 -4
- package/dist/src/view/index.d.ts +1 -1
- package/dist/src/view/index.js +2 -2
- package/dist/src/view/model.d.ts +2 -0
- package/dist/src/view/render.d.ts +5 -2
- package/dist/src/view/render.js +81 -13
- package/dist/src/view/templates/icons/call.svg +11 -0
- package/dist/src/view/templates/icons/export.svg +1 -0
- package/dist/src/view/templates/icons/file.svg +9 -0
- package/dist/src/view/templates/icons/import.svg +1 -0
- package/dist/src/view/templates/icons/package.svg +1 -0
- package/dist/src/view/templates/icons/symbol.svg +7 -0
- package/dist/src/view/templates/icons/user.svg +15 -0
- package/dist/src/view/templates/interactive.css +6 -3
- package/dist/src/view/templates/interactive.html +79 -9
- package/package.json +1 -1
package/dist/src/view/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface ViewRunDependencies {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function viewGraph(options?: ViewCommandOptions, dependencies?: ViewRunDependencies): Promise<ViewRunResult>;
|
|
23
23
|
export { buildViewModel, loadViewSnapshot } from './model.js';
|
|
24
|
-
export type { ViewSnapshot, ViewNode, ViewEdge, ViewBox, ViewModel } from './model.js';
|
|
24
|
+
export type { DisplayMode, ViewSnapshot, ViewNode, ViewEdge, ViewBox, ViewModel } from './model.js';
|
|
25
25
|
export { renderInteractiveHtml, renderSvgMarkup, writeSvgSnapshot } from './render.js';
|
|
26
26
|
export { openHtmlArtifactInBrowser, openInDefaultBrowser, writeTemporaryHtmlArtifact } from './open.js';
|
|
27
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/view/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export async function viewGraph(options = {}, dependencies = {}) {
|
|
|
7
7
|
const snapshot = loadViewSnapshot(options);
|
|
8
8
|
const model = buildViewModel(snapshot, options);
|
|
9
9
|
if (options.output) {
|
|
10
|
-
const svg = renderSvgMarkup(model, { includeHiddenNodes: false, interactive: false });
|
|
10
|
+
const svg = renderSvgMarkup(model, { includeHiddenNodes: false, interactive: false, display: options.display });
|
|
11
11
|
mkdirSync(path.dirname(options.output), { recursive: true });
|
|
12
12
|
writeSvgSnapshot(options.output, svg);
|
|
13
13
|
return {
|
|
@@ -21,7 +21,7 @@ export async function viewGraph(options = {}, dependencies = {}) {
|
|
|
21
21
|
edgeCount: model.edges.length
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
const html = renderInteractiveHtml(model);
|
|
24
|
+
const html = renderInteractiveHtml(model, { display: options.display });
|
|
25
25
|
const opened = openHtmlArtifactInBrowser(html, dependencies.opener);
|
|
26
26
|
return {
|
|
27
27
|
mode: 'interactive',
|
package/dist/src/view/model.d.ts
CHANGED
|
@@ -43,11 +43,13 @@ export interface ViewModel extends ViewSnapshot {
|
|
|
43
43
|
graphBounds: LayoutBounds;
|
|
44
44
|
startWithFullGraph: boolean;
|
|
45
45
|
}
|
|
46
|
+
export type DisplayMode = 'simple' | 'shape' | 'icon';
|
|
46
47
|
export interface ViewModelOptions {
|
|
47
48
|
root?: string | undefined;
|
|
48
49
|
scope?: StorageScope | 'both' | undefined;
|
|
49
50
|
anchor?: string | undefined;
|
|
50
51
|
search?: string | undefined;
|
|
52
|
+
display?: DisplayMode | undefined;
|
|
51
53
|
}
|
|
52
54
|
export declare function loadViewSnapshot({ root, scope }?: ViewModelOptions): ViewSnapshot;
|
|
53
55
|
export declare function buildViewModel(snapshot: ViewSnapshot, { anchor, search }?: ViewModelOptions): ViewModel;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { ViewModel } from './model.js';
|
|
1
|
+
import type { DisplayMode, ViewModel } from './model.js';
|
|
2
2
|
export interface RenderSvgOptions {
|
|
3
3
|
includeHiddenNodes?: boolean | undefined;
|
|
4
4
|
interactive?: boolean | undefined;
|
|
5
|
+
display?: DisplayMode | undefined;
|
|
5
6
|
}
|
|
6
7
|
export declare function renderSvgMarkup(model: ViewModel, options?: RenderSvgOptions): string;
|
|
7
|
-
export declare function renderInteractiveHtml(model: ViewModel
|
|
8
|
+
export declare function renderInteractiveHtml(model: ViewModel, options?: {
|
|
9
|
+
display?: DisplayMode | undefined;
|
|
10
|
+
}): string;
|
|
8
11
|
export declare function writeSvgSnapshot(filePath: string, svgMarkup: string): void;
|
|
9
12
|
//# sourceMappingURL=render.d.ts.map
|
package/dist/src/view/render.js
CHANGED
|
@@ -15,6 +15,8 @@ export function renderSvgMarkup(model, options = {}) {
|
|
|
15
15
|
const graphLabel = `${model.repository.name} store graph`;
|
|
16
16
|
const title = options.includeHiddenNodes ? `${graphLabel} · full graph` : graphLabel;
|
|
17
17
|
const mode = options.interactive ? 'interactive' : 'snapshot';
|
|
18
|
+
const display = options.display ?? 'shape';
|
|
19
|
+
const icons = display === 'icon' ? loadIconAssets() : null;
|
|
18
20
|
const renderedEdges = edges
|
|
19
21
|
.map((edge) => {
|
|
20
22
|
const source = nodes.find((node) => node.id === edge.sourceId);
|
|
@@ -28,10 +30,10 @@ export function renderSvgMarkup(model, options = {}) {
|
|
|
28
30
|
const renderedNodes = nodes
|
|
29
31
|
.map((node) => {
|
|
30
32
|
const presentation = nodePresentation(node);
|
|
31
|
-
const labelY = node.kind === 'file' ? 0 : presentation.height / 2 + 14;
|
|
33
|
+
const labelY = display === 'shape' && node.kind === 'file' ? 0 : presentation.height / 2 + 14;
|
|
32
34
|
return `
|
|
33
35
|
<g class="node node--${escapeHtml(node.kind)}" data-node-id="${escapeHtml(node.id)}" data-node-kind="${escapeHtml(node.kind)}" data-node-shape="${escapeHtml(presentation.shape)}" transform="translate(${formatNumber(node.x)},${formatNumber(node.y)})">
|
|
34
|
-
${renderNodeShapeMarkup(node, presentation)}
|
|
36
|
+
${renderNodeShapeMarkup(node, presentation, display, icons)}
|
|
35
37
|
<text class="node-label" text-anchor="middle" dominant-baseline="${node.kind === 'file' ? 'middle' : 'hanging'}" y="${formatNumber(labelY)}">${escapeHtml(node.label)}</text>
|
|
36
38
|
<title>${escapeHtml(node.label)} (${escapeHtml(node.id)})</title>
|
|
37
39
|
</g>`;
|
|
@@ -70,8 +72,8 @@ export function renderSvgMarkup(model, options = {}) {
|
|
|
70
72
|
}
|
|
71
73
|
const interactiveHtmlTemplate = loadTemplateAsset('interactive.html');
|
|
72
74
|
const interactiveCss = loadTemplateAsset('interactive.css');
|
|
73
|
-
export function renderInteractiveHtml(model) {
|
|
74
|
-
const data = serializeInteractiveGraph(model);
|
|
75
|
+
export function renderInteractiveHtml(model, options = {}) {
|
|
76
|
+
const data = serializeInteractiveGraph(model, options.display ?? 'shape');
|
|
75
77
|
const graphTitle = `${model.repository.name} store graph`;
|
|
76
78
|
const subtitle = model.anchorNodeId
|
|
77
79
|
? `Anchored on ${model.nodes.find((node) => node.id === model.anchorNodeId)?.label ?? model.anchorNodeId}`
|
|
@@ -103,8 +105,19 @@ export function writeSvgSnapshot(filePath, svgMarkup) {
|
|
|
103
105
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
104
106
|
writeFileSync(filePath, svgMarkup, 'utf8');
|
|
105
107
|
}
|
|
106
|
-
function serializeInteractiveGraph(model) {
|
|
107
|
-
const
|
|
108
|
+
function serializeInteractiveGraph(model, display) {
|
|
109
|
+
const serializedGraph = model.anchorNodeId
|
|
110
|
+
? downstreamGraph(model.fullNodes, model.fullEdges, model.anchorNodeId)
|
|
111
|
+
: { nodes: model.fullNodes, edges: model.fullEdges };
|
|
112
|
+
const serializedNodeIds = new Set(serializedGraph.nodes.map((node) => node.id));
|
|
113
|
+
const serializedEdgeIds = new Set(serializedGraph.edges.map((edge) => edge.id));
|
|
114
|
+
const neighborhoods = Object.fromEntries(Object.entries(model.fullNeighborhoods)
|
|
115
|
+
.filter(([nodeId]) => serializedNodeIds.has(nodeId))
|
|
116
|
+
.map(([nodeId, neighborhood]) => [nodeId, {
|
|
117
|
+
nodeIds: neighborhood.nodeIds.filter((id) => serializedNodeIds.has(id)),
|
|
118
|
+
edgeIds: neighborhood.edgeIds.filter((id) => serializedEdgeIds.has(id))
|
|
119
|
+
}]));
|
|
120
|
+
const nodes = serializedGraph.nodes.map((node) => {
|
|
108
121
|
const presentation = nodePresentation(node);
|
|
109
122
|
return {
|
|
110
123
|
id: node.id,
|
|
@@ -122,7 +135,7 @@ function serializeInteractiveGraph(model) {
|
|
|
122
135
|
presentationRadius: presentation.presentationRadius
|
|
123
136
|
};
|
|
124
137
|
});
|
|
125
|
-
const edges =
|
|
138
|
+
const edges = serializedGraph.edges.map((edge) => ({
|
|
126
139
|
id: edge.id,
|
|
127
140
|
kind: edge.kind,
|
|
128
141
|
source: edge.sourceId,
|
|
@@ -135,12 +148,56 @@ function serializeInteractiveGraph(model) {
|
|
|
135
148
|
scope: model.scope,
|
|
136
149
|
search: model.search,
|
|
137
150
|
anchorNodeId: model.anchorNodeId,
|
|
138
|
-
|
|
151
|
+
display,
|
|
152
|
+
icons: loadIconAssets(),
|
|
153
|
+
fullNeighborhoods: neighborhoods,
|
|
139
154
|
nodes,
|
|
140
155
|
edges
|
|
141
156
|
}));
|
|
142
157
|
}
|
|
143
|
-
function
|
|
158
|
+
function downstreamGraph(nodes, edges, anchorNodeId) {
|
|
159
|
+
const downstreamByNodeId = new Map();
|
|
160
|
+
for (const edge of edges) {
|
|
161
|
+
const downstream = downstreamByNodeId.get(edge.sourceId) ?? [];
|
|
162
|
+
downstream.push(edge.targetId);
|
|
163
|
+
downstreamByNodeId.set(edge.sourceId, downstream);
|
|
164
|
+
}
|
|
165
|
+
const selectedNodeIds = new Set([anchorNodeId]);
|
|
166
|
+
const queue = [anchorNodeId];
|
|
167
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
168
|
+
const nodeId = queue[index];
|
|
169
|
+
for (const downstreamNodeId of downstreamByNodeId.get(nodeId) ?? []) {
|
|
170
|
+
if (selectedNodeIds.has(downstreamNodeId))
|
|
171
|
+
continue;
|
|
172
|
+
selectedNodeIds.add(downstreamNodeId);
|
|
173
|
+
queue.push(downstreamNodeId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
nodes: nodes.filter((node) => selectedNodeIds.has(node.id)),
|
|
178
|
+
edges: edges.filter((edge) => selectedNodeIds.has(edge.sourceId) && selectedNodeIds.has(edge.targetId))
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const iconKinds = ['file', 'symbol', 'import', 'export', 'call', 'user', 'package'];
|
|
182
|
+
function loadIconAssets() {
|
|
183
|
+
return Object.fromEntries(iconKinds.map((kind) => {
|
|
184
|
+
const source = loadTemplateAsset(`icons/${kind}.svg`);
|
|
185
|
+
const match = source.match(/<svg[^>]*viewBox="([^"]+)"[^>]*>([\s\S]*)<\/svg>/);
|
|
186
|
+
if (!match)
|
|
187
|
+
throw new Error(`Invalid icon asset for ${kind}`);
|
|
188
|
+
return [kind, { viewBox: match[1], markup: match[2] }];
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
function renderNodeShapeMarkup(node, presentation, display, icons) {
|
|
192
|
+
if (display === 'simple') {
|
|
193
|
+
return `<circle class="node-shape node-shape--simple" cx="0" cy="0" r="${formatNumber(Math.min(presentation.width, presentation.height) / 2)}" fill="${colorForKind(node.kind)}" />`;
|
|
194
|
+
}
|
|
195
|
+
if (display === 'icon') {
|
|
196
|
+
const icon = icons?.[node.kind];
|
|
197
|
+
if (!icon)
|
|
198
|
+
throw new Error(`Missing icon asset for ${node.kind}`);
|
|
199
|
+
return `<svg class="node-shape node-shape--icon" x="${formatNumber(-presentation.width / 2)}" y="${formatNumber(-presentation.height / 2)}" width="${formatNumber(presentation.width)}" height="${formatNumber(presentation.height)}" viewBox="${escapeHtml(icon.viewBox)}" color="${colorForKind(node.kind)}" aria-hidden="true">${icon.markup}</svg>`;
|
|
200
|
+
}
|
|
144
201
|
const fill = colorForKind(node.kind);
|
|
145
202
|
switch (presentation.shape) {
|
|
146
203
|
case 'rounded-rect':
|
|
@@ -155,6 +212,8 @@ function renderNodeShapeMarkup(node, presentation) {
|
|
|
155
212
|
return `<polygon class="node-shape node-shape--hexagon" points="${shapePoints(presentation, 'hexagon')}" fill="${fill}" />`;
|
|
156
213
|
case 'user':
|
|
157
214
|
return renderUserNodeShapeMarkup(presentation, fill);
|
|
215
|
+
case 'package':
|
|
216
|
+
return renderPackageNodeShapeMarkup(presentation);
|
|
158
217
|
default:
|
|
159
218
|
return `<rect class="node-shape" x="${formatNumber(-presentation.width / 2)}" y="${formatNumber(-presentation.height / 2)}" width="${formatNumber(presentation.width)}" height="${formatNumber(presentation.height)}" rx="6" ry="6" fill="${fill}" />`;
|
|
160
219
|
}
|
|
@@ -173,12 +232,12 @@ function nodePresentation(node) {
|
|
|
173
232
|
const shape = shapeForKind(node.kind);
|
|
174
233
|
const width = node.kind === 'file'
|
|
175
234
|
? shapeWidthForKind(node.kind, labelLength)
|
|
176
|
-
: node.kind === 'user'
|
|
235
|
+
: node.kind === 'user' || node.kind === 'package'
|
|
177
236
|
? 72
|
|
178
237
|
: 16;
|
|
179
238
|
const height = node.kind === 'file'
|
|
180
239
|
? shapeHeightForKind(node.kind, labelLength)
|
|
181
|
-
: node.kind === 'user'
|
|
240
|
+
: node.kind === 'user' || node.kind === 'package'
|
|
182
241
|
? 72
|
|
183
242
|
: 16;
|
|
184
243
|
const presentationRadius = Math.max(width, height) / 2 + 10;
|
|
@@ -204,6 +263,8 @@ function shapeForKind(kind) {
|
|
|
204
263
|
return 'hexagon';
|
|
205
264
|
case 'user':
|
|
206
265
|
return 'user';
|
|
266
|
+
case 'package':
|
|
267
|
+
return 'hexagon';
|
|
207
268
|
default:
|
|
208
269
|
return 'box';
|
|
209
270
|
}
|
|
@@ -226,7 +287,8 @@ function shapeSquareSizeForKind(kind, labelLength) {
|
|
|
226
287
|
import: { base: 68, perChar: 7, min: 96, max: 220 },
|
|
227
288
|
export: { base: 68, perChar: 7, min: 96, max: 220 },
|
|
228
289
|
call: { base: 68, perChar: 7, min: 96, max: 220 },
|
|
229
|
-
user: { base: 68, perChar: 7, min: 72, max: 180 }
|
|
290
|
+
user: { base: 68, perChar: 7, min: 72, max: 180 },
|
|
291
|
+
package: { base: 72, perChar: 7, min: 96, max: 220 }
|
|
230
292
|
};
|
|
231
293
|
const spec = sizeByKind[kind];
|
|
232
294
|
return Math.max(spec.min, Math.min(spec.max, Math.round(spec.base + labelLength * spec.perChar)));
|
|
@@ -239,6 +301,9 @@ function expandViewBox(viewBox, padding) {
|
|
|
239
301
|
height: viewBox.height + padding * 2
|
|
240
302
|
};
|
|
241
303
|
}
|
|
304
|
+
function renderPackageNodeShapeMarkup(presentation) {
|
|
305
|
+
return `<svg class="node-shape node-shape--package" x="${formatNumber(-presentation.width / 2)}" y="${formatNumber(-presentation.height / 2)}" width="${formatNumber(presentation.width)}" height="${formatNumber(presentation.height)}" viewBox="0 0 512 512" aria-hidden="true"><polygon fill="#7E4F1F" points="511.516,107.105 511.788,107.036 264.065,31.004 0.479,106.898 0,106.759 0,411.849 238.676,480.721 238.676,480.159 238.888,480.996 512,412.125 512,107.036"/><polygon fill="#EDA637" points="264.065,31.004 0,107.036 238.676,175.907 511.788,107.036"/><polygon fill="#C27526" points="238.888,480.996 512,412.125 512,107.036 238.888,175.907"/><path fill="#FFEC99" d="M193.016 51.462 78.638 84.393l230.233 73.866v50.416c0 3.323 1.541 6.458 4.172 8.487 2.631 2.029 6.053 2.725 9.268 1.881l99.058-25.986c4.713-1.237 7.999-5.495 7.999-10.369v-54.816L193.016 51.462Z"/><path fill="#FFDC35" d="M486.158 374.572c0 6.29-4.278 11.775-10.378 13.307l-52.647 13.55c-2.779.698-5.736.663-8.41-.367-5.363-2.066-8.677-7.119-8.677-12.576V332.26c0-6.29 4.278-11.775 10.378-13.307l52.647-13.55c2.779-.698 5.736-.663 8.41.367 5.363 2.066 8.677 7.119 8.677 12.576v56.226Z"/><path fill="#FFDC35" d="M429.367 182.689c0 4.872-3.287 9.132-7.999 10.368l-99.058 25.986c-3.214.843-6.637.148-9.268-1.881-2.631-2.029-4.172-5.164-4.172-8.487v-50.416l120.497-30.386v54.816Z"/><path fill="#DF7400" d="m186.692 292.407-40.804-11.978c-5.273-1.548-8.895-6.385-8.895-11.881v-39.76c0-8.263 7.941-14.208 15.87-11.881l40.804 11.978c5.273 1.548 8.895 6.385 8.895 11.881v39.76c0 8.263-7.941 14.208-15.87 11.881Z"/></svg>`;
|
|
306
|
+
}
|
|
242
307
|
function renderUserNodeShapeMarkup(presentation, fill) {
|
|
243
308
|
const scale = Math.max(presentation.width, presentation.height) / 24;
|
|
244
309
|
return `<g class="node-shape node-shape--user" transform="scale(${formatNumber(scale)}) translate(-12,-12)" fill="none" stroke="${fill}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -261,6 +326,8 @@ function colorForKind(kind) {
|
|
|
261
326
|
return '#fb7185';
|
|
262
327
|
case 'user':
|
|
263
328
|
return '#14b8a6';
|
|
329
|
+
case 'package':
|
|
330
|
+
return '#f97316';
|
|
264
331
|
default:
|
|
265
332
|
return '#94a3b8';
|
|
266
333
|
}
|
|
@@ -296,7 +363,8 @@ function legendItems() {
|
|
|
296
363
|
['import', 'Import'],
|
|
297
364
|
['export', 'Export'],
|
|
298
365
|
['call', 'Call'],
|
|
299
|
-
['user', 'Contributor']
|
|
366
|
+
['user', 'Contributor'],
|
|
367
|
+
['package', 'Package']
|
|
300
368
|
];
|
|
301
369
|
return items
|
|
302
370
|
.map(([kind, label]) => `
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g id="call_icon">
|
|
3
|
+
<circle cx="24" cy="24" r="21" fill="#ff6d2e" stroke="inherit" stroke-width="4"></circle>
|
|
4
|
+
<path d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z" fill="#b71111" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
5
|
+
<path d="M42 6L37 11" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
6
|
+
<path d="M11 25.9999L14 22.9999L25 33.9999L22 36.9999C20.5 38.5 15 41 11 36.9999C7 32.9999 9.5 27.5 11 25.9999Z" fill="#b71111" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
7
|
+
<path d="M23 32L27 28" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
8
|
+
<path d="M6 42L11 37" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
9
|
+
<path d="M16 25L20 21" stroke="#3a1304" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
10
|
+
</g>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21V8M7 13l5-5 5 5M5 3h14"/></svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" >
|
|
2
|
+
<g id="file_icon" fill="#9ed2ff" stroke="#1d97e2" stroke-width="1" stroke-linecap="round" stroke-linejoin="miter">
|
|
3
|
+
<polygon points="14 2 3 2 3 22 21 22 21 9 14 9 14 2" fill="#059cf7" opacity="0.1" stroke-width="0"></polygon>
|
|
4
|
+
<polygon points="3 2 3 22 21 22 21 9 14 2 3 2"></polygon>
|
|
5
|
+
<polyline points="20.6 9 14 9 14 2.4" stroke-linecap="round"></polyline>
|
|
6
|
+
<line x1="16" y1="13" x2="8" y2="13"></line><line x1="10" y1="9" x2="8" y2="9"></line>
|
|
7
|
+
<line x1="16" y1="17" x2="8" y2="17"></line>
|
|
8
|
+
</g>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v13M7 11l5 5 5-5M5 21h14"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 512 512"><polygon fill="#7E4F1F" points="511.516,107.105 511.788,107.036 264.065,31.004 0.479,106.898 0,106.759 0,411.849 238.676,480.721 238.676,480.159 238.888,480.996 512,412.125 512,107.036"/><polygon fill="#EDA637" points="264.065,31.004 0,107.036 238.676,175.907 511.788,107.036"/><polygon fill="#C27526" points="238.888,480.996 512,412.125 512,107.036 238.888,175.907"/><path fill="#FFEC99" d="M193.016 51.462 78.638 84.393l230.233 73.866v50.416c0 3.323 1.541 6.458 4.172 8.487 2.631 2.029 6.053 2.725 9.268 1.881l99.058-25.986c4.713-1.237 7.999-5.495 7.999-10.369v-54.816L193.016 51.462Z"/><path fill="#FFDC35" d="M486.158 374.572c0 6.29-4.278 11.775-10.378 13.307l-52.647 13.55c-2.779.698-5.736.663-8.41-.367-5.363-2.066-8.677-7.119-8.677-12.576V332.26c0-6.29 4.278-11.775 10.378-13.307l52.647-13.55c2.779-.698 5.736-.663 8.41.367 5.363 2.066 8.677 7.119 8.677 12.576v56.226Z"/><path fill="#FFDC35" d="M429.367 182.689c0 4.872-3.287 9.132-7.999 10.368l-99.058 25.986c-3.214.843-6.637.148-9.268-1.881-2.631-2.029-4.172-5.164-4.172-8.487v-50.416l120.497-30.386v54.816Z"/><path fill="#DF7400" d="m186.692 292.407-40.804-11.978c-5.273-1.548-8.895-6.385-8.895-11.881v-39.76c0-8.263 7.941-14.208 15.87-11.881l40.804 11.978c5.273 1.548 8.895 6.385 8.895 11.881v39.76c0 8.263-7.941 14.208-15.87 11.881Z"/></svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" preserveAspectRatio="xMidYMid meet">
|
|
2
|
+
<g id="symbol_icon" fill="none" fill-rule="evenodd">
|
|
3
|
+
<path fill="#d5c007" d="M17 36a1.99 1.99 0 0 1-1.414-.586l-11-11A2 2 0 0 1 4 23V2A2 2 0 0 1 7.414.586L17 10.171L26.586.586A1.998 1.998 0 0 1 30 2v21a2 2 0 0 1-.586 1.414l-11 11A1.99 1.99 0 0 1 17 36z"></path>
|
|
4
|
+
<path fill="#47DED4" d="M17 13L28 2v21L17 34z"></path>
|
|
5
|
+
<path fill="#FFFF87" d="M6 2l11 11v21L6 23z"></path>
|
|
6
|
+
</g>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" >
|
|
2
|
+
<g id="user_icon" fill="#c7e2f9" stroke="#1995d8"
|
|
3
|
+
stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
|
|
4
|
+
<defs>
|
|
5
|
+
<style>.cls-1{fill:#c7e2f9;}.cls-1,.cls-2{fill-rule:evenodd;}.cls-2{fill:#4285f4;}</style>
|
|
6
|
+
</defs>
|
|
7
|
+
<g data-name="Product Icons">
|
|
8
|
+
<g>
|
|
9
|
+
<rect fill="#234cbc" x="6" y="4" width="12" height="16" rx="4"></rect>
|
|
10
|
+
<path class="cls-1" d="M12,2,3.79,5.42v5.63c0,5.06,3.5,9.8,8.21,11,4.71-1.15,8.21-5.89,8.21-10.95V5.42Zm0,3.79a2.63,2.63,0,1,1-1.86.77A2.63,2.63,0,0,1,12,5.79Zm4.11,11.15A8.64,8.64,0,0,1,12,19.87a8.64,8.64,0,0,1-4.11-2.93V14.69c0-1.67,2.74-2.52,4.11-2.52s4.11.85,4.11,2.52v2.25Z"></path>
|
|
11
|
+
<path class="cls-2" d="M12,2V5.79a2.63,2.63,0,1,1,0,5.26v1.12c1.37,0,4.11.85,4.11,2.52v2.25A8.64,8.64,0,0,1,12,19.87V22c4.71-1.15,8.21-5.89,8.21-10.95V5.42Z"></path>
|
|
12
|
+
</g>
|
|
13
|
+
</g>
|
|
14
|
+
</g>
|
|
15
|
+
</svg>
|
|
@@ -38,6 +38,7 @@ html, body {
|
|
|
38
38
|
border: 1px solid var(--panel-border);
|
|
39
39
|
border-radius: 16px;
|
|
40
40
|
background: var(--panel);
|
|
41
|
+
-webkit-backdrop-filter: blur(16px);
|
|
41
42
|
backdrop-filter: blur(16px);
|
|
42
43
|
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.35);
|
|
43
44
|
overflow: auto;
|
|
@@ -107,7 +108,8 @@ html, body {
|
|
|
107
108
|
color: var(--muted);
|
|
108
109
|
font-size: 13px;
|
|
109
110
|
}
|
|
110
|
-
.hud input
|
|
111
|
+
.hud input,
|
|
112
|
+
.hud select {
|
|
111
113
|
width: 100%;
|
|
112
114
|
box-sizing: border-box;
|
|
113
115
|
padding: 10px 12px;
|
|
@@ -187,12 +189,13 @@ html, body {
|
|
|
187
189
|
}
|
|
188
190
|
.node-label {
|
|
189
191
|
fill: var(--text);
|
|
190
|
-
font-size:
|
|
191
|
-
font-weight:
|
|
192
|
+
font-size: 8px;
|
|
193
|
+
font-weight: 200;
|
|
192
194
|
paint-order: stroke;
|
|
193
195
|
stroke: #020617;
|
|
194
196
|
stroke-width: 3px;
|
|
195
197
|
stroke-linejoin: round;
|
|
198
|
+
-webkit-user-select: none;
|
|
196
199
|
user-select: none;
|
|
197
200
|
pointer-events: none;
|
|
198
201
|
}
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
<input id="search" type="search" placeholder="Type to filter by label" autocomplete="off" value="{{SEARCH_TERM}}" />
|
|
29
29
|
</label>
|
|
30
30
|
</div>
|
|
31
|
+
<div class="section">
|
|
32
|
+
<label>
|
|
33
|
+
<span class="status">Node display</span>
|
|
34
|
+
<select id="display-select">
|
|
35
|
+
<option value="simple">Simple</option>
|
|
36
|
+
<option value="shape">Shape</option>
|
|
37
|
+
<option value="icon">Icon</option>
|
|
38
|
+
</select>
|
|
39
|
+
</label>
|
|
40
|
+
</div>
|
|
31
41
|
<div class="section">
|
|
32
42
|
<label>
|
|
33
43
|
<span class="status">View box zoom</span>
|
|
@@ -57,11 +67,13 @@
|
|
|
57
67
|
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
|
|
58
68
|
|
|
59
69
|
const data = {{DATA}};
|
|
70
|
+
let display = data.display ?? 'shape';
|
|
60
71
|
const app = document.getElementById('app');
|
|
61
72
|
const svg = d3.select('#graph');
|
|
62
73
|
const graphPanel = document.querySelector('.graph-panel');
|
|
63
74
|
const tooltip = document.getElementById('tooltip');
|
|
64
75
|
const searchInput = document.getElementById('search');
|
|
76
|
+
const displaySelect = document.getElementById('display-select');
|
|
65
77
|
const zoomInput = document.getElementById('zoom');
|
|
66
78
|
const zoomValue = document.getElementById('zoom-value');
|
|
67
79
|
const zoomAllButton = document.getElementById('zoom-all');
|
|
@@ -114,6 +126,30 @@
|
|
|
114
126
|
|
|
115
127
|
function appendNodeShape(group, node) {
|
|
116
128
|
const fill = node.color ?? '#94a3b8';
|
|
129
|
+
if (display === 'simple') {
|
|
130
|
+
group.append('circle')
|
|
131
|
+
.attr('class', 'node-shape node-shape--simple')
|
|
132
|
+
.attr('cx', 0)
|
|
133
|
+
.attr('cy', 0)
|
|
134
|
+
.attr('r', Math.min(node.width, node.height) / 2)
|
|
135
|
+
.attr('fill', fill);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (display === 'icon') {
|
|
139
|
+
const icon = data.icons?.[node.kind];
|
|
140
|
+
if (!icon) throw new Error('Missing icon asset for ' + node.kind);
|
|
141
|
+
group.append('svg')
|
|
142
|
+
.attr('class', 'node-shape node-shape--icon')
|
|
143
|
+
.attr('x', -node.width / 2)
|
|
144
|
+
.attr('y', -node.height / 2)
|
|
145
|
+
.attr('width', node.width)
|
|
146
|
+
.attr('height', node.height)
|
|
147
|
+
.attr('viewBox', icon.viewBox)
|
|
148
|
+
.attr('color', fill)
|
|
149
|
+
.attr('aria-hidden', 'true')
|
|
150
|
+
.html(icon.markup);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
117
153
|
switch (node.shape) {
|
|
118
154
|
case 'rounded-rect':
|
|
119
155
|
group.append('rect')
|
|
@@ -155,6 +191,17 @@
|
|
|
155
191
|
: (-node.width / 2 + node.height / 4) + ',' + (-node.height / 2) + ' ' + (node.width / 2 - node.height / 4) + ',' + (-node.height / 2) + ' ' + (node.width / 2) + ',0 ' + (node.width / 2 - node.height / 4) + ',' + (node.height / 2) + ' ' + (-node.width / 2 + node.height / 4) + ',' + (node.height / 2) + ' ' + (-node.width / 2) + ',0')
|
|
156
192
|
.attr('fill', fill);
|
|
157
193
|
break;
|
|
194
|
+
case 'package':
|
|
195
|
+
group.append('svg')
|
|
196
|
+
.attr('class', 'node-shape node-shape--package')
|
|
197
|
+
.attr('x', -node.width / 2)
|
|
198
|
+
.attr('y', -node.height / 2)
|
|
199
|
+
.attr('width', node.width)
|
|
200
|
+
.attr('height', node.height)
|
|
201
|
+
.attr('viewBox', '0 0 512 512')
|
|
202
|
+
.attr('aria-hidden', 'true')
|
|
203
|
+
.html('<polygon fill="#7E4F1F" points="511.516,107.105 511.788,107.036 264.065,31.004 0.479,106.898 0,106.759 0,411.849 238.676,480.721 238.676,480.159 238.888,480.996 512,412.125 512,107.036"/><polygon fill="#EDA637" points="264.065,31.004 0,107.036 238.676,175.907 511.788,107.036"/><polygon fill="#C27526" points="238.888,480.996 512,412.125 512,107.036 238.888,175.907"/><path fill="#FFEC99" d="M193.016 51.462 78.638 84.393l230.233 73.866v50.416c0 3.323 1.541 6.458 4.172 8.487 2.631 2.029 6.053 2.725 9.268 1.881l99.058-25.986c4.713-1.237 7.999-5.495 7.999-10.369v-54.816L193.016 51.462Z"/><path fill="#FFDC35" d="M486.158 374.572c0 6.29-4.278 11.775-10.378 13.307l-52.647 13.55c-2.779.698-5.736.663-8.41-.367-5.363-2.066-8.677-7.119-8.677-12.576V332.26c0-6.29 4.278-11.775 10.378-13.307l52.647-13.55c2.779-.698 5.736-.663 8.41.367 5.363 2.066 8.677 7.119 8.677 12.576v56.226Z"/><path fill="#FFDC35" d="M429.367 182.689c0 4.872-3.287 9.132-7.999 10.368l-99.058 25.986c-3.214.843-6.637.148-9.268-1.881-2.631-2.029-4.172-5.164-4.172-8.487v-50.416l120.497-30.386v54.816Z"/><path fill="#DF7400" d="m186.692 292.407-40.804-11.978c-5.273-1.548-8.895-6.385-8.895-11.881v-39.76c0-8.263 7.941-14.208 15.87-11.881l40.804 11.978c5.273 1.548 8.895 6.385 8.895 11.881v39.76c0 8.263-7.941 14.208-15.87 11.881Z"/>');
|
|
204
|
+
break;
|
|
158
205
|
case 'user':
|
|
159
206
|
const userIcon = group.append('svg')
|
|
160
207
|
.attr('class', 'node-shape node-shape--user')
|
|
@@ -213,12 +260,27 @@
|
|
|
213
260
|
group.append('text')
|
|
214
261
|
.attr('class', 'node-label')
|
|
215
262
|
.attr('text-anchor', 'middle')
|
|
216
|
-
.attr('dominant-baseline', d.kind === 'file' ? 'middle' : 'hanging')
|
|
217
|
-
.attr('y', d.kind === 'file' ? 0 : (d.height / 2) + 14)
|
|
263
|
+
.attr('dominant-baseline', display === 'shape' && d.kind === 'file' ? 'middle' : 'hanging')
|
|
264
|
+
.attr('y', display === 'shape' && d.kind === 'file' ? 0 : (d.height / 2) + 14)
|
|
218
265
|
.text(d.label);
|
|
219
266
|
group.append('title').text(d.label);
|
|
220
267
|
});
|
|
221
268
|
|
|
269
|
+
function updateDisplay() {
|
|
270
|
+
nodes.each(function(d) {
|
|
271
|
+
const group = d3.select(this);
|
|
272
|
+
group.selectAll('.node-shape').remove();
|
|
273
|
+
appendNodeShape(group, d);
|
|
274
|
+
group.select('.node-label')
|
|
275
|
+
.attr('dominant-baseline', display === 'shape' && d.kind === 'file' ? 'middle' : 'hanging')
|
|
276
|
+
.attr('y', display === 'shape' && d.kind === 'file' ? 0 : (d.height / 2) + 14);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (displaySelect instanceof HTMLSelectElement) {
|
|
281
|
+
displaySelect.value = display;
|
|
282
|
+
}
|
|
283
|
+
|
|
222
284
|
const zoom = d3.zoom().scaleExtent([0.2, 4]).on('zoom', (event) => {
|
|
223
285
|
zoomLayer.attr('transform', event.transform);
|
|
224
286
|
});
|
|
@@ -369,7 +431,7 @@
|
|
|
369
431
|
updateInfo();
|
|
370
432
|
updateZoomState();
|
|
371
433
|
updateLegendState();
|
|
372
|
-
reframe();
|
|
434
|
+
reframe(visibleNodeIds);
|
|
373
435
|
}
|
|
374
436
|
|
|
375
437
|
function setAnchor(nodeId) {
|
|
@@ -392,7 +454,7 @@
|
|
|
392
454
|
}
|
|
393
455
|
const nextSize = canvasSize();
|
|
394
456
|
svg.attr('viewBox', [0, 0, nextSize.width, nextSize.height]);
|
|
395
|
-
reframe();
|
|
457
|
+
reframe(getVisibleSets().visibleNodeIds);
|
|
396
458
|
}
|
|
397
459
|
|
|
398
460
|
function zoomAll() {
|
|
@@ -412,11 +474,11 @@
|
|
|
412
474
|
applyState();
|
|
413
475
|
}
|
|
414
476
|
|
|
415
|
-
function reframe() {
|
|
477
|
+
function reframe(visibleNodeIds = allNodeIds) {
|
|
416
478
|
const selectedNode = anchorId ? nodesData.find((candidate) => candidate.id === anchorId) ?? null : null;
|
|
417
479
|
const { width: canvasWidth, height: canvasHeight } = canvasSize();
|
|
418
480
|
|
|
419
|
-
if (focusMode === 'selected' && selectedNode) {
|
|
481
|
+
if (!focusVisibleOnly && focusMode === 'selected' && selectedNode) {
|
|
420
482
|
const transform = d3.zoomIdentity
|
|
421
483
|
.translate(canvasWidth / 2, canvasHeight / 2)
|
|
422
484
|
.scale(focusScale / zoomFactor)
|
|
@@ -425,12 +487,13 @@
|
|
|
425
487
|
return;
|
|
426
488
|
}
|
|
427
489
|
|
|
428
|
-
|
|
490
|
+
const visibleNodes = nodesData.filter((node) => visibleNodeIds.has(node.id));
|
|
491
|
+
if (visibleNodes.length === 0) {
|
|
429
492
|
svg.call(zoom.transform, d3.zoomIdentity);
|
|
430
493
|
return;
|
|
431
494
|
}
|
|
432
495
|
|
|
433
|
-
const bounds =
|
|
496
|
+
const bounds = visibleNodes.reduce((acc, current) => ({
|
|
434
497
|
minX: Math.min(acc.minX, current.x),
|
|
435
498
|
maxX: Math.max(acc.maxX, current.x),
|
|
436
499
|
minY: Math.min(acc.minY, current.y),
|
|
@@ -519,6 +582,13 @@
|
|
|
519
582
|
}
|
|
520
583
|
});
|
|
521
584
|
|
|
585
|
+
displaySelect?.addEventListener('change', (event) => {
|
|
586
|
+
const value = event.target instanceof HTMLSelectElement ? event.target.value : 'shape';
|
|
587
|
+
if (value !== 'simple' && value !== 'shape' && value !== 'icon') return;
|
|
588
|
+
display = value;
|
|
589
|
+
updateDisplay();
|
|
590
|
+
});
|
|
591
|
+
|
|
522
592
|
searchInput.value = filterText;
|
|
523
593
|
searchInput.addEventListener('input', (event) => {
|
|
524
594
|
filterText = event.target.value;
|
|
@@ -560,7 +630,7 @@
|
|
|
560
630
|
if (!graphPanel) return;
|
|
561
631
|
const nextSize = canvasSize();
|
|
562
632
|
svg.attr('viewBox', [0, 0, nextSize.width, nextSize.height]);
|
|
563
|
-
reframe();
|
|
633
|
+
reframe(getVisibleSets().visibleNodeIds);
|
|
564
634
|
});
|
|
565
635
|
|
|
566
636
|
simulation.on('tick', ticked);
|