@cocaxcode/ai-context-inspector 0.2.0 → 0.3.1

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,176 @@ 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 Scroll helper with offset for sticky nav + search \u2500\u2500
2103
+ const SCROLL_OFFSET = 110
2104
+
2105
+ function scrollToSection(targetId) {
2106
+ const target = document.getElementById(targetId)
2107
+ if (!target) return
2108
+ if (target.classList.contains('collapsed')) {
2109
+ target.classList.remove('collapsed')
2110
+ const content = target.querySelector('.section-content')
2111
+ if (content) {
2112
+ content.style.maxHeight = content.scrollHeight + 'px'
2113
+ setTimeout(() => { content.style.maxHeight = '' }, 400)
2114
+ }
2115
+ }
2116
+ const y = target.getBoundingClientRect().top + window.scrollY - SCROLL_OFFSET
2117
+ window.scrollTo({ top: y, behavior: 'smooth' })
2118
+ target.style.boxShadow = '0 0 0 2px var(--accent)'
2119
+ setTimeout(() => { target.style.boxShadow = '' }, 1500)
2120
+ }
2121
+
2122
+ // \u2500\u2500 Animated Counters \u2500\u2500
2123
+ document.querySelectorAll('.stat-number').forEach(el => {
2124
+ const target = parseInt(el.getAttribute('data-target') || '0')
2125
+ if (target === 0) { el.textContent = '0'; return }
2126
+ let current = 0
2127
+ const step = Math.max(1, Math.ceil(target / 25))
2128
+ const interval = setInterval(() => {
2129
+ current = Math.min(current + step, target)
2130
+ el.textContent = current
2131
+ if (current >= target) clearInterval(interval)
2132
+ }, 35)
2133
+ })
2134
+
2135
+ // \u2500\u2500 Section Collapse/Expand \u2500\u2500
1632
2136
  document.querySelectorAll('.section-header').forEach(header => {
1633
2137
  header.addEventListener('click', () => {
1634
- header.closest('.section').classList.toggle('collapsed')
2138
+ const section = header.closest('.section')
2139
+ if (!section) return
2140
+ const content = section.querySelector('.section-content')
2141
+ if (!content) return
2142
+
2143
+ if (section.classList.contains('collapsed')) {
2144
+ // Expand
2145
+ section.classList.remove('collapsed')
2146
+ content.style.maxHeight = content.scrollHeight + 'px'
2147
+ setTimeout(() => { content.style.maxHeight = '' }, 400)
2148
+ } else {
2149
+ // Collapse
2150
+ content.style.maxHeight = content.scrollHeight + 'px'
2151
+ requestAnimationFrame(() => {
2152
+ content.style.maxHeight = '0px'
2153
+ section.classList.add('collapsed')
2154
+ })
2155
+ }
1635
2156
  })
1636
2157
  })
1637
2158
 
1638
- // Preview toggle
2159
+ // \u2500\u2500 Preview Toggle \u2500\u2500
1639
2160
  document.querySelectorAll('.preview-toggle').forEach(btn => {
1640
2161
  btn.addEventListener('click', (e) => {
1641
2162
  e.stopPropagation()
@@ -1647,55 +2168,522 @@ document.addEventListener('DOMContentLoaded', () => {
1647
2168
  })
1648
2169
  })
1649
2170
 
1650
- // Global search
2171
+ // \u2500\u2500 Global Search \u2500\u2500
1651
2172
  const searchInput = document.getElementById('search-input')
2173
+ const resultsCount = document.querySelector('.search-results-count')
2174
+ const searchKbd = document.querySelector('.search-kbd')
2175
+
1652
2176
  if (searchInput) {
1653
2177
  searchInput.addEventListener('input', (e) => {
1654
2178
  const query = e.target.value.toLowerCase().trim()
2179
+ let visible = 0
2180
+ let total = 0
2181
+
1655
2182
  document.querySelectorAll('[data-searchable]').forEach(el => {
1656
2183
  const text = el.getAttribute('data-searchable').toLowerCase()
1657
- el.style.display = (!query || text.includes(query)) ? '' : 'none'
2184
+ const match = !query || text.includes(query)
2185
+ el.style.display = match ? '' : 'none'
2186
+ total++
2187
+ if (match) visible++
1658
2188
  })
2189
+
2190
+ if (resultsCount) {
2191
+ resultsCount.textContent = query ? visible + ' / ' + total : ''
2192
+ }
2193
+ if (searchKbd) {
2194
+ searchKbd.style.display = query ? 'none' : ''
2195
+ }
2196
+ })
2197
+
2198
+ searchInput.addEventListener('focus', () => {
2199
+ if (searchKbd) searchKbd.style.display = 'none'
2200
+ })
2201
+
2202
+ searchInput.addEventListener('blur', () => {
2203
+ if (searchKbd && !searchInput.value) searchKbd.style.display = ''
1659
2204
  })
1660
2205
  }
2206
+
2207
+ // \u2500\u2500 Keyboard Shortcuts \u2500\u2500
2208
+ document.addEventListener('keydown', (e) => {
2209
+ // Ignore when typing in input
2210
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
2211
+ if (e.key === 'Escape') {
2212
+ e.target.value = ''
2213
+ e.target.dispatchEvent(new Event('input'))
2214
+ e.target.blur()
2215
+ }
2216
+ return
2217
+ }
2218
+
2219
+ if (e.key === '/') {
2220
+ e.preventDefault()
2221
+ if (searchInput) searchInput.focus()
2222
+ }
2223
+ })
2224
+
2225
+ // \u2500\u2500 Nav Links (scroll to section) \u2500\u2500
2226
+ document.querySelectorAll('.nav-link[data-target]').forEach(link => {
2227
+ link.addEventListener('click', (e) => {
2228
+ e.preventDefault()
2229
+ scrollToSection(link.getAttribute('data-target'))
2230
+ })
2231
+ })
2232
+
2233
+ // \u2500\u2500 Ecosystem Map (click to scroll) \u2500\u2500
2234
+ document.querySelectorAll('.eco-cat-node[data-section]').forEach(node => {
2235
+ node.addEventListener('click', () => {
2236
+ scrollToSection(node.getAttribute('data-section'))
2237
+ })
2238
+ node.addEventListener('keydown', (e) => {
2239
+ if (e.key === 'Enter' || e.key === ' ') {
2240
+ e.preventDefault()
2241
+ scrollToSection(node.getAttribute('data-section'))
2242
+ }
2243
+ })
2244
+ })
2245
+
2246
+ // \u2500\u2500 Ecosystem Map Tooltips \u2500\u2500
2247
+ const ecoTooltip = document.getElementById('eco-tooltip')
2248
+ const ecoTooltipText = document.getElementById('eco-tooltip-text')
2249
+
2250
+ document.querySelectorAll('.eco-item[data-tooltip]').forEach(item => {
2251
+ item.addEventListener('mouseenter', () => {
2252
+ if (!ecoTooltip || !ecoTooltipText) return
2253
+ const text = item.getAttribute('data-tooltip')
2254
+ ecoTooltipText.textContent = text
2255
+ const textLen = Math.min(text.length * 6.5 + 20, 220)
2256
+ const rect = ecoTooltip.querySelector('rect')
2257
+ if (rect) {
2258
+ rect.setAttribute('width', textLen)
2259
+ ecoTooltipText.setAttribute('x', textLen / 2)
2260
+ }
2261
+ const circle = item.querySelector('circle')
2262
+ if (circle) {
2263
+ const cx = parseFloat(circle.getAttribute('cx'))
2264
+ const cy = parseFloat(circle.getAttribute('cy'))
2265
+ ecoTooltip.setAttribute('transform', 'translate(' + (cx - textLen / 2) + ',' + (cy - 38) + ')')
2266
+ }
2267
+ ecoTooltip.style.display = ''
2268
+ })
2269
+ item.addEventListener('mouseleave', () => {
2270
+ if (ecoTooltip) ecoTooltip.style.display = 'none'
2271
+ })
2272
+ })
2273
+
2274
+ // \u2500\u2500 Theme Toggle \u2500\u2500
2275
+ const themeToggle = document.getElementById('theme-toggle')
2276
+ if (themeToggle) {
2277
+ themeToggle.addEventListener('click', () => {
2278
+ const html = document.documentElement
2279
+ const current = html.getAttribute('data-theme')
2280
+ const next = current === 'light' ? 'dark' : 'light'
2281
+ html.setAttribute('data-theme', next)
2282
+ themeToggle.innerHTML = next === 'light' ? '☀' : '☾'
2283
+ })
2284
+ }
2285
+
2286
+ // \u2500\u2500 Export JSON \u2500\u2500
2287
+ const exportBtn = document.getElementById('export-btn')
2288
+ if (exportBtn) {
2289
+ exportBtn.addEventListener('click', () => {
2290
+ const dataEl = document.getElementById('scan-data')
2291
+ if (!dataEl) return
2292
+ const text = dataEl.textContent || ''
2293
+ const fallback = () => {
2294
+ const ta = document.createElement('textarea')
2295
+ ta.value = text
2296
+ ta.style.position = 'fixed'
2297
+ ta.style.opacity = '0'
2298
+ document.body.appendChild(ta)
2299
+ ta.select()
2300
+ document.execCommand('copy')
2301
+ document.body.removeChild(ta)
2302
+ }
2303
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2304
+ navigator.clipboard.writeText(text).catch(fallback)
2305
+ } else {
2306
+ fallback()
2307
+ }
2308
+ const orig = exportBtn.innerHTML
2309
+ exportBtn.textContent = '\\u2713'
2310
+ setTimeout(() => { exportBtn.innerHTML = orig }, 1500)
2311
+ })
2312
+ }
2313
+
2314
+ // \u2500\u2500 Copy Buttons \u2500\u2500
2315
+ document.querySelectorAll('.copy-btn').forEach(btn => {
2316
+ btn.addEventListener('click', (e) => {
2317
+ e.stopPropagation()
2318
+ const text = btn.getAttribute('data-copy') || ''
2319
+ const fallback = () => {
2320
+ const ta = document.createElement('textarea')
2321
+ ta.value = text
2322
+ ta.style.position = 'fixed'
2323
+ ta.style.opacity = '0'
2324
+ document.body.appendChild(ta)
2325
+ ta.select()
2326
+ document.execCommand('copy')
2327
+ document.body.removeChild(ta)
2328
+ }
2329
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2330
+ navigator.clipboard.writeText(text).catch(fallback)
2331
+ } else {
2332
+ fallback()
2333
+ }
2334
+ btn.classList.add('copy-btn--copied')
2335
+ btn.textContent = '\\u2713'
2336
+ setTimeout(() => {
2337
+ btn.classList.remove('copy-btn--copied')
2338
+ btn.textContent = 'copiar'
2339
+ }, 1500)
2340
+ })
2341
+ })
2342
+
1661
2343
  })
1662
2344
  `;
1663
2345
 
1664
- // src/report/sections.ts
2346
+ // src/report/ecosystem-map.ts
1665
2347
  function esc(str) {
1666
2348
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1667
2349
  }
2350
+ function buildCategories(result, summary) {
2351
+ return [
2352
+ {
2353
+ id: "section-mcp",
2354
+ label: "MCP",
2355
+ icon: "\u2699",
2356
+ count: summary.totalMcpServers,
2357
+ color: "#00d4ff",
2358
+ items: result.mcpServers.slice(0, 6).map((s) => ({
2359
+ name: s.name,
2360
+ detail: `${s.introspection?.tools.length ?? 0} tools`
2361
+ }))
2362
+ },
2363
+ {
2364
+ id: "section-context",
2365
+ label: "Contexto",
2366
+ icon: "\u{1F4C4}",
2367
+ count: summary.totalFiles,
2368
+ color: "#b388ff",
2369
+ items: result.contextFiles.slice(0, 6).map((f) => ({
2370
+ name: f.path,
2371
+ detail: f.tool
2372
+ }))
2373
+ },
2374
+ {
2375
+ id: "section-skills",
2376
+ label: "Skills",
2377
+ icon: "\u26A1",
2378
+ count: summary.totalSkills,
2379
+ color: "#ffab40",
2380
+ items: result.skills.slice(0, 6).map((s) => ({
2381
+ name: s.name
2382
+ }))
2383
+ },
2384
+ {
2385
+ id: "section-agents",
2386
+ label: "Agents",
2387
+ icon: "\u{1F916}",
2388
+ count: summary.totalAgents,
2389
+ color: "#00e676",
2390
+ items: result.agents.slice(0, 6).map((a) => ({
2391
+ name: a.name,
2392
+ detail: a.model
2393
+ }))
2394
+ },
2395
+ {
2396
+ id: "section-memories",
2397
+ label: "Memorias",
2398
+ icon: "\u{1F9E0}",
2399
+ count: summary.totalMemories,
2400
+ color: "#ff80ab",
2401
+ items: result.memories.slice(0, 6).map((m) => ({
2402
+ name: m.type,
2403
+ detail: m.status
2404
+ }))
2405
+ }
2406
+ ];
2407
+ }
2408
+ var COCAXCODE_ICON = `<g transform="translate(-16,-16)">
2409
+ <defs>
2410
+ <linearGradient id="cxc-g1" x1="0%" y1="0%" x2="100%" y2="100%">
2411
+ <stop offset="0%" stop-color="#22d3ee"/>
2412
+ <stop offset="100%" stop-color="#06b6d4"/>
2413
+ </linearGradient>
2414
+ <linearGradient id="cxc-g2" x1="0%" y1="0%" x2="100%" y2="100%">
2415
+ <stop offset="0%" stop-color="#0891b2"/>
2416
+ <stop offset="100%" stop-color="#0e7490"/>
2417
+ </linearGradient>
2418
+ </defs>
2419
+ <rect width="32" height="32" fill="#0a0a0c" rx="4"/>
2420
+ <rect x="9" y="7.5" width="16" height="18.5" rx="4" fill="url(#cxc-g2)" opacity="0.5"/>
2421
+ <rect x="8" y="6.5" width="16" height="18.5" rx="4" fill="url(#cxc-g1)"/>
2422
+ <path d="M11.5,11 L18,16 L11.5,21" fill="none" stroke="#0a0a0c" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
2423
+ <line x1="19" y1="20" x2="21.5" y2="20" stroke="#0a0a0c" stroke-width="1.8" stroke-linecap="round">
2424
+ <animate attributeName="x1" values="19;21.5;19" dur="1.4s" repeatCount="indefinite"/>
2425
+ <animate attributeName="x2" values="21.5;24;21.5" dur="1.4s" repeatCount="indefinite"/>
2426
+ </line>
2427
+ </g>`;
2428
+ function renderEcosystemMap(result, summary) {
2429
+ const categories = buildCategories(result, summary);
2430
+ const totalItems = summary.totalMcpServers + summary.totalFiles + summary.totalSkills + summary.totalAgents + summary.totalMemories;
2431
+ if (totalItems === 0) return "";
2432
+ const W = 900;
2433
+ const H = 480;
2434
+ const cx = W / 2;
2435
+ const cy = H / 2;
2436
+ const catRadius = 165;
2437
+ const itemRadius = 55;
2438
+ const startAngle = -Math.PI / 2;
2439
+ let defs = "";
2440
+ let connections = "";
2441
+ let itemNodes = "";
2442
+ let catNodes = "";
2443
+ defs += `<defs>
2444
+ <filter id="eco-glow" x="-40%" y="-40%" width="180%" height="180%">
2445
+ <feGaussianBlur stdDeviation="6" result="blur"/>
2446
+ <feComposite in="SourceGraphic" in2="blur" operator="over"/>
2447
+ </filter>
2448
+ <filter id="eco-glow-center" x="-50%" y="-50%" width="200%" height="200%">
2449
+ <feGaussianBlur stdDeviation="10" result="blur"/>
2450
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
2451
+ </filter>
2452
+ <radialGradient id="eco-bg-grad" cx="50%" cy="50%" r="55%">
2453
+ <stop offset="0%" stop-color="var(--accent)" stop-opacity="0.03"/>
2454
+ <stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
2455
+ </radialGradient>
2456
+ </defs>`;
2457
+ let bg = `<rect width="${W}" height="${H}" fill="none"/>
2458
+ <circle cx="${cx}" cy="${cy}" r="280" fill="url(#eco-bg-grad)"/>`;
2459
+ bg += `<circle cx="${cx}" cy="${cy}" r="${catRadius}" fill="none"
2460
+ stroke="var(--border)" stroke-width="0.5" stroke-dasharray="4 8" opacity="0.4"/>`;
2461
+ categories.forEach((cat, i) => {
2462
+ const angle = startAngle + i / categories.length * Math.PI * 2;
2463
+ const x = cx + Math.cos(angle) * catRadius;
2464
+ const y = cy + Math.sin(angle) * catRadius;
2465
+ const dimmed = cat.count === 0;
2466
+ const opacity = dimmed ? 0.2 : 1;
2467
+ const midX = (cx + x) / 2;
2468
+ const midY = (cy + y) / 2;
2469
+ const perpX = -(y - cy) * 0.08;
2470
+ const perpY = (x - cx) * 0.08;
2471
+ connections += `<path d="M${cx},${cy} Q${midX + perpX},${midY + perpY} ${x},${y}"
2472
+ fill="none" stroke="${cat.color}" stroke-width="${dimmed ? 0.8 : 1.5}"
2473
+ opacity="${dimmed ? 0.15 : 0.35}"
2474
+ stroke-dasharray="${dimmed ? "3 6" : "6 4"}"
2475
+ ${!dimmed ? `style="animation: eco-dash 15s linear infinite; animation-delay: ${i * 0.5}s"` : ""}/>`;
2476
+ if (!dimmed && cat.items.length > 0) {
2477
+ const maxItems = Math.min(cat.items.length, 6);
2478
+ const arcSpread = Math.min(Math.PI * 0.6, maxItems * 0.25);
2479
+ cat.items.slice(0, maxItems).forEach((item, j) => {
2480
+ const itemAngle = angle - arcSpread / 2 + (maxItems > 1 ? j / (maxItems - 1) * arcSpread : 0);
2481
+ const ix = x + Math.cos(itemAngle) * itemRadius;
2482
+ const iy = y + Math.sin(itemAngle) * itemRadius;
2483
+ connections += `<line x1="${x}" y1="${y}" x2="${ix}" y2="${iy}"
2484
+ stroke="${cat.color}" stroke-width="0.7" opacity="0.2"/>`;
2485
+ const shortName = item.name.length > 15 ? item.name.slice(0, 13) + ".." : item.name;
2486
+ itemNodes += `<g class="eco-item" data-tooltip="${esc(item.name)}${item.detail ? " (" + esc(item.detail) + ")" : ""}">
2487
+ <circle cx="${ix}" cy="${iy}" r="4.5" fill="${cat.color}" opacity="0.5"
2488
+ class="eco-item-dot"/>
2489
+ <circle cx="${ix}" cy="${iy}" r="4.5" fill="transparent" stroke="${cat.color}"
2490
+ stroke-width="1" opacity="0.3"/>
2491
+ <title>${esc(item.name)}${item.detail ? " \u2014 " + esc(item.detail) : ""}</title>
2492
+ </g>`;
2493
+ });
2494
+ if (cat.count > maxItems) {
2495
+ const extra = cat.count - maxItems;
2496
+ const moreAngle = angle + arcSpread / 2 + 0.35;
2497
+ const mx = x + Math.cos(moreAngle) * (itemRadius * 0.85);
2498
+ const my = y + Math.sin(moreAngle) * (itemRadius * 0.85);
2499
+ itemNodes += `<text x="${mx}" y="${my}"
2500
+ font-family="var(--font-mono)" font-size="9" fill="${cat.color}"
2501
+ opacity="0.5" text-anchor="middle" dominant-baseline="middle">+${extra}</text>`;
2502
+ }
2503
+ }
2504
+ const nodeR = dimmed ? 24 : 32;
2505
+ const labelY = y - nodeR - 10;
2506
+ catNodes += `<g class="eco-cat-node" data-section="${cat.id}"
2507
+ style="cursor:pointer;opacity:${opacity}" role="button" tabindex="0">
2508
+ <!-- Hit area -->
2509
+ <circle cx="${x}" cy="${y}" r="${nodeR + 8}" fill="transparent"/>
2510
+ <!-- Outer glow ring -->
2511
+ ${!dimmed ? `<circle cx="${x}" cy="${y}" r="${nodeR + 3}" fill="none"
2512
+ stroke="${cat.color}" stroke-width="1" opacity="0.15"
2513
+ style="animation: eco-pulse 3s ease-in-out infinite; animation-delay: ${i * 0.4}s"/>` : ""}
2514
+ <!-- Main circle -->
2515
+ <circle cx="${x}" cy="${y}" r="${nodeR}" fill="${cat.color}"
2516
+ opacity="${dimmed ? 0.08 : 0.12}" stroke="${cat.color}"
2517
+ stroke-width="${dimmed ? 1 : 2}" class="eco-cat-circle"
2518
+ ${!dimmed ? 'filter="url(#eco-glow)"' : ""}/>
2519
+ <!-- Count inside -->
2520
+ <text x="${x}" y="${y + 1}" text-anchor="middle" dominant-baseline="middle"
2521
+ font-family="var(--font-mono)" font-size="${dimmed ? 14 : 18}"
2522
+ font-weight="700" fill="${cat.color}" opacity="${dimmed ? 0.3 : 0.9}">${cat.count}</text>
2523
+ <!-- Label + icon ABOVE circle -->
2524
+ <text x="${x}" y="${labelY}" text-anchor="middle" dominant-baseline="auto"
2525
+ font-family="var(--font-mono)" font-size="11" font-weight="600"
2526
+ fill="${cat.color}" opacity="${dimmed ? 0.3 : 0.85}">${cat.icon} ${cat.label}</text>
2527
+ </g>`;
2528
+ });
2529
+ const projectName = result.project.name.length > 20 ? result.project.name.slice(0, 18) + ".." : result.project.name;
2530
+ const centerNode = `<g class="eco-center">
2531
+ <!-- Outer pulse ring -->
2532
+ <circle cx="${cx}" cy="${cy}" r="48" fill="none" stroke="var(--accent)"
2533
+ stroke-width="1" opacity="0.2"
2534
+ style="animation: eco-pulse 4s ease-in-out infinite"/>
2535
+ <!-- Main circle -->
2536
+ <circle cx="${cx}" cy="${cy}" r="42" fill="var(--bg-alt)" stroke="var(--accent)"
2537
+ stroke-width="2.5" filter="url(#eco-glow-center)"/>
2538
+ <!-- cocaxcode icon in center -->
2539
+ <g transform="translate(${cx},${cy - 8}) scale(0.85)">
2540
+ ${COCAXCODE_ICON}
2541
+ </g>
2542
+ <!-- Project name below icon -->
2543
+ <text x="${cx}" y="${cy + 20}" text-anchor="middle" dominant-baseline="auto"
2544
+ font-family="var(--font-mono)" font-size="10" font-weight="600"
2545
+ fill="var(--text-bright)" opacity="0.9">${esc(projectName)}</text>
2546
+ <!-- Total count -->
2547
+ <text x="${cx}" y="${cy + 32}" text-anchor="middle" dominant-baseline="auto"
2548
+ font-family="var(--font-mono)" font-size="8.5"
2549
+ fill="var(--text-dim)">${totalItems} elementos</text>
2550
+ </g>`;
2551
+ const tooltip = `<g id="eco-tooltip" style="display:none;pointer-events:none">
2552
+ <rect x="0" y="0" width="160" height="28" rx="4"
2553
+ fill="var(--bg-card)" stroke="var(--border)" stroke-width="1" opacity="0.95"/>
2554
+ <text x="80" y="18" text-anchor="middle" font-family="var(--font-mono)"
2555
+ font-size="10" fill="var(--text)" id="eco-tooltip-text"></text>
2556
+ </g>`;
2557
+ const watermark = `<text x="${W - 12}" y="${H - 10}" text-anchor="end"
2558
+ font-family="var(--font-mono)" font-size="9" fill="var(--text-dim)" opacity="0.3"
2559
+ letter-spacing="0.05em">cocaxcode</text>`;
2560
+ const svgContent = `${defs}${bg}${connections}${itemNodes}${catNodes}${centerNode}${tooltip}${watermark}`;
2561
+ return `
2562
+ <div class="ecosystem-map">
2563
+ <svg class="ecosystem-svg" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg"
2564
+ role="img" aria-label="Mapa del ecosistema AI del proyecto">
2565
+ <style>
2566
+ @keyframes eco-dash { to { stroke-dashoffset: -80; } }
2567
+ @keyframes eco-pulse { 0%,100% { opacity: 0.15; transform-origin: center; } 50% { opacity: 0.4; } }
2568
+ .eco-cat-node:hover .eco-cat-circle { stroke-width: 3; opacity: 0.25; }
2569
+ .eco-cat-node:hover { filter: brightness(1.2); }
2570
+ .eco-cat-node:focus { outline: none; }
2571
+ .eco-cat-node:focus .eco-cat-circle { stroke-width: 3; stroke-dasharray: 4 2; }
2572
+ .eco-item-dot { transition: r 0.2s, opacity 0.2s; }
2573
+ .eco-item:hover .eco-item-dot { r: 7; opacity: 0.8; }
2574
+ </style>
2575
+ ${svgContent}
2576
+ </svg>
2577
+ </div>`;
2578
+ }
2579
+
2580
+ // src/report/sections.ts
2581
+ function esc2(str) {
2582
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2583
+ }
1668
2584
  function formatBytes(bytes) {
1669
2585
  if (bytes < 1024) return `${bytes} B`;
1670
2586
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1671
2587
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1672
2588
  }
2589
+ function renderNavBar(summary) {
2590
+ const links = [
2591
+ { id: "section-mcp", label: "MCP", count: summary.totalMcpServers },
2592
+ { id: "section-context", label: "Contexto", count: summary.totalFiles },
2593
+ { id: "section-skills", label: "Skills", count: summary.totalSkills },
2594
+ { id: "section-agents", label: "Agents", count: summary.totalAgents },
2595
+ { id: "section-memories", label: "Memorias", count: summary.totalMemories }
2596
+ ];
2597
+ const navLinks = links.filter((l) => l.count > 0).map(
2598
+ (l) => `<a class="nav-link" data-target="${l.id}">${l.label} (${l.count})</a>`
2599
+ ).join("");
2600
+ return `
2601
+ <nav class="nav-bar">
2602
+ <div class="nav-links">
2603
+ <span style="font-family:var(--font-mono);font-size:0.8rem;color:var(--accent);margin-right:0.5rem">&gt;_</span>
2604
+ ${navLinks}
2605
+ </div>
2606
+ <div class="nav-actions">
2607
+ <button class="nav-btn" id="theme-toggle" title="Cambiar tema">&#9790;</button>
2608
+ <button class="nav-btn" id="export-btn" title="Exportar JSON">&#128203;</button>
2609
+ </div>
2610
+ </nav>`;
2611
+ }
1673
2612
  function renderHeader(project, summary, scanDuration) {
1674
2613
  const date = new Date(project.scannedAt).toLocaleString();
1675
2614
  return `
1676
2615
  <header class="header">
1677
2616
  <h1>&gt; ai-context-inspector</h1>
1678
- <div class="subtitle">${esc(project.name)} &mdash; ${date} &mdash; ${scanDuration}ms</div>
2617
+ <div class="subtitle">${esc2(project.name)} &mdash; ${date} &mdash; ${scanDuration}ms</div>
1679
2618
  <div class="badges">
1680
2619
  <span class="badge badge--accent">${summary.totalMcpServers} MCPs</span>
1681
2620
  <span class="badge badge--green">${summary.totalTools} tools</span>
1682
2621
  <span class="badge badge--purple">${summary.totalFiles} archivos</span>
1683
2622
  <span class="badge badge--orange">${summary.totalSkills} skills</span>
1684
2623
  <span class="badge badge--blue">${summary.totalAgents} agents</span>
1685
- <span class="badge badge--accent">${summary.totalMemories} memorias</span>
2624
+ <span class="badge badge--pink">${summary.totalMemories} memorias</span>
1686
2625
  </div>
1687
2626
  </header>`;
1688
2627
  }
2628
+ function renderStatsGrid(summary) {
2629
+ const stats = [
2630
+ {
2631
+ icon: "\u2699\uFE0F",
2632
+ value: summary.totalMcpServers,
2633
+ label: "MCP Servers",
2634
+ color: "#00d4ff"
2635
+ },
2636
+ {
2637
+ icon: "\u{1F6E0}\uFE0F",
2638
+ value: summary.totalTools,
2639
+ label: "MCP Tools",
2640
+ color: "#00e676"
2641
+ },
2642
+ {
2643
+ icon: "\u{1F4C4}",
2644
+ value: summary.totalFiles,
2645
+ label: "Archivos AI",
2646
+ color: "#b388ff"
2647
+ },
2648
+ {
2649
+ icon: "\u26A1",
2650
+ value: summary.totalSkills,
2651
+ label: "Skills",
2652
+ color: "#ffab40"
2653
+ },
2654
+ {
2655
+ icon: "\u{1F916}",
2656
+ value: summary.totalAgents,
2657
+ label: "Agents",
2658
+ color: "#4285f4"
2659
+ },
2660
+ {
2661
+ icon: "\u{1F9E0}",
2662
+ value: summary.totalMemories,
2663
+ label: "Memorias",
2664
+ color: "#ff80ab"
2665
+ }
2666
+ ];
2667
+ const cards = stats.map(
2668
+ (s) => `
2669
+ <div class="stat-card" style="--stat-color: ${s.color}">
2670
+ <span class="stat-icon">${s.icon}</span>
2671
+ <span class="stat-number" data-target="${s.value}">0</span>
2672
+ <span class="stat-label">${s.label}</span>
2673
+ </div>`
2674
+ ).join("");
2675
+ return `<div class="stats-grid">${cards}</div>`;
2676
+ }
1689
2677
  function renderMcpServers(servers) {
1690
2678
  if (servers.length === 0) return "";
1691
- const cards = servers.map((s) => {
2679
+ const cards = servers.map((s, i) => {
1692
2680
  const intro = s.introspection;
1693
2681
  const statusClass = intro ? `status--${intro.status}` : "status--configured";
1694
2682
  const statusText = intro ? intro.status === "ok" ? "OK" : intro.status === "timeout" ? "Timeout" : "Error" : "No introspectado";
1695
2683
  let toolsHtml = "";
1696
2684
  if (intro && intro.tools.length > 0) {
1697
2685
  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>`
2686
+ (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
2687
  ).join("");
1700
2688
  toolsHtml = `<div class="card-meta">${intro.tools.length} tools</div><ul class="tool-list">${items}</ul>`;
1701
2689
  }
@@ -1707,13 +2695,17 @@ function renderMcpServers(servers) {
1707
2695
  if (intro && intro.prompts.length > 0) {
1708
2696
  promptsHtml = `<div class="card-meta" style="margin-top:0.5rem">${intro.prompts.length} prompts</div>`;
1709
2697
  }
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>` : "";
2698
+ const serverVersion = intro?.serverInfo ? ` v${esc2(intro.serverInfo.version)}` : "";
2699
+ const errorHtml = intro?.error ? `<div class="card-meta" style="color:var(--red)">${esc2(intro.error)}</div>` : "";
2700
+ const cmdStr = s.config.command ? `${s.config.command} ${(s.config.args ?? []).join(" ")}` : s.config.url ?? "";
2701
+ const configHtml = cmdStr ? `<div class="card-meta">${esc2(cmdStr)}</div>` : "";
2702
+ const copyData = cmdStr || s.name;
1713
2703
  return `
1714
- <div class="card" data-searchable="${esc(s.name + " " + (intro?.serverInfo?.name ?? ""))}">
2704
+ <div class="card" data-searchable="${esc2(s.name + " " + (intro?.serverInfo?.name ?? ""))}"
2705
+ style="animation-delay: ${i * 0.05}s">
2706
+ <button class="copy-btn" data-copy="${esc2(copyData)}">copiar</button>
1715
2707
  <div class="card-title">
1716
- <span>${esc(s.name)}${serverVersion}</span>
2708
+ <span>${esc2(s.name)}${serverVersion}</span>
1717
2709
  <span class="status ${statusClass}">${statusText}</span>
1718
2710
  <span class="scope-badge scope-badge--${s.source}">${s.source}</span>
1719
2711
  </div>
@@ -1729,10 +2721,13 @@ function renderMcpServers(servers) {
1729
2721
  0
1730
2722
  );
1731
2723
  return `
1732
- <div class="section">
2724
+ <div class="section" id="section-mcp">
1733
2725
  <div class="section-header">
1734
- <h2>MCP Servers</h2>
1735
- <div>
2726
+ <div class="section-header-left">
2727
+ <span class="section-icon">\u2699\uFE0F</span>
2728
+ <h2>MCP Servers</h2>
2729
+ </div>
2730
+ <div class="section-header-right">
1736
2731
  <span class="count">${servers.length} servers &middot; ${totalTools} tools</span>
1737
2732
  <span class="arrow">&#9660;</span>
1738
2733
  </div>
@@ -1742,6 +2737,7 @@ function renderMcpServers(servers) {
1742
2737
  }
1743
2738
  function renderContextFiles(files) {
1744
2739
  if (files.length === 0) return "";
2740
+ const maxSize = Math.max(...files.map((f) => f.size), 1);
1745
2741
  const groups = /* @__PURE__ */ new Map();
1746
2742
  for (const f of files) {
1747
2743
  const existing = groups.get(f.tool) ?? [];
@@ -1750,16 +2746,21 @@ function renderContextFiles(files) {
1750
2746
  }
1751
2747
  let groupsHtml = "";
1752
2748
  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>` : "";
2749
+ const items = toolFiles.map((f, i) => {
2750
+ const previewHtml = f.preview ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc2(f.preview)}</div>` : "";
2751
+ const errorHtml = f.error ? `<span class="status status--error">${esc2(f.error)}</span>` : "";
1756
2752
  const sizeStr = f.size > 0 ? formatBytes(f.size) : "";
1757
- const typeIcon = f.type === "directory" ? "&#128193;" : "&#128196;";
2753
+ const typeIcon = f.type === "directory" ? "\u{1F4C1}" : "\u{1F4C4}";
2754
+ const sizePercent = f.size > 0 ? Math.max(3, f.size / maxSize * 100) : 0;
2755
+ const sizeBar = f.size > 0 ? `<div class="size-bar"><div class="size-bar-fill" style="width:${sizePercent}%"></div></div>` : "";
1758
2756
  const childrenHtml = f.children && f.children.length > 0 ? `<div class="card-meta" style="margin-top:0.3rem">${f.children.length} archivos dentro</div>` : "";
1759
2757
  return `
1760
- <div class="card" data-searchable="${esc(f.path + " " + tool)}">
1761
- <div class="card-title">${typeIcon} ${esc(f.path)} ${errorHtml}</div>
2758
+ <div class="card" data-searchable="${esc2(f.path + " " + tool)}"
2759
+ style="animation-delay: ${i * 0.04}s">
2760
+ <button class="copy-btn" data-copy="${esc2(f.path)}">copiar</button>
2761
+ <div class="card-title">${typeIcon} ${esc2(f.path)} ${errorHtml}</div>
1762
2762
  <div class="card-meta">${sizeStr} &middot; <span class="scope-badge scope-badge--${f.scope}">${f.scope}</span></div>
2763
+ ${sizeBar}
1763
2764
  ${childrenHtml}
1764
2765
  ${previewHtml}
1765
2766
  </div>`;
@@ -1774,10 +2775,13 @@ function renderContextFiles(files) {
1774
2775
  </div>`;
1775
2776
  }
1776
2777
  return `
1777
- <div class="section">
2778
+ <div class="section" id="section-context">
1778
2779
  <div class="section-header">
1779
- <h2>Archivos de Contexto</h2>
1780
- <div>
2780
+ <div class="section-header-left">
2781
+ <span class="section-icon">\u{1F4C4}</span>
2782
+ <h2>Archivos de Contexto</h2>
2783
+ </div>
2784
+ <div class="section-header-right">
1781
2785
  <span class="count">${files.length} archivos &middot; ${groups.size} herramientas</span>
1782
2786
  <span class="arrow">&#9660;</span>
1783
2787
  </div>
@@ -1787,23 +2791,27 @@ function renderContextFiles(files) {
1787
2791
  }
1788
2792
  function renderSkills(skills) {
1789
2793
  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>` : "";
2794
+ const items = skills.map((s, i) => {
2795
+ const triggersHtml = s.triggers ? `<div class="card-meta">${s.triggers.map((t) => esc2(t)).join(", ")}</div>` : "";
1792
2796
  return `
1793
- <div class="card" data-searchable="${esc(s.name + " " + (s.description ?? ""))}">
2797
+ <div class="card" data-searchable="${esc2(s.name + " " + (s.description ?? ""))}"
2798
+ style="animation-delay: ${i * 0.04}s">
1794
2799
  <div class="card-title">
1795
- ${esc(s.name)}
2800
+ \u26A1 ${esc2(s.name)}
1796
2801
  <span class="scope-badge scope-badge--${s.scope}">${s.scope}</span>
1797
2802
  </div>
1798
- ${s.description ? `<div class="card-meta">${esc(s.description)}</div>` : ""}
2803
+ ${s.description ? `<div class="card-meta">${esc2(s.description)}</div>` : ""}
1799
2804
  ${triggersHtml}
1800
2805
  </div>`;
1801
2806
  }).join("");
1802
2807
  return `
1803
- <div class="section">
2808
+ <div class="section" id="section-skills">
1804
2809
  <div class="section-header">
1805
- <h2>Skills</h2>
1806
- <div>
2810
+ <div class="section-header-left">
2811
+ <span class="section-icon">\u26A1</span>
2812
+ <h2>Skills</h2>
2813
+ </div>
2814
+ <div class="section-header-right">
1807
2815
  <span class="count">${skills.length}</span>
1808
2816
  <span class="arrow">&#9660;</span>
1809
2817
  </div>
@@ -1813,25 +2821,29 @@ function renderSkills(skills) {
1813
2821
  }
1814
2822
  function renderAgents(agents) {
1815
2823
  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>' : "";
2824
+ const items = agents.map((a, i) => {
2825
+ const modelHtml = a.model ? `<span class="badge badge--green" style="font-size:0.65rem;padding:0.1rem 0.5rem">${esc2(a.model)}</span>` : "";
2826
+ const memoryHtml = a.hasMemory ? '<span class="badge badge--pink" style="font-size:0.65rem;padding:0.1rem 0.5rem">\u{1F9E0} memoria</span>' : "";
1819
2827
  return `
1820
- <div class="card" data-searchable="${esc(a.name + " " + (a.description ?? ""))}">
2828
+ <div class="card" data-searchable="${esc2(a.name + " " + (a.description ?? ""))}"
2829
+ style="animation-delay: ${i * 0.04}s">
1821
2830
  <div class="card-title">
1822
- ${esc(a.name)}
2831
+ \u{1F916} ${esc2(a.name)}
1823
2832
  ${modelHtml}
1824
2833
  ${memoryHtml}
1825
2834
  <span class="scope-badge scope-badge--${a.scope}">${a.scope}</span>
1826
2835
  </div>
1827
- ${a.description ? `<div class="card-meta">${esc(a.description)}</div>` : ""}
2836
+ ${a.description ? `<div class="card-meta">${esc2(a.description)}</div>` : ""}
1828
2837
  </div>`;
1829
2838
  }).join("");
1830
2839
  return `
1831
- <div class="section">
2840
+ <div class="section" id="section-agents">
1832
2841
  <div class="section-header">
1833
- <h2>Agents</h2>
1834
- <div>
2842
+ <div class="section-header-left">
2843
+ <span class="section-icon">\u{1F916}</span>
2844
+ <h2>Agents</h2>
2845
+ </div>
2846
+ <div class="section-header-right">
1835
2847
  <span class="count">${agents.length}</span>
1836
2848
  <span class="arrow">&#9660;</span>
1837
2849
  </div>
@@ -1841,23 +2853,33 @@ function renderAgents(agents) {
1841
2853
  }
1842
2854
  function renderMemories(memories) {
1843
2855
  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>` : "";
2856
+ const items = memories.map((m, i) => {
2857
+ const detailEntries = m.details ? Object.entries(m.details).filter(
2858
+ ([k]) => k !== "preview"
2859
+ ) : [];
2860
+ const detailsHtml = detailEntries.length > 0 ? `<div class="card-meta">${detailEntries.map(([k, v]) => `${esc2(k)}: ${esc2(String(v))}`).join(" &middot; ")}</div>` : "";
2861
+ const previewVal = m.details && typeof m.details.preview === "string" ? m.details.preview : null;
2862
+ const previewHtml = previewVal ? `<button class="preview-toggle">Ver contenido</button><div class="preview">${esc2(previewVal)}</div>` : "";
1846
2863
  return `
1847
- <div class="card" data-searchable="${esc(m.type)}">
2864
+ <div class="card" data-searchable="${esc2(m.type + " " + (m.path ?? ""))}"
2865
+ style="animation-delay: ${i * 0.04}s">
1848
2866
  <div class="card-title">
1849
- ${esc(m.type)}
2867
+ \u{1F9E0} ${esc2(m.type)}
1850
2868
  <span class="status status--${m.status}">${m.status}</span>
1851
2869
  </div>
1852
- <div class="card-meta">${m.source}${m.path ? ` &middot; ${esc(m.path)}` : ""}</div>
2870
+ <div class="card-meta">${m.source}${m.path ? ` &middot; ${esc2(m.path)}` : ""}</div>
1853
2871
  ${detailsHtml}
2872
+ ${previewHtml}
1854
2873
  </div>`;
1855
2874
  }).join("");
1856
2875
  return `
1857
- <div class="section">
2876
+ <div class="section" id="section-memories">
1858
2877
  <div class="section-header">
1859
- <h2>Memorias</h2>
1860
- <div>
2878
+ <div class="section-header-left">
2879
+ <span class="section-icon">\u{1F9E0}</span>
2880
+ <h2>Memorias</h2>
2881
+ </div>
2882
+ <div class="section-header-right">
1861
2883
  <span class="count">${memories.length}</span>
1862
2884
  <span class="arrow">&#9660;</span>
1863
2885
  </div>
@@ -1865,14 +2887,40 @@ function renderMemories(memories) {
1865
2887
  <div class="section-content">${items}</div>
1866
2888
  </div>`;
1867
2889
  }
2890
+ function renderWarnings(warnings) {
2891
+ if (warnings.length === 0) return "";
2892
+ const items = warnings.map(
2893
+ (w) => `
2894
+ <div class="warning-card">
2895
+ <div class="warning-scanner">${esc2(w.scanner)}${w.path ? ` &mdash; ${esc2(w.path)}` : ""}</div>
2896
+ <div>${esc2(w.message)}</div>
2897
+ </div>`
2898
+ ).join("");
2899
+ return `
2900
+ <div class="section" id="section-warnings">
2901
+ <div class="section-header">
2902
+ <div class="section-header-left">
2903
+ <span class="section-icon">\u26A0\uFE0F</span>
2904
+ <h2>Advertencias</h2>
2905
+ </div>
2906
+ <div class="section-header-right">
2907
+ <span class="count">${warnings.length}</span>
2908
+ <span class="arrow">&#9660;</span>
2909
+ </div>
2910
+ </div>
2911
+ <div class="section-content">${items}</div>
2912
+ </div>`;
2913
+ }
1868
2914
  function renderEmptyState() {
1869
2915
  return `
1870
2916
  <div class="empty-state">
2917
+ <span class="empty-state-icon">\u{1F50D}</span>
1871
2918
  <h3>No se encontr\xF3 configuraci\xF3n AI en este proyecto</h3>
1872
2919
  <p>Este proyecto no tiene archivos de configuraci\xF3n de herramientas AI.</p>
1873
- <p style="margin-top:1rem;font-size:0.85rem">
2920
+ <p style="margin-top:1rem;font-size:0.85rem;color:var(--text-dim)">
1874
2921
  Herramientas soportadas: Claude, Cursor, Windsurf, Copilot, Gemini,
1875
- Codex, Aider, Cline, Continue, Amazon Q, Augment, Replit, Firebase Studio
2922
+ Codex, OpenCode, Aider, Cline, Roo, Continue, Amazon Q, Augment,
2923
+ Replit, Firebase Studio, Tabnine, Sourcegraph
1876
2924
  </p>
1877
2925
  </div>`;
1878
2926
  }
@@ -1894,14 +2942,46 @@ function computeSummary(result) {
1894
2942
  function generateHtml(result) {
1895
2943
  const summary = computeSummary(result);
1896
2944
  const isEmpty = summary.totalMcpServers === 0 && summary.totalFiles === 0 && summary.totalSkills === 0 && summary.totalAgents === 0 && summary.totalMemories === 0;
2945
+ const navBar = renderNavBar(summary);
1897
2946
  const header = renderHeader(result.project, summary, result.scanDuration);
2947
+ const statsGrid = renderStatsGrid(summary);
2948
+ const ecosystemMap = renderEcosystemMap(result, summary);
1898
2949
  const content = isEmpty ? renderEmptyState() : [
1899
2950
  renderMcpServers(result.mcpServers),
1900
2951
  renderContextFiles(result.contextFiles),
1901
2952
  renderSkills(result.skills),
1902
2953
  renderAgents(result.agents),
1903
- renderMemories(result.memories)
2954
+ renderMemories(result.memories),
2955
+ renderWarnings(result.warnings)
1904
2956
  ].join("");
2957
+ const exportData = {
2958
+ project: result.project.name,
2959
+ scannedAt: result.project.scannedAt,
2960
+ scanDuration: result.scanDuration,
2961
+ summary,
2962
+ mcpServers: result.mcpServers.map((s) => ({
2963
+ name: s.name,
2964
+ source: s.source,
2965
+ transport: s.config.transport,
2966
+ tools: s.introspection?.tools.length ?? 0
2967
+ })),
2968
+ contextFiles: result.contextFiles.map((f) => ({
2969
+ path: f.path,
2970
+ tool: f.tool,
2971
+ scope: f.scope
2972
+ })),
2973
+ skills: result.skills.map((s) => ({ name: s.name, scope: s.scope })),
2974
+ agents: result.agents.map((a) => ({
2975
+ name: a.name,
2976
+ scope: a.scope,
2977
+ model: a.model
2978
+ })),
2979
+ memories: result.memories.map((m) => ({
2980
+ type: m.type,
2981
+ source: m.source,
2982
+ status: m.status
2983
+ }))
2984
+ };
1905
2985
  return `<!DOCTYPE html>
1906
2986
  <html lang="es">
1907
2987
  <head>
@@ -1911,16 +2991,25 @@ function generateHtml(result) {
1911
2991
  <style>${CSS_STYLES}</style>
1912
2992
  </head>
1913
2993
  <body>
2994
+ ${navBar}
1914
2995
  <div class="container">
1915
2996
  ${header}
2997
+ ${statsGrid}
2998
+ ${ecosystemMap}
1916
2999
  <div class="search-bar">
1917
- <input type="text" id="search-input" placeholder="Buscar tools, archivos, skills, agents..." />
3000
+ <div class="search-bar-inner">
3001
+ <span class="search-icon">\u{1F50D}</span>
3002
+ <input type="text" id="search-input" placeholder="Buscar tools, archivos, skills, agents..." />
3003
+ <span class="search-kbd">/</span>
3004
+ </div>
3005
+ <span class="search-results-count"></span>
1918
3006
  </div>
1919
3007
  ${content}
1920
3008
  <footer class="footer">
1921
3009
  Generado por ai-context-inspector &mdash; ${new Date(result.project.scannedAt).toLocaleString()}
1922
3010
  </footer>
1923
3011
  </div>
3012
+ <script type="application/json" id="scan-data">${JSON.stringify(exportData)}</script>
1924
3013
  <script>${JS_SCRIPTS}</script>
1925
3014
  </body>
1926
3015
  </html>`;