@bfra.me/workspace-analyzer 0.1.0 → 0.2.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.
@@ -1310,12 +1310,12 @@ function findCycles(graph) {
1310
1310
  const cycles = [];
1311
1311
  const visited = /* @__PURE__ */ new Set();
1312
1312
  const recursionStack = /* @__PURE__ */ new Set();
1313
- const path16 = [];
1313
+ const path17 = [];
1314
1314
  function dfs(nodeId) {
1315
1315
  if (recursionStack.has(nodeId)) {
1316
- const cycleStart = path16.indexOf(nodeId);
1316
+ const cycleStart = path17.indexOf(nodeId);
1317
1317
  if (cycleStart !== -1) {
1318
- const cycleNodes = path16.slice(cycleStart);
1318
+ const cycleNodes = path17.slice(cycleStart);
1319
1319
  cycleNodes.push(nodeId);
1320
1320
  const cycleEdges = [];
1321
1321
  for (let i = 0; i < cycleNodes.length - 1; i++) {
@@ -1339,14 +1339,14 @@ function findCycles(graph) {
1339
1339
  }
1340
1340
  visited.add(nodeId);
1341
1341
  recursionStack.add(nodeId);
1342
- path16.push(nodeId);
1342
+ path17.push(nodeId);
1343
1343
  const node = graph.nodes.get(nodeId);
1344
1344
  if (node !== void 0) {
1345
1345
  for (const importId of node.imports) {
1346
1346
  dfs(importId);
1347
1347
  }
1348
1348
  }
1349
- path16.pop();
1349
+ path17.pop();
1350
1350
  recursionStack.delete(nodeId);
1351
1351
  }
1352
1352
  for (const nodeId of graph.nodes.keys()) {
@@ -5528,15 +5528,15 @@ var architecturalRulesSchema = z.object({
5528
5528
  enforcePublicApi: z.boolean().optional().describe("Enforce explicit public API exports")
5529
5529
  }).strict();
5530
5530
  var analyzerConfigSchema = z.object({
5531
- include: z.array(z.string()).optional().default(DEFAULT_ANALYZER_CONFIG.include).describe("Glob patterns for files to include in analysis"),
5532
- exclude: z.array(z.string()).optional().default(DEFAULT_ANALYZER_CONFIG.exclude).describe("Glob patterns for files to exclude from analysis"),
5531
+ include: z.array(z.string()).optional().default([...DEFAULT_ANALYZER_CONFIG.include]).describe("Glob patterns for files to include in analysis"),
5532
+ exclude: z.array(z.string()).optional().default([...DEFAULT_ANALYZER_CONFIG.exclude]).describe("Glob patterns for files to exclude from analysis"),
5533
5533
  minSeverity: severitySchema.optional().default(DEFAULT_ANALYZER_CONFIG.minSeverity).describe("Minimum severity level to report"),
5534
5534
  categories: z.array(categorySchema).optional().default([]).describe("Categories of issues to check (empty means all)"),
5535
5535
  cache: z.boolean().optional().default(true).describe("Enable incremental analysis caching"),
5536
5536
  rules: ruleConfigSchema.optional().default({}).describe("Custom rules configuration")
5537
5537
  }).strict();
5538
5538
  var workspaceOptionsSchema = z.object({
5539
- packagePatterns: z.array(z.string()).optional().default(DEFAULT_PACKAGE_PATTERNS).describe("Glob patterns for package locations"),
5539
+ packagePatterns: z.array(z.string()).optional().default([...DEFAULT_PACKAGE_PATTERNS]).describe("Glob patterns for package locations"),
5540
5540
  concurrency: z.number().int().min(1).max(16).optional().default(DEFAULT_CONCURRENCY).describe("Maximum parallel analysis operations"),
5541
5541
  cacheDir: z.string().optional().default(DEFAULT_CACHE_DIR).describe("Directory for analysis cache files"),
5542
5542
  maxCacheAge: z.number().int().min(0).optional().default(DEFAULT_MAX_CACHE_AGE).describe("Maximum cache age in milliseconds"),
@@ -6987,6 +6987,3669 @@ function getStatusEmoji(severity, useEmoji) {
6987
6987
  return SEVERITY_CONFIG[severity].emoji;
6988
6988
  }
6989
6989
 
6990
+ // src/visualizer/types.ts
6991
+ var DEFAULT_VISUALIZER_OPTIONS = {
6992
+ outputPath: "./workspace-graph.html",
6993
+ format: "html",
6994
+ autoOpen: true,
6995
+ title: "Workspace Dependency Graph",
6996
+ filters: {},
6997
+ includeTypeImports: true,
6998
+ maxNodes: 1e3
6999
+ };
7000
+ var SEVERITY_ORDER2 = ["critical", "error", "warning", "info"];
7001
+ function getHighestSeverity(severities) {
7002
+ if (severities.length === 0) {
7003
+ return void 0;
7004
+ }
7005
+ for (const level of SEVERITY_ORDER2) {
7006
+ if (severities.includes(level)) {
7007
+ return level;
7008
+ }
7009
+ }
7010
+ return severities[0];
7011
+ }
7012
+
7013
+ // src/visualizer/graph-builder.ts
7014
+ import { ok as ok23 } from "@bfra.me/es/result";
7015
+ var DEFAULT_BUILD_OPTIONS = {
7016
+ includeTypeImports: true,
7017
+ maxNodes: 1e3,
7018
+ title: "Workspace Dependency Graph"
7019
+ };
7020
+ function transformNodeToVisualization(node, context, nodeIdsInCycles) {
7021
+ const { cycles, issues, layerConfig, packageMap } = context;
7022
+ const cycleNodeSet = nodeIdsInCycles ?? buildCycleNodeSet(cycles);
7023
+ const isInCycle = cycleNodeSet.has(node.id);
7024
+ const nodeViolations = mapIssuesToViolations(node.id, issues);
7025
+ const severities = nodeViolations.map((v) => v.severity);
7026
+ const highestSeverity = getHighestSeverity(severities);
7027
+ const layer = layerConfig === void 0 ? void 0 : getFileLayer(node.filePath, layerConfig);
7028
+ const packageName = packageMap.get(node.filePath) ?? node.packageName;
7029
+ return {
7030
+ id: node.id,
7031
+ name: node.name,
7032
+ filePath: node.filePath,
7033
+ packageName,
7034
+ layer,
7035
+ importsCount: node.imports.length,
7036
+ importedByCount: node.importedBy.length,
7037
+ isInCycle,
7038
+ violations: nodeViolations,
7039
+ highestViolationSeverity: highestSeverity
7040
+ };
7041
+ }
7042
+ function transformEdgeToVisualization(edge, cycleEdgeSet, edgeToCycleId) {
7043
+ const edgeKey = `${edge.from}->${edge.to}`;
7044
+ const isInCycle = cycleEdgeSet.has(edgeKey);
7045
+ const cycleId = edgeToCycleId.get(edgeKey);
7046
+ const type = mapImportType(edge.type, edge.isTypeOnly);
7047
+ return {
7048
+ source: edge.from,
7049
+ target: edge.to,
7050
+ type,
7051
+ isInCycle,
7052
+ cycleId
7053
+ };
7054
+ }
7055
+ function transformCycleToVisualization(cycle, index) {
7056
+ const id = `cycle-${index + 1}`;
7057
+ const edges = cycle.edges.map((edge) => ({
7058
+ from: edge.from,
7059
+ to: edge.to
7060
+ }));
7061
+ return {
7062
+ id,
7063
+ nodes: cycle.nodes,
7064
+ edges,
7065
+ length: cycle.nodes.length,
7066
+ description: cycle.description
7067
+ };
7068
+ }
7069
+ function buildVisualizationData(context, options = {}) {
7070
+ const opts = { ...DEFAULT_BUILD_OPTIONS, ...options };
7071
+ const { graph, cycles, layerConfig, analyzerVersion } = context;
7072
+ const vizCycles = cycles.map((cycle, index) => transformCycleToVisualization(cycle, index));
7073
+ const { cycleEdgeSet, edgeToCycleId } = buildCycleEdgeMaps(vizCycles);
7074
+ const cycleNodeIds = buildCycleNodeSet(cycles);
7075
+ const filteredEdges = opts.includeTypeImports ? graph.edges : graph.edges.filter((e) => !e.isTypeOnly);
7076
+ const vizEdges = filteredEdges.map(
7077
+ (edge) => transformEdgeToVisualization(edge, cycleEdgeSet, edgeToCycleId)
7078
+ );
7079
+ const nodeIds = /* @__PURE__ */ new Set();
7080
+ for (const edge of filteredEdges) {
7081
+ nodeIds.add(edge.from);
7082
+ nodeIds.add(edge.to);
7083
+ }
7084
+ let vizNodes = [];
7085
+ for (const nodeId of nodeIds) {
7086
+ const node = graph.nodes.get(nodeId);
7087
+ if (node !== void 0) {
7088
+ vizNodes.push(transformNodeToVisualization(node, context, cycleNodeIds));
7089
+ }
7090
+ }
7091
+ if (vizNodes.length > opts.maxNodes) {
7092
+ vizNodes = prioritizeNodes(vizNodes, opts.maxNodes);
7093
+ }
7094
+ const includedNodeIds = new Set(vizNodes.map((n) => n.id));
7095
+ const filteredVizEdges = vizEdges.filter(
7096
+ (e) => includedNodeIds.has(e.source) && includedNodeIds.has(e.target)
7097
+ );
7098
+ const statistics = computeVisualizationStatistics(vizNodes, filteredVizEdges, vizCycles, context);
7099
+ const layers = layerConfig?.layers.map((layer) => ({
7100
+ name: layer.name,
7101
+ allowedDependencies: layer.allowedDependencies
7102
+ })) ?? [];
7103
+ const metadata = {
7104
+ workspacePath: graph.rootPath,
7105
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7106
+ analyzerVersion
7107
+ };
7108
+ return ok23({
7109
+ nodes: vizNodes,
7110
+ edges: filteredVizEdges,
7111
+ cycles: vizCycles,
7112
+ statistics,
7113
+ layers,
7114
+ metadata
7115
+ });
7116
+ }
7117
+ function buildCycleEdgeMaps(cycles) {
7118
+ const cycleEdgeSet = /* @__PURE__ */ new Set();
7119
+ const edgeToCycleId = /* @__PURE__ */ new Map();
7120
+ for (const cycle of cycles) {
7121
+ for (const edge of cycle.edges) {
7122
+ const key = `${edge.from}->${edge.to}`;
7123
+ cycleEdgeSet.add(key);
7124
+ edgeToCycleId.set(key, cycle.id);
7125
+ }
7126
+ }
7127
+ return { cycleEdgeSet, edgeToCycleId };
7128
+ }
7129
+ function buildCycleNodeSet(cycles) {
7130
+ const nodeIds = /* @__PURE__ */ new Set();
7131
+ for (const cycle of cycles) {
7132
+ for (const nodeId of cycle.nodes) {
7133
+ nodeIds.add(nodeId);
7134
+ }
7135
+ }
7136
+ return nodeIds;
7137
+ }
7138
+ function mapImportType(type, isTypeOnly) {
7139
+ if (isTypeOnly || type === "type-only") {
7140
+ return "type-only";
7141
+ }
7142
+ if (type === "dynamic") {
7143
+ return "dynamic";
7144
+ }
7145
+ if (type === "require") {
7146
+ return "require";
7147
+ }
7148
+ return "static";
7149
+ }
7150
+ function mapIssuesToViolations(nodeId, issues) {
7151
+ const violations = [];
7152
+ for (const issue of issues) {
7153
+ const normalizedIssuePath = issue.location.filePath.replaceAll("\\", "/");
7154
+ const normalizedNodeId = nodeId.replaceAll("\\", "/");
7155
+ if (normalizedIssuePath.endsWith(normalizedNodeId) || normalizedIssuePath === normalizedNodeId) {
7156
+ violations.push({
7157
+ id: `${issue.id}-${violations.length}`,
7158
+ message: issue.description,
7159
+ severity: issue.severity,
7160
+ ruleId: issue.id
7161
+ });
7162
+ }
7163
+ }
7164
+ return violations;
7165
+ }
7166
+ function prioritizeNodes(nodes, maxNodes) {
7167
+ const scored = nodes.map((node) => {
7168
+ let score = 0;
7169
+ if (node.isInCycle) {
7170
+ score += 100;
7171
+ }
7172
+ if (node.highestViolationSeverity !== void 0) {
7173
+ const severityIndex = SEVERITY_ORDER2.indexOf(node.highestViolationSeverity);
7174
+ score += (SEVERITY_ORDER2.length - severityIndex) * 20;
7175
+ }
7176
+ score += node.importsCount + node.importedByCount;
7177
+ return { node, score };
7178
+ });
7179
+ scored.sort((a, b) => b.score - a.score);
7180
+ return scored.slice(0, maxNodes).map((s) => s.node);
7181
+ }
7182
+ function computeVisualizationStatistics(nodes, edges, cycles, _context) {
7183
+ const nodesByLayer = {};
7184
+ const violationsBySeverity = {
7185
+ info: 0,
7186
+ warning: 0,
7187
+ error: 0,
7188
+ critical: 0
7189
+ };
7190
+ const packagesSet = /* @__PURE__ */ new Set();
7191
+ const filesSet = /* @__PURE__ */ new Set();
7192
+ for (const node of nodes) {
7193
+ filesSet.add(node.filePath);
7194
+ if (node.packageName !== void 0) {
7195
+ packagesSet.add(node.packageName);
7196
+ }
7197
+ if (node.layer !== void 0) {
7198
+ nodesByLayer[node.layer] = (nodesByLayer[node.layer] ?? 0) + 1;
7199
+ }
7200
+ for (const violation of node.violations) {
7201
+ violationsBySeverity[violation.severity]++;
7202
+ }
7203
+ }
7204
+ return {
7205
+ totalNodes: nodes.length,
7206
+ totalEdges: edges.length,
7207
+ totalCycles: cycles.length,
7208
+ nodesByLayer,
7209
+ violationsBySeverity,
7210
+ packagesAnalyzed: packagesSet.size,
7211
+ filesAnalyzed: filesSet.size
7212
+ };
7213
+ }
7214
+
7215
+ // src/visualizer/html-renderer.ts
7216
+ import { err as err6, ok as ok24 } from "@bfra.me/es/result";
7217
+
7218
+ // src/visualizer/templates/graph-template.ts
7219
+ function generateGraphInitScript() {
7220
+ return `
7221
+ /**
7222
+ * Dependency Graph Visualization
7223
+ * Generated by @bfra.me/workspace-analyzer
7224
+ */
7225
+ (function() {
7226
+ 'use strict';
7227
+
7228
+ // Get visualization data from global scope
7229
+ const visualizationData = window.VISUALIZATION_DATA;
7230
+ if (!visualizationData) {
7231
+ console.error('Visualization data not found');
7232
+ return;
7233
+ }
7234
+
7235
+ // Extract data
7236
+ const { nodes, edges, cycles, statistics, layers, metadata } = visualizationData;
7237
+
7238
+ // Configuration
7239
+ const config = {
7240
+ nodeRadius: 8,
7241
+ linkDistance: 80,
7242
+ chargeStrength: -300,
7243
+ centerStrength: 0.05,
7244
+ collisionRadiusMultiplier: 1.5,
7245
+ zoomExtent: [0.1, 4],
7246
+ alphaDecay: 0.0228,
7247
+ velocityDecay: 0.4
7248
+ };
7249
+
7250
+ // State management
7251
+ const state = {
7252
+ filters: {
7253
+ layers: new Set(layers.map(l => l.name)),
7254
+ severities: new Set(['critical', 'error', 'warning', 'info']),
7255
+ packages: new Set(),
7256
+ showCyclesOnly: false,
7257
+ showViolationsOnly: false,
7258
+ searchQuery: ''
7259
+ },
7260
+ selectedNode: null,
7261
+ highlightedNodes: new Set(),
7262
+ transform: d3.zoomIdentity
7263
+ };
7264
+
7265
+ // Color scales
7266
+ const severityColors = {
7267
+ critical: '#dc2626',
7268
+ error: '#ea580c',
7269
+ warning: '#ca8a04',
7270
+ info: '#2563eb'
7271
+ };
7272
+
7273
+ const layerColors = {
7274
+ domain: '#8b5cf6',
7275
+ application: '#06b6d4',
7276
+ infrastructure: '#84cc16',
7277
+ presentation: '#f97316',
7278
+ shared: '#6b7280',
7279
+ unknown: '#9ca3af'
7280
+ };
7281
+
7282
+ // Get container dimensions
7283
+ const container = document.querySelector('.graph-container');
7284
+ const width = container.clientWidth;
7285
+ const height = container.clientHeight;
7286
+
7287
+ // Create SVG
7288
+ const svg = d3.select('.graph-canvas')
7289
+ .attr('viewBox', [0, 0, width, height])
7290
+ .attr('preserveAspectRatio', 'xMidYMid meet');
7291
+
7292
+ // Create groups for layering
7293
+ const g = svg.append('g').attr('class', 'graph-layer');
7294
+ const edgeGroup = g.append('g').attr('class', 'edges');
7295
+ const nodeGroup = g.append('g').attr('class', 'nodes');
7296
+
7297
+ // Arrow marker definition
7298
+ svg.append('defs').append('marker')
7299
+ .attr('id', 'arrow')
7300
+ .attr('viewBox', '0 -5 10 10')
7301
+ .attr('refX', 20)
7302
+ .attr('refY', 0)
7303
+ .attr('markerWidth', 6)
7304
+ .attr('markerHeight', 6)
7305
+ .attr('orient', 'auto')
7306
+ .append('path')
7307
+ .attr('d', 'M0,-5L10,0L0,5')
7308
+ .attr('class', 'edge-arrow');
7309
+
7310
+ // Arrow marker for cycle edges
7311
+ svg.select('defs').append('marker')
7312
+ .attr('id', 'arrow-cycle')
7313
+ .attr('viewBox', '0 -5 10 10')
7314
+ .attr('refX', 20)
7315
+ .attr('refY', 0)
7316
+ .attr('markerWidth', 6)
7317
+ .attr('markerHeight', 6)
7318
+ .attr('orient', 'auto')
7319
+ .append('path')
7320
+ .attr('d', 'M0,-5L10,0L0,5')
7321
+ .attr('class', 'edge-arrow cycle');
7322
+
7323
+ // Create simulation data with mutable copies
7324
+ const simNodes = nodes.map(n => ({...n}));
7325
+ const simEdges = edges.map(e => ({
7326
+ ...e,
7327
+ source: e.source,
7328
+ target: e.target
7329
+ }));
7330
+
7331
+ // Create node ID lookup
7332
+ const nodeById = new Map(simNodes.map(n => [n.id, n]));
7333
+
7334
+ // Force simulation
7335
+ const simulation = d3.forceSimulation(simNodes)
7336
+ .force('link', d3.forceLink(simEdges)
7337
+ .id(d => d.id)
7338
+ .distance(config.linkDistance))
7339
+ .force('charge', d3.forceManyBody()
7340
+ .strength(config.chargeStrength))
7341
+ .force('center', d3.forceCenter(width / 2, height / 2)
7342
+ .strength(config.centerStrength))
7343
+ .force('collide', d3.forceCollide()
7344
+ .radius(config.nodeRadius * config.collisionRadiusMultiplier))
7345
+ .alphaDecay(config.alphaDecay)
7346
+ .velocityDecay(config.velocityDecay);
7347
+
7348
+ // Zoom behavior
7349
+ const zoom = d3.zoom()
7350
+ .scaleExtent(config.zoomExtent)
7351
+ .on('zoom', (event) => {
7352
+ state.transform = event.transform;
7353
+ g.attr('transform', 'translate(' + event.transform.x + ',' + event.transform.y + ') scale(' + event.transform.k + ')');
7354
+ updateZoomDisplay();
7355
+ });
7356
+
7357
+ svg.call(zoom);
7358
+
7359
+ // Draw edges
7360
+ function drawEdges() {
7361
+ const filteredEdges = getFilteredEdges();
7362
+
7363
+ const edge = edgeGroup.selectAll('.graph-edge')
7364
+ .data(filteredEdges, d => d.source.id + '-' + d.target.id);
7365
+
7366
+ edge.exit().remove();
7367
+
7368
+ const edgeEnter = edge.enter()
7369
+ .append('g')
7370
+ .attr('class', 'graph-edge');
7371
+
7372
+ // Edge line
7373
+ edgeEnter.append('line')
7374
+ .attr('class', d => getEdgeClass(d))
7375
+ .attr('marker-end', d => d.isInCycle ? 'url(#arrow-cycle)' : 'url(#arrow)')
7376
+ .on('mouseenter', handleEdgeMouseEnter)
7377
+ .on('mouseleave', handleEdgeMouseLeave)
7378
+ .on('click', handleEdgeClick);
7379
+
7380
+ // Merge and update
7381
+ const edgeMerged = edgeEnter.merge(edge);
7382
+
7383
+ edgeMerged.select('line')
7384
+ .attr('class', d => getEdgeClass(d));
7385
+
7386
+ return edgeMerged;
7387
+ }
7388
+
7389
+ // Draw nodes
7390
+ function drawNodes() {
7391
+ const filteredNodes = getFilteredNodes();
7392
+
7393
+ const node = nodeGroup.selectAll('.graph-node')
7394
+ .data(filteredNodes, d => d.id);
7395
+
7396
+ node.exit().remove();
7397
+
7398
+ const nodeEnter = node.enter()
7399
+ .append('g')
7400
+ .attr('class', 'graph-node')
7401
+ .call(drag(simulation));
7402
+
7403
+ // Node circle
7404
+ nodeEnter.append('circle')
7405
+ .attr('r', config.nodeRadius)
7406
+ .attr('class', d => getNodeClass(d));
7407
+
7408
+ // Node label
7409
+ nodeEnter.append('text')
7410
+ .attr('class', 'node-label')
7411
+ .attr('dy', config.nodeRadius + 12)
7412
+ .text(d => getNodeLabel(d));
7413
+
7414
+ // Merge and update
7415
+ const nodeMerged = nodeEnter.merge(node);
7416
+
7417
+ nodeMerged.select('circle')
7418
+ .attr('class', d => getNodeClass(d));
7419
+
7420
+ // Event handlers
7421
+ nodeMerged
7422
+ .on('mouseenter', handleNodeMouseEnter)
7423
+ .on('mouseleave', handleNodeMouseLeave)
7424
+ .on('click', handleNodeClick);
7425
+
7426
+ return nodeMerged;
7427
+ }
7428
+
7429
+ // Get CSS class for edge
7430
+ function getEdgeClass(edge) {
7431
+ const classes = ['edge-line'];
7432
+ if (edge.isInCycle) classes.push('cycle');
7433
+ classes.push('type-' + edge.type);
7434
+ return classes.join(' ');
7435
+ }
7436
+
7437
+ // Get CSS class for node
7438
+ function getNodeClass(node) {
7439
+ const classes = ['node-circle'];
7440
+
7441
+ // Cycle highlighting
7442
+ if (node.isInCycle) classes.push('cycle');
7443
+
7444
+ // Severity-based color (takes precedence)
7445
+ if (node.highestViolationSeverity) {
7446
+ classes.push('severity-' + node.highestViolationSeverity);
7447
+ } else if (node.layer) {
7448
+ // Layer-based color
7449
+ classes.push('layer-' + node.layer.toLowerCase());
7450
+ } else {
7451
+ classes.push('default');
7452
+ }
7453
+
7454
+ return classes.join(' ');
7455
+ }
7456
+
7457
+ // Get display label for node
7458
+ function getNodeLabel(node) {
7459
+ // Show just the filename
7460
+ const parts = node.name.split('/');
7461
+ return parts[parts.length - 1] || node.name;
7462
+ }
7463
+
7464
+ // Filtering
7465
+ function getFilteredNodes() {
7466
+ return simNodes.filter(node => {
7467
+ // Layer filter
7468
+ if (node.layer && !state.filters.layers.has(node.layer)) {
7469
+ return false;
7470
+ }
7471
+
7472
+ // Severity filter - only filter out nodes with violations if their highest severity is not selected
7473
+ if (node.highestViolationSeverity && !state.filters.severities.has(node.highestViolationSeverity)) {
7474
+ return false;
7475
+ }
7476
+
7477
+ // Cycles only
7478
+ if (state.filters.showCyclesOnly && !node.isInCycle) {
7479
+ return false;
7480
+ }
7481
+
7482
+ // Violations only
7483
+ if (state.filters.showViolationsOnly && node.violations.length === 0) {
7484
+ return false;
7485
+ }
7486
+
7487
+ // Search query
7488
+ if (state.filters.searchQuery) {
7489
+ const query = state.filters.searchQuery.toLowerCase();
7490
+ if (!node.name.toLowerCase().includes(query) &&
7491
+ !node.filePath.toLowerCase().includes(query) &&
7492
+ !(node.packageName && node.packageName.toLowerCase().includes(query))) {
7493
+ return false;
7494
+ }
7495
+ }
7496
+
7497
+ return true;
7498
+ });
7499
+ }
7500
+
7501
+ function getFilteredEdges() {
7502
+ const filteredNodeIds = new Set(getFilteredNodes().map(n => n.id));
7503
+ return simEdges.filter(edge => {
7504
+ const sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
7505
+ const targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
7506
+ return filteredNodeIds.has(sourceId) && filteredNodeIds.has(targetId);
7507
+ });
7508
+ }
7509
+
7510
+ // Drag behavior
7511
+ function drag(simulation) {
7512
+ function dragstarted(event) {
7513
+ if (!event.active) simulation.alphaTarget(0.3).restart();
7514
+ event.subject.fx = event.subject.x;
7515
+ event.subject.fy = event.subject.y;
7516
+ }
7517
+
7518
+ function dragged(event) {
7519
+ event.subject.fx = event.x;
7520
+ event.subject.fy = event.y;
7521
+ }
7522
+
7523
+ function dragended(event) {
7524
+ if (!event.active) simulation.alphaTarget(0);
7525
+ event.subject.fx = null;
7526
+ event.subject.fy = null;
7527
+ }
7528
+
7529
+ return d3.drag()
7530
+ .on('start', dragstarted)
7531
+ .on('drag', dragged)
7532
+ .on('end', dragended);
7533
+ }
7534
+
7535
+ // Tooltip
7536
+ const tooltip = d3.select('.tooltip');
7537
+
7538
+ function handleNodeMouseEnter(event, d) {
7539
+ // Show tooltip
7540
+ showTooltip(event, d);
7541
+
7542
+ // Highlight connected nodes
7543
+ highlightConnectedNodes(d);
7544
+ }
7545
+
7546
+ function handleNodeMouseLeave(event, d) {
7547
+ hideTooltip();
7548
+ clearHighlights();
7549
+ }
7550
+
7551
+ function handleNodeClick(event, d) {
7552
+ state.selectedNode = state.selectedNode === d ? null : d;
7553
+ if (state.selectedNode) {
7554
+ centerOnNode(d);
7555
+ }
7556
+ }
7557
+
7558
+ function handleEdgeMouseEnter(event, d) {
7559
+ showEdgeTooltip(event, d);
7560
+ }
7561
+
7562
+ function handleEdgeMouseLeave(event, d) {
7563
+ hideTooltip();
7564
+ }
7565
+
7566
+ function handleEdgeClick(event, d) {
7567
+ event.stopPropagation();
7568
+ showEdgeTooltip(event, d);
7569
+ }
7570
+
7571
+ function showTooltip(event, node) {
7572
+ const iconClass = node.highestViolationSeverity
7573
+ ? 'severity-' + node.highestViolationSeverity
7574
+ : node.isInCycle ? 'cycle' : 'default';
7575
+
7576
+ let violationsHtml = '';
7577
+ if (node.violations.length > 0) {
7578
+ violationsHtml = '<div class="tooltip-violations">' +
7579
+ '<div class="tooltip-violations-title">Violations</div>' +
7580
+ node.violations.slice(0, 5).map(v =>
7581
+ '<div class="tooltip-violation">' +
7582
+ '<div class="tooltip-violation-dot severity-dot ' + v.severity + '"></div>' +
7583
+ '<div class="tooltip-violation-text">' + escapeHtml(v.message) + '</div>' +
7584
+ '</div>'
7585
+ ).join('') +
7586
+ (node.violations.length > 5 ? '<div class="tooltip-violation"><div class="tooltip-violation-text">... and ' + (node.violations.length - 5) + ' more</div></div>' : '') +
7587
+ '</div>';
7588
+ }
7589
+
7590
+ const html = '<div class="tooltip-header">' +
7591
+ '<div class="tooltip-icon ' + iconClass + '">' +
7592
+ (node.isInCycle ? '\u21BB' : node.violations.length > 0 ? '!' : '\u25CB') +
7593
+ '</div>' +
7594
+ '<div>' +
7595
+ '<div class="tooltip-title">' + escapeHtml(node.name) + '</div>' +
7596
+ '<div class="tooltip-subtitle">' + escapeHtml(node.filePath) + '</div>' +
7597
+ '</div>' +
7598
+ '</div>' +
7599
+ '<div class="tooltip-content">' +
7600
+ '<div class="tooltip-row">' +
7601
+ '<span class="tooltip-label">Package</span>' +
7602
+ '<span class="tooltip-value">' + escapeHtml(node.packageName || 'N/A') + '</span>' +
7603
+ '</div>' +
7604
+ '<div class="tooltip-row">' +
7605
+ '<span class="tooltip-label">Layer</span>' +
7606
+ '<span class="tooltip-value">' + escapeHtml(node.layer || 'Unknown') + '</span>' +
7607
+ '</div>' +
7608
+ '<div class="tooltip-divider"></div>' +
7609
+ '<div class="tooltip-row">' +
7610
+ '<span class="tooltip-label">Imports</span>' +
7611
+ '<span class="tooltip-value">' + node.importsCount + '</span>' +
7612
+ '</div>' +
7613
+ '<div class="tooltip-row">' +
7614
+ '<span class="tooltip-label">Imported by</span>' +
7615
+ '<span class="tooltip-value">' + node.importedByCount + '</span>' +
7616
+ '</div>' +
7617
+ (node.isInCycle ? '<div class="tooltip-row"><span class="tooltip-label">In Cycle</span><span class="tooltip-value" style="color: #ef4444;">Yes</span></div>' : '') +
7618
+ '</div>' +
7619
+ violationsHtml;
7620
+
7621
+ tooltip.html(html);
7622
+
7623
+ // Position tooltip
7624
+ const tooltipNode = tooltip.node();
7625
+ const tooltipRect = tooltipNode.getBoundingClientRect();
7626
+ const viewportWidth = window.innerWidth;
7627
+ const viewportHeight = window.innerHeight;
7628
+
7629
+ let left = event.clientX + 10;
7630
+ let top = event.clientY + 10;
7631
+
7632
+ // Adjust if tooltip would overflow
7633
+ if (left + tooltipRect.width > viewportWidth - 20) {
7634
+ left = event.clientX - tooltipRect.width - 10;
7635
+ }
7636
+ if (top + tooltipRect.height > viewportHeight - 20) {
7637
+ top = event.clientY - tooltipRect.height - 10;
7638
+ }
7639
+
7640
+ tooltip
7641
+ .style('left', left + 'px')
7642
+ .style('top', top + 'px')
7643
+ .classed('visible', true);
7644
+ }
7645
+
7646
+ function showEdgeTooltip(event, edge) {
7647
+ const sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
7648
+ const targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
7649
+ const sourceNode = nodeById.get(sourceId);
7650
+ const targetNode = nodeById.get(targetId);
7651
+
7652
+ if (!sourceNode || !targetNode) return;
7653
+
7654
+ const iconClass = edge.isInCycle ? 'cycle' : 'default';
7655
+ const importType = edge.type === 'static' ? 'Static Import' :
7656
+ edge.type === 'dynamic' ? 'Dynamic Import' :
7657
+ edge.type === 'type-only' ? 'Type-Only Import' :
7658
+ 'Require Import';
7659
+
7660
+ const html = '<div class="tooltip-header">' +
7661
+ '<div class="tooltip-icon ' + iconClass + '">' +
7662
+ (edge.isInCycle ? '\u21BB' : '\u2192') +
7663
+ '</div>' +
7664
+ '<div>' +
7665
+ '<div class="tooltip-title">Import Relationship</div>' +
7666
+ '<div class="tooltip-subtitle">' + escapeHtml(importType) + '</div>' +
7667
+ '</div>' +
7668
+ '</div>' +
7669
+ '<div class="tooltip-content">' +
7670
+ '<div class="tooltip-row">' +
7671
+ '<span class="tooltip-label">From</span>' +
7672
+ '<span class="tooltip-value">' + escapeHtml(sourceNode.name) + '</span>' +
7673
+ '</div>' +
7674
+ '<div class="tooltip-row">' +
7675
+ '<span class="tooltip-label">To</span>' +
7676
+ '<span class="tooltip-value">' + escapeHtml(targetNode.name) + '</span>' +
7677
+ '</div>' +
7678
+ '<div class="tooltip-divider"></div>' +
7679
+ '<div class="tooltip-row">' +
7680
+ '<span class="tooltip-label">Type</span>' +
7681
+ '<span class="tooltip-value">' + escapeHtml(edge.type) + '</span>' +
7682
+ '</div>' +
7683
+ (edge.isInCycle ? '<div class="tooltip-row"><span class="tooltip-label">Part of Cycle</span><span class="tooltip-value" style="color: #ef4444;">Yes</span></div>' : '') +
7684
+ (edge.cycleId ? '<div class="tooltip-row"><span class="tooltip-label">Cycle ID</span><span class="tooltip-value">' + escapeHtml(edge.cycleId) + '</span></div>' : '') +
7685
+ '</div>';
7686
+
7687
+ tooltip.html(html);
7688
+
7689
+ // Position tooltip
7690
+ const tooltipNode = tooltip.node();
7691
+ const tooltipRect = tooltipNode.getBoundingClientRect();
7692
+ const viewportWidth = window.innerWidth;
7693
+ const viewportHeight = window.innerHeight;
7694
+
7695
+ let left = event.clientX + 10;
7696
+ let top = event.clientY + 10;
7697
+
7698
+ // Adjust if tooltip would overflow
7699
+ if (left + tooltipRect.width > viewportWidth - 20) {
7700
+ left = event.clientX - tooltipRect.width - 10;
7701
+ }
7702
+ if (top + tooltipRect.height > viewportHeight - 20) {
7703
+ top = event.clientY - tooltipRect.height - 10;
7704
+ }
7705
+
7706
+ tooltip
7707
+ .style('left', left + 'px')
7708
+ .style('top', top + 'px')
7709
+ .classed('visible', true);
7710
+ }
7711
+
7712
+ function hideTooltip() {
7713
+ tooltip.classed('visible', false);
7714
+ }
7715
+
7716
+ // Highlighting
7717
+ function highlightConnectedNodes(node) {
7718
+ const connectedIds = new Set([node.id]);
7719
+
7720
+ // Find connected nodes
7721
+ simEdges.forEach(edge => {
7722
+ const sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
7723
+ const targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
7724
+ if (sourceId === node.id) connectedIds.add(targetId);
7725
+ if (targetId === node.id) connectedIds.add(sourceId);
7726
+ });
7727
+
7728
+ state.highlightedNodes = connectedIds;
7729
+
7730
+ // Update visual state
7731
+ nodeGroup.selectAll('.graph-node')
7732
+ .classed('dimmed', d => !connectedIds.has(d.id))
7733
+ .classed('highlighted', d => connectedIds.has(d.id));
7734
+
7735
+ edgeGroup.selectAll('.graph-edge')
7736
+ .classed('dimmed', d => {
7737
+ const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
7738
+ const targetId = typeof d.target === 'object' ? d.target.id : d.target;
7739
+ return sourceId !== node.id && targetId !== node.id;
7740
+ })
7741
+ .classed('highlighted', d => {
7742
+ const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
7743
+ const targetId = typeof d.target === 'object' ? d.target.id : d.target;
7744
+ return sourceId === node.id || targetId === node.id;
7745
+ });
7746
+ }
7747
+
7748
+ function clearHighlights() {
7749
+ state.highlightedNodes.clear();
7750
+ nodeGroup.selectAll('.graph-node')
7751
+ .classed('dimmed', false)
7752
+ .classed('highlighted', false);
7753
+ edgeGroup.selectAll('.graph-edge')
7754
+ .classed('dimmed', false)
7755
+ .classed('highlighted', false);
7756
+ }
7757
+
7758
+ // Center on node
7759
+ function centerOnNode(node) {
7760
+ const scale = state.transform.k;
7761
+ const x = width / 2 - node.x * scale;
7762
+ const y = height / 2 - node.y * scale;
7763
+ svg.transition()
7764
+ .duration(500)
7765
+ .call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale));
7766
+ }
7767
+
7768
+ // Zoom controls
7769
+ function zoomIn() {
7770
+ svg.transition().call(zoom.scaleBy, 1.3);
7771
+ }
7772
+
7773
+ function zoomOut() {
7774
+ svg.transition().call(zoom.scaleBy, 0.7);
7775
+ }
7776
+
7777
+ function zoomReset() {
7778
+ svg.transition().call(zoom.transform, d3.zoomIdentity);
7779
+ }
7780
+
7781
+ function updateZoomDisplay() {
7782
+ const percentage = Math.round(state.transform.k * 100);
7783
+ const zoomLevel = document.querySelector('.zoom-level');
7784
+ if (zoomLevel) {
7785
+ zoomLevel.textContent = percentage + '%';
7786
+ }
7787
+ }
7788
+
7789
+ // Filter controls
7790
+ function setLayerFilter(layer, enabled) {
7791
+ if (enabled) {
7792
+ state.filters.layers.add(layer);
7793
+ } else {
7794
+ state.filters.layers.delete(layer);
7795
+ }
7796
+ updateGraph();
7797
+ }
7798
+
7799
+ function setSeverityFilter(severity, enabled) {
7800
+ if (enabled) {
7801
+ state.filters.severities.add(severity);
7802
+ } else {
7803
+ state.filters.severities.delete(severity);
7804
+ }
7805
+ updateGraph();
7806
+ }
7807
+
7808
+ function setViewMode(mode) {
7809
+ state.filters.showCyclesOnly = mode === 'cycles';
7810
+ state.filters.showViolationsOnly = mode === 'violations';
7811
+ updateGraph();
7812
+ }
7813
+
7814
+ function setSearchQuery(query) {
7815
+ state.filters.searchQuery = query;
7816
+ updateGraph();
7817
+ }
7818
+
7819
+ // Update graph when filters change
7820
+ function updateGraph() {
7821
+ drawEdges();
7822
+ drawNodes();
7823
+ simulation.alpha(0.3).restart();
7824
+ }
7825
+
7826
+ // Simulation tick handler
7827
+ simulation.on('tick', () => {
7828
+ edgeGroup.selectAll('.graph-edge line')
7829
+ .attr('x1', d => d.source.x)
7830
+ .attr('y1', d => d.source.y)
7831
+ .attr('x2', d => d.target.x)
7832
+ .attr('y2', d => d.target.y);
7833
+
7834
+ nodeGroup.selectAll('.graph-node')
7835
+ .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
7836
+ });
7837
+
7838
+ // Helper functions
7839
+ function escapeHtml(str) {
7840
+ if (!str) return '';
7841
+ return str
7842
+ .replace(/&/g, '&amp;')
7843
+ .replace(/</g, '&lt;')
7844
+ .replace(/>/g, '&gt;')
7845
+ .replace(/"/g, '&quot;')
7846
+ .replace(/'/g, '&#039;');
7847
+ }
7848
+
7849
+ // Initialize
7850
+ drawEdges();
7851
+ drawNodes();
7852
+
7853
+ // Update statistics display
7854
+ function updateStatistics() {
7855
+ const filtered = getFilteredNodes();
7856
+ const filteredEdges = getFilteredEdges();
7857
+
7858
+ document.getElementById('stat-nodes').textContent = filtered.length;
7859
+ document.getElementById('stat-edges').textContent = filteredEdges.length;
7860
+ document.getElementById('stat-cycles').textContent = cycles.length;
7861
+
7862
+ // Update severity counts
7863
+ const severityCounts = { critical: 0, error: 0, warning: 0, info: 0 };
7864
+ filtered.forEach(n => {
7865
+ n.violations.forEach(v => {
7866
+ severityCounts[v.severity]++;
7867
+ });
7868
+ });
7869
+
7870
+ Object.keys(severityCounts).forEach(severity => {
7871
+ const el = document.getElementById('stat-' + severity);
7872
+ if (el) el.textContent = severityCounts[severity];
7873
+ });
7874
+ }
7875
+
7876
+ updateStatistics();
7877
+
7878
+ // Expose API for control panel
7879
+ window.graphAPI = {
7880
+ zoomIn,
7881
+ zoomOut,
7882
+ zoomReset,
7883
+ setLayerFilter,
7884
+ setSeverityFilter,
7885
+ setViewMode,
7886
+ setSearchQuery,
7887
+ updateGraph,
7888
+ centerOnNode: (nodeId) => {
7889
+ const node = nodeById.get(nodeId);
7890
+ if (node) centerOnNode(node);
7891
+ }
7892
+ };
7893
+
7894
+ // Hide loading overlay
7895
+ const loadingOverlay = document.querySelector('.loading-overlay');
7896
+ if (loadingOverlay) {
7897
+ loadingOverlay.style.display = 'none';
7898
+ }
7899
+
7900
+ console.log('Dependency graph visualization initialized with', nodes.length, 'nodes and', edges.length, 'edges');
7901
+ })();
7902
+ `;
7903
+ }
7904
+ function generateControlPanelScript() {
7905
+ return `
7906
+ /**
7907
+ * Control Panel Script
7908
+ * Handles user interactions with filter controls
7909
+ */
7910
+ (function() {
7911
+ 'use strict';
7912
+
7913
+ // Wait for graph API to be available
7914
+ function waitForGraphAPI(callback) {
7915
+ if (window.graphAPI) {
7916
+ callback();
7917
+ } else {
7918
+ setTimeout(() => waitForGraphAPI(callback), 100);
7919
+ }
7920
+ }
7921
+
7922
+ waitForGraphAPI(() => {
7923
+ const api = window.graphAPI;
7924
+
7925
+ // Zoom controls
7926
+ document.getElementById('zoom-in')?.addEventListener('click', api.zoomIn);
7927
+ document.getElementById('zoom-out')?.addEventListener('click', api.zoomOut);
7928
+ document.getElementById('zoom-reset')?.addEventListener('click', api.zoomReset);
7929
+
7930
+ // View mode buttons
7931
+ document.querySelectorAll('.view-mode-btn').forEach(btn => {
7932
+ btn.addEventListener('click', () => {
7933
+ document.querySelectorAll('.view-mode-btn').forEach(b => b.classList.remove('active'));
7934
+ btn.classList.add('active');
7935
+ api.setViewMode(btn.dataset.mode);
7936
+ });
7937
+ });
7938
+
7939
+ // Search input
7940
+ const searchInput = document.getElementById('search-input');
7941
+ if (searchInput) {
7942
+ let debounceTimer;
7943
+ searchInput.addEventListener('input', (e) => {
7944
+ clearTimeout(debounceTimer);
7945
+ debounceTimer = setTimeout(() => {
7946
+ api.setSearchQuery(e.target.value);
7947
+ }, 300);
7948
+ });
7949
+ }
7950
+
7951
+ // Layer checkboxes
7952
+ document.querySelectorAll('.layer-checkbox').forEach(checkbox => {
7953
+ checkbox.addEventListener('change', (e) => {
7954
+ api.setLayerFilter(e.target.dataset.layer, e.target.checked);
7955
+ });
7956
+ });
7957
+
7958
+ // Severity checkboxes
7959
+ document.querySelectorAll('.severity-checkbox').forEach(checkbox => {
7960
+ checkbox.addEventListener('change', (e) => {
7961
+ api.setSeverityFilter(e.target.dataset.severity, e.target.checked);
7962
+ });
7963
+ });
7964
+
7965
+ // Keyboard shortcuts
7966
+ document.addEventListener('keydown', (e) => {
7967
+ // Only if not focused on input
7968
+ if (document.activeElement.tagName === 'INPUT') return;
7969
+
7970
+ switch (e.key) {
7971
+ case '+':
7972
+ case '=':
7973
+ api.zoomIn();
7974
+ break;
7975
+ case '-':
7976
+ case '_':
7977
+ api.zoomOut();
7978
+ break;
7979
+ case '0':
7980
+ api.zoomReset();
7981
+ break;
7982
+ case 'Escape':
7983
+ api.setSearchQuery('');
7984
+ if (searchInput) searchInput.value = '';
7985
+ break;
7986
+ }
7987
+ });
7988
+
7989
+ console.log('Control panel initialized');
7990
+ });
7991
+ })();
7992
+ `;
7993
+ }
7994
+
7995
+ // src/visualizer/templates/styles.ts
7996
+ var SEVERITY_COLORS = {
7997
+ critical: { primary: "#dc2626", background: "#fef2f2", text: "#991b1b" },
7998
+ error: { primary: "#ea580c", background: "#fff7ed", text: "#9a3412" },
7999
+ warning: { primary: "#ca8a04", background: "#fefce8", text: "#854d0e" },
8000
+ info: { primary: "#2563eb", background: "#eff6ff", text: "#1e40af" }
8001
+ };
8002
+ var LAYER_COLORS = {
8003
+ domain: "#8b5cf6",
8004
+ application: "#06b6d4",
8005
+ infrastructure: "#84cc16",
8006
+ presentation: "#f97316",
8007
+ shared: "#6b7280",
8008
+ unknown: "#9ca3af"
8009
+ };
8010
+ var CYCLE_COLORS = {
8011
+ edge: "#ef4444",
8012
+ node: "#fca5a5",
8013
+ glow: "rgba(239, 68, 68, 0.4)"
8014
+ };
8015
+ var CSS_VARIABLES = `
8016
+ :root {
8017
+ /* Layout */
8018
+ --sidebar-width: 280px;
8019
+ --control-panel-height: 60px;
8020
+ --tooltip-max-width: 360px;
8021
+
8022
+ /* Colors - Light theme */
8023
+ --bg-primary: #ffffff;
8024
+ --bg-secondary: #f9fafb;
8025
+ --bg-tertiary: #f3f4f6;
8026
+ --text-primary: #111827;
8027
+ --text-secondary: #4b5563;
8028
+ --text-muted: #9ca3af;
8029
+ --border-color: #e5e7eb;
8030
+ --focus-ring: #3b82f6;
8031
+
8032
+ /* Graph colors */
8033
+ --node-default: #6b7280;
8034
+ --node-hover: #374151;
8035
+ --edge-default: #d1d5db;
8036
+ --edge-hover: #9ca3af;
8037
+
8038
+ /* Severity colors */
8039
+ --severity-critical: ${SEVERITY_COLORS.critical.primary};
8040
+ --severity-critical-bg: ${SEVERITY_COLORS.critical.background};
8041
+ --severity-error: ${SEVERITY_COLORS.error.primary};
8042
+ --severity-error-bg: ${SEVERITY_COLORS.error.background};
8043
+ --severity-warning: ${SEVERITY_COLORS.warning.primary};
8044
+ --severity-warning-bg: ${SEVERITY_COLORS.warning.background};
8045
+ --severity-info: ${SEVERITY_COLORS.info.primary};
8046
+ --severity-info-bg: ${SEVERITY_COLORS.info.background};
8047
+
8048
+ /* Cycle colors */
8049
+ --cycle-edge: ${CYCLE_COLORS.edge};
8050
+ --cycle-node: ${CYCLE_COLORS.node};
8051
+ --cycle-glow: ${CYCLE_COLORS.glow};
8052
+
8053
+ /* Layer colors */
8054
+ --layer-domain: ${LAYER_COLORS.domain};
8055
+ --layer-application: ${LAYER_COLORS.application};
8056
+ --layer-infrastructure: ${LAYER_COLORS.infrastructure};
8057
+ --layer-presentation: ${LAYER_COLORS.presentation};
8058
+ --layer-shared: ${LAYER_COLORS.shared};
8059
+ --layer-unknown: ${LAYER_COLORS.unknown};
8060
+
8061
+ /* Shadows */
8062
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
8063
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
8064
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
8065
+
8066
+ /* Transitions */
8067
+ --transition-fast: 150ms ease;
8068
+ --transition-normal: 200ms ease;
8069
+ }
8070
+ `;
8071
+ var BASE_STYLES = `
8072
+ *, *::before, *::after {
8073
+ box-sizing: border-box;
8074
+ margin: 0;
8075
+ padding: 0;
8076
+ }
8077
+
8078
+ html, body {
8079
+ height: 100%;
8080
+ overflow: hidden;
8081
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
8082
+ font-size: 14px;
8083
+ line-height: 1.5;
8084
+ color: var(--text-primary);
8085
+ background-color: var(--bg-primary);
8086
+ -webkit-font-smoothing: antialiased;
8087
+ -moz-osx-font-smoothing: grayscale;
8088
+ }
8089
+
8090
+ button {
8091
+ font-family: inherit;
8092
+ font-size: inherit;
8093
+ cursor: pointer;
8094
+ border: none;
8095
+ background: none;
8096
+ }
8097
+
8098
+ button:focus-visible {
8099
+ outline: 2px solid var(--focus-ring);
8100
+ outline-offset: 2px;
8101
+ }
8102
+
8103
+ input {
8104
+ font-family: inherit;
8105
+ font-size: inherit;
8106
+ }
8107
+
8108
+ input:focus-visible {
8109
+ outline: 2px solid var(--focus-ring);
8110
+ outline-offset: 1px;
8111
+ }
8112
+ `;
8113
+ var LAYOUT_STYLES = `
8114
+ .app-container {
8115
+ display: flex;
8116
+ height: 100vh;
8117
+ width: 100vw;
8118
+ overflow: hidden;
8119
+ }
8120
+
8121
+ .sidebar {
8122
+ width: var(--sidebar-width);
8123
+ min-width: var(--sidebar-width);
8124
+ height: 100%;
8125
+ background-color: var(--bg-secondary);
8126
+ border-right: 1px solid var(--border-color);
8127
+ display: flex;
8128
+ flex-direction: column;
8129
+ overflow: hidden;
8130
+ }
8131
+
8132
+ .sidebar-header {
8133
+ padding: 16px;
8134
+ border-bottom: 1px solid var(--border-color);
8135
+ background-color: var(--bg-primary);
8136
+ }
8137
+
8138
+ .sidebar-title {
8139
+ font-size: 16px;
8140
+ font-weight: 600;
8141
+ color: var(--text-primary);
8142
+ margin-bottom: 4px;
8143
+ }
8144
+
8145
+ .sidebar-subtitle {
8146
+ font-size: 12px;
8147
+ color: var(--text-muted);
8148
+ }
8149
+
8150
+ .sidebar-content {
8151
+ flex: 1;
8152
+ overflow-y: auto;
8153
+ padding: 16px;
8154
+ }
8155
+
8156
+ .main-content {
8157
+ flex: 1;
8158
+ display: flex;
8159
+ flex-direction: column;
8160
+ overflow: hidden;
8161
+ position: relative;
8162
+ }
8163
+
8164
+ .control-panel {
8165
+ height: var(--control-panel-height);
8166
+ min-height: var(--control-panel-height);
8167
+ padding: 12px 16px;
8168
+ background-color: var(--bg-primary);
8169
+ border-bottom: 1px solid var(--border-color);
8170
+ display: flex;
8171
+ align-items: center;
8172
+ gap: 16px;
8173
+ }
8174
+
8175
+ .graph-container {
8176
+ flex: 1;
8177
+ position: relative;
8178
+ overflow: hidden;
8179
+ background-color: var(--bg-tertiary);
8180
+ }
8181
+
8182
+ .graph-canvas {
8183
+ width: 100%;
8184
+ height: 100%;
8185
+ display: block;
8186
+ }
8187
+ `;
8188
+ var CONTROL_STYLES = `
8189
+ .control-group {
8190
+ display: flex;
8191
+ align-items: center;
8192
+ gap: 8px;
8193
+ }
8194
+
8195
+ .control-label {
8196
+ font-size: 12px;
8197
+ font-weight: 500;
8198
+ color: var(--text-secondary);
8199
+ white-space: nowrap;
8200
+ }
8201
+
8202
+ .control-select {
8203
+ padding: 6px 28px 6px 10px;
8204
+ border: 1px solid var(--border-color);
8205
+ border-radius: 6px;
8206
+ background-color: var(--bg-primary);
8207
+ color: var(--text-primary);
8208
+ font-size: 13px;
8209
+ appearance: none;
8210
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E");
8211
+ background-repeat: no-repeat;
8212
+ background-position: right 8px center;
8213
+ cursor: pointer;
8214
+ transition: border-color var(--transition-fast);
8215
+ }
8216
+
8217
+ .control-select:hover {
8218
+ border-color: var(--text-muted);
8219
+ }
8220
+
8221
+ .control-input {
8222
+ padding: 6px 10px;
8223
+ border: 1px solid var(--border-color);
8224
+ border-radius: 6px;
8225
+ background-color: var(--bg-primary);
8226
+ color: var(--text-primary);
8227
+ font-size: 13px;
8228
+ width: 180px;
8229
+ transition: border-color var(--transition-fast);
8230
+ }
8231
+
8232
+ .control-input::placeholder {
8233
+ color: var(--text-muted);
8234
+ }
8235
+
8236
+ .control-input:hover {
8237
+ border-color: var(--text-muted);
8238
+ }
8239
+
8240
+ .control-btn {
8241
+ display: inline-flex;
8242
+ align-items: center;
8243
+ justify-content: center;
8244
+ padding: 6px 12px;
8245
+ border: 1px solid var(--border-color);
8246
+ border-radius: 6px;
8247
+ background-color: var(--bg-primary);
8248
+ color: var(--text-primary);
8249
+ font-size: 13px;
8250
+ font-weight: 500;
8251
+ transition: all var(--transition-fast);
8252
+ }
8253
+
8254
+ .control-btn:hover {
8255
+ background-color: var(--bg-secondary);
8256
+ border-color: var(--text-muted);
8257
+ }
8258
+
8259
+ .control-btn.active {
8260
+ background-color: var(--focus-ring);
8261
+ border-color: var(--focus-ring);
8262
+ color: white;
8263
+ }
8264
+
8265
+ .control-btn-group {
8266
+ display: flex;
8267
+ border: 1px solid var(--border-color);
8268
+ border-radius: 6px;
8269
+ overflow: hidden;
8270
+ }
8271
+
8272
+ .control-btn-group .control-btn {
8273
+ border: none;
8274
+ border-radius: 0;
8275
+ border-right: 1px solid var(--border-color);
8276
+ }
8277
+
8278
+ .control-btn-group .control-btn:last-child {
8279
+ border-right: none;
8280
+ }
8281
+
8282
+ .control-spacer {
8283
+ flex: 1;
8284
+ }
8285
+
8286
+ .zoom-controls {
8287
+ display: flex;
8288
+ align-items: center;
8289
+ gap: 4px;
8290
+ }
8291
+
8292
+ .zoom-btn {
8293
+ width: 32px;
8294
+ height: 32px;
8295
+ display: inline-flex;
8296
+ align-items: center;
8297
+ justify-content: center;
8298
+ border: 1px solid var(--border-color);
8299
+ border-radius: 6px;
8300
+ background-color: var(--bg-primary);
8301
+ color: var(--text-primary);
8302
+ font-size: 16px;
8303
+ font-weight: 500;
8304
+ transition: all var(--transition-fast);
8305
+ }
8306
+
8307
+ .zoom-btn:hover {
8308
+ background-color: var(--bg-secondary);
8309
+ }
8310
+
8311
+ .zoom-level {
8312
+ font-size: 12px;
8313
+ color: var(--text-secondary);
8314
+ min-width: 48px;
8315
+ text-align: center;
8316
+ }
8317
+ `;
8318
+ var SIDEBAR_SECTION_STYLES = `
8319
+ .sidebar-section {
8320
+ margin-bottom: 20px;
8321
+ }
8322
+
8323
+ .sidebar-section:last-child {
8324
+ margin-bottom: 0;
8325
+ }
8326
+
8327
+ .section-title {
8328
+ font-size: 11px;
8329
+ font-weight: 600;
8330
+ text-transform: uppercase;
8331
+ letter-spacing: 0.5px;
8332
+ color: var(--text-muted);
8333
+ margin-bottom: 10px;
8334
+ }
8335
+
8336
+ .stats-grid {
8337
+ display: grid;
8338
+ grid-template-columns: repeat(2, 1fr);
8339
+ gap: 8px;
8340
+ }
8341
+
8342
+ .stat-card {
8343
+ padding: 10px 12px;
8344
+ background-color: var(--bg-primary);
8345
+ border-radius: 8px;
8346
+ border: 1px solid var(--border-color);
8347
+ }
8348
+
8349
+ .stat-value {
8350
+ font-size: 20px;
8351
+ font-weight: 600;
8352
+ color: var(--text-primary);
8353
+ line-height: 1.2;
8354
+ }
8355
+
8356
+ .stat-label {
8357
+ font-size: 11px;
8358
+ color: var(--text-muted);
8359
+ margin-top: 2px;
8360
+ }
8361
+
8362
+ .stat-card.span-full {
8363
+ grid-column: span 2;
8364
+ }
8365
+
8366
+ .severity-list {
8367
+ display: flex;
8368
+ flex-direction: column;
8369
+ gap: 6px;
8370
+ }
8371
+
8372
+ .severity-item {
8373
+ display: flex;
8374
+ align-items: center;
8375
+ justify-content: space-between;
8376
+ padding: 8px 10px;
8377
+ background-color: var(--bg-primary);
8378
+ border-radius: 6px;
8379
+ border: 1px solid var(--border-color);
8380
+ }
8381
+
8382
+ .severity-indicator {
8383
+ display: flex;
8384
+ align-items: center;
8385
+ gap: 8px;
8386
+ }
8387
+
8388
+ .severity-dot {
8389
+ width: 10px;
8390
+ height: 10px;
8391
+ border-radius: 50%;
8392
+ }
8393
+
8394
+ .severity-dot.critical { background-color: var(--severity-critical); }
8395
+ .severity-dot.error { background-color: var(--severity-error); }
8396
+ .severity-dot.warning { background-color: var(--severity-warning); }
8397
+ .severity-dot.info { background-color: var(--severity-info); }
8398
+
8399
+ .severity-name {
8400
+ font-size: 13px;
8401
+ color: var(--text-primary);
8402
+ text-transform: capitalize;
8403
+ }
8404
+
8405
+ .severity-count {
8406
+ font-size: 13px;
8407
+ font-weight: 600;
8408
+ color: var(--text-primary);
8409
+ }
8410
+
8411
+ .severity-filter-list {
8412
+ display: flex;
8413
+ flex-direction: column;
8414
+ gap: 6px;
8415
+ }
8416
+
8417
+ .severity-filter-item {
8418
+ display: flex;
8419
+ align-items: center;
8420
+ gap: 8px;
8421
+ padding: 6px 0;
8422
+ cursor: pointer;
8423
+ }
8424
+
8425
+ .severity-filter-item:hover {
8426
+ opacity: 0.8;
8427
+ }
8428
+
8429
+ .severity-checkbox {
8430
+ width: 16px;
8431
+ height: 16px;
8432
+ cursor: pointer;
8433
+ accent-color: var(--focus-ring);
8434
+ }
8435
+
8436
+ .layer-list {
8437
+ display: flex;
8438
+ flex-direction: column;
8439
+ gap: 6px;
8440
+ }
8441
+
8442
+ .layer-item {
8443
+ display: flex;
8444
+ align-items: center;
8445
+ gap: 8px;
8446
+ padding: 6px 0;
8447
+ }
8448
+
8449
+ .layer-checkbox {
8450
+ width: 16px;
8451
+ height: 16px;
8452
+ cursor: pointer;
8453
+ accent-color: var(--focus-ring);
8454
+ }
8455
+
8456
+ .layer-color {
8457
+ width: 12px;
8458
+ height: 12px;
8459
+ border-radius: 3px;
8460
+ }
8461
+
8462
+ .layer-color.domain { background-color: var(--layer-domain); }
8463
+ .layer-color.application { background-color: var(--layer-application); }
8464
+ .layer-color.infrastructure { background-color: var(--layer-infrastructure); }
8465
+ .layer-color.presentation { background-color: var(--layer-presentation); }
8466
+ .layer-color.shared { background-color: var(--layer-shared); }
8467
+ .layer-color.unknown { background-color: var(--layer-unknown); }
8468
+
8469
+ .layer-name {
8470
+ font-size: 13px;
8471
+ color: var(--text-primary);
8472
+ text-transform: capitalize;
8473
+ flex: 1;
8474
+ }
8475
+
8476
+ .layer-count {
8477
+ font-size: 12px;
8478
+ color: var(--text-muted);
8479
+ }
8480
+
8481
+ .legend-section {
8482
+ padding-top: 16px;
8483
+ border-top: 1px solid var(--border-color);
8484
+ margin-top: 16px;
8485
+ }
8486
+
8487
+ .legend-item {
8488
+ display: flex;
8489
+ align-items: center;
8490
+ gap: 8px;
8491
+ padding: 4px 0;
8492
+ }
8493
+
8494
+ .legend-icon {
8495
+ width: 20px;
8496
+ height: 20px;
8497
+ display: flex;
8498
+ align-items: center;
8499
+ justify-content: center;
8500
+ }
8501
+
8502
+ .legend-icon svg {
8503
+ width: 16px;
8504
+ height: 16px;
8505
+ }
8506
+
8507
+ .legend-text {
8508
+ font-size: 12px;
8509
+ color: var(--text-secondary);
8510
+ }
8511
+ `;
8512
+ var TOOLTIP_STYLES = `
8513
+ .tooltip {
8514
+ position: fixed;
8515
+ max-width: var(--tooltip-max-width);
8516
+ padding: 12px 14px;
8517
+ background-color: var(--bg-primary);
8518
+ border: 1px solid var(--border-color);
8519
+ border-radius: 8px;
8520
+ box-shadow: var(--shadow-lg);
8521
+ pointer-events: none;
8522
+ z-index: 1000;
8523
+ opacity: 0;
8524
+ transform: translateY(4px);
8525
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
8526
+ }
8527
+
8528
+ .tooltip.visible {
8529
+ opacity: 1;
8530
+ transform: translateY(0);
8531
+ }
8532
+
8533
+ .tooltip-header {
8534
+ display: flex;
8535
+ align-items: flex-start;
8536
+ gap: 8px;
8537
+ margin-bottom: 8px;
8538
+ }
8539
+
8540
+ .tooltip-icon {
8541
+ width: 24px;
8542
+ height: 24px;
8543
+ border-radius: 6px;
8544
+ display: flex;
8545
+ align-items: center;
8546
+ justify-content: center;
8547
+ flex-shrink: 0;
8548
+ }
8549
+
8550
+ .tooltip-icon.severity-critical { background-color: var(--severity-critical-bg); color: var(--severity-critical); }
8551
+ .tooltip-icon.severity-error { background-color: var(--severity-error-bg); color: var(--severity-error); }
8552
+ .tooltip-icon.severity-warning { background-color: var(--severity-warning-bg); color: var(--severity-warning); }
8553
+ .tooltip-icon.severity-info { background-color: var(--severity-info-bg); color: var(--severity-info); }
8554
+ .tooltip-icon.cycle { background-color: #fef2f2; color: var(--cycle-edge); }
8555
+ .tooltip-icon.default { background-color: var(--bg-tertiary); color: var(--text-secondary); }
8556
+
8557
+ .tooltip-title {
8558
+ font-size: 13px;
8559
+ font-weight: 600;
8560
+ color: var(--text-primary);
8561
+ word-break: break-word;
8562
+ }
8563
+
8564
+ .tooltip-subtitle {
8565
+ font-size: 11px;
8566
+ color: var(--text-muted);
8567
+ margin-top: 2px;
8568
+ }
8569
+
8570
+ .tooltip-content {
8571
+ display: flex;
8572
+ flex-direction: column;
8573
+ gap: 6px;
8574
+ }
8575
+
8576
+ .tooltip-row {
8577
+ display: flex;
8578
+ justify-content: space-between;
8579
+ align-items: center;
8580
+ gap: 12px;
8581
+ }
8582
+
8583
+ .tooltip-label {
8584
+ font-size: 12px;
8585
+ color: var(--text-muted);
8586
+ }
8587
+
8588
+ .tooltip-value {
8589
+ font-size: 12px;
8590
+ font-weight: 500;
8591
+ color: var(--text-primary);
8592
+ }
8593
+
8594
+ .tooltip-divider {
8595
+ height: 1px;
8596
+ background-color: var(--border-color);
8597
+ margin: 6px 0;
8598
+ }
8599
+
8600
+ .tooltip-violations {
8601
+ margin-top: 8px;
8602
+ }
8603
+
8604
+ .tooltip-violations-title {
8605
+ font-size: 11px;
8606
+ font-weight: 600;
8607
+ color: var(--text-muted);
8608
+ margin-bottom: 6px;
8609
+ text-transform: uppercase;
8610
+ letter-spacing: 0.5px;
8611
+ }
8612
+
8613
+ .tooltip-violation {
8614
+ display: flex;
8615
+ align-items: flex-start;
8616
+ gap: 6px;
8617
+ padding: 6px 8px;
8618
+ background-color: var(--bg-secondary);
8619
+ border-radius: 4px;
8620
+ margin-bottom: 4px;
8621
+ }
8622
+
8623
+ .tooltip-violation:last-child {
8624
+ margin-bottom: 0;
8625
+ }
8626
+
8627
+ .tooltip-violation-dot {
8628
+ width: 6px;
8629
+ height: 6px;
8630
+ border-radius: 50%;
8631
+ margin-top: 5px;
8632
+ flex-shrink: 0;
8633
+ }
8634
+
8635
+ .tooltip-violation-text {
8636
+ font-size: 11px;
8637
+ color: var(--text-primary);
8638
+ line-height: 1.4;
8639
+ }
8640
+ `;
8641
+ var GRAPH_ELEMENT_STYLES = `
8642
+ .graph-node {
8643
+ cursor: pointer;
8644
+ transition: opacity var(--transition-fast);
8645
+ }
8646
+
8647
+ .graph-node:hover {
8648
+ opacity: 0.9;
8649
+ }
8650
+
8651
+ .graph-node.dimmed {
8652
+ opacity: 0.2;
8653
+ }
8654
+
8655
+ .graph-node.highlighted {
8656
+ opacity: 1;
8657
+ }
8658
+
8659
+ .node-circle {
8660
+ stroke: var(--bg-primary);
8661
+ stroke-width: 2px;
8662
+ transition: all var(--transition-fast);
8663
+ }
8664
+
8665
+ .node-circle.cycle {
8666
+ stroke: var(--cycle-edge);
8667
+ stroke-width: 3px;
8668
+ filter: drop-shadow(0 0 4px var(--cycle-glow));
8669
+ }
8670
+
8671
+ .node-circle.severity-critical { fill: var(--severity-critical); }
8672
+ .node-circle.severity-error { fill: var(--severity-error); }
8673
+ .node-circle.severity-warning { fill: var(--severity-warning); }
8674
+ .node-circle.severity-info { fill: var(--severity-info); }
8675
+
8676
+ .node-circle.layer-domain { fill: var(--layer-domain); }
8677
+ .node-circle.layer-application { fill: var(--layer-application); }
8678
+ .node-circle.layer-infrastructure { fill: var(--layer-infrastructure); }
8679
+ .node-circle.layer-presentation { fill: var(--layer-presentation); }
8680
+ .node-circle.layer-shared { fill: var(--layer-shared); }
8681
+ .node-circle.layer-unknown, .node-circle.default { fill: var(--node-default); }
8682
+
8683
+ .node-label {
8684
+ font-size: 10px;
8685
+ fill: var(--text-primary);
8686
+ pointer-events: none;
8687
+ text-anchor: middle;
8688
+ dominant-baseline: central;
8689
+ font-weight: 500;
8690
+ }
8691
+
8692
+ .graph-edge {
8693
+ transition: opacity var(--transition-fast);
8694
+ }
8695
+
8696
+ .graph-edge.dimmed {
8697
+ opacity: 0.1;
8698
+ }
8699
+
8700
+ .graph-edge.highlighted {
8701
+ opacity: 1;
8702
+ }
8703
+
8704
+ .edge-line {
8705
+ fill: none;
8706
+ stroke: var(--edge-default);
8707
+ stroke-width: 1.5px;
8708
+ transition: stroke var(--transition-fast);
8709
+ }
8710
+
8711
+ .edge-line.cycle {
8712
+ stroke: var(--cycle-edge);
8713
+ stroke-width: 2px;
8714
+ animation: pulse-edge 1.5s ease-in-out infinite;
8715
+ }
8716
+
8717
+ .edge-line.type-static { stroke-dasharray: none; }
8718
+ .edge-line.type-dynamic { stroke-dasharray: 4, 4; }
8719
+ .edge-line.type-type-only { stroke-dasharray: 2, 2; opacity: 0.6; }
8720
+ .edge-line.type-require { stroke-dasharray: 6, 2; }
8721
+
8722
+ .edge-arrow {
8723
+ fill: var(--edge-default);
8724
+ transition: fill var(--transition-fast);
8725
+ }
8726
+
8727
+ .edge-arrow.cycle {
8728
+ fill: var(--cycle-edge);
8729
+ }
8730
+
8731
+ @keyframes pulse-edge {
8732
+ 0%, 100% {
8733
+ opacity: 1;
8734
+ }
8735
+ 50% {
8736
+ opacity: 0.5;
8737
+ }
8738
+ }
8739
+
8740
+ .cycle-glow {
8741
+ fill: none;
8742
+ stroke: var(--cycle-glow);
8743
+ stroke-width: 8px;
8744
+ opacity: 0.5;
8745
+ animation: pulse-glow 2s ease-in-out infinite;
8746
+ }
8747
+
8748
+ @keyframes pulse-glow {
8749
+ 0%, 100% {
8750
+ opacity: 0.3;
8751
+ }
8752
+ 50% {
8753
+ opacity: 0.6;
8754
+ }
8755
+ }
8756
+ `;
8757
+ var STATE_STYLES = `
8758
+ .loading-overlay {
8759
+ position: absolute;
8760
+ inset: 0;
8761
+ display: flex;
8762
+ align-items: center;
8763
+ justify-content: center;
8764
+ background-color: var(--bg-tertiary);
8765
+ z-index: 100;
8766
+ }
8767
+
8768
+ .loading-spinner {
8769
+ width: 40px;
8770
+ height: 40px;
8771
+ border: 3px solid var(--border-color);
8772
+ border-top-color: var(--focus-ring);
8773
+ border-radius: 50%;
8774
+ animation: spin 1s linear infinite;
8775
+ }
8776
+
8777
+ @keyframes spin {
8778
+ to {
8779
+ transform: rotate(360deg);
8780
+ }
8781
+ }
8782
+
8783
+ .empty-state {
8784
+ position: absolute;
8785
+ inset: 0;
8786
+ display: flex;
8787
+ flex-direction: column;
8788
+ align-items: center;
8789
+ justify-content: center;
8790
+ color: var(--text-muted);
8791
+ padding: 24px;
8792
+ text-align: center;
8793
+ }
8794
+
8795
+ .empty-state-icon {
8796
+ width: 64px;
8797
+ height: 64px;
8798
+ margin-bottom: 16px;
8799
+ opacity: 0.5;
8800
+ }
8801
+
8802
+ .empty-state-title {
8803
+ font-size: 16px;
8804
+ font-weight: 600;
8805
+ color: var(--text-secondary);
8806
+ margin-bottom: 8px;
8807
+ }
8808
+
8809
+ .empty-state-text {
8810
+ font-size: 14px;
8811
+ max-width: 300px;
8812
+ }
8813
+ `;
8814
+ function generateStyles() {
8815
+ return [
8816
+ CSS_VARIABLES,
8817
+ BASE_STYLES,
8818
+ LAYOUT_STYLES,
8819
+ CONTROL_STYLES,
8820
+ SIDEBAR_SECTION_STYLES,
8821
+ TOOLTIP_STYLES,
8822
+ GRAPH_ELEMENT_STYLES,
8823
+ STATE_STYLES
8824
+ ].join("\n");
8825
+ }
8826
+
8827
+ // src/visualizer/html-renderer.ts
8828
+ var DEFAULT_HTML_RENDER_OPTIONS = {
8829
+ title: "Workspace Dependency Graph",
8830
+ inlineD3: true,
8831
+ minify: false
8832
+ };
8833
+ function sanitizeHtml(str) {
8834
+ if (typeof str !== "string") {
8835
+ return "";
8836
+ }
8837
+ return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;").replaceAll("/", "&#x2F;").replaceAll("`", "&#x60;").replaceAll("=", "&#x3D;");
8838
+ }
8839
+ function sanitizeJsString(str) {
8840
+ if (typeof str !== "string") {
8841
+ return "";
8842
+ }
8843
+ return str.replaceAll("\\", "\\\\").replaceAll("'", String.raw`\'`).replaceAll('"', String.raw`\"`).replaceAll("\n", String.raw`\n`).replaceAll("\r", String.raw`\r`).replaceAll(" ", String.raw`\t`).replaceAll("\u2028", String.raw`\u2028`).replaceAll("\u2029", String.raw`\u2029`).replaceAll("</script>", String.raw`<\/script>`);
8844
+ }
8845
+ function sanitizeFilePath(path17) {
8846
+ if (typeof path17 !== "string") {
8847
+ return "";
8848
+ }
8849
+ return path17.replaceAll("\0", "").replaceAll("../", "").replaceAll(/\.\.$/g, "");
8850
+ }
8851
+ function sanitizeVisualizationData(data) {
8852
+ return {
8853
+ nodes: data.nodes.map((node) => ({
8854
+ ...node,
8855
+ id: sanitizeJsString(node.id),
8856
+ name: sanitizeJsString(node.name),
8857
+ filePath: sanitizeJsString(sanitizeFilePath(node.filePath)),
8858
+ packageName: node.packageName === void 0 ? void 0 : sanitizeJsString(node.packageName),
8859
+ layer: node.layer === void 0 ? void 0 : sanitizeJsString(node.layer),
8860
+ violations: node.violations.map((v) => ({
8861
+ ...v,
8862
+ id: sanitizeJsString(v.id),
8863
+ message: sanitizeJsString(v.message),
8864
+ ruleId: sanitizeJsString(v.ruleId)
8865
+ }))
8866
+ })),
8867
+ edges: data.edges.map((edge) => ({
8868
+ ...edge,
8869
+ source: sanitizeJsString(edge.source),
8870
+ target: sanitizeJsString(edge.target),
8871
+ cycleId: edge.cycleId === void 0 ? void 0 : sanitizeJsString(edge.cycleId)
8872
+ })),
8873
+ cycles: data.cycles.map((cycle) => ({
8874
+ ...cycle,
8875
+ id: sanitizeJsString(cycle.id),
8876
+ nodes: cycle.nodes.map((n) => sanitizeJsString(n)),
8877
+ edges: cycle.edges.map((e) => ({
8878
+ from: sanitizeJsString(e.from),
8879
+ to: sanitizeJsString(e.to)
8880
+ })),
8881
+ description: sanitizeJsString(cycle.description)
8882
+ })),
8883
+ statistics: data.statistics,
8884
+ layers: data.layers.map((layer) => ({
8885
+ ...layer,
8886
+ name: sanitizeJsString(layer.name),
8887
+ allowedDependencies: layer.allowedDependencies.map((d) => sanitizeJsString(d))
8888
+ })),
8889
+ metadata: {
8890
+ ...data.metadata,
8891
+ workspacePath: sanitizeJsString(sanitizeFilePath(data.metadata.workspacePath)),
8892
+ generatedAt: sanitizeJsString(data.metadata.generatedAt),
8893
+ analyzerVersion: sanitizeJsString(data.metadata.analyzerVersion)
8894
+ }
8895
+ };
8896
+ }
8897
+ function generateSidebarHtml(data, title) {
8898
+ const stats = data.statistics;
8899
+ const layers = data.layers;
8900
+ const layerFiltersHtml = layers.map(
8901
+ (layer) => `
8902
+ <label class="layer-item">
8903
+ <input type="checkbox" class="layer-checkbox" data-layer="${sanitizeHtml(layer.name)}" checked>
8904
+ <span class="layer-color ${sanitizeHtml(layer.name.toLowerCase())}"></span>
8905
+ <span class="layer-name">${sanitizeHtml(layer.name)}</span>
8906
+ <span class="layer-count">${stats.nodesByLayer[layer.name] ?? 0}</span>
8907
+ </label>
8908
+ `
8909
+ ).join("");
8910
+ const severityStatsHtml = ["critical", "error", "warning", "info"].map(
8911
+ (severity) => `
8912
+ <div class="severity-item">
8913
+ <div class="severity-indicator">
8914
+ <div class="severity-dot ${severity}"></div>
8915
+ <span class="severity-name">${severity}</span>
8916
+ </div>
8917
+ <span class="severity-count" id="stat-${severity}">${stats.violationsBySeverity[severity] ?? 0}</span>
8918
+ </div>
8919
+ `
8920
+ ).join("");
8921
+ return `
8922
+ <div class="sidebar-header">
8923
+ <h1 class="sidebar-title">${sanitizeHtml(title)}</h1>
8924
+ <p class="sidebar-subtitle">Generated ${sanitizeHtml(data.metadata.generatedAt)}</p>
8925
+ </div>
8926
+ <div class="sidebar-content">
8927
+ <section class="sidebar-section">
8928
+ <h2 class="section-title">Statistics</h2>
8929
+ <div class="stats-grid">
8930
+ <div class="stat-card">
8931
+ <div class="stat-value" id="stat-nodes">${stats.totalNodes}</div>
8932
+ <div class="stat-label">Nodes</div>
8933
+ </div>
8934
+ <div class="stat-card">
8935
+ <div class="stat-value" id="stat-edges">${stats.totalEdges}</div>
8936
+ <div class="stat-label">Edges</div>
8937
+ </div>
8938
+ <div class="stat-card">
8939
+ <div class="stat-value" id="stat-cycles">${stats.totalCycles}</div>
8940
+ <div class="stat-label">Cycles</div>
8941
+ </div>
8942
+ <div class="stat-card">
8943
+ <div class="stat-value">${stats.packagesAnalyzed}</div>
8944
+ <div class="stat-label">Packages</div>
8945
+ </div>
8946
+ </div>
8947
+ </section>
8948
+
8949
+ <section class="sidebar-section">
8950
+ <h2 class="section-title">Violations by Severity</h2>
8951
+ <div class="severity-list">
8952
+ ${severityStatsHtml}
8953
+ </div>
8954
+ </section>
8955
+
8956
+ <section class="sidebar-section">
8957
+ <h2 class="section-title">Filter by Severity</h2>
8958
+ <div class="severity-filter-list">
8959
+ <label class="severity-filter-item">
8960
+ <input type="checkbox" class="severity-checkbox" data-severity="critical" checked>
8961
+ <div class="severity-indicator">
8962
+ <div class="severity-dot critical"></div>
8963
+ <span class="severity-name">Critical</span>
8964
+ </div>
8965
+ </label>
8966
+ <label class="severity-filter-item">
8967
+ <input type="checkbox" class="severity-checkbox" data-severity="error" checked>
8968
+ <div class="severity-indicator">
8969
+ <div class="severity-dot error"></div>
8970
+ <span class="severity-name">Error</span>
8971
+ </div>
8972
+ </label>
8973
+ <label class="severity-filter-item">
8974
+ <input type="checkbox" class="severity-checkbox" data-severity="warning" checked>
8975
+ <div class="severity-indicator">
8976
+ <div class="severity-dot warning"></div>
8977
+ <span class="severity-name">Warning</span>
8978
+ </div>
8979
+ </label>
8980
+ <label class="severity-filter-item">
8981
+ <input type="checkbox" class="severity-checkbox" data-severity="info" checked>
8982
+ <div class="severity-indicator">
8983
+ <div class="severity-dot info"></div>
8984
+ <span class="severity-name">Info</span>
8985
+ </div>
8986
+ </label>
8987
+ </div>
8988
+ </section>
8989
+
8990
+ <section class="sidebar-section">
8991
+ <h2 class="section-title">Filter by Layer</h2>
8992
+ <div class="layer-list">
8993
+ ${layerFiltersHtml}
8994
+ </div>
8995
+ </section>
8996
+
8997
+ <section class="sidebar-section legend-section">
8998
+ <h2 class="section-title">Legend</h2>
8999
+ <div class="legend-item">
9000
+ <div class="legend-icon">
9001
+ <svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="${SEVERITY_COLORS.critical.primary}" stroke="#fff" stroke-width="2"/></svg>
9002
+ </div>
9003
+ <span class="legend-text">Critical violation</span>
9004
+ </div>
9005
+ <div class="legend-item">
9006
+ <div class="legend-icon">
9007
+ <svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="#ef4444" stroke="#ef4444" stroke-width="3"/></svg>
9008
+ </div>
9009
+ <span class="legend-text">Node in cycle</span>
9010
+ </div>
9011
+ <div class="legend-item">
9012
+ <div class="legend-icon">
9013
+ <svg viewBox="0 0 16 16"><line x1="0" y1="8" x2="16" y2="8" stroke="#ef4444" stroke-width="2" stroke-dasharray="0"/></svg>
9014
+ </div>
9015
+ <span class="legend-text">Cycle edge (animated)</span>
9016
+ </div>
9017
+ <div class="legend-item">
9018
+ <div class="legend-icon">
9019
+ <svg viewBox="0 0 16 16"><line x1="0" y1="8" x2="16" y2="8" stroke="#d1d5db" stroke-width="2" stroke-dasharray="4,4"/></svg>
9020
+ </div>
9021
+ <span class="legend-text">Dynamic import</span>
9022
+ </div>
9023
+ </section>
9024
+ </div>
9025
+ `;
9026
+ }
9027
+ function generateControlPanelHtml() {
9028
+ return `
9029
+ <div class="control-group">
9030
+ <label class="control-label">View:</label>
9031
+ <div class="control-btn-group">
9032
+ <button class="control-btn view-mode-btn active" data-mode="all">All</button>
9033
+ <button class="control-btn view-mode-btn" data-mode="cycles">Cycles</button>
9034
+ <button class="control-btn view-mode-btn" data-mode="violations">Violations</button>
9035
+ </div>
9036
+ </div>
9037
+
9038
+ <div class="control-group">
9039
+ <label class="control-label" for="search-input">Search:</label>
9040
+ <input type="text" id="search-input" class="control-input" placeholder="Filter nodes...">
9041
+ </div>
9042
+
9043
+ <div class="control-spacer"></div>
9044
+
9045
+ <div class="zoom-controls">
9046
+ <button class="zoom-btn" id="zoom-out" title="Zoom out (-)">\u2212</button>
9047
+ <span class="zoom-level">100%</span>
9048
+ <button class="zoom-btn" id="zoom-in" title="Zoom in (+)">+</button>
9049
+ <button class="control-btn" id="zoom-reset" title="Reset zoom (0)">Reset</button>
9050
+ </div>
9051
+ `;
9052
+ }
9053
+ function validateVisualizationData(data) {
9054
+ if (data === null || data === void 0 || typeof data !== "object") {
9055
+ return err6({
9056
+ code: "INVALID_DATA",
9057
+ message: "Visualization data must be an object"
9058
+ });
9059
+ }
9060
+ const d = data;
9061
+ if (!Array.isArray(d.nodes)) {
9062
+ return err6({
9063
+ code: "INVALID_DATA",
9064
+ message: "Visualization data must have a nodes array"
9065
+ });
9066
+ }
9067
+ if (!Array.isArray(d.edges)) {
9068
+ return err6({
9069
+ code: "INVALID_DATA",
9070
+ message: "Visualization data must have an edges array"
9071
+ });
9072
+ }
9073
+ if (d.statistics === null || d.statistics === void 0 || typeof d.statistics !== "object") {
9074
+ return err6({
9075
+ code: "INVALID_DATA",
9076
+ message: "Visualization data must have statistics object"
9077
+ });
9078
+ }
9079
+ if (d.metadata === null || d.metadata === void 0 || typeof d.metadata !== "object") {
9080
+ return err6({
9081
+ code: "INVALID_DATA",
9082
+ message: "Visualization data must have metadata object"
9083
+ });
9084
+ }
9085
+ return ok24(data);
9086
+ }
9087
+ function renderVisualizationHtml(data, options = {}) {
9088
+ const opts = {
9089
+ ...DEFAULT_HTML_RENDER_OPTIONS,
9090
+ ...options
9091
+ };
9092
+ const validationResult = validateVisualizationData(data);
9093
+ if (validationResult.success === false) {
9094
+ return validationResult;
9095
+ }
9096
+ const sanitizedData = sanitizeVisualizationData(data);
9097
+ const dataJson = JSON.stringify(sanitizedData);
9098
+ const styles = generateStyles();
9099
+ const sidebarHtml = generateSidebarHtml(sanitizedData, opts.title);
9100
+ const controlPanelHtml = generateControlPanelHtml();
9101
+ const graphScript = generateGraphInitScript();
9102
+ const controlScript = generateControlPanelScript();
9103
+ const html = `<!DOCTYPE html>
9104
+ <html lang="en">
9105
+ <head>
9106
+ <meta charset="UTF-8">
9107
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
9108
+ <meta name="generator" content="@bfra.me/workspace-analyzer">
9109
+ <meta name="description" content="Interactive dependency graph visualization">
9110
+ <title>${sanitizeHtml(opts.title)}</title>
9111
+ <style>
9112
+ ${styles}
9113
+ ${opts.customCss ?? ""}
9114
+ </style>
9115
+ </head>
9116
+ <body>
9117
+ <div class="app-container">
9118
+ <aside class="sidebar">
9119
+ ${sidebarHtml}
9120
+ </aside>
9121
+ <main class="main-content">
9122
+ <div class="control-panel">
9123
+ ${controlPanelHtml}
9124
+ </div>
9125
+ <div class="graph-container">
9126
+ <svg class="graph-canvas"></svg>
9127
+ <div class="loading-overlay">
9128
+ <div class="loading-spinner"></div>
9129
+ </div>
9130
+ </div>
9131
+ </main>
9132
+ </div>
9133
+ <div class="tooltip"></div>
9134
+
9135
+ <script>
9136
+ // Visualization data (sanitized)
9137
+ window.VISUALIZATION_DATA = ${dataJson};
9138
+ </script>
9139
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
9140
+ <script>
9141
+ // Graph Initialization
9142
+ ${graphScript}
9143
+ </script>
9144
+ <script>
9145
+ // Control Panel
9146
+ ${controlScript}
9147
+ </script>
9148
+ ${opts.customJs === void 0 ? "" : ` <script>
9149
+ ${opts.customJs}
9150
+ </script>`}
9151
+ </body>
9152
+ </html>`;
9153
+ return ok24(html);
9154
+ }
9155
+ function exportVisualizationJson(data) {
9156
+ const sanitizedData = sanitizeVisualizationData(data);
9157
+ return JSON.stringify(sanitizedData, null, 2);
9158
+ }
9159
+ function estimateHtmlSize(data) {
9160
+ const baseSize = 5e4;
9161
+ const dataJson = JSON.stringify(data);
9162
+ const dataSize = dataJson.length * 1.1;
9163
+ const scriptsSize = 1e4;
9164
+ return Math.ceil(baseSize + dataSize + scriptsSize);
9165
+ }
9166
+ function isWithinSizeLimit(data, maxSizeBytes = 5 * 1024 * 1024) {
9167
+ return estimateHtmlSize(data) < maxSizeBytes;
9168
+ }
9169
+
9170
+ // src/visualizer/mermaid-exporter.ts
9171
+ var DEFAULT_MERMAID_OPTIONS = {
9172
+ cyclesOnly: false,
9173
+ maxNodes: Number.POSITIVE_INFINITY,
9174
+ includeViolations: true,
9175
+ direction: "LR"
9176
+ };
9177
+ function sanitizeMermaidId(id) {
9178
+ return id.replaceAll(/\W/g, "_");
9179
+ }
9180
+ function sanitizeMermaidLabel(label) {
9181
+ return label.replaceAll('"', String.raw`\"`).replaceAll("\n", " ");
9182
+ }
9183
+ function getShortName2(filePath) {
9184
+ const parts = filePath.split("/");
9185
+ return parts.at(-1) ?? filePath;
9186
+ }
9187
+ function generateNodeDefinition(nodeId, label, violations, includeViolations) {
9188
+ const sanitizedId = sanitizeMermaidId(nodeId);
9189
+ const sanitizedLabel = sanitizeMermaidLabel(label);
9190
+ let nodeLabel = sanitizedLabel;
9191
+ if (includeViolations && violations.length > 0) {
9192
+ const violationSummary = violations.reduce((acc, v) => {
9193
+ acc[v.severity] = (acc[v.severity] ?? 0) + 1;
9194
+ return acc;
9195
+ }, {});
9196
+ const violationText = Object.entries(violationSummary).map(([severity, count]) => `${count} ${severity}`).join(", ");
9197
+ nodeLabel = `${sanitizedLabel}<br/>(${violationText})`;
9198
+ }
9199
+ const highestSeverity = violations.length > 0 ? violations.reduce((highest, v) => {
9200
+ if (highest === void 0) return v.severity;
9201
+ const severityOrder = { critical: 0, error: 1, warning: 2, info: 3 };
9202
+ const currentOrder = severityOrder[v.severity] ?? 999;
9203
+ const highestOrder = severityOrder[highest] ?? 999;
9204
+ return currentOrder < highestOrder ? v.severity : highest;
9205
+ }, void 0) : void 0;
9206
+ const styleClass = highestSeverity !== void 0 && highestSeverity.length > 0 ? `class-${highestSeverity}` : "class-normal";
9207
+ return ` ${sanitizedId}["${nodeLabel}"]:::${styleClass}`;
9208
+ }
9209
+ function generateEdgeDefinition(edge) {
9210
+ const fromId = sanitizeMermaidId(edge.source);
9211
+ const toId = sanitizeMermaidId(edge.target);
9212
+ const edgeStyle = edge.isInCycle ? "==>" : edge.type === "type-only" ? "-.->" : edge.type === "dynamic" ? "-..->" : "-->";
9213
+ const linkClass = edge.isInCycle ? "linkStyle cycle" : "";
9214
+ return ` ${fromId} ${edgeStyle} ${toId}${linkClass ? ` :::${linkClass}` : ""}`;
9215
+ }
9216
+ function generateStyleDefinitions() {
9217
+ return `
9218
+ classDef class-critical fill:#ff4444,stroke:#cc0000,stroke-width:3px,color:#fff
9219
+ classDef class-error fill:#ff8844,stroke:#cc4400,stroke-width:2px,color:#fff
9220
+ classDef class-warning fill:#ffcc44,stroke:#ccaa00,stroke-width:2px,color:#000
9221
+ classDef class-info fill:#4488ff,stroke:#0044cc,stroke-width:1px,color:#fff
9222
+ classDef class-normal fill:#88ccff,stroke:#0088cc,stroke-width:1px,color:#000
9223
+ linkStyle cycle stroke:#ff0000,stroke-width:3px,stroke-dasharray:5 5
9224
+ `;
9225
+ }
9226
+ function exportVisualizationMermaid(data, options = {}) {
9227
+ const opts = { ...DEFAULT_MERMAID_OPTIONS, ...options };
9228
+ const lines = [`graph ${opts.direction}`];
9229
+ const cycleNodeIds = /* @__PURE__ */ new Set();
9230
+ if (opts.cyclesOnly) {
9231
+ for (const cycle of data.cycles) {
9232
+ for (const nodeId of cycle.nodes) {
9233
+ cycleNodeIds.add(nodeId);
9234
+ }
9235
+ }
9236
+ }
9237
+ const filteredNodes = opts.cyclesOnly ? data.nodes.filter((n) => cycleNodeIds.has(n.id)) : data.nodes;
9238
+ const nodesToInclude = filteredNodes.slice(0, opts.maxNodes);
9239
+ const nodeIds = new Set(nodesToInclude.map((n) => n.id));
9240
+ for (const node of nodesToInclude) {
9241
+ const label = getShortName2(node.filePath);
9242
+ const nodeDef = generateNodeDefinition(node.id, label, node.violations, opts.includeViolations);
9243
+ lines.push(nodeDef);
9244
+ }
9245
+ const filteredEdges = data.edges.filter(
9246
+ (e) => nodeIds.has(e.source) && nodeIds.has(e.target) && (!opts.cyclesOnly || e.isInCycle)
9247
+ );
9248
+ for (const edge of filteredEdges) {
9249
+ lines.push(generateEdgeDefinition(edge));
9250
+ }
9251
+ lines.push(generateStyleDefinitions());
9252
+ return lines.join("\n");
9253
+ }
9254
+ function exportCycleMermaid(cycle, data) {
9255
+ const lines = ["graph LR"];
9256
+ const nodeMap = new Map(data.nodes.map((n) => [n.id, n]));
9257
+ for (const nodeId of cycle.nodes) {
9258
+ const node = nodeMap.get(nodeId);
9259
+ if (node === void 0) continue;
9260
+ const label = getShortName2(node.filePath);
9261
+ const nodeDef = generateNodeDefinition(nodeId, label, node.violations, true);
9262
+ lines.push(nodeDef);
9263
+ }
9264
+ for (const edge of cycle.edges) {
9265
+ const fromId = sanitizeMermaidId(edge.from);
9266
+ const toId = sanitizeMermaidId(edge.to);
9267
+ lines.push(` ${fromId} ==> ${toId}`);
9268
+ }
9269
+ lines.push(generateStyleDefinitions());
9270
+ return lines.join("\n");
9271
+ }
9272
+
9273
+ // src/visualizer/templates/d3-bundle.ts
9274
+ var D3_CDN_URLS = {
9275
+ /** Full D3.js bundle from jsdelivr (minified) */
9276
+ jsdelivr: "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js",
9277
+ /** Full D3.js bundle from unpkg (minified) */
9278
+ unpkg: "https://unpkg.com/d3@7/dist/d3.min.js",
9279
+ /** Full D3.js bundle from cdnjs (minified) */
9280
+ cdnjs: "https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"
9281
+ };
9282
+ var D3_VERSION = "7.9.0";
9283
+ var D3_INLINE_MODULES = String.raw`
9284
+ /**
9285
+ * Minimal D3-compatible implementation for force-directed graphs.
9286
+ * This provides the core functionality needed without the full D3 library.
9287
+ *
9288
+ * For full D3.js functionality, the HTML renderer can optionally embed
9289
+ * the complete minified D3.js bundle (~280KB) or load from CDN.
9290
+ */
9291
+ (function(global) {
9292
+ 'use strict';
9293
+
9294
+ // Simple namespace
9295
+ const d3 = {};
9296
+
9297
+ // Selection implementation
9298
+ function Selection(nodes) {
9299
+ this._nodes = Array.isArray(nodes) ? nodes : [nodes];
9300
+ }
9301
+
9302
+ Selection.prototype.select = function(selector) {
9303
+ const node = this._nodes[0];
9304
+ if (!node) return new Selection([]);
9305
+ const selected = typeof selector === 'string'
9306
+ ? node.querySelector(selector)
9307
+ : selector;
9308
+ return new Selection(selected ? [selected] : []);
9309
+ };
9310
+
9311
+ Selection.prototype.selectAll = function(selector) {
9312
+ const results = [];
9313
+ this._nodes.forEach(node => {
9314
+ if (node) {
9315
+ const selected = typeof selector === 'string'
9316
+ ? node.querySelectorAll(selector)
9317
+ : selector;
9318
+ results.push(...Array.from(selected));
9319
+ }
9320
+ });
9321
+ return new Selection(results);
9322
+ };
9323
+
9324
+ Selection.prototype.append = function(type) {
9325
+ const results = [];
9326
+ this._nodes.forEach(node => {
9327
+ if (node) {
9328
+ const child = document.createElementNS(
9329
+ type === 'svg' || node.namespaceURI === 'http://www.w3.org/2000/svg'
9330
+ ? 'http://www.w3.org/2000/svg'
9331
+ : 'http://www.w3.org/1999/xhtml',
9332
+ type
9333
+ );
9334
+ node.appendChild(child);
9335
+ results.push(child);
9336
+ }
9337
+ });
9338
+ return new Selection(results);
9339
+ };
9340
+
9341
+ Selection.prototype.attr = function(name, value) {
9342
+ if (value === undefined) {
9343
+ const node = this._nodes[0];
9344
+ return node ? node.getAttribute(name) : null;
9345
+ }
9346
+ this._nodes.forEach((node, i) => {
9347
+ if (node) {
9348
+ const v = typeof value === 'function' ? value(node.__data__, i) : value;
9349
+ if (v === null) {
9350
+ node.removeAttribute(name);
9351
+ } else {
9352
+ node.setAttribute(name, v);
9353
+ }
9354
+ }
9355
+ });
9356
+ return this;
9357
+ };
9358
+
9359
+ Selection.prototype.style = function(name, value) {
9360
+ if (value === undefined) {
9361
+ const node = this._nodes[0];
9362
+ return node ? getComputedStyle(node).getPropertyValue(name) : null;
9363
+ }
9364
+ this._nodes.forEach((node, i) => {
9365
+ if (node) {
9366
+ const v = typeof value === 'function' ? value(node.__data__, i) : value;
9367
+ node.style.setProperty(name, v);
9368
+ }
9369
+ });
9370
+ return this;
9371
+ };
9372
+
9373
+ Selection.prototype.classed = function(names, value) {
9374
+ const classes = names.split(/\s+/);
9375
+ if (value === undefined) {
9376
+ const node = this._nodes[0];
9377
+ return node ? classes.every(c => node.classList.contains(c)) : false;
9378
+ }
9379
+ this._nodes.forEach((node, i) => {
9380
+ if (node) {
9381
+ const v = typeof value === 'function' ? value(node.__data__, i) : value;
9382
+ classes.forEach(c => {
9383
+ if (c) node.classList.toggle(c, v);
9384
+ });
9385
+ }
9386
+ });
9387
+ return this;
9388
+ };
9389
+
9390
+ Selection.prototype.text = function(value) {
9391
+ if (value === undefined) {
9392
+ const node = this._nodes[0];
9393
+ return node ? node.textContent : null;
9394
+ }
9395
+ this._nodes.forEach((node, i) => {
9396
+ if (node) {
9397
+ node.textContent = typeof value === 'function' ? value(node.__data__, i) : value;
9398
+ }
9399
+ });
9400
+ return this;
9401
+ };
9402
+
9403
+ Selection.prototype.html = function(value) {
9404
+ if (value === undefined) {
9405
+ const node = this._nodes[0];
9406
+ return node ? node.innerHTML : null;
9407
+ }
9408
+ this._nodes.forEach((node, i) => {
9409
+ if (node) {
9410
+ node.innerHTML = typeof value === 'function' ? value(node.__data__, i) : value;
9411
+ }
9412
+ });
9413
+ return this;
9414
+ };
9415
+
9416
+ Selection.prototype.on = function(type, listener) {
9417
+ const types = type.split('.');
9418
+ const eventType = types[0];
9419
+ this._nodes.forEach(node => {
9420
+ if (node) {
9421
+ if (listener === null) {
9422
+ node.removeEventListener(eventType, node['__on_' + type]);
9423
+ } else {
9424
+ const handler = function(event) {
9425
+ listener.call(this, event, this.__data__);
9426
+ };
9427
+ node['__on_' + type] = handler;
9428
+ node.addEventListener(eventType, handler);
9429
+ }
9430
+ }
9431
+ });
9432
+ return this;
9433
+ };
9434
+
9435
+ Selection.prototype.data = function(data, key) {
9436
+ const nodes = this._nodes;
9437
+ const dataArray = typeof data === 'function' ? data() : data;
9438
+
9439
+ // Simple data join - just bind data to existing nodes
9440
+ const update = [];
9441
+ const enter = [];
9442
+ const exit = [];
9443
+
9444
+ dataArray.forEach((d, i) => {
9445
+ if (nodes[i]) {
9446
+ nodes[i].__data__ = d;
9447
+ update.push(nodes[i]);
9448
+ } else {
9449
+ enter.push({__data__: d, _index: i});
9450
+ }
9451
+ });
9452
+
9453
+ for (let i = dataArray.length; i < nodes.length; i++) {
9454
+ exit.push(nodes[i]);
9455
+ }
9456
+
9457
+ const selection = new Selection(update);
9458
+ selection._enter = enter;
9459
+ selection._exit = exit;
9460
+ selection._parent = this._nodes[0]?.parentNode;
9461
+ return selection;
9462
+ };
9463
+
9464
+ Selection.prototype.enter = function() {
9465
+ const enterSelection = new Selection([]);
9466
+ enterSelection._enter = this._enter || [];
9467
+ enterSelection._parent = this._parent;
9468
+ enterSelection.append = function(type) {
9469
+ const results = [];
9470
+ const parent = this._parent || document.body;
9471
+ this._enter.forEach(d => {
9472
+ const node = document.createElementNS(
9473
+ type === 'svg' || parent.namespaceURI === 'http://www.w3.org/2000/svg'
9474
+ ? 'http://www.w3.org/2000/svg'
9475
+ : 'http://www.w3.org/1999/xhtml',
9476
+ type
9477
+ );
9478
+ node.__data__ = d.__data__;
9479
+ parent.appendChild(node);
9480
+ results.push(node);
9481
+ });
9482
+ return new Selection(results);
9483
+ };
9484
+ return enterSelection;
9485
+ };
9486
+
9487
+ Selection.prototype.exit = function() {
9488
+ return new Selection(this._exit || []);
9489
+ };
9490
+
9491
+ Selection.prototype.remove = function() {
9492
+ this._nodes.forEach(node => {
9493
+ if (node && node.parentNode) {
9494
+ node.parentNode.removeChild(node);
9495
+ }
9496
+ });
9497
+ return this;
9498
+ };
9499
+
9500
+ Selection.prototype.join = function(enter, update, exit) {
9501
+ const enterSel = this.enter().append(typeof enter === 'string' ? enter : 'g');
9502
+ const exitSel = this.exit();
9503
+ if (exit) exit(exitSel);
9504
+ else exitSel.remove();
9505
+ const merged = new Selection([...enterSel._nodes, ...this._nodes]);
9506
+ if (update) update(merged);
9507
+ return merged;
9508
+ };
9509
+
9510
+ Selection.prototype.each = function(callback) {
9511
+ this._nodes.forEach((node, i) => {
9512
+ if (node) callback.call(node, node.__data__, i);
9513
+ });
9514
+ return this;
9515
+ };
9516
+
9517
+ Selection.prototype.call = function(fn, ...args) {
9518
+ fn.apply(null, [this, ...args]);
9519
+ return this;
9520
+ };
9521
+
9522
+ Selection.prototype.node = function() {
9523
+ return this._nodes[0] || null;
9524
+ };
9525
+
9526
+ Selection.prototype.nodes = function() {
9527
+ return this._nodes.slice();
9528
+ };
9529
+
9530
+ Selection.prototype.empty = function() {
9531
+ return this._nodes.length === 0;
9532
+ };
9533
+
9534
+ Selection.prototype.size = function() {
9535
+ return this._nodes.length;
9536
+ };
9537
+
9538
+ Selection.prototype.raise = function() {
9539
+ this._nodes.forEach(node => {
9540
+ if (node && node.parentNode) {
9541
+ node.parentNode.appendChild(node);
9542
+ }
9543
+ });
9544
+ return this;
9545
+ };
9546
+
9547
+ Selection.prototype.lower = function() {
9548
+ this._nodes.forEach(node => {
9549
+ if (node && node.parentNode) {
9550
+ node.parentNode.insertBefore(node, node.parentNode.firstChild);
9551
+ }
9552
+ });
9553
+ return this;
9554
+ };
9555
+
9556
+ // Core selection functions
9557
+ d3.select = function(selector) {
9558
+ const node = typeof selector === 'string'
9559
+ ? document.querySelector(selector)
9560
+ : selector;
9561
+ return new Selection(node ? [node] : []);
9562
+ };
9563
+
9564
+ d3.selectAll = function(selector) {
9565
+ const nodes = typeof selector === 'string'
9566
+ ? document.querySelectorAll(selector)
9567
+ : selector;
9568
+ return new Selection(Array.from(nodes));
9569
+ };
9570
+
9571
+ d3.create = function(name) {
9572
+ const node = document.createElementNS(
9573
+ name === 'svg' ? 'http://www.w3.org/2000/svg' : 'http://www.w3.org/1999/xhtml',
9574
+ name
9575
+ );
9576
+ return new Selection([node]);
9577
+ };
9578
+
9579
+ // Force simulation
9580
+ function ForceSimulation(nodes) {
9581
+ this._nodes = nodes || [];
9582
+ this._forces = {};
9583
+ this._alpha = 1;
9584
+ this._alphaMin = 0.001;
9585
+ this._alphaDecay = 1 - Math.pow(this._alphaMin, 1 / 300);
9586
+ this._alphaTarget = 0;
9587
+ this._velocityDecay = 0.6;
9588
+ this._listeners = {tick: [], end: []};
9589
+ this._running = false;
9590
+
9591
+ // Initialize nodes
9592
+ this._nodes.forEach((node, i) => {
9593
+ if (node.x === undefined) node.x = Math.random() * 100;
9594
+ if (node.y === undefined) node.y = Math.random() * 100;
9595
+ if (node.vx === undefined) node.vx = 0;
9596
+ if (node.vy === undefined) node.vy = 0;
9597
+ node.index = i;
9598
+ });
9599
+ }
9600
+
9601
+ ForceSimulation.prototype.nodes = function(nodes) {
9602
+ if (nodes === undefined) return this._nodes;
9603
+ this._nodes = nodes;
9604
+ nodes.forEach((node, i) => {
9605
+ if (node.x === undefined) node.x = Math.random() * 100;
9606
+ if (node.y === undefined) node.y = Math.random() * 100;
9607
+ if (node.vx === undefined) node.vx = 0;
9608
+ if (node.vy === undefined) node.vy = 0;
9609
+ node.index = i;
9610
+ });
9611
+ // Re-initialize forces
9612
+ Object.values(this._forces).forEach(force => {
9613
+ if (force.initialize) force.initialize(this._nodes);
9614
+ });
9615
+ return this;
9616
+ };
9617
+
9618
+ ForceSimulation.prototype.force = function(name, force) {
9619
+ if (force === undefined) return this._forces[name];
9620
+ if (force === null) {
9621
+ delete this._forces[name];
9622
+ } else {
9623
+ this._forces[name] = force;
9624
+ if (force.initialize) force.initialize(this._nodes);
9625
+ }
9626
+ return this;
9627
+ };
9628
+
9629
+ ForceSimulation.prototype.alpha = function(alpha) {
9630
+ if (alpha === undefined) return this._alpha;
9631
+ this._alpha = alpha;
9632
+ return this;
9633
+ };
9634
+
9635
+ ForceSimulation.prototype.alphaMin = function(min) {
9636
+ if (min === undefined) return this._alphaMin;
9637
+ this._alphaMin = min;
9638
+ return this;
9639
+ };
9640
+
9641
+ ForceSimulation.prototype.alphaDecay = function(decay) {
9642
+ if (decay === undefined) return this._alphaDecay;
9643
+ this._alphaDecay = decay;
9644
+ return this;
9645
+ };
9646
+
9647
+ ForceSimulation.prototype.alphaTarget = function(target) {
9648
+ if (target === undefined) return this._alphaTarget;
9649
+ this._alphaTarget = target;
9650
+ return this;
9651
+ };
9652
+
9653
+ ForceSimulation.prototype.velocityDecay = function(decay) {
9654
+ if (decay === undefined) return this._velocityDecay;
9655
+ this._velocityDecay = decay;
9656
+ return this;
9657
+ };
9658
+
9659
+ ForceSimulation.prototype.tick = function(iterations) {
9660
+ iterations = iterations || 1;
9661
+ for (let k = 0; k < iterations; k++) {
9662
+ this._alpha += (this._alphaTarget - this._alpha) * this._alphaDecay;
9663
+
9664
+ // Apply forces
9665
+ Object.values(this._forces).forEach(force => {
9666
+ if (force) force(this._alpha);
9667
+ });
9668
+
9669
+ // Update positions
9670
+ this._nodes.forEach(node => {
9671
+ if (node.fx !== undefined && node.fx !== null) {
9672
+ node.x = node.fx;
9673
+ node.vx = 0;
9674
+ } else {
9675
+ node.vx *= this._velocityDecay;
9676
+ node.x += node.vx;
9677
+ }
9678
+ if (node.fy !== undefined && node.fy !== null) {
9679
+ node.y = node.fy;
9680
+ node.vy = 0;
9681
+ } else {
9682
+ node.vy *= this._velocityDecay;
9683
+ node.y += node.vy;
9684
+ }
9685
+ });
9686
+ }
9687
+ return this;
9688
+ };
9689
+
9690
+ ForceSimulation.prototype.restart = function() {
9691
+ if (this._running) return this;
9692
+ this._running = true;
9693
+ const self = this;
9694
+ function step() {
9695
+ if (!self._running) return;
9696
+ self.tick();
9697
+ self._listeners.tick.forEach(fn => fn());
9698
+ if (self._alpha < self._alphaMin) {
9699
+ self._running = false;
9700
+ self._listeners.end.forEach(fn => fn());
9701
+ return;
9702
+ }
9703
+ requestAnimationFrame(step);
9704
+ }
9705
+ requestAnimationFrame(step);
9706
+ return this;
9707
+ };
9708
+
9709
+ ForceSimulation.prototype.stop = function() {
9710
+ this._running = false;
9711
+ return this;
9712
+ };
9713
+
9714
+ ForceSimulation.prototype.on = function(type, listener) {
9715
+ const eventType = type.split('.')[0];
9716
+ if (listener === undefined) {
9717
+ return this._listeners[eventType] || [];
9718
+ }
9719
+ if (!this._listeners[eventType]) {
9720
+ this._listeners[eventType] = [];
9721
+ }
9722
+ if (listener === null) {
9723
+ this._listeners[eventType] = [];
9724
+ } else {
9725
+ this._listeners[eventType].push(listener);
9726
+ }
9727
+ return this;
9728
+ };
9729
+
9730
+ ForceSimulation.prototype.find = function(x, y, radius) {
9731
+ let closest = null;
9732
+ let minDist = radius !== undefined ? radius : Infinity;
9733
+ this._nodes.forEach(node => {
9734
+ const dx = node.x - x;
9735
+ const dy = node.y - y;
9736
+ const dist = Math.sqrt(dx * dx + dy * dy);
9737
+ if (dist < minDist) {
9738
+ minDist = dist;
9739
+ closest = node;
9740
+ }
9741
+ });
9742
+ return closest;
9743
+ };
9744
+
9745
+ d3.forceSimulation = function(nodes) {
9746
+ return new ForceSimulation(nodes);
9747
+ };
9748
+
9749
+ // Force: Link
9750
+ d3.forceLink = function(links) {
9751
+ let _links = links || [];
9752
+ let _id = d => d.index;
9753
+ let _distance = 30;
9754
+ let _strength = 1;
9755
+ let _nodes;
9756
+
9757
+ function force(alpha) {
9758
+ _links.forEach(link => {
9759
+ const source = typeof link.source === 'object' ? link.source : _nodes[link.source];
9760
+ const target = typeof link.target === 'object' ? link.target : _nodes[link.target];
9761
+ if (!source || !target) return;
9762
+
9763
+ let dx = target.x - source.x;
9764
+ let dy = target.y - source.y;
9765
+ let dist = Math.sqrt(dx * dx + dy * dy) || 1;
9766
+ let targetDist = typeof _distance === 'function' ? _distance(link) : _distance;
9767
+ let str = typeof _strength === 'function' ? _strength(link) : _strength;
9768
+
9769
+ const k = (dist - targetDist) / dist * alpha * str;
9770
+ dx *= k;
9771
+ dy *= k;
9772
+
9773
+ const sourceWeight = 1;
9774
+ const targetWeight = 1;
9775
+ const totalWeight = sourceWeight + targetWeight;
9776
+
9777
+ target.vx -= dx * sourceWeight / totalWeight;
9778
+ target.vy -= dy * sourceWeight / totalWeight;
9779
+ source.vx += dx * targetWeight / totalWeight;
9780
+ source.vy += dy * targetWeight / totalWeight;
9781
+ });
9782
+ }
9783
+
9784
+ force.initialize = function(nodes) {
9785
+ _nodes = nodes;
9786
+ const nodeById = new Map(nodes.map(d => [_id(d), d]));
9787
+ _links.forEach(link => {
9788
+ if (typeof link.source !== 'object') link.source = nodeById.get(link.source);
9789
+ if (typeof link.target !== 'object') link.target = nodeById.get(link.target);
9790
+ });
9791
+ };
9792
+
9793
+ force.links = function(links) {
9794
+ if (links === undefined) return _links;
9795
+ _links = links;
9796
+ return force;
9797
+ };
9798
+
9799
+ force.id = function(id) {
9800
+ if (id === undefined) return _id;
9801
+ _id = id;
9802
+ return force;
9803
+ };
9804
+
9805
+ force.distance = function(distance) {
9806
+ if (distance === undefined) return _distance;
9807
+ _distance = typeof distance === 'function' ? distance : +distance;
9808
+ return force;
9809
+ };
9810
+
9811
+ force.strength = function(strength) {
9812
+ if (strength === undefined) return _strength;
9813
+ _strength = typeof strength === 'function' ? strength : +strength;
9814
+ return force;
9815
+ };
9816
+
9817
+ return force;
9818
+ };
9819
+
9820
+ // Force: Many-Body (charge)
9821
+ d3.forceManyBody = function() {
9822
+ let _strength = -30;
9823
+ let _distanceMin = 1;
9824
+ let _distanceMax = Infinity;
9825
+ let _nodes;
9826
+
9827
+ function force(alpha) {
9828
+ for (let i = 0; i < _nodes.length; i++) {
9829
+ for (let j = i + 1; j < _nodes.length; j++) {
9830
+ const nodeI = _nodes[i];
9831
+ const nodeJ = _nodes[j];
9832
+
9833
+ let dx = nodeJ.x - nodeI.x;
9834
+ let dy = nodeJ.y - nodeI.y;
9835
+ let dist = Math.sqrt(dx * dx + dy * dy);
9836
+
9837
+ if (dist < _distanceMin) dist = _distanceMin;
9838
+ if (dist > _distanceMax) continue;
9839
+
9840
+ const str = typeof _strength === 'function' ? _strength(nodeI) : _strength;
9841
+ const k = str * alpha / (dist * dist);
9842
+
9843
+ dx *= k;
9844
+ dy *= k;
9845
+
9846
+ nodeJ.vx += dx;
9847
+ nodeJ.vy += dy;
9848
+ nodeI.vx -= dx;
9849
+ nodeI.vy -= dy;
9850
+ }
9851
+ }
9852
+ }
9853
+
9854
+ force.initialize = function(nodes) {
9855
+ _nodes = nodes;
9856
+ };
9857
+
9858
+ force.strength = function(strength) {
9859
+ if (strength === undefined) return _strength;
9860
+ _strength = typeof strength === 'function' ? strength : +strength;
9861
+ return force;
9862
+ };
9863
+
9864
+ force.distanceMin = function(distance) {
9865
+ if (distance === undefined) return _distanceMin;
9866
+ _distanceMin = +distance;
9867
+ return force;
9868
+ };
9869
+
9870
+ force.distanceMax = function(distance) {
9871
+ if (distance === undefined) return _distanceMax;
9872
+ _distanceMax = +distance;
9873
+ return force;
9874
+ };
9875
+
9876
+ return force;
9877
+ };
9878
+
9879
+ // Force: Center
9880
+ d3.forceCenter = function(x, y) {
9881
+ let _x = x || 0;
9882
+ let _y = y || 0;
9883
+ let _strength = 1;
9884
+ let _nodes;
9885
+
9886
+ function force(alpha) {
9887
+ let sx = 0, sy = 0;
9888
+ _nodes.forEach(node => {
9889
+ sx += node.x;
9890
+ sy += node.y;
9891
+ });
9892
+ sx = (sx / _nodes.length - _x) * _strength;
9893
+ sy = (sy / _nodes.length - _y) * _strength;
9894
+ _nodes.forEach(node => {
9895
+ node.x -= sx;
9896
+ node.y -= sy;
9897
+ });
9898
+ }
9899
+
9900
+ force.initialize = function(nodes) {
9901
+ _nodes = nodes;
9902
+ };
9903
+
9904
+ force.x = function(x) {
9905
+ if (x === undefined) return _x;
9906
+ _x = +x;
9907
+ return force;
9908
+ };
9909
+
9910
+ force.y = function(y) {
9911
+ if (y === undefined) return _y;
9912
+ _y = +y;
9913
+ return force;
9914
+ };
9915
+
9916
+ force.strength = function(strength) {
9917
+ if (strength === undefined) return _strength;
9918
+ _strength = +strength;
9919
+ return force;
9920
+ };
9921
+
9922
+ return force;
9923
+ };
9924
+
9925
+ // Force: Collide
9926
+ d3.forceCollide = function(radius) {
9927
+ let _radius = radius || 1;
9928
+ let _strength = 1;
9929
+ let _iterations = 1;
9930
+ let _nodes;
9931
+
9932
+ function force(alpha) {
9933
+ for (let iter = 0; iter < _iterations; iter++) {
9934
+ for (let i = 0; i < _nodes.length; i++) {
9935
+ for (let j = i + 1; j < _nodes.length; j++) {
9936
+ const nodeI = _nodes[i];
9937
+ const nodeJ = _nodes[j];
9938
+
9939
+ let dx = nodeJ.x - nodeI.x;
9940
+ let dy = nodeJ.y - nodeI.y;
9941
+ let dist = Math.sqrt(dx * dx + dy * dy) || 1;
9942
+
9943
+ const ri = typeof _radius === 'function' ? _radius(nodeI) : _radius;
9944
+ const rj = typeof _radius === 'function' ? _radius(nodeJ) : _radius;
9945
+ const minDist = ri + rj;
9946
+
9947
+ if (dist < minDist) {
9948
+ const k = (minDist - dist) / dist * _strength * 0.5;
9949
+ dx *= k;
9950
+ dy *= k;
9951
+ nodeJ.x += dx;
9952
+ nodeJ.y += dy;
9953
+ nodeI.x -= dx;
9954
+ nodeI.y -= dy;
9955
+ }
9956
+ }
9957
+ }
9958
+ }
9959
+ }
9960
+
9961
+ force.initialize = function(nodes) {
9962
+ _nodes = nodes;
9963
+ };
9964
+
9965
+ force.radius = function(radius) {
9966
+ if (radius === undefined) return _radius;
9967
+ _radius = typeof radius === 'function' ? radius : +radius;
9968
+ return force;
9969
+ };
9970
+
9971
+ force.strength = function(strength) {
9972
+ if (strength === undefined) return _strength;
9973
+ _strength = +strength;
9974
+ return force;
9975
+ };
9976
+
9977
+ force.iterations = function(iterations) {
9978
+ if (iterations === undefined) return _iterations;
9979
+ _iterations = +iterations;
9980
+ return force;
9981
+ };
9982
+
9983
+ return force;
9984
+ };
9985
+
9986
+ // Force: X positioning
9987
+ d3.forceX = function(x) {
9988
+ let _x = x || 0;
9989
+ let _strength = 0.1;
9990
+ let _nodes;
9991
+
9992
+ function force(alpha) {
9993
+ _nodes.forEach(node => {
9994
+ const targetX = typeof _x === 'function' ? _x(node) : _x;
9995
+ node.vx += (targetX - node.x) * _strength * alpha;
9996
+ });
9997
+ }
9998
+
9999
+ force.initialize = function(nodes) {
10000
+ _nodes = nodes;
10001
+ };
10002
+
10003
+ force.x = function(x) {
10004
+ if (x === undefined) return _x;
10005
+ _x = typeof x === 'function' ? x : +x;
10006
+ return force;
10007
+ };
10008
+
10009
+ force.strength = function(strength) {
10010
+ if (strength === undefined) return _strength;
10011
+ _strength = +strength;
10012
+ return force;
10013
+ };
10014
+
10015
+ return force;
10016
+ };
10017
+
10018
+ // Force: Y positioning
10019
+ d3.forceY = function(y) {
10020
+ let _y = y || 0;
10021
+ let _strength = 0.1;
10022
+ let _nodes;
10023
+
10024
+ function force(alpha) {
10025
+ _nodes.forEach(node => {
10026
+ const targetY = typeof _y === 'function' ? _y(node) : _y;
10027
+ node.vy += (targetY - node.y) * _strength * alpha;
10028
+ });
10029
+ }
10030
+
10031
+ force.initialize = function(nodes) {
10032
+ _nodes = nodes;
10033
+ };
10034
+
10035
+ force.y = function(y) {
10036
+ if (y === undefined) return _y;
10037
+ _y = typeof y === 'function' ? y : +y;
10038
+ return force;
10039
+ };
10040
+
10041
+ force.strength = function(strength) {
10042
+ if (strength === undefined) return _strength;
10043
+ _strength = +strength;
10044
+ return force;
10045
+ };
10046
+
10047
+ return force;
10048
+ };
10049
+
10050
+ // Zoom behavior
10051
+ function ZoomBehavior() {
10052
+ this._scaleExtent = [0.1, 10];
10053
+ this._translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]];
10054
+ this._transform = {k: 1, x: 0, y: 0};
10055
+ this._listeners = {};
10056
+ this._filter = () => true;
10057
+ }
10058
+
10059
+ ZoomBehavior.prototype.apply = function(selection) {
10060
+ const self = this;
10061
+ selection.each(function() {
10062
+ const element = this;
10063
+
10064
+ // Wheel zoom
10065
+ element.addEventListener('wheel', function(event) {
10066
+ if (!self._filter(event)) return;
10067
+ event.preventDefault();
10068
+ const rect = element.getBoundingClientRect();
10069
+ const x = event.clientX - rect.left;
10070
+ const y = event.clientY - rect.top;
10071
+
10072
+ const k0 = self._transform.k;
10073
+ const k1 = k0 * Math.pow(2, -event.deltaY * 0.002);
10074
+ const k = Math.max(self._scaleExtent[0], Math.min(self._scaleExtent[1], k1));
10075
+
10076
+ // Zoom towards mouse position
10077
+ self._transform.x += (x - self._transform.x) * (1 - k / k0);
10078
+ self._transform.y += (y - self._transform.y) * (1 - k / k0);
10079
+ self._transform.k = k;
10080
+
10081
+ self._emit('zoom', event);
10082
+ });
10083
+
10084
+ // Pan with mouse drag
10085
+ let dragging = false;
10086
+ let startX, startY, startTx, startTy;
10087
+
10088
+ element.addEventListener('mousedown', function(event) {
10089
+ if (!self._filter(event)) return;
10090
+ if (event.button !== 0) return;
10091
+ dragging = true;
10092
+ startX = event.clientX;
10093
+ startY = event.clientY;
10094
+ startTx = self._transform.x;
10095
+ startTy = self._transform.y;
10096
+ event.preventDefault();
10097
+ });
10098
+
10099
+ document.addEventListener('mousemove', function(event) {
10100
+ if (!dragging) return;
10101
+ self._transform.x = startTx + (event.clientX - startX);
10102
+ self._transform.y = startTy + (event.clientY - startY);
10103
+ self._emit('zoom', event);
10104
+ });
10105
+
10106
+ document.addEventListener('mouseup', function() {
10107
+ dragging = false;
10108
+ });
10109
+
10110
+ // Touch support
10111
+ element.addEventListener('touchstart', function(event) {
10112
+ if (!self._filter(event)) return;
10113
+ if (event.touches.length === 1) {
10114
+ dragging = true;
10115
+ startX = event.touches[0].clientX;
10116
+ startY = event.touches[0].clientY;
10117
+ startTx = self._transform.x;
10118
+ startTy = self._transform.y;
10119
+ }
10120
+ }, {passive: true});
10121
+
10122
+ element.addEventListener('touchmove', function(event) {
10123
+ if (!dragging || event.touches.length !== 1) return;
10124
+ self._transform.x = startTx + (event.touches[0].clientX - startX);
10125
+ self._transform.y = startTy + (event.touches[0].clientY - startY);
10126
+ self._emit('zoom', event);
10127
+ }, {passive: true});
10128
+
10129
+ element.addEventListener('touchend', function() {
10130
+ dragging = false;
10131
+ });
10132
+ });
10133
+ return this;
10134
+ };
10135
+
10136
+ ZoomBehavior.prototype.transform = function(selection, transform) {
10137
+ if (transform) {
10138
+ this._transform = {k: transform.k, x: transform.x, y: transform.y};
10139
+ this._emit('zoom', null);
10140
+ }
10141
+ return this;
10142
+ };
10143
+
10144
+ ZoomBehavior.prototype.scaleExtent = function(extent) {
10145
+ if (extent === undefined) return this._scaleExtent;
10146
+ this._scaleExtent = extent;
10147
+ return this;
10148
+ };
10149
+
10150
+ ZoomBehavior.prototype.translateExtent = function(extent) {
10151
+ if (extent === undefined) return this._translateExtent;
10152
+ this._translateExtent = extent;
10153
+ return this;
10154
+ };
10155
+
10156
+ ZoomBehavior.prototype.on = function(type, listener) {
10157
+ const eventType = type.split('.')[0];
10158
+ if (listener === undefined) {
10159
+ return this._listeners[eventType] || [];
10160
+ }
10161
+ if (!this._listeners[eventType]) {
10162
+ this._listeners[eventType] = [];
10163
+ }
10164
+ this._listeners[eventType].push(listener);
10165
+ return this;
10166
+ };
10167
+
10168
+ ZoomBehavior.prototype._emit = function(type, event) {
10169
+ const listeners = this._listeners[type] || [];
10170
+ const self = this;
10171
+ listeners.forEach(fn => {
10172
+ fn.call(null, {
10173
+ type: type,
10174
+ transform: self._transform,
10175
+ sourceEvent: event
10176
+ });
10177
+ });
10178
+ };
10179
+
10180
+ ZoomBehavior.prototype.scaleTo = function(selection, k) {
10181
+ this._transform.k = Math.max(this._scaleExtent[0], Math.min(this._scaleExtent[1], k));
10182
+ this._emit('zoom', null);
10183
+ return this;
10184
+ };
10185
+
10186
+ ZoomBehavior.prototype.scaleBy = function(selection, k) {
10187
+ return this.scaleTo(selection, this._transform.k * k);
10188
+ };
10189
+
10190
+ ZoomBehavior.prototype.translateTo = function(selection, x, y) {
10191
+ this._transform.x = x;
10192
+ this._transform.y = y;
10193
+ this._emit('zoom', null);
10194
+ return this;
10195
+ };
10196
+
10197
+ ZoomBehavior.prototype.translateBy = function(selection, x, y) {
10198
+ this._transform.x += x;
10199
+ this._transform.y += y;
10200
+ this._emit('zoom', null);
10201
+ return this;
10202
+ };
10203
+
10204
+ ZoomBehavior.prototype.filter = function(filter) {
10205
+ if (filter === undefined) return this._filter;
10206
+ this._filter = filter;
10207
+ return this;
10208
+ };
10209
+
10210
+ d3.zoom = function() {
10211
+ const behavior = new ZoomBehavior();
10212
+ const fn = function(selection) {
10213
+ behavior.apply(selection);
10214
+ };
10215
+ Object.keys(ZoomBehavior.prototype).forEach(key => {
10216
+ fn[key] = behavior[key].bind(behavior);
10217
+ });
10218
+ return fn;
10219
+ };
10220
+
10221
+ d3.zoomIdentity = {k: 1, x: 0, y: 0};
10222
+
10223
+ d3.zoomTransform = function(node) {
10224
+ return node.__zoom || d3.zoomIdentity;
10225
+ };
10226
+
10227
+ // Drag behavior
10228
+ function DragBehavior() {
10229
+ this._subject = null;
10230
+ this._container = null;
10231
+ this._listeners = {};
10232
+ this._filter = () => true;
10233
+ }
10234
+
10235
+ DragBehavior.prototype.apply = function(selection) {
10236
+ const self = this;
10237
+ selection.each(function() {
10238
+ const element = this;
10239
+
10240
+ element.addEventListener('mousedown', function(event) {
10241
+ if (!self._filter(event)) return;
10242
+ if (event.button !== 0) return;
10243
+ event.stopPropagation();
10244
+
10245
+ const subject = self._subject
10246
+ ? self._subject.call(element, event, element.__data__)
10247
+ : element.__data__;
10248
+
10249
+ if (subject == null) return;
10250
+
10251
+ const emitEvent = {
10252
+ type: 'start',
10253
+ subject: subject,
10254
+ x: event.clientX,
10255
+ y: event.clientY,
10256
+ dx: 0,
10257
+ dy: 0,
10258
+ active: 1,
10259
+ sourceEvent: event
10260
+ };
10261
+
10262
+ self._emit('start', emitEvent);
10263
+
10264
+ let lastX = event.clientX;
10265
+ let lastY = event.clientY;
10266
+
10267
+ function onMouseMove(event) {
10268
+ const moveEvent = {
10269
+ type: 'drag',
10270
+ subject: subject,
10271
+ x: event.clientX,
10272
+ y: event.clientY,
10273
+ dx: event.clientX - lastX,
10274
+ dy: event.clientY - lastY,
10275
+ active: 1,
10276
+ sourceEvent: event
10277
+ };
10278
+ lastX = event.clientX;
10279
+ lastY = event.clientY;
10280
+ self._emit('drag', moveEvent);
10281
+ }
10282
+
10283
+ function onMouseUp(event) {
10284
+ document.removeEventListener('mousemove', onMouseMove);
10285
+ document.removeEventListener('mouseup', onMouseUp);
10286
+
10287
+ const endEvent = {
10288
+ type: 'end',
10289
+ subject: subject,
10290
+ x: event.clientX,
10291
+ y: event.clientY,
10292
+ dx: 0,
10293
+ dy: 0,
10294
+ active: 0,
10295
+ sourceEvent: event
10296
+ };
10297
+ self._emit('end', endEvent);
10298
+ }
10299
+
10300
+ document.addEventListener('mousemove', onMouseMove);
10301
+ document.addEventListener('mouseup', onMouseUp);
10302
+ });
10303
+
10304
+ // Touch support
10305
+ element.addEventListener('touchstart', function(event) {
10306
+ if (!self._filter(event)) return;
10307
+ if (event.touches.length !== 1) return;
10308
+ event.stopPropagation();
10309
+
10310
+ const touch = event.touches[0];
10311
+ const subject = self._subject
10312
+ ? self._subject.call(element, event, element.__data__)
10313
+ : element.__data__;
10314
+
10315
+ if (subject == null) return;
10316
+
10317
+ const emitEvent = {
10318
+ type: 'start',
10319
+ subject: subject,
10320
+ x: touch.clientX,
10321
+ y: touch.clientY,
10322
+ dx: 0,
10323
+ dy: 0,
10324
+ active: 1,
10325
+ sourceEvent: event
10326
+ };
10327
+
10328
+ self._emit('start', emitEvent);
10329
+
10330
+ let lastX = touch.clientX;
10331
+ let lastY = touch.clientY;
10332
+
10333
+ function onTouchMove(event) {
10334
+ if (event.touches.length !== 1) return;
10335
+ const touch = event.touches[0];
10336
+ const moveEvent = {
10337
+ type: 'drag',
10338
+ subject: subject,
10339
+ x: touch.clientX,
10340
+ y: touch.clientY,
10341
+ dx: touch.clientX - lastX,
10342
+ dy: touch.clientY - lastY,
10343
+ active: 1,
10344
+ sourceEvent: event
10345
+ };
10346
+ lastX = touch.clientX;
10347
+ lastY = touch.clientY;
10348
+ self._emit('drag', moveEvent);
10349
+ }
10350
+
10351
+ function onTouchEnd(event) {
10352
+ element.removeEventListener('touchmove', onTouchMove);
10353
+ element.removeEventListener('touchend', onTouchEnd);
10354
+
10355
+ const endEvent = {
10356
+ type: 'end',
10357
+ subject: subject,
10358
+ x: lastX,
10359
+ y: lastY,
10360
+ dx: 0,
10361
+ dy: 0,
10362
+ active: 0,
10363
+ sourceEvent: event
10364
+ };
10365
+ self._emit('end', endEvent);
10366
+ }
10367
+
10368
+ element.addEventListener('touchmove', onTouchMove, {passive: true});
10369
+ element.addEventListener('touchend', onTouchEnd);
10370
+ }, {passive: true});
10371
+ });
10372
+ return this;
10373
+ };
10374
+
10375
+ DragBehavior.prototype.subject = function(subject) {
10376
+ if (subject === undefined) return this._subject;
10377
+ this._subject = subject;
10378
+ return this;
10379
+ };
10380
+
10381
+ DragBehavior.prototype.container = function(container) {
10382
+ if (container === undefined) return this._container;
10383
+ this._container = container;
10384
+ return this;
10385
+ };
10386
+
10387
+ DragBehavior.prototype.filter = function(filter) {
10388
+ if (filter === undefined) return this._filter;
10389
+ this._filter = filter;
10390
+ return this;
10391
+ };
10392
+
10393
+ DragBehavior.prototype.on = function(type, listener) {
10394
+ const eventType = type.split('.')[0];
10395
+ if (listener === undefined) {
10396
+ return this._listeners[eventType] || [];
10397
+ }
10398
+ if (!this._listeners[eventType]) {
10399
+ this._listeners[eventType] = [];
10400
+ }
10401
+ this._listeners[eventType].push(listener);
10402
+ return this;
10403
+ };
10404
+
10405
+ DragBehavior.prototype._emit = function(type, event) {
10406
+ const listeners = this._listeners[type] || [];
10407
+ listeners.forEach(fn => fn(event));
10408
+ };
10409
+
10410
+ d3.drag = function() {
10411
+ const behavior = new DragBehavior();
10412
+ const fn = function(selection) {
10413
+ behavior.apply(selection);
10414
+ };
10415
+ Object.keys(DragBehavior.prototype).forEach(key => {
10416
+ fn[key] = behavior[key].bind(behavior);
10417
+ });
10418
+ return fn;
10419
+ };
10420
+
10421
+ // Utility functions
10422
+ d3.extent = function(values, accessor) {
10423
+ let min, max;
10424
+ for (const value of values) {
10425
+ const v = accessor ? accessor(value) : value;
10426
+ if (v != null) {
10427
+ if (min === undefined) {
10428
+ min = max = v;
10429
+ } else {
10430
+ if (v < min) min = v;
10431
+ if (v > max) max = v;
10432
+ }
10433
+ }
10434
+ }
10435
+ return [min, max];
10436
+ };
10437
+
10438
+ d3.min = function(values, accessor) {
10439
+ let min;
10440
+ for (const value of values) {
10441
+ const v = accessor ? accessor(value) : value;
10442
+ if (v != null && (min === undefined || v < min)) {
10443
+ min = v;
10444
+ }
10445
+ }
10446
+ return min;
10447
+ };
10448
+
10449
+ d3.max = function(values, accessor) {
10450
+ let max;
10451
+ for (const value of values) {
10452
+ const v = accessor ? accessor(value) : value;
10453
+ if (v != null && (max === undefined || v > max)) {
10454
+ max = v;
10455
+ }
10456
+ }
10457
+ return max;
10458
+ };
10459
+
10460
+ // Scale functions (basic linear scale)
10461
+ d3.scaleLinear = function() {
10462
+ let _domain = [0, 1];
10463
+ let _range = [0, 1];
10464
+
10465
+ function scale(x) {
10466
+ const t = (_domain[1] - _domain[0]) || 1;
10467
+ const normalized = (x - _domain[0]) / t;
10468
+ return _range[0] + normalized * (_range[1] - _range[0]);
10469
+ }
10470
+
10471
+ scale.domain = function(domain) {
10472
+ if (domain === undefined) return _domain;
10473
+ _domain = domain;
10474
+ return scale;
10475
+ };
10476
+
10477
+ scale.range = function(range) {
10478
+ if (range === undefined) return _range;
10479
+ _range = range;
10480
+ return scale;
10481
+ };
10482
+
10483
+ scale.invert = function(y) {
10484
+ const t = (_range[1] - _range[0]) || 1;
10485
+ const normalized = (y - _range[0]) / t;
10486
+ return _domain[0] + normalized * (_domain[1] - _domain[0]);
10487
+ };
10488
+
10489
+ return scale;
10490
+ };
10491
+
10492
+ // Color scale (ordinal)
10493
+ d3.scaleOrdinal = function(range) {
10494
+ const _range = range || [];
10495
+ const _domain = [];
10496
+ const _index = new Map();
10497
+
10498
+ function scale(d) {
10499
+ let i = _index.get(d);
10500
+ if (i === undefined) {
10501
+ _index.set(d, i = _domain.push(d) - 1);
10502
+ }
10503
+ return _range[i % _range.length];
10504
+ }
10505
+
10506
+ scale.domain = function(domain) {
10507
+ if (domain === undefined) return _domain.slice();
10508
+ _domain.length = 0;
10509
+ _index.clear();
10510
+ for (const d of domain) {
10511
+ if (!_index.has(d)) {
10512
+ _index.set(d, _domain.push(d) - 1);
10513
+ }
10514
+ }
10515
+ return scale;
10516
+ };
10517
+
10518
+ scale.range = function(range) {
10519
+ if (range === undefined) return _range.slice();
10520
+ _range.length = 0;
10521
+ _range.push(...range);
10522
+ return scale;
10523
+ };
10524
+
10525
+ return scale;
10526
+ };
10527
+
10528
+ // Export d3 to global scope
10529
+ global.d3 = d3;
10530
+
10531
+ })(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);
10532
+ `;
10533
+
10534
+ // src/visualizer/violation-collector.ts
10535
+ import path16 from "path";
10536
+ import { createProject as createProject9 } from "@bfra.me/doc-sync";
10537
+ import { err as err7, isErr, ok as ok25 } from "@bfra.me/es/result";
10538
+ var DEFAULT_VIOLATION_COLLECTION_OPTIONS = {
10539
+ includeInfo: true,
10540
+ maxIssues: 1e4,
10541
+ excludePatterns: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
10542
+ };
10543
+ async function collectVisualizationViolations(context, options = {}) {
10544
+ const opts = {
10545
+ ...DEFAULT_VIOLATION_COLLECTION_OPTIONS,
10546
+ ...options
10547
+ };
10548
+ const { ruleEngine, packages, workspacePath, tsconfigPaths, reportProgress } = context;
10549
+ const allIssues = [];
10550
+ try {
10551
+ for (const pkg of packages) {
10552
+ reportProgress?.(`Collecting violations from ${pkg.name}...`);
10553
+ const tsconfigPath = path16.join(pkg.packagePath, "tsconfig.json");
10554
+ let project;
10555
+ try {
10556
+ project = createProject9({
10557
+ tsConfigPath: tsconfigPath
10558
+ });
10559
+ } catch {
10560
+ continue;
10561
+ }
10562
+ const sourceFiles = getSourceFiles(project);
10563
+ for (const sourceFile of sourceFiles) {
10564
+ const filePath = sourceFile.getFilePath();
10565
+ if (shouldSkipFile2(filePath, opts.excludePatterns)) {
10566
+ continue;
10567
+ }
10568
+ const ruleContext = {
10569
+ sourceFile,
10570
+ pkg,
10571
+ workspacePath,
10572
+ allPackages: packages,
10573
+ tsconfigPaths
10574
+ };
10575
+ const result = await ruleEngine.evaluateFile(ruleContext);
10576
+ if (isErr(result)) {
10577
+ continue;
10578
+ }
10579
+ const issues = result.data;
10580
+ const filteredIssues = opts.includeInfo ? issues : issues.filter((issue) => issue.severity !== "info");
10581
+ allIssues.push(...filteredIssues);
10582
+ if (allIssues.length >= opts.maxIssues) {
10583
+ reportProgress?.(`Reached maximum issue limit (${opts.maxIssues})`);
10584
+ break;
10585
+ }
10586
+ }
10587
+ if (allIssues.length >= opts.maxIssues) {
10588
+ break;
10589
+ }
10590
+ }
10591
+ return ok25(allIssues);
10592
+ } catch (error) {
10593
+ return err7({
10594
+ code: "TRANSFORM_FAILED",
10595
+ message: `Error collecting violations: ${error instanceof Error ? error.message : String(error)}`
10596
+ });
10597
+ }
10598
+ }
10599
+ function mapIssuesToNodes(nodes, issues) {
10600
+ const nodesByPath = /* @__PURE__ */ new Map();
10601
+ for (const node of nodes) {
10602
+ const normalizedPath = normalizePath2(node.filePath);
10603
+ nodesByPath.set(normalizedPath, node);
10604
+ }
10605
+ const issuesByPath = /* @__PURE__ */ new Map();
10606
+ for (const issue of issues) {
10607
+ const normalizedPath = normalizePath2(issue.location.filePath);
10608
+ const existing = issuesByPath.get(normalizedPath) ?? [];
10609
+ existing.push(issue);
10610
+ issuesByPath.set(normalizedPath, existing);
10611
+ }
10612
+ return nodes.map((node) => {
10613
+ const normalizedPath = normalizePath2(node.filePath);
10614
+ const nodeIssues = issuesByPath.get(normalizedPath) ?? [];
10615
+ if (nodeIssues.length === 0) {
10616
+ return node;
10617
+ }
10618
+ const violations = nodeIssues.map((issue, index) => ({
10619
+ id: `${issue.id}-${index}`,
10620
+ message: issue.description,
10621
+ severity: issue.severity,
10622
+ ruleId: issue.id
10623
+ }));
10624
+ const severities = violations.map((v) => v.severity);
10625
+ const highestSeverity = getHighestSeverity(severities);
10626
+ return {
10627
+ ...node,
10628
+ violations,
10629
+ highestViolationSeverity: highestSeverity
10630
+ };
10631
+ });
10632
+ }
10633
+ function getSourceFiles(project) {
10634
+ return project.getSourceFiles().filter((sf) => {
10635
+ const filePath = sf.getFilePath();
10636
+ return (filePath.endsWith(".ts") || filePath.endsWith(".tsx") || filePath.endsWith(".js") || filePath.endsWith(".jsx")) && !filePath.includes("node_modules") && !filePath.includes(".test.") && !filePath.includes(".spec.");
10637
+ });
10638
+ }
10639
+ function shouldSkipFile2(filePath, excludePatterns) {
10640
+ if (excludePatterns.length === 0) {
10641
+ return false;
10642
+ }
10643
+ const normalized = normalizePath2(filePath);
10644
+ return excludePatterns.some((pattern) => {
10645
+ const normalizedPattern = pattern.replaceAll("\\", "/").toLowerCase();
10646
+ return normalized.includes(normalizedPattern.replaceAll("**", "").replaceAll("*", ""));
10647
+ });
10648
+ }
10649
+ function normalizePath2(filePath) {
10650
+ return filePath.replaceAll("\\", "/").toLowerCase();
10651
+ }
10652
+
6990
10653
  export {
6991
10654
  getSourceFile,
6992
10655
  parseSourceFiles,
@@ -7117,6 +10780,30 @@ export {
7117
10780
  getRelativePath,
7118
10781
  createConsoleReporter,
7119
10782
  createJsonReporter,
7120
- createMarkdownReporter
10783
+ createMarkdownReporter,
10784
+ DEFAULT_VISUALIZER_OPTIONS,
10785
+ SEVERITY_ORDER2 as SEVERITY_ORDER,
10786
+ getHighestSeverity,
10787
+ DEFAULT_BUILD_OPTIONS,
10788
+ transformNodeToVisualization,
10789
+ transformEdgeToVisualization,
10790
+ transformCycleToVisualization,
10791
+ buildVisualizationData,
10792
+ DEFAULT_HTML_RENDER_OPTIONS,
10793
+ sanitizeHtml,
10794
+ sanitizeJsString,
10795
+ sanitizeFilePath,
10796
+ sanitizeVisualizationData,
10797
+ renderVisualizationHtml,
10798
+ exportVisualizationJson,
10799
+ estimateHtmlSize,
10800
+ isWithinSizeLimit,
10801
+ exportVisualizationMermaid,
10802
+ exportCycleMermaid,
10803
+ D3_CDN_URLS,
10804
+ D3_VERSION,
10805
+ DEFAULT_VIOLATION_COLLECTION_OPTIONS,
10806
+ collectVisualizationViolations,
10807
+ mapIssuesToNodes
7121
10808
  };
7122
- //# sourceMappingURL=chunk-WOJ4C7N7.js.map
10809
+ //# sourceMappingURL=chunk-4V5KYQED.js.map