@cocaxcode/ai-context-inspector 0.2.0 → 0.3.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.
@@ -1242,39 +1242,74 @@ async function runAllScanners(config) {
1242
1242
 
1243
1243
  // src/report/styles.ts
1244
1244
  var CSS_STYLES = `
1245
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1246
+ AI Context Inspector \u2014 Dashboard Styles
1247
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1248
+
1245
1249
  :root {
1246
1250
  --bg: #0a0a0f;
1251
+ --bg-alt: #0e0e16;
1247
1252
  --bg-card: #12121a;
1248
1253
  --bg-card-hover: #1a1a25;
1249
1254
  --border: #2a2a3a;
1255
+ --border-hover: #3a3a4a;
1250
1256
  --text: #e0e0e8;
1251
1257
  --text-dim: #8888a0;
1252
1258
  --text-bright: #ffffff;
1253
1259
  --accent: #00d4ff;
1254
1260
  --accent-dim: #0099bb;
1261
+ --accent-glow: #00d4ff30;
1255
1262
  --green: #00e676;
1256
1263
  --red: #ff5252;
1257
1264
  --orange: #ffab40;
1258
1265
  --purple: #b388ff;
1266
+ --pink: #ff80ab;
1267
+ --blue: #4285f4;
1259
1268
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
1260
1269
  --font-sans: system-ui, -apple-system, sans-serif;
1261
- --radius: 8px;
1270
+ --radius: 10px;
1271
+ --radius-sm: 6px;
1272
+ --shadow: 0 2px 12px rgba(0,0,0,0.3);
1273
+ --shadow-hover: 0 4px 20px rgba(0,212,255,0.1);
1274
+ --transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1262
1275
  }
1263
1276
 
1264
1277
  @media (prefers-color-scheme: light) {
1265
- :root {
1278
+ :root:not([data-theme="dark"]) {
1266
1279
  --bg: #f5f5f8;
1280
+ --bg-alt: #eeeef3;
1267
1281
  --bg-card: #ffffff;
1268
1282
  --bg-card-hover: #f0f0f5;
1269
1283
  --border: #d0d0dd;
1284
+ --border-hover: #b0b0c0;
1270
1285
  --text: #2a2a3a;
1271
1286
  --text-dim: #666680;
1272
1287
  --text-bright: #000000;
1273
1288
  --accent: #0088cc;
1274
1289
  --accent-dim: #006699;
1290
+ --accent-glow: #0088cc20;
1291
+ --shadow: 0 2px 12px rgba(0,0,0,0.08);
1292
+ --shadow-hover: 0 4px 20px rgba(0,136,204,0.1);
1275
1293
  }
1276
1294
  }
1277
1295
 
1296
+ [data-theme="light"] {
1297
+ --bg: #f5f5f8;
1298
+ --bg-alt: #eeeef3;
1299
+ --bg-card: #ffffff;
1300
+ --bg-card-hover: #f0f0f5;
1301
+ --border: #d0d0dd;
1302
+ --border-hover: #b0b0c0;
1303
+ --text: #2a2a3a;
1304
+ --text-dim: #666680;
1305
+ --text-bright: #000000;
1306
+ --accent: #0088cc;
1307
+ --accent-dim: #006699;
1308
+ --accent-glow: #0088cc20;
1309
+ --shadow: 0 2px 12px rgba(0,0,0,0.08);
1310
+ --shadow-hover: 0 4px 20px rgba(0,136,204,0.1);
1311
+ }
1312
+
1278
1313
  * { margin: 0; padding: 0; box-sizing: border-box; }
1279
1314
 
1280
1315
  body {
@@ -1285,6 +1320,72 @@ body {
1285
1320
  min-height: 100vh;
1286
1321
  }
1287
1322
 
1323
+ /* \u2500\u2500 Nav Bar \u2500\u2500 */
1324
+ .nav-bar {
1325
+ position: sticky;
1326
+ top: 0;
1327
+ z-index: 100;
1328
+ background: color-mix(in srgb, var(--bg) 85%, transparent);
1329
+ backdrop-filter: blur(12px);
1330
+ -webkit-backdrop-filter: blur(12px);
1331
+ border-bottom: 1px solid var(--border);
1332
+ padding: 0.5rem 2rem;
1333
+ display: flex;
1334
+ align-items: center;
1335
+ justify-content: space-between;
1336
+ gap: 1rem;
1337
+ }
1338
+
1339
+ .nav-links {
1340
+ display: flex;
1341
+ gap: 0.25rem;
1342
+ flex-wrap: wrap;
1343
+ align-items: center;
1344
+ }
1345
+
1346
+ .nav-link {
1347
+ padding: 0.3rem 0.7rem;
1348
+ border-radius: var(--radius-sm);
1349
+ font-size: 0.75rem;
1350
+ font-family: var(--font-mono);
1351
+ color: var(--text-dim);
1352
+ text-decoration: none;
1353
+ cursor: pointer;
1354
+ transition: all var(--transition);
1355
+ border: 1px solid transparent;
1356
+ }
1357
+
1358
+ .nav-link:hover {
1359
+ color: var(--accent);
1360
+ background: var(--accent-glow);
1361
+ border-color: var(--accent);
1362
+ }
1363
+
1364
+ .nav-actions {
1365
+ display: flex;
1366
+ gap: 0.5rem;
1367
+ align-items: center;
1368
+ }
1369
+
1370
+ .nav-btn {
1371
+ background: none;
1372
+ border: 1px solid var(--border);
1373
+ color: var(--text-dim);
1374
+ padding: 0.3rem 0.6rem;
1375
+ border-radius: var(--radius-sm);
1376
+ font-size: 0.8rem;
1377
+ cursor: pointer;
1378
+ transition: all var(--transition);
1379
+ font-family: var(--font-mono);
1380
+ }
1381
+
1382
+ .nav-btn:hover {
1383
+ color: var(--accent);
1384
+ border-color: var(--accent);
1385
+ background: var(--accent-glow);
1386
+ }
1387
+
1388
+ /* \u2500\u2500 Container \u2500\u2500 */
1288
1389
  .container {
1289
1390
  max-width: 1200px;
1290
1391
  margin: 0 auto;
@@ -1294,21 +1395,22 @@ body {
1294
1395
  /* \u2500\u2500 Header \u2500\u2500 */
1295
1396
  .header {
1296
1397
  text-align: center;
1297
- padding: 2rem 0 1.5rem;
1298
- border-bottom: 1px solid var(--border);
1299
- margin-bottom: 1.5rem;
1398
+ padding: 2.5rem 0 2rem;
1399
+ margin-bottom: 2rem;
1300
1400
  }
1301
1401
 
1302
1402
  .header h1 {
1303
1403
  font-family: var(--font-mono);
1304
- font-size: 1.5rem;
1404
+ font-size: 1.8rem;
1305
1405
  color: var(--accent);
1306
1406
  margin-bottom: 0.5rem;
1407
+ letter-spacing: -0.02em;
1307
1408
  }
1308
1409
 
1309
1410
  .header .subtitle {
1310
1411
  color: var(--text-dim);
1311
1412
  font-size: 0.85rem;
1413
+ font-family: var(--font-mono);
1312
1414
  }
1313
1415
 
1314
1416
  .badges {
@@ -1336,21 +1438,174 @@ body {
1336
1438
  .badge--green { border-color: var(--green); color: var(--green); }
1337
1439
  .badge--purple { border-color: var(--purple); color: var(--purple); }
1338
1440
  .badge--orange { border-color: var(--orange); color: var(--orange); }
1339
- .badge--blue { border-color: #4285f4; color: #4285f4; }
1441
+ .badge--blue { border-color: var(--blue); color: var(--blue); }
1442
+ .badge--pink { border-color: var(--pink); color: var(--pink); }
1443
+
1444
+ /* \u2500\u2500 Stats Grid \u2500\u2500 */
1445
+ .stats-grid {
1446
+ display: grid;
1447
+ grid-template-columns: repeat(6, 1fr);
1448
+ gap: 1rem;
1449
+ margin: 2rem 0;
1450
+ }
1451
+
1452
+ .stat-card {
1453
+ background: var(--bg-card);
1454
+ border: 1px solid var(--border);
1455
+ border-radius: var(--radius);
1456
+ padding: 1.2rem 1rem;
1457
+ text-align: center;
1458
+ transition: all var(--transition);
1459
+ cursor: default;
1460
+ position: relative;
1461
+ overflow: hidden;
1462
+ }
1463
+
1464
+ .stat-card::before {
1465
+ content: '';
1466
+ position: absolute;
1467
+ top: 0;
1468
+ left: 0;
1469
+ right: 0;
1470
+ height: 3px;
1471
+ background: var(--stat-color, var(--accent));
1472
+ opacity: 0.8;
1473
+ }
1474
+
1475
+ .stat-card:hover {
1476
+ border-color: var(--stat-color, var(--accent));
1477
+ box-shadow: var(--shadow-hover);
1478
+ transform: translateY(-2px);
1479
+ }
1480
+
1481
+ .stat-icon {
1482
+ font-size: 1.5rem;
1483
+ margin-bottom: 0.3rem;
1484
+ display: block;
1485
+ }
1486
+
1487
+ .stat-number {
1488
+ font-size: 2rem;
1489
+ font-weight: 700;
1490
+ font-family: var(--font-mono);
1491
+ color: var(--stat-color, var(--accent));
1492
+ line-height: 1.1;
1493
+ }
1494
+
1495
+ .stat-label {
1496
+ font-size: 0.7rem;
1497
+ color: var(--text-dim);
1498
+ text-transform: uppercase;
1499
+ letter-spacing: 0.08em;
1500
+ font-family: var(--font-mono);
1501
+ margin-top: 0.3rem;
1502
+ }
1503
+
1504
+ /* \u2500\u2500 Ecosystem Map \u2500\u2500 */
1505
+ .ecosystem-map {
1506
+ margin: 2rem 0;
1507
+ border: 1px solid var(--border);
1508
+ border-radius: var(--radius);
1509
+ background: var(--bg-alt);
1510
+ overflow: hidden;
1511
+ position: relative;
1512
+ }
1513
+
1514
+ .ecosystem-svg {
1515
+ width: 100%;
1516
+ height: auto;
1517
+ display: block;
1518
+ }
1519
+
1520
+ .eco-connection {
1521
+ stroke: var(--border);
1522
+ stroke-width: 1.5;
1523
+ fill: none;
1524
+ stroke-dasharray: 6 4;
1525
+ animation: dashFlow 20s linear infinite;
1526
+ }
1527
+
1528
+ .eco-connection--active {
1529
+ stroke: var(--accent);
1530
+ stroke-width: 2;
1531
+ opacity: 0.6;
1532
+ }
1533
+
1534
+ @keyframes dashFlow {
1535
+ to { stroke-dashoffset: -100; }
1536
+ }
1537
+
1538
+ .eco-node {
1539
+ cursor: pointer;
1540
+ transition: transform 0.2s ease;
1541
+ }
1542
+
1543
+ .eco-node:hover {
1544
+ transform: scale(1.1);
1545
+ }
1546
+
1547
+ .eco-node-circle {
1548
+ transition: all 0.2s ease;
1549
+ }
1550
+
1551
+ .eco-node:hover .eco-node-circle {
1552
+ filter: brightness(1.3);
1553
+ }
1554
+
1555
+ .eco-center-circle {
1556
+ filter: drop-shadow(0 0 12px var(--accent-glow));
1557
+ }
1558
+
1559
+ .eco-label {
1560
+ font-family: var(--font-mono);
1561
+ font-size: 11px;
1562
+ fill: var(--text);
1563
+ text-anchor: middle;
1564
+ pointer-events: none;
1565
+ }
1566
+
1567
+ .eco-label--center {
1568
+ font-size: 14px;
1569
+ font-weight: 700;
1570
+ fill: var(--text-bright);
1571
+ }
1572
+
1573
+ .eco-label--count {
1574
+ font-size: 10px;
1575
+ fill: var(--text-dim);
1576
+ }
1577
+
1578
+ .eco-item-circle {
1579
+ opacity: 0.7;
1580
+ transition: opacity 0.2s;
1581
+ }
1582
+
1583
+ .eco-item-circle:hover {
1584
+ opacity: 1;
1585
+ }
1586
+
1587
+ .eco-dimmed {
1588
+ opacity: 0.2;
1589
+ }
1340
1590
 
1341
1591
  /* \u2500\u2500 Search \u2500\u2500 */
1342
1592
  .search-bar {
1343
1593
  position: sticky;
1344
- top: 0;
1594
+ top: 45px;
1345
1595
  z-index: 10;
1346
- background: var(--bg);
1596
+ background: color-mix(in srgb, var(--bg) 90%, transparent);
1597
+ backdrop-filter: blur(8px);
1598
+ -webkit-backdrop-filter: blur(8px);
1347
1599
  padding: 0.75rem 0;
1348
- margin-bottom: 1rem;
1600
+ margin-bottom: 1.5rem;
1601
+ display: flex;
1602
+ align-items: center;
1603
+ gap: 0.75rem;
1349
1604
  }
1350
1605
 
1351
1606
  .search-bar input {
1352
- width: 100%;
1353
- padding: 0.6rem 1rem;
1607
+ flex: 1;
1608
+ padding: 0.65rem 1rem 0.65rem 2.5rem;
1354
1609
  border: 1px solid var(--border);
1355
1610
  border-radius: var(--radius);
1356
1611
  background: var(--bg-card);
@@ -1358,11 +1613,47 @@ body {
1358
1613
  font-family: var(--font-mono);
1359
1614
  font-size: 0.85rem;
1360
1615
  outline: none;
1361
- transition: border-color 0.2s;
1616
+ transition: all var(--transition);
1362
1617
  }
1363
1618
 
1364
1619
  .search-bar input:focus {
1365
1620
  border-color: var(--accent);
1621
+ box-shadow: 0 0 0 3px var(--accent-glow);
1622
+ }
1623
+
1624
+ .search-icon {
1625
+ position: absolute;
1626
+ left: 0.85rem;
1627
+ color: var(--text-dim);
1628
+ font-size: 0.9rem;
1629
+ pointer-events: none;
1630
+ }
1631
+
1632
+ .search-bar-inner {
1633
+ position: relative;
1634
+ flex: 1;
1635
+ }
1636
+
1637
+ .search-results-count {
1638
+ font-size: 0.75rem;
1639
+ color: var(--text-dim);
1640
+ font-family: var(--font-mono);
1641
+ white-space: nowrap;
1642
+ }
1643
+
1644
+ .search-kbd {
1645
+ font-size: 0.65rem;
1646
+ color: var(--text-dim);
1647
+ font-family: var(--font-mono);
1648
+ padding: 0.1rem 0.4rem;
1649
+ border: 1px solid var(--border);
1650
+ border-radius: 3px;
1651
+ position: absolute;
1652
+ right: 0.7rem;
1653
+ top: 50%;
1654
+ transform: translateY(-50%);
1655
+ pointer-events: none;
1656
+ opacity: 0.6;
1366
1657
  }
1367
1658
 
1368
1659
  /* \u2500\u2500 Sections \u2500\u2500 */
@@ -1371,29 +1662,48 @@ body {
1371
1662
  border: 1px solid var(--border);
1372
1663
  border-radius: var(--radius);
1373
1664
  overflow: hidden;
1665
+ box-shadow: var(--shadow);
1666
+ animation: fadeInUp 0.4s ease both;
1374
1667
  }
1375
1668
 
1376
1669
  .section-header {
1377
1670
  display: flex;
1378
1671
  align-items: center;
1379
1672
  justify-content: space-between;
1380
- padding: 0.75rem 1rem;
1673
+ padding: 0.85rem 1.2rem;
1381
1674
  background: var(--bg-card);
1382
1675
  cursor: pointer;
1383
1676
  user-select: none;
1384
- transition: background 0.2s;
1677
+ transition: background var(--transition);
1678
+ gap: 1rem;
1385
1679
  }
1386
1680
 
1387
1681
  .section-header:hover {
1388
1682
  background: var(--bg-card-hover);
1389
1683
  }
1390
1684
 
1685
+ .section-header-left {
1686
+ display: flex;
1687
+ align-items: center;
1688
+ gap: 0.6rem;
1689
+ }
1690
+
1691
+ .section-icon {
1692
+ font-size: 1.1rem;
1693
+ }
1694
+
1391
1695
  .section-header h2 {
1392
1696
  font-size: 1rem;
1393
1697
  font-family: var(--font-mono);
1394
1698
  color: var(--text-bright);
1395
1699
  }
1396
1700
 
1701
+ .section-header-right {
1702
+ display: flex;
1703
+ align-items: center;
1704
+ gap: 0.75rem;
1705
+ }
1706
+
1397
1707
  .section-header .count {
1398
1708
  font-size: 0.8rem;
1399
1709
  color: var(--text-dim);
@@ -1401,35 +1711,47 @@ body {
1401
1711
  }
1402
1712
 
1403
1713
  .section-header .arrow {
1404
- transition: transform 0.2s;
1714
+ transition: transform var(--transition);
1405
1715
  color: var(--text-dim);
1716
+ font-size: 0.7rem;
1717
+ }
1718
+
1719
+ .section-content {
1720
+ max-height: 8000px;
1721
+ overflow: hidden;
1722
+ transition: max-height 0.4s ease, opacity 0.3s ease, padding 0.3s ease;
1723
+ opacity: 1;
1724
+ padding: 1rem 1.2rem;
1725
+ border-top: 1px solid var(--border);
1406
1726
  }
1407
1727
 
1408
1728
  .section.collapsed .section-content {
1409
- display: none;
1729
+ max-height: 0;
1730
+ opacity: 0;
1731
+ padding-top: 0;
1732
+ padding-bottom: 0;
1733
+ border-top-color: transparent;
1410
1734
  }
1411
1735
 
1412
1736
  .section.collapsed .arrow {
1413
1737
  transform: rotate(-90deg);
1414
1738
  }
1415
1739
 
1416
- .section-content {
1417
- padding: 1rem;
1418
- border-top: 1px solid var(--border);
1419
- }
1420
-
1421
1740
  /* \u2500\u2500 Cards \u2500\u2500 */
1422
1741
  .card {
1423
1742
  background: var(--bg-card);
1424
1743
  border: 1px solid var(--border);
1425
1744
  border-radius: var(--radius);
1426
- padding: 1rem;
1745
+ padding: 1rem 1.2rem;
1427
1746
  margin-bottom: 0.75rem;
1428
- transition: border-color 0.2s;
1747
+ transition: all var(--transition);
1748
+ animation: fadeInUp 0.3s ease both;
1749
+ position: relative;
1429
1750
  }
1430
1751
 
1431
1752
  .card:hover {
1432
- border-color: var(--accent-dim);
1753
+ border-color: var(--border-hover);
1754
+ box-shadow: var(--shadow-hover);
1433
1755
  }
1434
1756
 
1435
1757
  .card:last-child {
@@ -1444,6 +1766,7 @@ body {
1444
1766
  display: flex;
1445
1767
  align-items: center;
1446
1768
  gap: 0.5rem;
1769
+ flex-wrap: wrap;
1447
1770
  }
1448
1771
 
1449
1772
  .card-meta {
@@ -1452,6 +1775,53 @@ body {
1452
1775
  margin-bottom: 0.5rem;
1453
1776
  }
1454
1777
 
1778
+ /* \u2500\u2500 Copy Button \u2500\u2500 */
1779
+ .copy-btn {
1780
+ background: none;
1781
+ border: 1px solid var(--border);
1782
+ color: var(--text-dim);
1783
+ font-size: 0.7rem;
1784
+ padding: 0.15rem 0.4rem;
1785
+ border-radius: 3px;
1786
+ cursor: pointer;
1787
+ transition: all var(--transition);
1788
+ font-family: var(--font-mono);
1789
+ opacity: 0;
1790
+ position: absolute;
1791
+ top: 0.75rem;
1792
+ right: 0.75rem;
1793
+ }
1794
+
1795
+ .card:hover .copy-btn {
1796
+ opacity: 1;
1797
+ }
1798
+
1799
+ .copy-btn:hover {
1800
+ color: var(--accent);
1801
+ border-color: var(--accent);
1802
+ }
1803
+
1804
+ .copy-btn--copied {
1805
+ color: var(--green) !important;
1806
+ border-color: var(--green) !important;
1807
+ }
1808
+
1809
+ /* \u2500\u2500 Size Bar \u2500\u2500 */
1810
+ .size-bar {
1811
+ height: 3px;
1812
+ background: var(--border);
1813
+ border-radius: 2px;
1814
+ margin-top: 0.3rem;
1815
+ overflow: hidden;
1816
+ }
1817
+
1818
+ .size-bar-fill {
1819
+ height: 100%;
1820
+ border-radius: 2px;
1821
+ background: linear-gradient(90deg, var(--accent-dim), var(--accent));
1822
+ transition: width 0.6s ease;
1823
+ }
1824
+
1455
1825
  /* \u2500\u2500 Tool badges \u2500\u2500 */
1456
1826
  .tool-badge {
1457
1827
  display: inline-block;
@@ -1497,6 +1867,7 @@ body {
1497
1867
  .status--error { background: #ff525220; color: var(--red); }
1498
1868
  .status--active { background: #00e67620; color: var(--green); }
1499
1869
  .status--configured { background: #00d4ff20; color: var(--accent); }
1870
+ .status--detected { background: #b388ff20; color: var(--purple); }
1500
1871
 
1501
1872
  /* \u2500\u2500 Tool list \u2500\u2500 */
1502
1873
  .tool-list {
@@ -1505,11 +1876,12 @@ body {
1505
1876
  }
1506
1877
 
1507
1878
  .tool-list li {
1508
- padding: 0.3rem 0;
1879
+ padding: 0.35rem 0;
1509
1880
  font-size: 0.8rem;
1510
1881
  border-bottom: 1px solid var(--border);
1511
1882
  display: flex;
1512
1883
  gap: 0.5rem;
1884
+ align-items: baseline;
1513
1885
  }
1514
1886
 
1515
1887
  .tool-list li:last-child {
@@ -1520,33 +1892,37 @@ body {
1520
1892
  font-family: var(--font-mono);
1521
1893
  color: var(--accent);
1522
1894
  white-space: nowrap;
1895
+ font-size: 0.8rem;
1523
1896
  }
1524
1897
 
1525
1898
  .tool-desc {
1526
1899
  color: var(--text-dim);
1527
1900
  overflow: hidden;
1528
1901
  text-overflow: ellipsis;
1902
+ font-size: 0.78rem;
1529
1903
  }
1530
1904
 
1531
1905
  /* \u2500\u2500 Preview \u2500\u2500 */
1532
1906
  .preview {
1533
1907
  background: var(--bg);
1534
1908
  border: 1px solid var(--border);
1535
- border-radius: 4px;
1536
- padding: 0.5rem;
1909
+ border-radius: var(--radius-sm);
1910
+ padding: 0.75rem;
1537
1911
  margin-top: 0.5rem;
1538
1912
  font-family: var(--font-mono);
1539
- font-size: 0.75rem;
1913
+ font-size: 0.73rem;
1540
1914
  color: var(--text-dim);
1541
1915
  white-space: pre-wrap;
1542
1916
  word-break: break-all;
1543
- max-height: 150px;
1917
+ max-height: 200px;
1544
1918
  overflow-y: auto;
1545
1919
  display: none;
1920
+ line-height: 1.5;
1546
1921
  }
1547
1922
 
1548
1923
  .preview.open {
1549
1924
  display: block;
1925
+ animation: fadeIn 0.2s ease;
1550
1926
  }
1551
1927
 
1552
1928
  .preview-toggle {
@@ -1557,15 +1933,17 @@ body {
1557
1933
  cursor: pointer;
1558
1934
  font-family: var(--font-mono);
1559
1935
  padding: 0;
1936
+ transition: color var(--transition);
1560
1937
  }
1561
1938
 
1562
1939
  .preview-toggle:hover {
1940
+ color: var(--accent-dim);
1563
1941
  text-decoration: underline;
1564
1942
  }
1565
1943
 
1566
1944
  /* \u2500\u2500 Groups \u2500\u2500 */
1567
1945
  .tool-group {
1568
- margin-bottom: 1rem;
1946
+ margin-bottom: 1.2rem;
1569
1947
  }
1570
1948
 
1571
1949
  .tool-group:last-child {
@@ -1576,22 +1954,30 @@ body {
1576
1954
  display: flex;
1577
1955
  align-items: center;
1578
1956
  gap: 0.5rem;
1579
- margin-bottom: 0.5rem;
1580
- padding-bottom: 0.3rem;
1957
+ margin-bottom: 0.6rem;
1958
+ padding-bottom: 0.4rem;
1581
1959
  border-bottom: 1px solid var(--border);
1582
1960
  }
1583
1961
 
1584
1962
  /* \u2500\u2500 Empty state \u2500\u2500 */
1585
1963
  .empty-state {
1586
1964
  text-align: center;
1587
- padding: 3rem;
1965
+ padding: 4rem 2rem;
1588
1966
  color: var(--text-dim);
1589
1967
  }
1590
1968
 
1969
+ .empty-state-icon {
1970
+ font-size: 3rem;
1971
+ margin-bottom: 1rem;
1972
+ display: block;
1973
+ opacity: 0.5;
1974
+ }
1975
+
1591
1976
  .empty-state h3 {
1592
- font-size: 1.1rem;
1977
+ font-size: 1.2rem;
1593
1978
  margin-bottom: 0.5rem;
1594
1979
  color: var(--text);
1980
+ font-family: var(--font-mono);
1595
1981
  }
1596
1982
 
1597
1983
  /* \u2500\u2500 Scope badge \u2500\u2500 */
@@ -1601,41 +1987,156 @@ body {
1601
1987
  border-radius: 3px;
1602
1988
  font-weight: 600;
1603
1989
  text-transform: uppercase;
1990
+ letter-spacing: 0.03em;
1604
1991
  }
1605
1992
 
1606
1993
  .scope-badge--project { background: var(--accent); color: var(--bg); }
1607
1994
  .scope-badge--user { background: var(--purple); color: var(--bg); }
1995
+ .scope-badge--vscode { background: #007acc; color: #fff; }
1996
+ .scope-badge--desktop { background: var(--orange); color: var(--bg); }
1997
+
1998
+ /* \u2500\u2500 Warnings \u2500\u2500 */
1999
+ .warning-card {
2000
+ background: var(--bg-card);
2001
+ border: 1px solid var(--orange);
2002
+ border-left: 4px solid var(--orange);
2003
+ border-radius: var(--radius);
2004
+ padding: 0.8rem 1rem;
2005
+ margin-bottom: 0.5rem;
2006
+ font-size: 0.8rem;
2007
+ }
2008
+
2009
+ .warning-card:last-child { margin-bottom: 0; }
2010
+
2011
+ .warning-scanner {
2012
+ font-family: var(--font-mono);
2013
+ font-size: 0.7rem;
2014
+ color: var(--orange);
2015
+ margin-bottom: 0.2rem;
2016
+ }
1608
2017
 
1609
2018
  /* \u2500\u2500 Footer \u2500\u2500 */
1610
2019
  .footer {
1611
2020
  text-align: center;
1612
- padding: 1.5rem 0;
1613
- margin-top: 1rem;
2021
+ padding: 2rem 0;
2022
+ margin-top: 2rem;
1614
2023
  border-top: 1px solid var(--border);
1615
2024
  font-size: 0.75rem;
1616
2025
  color: var(--text-dim);
1617
2026
  font-family: var(--font-mono);
1618
2027
  }
1619
2028
 
2029
+ .footer a {
2030
+ color: var(--accent);
2031
+ text-decoration: none;
2032
+ }
2033
+
2034
+ .footer a:hover {
2035
+ text-decoration: underline;
2036
+ }
2037
+
2038
+ /* \u2500\u2500 Animations \u2500\u2500 */
2039
+ @keyframes fadeInUp {
2040
+ from {
2041
+ opacity: 0;
2042
+ transform: translateY(12px);
2043
+ }
2044
+ to {
2045
+ opacity: 1;
2046
+ transform: translateY(0);
2047
+ }
2048
+ }
2049
+
2050
+ @keyframes fadeIn {
2051
+ from { opacity: 0; }
2052
+ to { opacity: 1; }
2053
+ }
2054
+
2055
+ @keyframes pulse {
2056
+ 0%, 100% { opacity: 0.6; }
2057
+ 50% { opacity: 1; }
2058
+ }
2059
+
1620
2060
  /* \u2500\u2500 Responsive \u2500\u2500 */
2061
+ @media (max-width: 1024px) {
2062
+ .stats-grid {
2063
+ grid-template-columns: repeat(3, 1fr);
2064
+ }
2065
+ }
2066
+
1621
2067
  @media (max-width: 768px) {
1622
2068
  .container { padding: 1rem; }
2069
+ .nav-bar { padding: 0.5rem 1rem; flex-wrap: wrap; }
2070
+ .nav-links { display: none; }
1623
2071
  .badges { gap: 0.5rem; }
1624
2072
  .badge { font-size: 0.7rem; padding: 0.2rem 0.6rem; }
2073
+ .stats-grid { grid-template-columns: repeat(2, 1fr); gap: 0.6rem; }
2074
+ .stat-number { font-size: 1.5rem; }
2075
+ .header h1 { font-size: 1.3rem; }
2076
+ .ecosystem-map { display: none; }
2077
+ .section-content { padding: 0.75rem; }
2078
+ }
2079
+
2080
+ @media (max-width: 480px) {
2081
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
2082
+ .nav-actions { gap: 0.3rem; }
2083
+ }
2084
+
2085
+ /* \u2500\u2500 Print \u2500\u2500 */
2086
+ @media print {
2087
+ .nav-bar, .search-bar, .ecosystem-map, .nav-btn, .copy-btn, .preview-toggle { display: none !important; }
2088
+ body { background: #fff; color: #000; }
2089
+ .container { max-width: 100%; padding: 1rem; }
2090
+ .card, .section { break-inside: avoid; box-shadow: none; border-color: #ccc; }
2091
+ .section-content { max-height: none !important; opacity: 1 !important; }
2092
+ .header h1 { color: #000; }
2093
+ .stat-card { border-color: #ccc; }
2094
+ .stat-number { color: #333; }
1625
2095
  }
1626
2096
  `;
1627
2097
 
1628
2098
  // src/report/scripts.ts
1629
2099
  var JS_SCRIPTS = `
1630
2100
  document.addEventListener('DOMContentLoaded', () => {
1631
- // Section collapse/expand
2101
+
2102
+ // \u2500\u2500 Animated Counters \u2500\u2500
2103
+ document.querySelectorAll('.stat-number').forEach(el => {
2104
+ const target = parseInt(el.getAttribute('data-target') || '0')
2105
+ if (target === 0) { el.textContent = '0'; return }
2106
+ let current = 0
2107
+ const step = Math.max(1, Math.ceil(target / 25))
2108
+ const interval = setInterval(() => {
2109
+ current = Math.min(current + step, target)
2110
+ el.textContent = current
2111
+ if (current >= target) clearInterval(interval)
2112
+ }, 35)
2113
+ })
2114
+
2115
+ // \u2500\u2500 Section Collapse/Expand \u2500\u2500
1632
2116
  document.querySelectorAll('.section-header').forEach(header => {
1633
2117
  header.addEventListener('click', () => {
1634
- header.closest('.section').classList.toggle('collapsed')
2118
+ const section = header.closest('.section')
2119
+ if (!section) return
2120
+ const content = section.querySelector('.section-content')
2121
+ if (!content) return
2122
+
2123
+ if (section.classList.contains('collapsed')) {
2124
+ // Expand
2125
+ section.classList.remove('collapsed')
2126
+ content.style.maxHeight = content.scrollHeight + 'px'
2127
+ setTimeout(() => { content.style.maxHeight = '' }, 400)
2128
+ } else {
2129
+ // Collapse
2130
+ content.style.maxHeight = content.scrollHeight + 'px'
2131
+ requestAnimationFrame(() => {
2132
+ content.style.maxHeight = '0px'
2133
+ section.classList.add('collapsed')
2134
+ })
2135
+ }
1635
2136
  })
1636
2137
  })
1637
2138
 
1638
- // Preview toggle
2139
+ // \u2500\u2500 Preview Toggle \u2500\u2500
1639
2140
  document.querySelectorAll('.preview-toggle').forEach(btn => {
1640
2141
  btn.addEventListener('click', (e) => {
1641
2142
  e.stopPropagation()
@@ -1647,55 +2148,428 @@ document.addEventListener('DOMContentLoaded', () => {
1647
2148
  })
1648
2149
  })
1649
2150
 
1650
- // Global search
2151
+ // \u2500\u2500 Global Search \u2500\u2500
1651
2152
  const searchInput = document.getElementById('search-input')
2153
+ const resultsCount = document.querySelector('.search-results-count')
2154
+ const searchKbd = document.querySelector('.search-kbd')
2155
+
1652
2156
  if (searchInput) {
1653
2157
  searchInput.addEventListener('input', (e) => {
1654
2158
  const query = e.target.value.toLowerCase().trim()
2159
+ let visible = 0
2160
+ let total = 0
2161
+
1655
2162
  document.querySelectorAll('[data-searchable]').forEach(el => {
1656
2163
  const text = el.getAttribute('data-searchable').toLowerCase()
1657
- el.style.display = (!query || text.includes(query)) ? '' : 'none'
2164
+ const match = !query || text.includes(query)
2165
+ el.style.display = match ? '' : 'none'
2166
+ total++
2167
+ if (match) visible++
1658
2168
  })
2169
+
2170
+ if (resultsCount) {
2171
+ resultsCount.textContent = query ? visible + ' / ' + total : ''
2172
+ }
2173
+ if (searchKbd) {
2174
+ searchKbd.style.display = query ? 'none' : ''
2175
+ }
2176
+ })
2177
+
2178
+ searchInput.addEventListener('focus', () => {
2179
+ if (searchKbd) searchKbd.style.display = 'none'
2180
+ })
2181
+
2182
+ searchInput.addEventListener('blur', () => {
2183
+ if (searchKbd && !searchInput.value) searchKbd.style.display = ''
1659
2184
  })
1660
2185
  }
2186
+
2187
+ // \u2500\u2500 Keyboard Shortcuts \u2500\u2500
2188
+ document.addEventListener('keydown', (e) => {
2189
+ // Ignore when typing in input
2190
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
2191
+ if (e.key === 'Escape') {
2192
+ e.target.value = ''
2193
+ e.target.dispatchEvent(new Event('input'))
2194
+ e.target.blur()
2195
+ }
2196
+ return
2197
+ }
2198
+
2199
+ if (e.key === '/') {
2200
+ e.preventDefault()
2201
+ if (searchInput) searchInput.focus()
2202
+ }
2203
+ })
2204
+
2205
+ // \u2500\u2500 Nav Links (scroll to section) \u2500\u2500
2206
+ document.querySelectorAll('.nav-link[data-target]').forEach(link => {
2207
+ link.addEventListener('click', (e) => {
2208
+ e.preventDefault()
2209
+ const targetId = link.getAttribute('data-target')
2210
+ const target = document.getElementById(targetId)
2211
+ if (target) {
2212
+ // Expand if collapsed
2213
+ if (target.classList.contains('collapsed')) {
2214
+ target.classList.remove('collapsed')
2215
+ const content = target.querySelector('.section-content')
2216
+ if (content) {
2217
+ content.style.maxHeight = content.scrollHeight + 'px'
2218
+ setTimeout(() => { content.style.maxHeight = '' }, 400)
2219
+ }
2220
+ }
2221
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
2222
+ }
2223
+ })
2224
+ })
2225
+
2226
+ // \u2500\u2500 Ecosystem Map (click to scroll) \u2500\u2500
2227
+ document.querySelectorAll('.eco-node[data-section]').forEach(node => {
2228
+ node.addEventListener('click', () => {
2229
+ const targetId = node.getAttribute('data-section')
2230
+ const target = document.getElementById(targetId)
2231
+ if (target) {
2232
+ if (target.classList.contains('collapsed')) {
2233
+ target.classList.remove('collapsed')
2234
+ const content = target.querySelector('.section-content')
2235
+ if (content) {
2236
+ content.style.maxHeight = content.scrollHeight + 'px'
2237
+ setTimeout(() => { content.style.maxHeight = '' }, 400)
2238
+ }
2239
+ }
2240
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
2241
+ }
2242
+ })
2243
+ })
2244
+
2245
+ // \u2500\u2500 Theme Toggle \u2500\u2500
2246
+ const themeToggle = document.getElementById('theme-toggle')
2247
+ if (themeToggle) {
2248
+ themeToggle.addEventListener('click', () => {
2249
+ const html = document.documentElement
2250
+ const current = html.getAttribute('data-theme')
2251
+ const next = current === 'light' ? 'dark' : 'light'
2252
+ html.setAttribute('data-theme', next)
2253
+ themeToggle.innerHTML = next === 'light' ? '☀' : '☾'
2254
+ })
2255
+ }
2256
+
2257
+ // \u2500\u2500 Export JSON \u2500\u2500
2258
+ const exportBtn = document.getElementById('export-btn')
2259
+ if (exportBtn) {
2260
+ exportBtn.addEventListener('click', () => {
2261
+ const dataEl = document.getElementById('scan-data')
2262
+ if (!dataEl) return
2263
+ const text = dataEl.textContent || ''
2264
+ const fallback = () => {
2265
+ const ta = document.createElement('textarea')
2266
+ ta.value = text
2267
+ ta.style.position = 'fixed'
2268
+ ta.style.opacity = '0'
2269
+ document.body.appendChild(ta)
2270
+ ta.select()
2271
+ document.execCommand('copy')
2272
+ document.body.removeChild(ta)
2273
+ }
2274
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2275
+ navigator.clipboard.writeText(text).catch(fallback)
2276
+ } else {
2277
+ fallback()
2278
+ }
2279
+ const orig = exportBtn.innerHTML
2280
+ exportBtn.textContent = '\\u2713'
2281
+ setTimeout(() => { exportBtn.innerHTML = orig }, 1500)
2282
+ })
2283
+ }
2284
+
2285
+ // \u2500\u2500 Copy Buttons \u2500\u2500
2286
+ document.querySelectorAll('.copy-btn').forEach(btn => {
2287
+ btn.addEventListener('click', (e) => {
2288
+ e.stopPropagation()
2289
+ const text = btn.getAttribute('data-copy') || ''
2290
+ const fallback = () => {
2291
+ const ta = document.createElement('textarea')
2292
+ ta.value = text
2293
+ ta.style.position = 'fixed'
2294
+ ta.style.opacity = '0'
2295
+ document.body.appendChild(ta)
2296
+ ta.select()
2297
+ document.execCommand('copy')
2298
+ document.body.removeChild(ta)
2299
+ }
2300
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2301
+ navigator.clipboard.writeText(text).catch(fallback)
2302
+ } else {
2303
+ fallback()
2304
+ }
2305
+ btn.classList.add('copy-btn--copied')
2306
+ btn.textContent = '\\u2713'
2307
+ setTimeout(() => {
2308
+ btn.classList.remove('copy-btn--copied')
2309
+ btn.textContent = 'copiar'
2310
+ }, 1500)
2311
+ })
2312
+ })
2313
+
1661
2314
  })
1662
2315
  `;
1663
2316
 
1664
- // src/report/sections.ts
2317
+ // src/report/ecosystem-map.ts
1665
2318
  function esc(str) {
1666
2319
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1667
2320
  }
2321
+ function buildCategories(result, summary) {
2322
+ return [
2323
+ {
2324
+ id: "section-mcp",
2325
+ label: "MCP",
2326
+ icon: "\u2699",
2327
+ count: summary.totalMcpServers,
2328
+ color: "#00d4ff",
2329
+ items: result.mcpServers.slice(0, 8).map((s) => ({
2330
+ name: s.name,
2331
+ detail: `${s.introspection?.tools.length ?? 0} tools`
2332
+ }))
2333
+ },
2334
+ {
2335
+ id: "section-context",
2336
+ label: "Contexto",
2337
+ icon: "\u{1F4C4}",
2338
+ count: summary.totalFiles,
2339
+ color: "#b388ff",
2340
+ items: result.contextFiles.slice(0, 8).map((f) => ({
2341
+ name: f.path,
2342
+ detail: f.tool
2343
+ }))
2344
+ },
2345
+ {
2346
+ id: "section-skills",
2347
+ label: "Skills",
2348
+ icon: "\u26A1",
2349
+ count: summary.totalSkills,
2350
+ color: "#ffab40",
2351
+ items: result.skills.slice(0, 8).map((s) => ({
2352
+ name: s.name
2353
+ }))
2354
+ },
2355
+ {
2356
+ id: "section-agents",
2357
+ label: "Agents",
2358
+ icon: "\u{1F916}",
2359
+ count: summary.totalAgents,
2360
+ color: "#00e676",
2361
+ items: result.agents.slice(0, 8).map((a) => ({
2362
+ name: a.name,
2363
+ detail: a.model
2364
+ }))
2365
+ },
2366
+ {
2367
+ id: "section-memories",
2368
+ label: "Memorias",
2369
+ icon: "\u{1F9E0}",
2370
+ count: summary.totalMemories,
2371
+ color: "#ff80ab",
2372
+ items: result.memories.slice(0, 8).map((m) => ({
2373
+ name: m.type,
2374
+ detail: m.status
2375
+ }))
2376
+ }
2377
+ ];
2378
+ }
2379
+ function renderEcosystemMap(result, summary) {
2380
+ const categories = buildCategories(result, summary);
2381
+ const totalItems = summary.totalMcpServers + summary.totalFiles + summary.totalSkills + summary.totalAgents + summary.totalMemories;
2382
+ if (totalItems === 0) return "";
2383
+ const W = 800;
2384
+ const H = 420;
2385
+ const cx = W / 2;
2386
+ const cy = H / 2;
2387
+ const catRadius = 155;
2388
+ const itemRadius = 70;
2389
+ const startAngle = -Math.PI / 2;
2390
+ let svg = "";
2391
+ svg += `<defs>
2392
+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
2393
+ <feGaussianBlur stdDeviation="4" result="blur"/>
2394
+ <feComposite in="SourceGraphic" in2="blur" operator="over"/>
2395
+ </filter>
2396
+ <filter id="glow-strong" x="-50%" y="-50%" width="200%" height="200%">
2397
+ <feGaussianBlur stdDeviation="8" result="blur"/>
2398
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
2399
+ </filter>
2400
+ </defs>`;
2401
+ svg += `<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
2402
+ <circle cx="15" cy="15" r="0.5" fill="var(--text-dim)" opacity="0.15"/>
2403
+ </pattern>
2404
+ <rect width="${W}" height="${H}" fill="url(#grid)"/>`;
2405
+ const catPositions = [];
2406
+ categories.forEach((cat, i) => {
2407
+ const angle = startAngle + i / categories.length * Math.PI * 2;
2408
+ const x = cx + Math.cos(angle) * catRadius;
2409
+ const y = cy + Math.sin(angle) * catRadius;
2410
+ catPositions.push({ x, y, cat });
2411
+ const dimmed = cat.count === 0;
2412
+ svg += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}"
2413
+ class="eco-connection ${dimmed ? "eco-dimmed" : "eco-connection--active"}"
2414
+ style="animation-delay: ${i * 0.3}s"/>`;
2415
+ if (!dimmed && cat.items.length > 0) {
2416
+ const maxItems = Math.min(cat.items.length, 8);
2417
+ const arcSpread = Math.min(Math.PI * 0.5, maxItems * 0.2);
2418
+ cat.items.slice(0, maxItems).forEach((item, j) => {
2419
+ const itemAngle = angle - arcSpread / 2 + (maxItems > 1 ? j / (maxItems - 1) * arcSpread : 0);
2420
+ const ix = x + Math.cos(itemAngle) * itemRadius;
2421
+ const iy = y + Math.sin(itemAngle) * itemRadius;
2422
+ svg += `<line x1="${x}" y1="${y}" x2="${ix}" y2="${iy}"
2423
+ stroke="${cat.color}" stroke-width="0.8" opacity="0.25"/>`;
2424
+ svg += `<circle cx="${ix}" cy="${iy}" r="4" fill="${cat.color}"
2425
+ class="eco-item-circle" opacity="0.5">
2426
+ <title>${esc(item.name)}${item.detail ? " (" + esc(item.detail) + ")" : ""}</title>
2427
+ </circle>`;
2428
+ });
2429
+ if (cat.items.length < cat.count) {
2430
+ const extra = cat.count - cat.items.length;
2431
+ const moreAngle = angle + arcSpread / 2 + 0.3;
2432
+ const mx = x + Math.cos(moreAngle) * (itemRadius - 10);
2433
+ const my = y + Math.sin(moreAngle) * (itemRadius - 10);
2434
+ svg += `<text x="${mx}" y="${my}" font-family="var(--font-mono)"
2435
+ font-size="9" fill="${cat.color}" opacity="0.6" text-anchor="middle"
2436
+ dominant-baseline="middle">+${extra}</text>`;
2437
+ }
2438
+ }
2439
+ const nodeR = dimmed ? 22 : 28;
2440
+ svg += `<g class="eco-node ${dimmed ? "eco-dimmed" : ""}" data-section="${cat.id}">
2441
+ <circle cx="${x}" cy="${y}" r="${nodeR}"
2442
+ fill="${cat.color}15" stroke="${cat.color}" stroke-width="2"
2443
+ class="eco-node-circle" ${!dimmed ? 'filter="url(#glow)"' : ""}/>
2444
+ <text x="${x}" y="${y - 3}" class="eco-label"
2445
+ fill="${cat.color}" font-weight="600">${cat.icon} ${cat.label}</text>
2446
+ <text x="${x}" y="${y + 12}" class="eco-label eco-label--count">${cat.count}</text>
2447
+ </g>`;
2448
+ });
2449
+ const projectName = result.project.name.length > 18 ? result.project.name.slice(0, 16) + ".." : result.project.name;
2450
+ svg += `<g class="eco-node">
2451
+ <circle cx="${cx}" cy="${cy}" r="38" fill="var(--accent)" opacity="0.1"
2452
+ stroke="var(--accent)" stroke-width="2" class="eco-center-circle"
2453
+ filter="url(#glow-strong)"/>
2454
+ <circle cx="${cx}" cy="${cy}" r="36" fill="var(--bg-alt)" opacity="0.9"/>
2455
+ <text x="${cx}" y="${cy - 5}" class="eco-label eco-label--center">${esc(projectName)}</text>
2456
+ <text x="${cx}" y="${cy + 12}" class="eco-label eco-label--count">${totalItems} elementos</text>
2457
+ </g>`;
2458
+ return `
2459
+ <div class="ecosystem-map">
2460
+ <svg class="ecosystem-svg" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg">
2461
+ ${svg}
2462
+ </svg>
2463
+ </div>`;
2464
+ }
2465
+
2466
+ // src/report/sections.ts
2467
+ function esc2(str) {
2468
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2469
+ }
1668
2470
  function formatBytes(bytes) {
1669
2471
  if (bytes < 1024) return `${bytes} B`;
1670
2472
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1671
2473
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1672
2474
  }
2475
+ function renderNavBar(summary) {
2476
+ const links = [
2477
+ { id: "section-mcp", label: "MCP", count: summary.totalMcpServers },
2478
+ { id: "section-context", label: "Contexto", count: summary.totalFiles },
2479
+ { id: "section-skills", label: "Skills", count: summary.totalSkills },
2480
+ { id: "section-agents", label: "Agents", count: summary.totalAgents },
2481
+ { id: "section-memories", label: "Memorias", count: summary.totalMemories }
2482
+ ];
2483
+ const navLinks = links.filter((l) => l.count > 0).map(
2484
+ (l) => `<a class="nav-link" data-target="${l.id}">${l.label} (${l.count})</a>`
2485
+ ).join("");
2486
+ return `
2487
+ <nav class="nav-bar">
2488
+ <div class="nav-links">
2489
+ <span style="font-family:var(--font-mono);font-size:0.8rem;color:var(--accent);margin-right:0.5rem">&gt;_</span>
2490
+ ${navLinks}
2491
+ </div>
2492
+ <div class="nav-actions">
2493
+ <button class="nav-btn" id="theme-toggle" title="Cambiar tema">&#9790;</button>
2494
+ <button class="nav-btn" id="export-btn" title="Exportar JSON">&#128203;</button>
2495
+ </div>
2496
+ </nav>`;
2497
+ }
1673
2498
  function renderHeader(project, summary, scanDuration) {
1674
2499
  const date = new Date(project.scannedAt).toLocaleString();
1675
2500
  return `
1676
2501
  <header class="header">
1677
2502
  <h1>&gt; ai-context-inspector</h1>
1678
- <div class="subtitle">${esc(project.name)} &mdash; ${date} &mdash; ${scanDuration}ms</div>
2503
+ <div class="subtitle">${esc2(project.name)} &mdash; ${date} &mdash; ${scanDuration}ms</div>
1679
2504
  <div class="badges">
1680
2505
  <span class="badge badge--accent">${summary.totalMcpServers} MCPs</span>
1681
2506
  <span class="badge badge--green">${summary.totalTools} tools</span>
1682
2507
  <span class="badge badge--purple">${summary.totalFiles} archivos</span>
1683
2508
  <span class="badge badge--orange">${summary.totalSkills} skills</span>
1684
2509
  <span class="badge badge--blue">${summary.totalAgents} agents</span>
1685
- <span class="badge badge--accent">${summary.totalMemories} memorias</span>
2510
+ <span class="badge badge--pink">${summary.totalMemories} memorias</span>
1686
2511
  </div>
1687
2512
  </header>`;
1688
2513
  }
2514
+ function renderStatsGrid(summary) {
2515
+ const stats = [
2516
+ {
2517
+ icon: "\u2699\uFE0F",
2518
+ value: summary.totalMcpServers,
2519
+ label: "MCP Servers",
2520
+ color: "#00d4ff"
2521
+ },
2522
+ {
2523
+ icon: "\u{1F6E0}\uFE0F",
2524
+ value: summary.totalTools,
2525
+ label: "MCP Tools",
2526
+ color: "#00e676"
2527
+ },
2528
+ {
2529
+ icon: "\u{1F4C4}",
2530
+ value: summary.totalFiles,
2531
+ label: "Archivos AI",
2532
+ color: "#b388ff"
2533
+ },
2534
+ {
2535
+ icon: "\u26A1",
2536
+ value: summary.totalSkills,
2537
+ label: "Skills",
2538
+ color: "#ffab40"
2539
+ },
2540
+ {
2541
+ icon: "\u{1F916}",
2542
+ value: summary.totalAgents,
2543
+ label: "Agents",
2544
+ color: "#4285f4"
2545
+ },
2546
+ {
2547
+ icon: "\u{1F9E0}",
2548
+ value: summary.totalMemories,
2549
+ label: "Memorias",
2550
+ color: "#ff80ab"
2551
+ }
2552
+ ];
2553
+ const cards = stats.map(
2554
+ (s) => `
2555
+ <div class="stat-card" style="--stat-color: ${s.color}">
2556
+ <span class="stat-icon">${s.icon}</span>
2557
+ <span class="stat-number" data-target="${s.value}">0</span>
2558
+ <span class="stat-label">${s.label}</span>
2559
+ </div>`
2560
+ ).join("");
2561
+ return `<div class="stats-grid">${cards}</div>`;
2562
+ }
1689
2563
  function renderMcpServers(servers) {
1690
2564
  if (servers.length === 0) return "";
1691
- const cards = servers.map((s) => {
2565
+ const cards = servers.map((s, i) => {
1692
2566
  const intro = s.introspection;
1693
2567
  const statusClass = intro ? `status--${intro.status}` : "status--configured";
1694
2568
  const statusText = intro ? intro.status === "ok" ? "OK" : intro.status === "timeout" ? "Timeout" : "Error" : "No introspectado";
1695
2569
  let toolsHtml = "";
1696
2570
  if (intro && intro.tools.length > 0) {
1697
2571
  const items = intro.tools.map(
1698
- (t) => `<li data-searchable="${esc(t.name + " " + (t.description ?? ""))}"><span class="tool-name">${esc(t.name)}</span><span class="tool-desc">${esc(t.description ?? "")}</span></li>`
2572
+ (t) => `<li data-searchable="${esc2(t.name + " " + (t.description ?? ""))}"><span class="tool-name">${esc2(t.name)}</span><span class="tool-desc">${esc2(t.description ?? "")}</span></li>`
1699
2573
  ).join("");
1700
2574
  toolsHtml = `<div class="card-meta">${intro.tools.length} tools</div><ul class="tool-list">${items}</ul>`;
1701
2575
  }
@@ -1707,13 +2581,17 @@ function renderMcpServers(servers) {
1707
2581
  if (intro && intro.prompts.length > 0) {
1708
2582
  promptsHtml = `<div class="card-meta" style="margin-top:0.5rem">${intro.prompts.length} prompts</div>`;
1709
2583
  }
1710
- const serverVersion = intro?.serverInfo ? ` v${esc(intro.serverInfo.version)}` : "";
1711
- const errorHtml = intro?.error ? `<div class="card-meta" style="color:var(--red)">${esc(intro.error)}</div>` : "";
1712
- const configHtml = s.config.command ? `<div class="card-meta">${esc(s.config.command)} ${esc((s.config.args ?? []).join(" "))}</div>` : s.config.url ? `<div class="card-meta">${esc(s.config.url)}</div>` : "";
2584
+ const serverVersion = intro?.serverInfo ? ` v${esc2(intro.serverInfo.version)}` : "";
2585
+ const errorHtml = intro?.error ? `<div class="card-meta" style="color:var(--red)">${esc2(intro.error)}</div>` : "";
2586
+ const cmdStr = s.config.command ? `${s.config.command} ${(s.config.args ?? []).join(" ")}` : s.config.url ?? "";
2587
+ const configHtml = cmdStr ? `<div class="card-meta">${esc2(cmdStr)}</div>` : "";
2588
+ const copyData = cmdStr || s.name;
1713
2589
  return `
1714
- <div class="card" data-searchable="${esc(s.name + " " + (intro?.serverInfo?.name ?? ""))}">
2590
+ <div class="card" data-searchable="${esc2(s.name + " " + (intro?.serverInfo?.name ?? ""))}"
2591
+ style="animation-delay: ${i * 0.05}s">
2592
+ <button class="copy-btn" data-copy="${esc2(copyData)}">copiar</button>
1715
2593
  <div class="card-title">
1716
- <span>${esc(s.name)}${serverVersion}</span>
2594
+ <span>${esc2(s.name)}${serverVersion}</span>
1717
2595
  <span class="status ${statusClass}">${statusText}</span>
1718
2596
  <span class="scope-badge scope-badge--${s.source}">${s.source}</span>
1719
2597
  </div>
@@ -1729,10 +2607,13 @@ function renderMcpServers(servers) {
1729
2607
  0
1730
2608
  );
1731
2609
  return `
1732
- <div class="section">
2610
+ <div class="section" id="section-mcp">
1733
2611
  <div class="section-header">
1734
- <h2>MCP Servers</h2>
1735
- <div>
2612
+ <div class="section-header-left">
2613
+ <span class="section-icon">\u2699\uFE0F</span>
2614
+ <h2>MCP Servers</h2>
2615
+ </div>
2616
+ <div class="section-header-right">
1736
2617
  <span class="count">${servers.length} servers &middot; ${totalTools} tools</span>
1737
2618
  <span class="arrow">&#9660;</span>
1738
2619
  </div>
@@ -1742,6 +2623,7 @@ function renderMcpServers(servers) {
1742
2623
  }
1743
2624
  function renderContextFiles(files) {
1744
2625
  if (files.length === 0) return "";
2626
+ const maxSize = Math.max(...files.map((f) => f.size), 1);
1745
2627
  const groups = /* @__PURE__ */ new Map();
1746
2628
  for (const f of files) {
1747
2629
  const existing = groups.get(f.tool) ?? [];
@@ -1750,16 +2632,21 @@ function renderContextFiles(files) {
1750
2632
  }
1751
2633
  let groupsHtml = "";
1752
2634
  for (const [tool, toolFiles] of groups) {
1753
- const items = toolFiles.map((f) => {
1754
- const previewHtml = f.preview ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc(f.preview)}</div>` : "";
1755
- const errorHtml = f.error ? `<span class="status status--error">${f.error}</span>` : "";
2635
+ const items = toolFiles.map((f, i) => {
2636
+ const previewHtml = f.preview ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc2(f.preview)}</div>` : "";
2637
+ const errorHtml = f.error ? `<span class="status status--error">${esc2(f.error)}</span>` : "";
1756
2638
  const sizeStr = f.size > 0 ? formatBytes(f.size) : "";
1757
- const typeIcon = f.type === "directory" ? "&#128193;" : "&#128196;";
2639
+ const typeIcon = f.type === "directory" ? "\u{1F4C1}" : "\u{1F4C4}";
2640
+ const sizePercent = f.size > 0 ? Math.max(3, f.size / maxSize * 100) : 0;
2641
+ const sizeBar = f.size > 0 ? `<div class="size-bar"><div class="size-bar-fill" style="width:${sizePercent}%"></div></div>` : "";
1758
2642
  const childrenHtml = f.children && f.children.length > 0 ? `<div class="card-meta" style="margin-top:0.3rem">${f.children.length} archivos dentro</div>` : "";
1759
2643
  return `
1760
- <div class="card" data-searchable="${esc(f.path + " " + tool)}">
1761
- <div class="card-title">${typeIcon} ${esc(f.path)} ${errorHtml}</div>
2644
+ <div class="card" data-searchable="${esc2(f.path + " " + tool)}"
2645
+ style="animation-delay: ${i * 0.04}s">
2646
+ <button class="copy-btn" data-copy="${esc2(f.path)}">copiar</button>
2647
+ <div class="card-title">${typeIcon} ${esc2(f.path)} ${errorHtml}</div>
1762
2648
  <div class="card-meta">${sizeStr} &middot; <span class="scope-badge scope-badge--${f.scope}">${f.scope}</span></div>
2649
+ ${sizeBar}
1763
2650
  ${childrenHtml}
1764
2651
  ${previewHtml}
1765
2652
  </div>`;
@@ -1774,10 +2661,13 @@ function renderContextFiles(files) {
1774
2661
  </div>`;
1775
2662
  }
1776
2663
  return `
1777
- <div class="section">
2664
+ <div class="section" id="section-context">
1778
2665
  <div class="section-header">
1779
- <h2>Archivos de Contexto</h2>
1780
- <div>
2666
+ <div class="section-header-left">
2667
+ <span class="section-icon">\u{1F4C4}</span>
2668
+ <h2>Archivos de Contexto</h2>
2669
+ </div>
2670
+ <div class="section-header-right">
1781
2671
  <span class="count">${files.length} archivos &middot; ${groups.size} herramientas</span>
1782
2672
  <span class="arrow">&#9660;</span>
1783
2673
  </div>
@@ -1787,23 +2677,27 @@ function renderContextFiles(files) {
1787
2677
  }
1788
2678
  function renderSkills(skills) {
1789
2679
  if (skills.length === 0) return "";
1790
- const items = skills.map((s) => {
1791
- const triggersHtml = s.triggers ? `<div class="card-meta">${s.triggers.map((t) => esc(t)).join(", ")}</div>` : "";
2680
+ const items = skills.map((s, i) => {
2681
+ const triggersHtml = s.triggers ? `<div class="card-meta">${s.triggers.map((t) => esc2(t)).join(", ")}</div>` : "";
1792
2682
  return `
1793
- <div class="card" data-searchable="${esc(s.name + " " + (s.description ?? ""))}">
2683
+ <div class="card" data-searchable="${esc2(s.name + " " + (s.description ?? ""))}"
2684
+ style="animation-delay: ${i * 0.04}s">
1794
2685
  <div class="card-title">
1795
- ${esc(s.name)}
2686
+ \u26A1 ${esc2(s.name)}
1796
2687
  <span class="scope-badge scope-badge--${s.scope}">${s.scope}</span>
1797
2688
  </div>
1798
- ${s.description ? `<div class="card-meta">${esc(s.description)}</div>` : ""}
2689
+ ${s.description ? `<div class="card-meta">${esc2(s.description)}</div>` : ""}
1799
2690
  ${triggersHtml}
1800
2691
  </div>`;
1801
2692
  }).join("");
1802
2693
  return `
1803
- <div class="section">
2694
+ <div class="section" id="section-skills">
1804
2695
  <div class="section-header">
1805
- <h2>Skills</h2>
1806
- <div>
2696
+ <div class="section-header-left">
2697
+ <span class="section-icon">\u26A1</span>
2698
+ <h2>Skills</h2>
2699
+ </div>
2700
+ <div class="section-header-right">
1807
2701
  <span class="count">${skills.length}</span>
1808
2702
  <span class="arrow">&#9660;</span>
1809
2703
  </div>
@@ -1813,25 +2707,29 @@ function renderSkills(skills) {
1813
2707
  }
1814
2708
  function renderAgents(agents) {
1815
2709
  if (agents.length === 0) return "";
1816
- const items = agents.map((a) => {
1817
- const modelHtml = a.model ? `<span class="badge badge--green">${esc(a.model)}</span>` : "";
1818
- const memoryHtml = a.hasMemory ? '<span class="badge badge--accent">memoria</span>' : "";
2710
+ const items = agents.map((a, i) => {
2711
+ const modelHtml = a.model ? `<span class="badge badge--green" style="font-size:0.65rem;padding:0.1rem 0.5rem">${esc2(a.model)}</span>` : "";
2712
+ const memoryHtml = a.hasMemory ? '<span class="badge badge--pink" style="font-size:0.65rem;padding:0.1rem 0.5rem">\u{1F9E0} memoria</span>' : "";
1819
2713
  return `
1820
- <div class="card" data-searchable="${esc(a.name + " " + (a.description ?? ""))}">
2714
+ <div class="card" data-searchable="${esc2(a.name + " " + (a.description ?? ""))}"
2715
+ style="animation-delay: ${i * 0.04}s">
1821
2716
  <div class="card-title">
1822
- ${esc(a.name)}
2717
+ \u{1F916} ${esc2(a.name)}
1823
2718
  ${modelHtml}
1824
2719
  ${memoryHtml}
1825
2720
  <span class="scope-badge scope-badge--${a.scope}">${a.scope}</span>
1826
2721
  </div>
1827
- ${a.description ? `<div class="card-meta">${esc(a.description)}</div>` : ""}
2722
+ ${a.description ? `<div class="card-meta">${esc2(a.description)}</div>` : ""}
1828
2723
  </div>`;
1829
2724
  }).join("");
1830
2725
  return `
1831
- <div class="section">
2726
+ <div class="section" id="section-agents">
1832
2727
  <div class="section-header">
1833
- <h2>Agents</h2>
1834
- <div>
2728
+ <div class="section-header-left">
2729
+ <span class="section-icon">\u{1F916}</span>
2730
+ <h2>Agents</h2>
2731
+ </div>
2732
+ <div class="section-header-right">
1835
2733
  <span class="count">${agents.length}</span>
1836
2734
  <span class="arrow">&#9660;</span>
1837
2735
  </div>
@@ -1841,23 +2739,33 @@ function renderAgents(agents) {
1841
2739
  }
1842
2740
  function renderMemories(memories) {
1843
2741
  if (memories.length === 0) return "";
1844
- const items = memories.map((m) => {
1845
- const detailsHtml = m.details ? `<div class="card-meta">${Object.entries(m.details).map(([k, v]) => `${esc(k)}: ${esc(String(v))}`).join(" &middot; ")}</div>` : "";
2742
+ const items = memories.map((m, i) => {
2743
+ const detailEntries = m.details ? Object.entries(m.details).filter(
2744
+ ([k]) => k !== "preview"
2745
+ ) : [];
2746
+ const detailsHtml = detailEntries.length > 0 ? `<div class="card-meta">${detailEntries.map(([k, v]) => `${esc2(k)}: ${esc2(String(v))}`).join(" &middot; ")}</div>` : "";
2747
+ const previewVal = m.details && typeof m.details.preview === "string" ? m.details.preview : null;
2748
+ const previewHtml = previewVal ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc2(previewVal)}</div>` : "";
1846
2749
  return `
1847
- <div class="card" data-searchable="${esc(m.type)}">
2750
+ <div class="card" data-searchable="${esc2(m.type + " " + (m.path ?? ""))}"
2751
+ style="animation-delay: ${i * 0.04}s">
1848
2752
  <div class="card-title">
1849
- ${esc(m.type)}
2753
+ \u{1F9E0} ${esc2(m.type)}
1850
2754
  <span class="status status--${m.status}">${m.status}</span>
1851
2755
  </div>
1852
- <div class="card-meta">${m.source}${m.path ? ` &middot; ${esc(m.path)}` : ""}</div>
2756
+ <div class="card-meta">${m.source}${m.path ? ` &middot; ${esc2(m.path)}` : ""}</div>
1853
2757
  ${detailsHtml}
2758
+ ${previewHtml}
1854
2759
  </div>`;
1855
2760
  }).join("");
1856
2761
  return `
1857
- <div class="section">
2762
+ <div class="section" id="section-memories">
1858
2763
  <div class="section-header">
1859
- <h2>Memorias</h2>
1860
- <div>
2764
+ <div class="section-header-left">
2765
+ <span class="section-icon">\u{1F9E0}</span>
2766
+ <h2>Memorias</h2>
2767
+ </div>
2768
+ <div class="section-header-right">
1861
2769
  <span class="count">${memories.length}</span>
1862
2770
  <span class="arrow">&#9660;</span>
1863
2771
  </div>
@@ -1865,14 +2773,40 @@ function renderMemories(memories) {
1865
2773
  <div class="section-content">${items}</div>
1866
2774
  </div>`;
1867
2775
  }
2776
+ function renderWarnings(warnings) {
2777
+ if (warnings.length === 0) return "";
2778
+ const items = warnings.map(
2779
+ (w) => `
2780
+ <div class="warning-card">
2781
+ <div class="warning-scanner">${esc2(w.scanner)}${w.path ? ` &mdash; ${esc2(w.path)}` : ""}</div>
2782
+ <div>${esc2(w.message)}</div>
2783
+ </div>`
2784
+ ).join("");
2785
+ return `
2786
+ <div class="section" id="section-warnings">
2787
+ <div class="section-header">
2788
+ <div class="section-header-left">
2789
+ <span class="section-icon">\u26A0\uFE0F</span>
2790
+ <h2>Advertencias</h2>
2791
+ </div>
2792
+ <div class="section-header-right">
2793
+ <span class="count">${warnings.length}</span>
2794
+ <span class="arrow">&#9660;</span>
2795
+ </div>
2796
+ </div>
2797
+ <div class="section-content">${items}</div>
2798
+ </div>`;
2799
+ }
1868
2800
  function renderEmptyState() {
1869
2801
  return `
1870
2802
  <div class="empty-state">
2803
+ <span class="empty-state-icon">\u{1F50D}</span>
1871
2804
  <h3>No se encontr\xF3 configuraci\xF3n AI en este proyecto</h3>
1872
2805
  <p>Este proyecto no tiene archivos de configuraci\xF3n de herramientas AI.</p>
1873
- <p style="margin-top:1rem;font-size:0.85rem">
2806
+ <p style="margin-top:1rem;font-size:0.85rem;color:var(--text-dim)">
1874
2807
  Herramientas soportadas: Claude, Cursor, Windsurf, Copilot, Gemini,
1875
- Codex, Aider, Cline, Continue, Amazon Q, Augment, Replit, Firebase Studio
2808
+ Codex, OpenCode, Aider, Cline, Roo, Continue, Amazon Q, Augment,
2809
+ Replit, Firebase Studio, Tabnine, Sourcegraph
1876
2810
  </p>
1877
2811
  </div>`;
1878
2812
  }
@@ -1894,14 +2828,46 @@ function computeSummary(result) {
1894
2828
  function generateHtml(result) {
1895
2829
  const summary = computeSummary(result);
1896
2830
  const isEmpty = summary.totalMcpServers === 0 && summary.totalFiles === 0 && summary.totalSkills === 0 && summary.totalAgents === 0 && summary.totalMemories === 0;
2831
+ const navBar = renderNavBar(summary);
1897
2832
  const header = renderHeader(result.project, summary, result.scanDuration);
2833
+ const statsGrid = renderStatsGrid(summary);
2834
+ const ecosystemMap = renderEcosystemMap(result, summary);
1898
2835
  const content = isEmpty ? renderEmptyState() : [
1899
2836
  renderMcpServers(result.mcpServers),
1900
2837
  renderContextFiles(result.contextFiles),
1901
2838
  renderSkills(result.skills),
1902
2839
  renderAgents(result.agents),
1903
- renderMemories(result.memories)
2840
+ renderMemories(result.memories),
2841
+ renderWarnings(result.warnings)
1904
2842
  ].join("");
2843
+ const exportData = {
2844
+ project: result.project.name,
2845
+ scannedAt: result.project.scannedAt,
2846
+ scanDuration: result.scanDuration,
2847
+ summary,
2848
+ mcpServers: result.mcpServers.map((s) => ({
2849
+ name: s.name,
2850
+ source: s.source,
2851
+ transport: s.config.transport,
2852
+ tools: s.introspection?.tools.length ?? 0
2853
+ })),
2854
+ contextFiles: result.contextFiles.map((f) => ({
2855
+ path: f.path,
2856
+ tool: f.tool,
2857
+ scope: f.scope
2858
+ })),
2859
+ skills: result.skills.map((s) => ({ name: s.name, scope: s.scope })),
2860
+ agents: result.agents.map((a) => ({
2861
+ name: a.name,
2862
+ scope: a.scope,
2863
+ model: a.model
2864
+ })),
2865
+ memories: result.memories.map((m) => ({
2866
+ type: m.type,
2867
+ source: m.source,
2868
+ status: m.status
2869
+ }))
2870
+ };
1905
2871
  return `<!DOCTYPE html>
1906
2872
  <html lang="es">
1907
2873
  <head>
@@ -1911,16 +2877,25 @@ function generateHtml(result) {
1911
2877
  <style>${CSS_STYLES}</style>
1912
2878
  </head>
1913
2879
  <body>
2880
+ ${navBar}
1914
2881
  <div class="container">
1915
2882
  ${header}
2883
+ ${statsGrid}
2884
+ ${ecosystemMap}
1916
2885
  <div class="search-bar">
1917
- <input type="text" id="search-input" placeholder="Buscar tools, archivos, skills, agents..." />
2886
+ <div class="search-bar-inner">
2887
+ <span class="search-icon">\u{1F50D}</span>
2888
+ <input type="text" id="search-input" placeholder="Buscar tools, archivos, skills, agents..." />
2889
+ <span class="search-kbd">/</span>
2890
+ </div>
2891
+ <span class="search-results-count"></span>
1918
2892
  </div>
1919
2893
  ${content}
1920
2894
  <footer class="footer">
1921
2895
  Generado por ai-context-inspector &mdash; ${new Date(result.project.scannedAt).toLocaleString()}
1922
2896
  </footer>
1923
2897
  </div>
2898
+ <script type="application/json" id="scan-data">${JSON.stringify(exportData)}</script>
1924
2899
  <script>${JS_SCRIPTS}</script>
1925
2900
  </body>
1926
2901
  </html>`;