archtracker-mcp 0.5.0 → 0.6.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/dist/cli/index.js +1709 -1473
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +101 -101
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +148 -134
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/arch-analyze/SKILL.md +2 -0
- package/skills/arch-check/SKILL.md +1 -0
- package/skills/arch-search/SKILL.md +4 -0
- package/skills/arch-snapshot/SKILL.md +1 -0
package/dist/cli/index.js
CHANGED
|
@@ -2175,44 +2175,145 @@ function mergeLayerGraphs(projectRoot, layers) {
|
|
|
2175
2175
|
};
|
|
2176
2176
|
}
|
|
2177
2177
|
|
|
2178
|
-
// src/storage/
|
|
2179
|
-
import {
|
|
2178
|
+
// src/storage/layers.ts
|
|
2179
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
2180
2180
|
import { join as join5 } from "path";
|
|
2181
2181
|
import { z } from "zod";
|
|
2182
|
+
var ARCHTRACKER_DIR = ".archtracker";
|
|
2183
|
+
var LAYERS_FILE = "layers.json";
|
|
2184
|
+
var LayerDefinitionSchema = z.object({
|
|
2185
|
+
name: z.string().min(1).regex(
|
|
2186
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
2187
|
+
"Layer name must be alphanumeric (hyphens/underscores allowed)"
|
|
2188
|
+
),
|
|
2189
|
+
targetDir: z.string().min(1),
|
|
2190
|
+
language: z.enum(LANGUAGE_IDS).optional(),
|
|
2191
|
+
exclude: z.array(z.string()).optional(),
|
|
2192
|
+
color: z.string().optional(),
|
|
2193
|
+
description: z.string().optional()
|
|
2194
|
+
});
|
|
2195
|
+
var CrossLayerConnectionSchema = z.object({
|
|
2196
|
+
fromLayer: z.string(),
|
|
2197
|
+
fromFile: z.string(),
|
|
2198
|
+
toLayer: z.string(),
|
|
2199
|
+
toFile: z.string(),
|
|
2200
|
+
type: z.enum(["api-call", "event", "data-flow", "manual"]),
|
|
2201
|
+
label: z.string().optional()
|
|
2202
|
+
});
|
|
2203
|
+
var LayerConfigSchema = z.object({
|
|
2204
|
+
version: z.literal("1.0"),
|
|
2205
|
+
layers: z.array(LayerDefinitionSchema).min(1).refine(
|
|
2206
|
+
(layers) => {
|
|
2207
|
+
const names = layers.map((l) => l.name);
|
|
2208
|
+
return new Set(names).size === names.length;
|
|
2209
|
+
},
|
|
2210
|
+
{ message: "Layer names must be unique" }
|
|
2211
|
+
),
|
|
2212
|
+
connections: z.array(CrossLayerConnectionSchema).optional()
|
|
2213
|
+
});
|
|
2214
|
+
async function loadLayerConfig(projectRoot) {
|
|
2215
|
+
const filePath = join5(projectRoot, ARCHTRACKER_DIR, LAYERS_FILE);
|
|
2216
|
+
let raw;
|
|
2217
|
+
try {
|
|
2218
|
+
raw = await readFile2(filePath, "utf-8");
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
throw new Error(`Failed to read ${filePath}`);
|
|
2224
|
+
}
|
|
2225
|
+
let parsed;
|
|
2226
|
+
try {
|
|
2227
|
+
parsed = JSON.parse(raw);
|
|
2228
|
+
} catch {
|
|
2229
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
2230
|
+
}
|
|
2231
|
+
const result = LayerConfigSchema.safeParse(parsed);
|
|
2232
|
+
if (!result.success) {
|
|
2233
|
+
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
|
|
2234
|
+
throw new Error(`layers.json validation failed:
|
|
2235
|
+
${issues}`);
|
|
2236
|
+
}
|
|
2237
|
+
return result.data;
|
|
2238
|
+
}
|
|
2239
|
+
async function saveLayerConfig(projectRoot, config) {
|
|
2240
|
+
const dirPath = join5(projectRoot, ARCHTRACKER_DIR);
|
|
2241
|
+
const filePath = join5(dirPath, LAYERS_FILE);
|
|
2242
|
+
await mkdir(dirPath, { recursive: true });
|
|
2243
|
+
await writeFile(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2244
|
+
}
|
|
2245
|
+
function isNodeError(error) {
|
|
2246
|
+
return error instanceof Error && "code" in error;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// src/analyzer/resolve.ts
|
|
2250
|
+
async function resolveGraph(opts) {
|
|
2251
|
+
const layerConfig = await loadLayerConfig(opts.projectRoot);
|
|
2252
|
+
if (layerConfig) {
|
|
2253
|
+
const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
|
|
2254
|
+
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
2255
|
+
const manualConnections = layerConfig.connections ?? [];
|
|
2256
|
+
const manualKeys = new Set(manualConnections.map(
|
|
2257
|
+
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
2258
|
+
));
|
|
2259
|
+
const merged = [
|
|
2260
|
+
...manualConnections,
|
|
2261
|
+
...autoConnections.filter(
|
|
2262
|
+
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
2263
|
+
)
|
|
2264
|
+
];
|
|
2265
|
+
return {
|
|
2266
|
+
graph: multi.merged,
|
|
2267
|
+
multiLayer: multi,
|
|
2268
|
+
layerMetadata: multi.layerMetadata,
|
|
2269
|
+
crossLayerEdges: merged
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
const graph = await analyzeProject(opts.targetDir, {
|
|
2273
|
+
exclude: opts.exclude,
|
|
2274
|
+
language: opts.language
|
|
2275
|
+
});
|
|
2276
|
+
return { graph };
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/storage/snapshot.ts
|
|
2280
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, access } from "fs/promises";
|
|
2281
|
+
import { join as join6 } from "path";
|
|
2282
|
+
import { z as z2 } from "zod";
|
|
2182
2283
|
|
|
2183
2284
|
// src/types/schema.ts
|
|
2184
2285
|
var SCHEMA_VERSION = "1.1";
|
|
2185
2286
|
|
|
2186
2287
|
// src/storage/snapshot.ts
|
|
2187
|
-
var
|
|
2288
|
+
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
2188
2289
|
var SNAPSHOT_FILE = "snapshot.json";
|
|
2189
|
-
var FileNodeSchema =
|
|
2190
|
-
path:
|
|
2191
|
-
exists:
|
|
2192
|
-
dependencies:
|
|
2193
|
-
dependents:
|
|
2290
|
+
var FileNodeSchema = z2.object({
|
|
2291
|
+
path: z2.string(),
|
|
2292
|
+
exists: z2.boolean(),
|
|
2293
|
+
dependencies: z2.array(z2.string()),
|
|
2294
|
+
dependents: z2.array(z2.string())
|
|
2194
2295
|
});
|
|
2195
|
-
var DependencyGraphSchema =
|
|
2196
|
-
rootDir:
|
|
2197
|
-
files:
|
|
2198
|
-
edges:
|
|
2199
|
-
source:
|
|
2200
|
-
target:
|
|
2201
|
-
type:
|
|
2296
|
+
var DependencyGraphSchema = z2.object({
|
|
2297
|
+
rootDir: z2.string(),
|
|
2298
|
+
files: z2.record(z2.string(), FileNodeSchema),
|
|
2299
|
+
edges: z2.array(z2.object({
|
|
2300
|
+
source: z2.string(),
|
|
2301
|
+
target: z2.string(),
|
|
2302
|
+
type: z2.enum(["static", "dynamic", "type-only"])
|
|
2202
2303
|
})),
|
|
2203
|
-
circularDependencies:
|
|
2204
|
-
totalFiles:
|
|
2205
|
-
totalEdges:
|
|
2304
|
+
circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
|
|
2305
|
+
totalFiles: z2.number(),
|
|
2306
|
+
totalEdges: z2.number()
|
|
2206
2307
|
});
|
|
2207
|
-
var SnapshotSchema =
|
|
2208
|
-
version:
|
|
2209
|
-
timestamp:
|
|
2210
|
-
rootDir:
|
|
2308
|
+
var SnapshotSchema = z2.object({
|
|
2309
|
+
version: z2.enum([SCHEMA_VERSION, "1.0"]),
|
|
2310
|
+
timestamp: z2.string(),
|
|
2311
|
+
rootDir: z2.string(),
|
|
2211
2312
|
graph: DependencyGraphSchema
|
|
2212
2313
|
});
|
|
2213
2314
|
async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
2214
|
-
const dirPath =
|
|
2215
|
-
const filePath =
|
|
2315
|
+
const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
|
|
2316
|
+
const filePath = join6(dirPath, SNAPSHOT_FILE);
|
|
2216
2317
|
const snapshot = {
|
|
2217
2318
|
version: SCHEMA_VERSION,
|
|
2218
2319
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2220,17 +2321,17 @@ async function saveSnapshot(projectRoot, graph, multiLayer) {
|
|
|
2220
2321
|
graph,
|
|
2221
2322
|
...multiLayer ? { multiLayer } : {}
|
|
2222
2323
|
};
|
|
2223
|
-
await
|
|
2224
|
-
await
|
|
2324
|
+
await mkdir2(dirPath, { recursive: true });
|
|
2325
|
+
await writeFile2(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
2225
2326
|
return snapshot;
|
|
2226
2327
|
}
|
|
2227
2328
|
async function loadSnapshot(projectRoot) {
|
|
2228
|
-
const filePath =
|
|
2329
|
+
const filePath = join6(projectRoot, ARCHTRACKER_DIR2, SNAPSHOT_FILE);
|
|
2229
2330
|
let raw;
|
|
2230
2331
|
try {
|
|
2231
|
-
raw = await
|
|
2332
|
+
raw = await readFile3(filePath, "utf-8");
|
|
2232
2333
|
} catch (error) {
|
|
2233
|
-
if (
|
|
2334
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2234
2335
|
return null;
|
|
2235
2336
|
}
|
|
2236
2337
|
throw new StorageError(
|
|
@@ -2261,7 +2362,7 @@ var StorageError = class extends Error {
|
|
|
2261
2362
|
this.name = "StorageError";
|
|
2262
2363
|
}
|
|
2263
2364
|
};
|
|
2264
|
-
function
|
|
2365
|
+
function isNodeError2(error) {
|
|
2265
2366
|
return error instanceof Error && "code" in error;
|
|
2266
2367
|
}
|
|
2267
2368
|
|
|
@@ -2362,121 +2463,12 @@ function arraysEqual(a, b) {
|
|
|
2362
2463
|
return true;
|
|
2363
2464
|
}
|
|
2364
2465
|
|
|
2365
|
-
// src/storage/layers.ts
|
|
2366
|
-
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
2367
|
-
import { join as join6 } from "path";
|
|
2368
|
-
import { z as z2 } from "zod";
|
|
2369
|
-
var ARCHTRACKER_DIR2 = ".archtracker";
|
|
2370
|
-
var LAYERS_FILE = "layers.json";
|
|
2371
|
-
var LayerDefinitionSchema = z2.object({
|
|
2372
|
-
name: z2.string().min(1).regex(
|
|
2373
|
-
/^[a-zA-Z0-9_-]+$/,
|
|
2374
|
-
"Layer name must be alphanumeric (hyphens/underscores allowed)"
|
|
2375
|
-
),
|
|
2376
|
-
targetDir: z2.string().min(1),
|
|
2377
|
-
language: z2.enum(LANGUAGE_IDS).optional(),
|
|
2378
|
-
exclude: z2.array(z2.string()).optional(),
|
|
2379
|
-
color: z2.string().optional(),
|
|
2380
|
-
description: z2.string().optional()
|
|
2381
|
-
});
|
|
2382
|
-
var CrossLayerConnectionSchema = z2.object({
|
|
2383
|
-
fromLayer: z2.string(),
|
|
2384
|
-
fromFile: z2.string(),
|
|
2385
|
-
toLayer: z2.string(),
|
|
2386
|
-
toFile: z2.string(),
|
|
2387
|
-
type: z2.enum(["api-call", "event", "data-flow", "manual"]),
|
|
2388
|
-
label: z2.string().optional()
|
|
2389
|
-
});
|
|
2390
|
-
var LayerConfigSchema = z2.object({
|
|
2391
|
-
version: z2.literal("1.0"),
|
|
2392
|
-
layers: z2.array(LayerDefinitionSchema).min(1).refine(
|
|
2393
|
-
(layers) => {
|
|
2394
|
-
const names = layers.map((l) => l.name);
|
|
2395
|
-
return new Set(names).size === names.length;
|
|
2396
|
-
},
|
|
2397
|
-
{ message: "Layer names must be unique" }
|
|
2398
|
-
),
|
|
2399
|
-
connections: z2.array(CrossLayerConnectionSchema).optional()
|
|
2400
|
-
});
|
|
2401
|
-
async function loadLayerConfig(projectRoot) {
|
|
2402
|
-
const filePath = join6(projectRoot, ARCHTRACKER_DIR2, LAYERS_FILE);
|
|
2403
|
-
let raw;
|
|
2404
|
-
try {
|
|
2405
|
-
raw = await readFile3(filePath, "utf-8");
|
|
2406
|
-
} catch (error) {
|
|
2407
|
-
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2408
|
-
return null;
|
|
2409
|
-
}
|
|
2410
|
-
throw new Error(`Failed to read ${filePath}`);
|
|
2411
|
-
}
|
|
2412
|
-
let parsed;
|
|
2413
|
-
try {
|
|
2414
|
-
parsed = JSON.parse(raw);
|
|
2415
|
-
} catch {
|
|
2416
|
-
throw new Error(`Invalid JSON in ${filePath}`);
|
|
2417
|
-
}
|
|
2418
|
-
const result = LayerConfigSchema.safeParse(parsed);
|
|
2419
|
-
if (!result.success) {
|
|
2420
|
-
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
|
|
2421
|
-
throw new Error(`layers.json validation failed:
|
|
2422
|
-
${issues}`);
|
|
2423
|
-
}
|
|
2424
|
-
return result.data;
|
|
2425
|
-
}
|
|
2426
|
-
async function saveLayerConfig(projectRoot, config) {
|
|
2427
|
-
const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
|
|
2428
|
-
const filePath = join6(dirPath, LAYERS_FILE);
|
|
2429
|
-
await mkdir2(dirPath, { recursive: true });
|
|
2430
|
-
await writeFile2(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2431
|
-
}
|
|
2432
|
-
function isNodeError2(error) {
|
|
2433
|
-
return error instanceof Error && "code" in error;
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
2466
|
// src/web/server.ts
|
|
2437
2467
|
import { createServer } from "http";
|
|
2438
2468
|
|
|
2439
|
-
// src/web/
|
|
2440
|
-
function
|
|
2441
|
-
|
|
2442
|
-
const diff = options.diff ?? null;
|
|
2443
|
-
const layers = options.layerMetadata ?? null;
|
|
2444
|
-
const crossEdges = options.crossLayerEdges ?? null;
|
|
2445
|
-
const files = Object.values(graph.files);
|
|
2446
|
-
const nodes = files.map((f) => ({
|
|
2447
|
-
id: f.path,
|
|
2448
|
-
deps: f.dependencies.length,
|
|
2449
|
-
dependents: f.dependents.length,
|
|
2450
|
-
dependencies: f.dependencies,
|
|
2451
|
-
dependentsList: f.dependents,
|
|
2452
|
-
isOrphan: f.dependencies.length === 0 && f.dependents.length === 0,
|
|
2453
|
-
dir: f.path.includes("/") ? f.path.substring(0, f.path.lastIndexOf("/")) : ".",
|
|
2454
|
-
layer: layers && f.path.includes("/") ? f.path.substring(0, f.path.indexOf("/")) : null
|
|
2455
|
-
}));
|
|
2456
|
-
const links = graph.edges.map((e) => ({
|
|
2457
|
-
source: e.source,
|
|
2458
|
-
target: e.target,
|
|
2459
|
-
type: e.type
|
|
2460
|
-
}));
|
|
2461
|
-
const circularFiles = /* @__PURE__ */ new Set();
|
|
2462
|
-
for (const c of graph.circularDependencies) {
|
|
2463
|
-
for (const f of c.cycle) circularFiles.add(f);
|
|
2464
|
-
}
|
|
2465
|
-
const dirs = [...new Set(nodes.map((n) => n.dir))].sort();
|
|
2466
|
-
const projectName = graph.rootDir.split("/").filter(Boolean).pop() || "Project";
|
|
2467
|
-
const diffData = diff ? JSON.stringify(diff) : "null";
|
|
2468
|
-
const layersData = layers ? JSON.stringify(layers) : "null";
|
|
2469
|
-
const crossEdgesData = crossEdges ? JSON.stringify(crossEdges) : "null";
|
|
2470
|
-
const graphData = JSON.stringify({ nodes, links, circularFiles: [...circularFiles], dirs, projectName });
|
|
2471
|
-
return (
|
|
2472
|
-
/* html */
|
|
2473
|
-
`<!DOCTYPE html>
|
|
2474
|
-
<html lang="${locale}">
|
|
2475
|
-
<head>
|
|
2476
|
-
<meta charset="utf-8">
|
|
2477
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2478
|
-
<title>${projectName} \u2014 Architecture Viewer</title>
|
|
2479
|
-
<style>
|
|
2469
|
+
// src/web/styles.ts
|
|
2470
|
+
function buildStyles() {
|
|
2471
|
+
return `<style>
|
|
2480
2472
|
:root {
|
|
2481
2473
|
--bg: #0d1117; --bg-card: #161b22; --bg-hover: #1c2129;
|
|
2482
2474
|
--border: #30363d; --border-active: #58a6ff;
|
|
@@ -2606,6 +2598,9 @@ kbd { background: #21262d; border: 1px solid var(--border); border-radius: 3px;
|
|
|
2606
2598
|
#impact-btn.active { background: var(--accent) !important; color: #fff !important; border-color: var(--accent) !important; }
|
|
2607
2599
|
#impact-badge { position: absolute; bottom: 52px; left: 12px; z-index: 10; display: none; background: var(--accent); color: #fff; font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: var(--radius); }
|
|
2608
2600
|
|
|
2601
|
+
/* \u2500\u2500\u2500 Diff focus button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2602
|
+
#diff-focus-btn.active { background: var(--accent) !important; color: #fff !important; border-color: var(--accent) !important; }
|
|
2603
|
+
|
|
2609
2604
|
/* \u2500\u2500\u2500 Help bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2610
2605
|
#help-bar { position: absolute; bottom: 12px; right: 12px; z-index: 10; font-size: 11px; color: var(--text-muted); background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 10px; transition: background 0.3s; }
|
|
2611
2606
|
|
|
@@ -2626,10 +2621,12 @@ kbd { background: #21262d; border: 1px solid var(--border); border-radius: 3px;
|
|
|
2626
2621
|
.layer-pill.active { border-color: var(--accent); }
|
|
2627
2622
|
.layer-pill .lp-dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
2628
2623
|
.layer-pill .lp-count { color: var(--text-muted); font-size: 9px; font-weight: 400; }
|
|
2629
|
-
</style
|
|
2630
|
-
|
|
2631
|
-
<body>
|
|
2624
|
+
</style>`;
|
|
2625
|
+
}
|
|
2632
2626
|
|
|
2627
|
+
// src/web/viewer-html.ts
|
|
2628
|
+
function buildViewerHtml() {
|
|
2629
|
+
return `
|
|
2633
2630
|
<!-- Tab bar -->
|
|
2634
2631
|
<div id="tab-bar">
|
|
2635
2632
|
<span class="logo" id="project-title" contenteditable="true" spellcheck="false" title="Click to edit project name"></span>
|
|
@@ -2735,7 +2732,7 @@ kbd { background: #21262d; border: 1px solid var(--border); border-radius: 3px;
|
|
|
2735
2732
|
<div id="zoom-ctrl">
|
|
2736
2733
|
<button onclick="zoomIn()" title="Zoom in">+</button>
|
|
2737
2734
|
<button onclick="zoomOut()" title="Zoom out">\u2212</button>
|
|
2738
|
-
<button onclick="zoomFit()" title="Fit">\
|
|
2735
|
+
<button onclick="zoomFit()" title="Fit">\u229E</button>
|
|
2739
2736
|
<button id="impact-btn" onclick="toggleImpactMode()" title="Impact simulation" style="font-size:12px;margin-top:4px" data-i18n="impact.btn">Impact</button>
|
|
2740
2737
|
</div>
|
|
2741
2738
|
<div id="impact-badge"></div>
|
|
@@ -2768,15 +2765,26 @@ kbd { background: #21262d; border: 1px solid var(--border); border-radius: 3px;
|
|
|
2768
2765
|
<!-- Diff View -->
|
|
2769
2766
|
<div id="diff-view" class="view">
|
|
2770
2767
|
<svg id="diff-svg"></svg>
|
|
2771
|
-
<div id="diff-
|
|
2768
|
+
<div id="diff-hud" style="position:absolute;top:12px;left:12px;z-index:10;display:flex;flex-direction:column;gap:8px;">
|
|
2772
2769
|
<div class="hud-panel">
|
|
2773
|
-
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> <span data-i18n="diff.addedLabel">Added</span></div>
|
|
2774
|
-
<div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div> <span data-i18n="diff.removedLabel">Removed</span></div>
|
|
2775
|
-
<div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div> <span data-i18n="diff.modifiedLabel">Modified</span></div>
|
|
2776
|
-
<div class="legend-item"><div class="legend-dot" style="background:var(--accent)"></div> <span data-i18n="diff.affectedLabel">Affected</span></div>
|
|
2770
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> <span data-i18n="diff.addedLabel">Added</span> <b id="diff-added-count" style="margin-left:auto">0</b></div>
|
|
2771
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div> <span data-i18n="diff.removedLabel">Removed</span> <b id="diff-removed-count" style="margin-left:auto">0</b></div>
|
|
2772
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div> <span data-i18n="diff.modifiedLabel">Modified</span> <b id="diff-modified-count" style="margin-left:auto">0</b></div>
|
|
2773
|
+
<div class="legend-item"><div class="legend-dot" style="background:var(--accent)"></div> <span data-i18n="diff.affectedLabel">Affected</span> <b id="diff-affected-count" style="margin-left:auto">0</b></div>
|
|
2774
|
+
<div style="margin-top:6px;border-top:1px solid var(--border);padding-top:6px;">
|
|
2775
|
+
<button id="diff-focus-btn" onclick="toggleDiffFocus()" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px 10px;cursor:pointer;color:var(--text-dim);font-size:11px;width:100%;transition:all 0.15s;" data-i18n="diff.focusChanges">Focus changes</button>
|
|
2776
|
+
</div>
|
|
2777
2777
|
</div>
|
|
2778
2778
|
</div>
|
|
2779
|
-
<div id="
|
|
2779
|
+
<div id="diff-detail" style="position:absolute;top:12px;right:12px;width:280px;z-index:10;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;font-size:13px;display:none;max-height:calc(100vh - 100px);overflow-y:auto;transition:background 0.3s;">
|
|
2780
|
+
<button class="close-btn" onclick="closeDiffDetail()" style="position:absolute;top:8px;right:10px;background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;">\u2715</button>
|
|
2781
|
+
<div class="detail-name" id="dd-name"></div>
|
|
2782
|
+
<div id="dd-status" style="margin:6px 0;font-size:12px;font-weight:600;"></div>
|
|
2783
|
+
<div class="detail-meta" id="dd-meta"></div>
|
|
2784
|
+
<div class="detail-section"><h4 data-i18n="diff.affectedByChange">Affected by this change</h4><ul class="detail-list" id="dd-affected"></ul></div>
|
|
2785
|
+
<div class="detail-section"><h4 data-i18n="detail.imports">Imports</h4><ul class="detail-list" id="dd-deps"></ul></div>
|
|
2786
|
+
</div>
|
|
2787
|
+
<div id="help-bar" style="position:absolute" data-i18n="help.diff">Green=added \xB7 Red=removed \xB7 Yellow=modified \xB7 Blue=affected \xB7 Click: impact chain</div>
|
|
2780
2788
|
</div>
|
|
2781
2789
|
|
|
2782
2790
|
<!-- Tooltip (shared, interactive) -->
|
|
@@ -2787,1441 +2795,1692 @@ kbd { background: #21262d; border: 1px solid var(--border); border-radius: 3px;
|
|
|
2787
2795
|
<span class="tt-badge tt-in" id="tt-dpt-count" style="margin-left:6px"></span> <span data-i18n="tooltip.importedBy">imported by</span>
|
|
2788
2796
|
</div>
|
|
2789
2797
|
<div class="tt-section" id="tt-details"></div>
|
|
2790
|
-
</div
|
|
2791
|
-
|
|
2792
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2793
|
-
<script>
|
|
2794
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2795
|
-
// i18n
|
|
2796
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2797
|
-
const I18N = {
|
|
2798
|
-
en: {
|
|
2799
|
-
'tab.graph': 'Graph', 'tab.hierarchy': 'Hierarchy',
|
|
2800
|
-
'stats.files': 'Files', 'stats.edges': 'Edges', 'stats.circular': 'Circular',
|
|
2801
|
-
'settings.title': 'Settings', 'settings.theme': 'Theme', 'settings.fontSize': 'Font Size',
|
|
2802
|
-
'settings.nodeSize': 'Node Size', 'settings.linkOpacity': 'Link Opacity', 'settings.gravity': 'Gravity', 'settings.language': 'Language', 'settings.export': 'Export',
|
|
2803
|
-
'impact.title': 'Impact Simulation', 'impact.btn': 'Impact', 'impact.transitive': 'files affected',
|
|
2804
|
-
'search.placeholder': 'Search files...',
|
|
2805
|
-
'legend.circular': 'Circular dep', 'legend.orphan': 'Orphan', 'legend.highCoupling': 'High coupling',
|
|
2806
|
-
'legend.imports': 'imports', 'legend.importedBy': 'imported by',
|
|
2807
|
-
'detail.importedBy': 'Imported by', 'detail.imports': 'Imports',
|
|
2808
|
-
'detail.none': 'none', 'detail.dir': 'Dir', 'detail.dependencies': 'Dependencies', 'detail.dependents': 'Dependents',
|
|
2809
|
-
'tooltip.imports': 'imports', 'tooltip.importedBy': 'imported by',
|
|
2810
|
-
'help.graph': 'Scroll: zoom \xB7 Drag: pan \xB7 Click: select \xB7 / search',
|
|
2811
|
-
'help.hierarchy': 'Scroll to navigate \xB7 Click to highlight',
|
|
2812
|
-
'help.diff': 'Green=added \xB7 Red=removed \xB7 Yellow=modified \xB7 Blue=affected',
|
|
2813
|
-
'tab.diff': 'Diff',
|
|
2814
|
-
'diff.addedLabel': 'Added', 'diff.removedLabel': 'Removed', 'diff.modifiedLabel': 'Modified', 'diff.affectedLabel': 'Affected',
|
|
2815
|
-
},
|
|
2816
|
-
ja: {
|
|
2817
|
-
'tab.graph': '\u30B0\u30E9\u30D5', 'tab.hierarchy': '\u968E\u5C64\u56F3',
|
|
2818
|
-
'stats.files': '\u30D5\u30A1\u30A4\u30EB', 'stats.edges': '\u30A8\u30C3\u30B8', 'stats.circular': '\u5FAA\u74B0\u53C2\u7167',
|
|
2819
|
-
'settings.title': '\u8A2D\u5B9A', 'settings.theme': '\u30C6\u30FC\u30DE', 'settings.fontSize': '\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA',
|
|
2820
|
-
'settings.nodeSize': '\u30CE\u30FC\u30C9\u30B5\u30A4\u30BA', 'settings.linkOpacity': '\u30EA\u30F3\u30AF\u900F\u660E\u5EA6', 'settings.gravity': '\u91CD\u529B', 'settings.language': '\u8A00\u8A9E', 'settings.export': '\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8',
|
|
2821
|
-
'impact.title': '\u5F71\u97FF\u7BC4\u56F2\u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3', 'impact.btn': '\u5F71\u97FF', 'impact.transitive': '\u30D5\u30A1\u30A4\u30EB\u306B\u5F71\u97FF',
|
|
2822
|
-
'search.placeholder': '\u30D5\u30A1\u30A4\u30EB\u691C\u7D22...',
|
|
2823
|
-
'legend.circular': '\u5FAA\u74B0\u53C2\u7167', 'legend.orphan': '\u5B64\u7ACB', 'legend.highCoupling': '\u9AD8\u7D50\u5408',
|
|
2824
|
-
'legend.imports': 'import\u5148', 'legend.importedBy': 'import\u5143',
|
|
2825
|
-
'detail.importedBy': 'import\u5143', 'detail.imports': 'import\u5148',
|
|
2826
|
-
'detail.none': '\u306A\u3057', 'detail.dir': '\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA', 'detail.dependencies': '\u4F9D\u5B58\u5148', 'detail.dependents': '\u88AB\u4F9D\u5B58',
|
|
2827
|
-
'tooltip.imports': 'import\u5148', 'tooltip.importedBy': 'import\u5143',
|
|
2828
|
-
'help.graph': '\u30B9\u30AF\u30ED\u30FC\u30EB: \u30BA\u30FC\u30E0 \xB7 \u30C9\u30E9\u30C3\u30B0: \u79FB\u52D5 \xB7 \u30AF\u30EA\u30C3\u30AF: \u9078\u629E \xB7 / \u691C\u7D22',
|
|
2829
|
-
'help.hierarchy': '\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u79FB\u52D5 \xB7 \u30AF\u30EA\u30C3\u30AF\u3067\u30CF\u30A4\u30E9\u30A4\u30C8',
|
|
2830
|
-
'help.diff': '\u7DD1=\u8FFD\u52A0 \xB7 \u8D64=\u524A\u9664 \xB7 \u9EC4=\u5909\u66F4 \xB7 \u9752=\u5F71\u97FF',
|
|
2831
|
-
'tab.diff': '\u5DEE\u5206',
|
|
2832
|
-
'diff.addedLabel': '\u8FFD\u52A0', 'diff.removedLabel': '\u524A\u9664', 'diff.modifiedLabel': '\u5909\u66F4', 'diff.affectedLabel': '\u5F71\u97FF',
|
|
2833
|
-
}
|
|
2834
|
-
};
|
|
2835
|
-
let currentLang = '${locale}';
|
|
2836
|
-
function applyI18n() {
|
|
2837
|
-
const msgs = I18N[currentLang] || I18N.en;
|
|
2838
|
-
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
2839
|
-
const key = el.getAttribute('data-i18n');
|
|
2840
|
-
if (msgs[key]) el.textContent = msgs[key];
|
|
2841
|
-
});
|
|
2842
|
-
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
2843
|
-
const key = el.getAttribute('data-i18n-placeholder');
|
|
2844
|
-
if (msgs[key]) el.placeholder = msgs[key];
|
|
2845
|
-
});
|
|
2846
|
-
document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.dataset.lang === currentLang));
|
|
2798
|
+
</div>`;
|
|
2847
2799
|
}
|
|
2848
|
-
window.setLang = (lang) => { currentLang = lang; applyI18n(); saveSettings(); };
|
|
2849
|
-
function i(key) { return (I18N[currentLang] || I18N.en)[key] || key; }
|
|
2850
2800
|
|
|
2801
|
+
// src/web/js-hierarchy.ts
|
|
2802
|
+
function buildHierarchyJs() {
|
|
2803
|
+
return `
|
|
2851
2804
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2852
|
-
//
|
|
2805
|
+
// HIERARCHY VIEW
|
|
2853
2806
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
function loadSettings() {
|
|
2860
|
-
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || null; } catch(e) { return null; }
|
|
2861
|
-
}
|
|
2807
|
+
function buildHierarchy(){
|
|
2808
|
+
const hSvg=d3.select('#hier-svg');
|
|
2809
|
+
const hG=hSvg.append('g');
|
|
2810
|
+
const hZoom=d3.zoom().scaleExtent([0.1,4]).on('zoom',e=>hG.attr('transform',e.transform));
|
|
2811
|
+
hSvg.call(hZoom);
|
|
2862
2812
|
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
};
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
saveSettings();
|
|
2877
|
-
};
|
|
2878
|
-
window.setNodeScale = (v) => {
|
|
2879
|
-
nodeScale = v / 100;
|
|
2880
|
-
document.getElementById('node-size-val').textContent = v;
|
|
2881
|
-
if (typeof node !== 'undefined') {
|
|
2882
|
-
node.select('circle').attr('r', d => nodeRadius(d) * nodeScale);
|
|
2883
|
-
node.select('text').attr('dx', d => nodeRadius(d) * nodeScale + 4);
|
|
2884
|
-
simulation.force('collision', d3.forceCollide().radius(d => nodeRadius(d) * nodeScale + 4));
|
|
2885
|
-
simulation.alpha(0.3).restart();
|
|
2886
|
-
}
|
|
2887
|
-
saveSettings();
|
|
2888
|
-
};
|
|
2889
|
-
window.setLinkOpacity = (v) => {
|
|
2890
|
-
baseLinkOpacity = v / 100;
|
|
2891
|
-
document.getElementById('link-opacity-val').textContent = v;
|
|
2892
|
-
if (typeof link !== 'undefined') link.attr('opacity', baseLinkOpacity);
|
|
2893
|
-
saveSettings();
|
|
2894
|
-
};
|
|
2895
|
-
let gravityStrength = 150;
|
|
2896
|
-
window.setGravity = (v) => {
|
|
2897
|
-
gravityStrength = +v;
|
|
2898
|
-
document.getElementById('gravity-val').textContent = v;
|
|
2899
|
-
if (typeof simulation !== 'undefined') {
|
|
2900
|
-
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500));
|
|
2901
|
-
simulation.alpha(0.5).restart();
|
|
2813
|
+
const nodeMap={}; DATA.nodes.forEach(n=>nodeMap[n.id]=n);
|
|
2814
|
+
const importsMap={}; DATA.links.forEach(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(!importsMap[s])importsMap[s]=[];importsMap[s].push(t);});
|
|
2815
|
+
|
|
2816
|
+
const entryPoints=DATA.nodes.filter(n=>n.dependents===0).map(n=>n.id);
|
|
2817
|
+
const layers={};const visited=new Set();
|
|
2818
|
+
const queue=entryPoints.map(id=>({id,layer:0}));
|
|
2819
|
+
DATA.nodes.forEach(n=>{if(n.isOrphan)layers[n.id]=0;});
|
|
2820
|
+
|
|
2821
|
+
while(queue.length>0){
|
|
2822
|
+
const{id,layer}=queue.shift();
|
|
2823
|
+
if(visited.has(id)&&(layers[id]??-1)>=layer)continue;
|
|
2824
|
+
layers[id]=Math.max(layers[id]??0,layer);visited.add(id);
|
|
2825
|
+
(importsMap[id]||[]).forEach(t=>queue.push({id:t,layer:layer+1}));
|
|
2902
2826
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2827
|
+
DATA.nodes.forEach(n=>{if(!(n.id in layers))layers[n.id]=0;});
|
|
2828
|
+
|
|
2829
|
+
const maxLayer=Math.max(0,...Object.values(layers));
|
|
2830
|
+
const layerGroups={};
|
|
2831
|
+
for(let i=0;i<=maxLayer;i++)layerGroups[i]=[];
|
|
2832
|
+
Object.entries(layers).forEach(([id,l])=>layerGroups[l].push(id));
|
|
2833
|
+
Object.values(layerGroups).forEach(arr=>arr.sort((a,b)=>(nodeMap[a]?.dir||'').localeCompare(nodeMap[b]?.dir||'')||a.localeCompare(b)));
|
|
2834
|
+
|
|
2835
|
+
const boxW=200,boxH=30,gapX=24,gapY=70,padY=60,padX=40;
|
|
2836
|
+
const positions={};let maxRowWidth=0;
|
|
2837
|
+
for(let layer=0;layer<=maxLayer;layer++){const items=layerGroups[layer];maxRowWidth=Math.max(maxRowWidth,items.length*(boxW+gapX)-gapX);}
|
|
2838
|
+
for(let layer=0;layer<=maxLayer;layer++){
|
|
2839
|
+
const items=layerGroups[layer],rowWidth=items.length*(boxW+gapX)-gapX,startX=padX+(maxRowWidth-rowWidth)/2;
|
|
2840
|
+
items.forEach((id,i)=>{positions[id]={x:startX+i*(boxW+gapX),y:padY+layer*(boxH+gapY)};});
|
|
2911
2841
|
}
|
|
2912
|
-
saveSettings();
|
|
2913
|
-
};
|
|
2914
2842
|
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2918
|
-
window.exportSVG = () => {
|
|
2919
|
-
const activeView = document.querySelector('.view.active svg');
|
|
2920
|
-
if (!activeView) return;
|
|
2921
|
-
const clone = activeView.cloneNode(true);
|
|
2922
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
2923
|
-
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
|
|
2924
|
-
const a = document.createElement('a');
|
|
2925
|
-
a.href = URL.createObjectURL(blob);
|
|
2926
|
-
a.download = (document.getElementById('project-title').textContent || 'graph') + '.svg';
|
|
2927
|
-
a.click(); URL.revokeObjectURL(a.href);
|
|
2928
|
-
};
|
|
2929
|
-
window.exportPNG = () => {
|
|
2930
|
-
const activeView = document.querySelector('.view.active svg');
|
|
2931
|
-
if (!activeView) return;
|
|
2932
|
-
const clone = activeView.cloneNode(true);
|
|
2933
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
2934
|
-
const svgStr = new XMLSerializer().serializeToString(clone);
|
|
2935
|
-
const canvas = document.createElement('canvas');
|
|
2936
|
-
const bbox = activeView.getBoundingClientRect();
|
|
2937
|
-
canvas.width = bbox.width * 2; canvas.height = bbox.height * 2;
|
|
2938
|
-
const ctx = canvas.getContext('2d');
|
|
2939
|
-
ctx.scale(2, 2);
|
|
2940
|
-
const img = new Image();
|
|
2941
|
-
img.onload = () => { ctx.drawImage(img, 0, 0); const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = (document.getElementById('project-title').textContent || 'graph') + '.png'; a.click(); };
|
|
2942
|
-
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));
|
|
2943
|
-
};
|
|
2843
|
+
const totalW=maxRowWidth+padX*2,totalH=padY*2+(maxLayer+1)*(boxH+gapY);
|
|
2844
|
+
hSvg.attr('width',Math.max(totalW,W)).attr('height',Math.max(totalH,H));
|
|
2944
2845
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
const
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2846
|
+
const linkG=hG.append('g');
|
|
2847
|
+
DATA.links.forEach(l=>{
|
|
2848
|
+
const sId=l.source.id??l.source,tId=l.target.id??l.target;
|
|
2849
|
+
const s=positions[sId],t=positions[tId]; if(!s||!t)return;
|
|
2850
|
+
const x1=s.x+boxW/2,y1=s.y+boxH,x2=t.x+boxW/2,y2=t.y,midY=(y1+y2)/2;
|
|
2851
|
+
linkG.append('path').attr('class','hier-link')
|
|
2852
|
+
.attr('d',\`M\${x1},\${y1} C\${x1},\${midY} \${x2},\${midY} \${x2},\${y2}\`)
|
|
2853
|
+
.attr('stroke',l.type==='type-only'?'#1f3d5c':'var(--border)')
|
|
2854
|
+
.attr('stroke-dasharray',l.type==='type-only'?'4,3':null)
|
|
2855
|
+
.attr('data-source',sId).attr('data-target',tId);
|
|
2856
|
+
});
|
|
2953
2857
|
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
titleEl.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } });
|
|
2858
|
+
hSvg.append('defs').append('marker').attr('id','harrow').attr('viewBox','0 -3 6 6')
|
|
2859
|
+
.attr('refX',6).attr('refY',0).attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
|
|
2860
|
+
.append('path').attr('d','M0,-3L6,0L0,3Z').attr('fill','var(--border)');
|
|
2861
|
+
linkG.selectAll('path').attr('marker-end','url(#harrow)');
|
|
2959
2862
|
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
// Set slider positions (visual only \u2014 graph not built yet)
|
|
2967
|
-
if (_savedSettings.fontSize) { document.getElementById('font-size-slider').value = _savedSettings.fontSize; document.getElementById('font-size-val').textContent = _savedSettings.fontSize; }
|
|
2968
|
-
if (_savedSettings.nodeSize) { document.getElementById('node-size-slider').value = _savedSettings.nodeSize; document.getElementById('node-size-val').textContent = _savedSettings.nodeSize; nodeScale = _savedSettings.nodeSize / 100; }
|
|
2969
|
-
if (_savedSettings.linkOpacity) { document.getElementById('link-opacity-slider').value = _savedSettings.linkOpacity; document.getElementById('link-opacity-val').textContent = _savedSettings.linkOpacity; baseLinkOpacity = _savedSettings.linkOpacity / 100; }
|
|
2970
|
-
if (_savedSettings.gravity) { document.getElementById('gravity-slider').value = _savedSettings.gravity; document.getElementById('gravity-val').textContent = _savedSettings.gravity; gravityStrength = +_savedSettings.gravity; }
|
|
2971
|
-
if (_savedSettings.layerGravity) { document.getElementById('layer-gravity-slider').value = _savedSettings.layerGravity; document.getElementById('layer-gravity-val').textContent = _savedSettings.layerGravity; layerGravity = +_savedSettings.layerGravity; }
|
|
2972
|
-
}
|
|
2863
|
+
for(let layer=0;layer<=maxLayer;layer++){
|
|
2864
|
+
if(!layerGroups[layer].length)continue;
|
|
2865
|
+
hG.append('text').attr('class','hier-layer-label').attr('font-size',11)
|
|
2866
|
+
.attr('data-depth-idx',layer)
|
|
2867
|
+
.attr('x',12).attr('y',padY+layer*(boxH+gapY)+boxH/2+4).text('L'+layer);
|
|
2868
|
+
}
|
|
2973
2869
|
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2870
|
+
const nodeG=hG.append('g');
|
|
2871
|
+
DATA.nodes.forEach(n=>{
|
|
2872
|
+
const pos=positions[n.id]; if(!pos)return;
|
|
2873
|
+
const gn=nodeG.append('g').attr('class','hier-node').attr('transform',\`translate(\${pos.x},\${pos.y})\`);
|
|
2874
|
+
gn.append('rect').attr('width',boxW).attr('height',boxH)
|
|
2875
|
+
.attr('fill','var(--bg-card)').attr('stroke',nodeColor(n))
|
|
2876
|
+
.attr('stroke-width',circularSet.has(n.id)?2:1.5);
|
|
2877
|
+
gn.append('text').attr('x',8).attr('y',boxH/2+4).attr('font-size',11)
|
|
2878
|
+
.text(fileName(n.id).length>24?fileName(n.id).slice(0,22)+'\\u2026':fileName(n.id));
|
|
2879
|
+
gn.append('text').attr('x',boxW-8).attr('y',boxH/2+4)
|
|
2880
|
+
.attr('text-anchor','end').attr('font-size',10).attr('fill','var(--text-muted)')
|
|
2881
|
+
.text(n.dependents>0?'\\u2191'+n.dependents:'');
|
|
2882
|
+
gn.append('text').attr('x',8).attr('y',-4).attr('font-size',9)
|
|
2883
|
+
.attr('fill',dirColor(n.dir)).attr('opacity',0.7).text(n.dir);
|
|
2977
2884
|
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2885
|
+
gn.node().__data_id=n.id;
|
|
2886
|
+
gn.on('mouseover',e=>{
|
|
2887
|
+
showTooltip(e,n);
|
|
2888
|
+
if (!hierPinned) hierHighlight(n.id);
|
|
2889
|
+
})
|
|
2890
|
+
.on('mousemove',e=>positionTooltip(e))
|
|
2891
|
+
.on('mouseout',()=>{
|
|
2892
|
+
scheduleHideTooltip();
|
|
2893
|
+
if (!hierPinned) hierResetHighlight();
|
|
2894
|
+
})
|
|
2895
|
+
.on('click',(e)=>{
|
|
2896
|
+
e.stopPropagation();
|
|
2897
|
+
hierPinned=n.id;
|
|
2898
|
+
hierHighlight(n.id);
|
|
2899
|
+
showHierDetail(n);
|
|
2900
|
+
});
|
|
2901
|
+
});
|
|
2981
2902
|
|
|
2982
|
-
//
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
if (
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2903
|
+
// Hierarchy highlight helpers
|
|
2904
|
+
let hierPinned=null;
|
|
2905
|
+
function hierHighlight(nId){
|
|
2906
|
+
linkG.selectAll('path')
|
|
2907
|
+
.attr('stroke',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');if(s===nId)return'#58a6ff';if(t===nId)return'#3fb950';return this.getAttribute('stroke-dasharray')?'#1f3d5c':'var(--border)';})
|
|
2908
|
+
.attr('stroke-width',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');return(s===nId||t===nId)?2.5:1;})
|
|
2909
|
+
.attr('opacity',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');return(s===nId||t===nId)?1:0.15;});
|
|
2910
|
+
nodeG.selectAll('.hier-node').attr('opacity',function(){
|
|
2911
|
+
const id=this.__data_id; if(id===nId)return 1;
|
|
2912
|
+
const connected=DATA.links.some(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return(s===nId&&t===id)||(t===nId&&s===id);});
|
|
2913
|
+
return connected?1:0.3;
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
function hierResetHighlight(){
|
|
2917
|
+
hierPinned=null;
|
|
2918
|
+
linkG.selectAll('path')
|
|
2919
|
+
.attr('stroke',function(){return this.getAttribute('stroke-dasharray')?'#1f3d5c':'var(--border)';})
|
|
2920
|
+
.attr('stroke-width',1).attr('opacity',1);
|
|
2921
|
+
nodeG.selectAll('.hier-node').attr('opacity',1);
|
|
2922
|
+
}
|
|
2923
|
+
function showHierDetail(n){
|
|
2924
|
+
const p=document.getElementById('hier-detail');
|
|
2925
|
+
document.getElementById('hd-name').textContent=n.id;
|
|
2926
|
+
document.getElementById('hd-meta').innerHTML=i('detail.dir')+': '+esc(n.dir)+'<br>'+i('detail.dependencies')+': '+n.deps+' \\u00b7 '+i('detail.dependents')+': '+n.dependents;
|
|
2927
|
+
document.getElementById('hd-dependents').innerHTML=(n.dependentsList||[]).map(x=>'<li>\\u2190 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
2928
|
+
document.getElementById('hd-deps').innerHTML=(n.dependencies||[]).map(x=>'<li>\\u2192 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
2929
|
+
p.classList.add('open');
|
|
2930
|
+
}
|
|
2931
|
+
window.closeHierDetail=()=>{document.getElementById('hier-detail').classList.remove('open');hierResetHighlight();tooltip.style.display='none';tooltipLocked=false;};
|
|
2990
2932
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
if (d.isOrphan) return '#484f58';
|
|
2994
|
-
// Layer coloring: all-visible or multi-select \u2192 layer colors; single-select \u2192 dir colors
|
|
2995
|
-
if (LAYERS && d.layer && layerColorMap[d.layer] && activeLayers.size !== 1) return layerColorMap[d.layer];
|
|
2996
|
-
return dirColor(d.dir);
|
|
2997
|
-
}
|
|
2998
|
-
function nodeRadius(d) { return Math.max(5, Math.min(22, 4 + d.dependents * 1.8)); }
|
|
2999
|
-
function fileName(id) { return id.split('/').pop(); }
|
|
2933
|
+
// Click on empty space to deselect
|
|
2934
|
+
hSvg.on('click',()=>{closeHierDetail();});
|
|
3000
2935
|
|
|
3001
|
-
// \
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
let hierSyncFromTab = null;
|
|
3007
|
-
document.querySelectorAll('.tab').forEach(tab => {
|
|
3008
|
-
tab.addEventListener('click', () => {
|
|
3009
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
3010
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
3011
|
-
tab.classList.add('active');
|
|
3012
|
-
document.getElementById(tab.dataset.view).classList.add('active');
|
|
3013
|
-
if (tab.dataset.view === 'hier-view') {
|
|
3014
|
-
if (!hierBuilt) { buildHierarchy(); hierBuilt = true; }
|
|
3015
|
-
if (hierSyncFromTab) { hierSyncFromTab(null); hierRelayout(); }
|
|
3016
|
-
}
|
|
3017
|
-
});
|
|
3018
|
-
});
|
|
2936
|
+
// Hierarchy filters \u2014 layer pills or dir pills
|
|
2937
|
+
const hFilterRow=document.getElementById('hier-filter-row');
|
|
2938
|
+
const hFilterBar=document.getElementById('hier-filter-bar');
|
|
2939
|
+
if (hFilterBar) hFilterBar.style.display='';
|
|
2940
|
+
const hActiveLayers=new Set(); // empty = show all (same as graph view)
|
|
3019
2941
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
2942
|
+
function hierRelayoutInner() {
|
|
2943
|
+
function isVisible(nId) {
|
|
2944
|
+
var nd = nodeMap[nId];
|
|
2945
|
+
if (!nd) return false;
|
|
2946
|
+
if (LAYERS && nd.layer && hActiveLayers.size > 0 && !hActiveLayers.has(nd.layer)) return false;
|
|
2947
|
+
return true;
|
|
2948
|
+
}
|
|
3026
2949
|
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
positionTooltip(e);
|
|
3037
|
-
}
|
|
3038
|
-
function positionTooltip(e) {
|
|
3039
|
-
const gap = 24;
|
|
3040
|
-
const tw = 420, th = tooltip.offsetHeight || 200;
|
|
3041
|
-
// Prefer placing to the right and above the cursor so it doesn't cover nodes below
|
|
3042
|
-
let x = e.clientX + gap;
|
|
3043
|
-
let y = e.clientY - th - 12;
|
|
3044
|
-
// If no room on the right, flip left
|
|
3045
|
-
if (x + tw > window.innerWidth) x = e.clientX - tw - gap;
|
|
3046
|
-
// If no room above, place below the cursor with gap
|
|
3047
|
-
if (y < 50) y = e.clientY + gap;
|
|
3048
|
-
// Final clamp
|
|
3049
|
-
if (y + th > window.innerHeight) y = window.innerHeight - th - 8;
|
|
3050
|
-
if (x < 8) x = 8;
|
|
3051
|
-
tooltip.style.left = x + 'px';
|
|
3052
|
-
tooltip.style.top = y + 'px';
|
|
3053
|
-
}
|
|
3054
|
-
function scheduleHideTooltip() {
|
|
3055
|
-
clearTimeout(tooltipHideTimer);
|
|
3056
|
-
tooltipHideTimer = setTimeout(() => {
|
|
3057
|
-
if (!tooltipLocked) {
|
|
3058
|
-
tooltip.style.display = 'none';
|
|
3059
|
-
if (!pinnedNode) resetGraphHighlight();
|
|
2950
|
+
// Build visible layer groups and compact Y positions
|
|
2951
|
+
var visibleDepths = [];
|
|
2952
|
+
var visLayerGroups = {};
|
|
2953
|
+
for (var depth = 0; depth <= maxLayer; depth++) {
|
|
2954
|
+
var visItems = layerGroups[depth].filter(function(id) { return isVisible(id); });
|
|
2955
|
+
if (visItems.length > 0) {
|
|
2956
|
+
visLayerGroups[depth] = visItems;
|
|
2957
|
+
visibleDepths.push(depth);
|
|
2958
|
+
}
|
|
3060
2959
|
}
|
|
3061
|
-
}, 250);
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3064
|
-
// Keep tooltip visible when mouse enters it
|
|
3065
|
-
tooltip.addEventListener('mouseenter', () => {
|
|
3066
|
-
clearTimeout(tooltipHideTimer);
|
|
3067
|
-
tooltipLocked = true;
|
|
3068
|
-
});
|
|
3069
|
-
tooltip.addEventListener('mouseleave', () => {
|
|
3070
|
-
tooltipLocked = false;
|
|
3071
|
-
scheduleHideTooltip();
|
|
3072
|
-
});
|
|
3073
|
-
|
|
3074
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3075
|
-
// GRAPH VIEW
|
|
3076
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3077
|
-
const svg = d3.select('#graph-svg').attr('width', W).attr('height', H);
|
|
3078
|
-
const g = svg.append('g');
|
|
3079
|
-
const zoom = d3.zoom().scaleExtent([0.05, 10]).on('zoom', e => g.attr('transform', e.transform));
|
|
3080
|
-
svg.call(zoom);
|
|
3081
|
-
svg.call(zoom.transform, d3.zoomIdentity.translate(W/2, H/2).scale(0.7));
|
|
3082
|
-
|
|
3083
|
-
window.zoomIn = () => svg.transition().duration(300).call(zoom.scaleBy, 1.4);
|
|
3084
|
-
window.zoomOut = () => svg.transition().duration(300).call(zoom.scaleBy, 0.7);
|
|
3085
|
-
window.zoomFit = () => {
|
|
3086
|
-
const b = g.node().getBBox(); if (!b.width) return;
|
|
3087
|
-
const s = Math.min(W/(b.width+80), H/(b.height+80))*0.9;
|
|
3088
|
-
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity.translate(W/2-(b.x+b.width/2)*s, H/2-(b.y+b.height/2)*s).scale(s));
|
|
3089
|
-
};
|
|
3090
|
-
|
|
3091
|
-
// Defs
|
|
3092
|
-
const defs = svg.append('defs');
|
|
3093
|
-
['#30363d','#58a6ff','#3fb950'].forEach((c,i) => {
|
|
3094
|
-
defs.append('marker').attr('id','arrow-'+i).attr('viewBox','0 -4 8 8')
|
|
3095
|
-
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3096
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill',c);
|
|
3097
|
-
});
|
|
3098
|
-
|
|
3099
|
-
// Links
|
|
3100
|
-
const link = g.append('g').selectAll('line').data(DATA.links).join('line')
|
|
3101
|
-
.attr('stroke', d => d.type==='type-only'?'#1f3d5c':'#30363d')
|
|
3102
|
-
.attr('stroke-width',1)
|
|
3103
|
-
.attr('stroke-dasharray', d => d.type==='type-only'?'4,3':d.type==='dynamic'?'6,3':null)
|
|
3104
|
-
.attr('marker-end','url(#arrow-0)')
|
|
3105
|
-
.attr('opacity', baseLinkOpacity);
|
|
3106
|
-
|
|
3107
|
-
// Cross-layer links (from layers.json connections)
|
|
3108
|
-
defs.append('marker').attr('id','arrow-cross').attr('viewBox','0 -4 8 8')
|
|
3109
|
-
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3110
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill','#f0883e');
|
|
3111
|
-
|
|
3112
|
-
const crossLinkData = (CROSS_EDGES || []).map(e => ({
|
|
3113
|
-
source: e.fromLayer + '/' + e.fromFile,
|
|
3114
|
-
target: e.toLayer + '/' + e.toFile,
|
|
3115
|
-
sourceLayer: e.fromLayer,
|
|
3116
|
-
targetLayer: e.toLayer,
|
|
3117
|
-
type: e.type || 'api-call',
|
|
3118
|
-
label: e.label || e.type || '',
|
|
3119
|
-
})).filter(e => DATA.nodes.some(n => n.id === e.source) && DATA.nodes.some(n => n.id === e.target));
|
|
3120
2960
|
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
// Nodes
|
|
3137
|
-
const node = g.append('g').selectAll('g').data(DATA.nodes).join('g')
|
|
3138
|
-
.attr('cursor','pointer')
|
|
3139
|
-
.call(d3.drag().on('start',dragStart).on('drag',dragging).on('end',dragEnd));
|
|
2961
|
+
// Recalculate positions for visible nodes (compacted)
|
|
2962
|
+
var newPositions = {};
|
|
2963
|
+
var newMaxRowWidth = 0;
|
|
2964
|
+
visibleDepths.forEach(function(depth) {
|
|
2965
|
+
newMaxRowWidth = Math.max(newMaxRowWidth, visLayerGroups[depth].length * (boxW + gapX) - gapX);
|
|
2966
|
+
});
|
|
2967
|
+
visibleDepths.forEach(function(depth, yIdx) {
|
|
2968
|
+
var items = visLayerGroups[depth];
|
|
2969
|
+
var rowWidth = items.length * (boxW + gapX) - gapX;
|
|
2970
|
+
var startX = padX + (newMaxRowWidth - rowWidth) / 2;
|
|
2971
|
+
items.forEach(function(id, idx) {
|
|
2972
|
+
newPositions[id] = { x: startX + idx * (boxW + gapX), y: padY + yIdx * (boxH + gapY) };
|
|
2973
|
+
});
|
|
2974
|
+
});
|
|
3140
2975
|
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
.attr('stroke-width', d => d.deps>=5?2.5:1.5)
|
|
3146
|
-
.attr('stroke-opacity', d => d.deps>=5?0.8:0.3);
|
|
2976
|
+
// Update SVG size
|
|
2977
|
+
var newTotalW = (newMaxRowWidth || 0) + padX * 2;
|
|
2978
|
+
var newTotalH = padY * 2 + Math.max(1, visibleDepths.length) * (boxH + gapY);
|
|
2979
|
+
hSvg.attr('width', Math.max(newTotalW, W)).attr('height', Math.max(newTotalH, H));
|
|
3147
2980
|
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
2981
|
+
// Update nodes: hide/show + transition positions
|
|
2982
|
+
nodeG.selectAll('.hier-node').each(function() {
|
|
2983
|
+
var nId = this.__data_id;
|
|
2984
|
+
var el = d3.select(this);
|
|
2985
|
+
if (!isVisible(nId) || !newPositions[nId]) {
|
|
2986
|
+
el.attr('display', 'none');
|
|
2987
|
+
} else {
|
|
2988
|
+
el.attr('display', null)
|
|
2989
|
+
.transition().duration(300)
|
|
2990
|
+
.attr('transform', 'translate(' + newPositions[nId].x + ',' + newPositions[nId].y + ')');
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
3157
2993
|
|
|
3158
|
-
//
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
2994
|
+
// Update links: show only if both endpoints visible, recalculate bezier
|
|
2995
|
+
linkG.selectAll('path').each(function() {
|
|
2996
|
+
var sId = this.getAttribute('data-source');
|
|
2997
|
+
var tId = this.getAttribute('data-target');
|
|
2998
|
+
var el = d3.select(this);
|
|
2999
|
+
if (!isVisible(sId) || !isVisible(tId) || !newPositions[sId] || !newPositions[tId]) {
|
|
3000
|
+
el.attr('display', 'none');
|
|
3001
|
+
} else {
|
|
3002
|
+
var s = newPositions[sId], t = newPositions[tId];
|
|
3003
|
+
var x1 = s.x + boxW / 2, y1 = s.y + boxH;
|
|
3004
|
+
var x2 = t.x + boxW / 2, y2 = t.y;
|
|
3005
|
+
var midY = (y1 + y2) / 2;
|
|
3006
|
+
el.attr('display', null)
|
|
3007
|
+
.transition().duration(300)
|
|
3008
|
+
.attr('d', 'M' + x1 + ',' + y1 + ' C' + x1 + ',' + midY + ' ' + x2 + ',' + midY + ' ' + x2 + ',' + y2);
|
|
3009
|
+
}
|
|
3174
3010
|
});
|
|
3175
|
-
node.attr('transform', d=>\`translate(\${d.x},\${d.y})\`);
|
|
3176
|
-
});
|
|
3177
3011
|
|
|
3178
|
-
//
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3012
|
+
// Update depth labels: hide empty depths, reposition visible ones
|
|
3013
|
+
hG.selectAll('.hier-layer-label').each(function() {
|
|
3014
|
+
var depthIdx = +this.getAttribute('data-depth-idx');
|
|
3015
|
+
var el = d3.select(this);
|
|
3016
|
+
var yIdx = visibleDepths.indexOf(depthIdx);
|
|
3017
|
+
if (yIdx === -1) {
|
|
3018
|
+
el.attr('display', 'none');
|
|
3019
|
+
} else {
|
|
3020
|
+
el.attr('display', null)
|
|
3021
|
+
.transition().duration(300)
|
|
3022
|
+
.attr('y', padY + yIdx * (boxH + gapY) + boxH / 2 + 4);
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
3184
3025
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
const allLayerCenters = {};
|
|
3191
|
-
LAYERS.forEach((l, idx) => {
|
|
3192
|
-
const angle = (2 * Math.PI * idx) / allLayerCount - Math.PI / 2;
|
|
3193
|
-
allLayerCenters[l.name] = { x: Math.cos(angle) * allBaseRadius, y: Math.sin(angle) * allBaseRadius };
|
|
3194
|
-
});
|
|
3026
|
+
// Close detail panel if pinned node became hidden
|
|
3027
|
+
if (hierPinned && !isVisible(hierPinned)) {
|
|
3028
|
+
closeHierDetail();
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3195
3031
|
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3032
|
+
function hierSyncFromTabInner() {
|
|
3033
|
+
if (!LAYERS) return;
|
|
3034
|
+
hActiveLayers.clear();
|
|
3035
|
+
activeLayers.forEach(function(name) { hActiveLayers.add(name); });
|
|
3036
|
+
// Sync pill UI
|
|
3037
|
+
hFilterRow.querySelectorAll('.layer-pill').forEach(function(p) {
|
|
3038
|
+
var ln = p.dataset.layer;
|
|
3039
|
+
if (ln === 'all') {
|
|
3040
|
+
p.classList.toggle('active', hActiveLayers.size === 0);
|
|
3041
|
+
} else {
|
|
3042
|
+
p.classList.toggle('active', hActiveLayers.has(ln));
|
|
3043
|
+
}
|
|
3207
3044
|
});
|
|
3208
|
-
return centers;
|
|
3209
3045
|
}
|
|
3210
3046
|
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3047
|
+
if (LAYERS) {
|
|
3048
|
+
// "All" button
|
|
3049
|
+
const allPill=document.createElement('div');
|
|
3050
|
+
allPill.className='layer-pill active';
|
|
3051
|
+
allPill.style.fontWeight='400';
|
|
3052
|
+
allPill.textContent='All';
|
|
3053
|
+
allPill.dataset.layer='all';
|
|
3054
|
+
allPill.onclick=()=>{
|
|
3055
|
+
hActiveLayers.clear();
|
|
3056
|
+
hFilterRow.querySelectorAll('.layer-pill').forEach(p=>p.classList.remove('active'));
|
|
3057
|
+
allPill.classList.add('active');
|
|
3058
|
+
hierRelayoutInner();
|
|
3059
|
+
};
|
|
3060
|
+
hFilterRow.appendChild(allPill);
|
|
3216
3061
|
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
const
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
if (
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3062
|
+
LAYERS.forEach(layer => {
|
|
3063
|
+
const pill=document.createElement('div');
|
|
3064
|
+
pill.className='layer-pill';
|
|
3065
|
+
pill.dataset.layer=layer.name;
|
|
3066
|
+
const count=DATA.nodes.filter(n=>n.layer===layer.name).length;
|
|
3067
|
+
pill.innerHTML='<div class="lp-dot" style="background:'+esc(layer.color)+'"></div>'+esc(layer.name)+' <span class="lp-count">'+count+'</span>';
|
|
3068
|
+
pill.onclick=(e)=>{
|
|
3069
|
+
if (e.shiftKey) {
|
|
3070
|
+
hActiveLayers.clear();
|
|
3071
|
+
hActiveLayers.add(layer.name);
|
|
3072
|
+
} else {
|
|
3073
|
+
if (hActiveLayers.has(layer.name)) hActiveLayers.delete(layer.name);
|
|
3074
|
+
else hActiveLayers.add(layer.name);
|
|
3075
|
+
}
|
|
3076
|
+
// Sync pill UI
|
|
3077
|
+
hFilterRow.querySelectorAll('.layer-pill').forEach(function(p) {
|
|
3078
|
+
var ln = p.dataset.layer;
|
|
3079
|
+
if (ln === 'all') p.classList.toggle('active', hActiveLayers.size === 0);
|
|
3080
|
+
else p.classList.toggle('active', hActiveLayers.has(ln));
|
|
3081
|
+
});
|
|
3082
|
+
hierRelayoutInner();
|
|
3083
|
+
};
|
|
3084
|
+
hFilterRow.appendChild(pill);
|
|
3085
|
+
});
|
|
3086
|
+
} else {
|
|
3087
|
+
const hActiveDirs=new Set(DATA.dirs);
|
|
3088
|
+
DATA.dirs.forEach(dir=>{
|
|
3089
|
+
const pill=document.createElement('div');
|
|
3090
|
+
pill.className='filter-pill active';
|
|
3091
|
+
pill.innerHTML='<div class="pill-dot" style="background:'+dirColor(dir)+'"></div>'+esc(dir||'.')+' <span class="pill-count">'+(dirCounts[dir]||0)+'</span>';
|
|
3092
|
+
pill.onclick=()=>{
|
|
3093
|
+
if(hActiveDirs.has(dir)){hActiveDirs.delete(dir);pill.classList.remove('active');}
|
|
3094
|
+
else{hActiveDirs.add(dir);pill.classList.add('active');}
|
|
3095
|
+
nodeG.selectAll('.hier-node').attr('opacity',function(){const nId=this.__data_id;return hActiveDirs.has(nodeMap[nId]?.dir)?1:0.1;});
|
|
3096
|
+
};
|
|
3097
|
+
hFilterRow.appendChild(pill);
|
|
3098
|
+
});
|
|
3244
3099
|
}
|
|
3245
|
-
simulation.force('cluster', clusterForce());
|
|
3246
|
-
|
|
3247
|
-
// Boost link strength for intra-layer edges (tighter connections within a layer)
|
|
3248
|
-
simulation.force('link').strength(l => {
|
|
3249
|
-
const sLayer = (l.source.layer ?? l.source);
|
|
3250
|
-
const tLayer = (l.target.layer ?? l.target);
|
|
3251
|
-
return sLayer === tLayer ? 0.4 : 0.1;
|
|
3252
|
-
});
|
|
3253
3100
|
|
|
3254
|
-
|
|
3101
|
+
// Assign function pointers for cross-view sync
|
|
3102
|
+
hierRelayout = hierRelayoutInner;
|
|
3103
|
+
hierSyncFromTab = hierSyncFromTabInner;
|
|
3255
3104
|
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
// Show hulls always (filter to selected layers when focused)
|
|
3105
|
+
hSvg.call(hZoom.transform,d3.zoomIdentity.translate(
|
|
3106
|
+
Math.max(0,(W-totalW)/2),20
|
|
3107
|
+
).scale(Math.min(1,W/(totalW+40),H/(totalH+40))));
|
|
3260
3108
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3109
|
+
// If layers were already filtered in graph view, sync hierarchy on first build
|
|
3110
|
+
if (activeLayers.size > 0) {
|
|
3111
|
+
hierSyncFromTabInner();
|
|
3112
|
+
hierRelayoutInner();
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
`;
|
|
3116
|
+
}
|
|
3265
3117
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3118
|
+
// src/web/js-diff.ts
|
|
3119
|
+
function buildDiffJs(diffData) {
|
|
3120
|
+
return `
|
|
3121
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3122
|
+
// DIFF VIEW
|
|
3123
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3124
|
+
const DIFF = ${diffData};
|
|
3125
|
+
if (DIFF) {
|
|
3126
|
+
document.getElementById('diff-tab').style.display = '';
|
|
3127
|
+
const addedSet = new Set(DIFF.added||[]);
|
|
3128
|
+
const removedSet = new Set(DIFF.removed||[]);
|
|
3129
|
+
const modifiedSet = new Set(DIFF.modified||[]);
|
|
3130
|
+
const affectedSet = new Set((DIFF.affectedDependents||[]).map(a=>a.file));
|
|
3275
3131
|
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
const dx = (n.x || 0) - cx, dy = (n.y || 0) - cy;
|
|
3282
|
-
return Math.sqrt(dx*dx + dy*dy) + nodeRadius(n) * nodeScale + 30;
|
|
3283
|
-
}));
|
|
3284
|
-
hullGroup.append('circle')
|
|
3285
|
-
.attr('cx', cx).attr('cy', cy).attr('r', maxR)
|
|
3286
|
-
.attr('class', 'layer-hull')
|
|
3287
|
-
.attr('fill', layer.color).attr('stroke', layer.color);
|
|
3288
|
-
hullGroup.append('text')
|
|
3289
|
-
.attr('class', 'layer-hull-label')
|
|
3290
|
-
.attr('x', cx).attr('y', cy - maxR - 8)
|
|
3291
|
-
.attr('text-anchor', 'middle')
|
|
3292
|
-
.attr('fill', layer.color)
|
|
3293
|
-
.text(layer.name);
|
|
3294
|
-
return;
|
|
3295
|
-
}
|
|
3132
|
+
// Populate summary counts
|
|
3133
|
+
document.getElementById('diff-added-count').textContent = addedSet.size;
|
|
3134
|
+
document.getElementById('diff-removed-count').textContent = removedSet.size;
|
|
3135
|
+
document.getElementById('diff-modified-count').textContent = modifiedSet.size;
|
|
3136
|
+
document.getElementById('diff-affected-count').textContent = affectedSet.size;
|
|
3296
3137
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3138
|
+
function isDiffNode(id) {
|
|
3139
|
+
return addedSet.has(id) || removedSet.has(id) || modifiedSet.has(id) || affectedSet.has(id);
|
|
3140
|
+
}
|
|
3299
3141
|
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3142
|
+
function diffStatus(id) {
|
|
3143
|
+
if (addedSet.has(id)) return 'Added';
|
|
3144
|
+
if (removedSet.has(id)) return 'Removed';
|
|
3145
|
+
if (modifiedSet.has(id)) return 'Modified';
|
|
3146
|
+
if (affectedSet.has(id)) return 'Affected';
|
|
3147
|
+
return 'Unchanged';
|
|
3148
|
+
}
|
|
3305
3149
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
.attr('fill', layer.color)
|
|
3313
|
-
.text(layer.name);
|
|
3314
|
-
});
|
|
3150
|
+
function diffStatusColor(id) {
|
|
3151
|
+
if (addedSet.has(id)) return 'var(--green)';
|
|
3152
|
+
if (removedSet.has(id)) return 'var(--red)';
|
|
3153
|
+
if (modifiedSet.has(id)) return 'var(--yellow)';
|
|
3154
|
+
if (affectedSet.has(id)) return 'var(--accent)';
|
|
3155
|
+
return 'var(--text-muted)';
|
|
3315
3156
|
}
|
|
3316
3157
|
|
|
3317
|
-
//
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
const rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
3324
|
-
d3.select(this)
|
|
3325
|
-
.attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
3326
|
-
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
3327
|
-
});
|
|
3328
|
-
node.attr('transform', d=>\`translate(\${d.x},\${d.y})\`);
|
|
3329
|
-
// Cross-layer links \u2014 resolve node positions by ID
|
|
3330
|
-
if (crossLinkData.length > 0) {
|
|
3331
|
-
const nodeById = {};
|
|
3332
|
-
DATA.nodes.forEach(n => { nodeById[n.id] = n; });
|
|
3333
|
-
crossLink.each(function(d) {
|
|
3334
|
-
const sN = nodeById[d.source], tN = nodeById[d.target];
|
|
3335
|
-
if (!sN || !tN) return;
|
|
3336
|
-
const dx = tN.x - sN.x, dy = tN.y - sN.y;
|
|
3337
|
-
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
3338
|
-
const rS = nodeRadius(sN) * nodeScale, rT = nodeRadius(tN) * nodeScale;
|
|
3339
|
-
d3.select(this)
|
|
3340
|
-
.attr('x1', sN.x + (dx/dist)*rS).attr('y1', sN.y + (dy/dist)*rS)
|
|
3341
|
-
.attr('x2', tN.x - (dx/dist)*rT).attr('y2', tN.y - (dy/dist)*rT);
|
|
3342
|
-
});
|
|
3343
|
-
crossLabel.each(function(d) {
|
|
3344
|
-
const sN = nodeById[d.source], tN = nodeById[d.target];
|
|
3345
|
-
if (!sN || !tN) return;
|
|
3346
|
-
d3.select(this).attr('x', (sN.x + tN.x) / 2).attr('y', (sN.y + tN.y) / 2 - 6);
|
|
3347
|
-
});
|
|
3348
|
-
}
|
|
3349
|
-
updateHulls();
|
|
3158
|
+
// Build reverse dependency map for impact chain
|
|
3159
|
+
var diffRevMap = {};
|
|
3160
|
+
DATA.links.forEach(function(l) {
|
|
3161
|
+
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3162
|
+
if (!diffRevMap[t]) diffRevMap[t] = [];
|
|
3163
|
+
diffRevMap[t].push(s);
|
|
3350
3164
|
});
|
|
3351
3165
|
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
const crossItem = document.createElement('div');
|
|
3363
|
-
crossItem.className = 'legend-item';
|
|
3364
|
-
crossItem.innerHTML = '<span style="color:#f0883e;font-size:11px">- - \u2192</span> Cross-layer link';
|
|
3365
|
-
layerLegend.appendChild(crossItem);
|
|
3166
|
+
function getImpactChain(startId) {
|
|
3167
|
+
var result = new Set();
|
|
3168
|
+
var queue = [startId];
|
|
3169
|
+
while (queue.length) {
|
|
3170
|
+
var id = queue.shift();
|
|
3171
|
+
if (result.has(id)) continue;
|
|
3172
|
+
result.add(id);
|
|
3173
|
+
(diffRevMap[id] || []).forEach(function(x) { queue.push(x); });
|
|
3174
|
+
}
|
|
3175
|
+
return result;
|
|
3366
3176
|
}
|
|
3367
|
-
// Add separator
|
|
3368
|
-
const sep = document.createElement('hr');
|
|
3369
|
-
sep.style.cssText = 'border:none;border-top:1px solid var(--border);margin:6px 0;';
|
|
3370
|
-
layerLegend.appendChild(sep);
|
|
3371
3177
|
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3178
|
+
let diffFocusMode = false;
|
|
3179
|
+
var dNode, dLink, dSim, simNodes, simLinks;
|
|
3180
|
+
|
|
3181
|
+
window.toggleDiffFocus = function() {
|
|
3182
|
+
diffFocusMode = !diffFocusMode;
|
|
3183
|
+
var btn = document.getElementById('diff-focus-btn');
|
|
3184
|
+
btn.classList.toggle('active', diffFocusMode);
|
|
3185
|
+
btn.textContent = diffFocusMode ? i('diff.showAll') : i('diff.focusChanges');
|
|
3186
|
+
if (!diffBuilt) return;
|
|
3187
|
+
applyDiffFilter();
|
|
3382
3188
|
};
|
|
3383
|
-
layerTabsEl.appendChild(allTab);
|
|
3384
3189
|
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
tab.innerHTML = '<div class="lt-dot" style="background:' + layer.color + '"></div>' + layer.name;
|
|
3390
|
-
tab.onclick = (e) => {
|
|
3391
|
-
if (e.shiftKey) {
|
|
3392
|
-
// Shift+click: solo this layer
|
|
3393
|
-
activeLayers.clear();
|
|
3394
|
-
activeLayers.add(layer.name);
|
|
3395
|
-
} else {
|
|
3396
|
-
// Toggle
|
|
3397
|
-
if (activeLayers.has(layer.name)) activeLayers.delete(layer.name);
|
|
3398
|
-
else activeLayers.add(layer.name);
|
|
3399
|
-
}
|
|
3400
|
-
syncLayerTabUI();
|
|
3401
|
-
applyLayerFilter();
|
|
3402
|
-
if (hierBuilt && hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
3403
|
-
};
|
|
3404
|
-
layerTabsEl.appendChild(tab);
|
|
3405
|
-
});
|
|
3190
|
+
window.closeDiffDetail = function() {
|
|
3191
|
+
document.getElementById('diff-detail').style.display = 'none';
|
|
3192
|
+
if (diffBuilt) resetDiffHighlight();
|
|
3193
|
+
};
|
|
3406
3194
|
|
|
3407
|
-
function
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3195
|
+
function applyDiffFilter() {
|
|
3196
|
+
dNode.attr('display', function(d) {
|
|
3197
|
+
if (!diffFocusMode) return null;
|
|
3198
|
+
return isDiffNode(d.id) ? null : 'none';
|
|
3411
3199
|
});
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3200
|
+
dNode.select('circle')
|
|
3201
|
+
.attr('opacity', function(d) {
|
|
3202
|
+
if (diffFocusMode) return isDiffNode(d.id) ? 1 : 0;
|
|
3203
|
+
return isDiffNode(d.id) ? 1 : 0.12;
|
|
3204
|
+
});
|
|
3205
|
+
dNode.select('text')
|
|
3206
|
+
.attr('opacity', function(d) {
|
|
3207
|
+
if (diffFocusMode) return isDiffNode(d.id) ? 1 : 0;
|
|
3208
|
+
return isDiffNode(d.id) ? 1 : 0.08;
|
|
3209
|
+
});
|
|
3210
|
+
dLink.attr('display', function(l) {
|
|
3211
|
+
if (!diffFocusMode) return null;
|
|
3212
|
+
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3213
|
+
return (isDiffNode(s) && isDiffNode(t)) ? null : 'none';
|
|
3415
3214
|
});
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
node.attr('display', d => {
|
|
3422
|
-
if (!activeDirs.has(d.dir)) return 'none';
|
|
3423
|
-
if (hasLayerFilter && !activeLayers.has(d.layer)) return 'none';
|
|
3424
|
-
return null;
|
|
3425
|
-
});
|
|
3426
|
-
link.attr('display', l => {
|
|
3427
|
-
const s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3428
|
-
const sN = DATA.nodes.find(n => n.id === s), tN = DATA.nodes.find(n => n.id === t);
|
|
3429
|
-
if (!sN || !tN) return 'none';
|
|
3430
|
-
if (!activeDirs.has(sN.dir) || !activeDirs.has(tN.dir)) return 'none';
|
|
3431
|
-
if (hasLayerFilter && (!activeLayers.has(sN.layer) || !activeLayers.has(tN.layer))) return 'none';
|
|
3432
|
-
return null;
|
|
3433
|
-
});
|
|
3434
|
-
// Refresh node colors: single-layer = dir-based, multi-layer = layer-based
|
|
3435
|
-
node.select('circle')
|
|
3436
|
-
.attr('fill', nodeColor)
|
|
3437
|
-
.attr('stroke', d => d.deps >= 5 ? 'var(--yellow)' : nodeColor(d));
|
|
3438
|
-
// Cross-layer links: respect user toggle + layer filter
|
|
3439
|
-
if (typeof crossLink !== 'undefined') {
|
|
3440
|
-
if (!crossLinksUserEnabled || isSingleLayer) {
|
|
3441
|
-
crossLink.attr('display', 'none');
|
|
3442
|
-
crossLabel.attr('display', 'none');
|
|
3443
|
-
} else if (hasLayerFilter) {
|
|
3444
|
-
crossLink.attr('display', d => (activeLayers.has(d.sourceLayer) && activeLayers.has(d.targetLayer)) ? null : 'none');
|
|
3445
|
-
crossLabel.attr('display', d => (activeLayers.has(d.sourceLayer) && activeLayers.has(d.targetLayer)) ? null : 'none');
|
|
3446
|
-
} else {
|
|
3447
|
-
crossLink.attr('display', null);
|
|
3448
|
-
crossLabel.attr('display', null);
|
|
3449
|
-
}
|
|
3450
|
-
}
|
|
3451
|
-
// Update stats
|
|
3452
|
-
const visibleNodes = DATA.nodes.filter(d => {
|
|
3453
|
-
if (!activeDirs.has(d.dir)) return false;
|
|
3454
|
-
if (hasLayerFilter && !activeLayers.has(d.layer)) return false;
|
|
3455
|
-
return true;
|
|
3215
|
+
dLink.attr('opacity', function(l) {
|
|
3216
|
+
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3217
|
+
if (isDiffNode(s) && isDiffNode(t)) return 0.6;
|
|
3218
|
+
if (isDiffNode(s) || isDiffNode(t)) return 0.15;
|
|
3219
|
+
return diffFocusMode ? 0 : 0.05;
|
|
3456
3220
|
});
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
return
|
|
3221
|
+
dLink.attr('stroke', function(l) {
|
|
3222
|
+
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3223
|
+
if (isDiffNode(s) && isDiffNode(t)) return diffStatusColor(s);
|
|
3224
|
+
return '#30363d';
|
|
3461
3225
|
});
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3226
|
+
dNode.select('circle')
|
|
3227
|
+
.attr('stroke-width', function(d) { return isDiffNode(d.id) ? 3 : 1; });
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
function resetDiffHighlight() {
|
|
3231
|
+
applyDiffFilter();
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
function highlightDiffImpact(d) {
|
|
3235
|
+
var chain = getImpactChain(d.id);
|
|
3236
|
+
dNode.select('circle').transition().duration(200)
|
|
3237
|
+
.attr('opacity', function(n) { return chain.has(n.id) ? 1 : 0.04; })
|
|
3238
|
+
.attr('stroke-width', function(n) { return chain.has(n.id) && n.id !== d.id ? 3 : isDiffNode(n.id) ? 3 : 1; })
|
|
3239
|
+
.attr('stroke', function(n) { return chain.has(n.id) && n.id !== d.id ? 'var(--red)' : diffStatusColor(n.id); });
|
|
3240
|
+
dNode.select('text').transition().duration(200)
|
|
3241
|
+
.attr('opacity', function(n) { return chain.has(n.id) ? 1 : 0.03; });
|
|
3242
|
+
dLink.transition().duration(200)
|
|
3243
|
+
.attr('opacity', function(l) {
|
|
3244
|
+
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3245
|
+
return (chain.has(s) && chain.has(t)) ? 0.8 : 0.03;
|
|
3246
|
+
})
|
|
3247
|
+
.attr('stroke', function(l) {
|
|
3248
|
+
var s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
3249
|
+
return (chain.has(s) && chain.has(t)) ? 'var(--red)' : '#30363d';
|
|
3250
|
+
});
|
|
3251
|
+
return chain;
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
function showDiffDetail(d) {
|
|
3255
|
+
var panel = document.getElementById('diff-detail');
|
|
3256
|
+
document.getElementById('dd-name').textContent = d.id;
|
|
3257
|
+
var statusEl = document.getElementById('dd-status');
|
|
3258
|
+
statusEl.textContent = diffStatus(d.id);
|
|
3259
|
+
statusEl.style.color = diffStatusColor(d.id);
|
|
3260
|
+
document.getElementById('dd-meta').innerHTML = i('detail.dir') + ': ' + esc(d.dir) + '<br>' + i('detail.dependencies') + ': ' + d.deps + ' \\u00b7 ' + i('detail.dependents') + ': ' + d.dependents;
|
|
3261
|
+
|
|
3262
|
+
// Show impact chain
|
|
3263
|
+
var chain = getImpactChain(d.id);
|
|
3264
|
+
chain.delete(d.id);
|
|
3265
|
+
var affectedList = document.getElementById('dd-affected');
|
|
3266
|
+
if (chain.size > 0) {
|
|
3267
|
+
affectedList.innerHTML = Array.from(chain).map(function(id) {
|
|
3268
|
+
return '<li style="color:' + diffStatusColor(id) + '">\\u2190 ' + esc(id) + ' <span style="font-size:10px;color:var(--text-muted)">(' + diffStatus(id) + ')</span></li>';
|
|
3269
|
+
}).join('');
|
|
3473
3270
|
} else {
|
|
3474
|
-
|
|
3475
|
-
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500));
|
|
3476
|
-
simulation.force('layerX', d3.forceX(d => centers[d.layer]?.x || 0).strength(d => d.layer ? lStrength : 0.03));
|
|
3477
|
-
simulation.force('layerY', d3.forceY(d => centers[d.layer]?.y || 0).strength(d => d.layer ? lStrength : 0.03));
|
|
3271
|
+
affectedList.innerHTML = '<li style="color:var(--text-muted)">' + i('diff.noImpact') + '</li>';
|
|
3478
3272
|
}
|
|
3479
|
-
|
|
3480
|
-
//
|
|
3481
|
-
|
|
3273
|
+
|
|
3274
|
+
// Show imports
|
|
3275
|
+
var depsList = document.getElementById('dd-deps');
|
|
3276
|
+
depsList.innerHTML = (d.dependencies || []).map(function(x) {
|
|
3277
|
+
return '<li style="color:' + diffStatusColor(x) + '">\\u2192 ' + esc(x) + '</li>';
|
|
3278
|
+
}).join('') || '<li style="color:var(--text-muted)">' + i('detail.none') + '</li>';
|
|
3279
|
+
|
|
3280
|
+
panel.style.display = 'block';
|
|
3482
3281
|
}
|
|
3483
3282
|
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3283
|
+
function buildDiffView() {
|
|
3284
|
+
const dSvg = d3.select('#diff-svg').attr('width', W).attr('height', H);
|
|
3285
|
+
const dG = dSvg.append('g');
|
|
3286
|
+
const dZoom = d3.zoom().scaleExtent([0.05,10]).on('zoom', e=>dG.attr('transform',e.transform));
|
|
3287
|
+
dSvg.call(dZoom);
|
|
3487
3288
|
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
dirToggle.textContent = dirPanelEl.classList.contains('open') ? '\u25BE Dirs' : '\u25B8 Dirs';
|
|
3496
|
-
};
|
|
3497
|
-
layerRowEl.appendChild(dirToggle);
|
|
3289
|
+
function diffColor(d) {
|
|
3290
|
+
if (addedSet.has(d.id)) return 'var(--green)';
|
|
3291
|
+
if (removedSet.has(d.id)) return 'var(--red)';
|
|
3292
|
+
if (modifiedSet.has(d.id)) return 'var(--yellow)';
|
|
3293
|
+
if (affectedSet.has(d.id)) return 'var(--accent)';
|
|
3294
|
+
return '#30363d';
|
|
3295
|
+
}
|
|
3498
3296
|
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
};
|
|
3510
|
-
}
|
|
3297
|
+
const dDefs = dSvg.append('defs');
|
|
3298
|
+
dDefs.append('marker').attr('id','darrow').attr('viewBox','0 -4 8 8')
|
|
3299
|
+
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3300
|
+
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill','#30363d');
|
|
3301
|
+
// Colored arrow markers for diff edges
|
|
3302
|
+
[['var(--green)','darrow-g'],['var(--red)','darrow-r'],['var(--yellow)','darrow-y'],['var(--accent)','darrow-a']].forEach(function(pair) {
|
|
3303
|
+
dDefs.append('marker').attr('id',pair[1]).attr('viewBox','0 -4 8 8')
|
|
3304
|
+
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3305
|
+
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill',pair[0]);
|
|
3306
|
+
});
|
|
3511
3307
|
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
const pill = document.createElement('div');
|
|
3515
|
-
pill.className = 'layer-pill';
|
|
3516
|
-
pill.dataset.layer = layer.name;
|
|
3517
|
-
pill.innerHTML = '<div class="lp-dot" style="background:' + layer.color + '"></div>' + layer.name + ' <span class="lp-count">' + layerNodes.length + '</span>';
|
|
3518
|
-
pill.onclick = () => {
|
|
3519
|
-
if (activeLayers.has(layer.name)) activeLayers.delete(layer.name);
|
|
3520
|
-
else activeLayers.add(layer.name);
|
|
3521
|
-
syncLayerTabUI();
|
|
3522
|
-
applyLayerFilter();
|
|
3523
|
-
};
|
|
3524
|
-
pill.onmouseenter = () => {
|
|
3525
|
-
if (pinnedNode) return;
|
|
3526
|
-
node.select('circle').transition().duration(120).attr('opacity', d => d.layer === layer.name ? 1 : 0.1);
|
|
3527
|
-
node.select('text').transition().duration(120).attr('opacity', d => d.layer === layer.name ? 1 : 0.05);
|
|
3528
|
-
};
|
|
3529
|
-
pill.onmouseleave = () => {
|
|
3530
|
-
if (pinnedNode) return;
|
|
3531
|
-
node.select('circle').transition().duration(150).attr('opacity', 1);
|
|
3532
|
-
node.select('text').transition().duration(150).attr('opacity', d => d.dependents >= 1 || d.deps >= 3 ? 1 : 0.5);
|
|
3533
|
-
};
|
|
3534
|
-
layerRowEl.appendChild(pill);
|
|
3308
|
+
simNodes = DATA.nodes.map(d=>({...d, x:undefined, y:undefined, vx:undefined, vy:undefined}));
|
|
3309
|
+
simLinks = DATA.links.map(d=>({source:d.source.id??d.source,target:d.target.id??d.target,type:d.type}));
|
|
3535
3310
|
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3311
|
+
dLink = dG.append('g').selectAll('line').data(simLinks).join('line')
|
|
3312
|
+
.attr('stroke','#30363d').attr('stroke-width',1).attr('marker-end','url(#darrow)').attr('opacity',0.05);
|
|
3313
|
+
|
|
3314
|
+
dNode = dG.append('g').selectAll('g').data(simNodes).join('g').attr('cursor','pointer');
|
|
3315
|
+
dNode.append('circle')
|
|
3316
|
+
.attr('r', d=>nodeRadius(d)*nodeScale)
|
|
3317
|
+
.attr('fill', diffColor)
|
|
3318
|
+
.attr('stroke', diffColor).attr('stroke-width', d=>isDiffNode(d.id)?3:1)
|
|
3319
|
+
.attr('opacity', d=>isDiffNode(d.id)?1:0.12);
|
|
3320
|
+
dNode.append('text')
|
|
3321
|
+
.text(d=>fileName(d.id).replace(/\\.tsx?$/,''))
|
|
3322
|
+
.attr('dx', d=>nodeRadius(d)*nodeScale+4).attr('dy',3.5).attr('font-size',11)
|
|
3323
|
+
.attr('fill', d=>isDiffNode(d.id)?'var(--text)':'var(--text-muted)')
|
|
3324
|
+
.attr('opacity', d=>isDiffNode(d.id)?1:0.08)
|
|
3325
|
+
.attr('pointer-events','none');
|
|
3326
|
+
|
|
3327
|
+
dSim = d3.forceSimulation(simNodes)
|
|
3328
|
+
.force('link', d3.forceLink(simLinks).id(d=>d.id).distance(70).strength(0.25))
|
|
3329
|
+
.force('charge', d3.forceManyBody().strength(-150).distanceMax(500))
|
|
3330
|
+
.force('center', d3.forceCenter(0,0))
|
|
3331
|
+
.force('collision', d3.forceCollide().radius(d=>nodeRadius(d)*nodeScale+4));
|
|
3332
|
+
|
|
3333
|
+
// Layer-aware physics for diff view (same pattern as graph view)
|
|
3334
|
+
var dHullGroup = null;
|
|
3335
|
+
if (LAYERS && LAYERS.length > 0) {
|
|
3336
|
+
var dLayerCenters = {};
|
|
3337
|
+
var dLayerCount = LAYERS.length;
|
|
3338
|
+
var dBaseRadius = Math.max(60, Math.min(W, H) * 0.04 * Math.sqrt(dLayerCount));
|
|
3339
|
+
LAYERS.forEach(function(l, idx) {
|
|
3340
|
+
var angle = (2 * Math.PI * idx) / dLayerCount - Math.PI / 2;
|
|
3341
|
+
dLayerCenters[l.name] = { x: Math.cos(angle) * dBaseRadius, y: Math.sin(angle) * dBaseRadius };
|
|
3342
|
+
});
|
|
3343
|
+
dSim.force('center', null);
|
|
3344
|
+
dSim.force('layerX', d3.forceX(function(d) { return dLayerCenters[d.layer]?.x || 0; }).strength(function(d) { return d.layer ? 0.12 : 0.03; }));
|
|
3345
|
+
dSim.force('layerY', d3.forceY(function(d) { return dLayerCenters[d.layer]?.y || 0; }).strength(function(d) { return d.layer ? 0.12 : 0.03; }));
|
|
3346
|
+
dSim.force('link').strength(function(l) {
|
|
3347
|
+
var sL = l.source.layer ?? l.source, tL = l.target.layer ?? l.target;
|
|
3348
|
+
return sL === tL ? 0.4 : 0.1;
|
|
3349
|
+
});
|
|
3350
|
+
// Cluster force for diff view
|
|
3351
|
+
dSim.force('cluster', (function() {
|
|
3352
|
+
var ns;
|
|
3353
|
+
function f(alpha) {
|
|
3354
|
+
var centroids = {}, counts = {};
|
|
3355
|
+
ns.forEach(function(n) {
|
|
3356
|
+
if (!n.layer) return;
|
|
3357
|
+
if (!centroids[n.layer]) { centroids[n.layer] = {x:0,y:0}; counts[n.layer] = 0; }
|
|
3358
|
+
centroids[n.layer].x += n.x; centroids[n.layer].y += n.y; counts[n.layer]++;
|
|
3359
|
+
});
|
|
3360
|
+
Object.keys(centroids).forEach(function(k) { centroids[k].x /= counts[k]; centroids[k].y /= counts[k]; });
|
|
3361
|
+
ns.forEach(function(n) {
|
|
3362
|
+
if (!n.layer || !centroids[n.layer]) return;
|
|
3363
|
+
n.vx += (centroids[n.layer].x - n.x) * alpha * 0.2;
|
|
3364
|
+
n.vy += (centroids[n.layer].y - n.y) * alpha * 0.2;
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
f.initialize = function(n) { ns = n; };
|
|
3368
|
+
return f;
|
|
3369
|
+
})());
|
|
3370
|
+
|
|
3371
|
+
dHullGroup = dG.insert('g', ':first-child');
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function updateDiffHulls() {
|
|
3375
|
+
if (!dHullGroup) return;
|
|
3376
|
+
dHullGroup.selectAll('*').remove();
|
|
3377
|
+
LAYERS.forEach(function(layer) {
|
|
3378
|
+
var layerNodes = simNodes.filter(function(n) { return n.layer === layer.name; });
|
|
3379
|
+
if (layerNodes.length === 0) return;
|
|
3380
|
+
if (diffFocusMode && !layerNodes.some(function(n) { return isDiffNode(n.id); })) return;
|
|
3381
|
+
var hasDiff = layerNodes.some(function(n) { return isDiffNode(n.id); });
|
|
3382
|
+
|
|
3383
|
+
var points = [];
|
|
3384
|
+
layerNodes.forEach(function(n) {
|
|
3385
|
+
if (n.x == null || n.y == null) return;
|
|
3386
|
+
if (diffFocusMode && !isDiffNode(n.id)) return;
|
|
3387
|
+
var r = nodeRadius(n) * nodeScale + 30;
|
|
3388
|
+
for (var a = 0; a < Math.PI * 2; a += Math.PI / 4) {
|
|
3389
|
+
points.push([n.x + Math.cos(a) * r, n.y + Math.sin(a) * r]);
|
|
3390
|
+
}
|
|
3391
|
+
});
|
|
3392
|
+
|
|
3393
|
+
var fillOp = hasDiff ? 0.15 : 0.06;
|
|
3394
|
+
var strokeOp = hasDiff ? 0.6 : 0.2;
|
|
3395
|
+
var sw = hasDiff ? 2.5 : 1;
|
|
3396
|
+
if (points.length < 6) {
|
|
3397
|
+
var cx = layerNodes.reduce(function(s, n) { return s + (n.x||0); }, 0) / layerNodes.length;
|
|
3398
|
+
var cy = layerNodes.reduce(function(s, n) { return s + (n.y||0); }, 0) / layerNodes.length;
|
|
3399
|
+
dHullGroup.append('circle').attr('cx', cx).attr('cy', cy).attr('r', 50)
|
|
3400
|
+
.attr('fill', layer.color).attr('fill-opacity', fillOp)
|
|
3401
|
+
.attr('stroke', layer.color).attr('stroke-opacity', strokeOp).attr('stroke-width', sw);
|
|
3402
|
+
} else {
|
|
3403
|
+
var hull = d3.polygonHull(points);
|
|
3404
|
+
if (hull) {
|
|
3405
|
+
dHullGroup.append('path')
|
|
3406
|
+
.attr('d', 'M' + hull.map(function(p) { return p.join(','); }).join('L') + 'Z')
|
|
3407
|
+
.attr('fill', layer.color).attr('fill-opacity', fillOp)
|
|
3408
|
+
.attr('stroke', layer.color).attr('stroke-opacity', strokeOp).attr('stroke-width', sw)
|
|
3409
|
+
.attr('stroke-dasharray', hasDiff ? null : '6,3');
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
// Layer name label
|
|
3413
|
+
var visNodes = diffFocusMode ? layerNodes.filter(function(n) { return isDiffNode(n.id); }) : layerNodes;
|
|
3414
|
+
if (visNodes.length === 0) return;
|
|
3415
|
+
var lx = visNodes.reduce(function(s, n) { return s + (n.x||0); }, 0) / visNodes.length;
|
|
3416
|
+
var ly = Math.min.apply(null, visNodes.map(function(n) { return n.y||0; })) - 25;
|
|
3417
|
+
dHullGroup.append('text')
|
|
3418
|
+
.attr('x', lx).attr('y', ly).attr('text-anchor', 'middle')
|
|
3419
|
+
.attr('fill', layer.color).attr('fill-opacity', hasDiff ? 0.9 : 0.4)
|
|
3420
|
+
.attr('font-size', 12).attr('font-weight', 600).text(layer.name);
|
|
3558
3421
|
});
|
|
3559
|
-
group.appendChild(pillsWrap);
|
|
3560
|
-
dirPanelEl.appendChild(group);
|
|
3561
3422
|
}
|
|
3423
|
+
|
|
3424
|
+
var dTickCount = 0;
|
|
3425
|
+
dSim.on('tick', function() {
|
|
3426
|
+
dLink.each(function(d) {
|
|
3427
|
+
var dx=d.target.x-d.source.x, dy=d.target.y-d.source.y, dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
3428
|
+
var rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
3429
|
+
d3.select(this).attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
3430
|
+
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
3431
|
+
});
|
|
3432
|
+
dNode.attr('transform', function(d) { return 'translate('+d.x+','+d.y+')'; });
|
|
3433
|
+
if (++dTickCount % 5 === 0) updateDiffHulls();
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
// Click: show impact chain + detail panel
|
|
3437
|
+
dNode.on('click', function(e, d) {
|
|
3438
|
+
e.stopPropagation();
|
|
3439
|
+
highlightDiffImpact(d);
|
|
3440
|
+
showDiffDetail(d);
|
|
3441
|
+
});
|
|
3442
|
+
|
|
3443
|
+
// Click on empty space to deselect
|
|
3444
|
+
dSvg.on('click', function() {
|
|
3445
|
+
closeDiffDetail();
|
|
3446
|
+
});
|
|
3447
|
+
|
|
3448
|
+
dNode.on('mouseover',function(e,d) { showTooltip(e,d); }).on('mousemove',function(e) { positionTooltip(e); }).on('mouseout',function() { scheduleHideTooltip(); });
|
|
3449
|
+
|
|
3450
|
+
// Apply initial filter (in case focus was toggled before build)
|
|
3451
|
+
applyDiffFilter();
|
|
3452
|
+
|
|
3453
|
+
var dAutoFitDone = false;
|
|
3454
|
+
dSim.on('end', function() {
|
|
3455
|
+
if (dAutoFitDone) return;
|
|
3456
|
+
dAutoFitDone = true;
|
|
3457
|
+
var b=dG.node().getBBox(); if(!b.width) return;
|
|
3458
|
+
var s=Math.min(W/(b.width+80),H/(b.height+80))*0.9;
|
|
3459
|
+
dSvg.call(dZoom.transform,d3.zoomIdentity.translate(W/2-(b.x+b.width/2)*s,H/2-(b.y+b.height/2)*s).scale(s));
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
}
|
|
3464
|
+
`;
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// src/utils/html-escape.ts
|
|
3468
|
+
var ESC_FUNCTION_JS = `function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }`;
|
|
3469
|
+
|
|
3470
|
+
// src/web/template.ts
|
|
3471
|
+
function buildGraphPage(graph, options = {}) {
|
|
3472
|
+
const locale = options.locale ?? "en";
|
|
3473
|
+
const diff = options.diff ?? null;
|
|
3474
|
+
const layers = options.layerMetadata ?? null;
|
|
3475
|
+
const crossEdges = options.crossLayerEdges ?? null;
|
|
3476
|
+
const files = Object.values(graph.files);
|
|
3477
|
+
const nodes = files.map((f) => ({
|
|
3478
|
+
id: f.path,
|
|
3479
|
+
deps: f.dependencies.length,
|
|
3480
|
+
dependents: f.dependents.length,
|
|
3481
|
+
dependencies: f.dependencies,
|
|
3482
|
+
dependentsList: f.dependents,
|
|
3483
|
+
isOrphan: f.dependencies.length === 0 && f.dependents.length === 0,
|
|
3484
|
+
dir: f.path.includes("/") ? f.path.substring(0, f.path.lastIndexOf("/")) : ".",
|
|
3485
|
+
layer: layers && f.path.includes("/") ? f.path.substring(0, f.path.indexOf("/")) : null
|
|
3486
|
+
}));
|
|
3487
|
+
const links = graph.edges.map((e) => ({
|
|
3488
|
+
source: e.source,
|
|
3489
|
+
target: e.target,
|
|
3490
|
+
type: e.type
|
|
3491
|
+
}));
|
|
3492
|
+
const circularFiles = /* @__PURE__ */ new Set();
|
|
3493
|
+
for (const c of graph.circularDependencies) {
|
|
3494
|
+
for (const f of c.cycle) circularFiles.add(f);
|
|
3495
|
+
}
|
|
3496
|
+
const dirs = [...new Set(nodes.map((n) => n.dir))].sort();
|
|
3497
|
+
const projectName = graph.rootDir.split("/").filter(Boolean).pop() || "Project";
|
|
3498
|
+
const diffData = diff ? JSON.stringify(diff) : "null";
|
|
3499
|
+
const layersData = layers ? JSON.stringify(layers) : "null";
|
|
3500
|
+
const crossEdgesData = crossEdges ? JSON.stringify(crossEdges) : "null";
|
|
3501
|
+
const graphData = JSON.stringify({ nodes, links, circularFiles: [...circularFiles], dirs, projectName });
|
|
3502
|
+
return (
|
|
3503
|
+
/* html */
|
|
3504
|
+
`<!DOCTYPE html>
|
|
3505
|
+
<html lang="${locale}">
|
|
3506
|
+
<head>
|
|
3507
|
+
<meta charset="utf-8">
|
|
3508
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3509
|
+
<title>${projectName} \u2014 Architecture Viewer</title>
|
|
3510
|
+
${buildStyles()}
|
|
3511
|
+
</head>
|
|
3512
|
+
<body>
|
|
3513
|
+
${buildViewerHtml()}
|
|
3514
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
3515
|
+
<script>
|
|
3516
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3517
|
+
// i18n
|
|
3518
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3519
|
+
const I18N = {
|
|
3520
|
+
en: {
|
|
3521
|
+
'tab.graph': 'Graph', 'tab.hierarchy': 'Hierarchy',
|
|
3522
|
+
'stats.files': 'Files', 'stats.edges': 'Edges', 'stats.circular': 'Circular',
|
|
3523
|
+
'settings.title': 'Settings', 'settings.theme': 'Theme', 'settings.fontSize': 'Font Size',
|
|
3524
|
+
'settings.nodeSize': 'Node Size', 'settings.linkOpacity': 'Link Opacity', 'settings.gravity': 'Gravity', 'settings.language': 'Language', 'settings.export': 'Export',
|
|
3525
|
+
'impact.title': 'Impact Simulation', 'impact.btn': 'Impact', 'impact.transitive': 'files affected',
|
|
3526
|
+
'search.placeholder': 'Search files...',
|
|
3527
|
+
'legend.circular': 'Circular dep', 'legend.orphan': 'Orphan', 'legend.highCoupling': 'High coupling',
|
|
3528
|
+
'legend.imports': 'imports', 'legend.importedBy': 'imported by',
|
|
3529
|
+
'detail.importedBy': 'Imported by', 'detail.imports': 'Imports',
|
|
3530
|
+
'detail.none': 'none', 'detail.dir': 'Dir', 'detail.dependencies': 'Dependencies', 'detail.dependents': 'Dependents',
|
|
3531
|
+
'tooltip.imports': 'imports', 'tooltip.importedBy': 'imported by',
|
|
3532
|
+
'help.graph': 'Scroll: zoom \xB7 Drag: pan \xB7 Click: select \xB7 / search',
|
|
3533
|
+
'help.hierarchy': 'Scroll to navigate \xB7 Click to highlight',
|
|
3534
|
+
'help.diff': 'Green=added \xB7 Red=removed \xB7 Yellow=modified \xB7 Blue=affected',
|
|
3535
|
+
'tab.diff': 'Diff',
|
|
3536
|
+
'diff.addedLabel': 'Added', 'diff.removedLabel': 'Removed', 'diff.modifiedLabel': 'Modified', 'diff.affectedLabel': 'Affected',
|
|
3537
|
+
'diff.showAll': 'Show all', 'diff.focusChanges': 'Focus changes', 'diff.noImpact': 'No downstream impact',
|
|
3538
|
+
'diff.affectedByChange': 'Affected by this change',
|
|
3539
|
+
},
|
|
3540
|
+
ja: {
|
|
3541
|
+
'tab.graph': '\u30B0\u30E9\u30D5', 'tab.hierarchy': '\u968E\u5C64\u56F3',
|
|
3542
|
+
'stats.files': '\u30D5\u30A1\u30A4\u30EB', 'stats.edges': '\u30A8\u30C3\u30B8', 'stats.circular': '\u5FAA\u74B0\u53C2\u7167',
|
|
3543
|
+
'settings.title': '\u8A2D\u5B9A', 'settings.theme': '\u30C6\u30FC\u30DE', 'settings.fontSize': '\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA',
|
|
3544
|
+
'settings.nodeSize': '\u30CE\u30FC\u30C9\u30B5\u30A4\u30BA', 'settings.linkOpacity': '\u30EA\u30F3\u30AF\u900F\u660E\u5EA6', 'settings.gravity': '\u91CD\u529B', 'settings.language': '\u8A00\u8A9E', 'settings.export': '\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8',
|
|
3545
|
+
'impact.title': '\u5F71\u97FF\u7BC4\u56F2\u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3', 'impact.btn': '\u5F71\u97FF', 'impact.transitive': '\u30D5\u30A1\u30A4\u30EB\u306B\u5F71\u97FF',
|
|
3546
|
+
'search.placeholder': '\u30D5\u30A1\u30A4\u30EB\u691C\u7D22...',
|
|
3547
|
+
'legend.circular': '\u5FAA\u74B0\u53C2\u7167', 'legend.orphan': '\u5B64\u7ACB', 'legend.highCoupling': '\u9AD8\u7D50\u5408',
|
|
3548
|
+
'legend.imports': 'import\u5148', 'legend.importedBy': 'import\u5143',
|
|
3549
|
+
'detail.importedBy': 'import\u5143', 'detail.imports': 'import\u5148',
|
|
3550
|
+
'detail.none': '\u306A\u3057', 'detail.dir': '\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA', 'detail.dependencies': '\u4F9D\u5B58\u5148', 'detail.dependents': '\u88AB\u4F9D\u5B58',
|
|
3551
|
+
'tooltip.imports': 'import\u5148', 'tooltip.importedBy': 'import\u5143',
|
|
3552
|
+
'help.graph': '\u30B9\u30AF\u30ED\u30FC\u30EB: \u30BA\u30FC\u30E0 \xB7 \u30C9\u30E9\u30C3\u30B0: \u79FB\u52D5 \xB7 \u30AF\u30EA\u30C3\u30AF: \u9078\u629E \xB7 / \u691C\u7D22',
|
|
3553
|
+
'help.hierarchy': '\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u79FB\u52D5 \xB7 \u30AF\u30EA\u30C3\u30AF\u3067\u30CF\u30A4\u30E9\u30A4\u30C8',
|
|
3554
|
+
'help.diff': '\u7DD1=\u8FFD\u52A0 \xB7 \u8D64=\u524A\u9664 \xB7 \u9EC4=\u5909\u66F4 \xB7 \u9752=\u5F71\u97FF',
|
|
3555
|
+
'tab.diff': '\u5DEE\u5206',
|
|
3556
|
+
'diff.addedLabel': '\u8FFD\u52A0', 'diff.removedLabel': '\u524A\u9664', 'diff.modifiedLabel': '\u5909\u66F4', 'diff.affectedLabel': '\u5F71\u97FF',
|
|
3557
|
+
'diff.showAll': '\u5168\u8868\u793A', 'diff.focusChanges': '\u5909\u66F4\u306E\u307F\u8868\u793A', 'diff.noImpact': '\u4E0B\u6D41\u3078\u306E\u5F71\u97FF\u306A\u3057',
|
|
3558
|
+
'diff.affectedByChange': '\u3053\u306E\u5909\u66F4\u306E\u5F71\u97FF\u7BC4\u56F2',
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
let currentLang = '${locale}';
|
|
3562
|
+
function applyI18n() {
|
|
3563
|
+
const msgs = I18N[currentLang] || I18N.en;
|
|
3564
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
3565
|
+
const key = el.getAttribute('data-i18n');
|
|
3566
|
+
if (msgs[key]) el.textContent = msgs[key];
|
|
3562
3567
|
});
|
|
3568
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
3569
|
+
const key = el.getAttribute('data-i18n-placeholder');
|
|
3570
|
+
if (msgs[key]) el.placeholder = msgs[key];
|
|
3571
|
+
});
|
|
3572
|
+
document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.dataset.lang === currentLang));
|
|
3573
|
+
}
|
|
3574
|
+
window.setLang = (lang) => { currentLang = lang; applyI18n(); saveSettings(); };
|
|
3575
|
+
function i(key) { return (I18N[currentLang] || I18N.en)[key] || key; }
|
|
3576
|
+
${ESC_FUNCTION_JS}
|
|
3577
|
+
|
|
3578
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3579
|
+
// SETTINGS (persisted to localStorage)
|
|
3580
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3581
|
+
const STORAGE_KEY = 'archtracker-settings';
|
|
3582
|
+
function saveSettings() {
|
|
3583
|
+
const s = { theme: document.body.getAttribute('data-theme') || 'dark', fontSize: document.getElementById('font-size-val').textContent, nodeSize: document.getElementById('node-size-val').textContent, linkOpacity: document.getElementById('link-opacity-val').textContent, gravity: document.getElementById('gravity-val').textContent, layerGravity: document.getElementById('layer-gravity-val').textContent, lang: currentLang, projectTitle: document.getElementById('project-title').textContent };
|
|
3584
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch(e) {}
|
|
3585
|
+
}
|
|
3586
|
+
function loadSettings() {
|
|
3587
|
+
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || null; } catch(e) { return null; }
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
let nodeScale = 1, baseLinkOpacity = 0.4;
|
|
3591
|
+
window.toggleSettings = () => document.getElementById('settings-panel').classList.toggle('open');
|
|
3592
|
+
window.setTheme = (theme) => {
|
|
3593
|
+
document.body.setAttribute('data-theme', theme === 'light' ? 'light' : '');
|
|
3594
|
+
document.querySelectorAll('.theme-btn[data-theme-val]').forEach(b => b.classList.toggle('active', b.dataset.themeVal === theme));
|
|
3595
|
+
saveSettings();
|
|
3596
|
+
};
|
|
3597
|
+
window.setFontSize = (v) => {
|
|
3598
|
+
document.getElementById('font-size-val').textContent = v;
|
|
3599
|
+
const scale = v / 13;
|
|
3600
|
+
if (typeof node !== 'undefined') {
|
|
3601
|
+
node.select('text').attr('font-size', d => (d.dependents>=3?12:10) * scale);
|
|
3602
|
+
}
|
|
3603
|
+
saveSettings();
|
|
3604
|
+
};
|
|
3605
|
+
window.setNodeScale = (v) => {
|
|
3606
|
+
nodeScale = v / 100;
|
|
3607
|
+
document.getElementById('node-size-val').textContent = v;
|
|
3608
|
+
if (typeof node !== 'undefined') {
|
|
3609
|
+
node.select('circle').attr('r', d => nodeRadius(d) * nodeScale);
|
|
3610
|
+
node.select('text').attr('dx', d => nodeRadius(d) * nodeScale + 4);
|
|
3611
|
+
simulation.force('collision', d3.forceCollide().radius(d => nodeRadius(d) * nodeScale + 4));
|
|
3612
|
+
simulation.alpha(0.3).restart();
|
|
3613
|
+
}
|
|
3614
|
+
saveSettings();
|
|
3615
|
+
};
|
|
3616
|
+
window.setLinkOpacity = (v) => {
|
|
3617
|
+
baseLinkOpacity = v / 100;
|
|
3618
|
+
document.getElementById('link-opacity-val').textContent = v;
|
|
3619
|
+
if (typeof link !== 'undefined') link.attr('opacity', baseLinkOpacity);
|
|
3620
|
+
saveSettings();
|
|
3621
|
+
};
|
|
3622
|
+
let gravityStrength = 150;
|
|
3623
|
+
window.setGravity = (v) => {
|
|
3624
|
+
gravityStrength = +v;
|
|
3625
|
+
document.getElementById('gravity-val').textContent = v;
|
|
3626
|
+
if (typeof simulation !== 'undefined') {
|
|
3627
|
+
if (typeof updateLayerPhysics === 'function') {
|
|
3628
|
+
updateLayerPhysics();
|
|
3629
|
+
} else {
|
|
3630
|
+
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500));
|
|
3631
|
+
}
|
|
3632
|
+
simulation.alpha(0.5).restart();
|
|
3633
|
+
}
|
|
3634
|
+
saveSettings();
|
|
3635
|
+
};
|
|
3636
|
+
let layerGravity = 12;
|
|
3637
|
+
window.setLayerGravity = (v) => {
|
|
3638
|
+
layerGravity = +v;
|
|
3639
|
+
document.getElementById('layer-gravity-val').textContent = v;
|
|
3640
|
+
if (typeof simulation !== 'undefined' && typeof updateLayerPhysics === 'function') {
|
|
3641
|
+
updateLayerPhysics();
|
|
3642
|
+
simulation.alpha(0.5).restart();
|
|
3643
|
+
}
|
|
3644
|
+
saveSettings();
|
|
3645
|
+
};
|
|
3563
3646
|
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3647
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3648
|
+
// EXPORT
|
|
3649
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3650
|
+
window.exportSVG = () => {
|
|
3651
|
+
const activeView = document.querySelector('.view.active svg');
|
|
3652
|
+
if (!activeView) return;
|
|
3653
|
+
const clone = activeView.cloneNode(true);
|
|
3654
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
3655
|
+
const blob = new Blob([clone.outerHTML], {type: 'image/svg+xml'});
|
|
3656
|
+
const a = document.createElement('a');
|
|
3657
|
+
a.href = URL.createObjectURL(blob);
|
|
3658
|
+
a.download = (document.getElementById('project-title').textContent || 'graph') + '.svg';
|
|
3659
|
+
a.click(); URL.revokeObjectURL(a.href);
|
|
3660
|
+
};
|
|
3661
|
+
window.exportPNG = () => {
|
|
3662
|
+
const activeView = document.querySelector('.view.active svg');
|
|
3663
|
+
if (!activeView) return;
|
|
3664
|
+
const clone = activeView.cloneNode(true);
|
|
3665
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
3666
|
+
const svgStr = new XMLSerializer().serializeToString(clone);
|
|
3667
|
+
const canvas = document.createElement('canvas');
|
|
3668
|
+
const bbox = activeView.getBoundingClientRect();
|
|
3669
|
+
canvas.width = bbox.width * 2; canvas.height = bbox.height * 2;
|
|
3670
|
+
const ctx = canvas.getContext('2d');
|
|
3671
|
+
ctx.scale(2, 2);
|
|
3672
|
+
const img = new Image();
|
|
3673
|
+
img.onload = () => { ctx.drawImage(img, 0, 0); const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = (document.getElementById('project-title').textContent || 'graph') + '.png'; a.click(); };
|
|
3674
|
+
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));
|
|
3675
|
+
};
|
|
3567
3676
|
|
|
3568
|
-
|
|
3677
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3678
|
+
// DATA
|
|
3679
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3680
|
+
const DATA = ${graphData};
|
|
3681
|
+
const LAYERS = ${layersData};
|
|
3682
|
+
const CROSS_EDGES = ${crossEdgesData};
|
|
3683
|
+
const W = window.innerWidth, H = window.innerHeight - 44;
|
|
3684
|
+
const circularSet = new Set(DATA.circularFiles);
|
|
3569
3685
|
|
|
3570
|
-
//
|
|
3686
|
+
// Project title (editable)
|
|
3687
|
+
const titleEl = document.getElementById('project-title');
|
|
3688
|
+
titleEl.textContent = DATA.projectName;
|
|
3689
|
+
titleEl.addEventListener('blur', () => { if (!titleEl.textContent.trim()) titleEl.textContent = DATA.projectName; document.title = titleEl.textContent + ' \u2014 Architecture Viewer'; saveSettings(); });
|
|
3690
|
+
titleEl.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } });
|
|
3691
|
+
|
|
3692
|
+
// Restore saved settings \u2014 phase 1: non-graph settings (before graph init)
|
|
3693
|
+
const _savedSettings = loadSettings();
|
|
3571
3694
|
if (_savedSettings) {
|
|
3572
|
-
if (_savedSettings.
|
|
3695
|
+
if (_savedSettings.theme) setTheme(_savedSettings.theme);
|
|
3696
|
+
if (_savedSettings.lang) { currentLang = _savedSettings.lang; applyI18n(); }
|
|
3697
|
+
if (_savedSettings.projectTitle) { titleEl.textContent = _savedSettings.projectTitle; document.title = _savedSettings.projectTitle + ' \u2014 Architecture Viewer'; }
|
|
3698
|
+
// Set slider positions (visual only \u2014 graph not built yet)
|
|
3699
|
+
if (_savedSettings.fontSize) { document.getElementById('font-size-slider').value = _savedSettings.fontSize; document.getElementById('font-size-val').textContent = _savedSettings.fontSize; }
|
|
3700
|
+
if (_savedSettings.nodeSize) { document.getElementById('node-size-slider').value = _savedSettings.nodeSize; document.getElementById('node-size-val').textContent = _savedSettings.nodeSize; nodeScale = _savedSettings.nodeSize / 100; }
|
|
3701
|
+
if (_savedSettings.linkOpacity) { document.getElementById('link-opacity-slider').value = _savedSettings.linkOpacity; document.getElementById('link-opacity-val').textContent = _savedSettings.linkOpacity; baseLinkOpacity = _savedSettings.linkOpacity / 100; }
|
|
3702
|
+
if (_savedSettings.gravity) { document.getElementById('gravity-slider').value = _savedSettings.gravity; document.getElementById('gravity-val').textContent = _savedSettings.gravity; gravityStrength = +_savedSettings.gravity; }
|
|
3703
|
+
if (_savedSettings.layerGravity) { document.getElementById('layer-gravity-slider').value = _savedSettings.layerGravity; document.getElementById('layer-gravity-val').textContent = _savedSettings.layerGravity; layerGravity = +_savedSettings.layerGravity; }
|
|
3573
3704
|
}
|
|
3574
3705
|
|
|
3575
|
-
|
|
3576
|
-
|
|
3706
|
+
document.getElementById('s-files').textContent = DATA.nodes.length;
|
|
3707
|
+
document.getElementById('s-edges').textContent = DATA.links.length;
|
|
3708
|
+
document.getElementById('s-circular').textContent = DATA.circularFiles.length;
|
|
3577
3709
|
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
node.select('circle').transition().duration(150).attr('opacity',n=>conn.has(n.id)?1:0.1);
|
|
3582
|
-
node.select('text').transition().duration(150).attr('opacity',n=>conn.has(n.id)?1:0.05);
|
|
3583
|
-
link.transition().duration(150)
|
|
3584
|
-
.attr('opacity',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return s===d.id||t===d.id?0.9:0.03;})
|
|
3585
|
-
.attr('stroke',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(s===d.id)return'#58a6ff';if(t===d.id)return'#3fb950';return l.type==='type-only'?'#1f3d5c':'#30363d';})
|
|
3586
|
-
.attr('stroke-width',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return s===d.id||t===d.id?2:1;})
|
|
3587
|
-
.attr('marker-end',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(s===d.id)return'url(#arrow-1)';if(t===d.id)return'url(#arrow-2)';return'url(#arrow-0)';});
|
|
3588
|
-
}
|
|
3710
|
+
const dirColor = d3.scaleOrdinal()
|
|
3711
|
+
.domain(DATA.dirs)
|
|
3712
|
+
.range(['#58a6ff','#3fb950','#d2a8ff','#f0883e','#79c0ff','#56d4dd','#db61a2','#f778ba','#ffa657','#7ee787']);
|
|
3589
3713
|
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
.attr('stroke-width',1).attr('marker-end','url(#arrow-0)');
|
|
3714
|
+
// Layer color map (from LAYERS metadata)
|
|
3715
|
+
const layerColorMap = {};
|
|
3716
|
+
let activeLayerFilter = null; // DEPRECATED \u2014 kept for backward compat, always null with multi-select tabs
|
|
3717
|
+
const activeLayers = new Set(); // empty = no filter (show all); non-empty = show only selected
|
|
3718
|
+
if (LAYERS) {
|
|
3719
|
+
LAYERS.forEach(l => { layerColorMap[l.name] = l.color; });
|
|
3720
|
+
document.getElementById('layer-gravity-setting').style.display = '';
|
|
3598
3721
|
}
|
|
3599
3722
|
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
.on('mouseout', () => { scheduleHideTooltip(); if (!pinnedNode) { /* highlight resets via scheduleHideTooltip */ } });
|
|
3607
|
-
|
|
3608
|
-
// \u2500\u2500\u2500 Click: pin highlight + detail panel \u2500\u2500\u2500\u2500\u2500
|
|
3609
|
-
node.on('click', (e,d) => {
|
|
3610
|
-
e.stopPropagation();
|
|
3611
|
-
pinnedNode = d;
|
|
3612
|
-
highlightNode(d);
|
|
3613
|
-
showDetail(d);
|
|
3614
|
-
});
|
|
3615
|
-
svg.on('click', () => {
|
|
3616
|
-
resetGraphHighlight();
|
|
3617
|
-
tooltip.style.display = 'none';
|
|
3618
|
-
tooltipLocked = false;
|
|
3619
|
-
closeDetail();
|
|
3620
|
-
});
|
|
3621
|
-
|
|
3622
|
-
function showDetail(d) {
|
|
3623
|
-
const p=document.getElementById('detail');
|
|
3624
|
-
document.getElementById('d-name').textContent=d.id;
|
|
3625
|
-
document.getElementById('d-meta').innerHTML=i('detail.dir')+': '+d.dir+'<br>'+i('detail.dependencies')+': '+d.deps+' \xB7 '+i('detail.dependents')+': '+d.dependents;
|
|
3626
|
-
const deptL=document.getElementById('d-dependents'), depsL=document.getElementById('d-deps');
|
|
3627
|
-
deptL.innerHTML=(d.dependentsList||[]).map(x=>'<li onclick="focusNode(\\''+x+'\\')">\u2190 '+x+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
3628
|
-
depsL.innerHTML=(d.dependencies||[]).map(x=>'<li onclick="focusNode(\\''+x+'\\')">\u2192 '+x+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
3629
|
-
p.classList.add('open');
|
|
3723
|
+
function nodeColor(d) {
|
|
3724
|
+
if (circularSet.has(d.id)) return '#f97583';
|
|
3725
|
+
if (d.isOrphan) return '#484f58';
|
|
3726
|
+
// Layer coloring: all-visible or multi-select \u2192 layer colors; single-select \u2192 dir colors
|
|
3727
|
+
if (LAYERS && d.layer && layerColorMap[d.layer] && activeLayers.size !== 1) return layerColorMap[d.layer];
|
|
3728
|
+
return dirColor(d.dir);
|
|
3630
3729
|
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
const n=DATA.nodes.find(x=>x.id===id); if(!n)return; showDetail(n);
|
|
3634
|
-
svg.transition().duration(500).call(zoom.transform,d3.zoomIdentity.translate(W/2-n.x*1.5,H/2-n.y*1.5).scale(1.5));
|
|
3635
|
-
};
|
|
3636
|
-
|
|
3637
|
-
// Drag
|
|
3638
|
-
function dragStart(e,d){if(!e.active)simulation.alphaTarget(0.3).restart();d.fx=d.x;d.fy=d.y;}
|
|
3639
|
-
function dragging(e,d){d.fx=e.x;d.fy=e.y;}
|
|
3640
|
-
function dragEnd(e,d){if(!e.active)simulation.alphaTarget(0);}
|
|
3730
|
+
function nodeRadius(d) { return Math.max(5, Math.min(22, 4 + d.dependents * 1.8)); }
|
|
3731
|
+
function fileName(id) { return id.split('/').pop(); }
|
|
3641
3732
|
|
|
3642
|
-
// \
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3733
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3734
|
+
// TAB SWITCHING
|
|
3735
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3736
|
+
let hierBuilt = false;
|
|
3737
|
+
let diffBuilt = false;
|
|
3738
|
+
let hierRelayout = null;
|
|
3739
|
+
let hierSyncFromTab = null;
|
|
3740
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
3741
|
+
tab.addEventListener('click', () => {
|
|
3742
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
3743
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
3744
|
+
tab.classList.add('active');
|
|
3745
|
+
document.getElementById(tab.dataset.view).classList.add('active');
|
|
3746
|
+
if (tab.dataset.view === 'hier-view') {
|
|
3747
|
+
if (!hierBuilt) { buildHierarchy(); hierBuilt = true; }
|
|
3748
|
+
if (hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
3749
|
+
}
|
|
3750
|
+
if (tab.dataset.view === 'diff-view') {
|
|
3751
|
+
if (!diffBuilt) { buildDiffView(); diffBuilt = true; }
|
|
3752
|
+
}
|
|
3753
|
+
});
|
|
3654
3754
|
});
|
|
3655
3755
|
|
|
3656
|
-
// \
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
};
|
|
3674
|
-
pill.onmouseleave=()=>{
|
|
3675
|
-
if(pinnedNode)return;
|
|
3676
|
-
node.select('circle').transition().duration(150).attr('opacity',1);
|
|
3677
|
-
node.select('text').transition().duration(150).attr('opacity',d=>d.dependents>=1||d.deps>=3?1:0.5);
|
|
3678
|
-
};
|
|
3679
|
-
filterRowEl.appendChild(pill);
|
|
3680
|
-
});
|
|
3756
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3757
|
+
// TOOLTIP \u2014 delayed hide + interactive
|
|
3758
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3759
|
+
const tooltip = document.getElementById('tooltip');
|
|
3760
|
+
let tooltipHideTimer = null;
|
|
3761
|
+
let tooltipLocked = false;
|
|
3762
|
+
|
|
3763
|
+
function showTooltip(e, d) {
|
|
3764
|
+
clearTimeout(tooltipHideTimer);
|
|
3765
|
+
document.getElementById('tt-name').textContent = d.id;
|
|
3766
|
+
document.getElementById('tt-dep-count').textContent = d.deps;
|
|
3767
|
+
document.getElementById('tt-dpt-count').textContent = d.dependents;
|
|
3768
|
+
const out = (d.dependencies||[]).map(x => '<div class="tt-out">\u2192 '+esc(x)+'</div>');
|
|
3769
|
+
const inc = (d.dependentsList||[]).map(x => '<div class="tt-in">\u2190 '+esc(x)+'</div>');
|
|
3770
|
+
document.getElementById('tt-details').innerHTML = [...out, ...inc].join('');
|
|
3771
|
+
tooltip.style.display = 'block';
|
|
3772
|
+
positionTooltip(e);
|
|
3681
3773
|
}
|
|
3682
|
-
function
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3774
|
+
function positionTooltip(e) {
|
|
3775
|
+
const gap = 24;
|
|
3776
|
+
const tw = 420, th = tooltip.offsetHeight || 200;
|
|
3777
|
+
// Prefer placing to the right and above the cursor so it doesn't cover nodes below
|
|
3778
|
+
let x = e.clientX + gap;
|
|
3779
|
+
let y = e.clientY - th - 12;
|
|
3780
|
+
// If no room on the right, flip left
|
|
3781
|
+
if (x + tw > window.innerWidth) x = e.clientX - tw - gap;
|
|
3782
|
+
// If no room above, place below the cursor with gap
|
|
3783
|
+
if (y < 50) y = e.clientY + gap;
|
|
3784
|
+
// Final clamp
|
|
3785
|
+
if (y + th > window.innerHeight) y = window.innerHeight - th - 8;
|
|
3786
|
+
if (x < 8) x = 8;
|
|
3787
|
+
tooltip.style.left = x + 'px';
|
|
3788
|
+
tooltip.style.top = y + 'px';
|
|
3693
3789
|
}
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
};
|
|
3703
|
-
function getTransitiveDependents(startId){
|
|
3704
|
-
const result=new Set();const queue=[startId];
|
|
3705
|
-
const revMap={};
|
|
3706
|
-
DATA.links.forEach(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(!revMap[t])revMap[t]=[];revMap[t].push(s);});
|
|
3707
|
-
while(queue.length){const id=queue.shift();if(result.has(id))continue;result.add(id);(revMap[id]||[]).forEach(x=>queue.push(x));}
|
|
3708
|
-
return result;
|
|
3790
|
+
function scheduleHideTooltip() {
|
|
3791
|
+
clearTimeout(tooltipHideTimer);
|
|
3792
|
+
tooltipHideTimer = setTimeout(() => {
|
|
3793
|
+
if (!tooltipLocked) {
|
|
3794
|
+
tooltip.style.display = 'none';
|
|
3795
|
+
if (!pinnedNode) resetGraphHighlight();
|
|
3796
|
+
}
|
|
3797
|
+
}, 250);
|
|
3709
3798
|
}
|
|
3710
|
-
// Override click in impact mode
|
|
3711
|
-
const origClick=node.on('click');
|
|
3712
|
-
node.on('click',(e,d)=>{
|
|
3713
|
-
if(!impactMode){e.stopPropagation();pinnedNode=d;highlightNode(d);showDetail(d);return;}
|
|
3714
|
-
e.stopPropagation();
|
|
3715
|
-
const affected=getTransitiveDependents(d.id);
|
|
3716
|
-
node.select('circle').transition().duration(200).attr('opacity',n=>affected.has(n.id)?1:0.06)
|
|
3717
|
-
.attr('stroke',n=>affected.has(n.id)&&n.id!==d.id?'var(--red)':n.deps>=5?'var(--yellow)':nodeColor(n))
|
|
3718
|
-
.attr('stroke-width',n=>affected.has(n.id)?3:1.5);
|
|
3719
|
-
node.select('text').transition().duration(200).attr('opacity',n=>affected.has(n.id)?1:0.04);
|
|
3720
|
-
link.transition().duration(200).attr('opacity',l=>{
|
|
3721
|
-
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
3722
|
-
return affected.has(s)&&affected.has(t)?0.8:0.03;
|
|
3723
|
-
}).attr('stroke',l=>{
|
|
3724
|
-
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
3725
|
-
return affected.has(s)&&affected.has(t)?'var(--red)':l.type==='type-only'?'#1f3d5c':'#30363d';
|
|
3726
|
-
});
|
|
3727
|
-
impactBadge.textContent=d.id.split('/').pop()+' \u2192 '+(affected.size-1)+' '+i('impact.transitive');
|
|
3728
|
-
impactBadge.style.display='block';
|
|
3729
|
-
});
|
|
3730
3799
|
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3800
|
+
// Keep tooltip visible when mouse enters it
|
|
3801
|
+
tooltip.addEventListener('mouseenter', () => {
|
|
3802
|
+
clearTimeout(tooltipHideTimer);
|
|
3803
|
+
tooltipLocked = true;
|
|
3804
|
+
});
|
|
3805
|
+
tooltip.addEventListener('mouseleave', () => {
|
|
3806
|
+
tooltipLocked = false;
|
|
3807
|
+
scheduleHideTooltip();
|
|
3734
3808
|
});
|
|
3735
3809
|
|
|
3736
3810
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3737
|
-
//
|
|
3811
|
+
// GRAPH VIEW
|
|
3738
3812
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3813
|
+
const svg = d3.select('#graph-svg').attr('width', W).attr('height', H);
|
|
3814
|
+
const g = svg.append('g');
|
|
3815
|
+
const zoom = d3.zoom().scaleExtent([0.05, 10]).on('zoom', e => g.attr('transform', e.transform));
|
|
3816
|
+
svg.call(zoom);
|
|
3817
|
+
svg.call(zoom.transform, d3.zoomIdentity.translate(W/2, H/2).scale(0.7));
|
|
3744
3818
|
|
|
3745
|
-
|
|
3746
|
-
|
|
3819
|
+
window.zoomIn = () => svg.transition().duration(300).call(zoom.scaleBy, 1.4);
|
|
3820
|
+
window.zoomOut = () => svg.transition().duration(300).call(zoom.scaleBy, 0.7);
|
|
3821
|
+
window.zoomFit = () => {
|
|
3822
|
+
const b = g.node().getBBox(); if (!b.width) return;
|
|
3823
|
+
const s = Math.min(W/(b.width+80), H/(b.height+80))*0.9;
|
|
3824
|
+
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity.translate(W/2-(b.x+b.width/2)*s, H/2-(b.y+b.height/2)*s).scale(s));
|
|
3825
|
+
};
|
|
3747
3826
|
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3827
|
+
// Defs
|
|
3828
|
+
const defs = svg.append('defs');
|
|
3829
|
+
['#30363d','#58a6ff','#3fb950'].forEach((c,i) => {
|
|
3830
|
+
defs.append('marker').attr('id','arrow-'+i).attr('viewBox','0 -4 8 8')
|
|
3831
|
+
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3832
|
+
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill',c);
|
|
3833
|
+
});
|
|
3752
3834
|
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3835
|
+
// Links
|
|
3836
|
+
const link = g.append('g').selectAll('line').data(DATA.links).join('line')
|
|
3837
|
+
.attr('stroke', d => d.type==='type-only'?'#1f3d5c':'#30363d')
|
|
3838
|
+
.attr('stroke-width',1)
|
|
3839
|
+
.attr('stroke-dasharray', d => d.type==='type-only'?'4,3':d.type==='dynamic'?'6,3':null)
|
|
3840
|
+
.attr('marker-end','url(#arrow-0)')
|
|
3841
|
+
.attr('opacity', baseLinkOpacity);
|
|
3760
3842
|
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
Object.values(layerGroups).forEach(arr=>arr.sort((a,b)=>(nodeMap[a]?.dir||'').localeCompare(nodeMap[b]?.dir||'')||a.localeCompare(b)));
|
|
3843
|
+
// Cross-layer links (from layers.json connections)
|
|
3844
|
+
defs.append('marker').attr('id','arrow-cross').attr('viewBox','0 -4 8 8')
|
|
3845
|
+
.attr('refX',8).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7).attr('orient','auto')
|
|
3846
|
+
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill','#f0883e');
|
|
3766
3847
|
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3848
|
+
const crossLinkData = (CROSS_EDGES || []).map(e => ({
|
|
3849
|
+
source: e.fromLayer + '/' + e.fromFile,
|
|
3850
|
+
target: e.toLayer + '/' + e.toFile,
|
|
3851
|
+
sourceLayer: e.fromLayer,
|
|
3852
|
+
targetLayer: e.toLayer,
|
|
3853
|
+
type: e.type || 'api-call',
|
|
3854
|
+
label: e.label || e.type || '',
|
|
3855
|
+
})).filter(e => DATA.nodes.some(n => n.id === e.source) && DATA.nodes.some(n => n.id === e.target));
|
|
3774
3856
|
|
|
3775
|
-
|
|
3776
|
-
|
|
3857
|
+
const crossLinkG = g.append('g');
|
|
3858
|
+
const crossLink = crossLinkG.selectAll('line').data(crossLinkData).join('line')
|
|
3859
|
+
.attr('stroke', '#f0883e')
|
|
3860
|
+
.attr('stroke-width', 2)
|
|
3861
|
+
.attr('stroke-dasharray', '8,4')
|
|
3862
|
+
.attr('marker-end', 'url(#arrow-cross)')
|
|
3863
|
+
.attr('opacity', 0.7);
|
|
3864
|
+
const crossLabel = crossLinkG.selectAll('text').data(crossLinkData).join('text')
|
|
3865
|
+
.text(d => d.label)
|
|
3866
|
+
.attr('font-size', 9)
|
|
3867
|
+
.attr('fill', '#f0883e')
|
|
3868
|
+
.attr('text-anchor', 'middle')
|
|
3869
|
+
.attr('opacity', 0.8)
|
|
3870
|
+
.attr('pointer-events', 'none');
|
|
3777
3871
|
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3872
|
+
// Nodes
|
|
3873
|
+
const node = g.append('g').selectAll('g').data(DATA.nodes).join('g')
|
|
3874
|
+
.attr('cursor','pointer')
|
|
3875
|
+
.call(d3.drag().on('start',dragStart).on('drag',dragging).on('end',dragEnd));
|
|
3876
|
+
|
|
3877
|
+
node.append('circle')
|
|
3878
|
+
.attr('r', d => nodeRadius(d) * nodeScale)
|
|
3879
|
+
.attr('fill', nodeColor)
|
|
3880
|
+
.attr('stroke', d => d.deps>=5?'var(--yellow)':nodeColor(d))
|
|
3881
|
+
.attr('stroke-width', d => d.deps>=5?2.5:1.5)
|
|
3882
|
+
.attr('stroke-opacity', d => d.deps>=5?0.8:0.3);
|
|
3883
|
+
|
|
3884
|
+
node.append('text')
|
|
3885
|
+
.text(d => fileName(d.id).replace(/\\.tsx?$/,''))
|
|
3886
|
+
.attr('dx', d => nodeRadius(d)*nodeScale+4)
|
|
3887
|
+
.attr('dy',3.5)
|
|
3888
|
+
.attr('font-size', d => d.dependents>=3?12:10)
|
|
3889
|
+
.attr('font-weight', d => d.dependents>=3?600:400)
|
|
3890
|
+
.attr('fill', d => d.dependents>=3?'var(--text)':'var(--text-dim)')
|
|
3891
|
+
.attr('opacity', d => d.dependents>=1||d.deps>=3?1:0.5)
|
|
3892
|
+
.attr('pointer-events','none');
|
|
3893
|
+
|
|
3894
|
+
// Simulation
|
|
3895
|
+
const simulation = d3.forceSimulation(DATA.nodes)
|
|
3896
|
+
.force('link', d3.forceLink(DATA.links).id(d=>d.id).distance(70).strength(0.25))
|
|
3897
|
+
.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500))
|
|
3898
|
+
.force('center', d3.forceCenter(0,0))
|
|
3899
|
+
.force('collision', d3.forceCollide().radius(d=>nodeRadius(d)*nodeScale+4))
|
|
3900
|
+
.force('x', d3.forceX(0).strength(0.03))
|
|
3901
|
+
.force('y', d3.forceY(0).strength(0.03))
|
|
3902
|
+
.on('tick', () => {
|
|
3903
|
+
link.each(function(d) {
|
|
3904
|
+
const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y;
|
|
3905
|
+
const dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
3906
|
+
const rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
3907
|
+
d3.select(this)
|
|
3908
|
+
.attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
3909
|
+
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
3910
|
+
});
|
|
3911
|
+
node.attr('transform', d=>\`translate(\${d.x},\${d.y})\`);
|
|
3788
3912
|
});
|
|
3789
3913
|
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3914
|
+
// \u2500\u2500\u2500 Layer convex hulls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3915
|
+
let hullGroup = null;
|
|
3916
|
+
const activeDirs = new Set(DATA.dirs);
|
|
3917
|
+
const dirCounts = {};
|
|
3918
|
+
DATA.nodes.forEach(n => dirCounts[n.dir] = (dirCounts[n.dir] || 0) + 1);
|
|
3919
|
+
var applyLayerFilter = null; // hoisted for dir-filter integration
|
|
3920
|
+
var updateLayerPhysics = null; // hoisted \u2014 updates charge/layer forces without visibility changes
|
|
3794
3921
|
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3922
|
+
if (LAYERS && LAYERS.length > 0) {
|
|
3923
|
+
// \u2500\u2500\u2500 Water droplet physics: intra-layer cohesion + inter-layer separation \u2500\u2500\u2500
|
|
3924
|
+
const allLayerCount = LAYERS.length;
|
|
3925
|
+
const allBaseRadius = Math.max(60, Math.min(W, H) * 0.04 * Math.sqrt(allLayerCount));
|
|
3926
|
+
// Pre-compute full-circle positions for all layers (used when no filter)
|
|
3927
|
+
const allLayerCenters = {};
|
|
3928
|
+
LAYERS.forEach((l, idx) => {
|
|
3929
|
+
const angle = (2 * Math.PI * idx) / allLayerCount - Math.PI / 2;
|
|
3930
|
+
allLayerCenters[l.name] = { x: Math.cos(angle) * allBaseRadius, y: Math.sin(angle) * allBaseRadius };
|
|
3931
|
+
});
|
|
3932
|
+
|
|
3933
|
+
// Dynamic center calculation: compact when multi-selecting, full spread when all
|
|
3934
|
+
function getLayerCenters() {
|
|
3935
|
+
if (activeLayers.size <= 1) return allLayerCenters; // 0 = all, 1 = single (centered)
|
|
3936
|
+
// Multi-select: arrange only selected layers compactly on a smaller circle
|
|
3937
|
+
const selected = LAYERS.filter(l => activeLayers.has(l.name));
|
|
3938
|
+
const count = selected.length;
|
|
3939
|
+
const compactRadius = Math.max(40, Math.min(W, H) * 0.03 * Math.sqrt(count));
|
|
3940
|
+
const centers = {};
|
|
3941
|
+
selected.forEach((l, idx) => {
|
|
3942
|
+
const angle = (2 * Math.PI * idx) / count - Math.PI / 2;
|
|
3943
|
+
centers[l.name] = { x: Math.cos(angle) * compactRadius, y: Math.sin(angle) * compactRadius };
|
|
3944
|
+
});
|
|
3945
|
+
return centers;
|
|
3800
3946
|
}
|
|
3801
3947
|
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3948
|
+
// Replace default centering forces with per-layer positioning
|
|
3949
|
+
const layerStrength = layerGravity / 100;
|
|
3950
|
+
simulation.force('x', null).force('y', null).force('center', null);
|
|
3951
|
+
simulation.force('layerX', d3.forceX(d => allLayerCenters[d.layer]?.x || 0).strength(d => d.layer ? layerStrength : 0.03));
|
|
3952
|
+
simulation.force('layerY', d3.forceY(d => allLayerCenters[d.layer]?.y || 0).strength(d => d.layer ? layerStrength : 0.03));
|
|
3953
|
+
|
|
3954
|
+
// Custom clustering force \u2014 surface tension pulling nodes toward their layer centroid
|
|
3955
|
+
function clusterForce() {
|
|
3956
|
+
let nodes;
|
|
3957
|
+
function force(alpha) {
|
|
3958
|
+
const centroids = {};
|
|
3959
|
+
const counts = {};
|
|
3960
|
+
nodes.forEach(n => {
|
|
3961
|
+
if (!n.layer) return;
|
|
3962
|
+
if (!centroids[n.layer]) { centroids[n.layer] = {x: 0, y: 0}; counts[n.layer] = 0; }
|
|
3963
|
+
centroids[n.layer].x += n.x;
|
|
3964
|
+
centroids[n.layer].y += n.y;
|
|
3965
|
+
counts[n.layer]++;
|
|
3966
|
+
});
|
|
3967
|
+
Object.keys(centroids).forEach(k => {
|
|
3968
|
+
centroids[k].x /= counts[k];
|
|
3969
|
+
centroids[k].y /= counts[k];
|
|
3970
|
+
});
|
|
3971
|
+
// Pull each node toward its layer centroid (surface tension)
|
|
3972
|
+
const strength = 0.2;
|
|
3973
|
+
nodes.forEach(n => {
|
|
3974
|
+
if (!n.layer || !centroids[n.layer]) return;
|
|
3975
|
+
n.vx += (centroids[n.layer].x - n.x) * alpha * strength;
|
|
3976
|
+
n.vy += (centroids[n.layer].y - n.y) * alpha * strength;
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
force.initialize = (n) => { nodes = n; };
|
|
3980
|
+
return force;
|
|
3981
|
+
}
|
|
3982
|
+
simulation.force('cluster', clusterForce());
|
|
3816
3983
|
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
.on('mousemove',e=>positionTooltip(e))
|
|
3823
|
-
.on('mouseout',()=>{
|
|
3824
|
-
scheduleHideTooltip();
|
|
3825
|
-
if (!hierPinned) hierResetHighlight();
|
|
3826
|
-
})
|
|
3827
|
-
.on('click',(e)=>{
|
|
3828
|
-
e.stopPropagation();
|
|
3829
|
-
hierPinned=n.id;
|
|
3830
|
-
hierHighlight(n.id);
|
|
3831
|
-
showHierDetail(n);
|
|
3832
|
-
});
|
|
3984
|
+
// Boost link strength for intra-layer edges (tighter connections within a layer)
|
|
3985
|
+
simulation.force('link').strength(l => {
|
|
3986
|
+
const sLayer = (l.source.layer ?? l.source);
|
|
3987
|
+
const tLayer = (l.target.layer ?? l.target);
|
|
3988
|
+
return sLayer === tLayer ? 0.4 : 0.1;
|
|
3833
3989
|
});
|
|
3834
3990
|
|
|
3835
|
-
|
|
3836
|
-
let hierPinned=null;
|
|
3837
|
-
function hierHighlight(nId){
|
|
3838
|
-
linkG.selectAll('path')
|
|
3839
|
-
.attr('stroke',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');if(s===nId)return'#58a6ff';if(t===nId)return'#3fb950';return this.getAttribute('stroke-dasharray')?'#1f3d5c':'var(--border)';})
|
|
3840
|
-
.attr('stroke-width',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');return(s===nId||t===nId)?2.5:1;})
|
|
3841
|
-
.attr('opacity',function(){const s=this.getAttribute('data-source'),t=this.getAttribute('data-target');return(s===nId||t===nId)?1:0.15;});
|
|
3842
|
-
nodeG.selectAll('.hier-node').attr('opacity',function(){
|
|
3843
|
-
const id=this.__data_id; if(id===nId)return 1;
|
|
3844
|
-
const connected=DATA.links.some(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return(s===nId&&t===id)||(t===nId&&s===id);});
|
|
3845
|
-
return connected?1:0.3;
|
|
3846
|
-
});
|
|
3847
|
-
}
|
|
3848
|
-
function hierResetHighlight(){
|
|
3849
|
-
hierPinned=null;
|
|
3850
|
-
linkG.selectAll('path')
|
|
3851
|
-
.attr('stroke',function(){return this.getAttribute('stroke-dasharray')?'#1f3d5c':'var(--border)';})
|
|
3852
|
-
.attr('stroke-width',1).attr('opacity',1);
|
|
3853
|
-
nodeG.selectAll('.hier-node').attr('opacity',1);
|
|
3854
|
-
}
|
|
3855
|
-
function showHierDetail(n){
|
|
3856
|
-
const p=document.getElementById('hier-detail');
|
|
3857
|
-
document.getElementById('hd-name').textContent=n.id;
|
|
3858
|
-
document.getElementById('hd-meta').innerHTML=i('detail.dir')+': '+n.dir+'<br>'+i('detail.dependencies')+': '+n.deps+' \xB7 '+i('detail.dependents')+': '+n.dependents;
|
|
3859
|
-
document.getElementById('hd-dependents').innerHTML=(n.dependentsList||[]).map(x=>'<li>\u2190 '+x+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
3860
|
-
document.getElementById('hd-deps').innerHTML=(n.dependencies||[]).map(x=>'<li>\u2192 '+x+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
3861
|
-
p.classList.add('open');
|
|
3862
|
-
}
|
|
3863
|
-
window.closeHierDetail=()=>{document.getElementById('hier-detail').classList.remove('open');hierResetHighlight();tooltip.style.display='none';tooltipLocked=false;};
|
|
3991
|
+
hullGroup = g.insert('g', ':first-child');
|
|
3864
3992
|
|
|
3865
|
-
|
|
3866
|
-
|
|
3993
|
+
function updateHulls() {
|
|
3994
|
+
if (!hullGroup) return;
|
|
3995
|
+
hullGroup.selectAll('*').remove();
|
|
3996
|
+
// Show hulls always (filter to selected layers when focused)
|
|
3867
3997
|
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
const hActiveLayers=new Set(); // empty = show all (same as graph view)
|
|
3998
|
+
LAYERS.forEach(layer => {
|
|
3999
|
+
if (activeLayers.size > 0 && !activeLayers.has(layer.name)) return;
|
|
4000
|
+
const layerNodes = DATA.nodes.filter(n => n.layer === layer.name);
|
|
4001
|
+
if (layerNodes.length === 0) return;
|
|
3873
4002
|
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
4003
|
+
const points = [];
|
|
4004
|
+
layerNodes.forEach(n => {
|
|
4005
|
+
if (n.x == null || n.y == null) return;
|
|
4006
|
+
const r = nodeRadius(n) * nodeScale + 30;
|
|
4007
|
+
// Add expanded points for a nicer hull shape
|
|
4008
|
+
for (let a = 0; a < Math.PI * 2; a += Math.PI / 4) {
|
|
4009
|
+
points.push([n.x + Math.cos(a) * r, n.y + Math.sin(a) * r]);
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
3881
4012
|
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
4013
|
+
if (points.length < 3) {
|
|
4014
|
+
// Fallback: circle for 1-2 nodes
|
|
4015
|
+
const cx = layerNodes.reduce((s, n) => s + (n.x || 0), 0) / layerNodes.length;
|
|
4016
|
+
const cy = layerNodes.reduce((s, n) => s + (n.y || 0), 0) / layerNodes.length;
|
|
4017
|
+
const maxR = Math.max(60, ...layerNodes.map(n => {
|
|
4018
|
+
const dx = (n.x || 0) - cx, dy = (n.y || 0) - cy;
|
|
4019
|
+
return Math.sqrt(dx*dx + dy*dy) + nodeRadius(n) * nodeScale + 30;
|
|
4020
|
+
}));
|
|
4021
|
+
hullGroup.append('circle')
|
|
4022
|
+
.attr('cx', cx).attr('cy', cy).attr('r', maxR)
|
|
4023
|
+
.attr('class', 'layer-hull')
|
|
4024
|
+
.attr('fill', layer.color).attr('stroke', layer.color);
|
|
4025
|
+
hullGroup.append('text')
|
|
4026
|
+
.attr('class', 'layer-hull-label')
|
|
4027
|
+
.attr('x', cx).attr('y', cy - maxR - 8)
|
|
4028
|
+
.attr('text-anchor', 'middle')
|
|
4029
|
+
.attr('fill', layer.color)
|
|
4030
|
+
.text(layer.name);
|
|
4031
|
+
return;
|
|
3890
4032
|
}
|
|
3891
|
-
}
|
|
3892
4033
|
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
var newMaxRowWidth = 0;
|
|
3896
|
-
visibleDepths.forEach(function(depth) {
|
|
3897
|
-
newMaxRowWidth = Math.max(newMaxRowWidth, visLayerGroups[depth].length * (boxW + gapX) - gapX);
|
|
3898
|
-
});
|
|
3899
|
-
visibleDepths.forEach(function(depth, yIdx) {
|
|
3900
|
-
var items = visLayerGroups[depth];
|
|
3901
|
-
var rowWidth = items.length * (boxW + gapX) - gapX;
|
|
3902
|
-
var startX = padX + (newMaxRowWidth - rowWidth) / 2;
|
|
3903
|
-
items.forEach(function(id, idx) {
|
|
3904
|
-
newPositions[id] = { x: startX + idx * (boxW + gapX), y: padY + yIdx * (boxH + gapY) };
|
|
3905
|
-
});
|
|
3906
|
-
});
|
|
4034
|
+
const hull = d3.polygonHull(points);
|
|
4035
|
+
if (!hull) return;
|
|
3907
4036
|
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4037
|
+
// Smooth the hull with a cardinal closed curve
|
|
4038
|
+
hullGroup.append('path')
|
|
4039
|
+
.attr('class', 'layer-hull')
|
|
4040
|
+
.attr('d', d3.line().curve(d3.curveCatmullRomClosed.alpha(0.5))(hull))
|
|
4041
|
+
.attr('fill', layer.color).attr('stroke', layer.color);
|
|
3912
4042
|
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
.transition().duration(300)
|
|
3922
|
-
.attr('transform', 'translate(' + newPositions[nId].x + ',' + newPositions[nId].y + ')');
|
|
3923
|
-
}
|
|
4043
|
+
// Label at the top of the hull
|
|
4044
|
+
const topPt = hull.reduce((best, p) => p[1] < best[1] ? p : best, hull[0]);
|
|
4045
|
+
hullGroup.append('text')
|
|
4046
|
+
.attr('class', 'layer-hull-label')
|
|
4047
|
+
.attr('x', topPt[0]).attr('y', topPt[1] - 10)
|
|
4048
|
+
.attr('text-anchor', 'middle')
|
|
4049
|
+
.attr('fill', layer.color)
|
|
4050
|
+
.text(layer.name);
|
|
3924
4051
|
});
|
|
4052
|
+
}
|
|
3925
4053
|
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
var x2 = t.x + boxW / 2, y2 = t.y;
|
|
3937
|
-
var midY = (y1 + y2) / 2;
|
|
3938
|
-
el.attr('display', null)
|
|
3939
|
-
.transition().duration(300)
|
|
3940
|
-
.attr('d', 'M' + x1 + ',' + y1 + ' C' + x1 + ',' + midY + ' ' + x2 + ',' + midY + ' ' + x2 + ',' + y2);
|
|
3941
|
-
}
|
|
4054
|
+
// Update hulls + cross-layer links on each tick
|
|
4055
|
+
simulation.on('tick', () => {
|
|
4056
|
+
// Regular links
|
|
4057
|
+
link.each(function(d) {
|
|
4058
|
+
const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y;
|
|
4059
|
+
const dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
4060
|
+
const rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
4061
|
+
d3.select(this)
|
|
4062
|
+
.attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
4063
|
+
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
3942
4064
|
});
|
|
4065
|
+
node.attr('transform', d=>\`translate(\${d.x},\${d.y})\`);
|
|
4066
|
+
// Cross-layer links \u2014 resolve node positions by ID
|
|
4067
|
+
if (crossLinkData.length > 0) {
|
|
4068
|
+
const nodeById = {};
|
|
4069
|
+
DATA.nodes.forEach(n => { nodeById[n.id] = n; });
|
|
4070
|
+
crossLink.each(function(d) {
|
|
4071
|
+
const sN = nodeById[d.source], tN = nodeById[d.target];
|
|
4072
|
+
if (!sN || !tN) return;
|
|
4073
|
+
const dx = tN.x - sN.x, dy = tN.y - sN.y;
|
|
4074
|
+
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
4075
|
+
const rS = nodeRadius(sN) * nodeScale, rT = nodeRadius(tN) * nodeScale;
|
|
4076
|
+
d3.select(this)
|
|
4077
|
+
.attr('x1', sN.x + (dx/dist)*rS).attr('y1', sN.y + (dy/dist)*rS)
|
|
4078
|
+
.attr('x2', tN.x - (dx/dist)*rT).attr('y2', tN.y - (dy/dist)*rT);
|
|
4079
|
+
});
|
|
4080
|
+
crossLabel.each(function(d) {
|
|
4081
|
+
const sN = nodeById[d.source], tN = nodeById[d.target];
|
|
4082
|
+
if (!sN || !tN) return;
|
|
4083
|
+
d3.select(this).attr('x', (sN.x + tN.x) / 2).attr('y', (sN.y + tN.y) / 2 - 6);
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
updateHulls();
|
|
4087
|
+
});
|
|
3943
4088
|
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4089
|
+
// \u2500\u2500\u2500 Layer legend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4090
|
+
const layerLegend = document.getElementById('layer-legend');
|
|
4091
|
+
LAYERS.forEach(layer => {
|
|
4092
|
+
const item = document.createElement('div');
|
|
4093
|
+
item.className = 'legend-item';
|
|
4094
|
+
item.innerHTML = '<div class="legend-dot" style="background:' + esc(layer.color) + '"></div> ' + esc(layer.name);
|
|
4095
|
+
layerLegend.appendChild(item);
|
|
4096
|
+
});
|
|
4097
|
+
// Cross-layer edge legend
|
|
4098
|
+
if (CROSS_EDGES && CROSS_EDGES.length > 0) {
|
|
4099
|
+
const crossItem = document.createElement('div');
|
|
4100
|
+
crossItem.className = 'legend-item';
|
|
4101
|
+
crossItem.innerHTML = '<span style="color:#f0883e;font-size:11px">- - \u2192</span> Cross-layer link';
|
|
4102
|
+
layerLegend.appendChild(crossItem);
|
|
4103
|
+
}
|
|
4104
|
+
// Add separator
|
|
4105
|
+
const sep = document.createElement('hr');
|
|
4106
|
+
sep.style.cssText = 'border:none;border-top:1px solid var(--border);margin:6px 0;';
|
|
4107
|
+
layerLegend.appendChild(sep);
|
|
4108
|
+
|
|
4109
|
+
// \u2500\u2500\u2500 Layer tabs (multi-select toggles in tab bar) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4110
|
+
const layerTabsEl = document.getElementById('layer-tabs');
|
|
4111
|
+
const allTab = document.createElement('div');
|
|
4112
|
+
allTab.className = 'layer-tab active';
|
|
4113
|
+
allTab.textContent = 'All';
|
|
4114
|
+
allTab.onclick = () => {
|
|
4115
|
+
activeLayers.clear();
|
|
4116
|
+
syncLayerTabUI();
|
|
4117
|
+
applyLayerFilter();
|
|
4118
|
+
if (hierBuilt && hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
4119
|
+
};
|
|
4120
|
+
layerTabsEl.appendChild(allTab);
|
|
4121
|
+
|
|
4122
|
+
LAYERS.forEach(layer => {
|
|
4123
|
+
const tab = document.createElement('div');
|
|
4124
|
+
tab.className = 'layer-tab';
|
|
4125
|
+
tab.dataset.layer = layer.name;
|
|
4126
|
+
tab.innerHTML = '<div class="lt-dot" style="background:' + esc(layer.color) + '"></div>' + esc(layer.name);
|
|
4127
|
+
tab.onclick = (e) => {
|
|
4128
|
+
if (e.shiftKey) {
|
|
4129
|
+
// Shift+click: solo this layer
|
|
4130
|
+
activeLayers.clear();
|
|
4131
|
+
activeLayers.add(layer.name);
|
|
3951
4132
|
} else {
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
4133
|
+
// Toggle
|
|
4134
|
+
if (activeLayers.has(layer.name)) activeLayers.delete(layer.name);
|
|
4135
|
+
else activeLayers.add(layer.name);
|
|
3955
4136
|
}
|
|
3956
|
-
|
|
4137
|
+
syncLayerTabUI();
|
|
4138
|
+
applyLayerFilter();
|
|
4139
|
+
if (hierBuilt && hierSyncFromTab) { hierSyncFromTab(); hierRelayout(); }
|
|
4140
|
+
};
|
|
4141
|
+
layerTabsEl.appendChild(tab);
|
|
4142
|
+
});
|
|
3957
4143
|
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
4144
|
+
function syncLayerTabUI() {
|
|
4145
|
+
allTab.classList.toggle('active', activeLayers.size === 0);
|
|
4146
|
+
layerTabsEl.querySelectorAll('.layer-tab[data-layer]').forEach(t => {
|
|
4147
|
+
t.classList.toggle('active', activeLayers.has(t.dataset.layer));
|
|
4148
|
+
});
|
|
4149
|
+
// Also sync the filter bar layer pills
|
|
4150
|
+
layerRowEl.querySelectorAll('.layer-pill[data-layer]').forEach(p => {
|
|
4151
|
+
p.classList.toggle('active', activeLayers.has(p.dataset.layer));
|
|
4152
|
+
});
|
|
3962
4153
|
}
|
|
3963
4154
|
|
|
3964
|
-
function
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4155
|
+
applyLayerFilter = function() {
|
|
4156
|
+
const isSingleLayer = activeLayers.size === 1;
|
|
4157
|
+
const hasLayerFilter = activeLayers.size > 0;
|
|
4158
|
+
node.attr('display', d => {
|
|
4159
|
+
if (!activeDirs.has(d.dir)) return 'none';
|
|
4160
|
+
if (hasLayerFilter && !activeLayers.has(d.layer)) return 'none';
|
|
4161
|
+
return null;
|
|
4162
|
+
});
|
|
4163
|
+
link.attr('display', l => {
|
|
4164
|
+
const s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
4165
|
+
const sN = DATA.nodes.find(n => n.id === s), tN = DATA.nodes.find(n => n.id === t);
|
|
4166
|
+
if (!sN || !tN) return 'none';
|
|
4167
|
+
if (!activeDirs.has(sN.dir) || !activeDirs.has(tN.dir)) return 'none';
|
|
4168
|
+
if (hasLayerFilter && (!activeLayers.has(sN.layer) || !activeLayers.has(tN.layer))) return 'none';
|
|
4169
|
+
return null;
|
|
4170
|
+
});
|
|
4171
|
+
// Refresh node colors: single-layer = dir-based, multi-layer = layer-based
|
|
4172
|
+
node.select('circle')
|
|
4173
|
+
.attr('fill', nodeColor)
|
|
4174
|
+
.attr('stroke', d => d.deps >= 5 ? 'var(--yellow)' : nodeColor(d));
|
|
4175
|
+
// Cross-layer links: respect user toggle + layer filter
|
|
4176
|
+
if (typeof crossLink !== 'undefined') {
|
|
4177
|
+
if (!crossLinksUserEnabled || isSingleLayer) {
|
|
4178
|
+
crossLink.attr('display', 'none');
|
|
4179
|
+
crossLabel.attr('display', 'none');
|
|
4180
|
+
} else if (hasLayerFilter) {
|
|
4181
|
+
crossLink.attr('display', d => (activeLayers.has(d.sourceLayer) && activeLayers.has(d.targetLayer)) ? null : 'none');
|
|
4182
|
+
crossLabel.attr('display', d => (activeLayers.has(d.sourceLayer) && activeLayers.has(d.targetLayer)) ? null : 'none');
|
|
3973
4183
|
} else {
|
|
3974
|
-
|
|
4184
|
+
crossLink.attr('display', null);
|
|
4185
|
+
crossLabel.attr('display', null);
|
|
3975
4186
|
}
|
|
4187
|
+
}
|
|
4188
|
+
// Update stats
|
|
4189
|
+
const visibleNodes = DATA.nodes.filter(d => {
|
|
4190
|
+
if (!activeDirs.has(d.dir)) return false;
|
|
4191
|
+
if (hasLayerFilter && !activeLayers.has(d.layer)) return false;
|
|
4192
|
+
return true;
|
|
4193
|
+
});
|
|
4194
|
+
const visibleIds = new Set(visibleNodes.map(n => n.id));
|
|
4195
|
+
const visibleEdges = DATA.links.filter(l => {
|
|
4196
|
+
const s = l.source.id ?? l.source, t = l.target.id ?? l.target;
|
|
4197
|
+
return visibleIds.has(s) && visibleIds.has(t);
|
|
3976
4198
|
});
|
|
4199
|
+
document.getElementById('s-files').textContent = visibleNodes.length;
|
|
4200
|
+
document.getElementById('s-edges').textContent = visibleEdges.length;
|
|
4201
|
+
const visCirc = DATA.circularFiles.filter(f => visibleIds.has(f));
|
|
4202
|
+
document.getElementById('s-circular').textContent = visCirc.length;
|
|
4203
|
+
updateHulls();
|
|
4204
|
+
// Delegate physics update and zoom to fit
|
|
4205
|
+
updateLayerPhysics();
|
|
4206
|
+
simulation.alpha(0.6).restart();
|
|
4207
|
+
setTimeout(() => zoomFit(), 600);
|
|
3977
4208
|
}
|
|
3978
4209
|
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
LAYERS.forEach(layer => {
|
|
3995
|
-
const pill=document.createElement('div');
|
|
3996
|
-
pill.className='layer-pill';
|
|
3997
|
-
pill.dataset.layer=layer.name;
|
|
3998
|
-
const count=DATA.nodes.filter(n=>n.layer===layer.name).length;
|
|
3999
|
-
pill.innerHTML='<div class="lp-dot" style="background:'+layer.color+'"></div>'+layer.name+' <span class="lp-count">'+count+'</span>';
|
|
4000
|
-
pill.onclick=(e)=>{
|
|
4001
|
-
if (e.shiftKey) {
|
|
4002
|
-
hActiveLayers.clear();
|
|
4003
|
-
hActiveLayers.add(layer.name);
|
|
4004
|
-
} else {
|
|
4005
|
-
if (hActiveLayers.has(layer.name)) hActiveLayers.delete(layer.name);
|
|
4006
|
-
else hActiveLayers.add(layer.name);
|
|
4007
|
-
}
|
|
4008
|
-
// Sync pill UI
|
|
4009
|
-
hFilterRow.querySelectorAll('.layer-pill').forEach(function(p) {
|
|
4010
|
-
var ln = p.dataset.layer;
|
|
4011
|
-
if (ln === 'all') p.classList.toggle('active', hActiveLayers.size === 0);
|
|
4012
|
-
else p.classList.toggle('active', hActiveLayers.has(ln));
|
|
4013
|
-
});
|
|
4014
|
-
hierRelayoutInner();
|
|
4015
|
-
};
|
|
4016
|
-
hFilterRow.appendChild(pill);
|
|
4017
|
-
});
|
|
4018
|
-
} else {
|
|
4019
|
-
const hActiveDirs=new Set(DATA.dirs);
|
|
4020
|
-
DATA.dirs.forEach(dir=>{
|
|
4021
|
-
const pill=document.createElement('div');
|
|
4022
|
-
pill.className='filter-pill active';
|
|
4023
|
-
pill.innerHTML='<div class="pill-dot" style="background:'+dirColor(dir)+'"></div>'+(dir||'.')+' <span class="pill-count">'+(dirCounts[dir]||0)+'</span>';
|
|
4024
|
-
pill.onclick=()=>{
|
|
4025
|
-
if(hActiveDirs.has(dir)){hActiveDirs.delete(dir);pill.classList.remove('active');}
|
|
4026
|
-
else{hActiveDirs.add(dir);pill.classList.add('active');}
|
|
4027
|
-
nodeG.selectAll('.hier-node').attr('opacity',function(){const nId=this.__data_id;return hActiveDirs.has(nodeMap[nId]?.dir)?1:0.1;});
|
|
4028
|
-
};
|
|
4029
|
-
hFilterRow.appendChild(pill);
|
|
4030
|
-
});
|
|
4210
|
+
// Separated physics update: handles charge/layer forces based on filter state.
|
|
4211
|
+
// Called by applyLayerFilter (with zoomFit), setGravity, setLayerGravity (without zoomFit).
|
|
4212
|
+
updateLayerPhysics = function() {
|
|
4213
|
+
const isSingleLayer = activeLayers.size === 1;
|
|
4214
|
+
const lStrength = layerGravity / 100;
|
|
4215
|
+
if (isSingleLayer) {
|
|
4216
|
+
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength * 3).distanceMax(800));
|
|
4217
|
+
simulation.force('layerX', d3.forceX(0).strength(0.03));
|
|
4218
|
+
simulation.force('layerY', d3.forceY(0).strength(0.03));
|
|
4219
|
+
} else {
|
|
4220
|
+
const centers = getLayerCenters();
|
|
4221
|
+
simulation.force('charge', d3.forceManyBody().strength(-gravityStrength).distanceMax(500));
|
|
4222
|
+
simulation.force('layerX', d3.forceX(d => centers[d.layer]?.x || 0).strength(d => d.layer ? lStrength : 0.03));
|
|
4223
|
+
simulation.force('layerY', d3.forceY(d => centers[d.layer]?.y || 0).strength(d => d.layer ? lStrength : 0.03));
|
|
4224
|
+
}
|
|
4031
4225
|
}
|
|
4032
4226
|
|
|
4033
|
-
//
|
|
4034
|
-
|
|
4035
|
-
|
|
4227
|
+
// \u2500\u2500\u2500 Layer filter pills (new grouped bar) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4228
|
+
const layerRowEl = document.getElementById('filter-layer-row');
|
|
4229
|
+
const dirPanelEl = document.getElementById('filter-dir-panel');
|
|
4036
4230
|
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4231
|
+
// Dir toggle button
|
|
4232
|
+
const dirToggle = document.createElement('div');
|
|
4233
|
+
dirToggle.id = 'filter-dir-toggle';
|
|
4234
|
+
dirToggle.textContent = '\u25B8 Dirs';
|
|
4235
|
+
dirToggle.onclick = () => {
|
|
4236
|
+
dirToggle.classList.toggle('open');
|
|
4237
|
+
dirPanelEl.classList.toggle('open');
|
|
4238
|
+
dirToggle.textContent = dirPanelEl.classList.contains('open') ? '\u25BE Dirs' : '\u25B8 Dirs';
|
|
4239
|
+
};
|
|
4240
|
+
layerRowEl.appendChild(dirToggle);
|
|
4040
4241
|
|
|
4041
|
-
//
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4242
|
+
// Cross-layer link toggle (in settings sidebar)
|
|
4243
|
+
let crossLinksUserEnabled = true;
|
|
4244
|
+
if (crossLinkData.length > 0) {
|
|
4245
|
+
document.getElementById('cross-layer-setting').style.display = '';
|
|
4246
|
+
window.toggleCrossLinks = () => {
|
|
4247
|
+
crossLinksUserEnabled = !crossLinksUserEnabled;
|
|
4248
|
+
const btn = document.getElementById('cross-link-toggle');
|
|
4249
|
+
btn.textContent = crossLinksUserEnabled ? 'ON' : 'OFF';
|
|
4250
|
+
btn.classList.toggle('active', crossLinksUserEnabled);
|
|
4251
|
+
applyLayerFilter();
|
|
4252
|
+
};
|
|
4045
4253
|
}
|
|
4046
|
-
}
|
|
4047
|
-
|
|
4048
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
4049
|
-
// DIFF VIEW
|
|
4050
|
-
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
4051
|
-
const DIFF = ${diffData};
|
|
4052
|
-
if (DIFF) {
|
|
4053
|
-
document.getElementById('diff-tab').style.display = '';
|
|
4054
|
-
const addedSet = new Set(DIFF.added||[]);
|
|
4055
|
-
const removedSet = new Set(DIFF.removed||[]);
|
|
4056
|
-
const modifiedSet = new Set(DIFF.modified||[]);
|
|
4057
|
-
const affectedSet = new Set((DIFF.affectedDependents||[]).map(a=>a.file));
|
|
4058
4254
|
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4255
|
+
LAYERS.forEach(layer => {
|
|
4256
|
+
const layerNodes = DATA.nodes.filter(n => n.layer === layer.name);
|
|
4257
|
+
const pill = document.createElement('div');
|
|
4258
|
+
pill.className = 'layer-pill';
|
|
4259
|
+
pill.dataset.layer = layer.name;
|
|
4260
|
+
pill.innerHTML = '<div class="lp-dot" style="background:' + esc(layer.color) + '"></div>' + esc(layer.name) + ' <span class="lp-count">' + layerNodes.length + '</span>';
|
|
4261
|
+
pill.onclick = () => {
|
|
4262
|
+
if (activeLayers.has(layer.name)) activeLayers.delete(layer.name);
|
|
4263
|
+
else activeLayers.add(layer.name);
|
|
4264
|
+
syncLayerTabUI();
|
|
4265
|
+
applyLayerFilter();
|
|
4266
|
+
};
|
|
4267
|
+
pill.onmouseenter = () => {
|
|
4268
|
+
if (pinnedNode) return;
|
|
4269
|
+
node.select('circle').transition().duration(120).attr('opacity', d => d.layer === layer.name ? 1 : 0.1);
|
|
4270
|
+
node.select('text').transition().duration(120).attr('opacity', d => d.layer === layer.name ? 1 : 0.05);
|
|
4271
|
+
};
|
|
4272
|
+
pill.onmouseleave = () => {
|
|
4273
|
+
if (pinnedNode) return;
|
|
4274
|
+
node.select('circle').transition().duration(150).attr('opacity', 1);
|
|
4275
|
+
node.select('text').transition().duration(150).attr('opacity', d => d.dependents >= 1 || d.deps >= 3 ? 1 : 0.5);
|
|
4276
|
+
};
|
|
4277
|
+
layerRowEl.appendChild(pill);
|
|
4065
4278
|
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4279
|
+
// Build dir group in panel for this layer
|
|
4280
|
+
const layerDirs = [...new Set(layerNodes.map(n => n.dir))].sort();
|
|
4281
|
+
if (layerDirs.length > 0) {
|
|
4282
|
+
const group = document.createElement('div');
|
|
4283
|
+
group.className = 'dir-group';
|
|
4284
|
+
const label = document.createElement('div');
|
|
4285
|
+
label.className = 'dir-group-label';
|
|
4286
|
+
label.innerHTML = '<div class="dg-dot" style="background:' + esc(layer.color) + '"></div>' + esc(layer.name);
|
|
4287
|
+
group.appendChild(label);
|
|
4288
|
+
const pillsWrap = document.createElement('div');
|
|
4289
|
+
pillsWrap.className = 'dir-group-pills';
|
|
4290
|
+
layerDirs.forEach(dir => {
|
|
4291
|
+
const dp = document.createElement('div');
|
|
4292
|
+
dp.className = 'filter-pill active';
|
|
4293
|
+
const shortDir = dir.includes('/') ? dir.substring(dir.indexOf('/') + 1) : dir;
|
|
4294
|
+
dp.innerHTML = '<div class="pill-dot" style="background:' + dirColor(dir) + '"></div>' + esc(shortDir || '.') + ' <span class="pill-count">' + (dirCounts[dir] || 0) + '</span>';
|
|
4295
|
+
dp.onclick = () => {
|
|
4296
|
+
if (activeDirs.has(dir)) { activeDirs.delete(dir); dp.classList.remove('active'); }
|
|
4297
|
+
else { activeDirs.add(dir); dp.classList.add('active'); }
|
|
4298
|
+
applyLayerFilter();
|
|
4299
|
+
};
|
|
4300
|
+
pillsWrap.appendChild(dp);
|
|
4301
|
+
});
|
|
4302
|
+
group.appendChild(pillsWrap);
|
|
4303
|
+
dirPanelEl.appendChild(group);
|
|
4072
4304
|
}
|
|
4305
|
+
});
|
|
4073
4306
|
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
.append('path').attr('d','M0,-3.5L8,0L0,3.5Z').attr('fill','#30363d');
|
|
4078
|
-
|
|
4079
|
-
const simNodes = DATA.nodes.map(d=>({...d, x:undefined, y:undefined, vx:undefined, vy:undefined}));
|
|
4080
|
-
const simLinks = DATA.links.map(d=>({source:d.source.id??d.source,target:d.target.id??d.target,type:d.type}));
|
|
4081
|
-
|
|
4082
|
-
const dLink = dG.append('g').selectAll('line').data(simLinks).join('line')
|
|
4083
|
-
.attr('stroke','#30363d').attr('stroke-width',1).attr('marker-end','url(#darrow)').attr('opacity',0.3);
|
|
4307
|
+
// Override applyFilter to respect layers
|
|
4308
|
+
window._origApplyFilter = applyFilter;
|
|
4309
|
+
}
|
|
4084
4310
|
|
|
4085
|
-
|
|
4086
|
-
dNode.append('circle')
|
|
4087
|
-
.attr('r', d=>nodeRadius(d)*nodeScale)
|
|
4088
|
-
.attr('fill', diffColor)
|
|
4089
|
-
.attr('stroke', diffColor).attr('stroke-width', d=>(addedSet.has(d.id)||removedSet.has(d.id)||modifiedSet.has(d.id)||affectedSet.has(d.id))?3:1)
|
|
4090
|
-
.attr('opacity', d=>(addedSet.has(d.id)||removedSet.has(d.id)||modifiedSet.has(d.id)||affectedSet.has(d.id))?1:0.2);
|
|
4091
|
-
dNode.append('text')
|
|
4092
|
-
.text(d=>fileName(d.id).replace(/\\.tsx?$/,''))
|
|
4093
|
-
.attr('dx', d=>nodeRadius(d)*nodeScale+4).attr('dy',3.5).attr('font-size',11)
|
|
4094
|
-
.attr('fill', d=>(addedSet.has(d.id)||removedSet.has(d.id)||modifiedSet.has(d.id)||affectedSet.has(d.id))?'var(--text)':'var(--text-muted)')
|
|
4095
|
-
.attr('opacity', d=>(addedSet.has(d.id)||removedSet.has(d.id)||modifiedSet.has(d.id)||affectedSet.has(d.id))?1:0.2)
|
|
4096
|
-
.attr('pointer-events','none');
|
|
4311
|
+
setTimeout(()=>zoomFit(), 1500);
|
|
4097
4312
|
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
.force('collision', d3.forceCollide().radius(d=>nodeRadius(d)*nodeScale+4));
|
|
4313
|
+
// Restore saved settings \u2014 phase 2: apply to graph elements now that they exist
|
|
4314
|
+
if (_savedSettings) {
|
|
4315
|
+
if (_savedSettings.fontSize) setFontSize(_savedSettings.fontSize);
|
|
4316
|
+
}
|
|
4103
4317
|
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
if (LAYERS && LAYERS.length > 0) {
|
|
4107
|
-
var dLayerCenters = {};
|
|
4108
|
-
var dLayerCount = LAYERS.length;
|
|
4109
|
-
var dBaseRadius = Math.max(60, Math.min(W, H) * 0.04 * Math.sqrt(dLayerCount));
|
|
4110
|
-
LAYERS.forEach(function(l, idx) {
|
|
4111
|
-
var angle = (2 * Math.PI * idx) / dLayerCount - Math.PI / 2;
|
|
4112
|
-
dLayerCenters[l.name] = { x: Math.cos(angle) * dBaseRadius, y: Math.sin(angle) * dBaseRadius };
|
|
4113
|
-
});
|
|
4114
|
-
dSim.force('center', null);
|
|
4115
|
-
dSim.force('layerX', d3.forceX(function(d) { return dLayerCenters[d.layer]?.x || 0; }).strength(function(d) { return d.layer ? 0.12 : 0.03; }));
|
|
4116
|
-
dSim.force('layerY', d3.forceY(function(d) { return dLayerCenters[d.layer]?.y || 0; }).strength(function(d) { return d.layer ? 0.12 : 0.03; }));
|
|
4117
|
-
dSim.force('link').strength(function(l) {
|
|
4118
|
-
var sL = l.source.layer ?? l.source, tL = l.target.layer ?? l.target;
|
|
4119
|
-
return sL === tL ? 0.4 : 0.1;
|
|
4120
|
-
});
|
|
4121
|
-
// Cluster force for diff view
|
|
4122
|
-
dSim.force('cluster', (function() {
|
|
4123
|
-
var ns;
|
|
4124
|
-
function f(alpha) {
|
|
4125
|
-
var centroids = {}, counts = {};
|
|
4126
|
-
ns.forEach(function(n) {
|
|
4127
|
-
if (!n.layer) return;
|
|
4128
|
-
if (!centroids[n.layer]) { centroids[n.layer] = {x:0,y:0}; counts[n.layer] = 0; }
|
|
4129
|
-
centroids[n.layer].x += n.x; centroids[n.layer].y += n.y; counts[n.layer]++;
|
|
4130
|
-
});
|
|
4131
|
-
Object.keys(centroids).forEach(function(k) { centroids[k].x /= counts[k]; centroids[k].y /= counts[k]; });
|
|
4132
|
-
ns.forEach(function(n) {
|
|
4133
|
-
if (!n.layer || !centroids[n.layer]) return;
|
|
4134
|
-
n.vx += (centroids[n.layer].x - n.x) * alpha * 0.2;
|
|
4135
|
-
n.vy += (centroids[n.layer].y - n.y) * alpha * 0.2;
|
|
4136
|
-
});
|
|
4137
|
-
}
|
|
4138
|
-
f.initialize = function(n) { ns = n; };
|
|
4139
|
-
return f;
|
|
4140
|
-
})());
|
|
4318
|
+
// \u2500\u2500\u2500 Highlight helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4319
|
+
let pinnedNode = null;
|
|
4141
4320
|
|
|
4142
|
-
|
|
4143
|
-
|
|
4321
|
+
function highlightNode(d) {
|
|
4322
|
+
const conn = new Set([d.id]);
|
|
4323
|
+
DATA.links.forEach(l => { const s=l.source.id??l.source,t=l.target.id??l.target; if(s===d.id)conn.add(t); if(t===d.id)conn.add(s); });
|
|
4324
|
+
node.select('circle').transition().duration(150).attr('opacity',n=>conn.has(n.id)?1:0.1);
|
|
4325
|
+
node.select('text').transition().duration(150).attr('opacity',n=>conn.has(n.id)?1:0.05);
|
|
4326
|
+
link.transition().duration(150)
|
|
4327
|
+
.attr('opacity',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return s===d.id||t===d.id?0.9:0.03;})
|
|
4328
|
+
.attr('stroke',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(s===d.id)return'#58a6ff';if(t===d.id)return'#3fb950';return l.type==='type-only'?'#1f3d5c':'#30363d';})
|
|
4329
|
+
.attr('stroke-width',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;return s===d.id||t===d.id?2:1;})
|
|
4330
|
+
.attr('marker-end',l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(s===d.id)return'url(#arrow-1)';if(t===d.id)return'url(#arrow-2)';return'url(#arrow-0)';});
|
|
4331
|
+
}
|
|
4144
4332
|
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4333
|
+
function resetGraphHighlight() {
|
|
4334
|
+
pinnedNode = null;
|
|
4335
|
+
node.select('circle').transition().duration(200).attr('opacity',1);
|
|
4336
|
+
node.select('text').transition().duration(200).attr('opacity',d=>d.dependents>=1||d.deps>=3?1:0.5);
|
|
4337
|
+
link.transition().duration(200)
|
|
4338
|
+
.attr('opacity',baseLinkOpacity)
|
|
4339
|
+
.attr('stroke',d=>d.type==='type-only'?'#1f3d5c':'#30363d')
|
|
4340
|
+
.attr('stroke-width',1).attr('marker-end','url(#arrow-0)');
|
|
4341
|
+
}
|
|
4148
4342
|
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4343
|
+
// \u2500\u2500\u2500 Hover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4344
|
+
node.on('mouseover', (e,d) => {
|
|
4345
|
+
showTooltip(e,d);
|
|
4346
|
+
if (!pinnedNode) highlightNode(d);
|
|
4347
|
+
})
|
|
4348
|
+
.on('mousemove', e=>positionTooltip(e))
|
|
4349
|
+
.on('mouseout', () => { scheduleHideTooltip(); if (!pinnedNode) { /* highlight resets via scheduleHideTooltip */ } });
|
|
4156
4350
|
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4351
|
+
// \u2500\u2500\u2500 Click: pin highlight + detail panel \u2500\u2500\u2500\u2500\u2500
|
|
4352
|
+
node.on('click', (e,d) => {
|
|
4353
|
+
e.stopPropagation();
|
|
4354
|
+
pinnedNode = d;
|
|
4355
|
+
highlightNode(d);
|
|
4356
|
+
showDetail(d);
|
|
4357
|
+
});
|
|
4358
|
+
svg.on('click', () => {
|
|
4359
|
+
resetGraphHighlight();
|
|
4360
|
+
tooltip.style.display = 'none';
|
|
4361
|
+
tooltipLocked = false;
|
|
4362
|
+
closeDetail();
|
|
4363
|
+
});
|
|
4165
4364
|
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
}
|
|
4184
|
-
}
|
|
4185
|
-
// Layer name label
|
|
4186
|
-
var lx = layerNodes.reduce(function(s, n) { return s + (n.x||0); }, 0) / layerNodes.length;
|
|
4187
|
-
var ly = Math.min.apply(null, layerNodes.map(function(n) { return n.y||0; })) - 25;
|
|
4188
|
-
dHullGroup.append('text')
|
|
4189
|
-
.attr('x', lx).attr('y', ly).attr('text-anchor', 'middle')
|
|
4190
|
-
.attr('fill', layer.color).attr('fill-opacity', hasDiff ? 0.9 : 0.4)
|
|
4191
|
-
.attr('font-size', 12).attr('font-weight', 600).text(layer.name);
|
|
4192
|
-
});
|
|
4193
|
-
}
|
|
4365
|
+
function showDetail(d) {
|
|
4366
|
+
const p=document.getElementById('detail');
|
|
4367
|
+
document.getElementById('d-name').textContent=d.id;
|
|
4368
|
+
document.getElementById('d-meta').innerHTML=i('detail.dir')+': '+esc(d.dir)+'<br>'+i('detail.dependencies')+': '+d.deps+' \\u00b7 '+i('detail.dependents')+': '+d.dependents;
|
|
4369
|
+
const deptL=document.getElementById('d-dependents'), depsL=document.getElementById('d-deps');
|
|
4370
|
+
deptL.innerHTML=(d.dependentsList||[]).map(x=>'<li data-focus="'+esc(x)+'">\\u2190 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
4371
|
+
depsL.innerHTML=(d.dependencies||[]).map(x=>'<li data-focus="'+esc(x)+'">\\u2192 '+esc(x)+'</li>').join('')||'<li style="color:var(--text-muted)">'+i('detail.none')+'</li>';
|
|
4372
|
+
p.classList.add('open');
|
|
4373
|
+
}
|
|
4374
|
+
// Event delegation for detail panel list items (avoids inline onclick)
|
|
4375
|
+
document.getElementById('d-dependents').addEventListener('click', function(e) { var li=e.target.closest('li[data-focus]'); if(li) focusNode(li.dataset.focus); });
|
|
4376
|
+
document.getElementById('d-deps').addEventListener('click', function(e) { var li=e.target.closest('li[data-focus]'); if(li) focusNode(li.dataset.focus); });
|
|
4377
|
+
window.closeDetail=()=>document.getElementById('detail').classList.remove('open');
|
|
4378
|
+
window.focusNode=(id)=>{
|
|
4379
|
+
const n=DATA.nodes.find(x=>x.id===id); if(!n)return; showDetail(n);
|
|
4380
|
+
svg.transition().duration(500).call(zoom.transform,d3.zoomIdentity.translate(W/2-n.x*1.5,H/2-n.y*1.5).scale(1.5));
|
|
4381
|
+
};
|
|
4194
4382
|
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
var rT=nodeRadius(d.target)*nodeScale, rS=nodeRadius(d.source)*nodeScale;
|
|
4200
|
-
d3.select(this).attr('x1',d.source.x+(dx/dist)*rS).attr('y1',d.source.y+(dy/dist)*rS)
|
|
4201
|
-
.attr('x2',d.target.x-(dx/dist)*rT).attr('y2',d.target.y-(dy/dist)*rT);
|
|
4202
|
-
});
|
|
4203
|
-
dNode.attr('transform', function(d) { return 'translate('+d.x+','+d.y+')'; });
|
|
4204
|
-
if (++dTickCount % 3 === 0) updateDiffHulls();
|
|
4205
|
-
});
|
|
4383
|
+
// Drag
|
|
4384
|
+
function dragStart(e,d){if(!e.active)simulation.alphaTarget(0.3).restart();d.fx=d.x;d.fy=d.y;}
|
|
4385
|
+
function dragging(e,d){d.fx=e.x;d.fy=e.y;}
|
|
4386
|
+
function dragEnd(e,d){if(!e.active)simulation.alphaTarget(0);}
|
|
4206
4387
|
|
|
4207
|
-
|
|
4388
|
+
// \u2500\u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4389
|
+
const searchInput=document.getElementById('search');
|
|
4390
|
+
document.addEventListener('keydown',e=>{
|
|
4391
|
+
if(e.key==='/'&&document.activeElement!==searchInput){e.preventDefault();searchInput.focus();}
|
|
4392
|
+
if(e.key==='Escape'){searchInput.value='';searchInput.blur();resetGraphHighlight();}
|
|
4393
|
+
});
|
|
4394
|
+
searchInput.addEventListener('input',e=>{
|
|
4395
|
+
const q=e.target.value.toLowerCase();
|
|
4396
|
+
if(!q){resetGraphHighlight();return;}
|
|
4397
|
+
node.select('circle').attr('opacity',d=>d.id.toLowerCase().includes(q)?1:0.06);
|
|
4398
|
+
node.select('text').attr('opacity',d=>d.id.toLowerCase().includes(q)?1:0.04);
|
|
4399
|
+
link.attr('opacity',0.03);
|
|
4400
|
+
});
|
|
4208
4401
|
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4402
|
+
// \u2500\u2500\u2500 Filters (click=toggle, hover=highlight nodes) \u2500\u2500
|
|
4403
|
+
if (!LAYERS) {
|
|
4404
|
+
// Non-layer mode: flat pills in filter-layer-row
|
|
4405
|
+
const filterRowEl=document.getElementById('filter-layer-row');
|
|
4406
|
+
DATA.dirs.forEach(dir=>{
|
|
4407
|
+
const pill=document.createElement('div');
|
|
4408
|
+
pill.className='filter-pill active';
|
|
4409
|
+
pill.innerHTML='<div class="pill-dot" style="background:'+dirColor(dir)+'"></div>'+esc(dir||'.')+' <span class="pill-count">'+dirCounts[dir]+'</span>';
|
|
4410
|
+
pill.onclick=()=>{
|
|
4411
|
+
if(activeDirs.has(dir)){activeDirs.delete(dir);pill.classList.remove('active');}
|
|
4412
|
+
else{activeDirs.add(dir);pill.classList.add('active');}
|
|
4413
|
+
applyFilter();
|
|
4414
|
+
};
|
|
4415
|
+
pill.onmouseenter=()=>{
|
|
4416
|
+
if(pinnedNode)return;
|
|
4417
|
+
node.select('circle').transition().duration(120).attr('opacity',d=>d.dir===dir?1:0.1);
|
|
4418
|
+
node.select('text').transition().duration(120).attr('opacity',d=>d.dir===dir?1:0.05);
|
|
4419
|
+
};
|
|
4420
|
+
pill.onmouseleave=()=>{
|
|
4421
|
+
if(pinnedNode)return;
|
|
4422
|
+
node.select('circle').transition().duration(150).attr('opacity',1);
|
|
4423
|
+
node.select('text').transition().duration(150).attr('opacity',d=>d.dependents>=1||d.deps>=3?1:0.5);
|
|
4424
|
+
};
|
|
4425
|
+
filterRowEl.appendChild(pill);
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
function applyFilter(){
|
|
4429
|
+
if (LAYERS) {
|
|
4430
|
+
// Delegate to layer-aware filter
|
|
4431
|
+
if (typeof applyLayerFilter === 'function') { applyLayerFilter(); return; }
|
|
4214
4432
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
if(tab.dataset.view==='diff-view'&&!diffBuilt){buildDiffView();diffBuilt=true;}
|
|
4221
|
-
});
|
|
4433
|
+
node.attr('display',d=>activeDirs.has(d.dir)?null:'none');
|
|
4434
|
+
link.attr('display',l=>{
|
|
4435
|
+
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
4436
|
+
const sD=DATA.nodes.find(n=>n.id===s)?.dir,tD=DATA.nodes.find(n=>n.id===t)?.dir;
|
|
4437
|
+
return activeDirs.has(sD)&&activeDirs.has(tD)?null:'none';
|
|
4222
4438
|
});
|
|
4223
4439
|
}
|
|
4224
4440
|
|
|
4441
|
+
// \u2500\u2500\u2500 Impact simulation mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4442
|
+
let impactMode=false;
|
|
4443
|
+
const impactBadge=document.getElementById('impact-badge');
|
|
4444
|
+
window.toggleImpactMode=()=>{
|
|
4445
|
+
impactMode=!impactMode;
|
|
4446
|
+
document.getElementById('impact-btn').classList.toggle('active',impactMode);
|
|
4447
|
+
if(!impactMode){impactBadge.style.display='none';resetGraphHighlight();}
|
|
4448
|
+
};
|
|
4449
|
+
function getTransitiveDependents(startId){
|
|
4450
|
+
const result=new Set();const queue=[startId];
|
|
4451
|
+
const revMap={};
|
|
4452
|
+
DATA.links.forEach(l=>{const s=l.source.id??l.source,t=l.target.id??l.target;if(!revMap[t])revMap[t]=[];revMap[t].push(s);});
|
|
4453
|
+
while(queue.length){const id=queue.shift();if(result.has(id))continue;result.add(id);(revMap[id]||[]).forEach(x=>queue.push(x));}
|
|
4454
|
+
return result;
|
|
4455
|
+
}
|
|
4456
|
+
// Override click in impact mode
|
|
4457
|
+
const origClick=node.on('click');
|
|
4458
|
+
node.on('click',(e,d)=>{
|
|
4459
|
+
if(!impactMode){e.stopPropagation();pinnedNode=d;highlightNode(d);showDetail(d);return;}
|
|
4460
|
+
e.stopPropagation();
|
|
4461
|
+
const affected=getTransitiveDependents(d.id);
|
|
4462
|
+
node.select('circle').transition().duration(200).attr('opacity',n=>affected.has(n.id)?1:0.06)
|
|
4463
|
+
.attr('stroke',n=>affected.has(n.id)&&n.id!==d.id?'var(--red)':n.deps>=5?'var(--yellow)':nodeColor(n))
|
|
4464
|
+
.attr('stroke-width',n=>affected.has(n.id)?3:1.5);
|
|
4465
|
+
node.select('text').transition().duration(200).attr('opacity',n=>affected.has(n.id)?1:0.04);
|
|
4466
|
+
link.transition().duration(200).attr('opacity',l=>{
|
|
4467
|
+
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
4468
|
+
return affected.has(s)&&affected.has(t)?0.8:0.03;
|
|
4469
|
+
}).attr('stroke',l=>{
|
|
4470
|
+
const s=l.source.id??l.source,t=l.target.id??l.target;
|
|
4471
|
+
return affected.has(s)&&affected.has(t)?'var(--red)':l.type==='type-only'?'#1f3d5c':'#30363d';
|
|
4472
|
+
});
|
|
4473
|
+
impactBadge.textContent=d.id.split('/').pop()+' \u2192 '+(affected.size-1)+' '+i('impact.transitive');
|
|
4474
|
+
impactBadge.style.display='block';
|
|
4475
|
+
});
|
|
4476
|
+
|
|
4477
|
+
window.addEventListener('resize',()=>{
|
|
4478
|
+
const w=window.innerWidth,h=window.innerHeight-44;
|
|
4479
|
+
svg.attr('width',w).attr('height',h);
|
|
4480
|
+
});
|
|
4481
|
+
|
|
4482
|
+
${buildHierarchyJs()}
|
|
4483
|
+
${buildDiffJs(diffData)}
|
|
4225
4484
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
4226
4485
|
// INIT
|
|
4227
4486
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
@@ -4279,36 +4538,13 @@ var VERSION = loadVersion();
|
|
|
4279
4538
|
|
|
4280
4539
|
// src/cli/index.ts
|
|
4281
4540
|
var VALID_LANGUAGES = LANGUAGE_IDS;
|
|
4282
|
-
async function
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
if (layerConfig) {
|
|
4287
|
-
const multi = await analyzeMultiLayer(opts.root, layerConfig.layers);
|
|
4288
|
-
const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
|
|
4289
|
-
const manualConnections = layerConfig.connections ?? [];
|
|
4290
|
-
const manualKeys = new Set(manualConnections.map(
|
|
4291
|
-
(c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
|
|
4292
|
-
));
|
|
4293
|
-
const merged = [
|
|
4294
|
-
...manualConnections,
|
|
4295
|
-
...autoConnections.filter(
|
|
4296
|
-
(c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
|
|
4297
|
-
)
|
|
4298
|
-
];
|
|
4299
|
-
return {
|
|
4300
|
-
graph: multi.merged,
|
|
4301
|
-
multiLayer: multi,
|
|
4302
|
-
layerMetadata: multi.layerMetadata,
|
|
4303
|
-
crossLayerEdges: merged
|
|
4304
|
-
};
|
|
4305
|
-
}
|
|
4306
|
-
}
|
|
4307
|
-
const graph = await analyzeProject(opts.target, {
|
|
4541
|
+
async function resolveGraphCli(opts) {
|
|
4542
|
+
return resolveGraph({
|
|
4543
|
+
targetDir: opts.target,
|
|
4544
|
+
projectRoot: opts.root,
|
|
4308
4545
|
exclude: opts.exclude,
|
|
4309
4546
|
language: opts.language
|
|
4310
4547
|
});
|
|
4311
|
-
return { graph };
|
|
4312
4548
|
}
|
|
4313
4549
|
var program = new Command();
|
|
4314
4550
|
program.name("archtracker").description(
|
|
@@ -4326,7 +4562,7 @@ program.command("init").description("Generate initial snapshot and save to .arch
|
|
|
4326
4562
|
try {
|
|
4327
4563
|
const language = validateLanguage(opts.language);
|
|
4328
4564
|
console.log(t("cli.analyzing"));
|
|
4329
|
-
const { graph, multiLayer } = await
|
|
4565
|
+
const { graph, multiLayer } = await resolveGraphCli({
|
|
4330
4566
|
target: opts.target,
|
|
4331
4567
|
root: opts.root,
|
|
4332
4568
|
exclude: opts.exclude,
|
|
@@ -4363,7 +4599,7 @@ program.command("analyze").description(
|
|
|
4363
4599
|
try {
|
|
4364
4600
|
const language = validateLanguage(opts.language);
|
|
4365
4601
|
console.log(t("cli.analyzing"));
|
|
4366
|
-
const { graph, multiLayer } = await
|
|
4602
|
+
const { graph, multiLayer } = await resolveGraphCli({
|
|
4367
4603
|
target: opts.target,
|
|
4368
4604
|
root: opts.root,
|
|
4369
4605
|
exclude: opts.exclude,
|
|
@@ -4390,7 +4626,7 @@ program.command("check").description(
|
|
|
4390
4626
|
process.exit(1);
|
|
4391
4627
|
}
|
|
4392
4628
|
console.log(t("cli.analyzing"));
|
|
4393
|
-
const { graph: currentGraph } = await
|
|
4629
|
+
const { graph: currentGraph } = await resolveGraphCli({
|
|
4394
4630
|
target: opts.target,
|
|
4395
4631
|
root: opts.root,
|
|
4396
4632
|
language
|
|
@@ -4414,7 +4650,7 @@ program.command("context").description(
|
|
|
4414
4650
|
let snapshot = await loadSnapshot(opts.root);
|
|
4415
4651
|
if (!snapshot) {
|
|
4416
4652
|
console.log(t("cli.autoGenerating"));
|
|
4417
|
-
const result = await
|
|
4653
|
+
const result = await resolveGraphCli({
|
|
4418
4654
|
target: opts.target,
|
|
4419
4655
|
root: opts.root,
|
|
4420
4656
|
language
|
|
@@ -4462,7 +4698,7 @@ program.command("serve").description(
|
|
|
4462
4698
|
console.log(t("web.starting"));
|
|
4463
4699
|
console.log(t("cli.analyzing"));
|
|
4464
4700
|
let diff = null;
|
|
4465
|
-
const result = await
|
|
4701
|
+
const result = await resolveGraphCli({
|
|
4466
4702
|
target: opts.target,
|
|
4467
4703
|
root: opts.root,
|
|
4468
4704
|
exclude: opts.exclude,
|
|
@@ -4489,7 +4725,7 @@ program.command("serve").description(
|
|
|
4489
4725
|
debounce = setTimeout(async () => {
|
|
4490
4726
|
try {
|
|
4491
4727
|
console.log(t("web.reloading"));
|
|
4492
|
-
const newResult = await
|
|
4728
|
+
const newResult = await resolveGraphCli({
|
|
4493
4729
|
target: opts.target,
|
|
4494
4730
|
root: opts.root,
|
|
4495
4731
|
exclude: opts.exclude,
|