viberadar 0.3.111 → 0.3.112

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.
@@ -693,6 +693,53 @@
693
693
  .tt-fix-btn:hover { background: rgba(255,200,0,0.1); color: var(--yellow); border-color: var(--yellow); }
694
694
  .tt-write-btn { border-color: var(--accent); color: var(--accent); }
695
695
  .tt-write-btn:hover { background: rgba(88,166,255,0.1); color: var(--accent); border-color: var(--accent); }
696
+
697
+ /* ─── Test Navigator ──────────────────────────────────────────────── */
698
+ .tn-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 16px; }
699
+ .tn-stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
700
+ .tn-stat-val { font-size: 24px; font-weight: 700; line-height: 1; margin-bottom: 2px; }
701
+ .tn-stat-label { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.4px; }
702
+ .tn-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
703
+ .tn-row {
704
+ display: flex; align-items: center; gap: 8px;
705
+ padding: 7px 10px; border-radius: 6px; cursor: pointer;
706
+ font-size: 13px; transition: background 0.1s;
707
+ content-visibility: auto; contain-intrinsic-size: 34px;
708
+ }
709
+ .tn-row:hover { background: var(--bg-card); }
710
+ .tn-row.active { background: var(--bg-card); outline: 1px solid var(--accent); }
711
+ .tn-row.has-problem { border-left: 3px solid var(--red); }
712
+ .tn-row-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex-shrink: 1; }
713
+ .tn-row-dir { font-size: 11px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-left: auto; flex-shrink: 2; min-width: 0; }
714
+ .tn-badges { display: flex; gap: 4px; flex-wrap: wrap; flex-shrink: 0; }
715
+ .tn-feature-badge {
716
+ display: inline-flex; align-items: center; gap: 3px;
717
+ font-size: 10px; padding: 1px 6px; border-radius: 10px;
718
+ background: var(--bg); border: 1px solid var(--border); cursor: pointer;
719
+ }
720
+ .tn-feature-badge:hover { border-color: var(--dim); }
721
+ .tn-problem-badge {
722
+ font-size: 10px; padding: 1px 6px; border-radius: 10px; font-weight: 600;
723
+ }
724
+ .tn-problem-badge.orphan { background: #3a1a1a; color: var(--red); }
725
+ .tn-problem-badge.stale { background: #3a3020; color: var(--yellow); }
726
+ .tn-problem-badge.duplicate { background: #2a2a3a; color: #b388ff; }
727
+ .tn-source-link { font-size: 11px; color: var(--muted); cursor: pointer; flex-shrink: 0; }
728
+ .tn-source-link:hover { color: var(--text); text-decoration: underline; }
729
+ .tn-no-source { font-size: 11px; color: var(--red); font-style: italic; flex-shrink: 0; }
730
+ .tn-test-count { font-size: 10px; padding: 1px 5px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); color: var(--muted); flex-shrink: 0; }
731
+ .tn-sidebar-section { margin-bottom: 14px; }
732
+ .tn-sidebar-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); margin-bottom: 6px; }
733
+ .tn-filter {
734
+ display: flex; align-items: center; gap: 6px;
735
+ padding: 4px 8px; border-radius: 5px; cursor: pointer;
736
+ font-size: 12px; color: var(--muted); transition: background 0.1s, color 0.1s;
737
+ }
738
+ .tn-filter:hover { background: var(--bg-hover); color: var(--text); }
739
+ .tn-filter.active { background: var(--bg-card); color: var(--text); font-weight: 600; }
740
+ .tn-filter-count { margin-left: auto; font-size: 10px; color: var(--dim); }
741
+ .tn-filter-count.red { color: var(--red); }
742
+ .tn-filter-count.yellow { color: var(--yellow); }
696
743
  .file-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
697
744
  .file-row {
698
745
  display: flex;
@@ -1362,6 +1409,7 @@
1362
1409
  <div class="view-tabs" id="viewTabs">
1363
1410
  <div class="view-tab" data-view="features">Features</div>
1364
1411
  <div class="view-tab" data-view="files">Files</div>
1412
+ <div class="view-tab" data-view="tests">Tests</div>
1365
1413
  </div>
1366
1414
  <input class="search-input" id="searchInput" type="text" placeholder="Search…" />
1367
1415
  <div id="sidebarExtra"></div>
@@ -1429,6 +1477,9 @@ const FILE_ROWS_INITIAL_LIMIT = 250;
1429
1477
  const FILE_ROWS_LIMIT_STEP = 250;
1430
1478
  let fileRowsRenderKey = '';
1431
1479
  let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
1480
+ let testNavType = 'all'; // 'all'|'unit'|'integration'|'e2e'
1481
+ let testNavFeature = null; // null | featureKey string
1482
+ let testNavProblem = null; // null|'no-feature'|'no-source'|'duplicate'|'stale'
1432
1483
  let e2ePlan = null; // current E2E plan object
1433
1484
  let e2ePlanLoading = false;
1434
1485
  let obsActiveTab = 'overview'; // active observability tab
@@ -1447,9 +1498,9 @@ function switchObsTab(tabId) {
1447
1498
  });
1448
1499
  }
1449
1500
  const modeStore = {
1450
- qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1451
- observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1452
- docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1501
+ qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
1502
+ observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
1503
+ docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
1453
1504
  };
1454
1505
 
1455
1506
  function getModeFromPath(pathname = window.location.pathname) {
@@ -1479,6 +1530,9 @@ function saveModeState(mode) {
1479
1530
  drillTestType,
1480
1531
  activePanelKey,
1481
1532
  showOnlyUntestedInFeature,
1533
+ testNavType,
1534
+ testNavFeature,
1535
+ testNavProblem,
1482
1536
  };
1483
1537
  }
1484
1538
 
@@ -1491,6 +1545,9 @@ function restoreModeState(mode) {
1491
1545
  drillTestType = state.drillTestType;
1492
1546
  activePanelKey = state.activePanelKey;
1493
1547
  showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
1548
+ testNavType = state.testNavType || 'all';
1549
+ testNavFeature = state.testNavFeature || null;
1550
+ testNavProblem = state.testNavProblem || null;
1494
1551
  }
1495
1552
 
1496
1553
  function switchMode(nextMode) {
@@ -1602,6 +1659,17 @@ function applyHashRoute() {
1602
1659
  return;
1603
1660
  }
1604
1661
  const params = new URLSearchParams(hash);
1662
+
1663
+ // tests view hash: #view=tests&type=e2e&problem=stale&feature=auth
1664
+ if (params.get('view') === 'tests') {
1665
+ view = 'tests';
1666
+ testNavType = params.get('type') || 'all';
1667
+ testNavProblem = params.get('problem') || null;
1668
+ testNavFeature = params.get('tfeature') || null;
1669
+ activePanelKey = null;
1670
+ return;
1671
+ }
1672
+
1605
1673
  const featureKey = params.get('feature');
1606
1674
  if (!featureKey) return;
1607
1675
  const isKnownFeature = Array.isArray(D?.features) && D.features.some(f => f.key === featureKey);
@@ -2802,6 +2870,7 @@ function renderSidebar() {
2802
2870
  t.classList.toggle('active', t.dataset.view === view)
2803
2871
  );
2804
2872
 
2873
+ if (view === 'tests') { renderTestNavSidebar(extra); return; }
2805
2874
  if (view !== 'files') { extra.innerHTML = ''; return; }
2806
2875
 
2807
2876
  const types = [...new Set(D.modules.map(m => m.type))].sort();
@@ -2850,6 +2919,8 @@ function renderContent() {
2850
2919
  if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
2851
2920
  else if (drillFeatureKey) renderFeatureDetail(c);
2852
2921
  else renderFeatureCards(c);
2922
+ } else if (view === 'tests') {
2923
+ renderTestNavigator(c);
2853
2924
  } else {
2854
2925
  renderModuleGrid(c);
2855
2926
  }
@@ -4781,6 +4852,269 @@ function toggleSourceSelection(relPath, checked) {
4781
4852
  renderContent();
4782
4853
  }
4783
4854
 
4855
+ // ─── Test Navigator ──────────────────────────────────────────────────────────
4856
+ function buildTestNavData() {
4857
+ const allTests = D.modules.filter(m => m.type === 'test');
4858
+ // reverse map: testRelPath → source modules that reference it
4859
+ const testToSources = new Map();
4860
+ // sourceRelPath → test relPaths (for duplicate detection)
4861
+ const sourceToTests = new Map();
4862
+ for (const m of D.modules) {
4863
+ if (m.type === 'test' || !m.testFile) continue;
4864
+ const tf = m.testFile.replace(/\\/g, '/');
4865
+ if (!testToSources.has(tf)) testToSources.set(tf, []);
4866
+ testToSources.get(tf).push(m);
4867
+ const sp = m.relativePath.replace(/\\/g, '/');
4868
+ if (!sourceToTests.has(sp)) sourceToTests.set(sp, []);
4869
+ sourceToTests.get(sp).push(tf);
4870
+ }
4871
+ return allTests.map(t => {
4872
+ const rel = t.relativePath.replace(/\\/g, '/');
4873
+ const linkedSources = testToSources.get(rel) || [];
4874
+ const features = t.featureKeys || [];
4875
+ const noFeature = features.length === 0;
4876
+ const noSource = linkedSources.length === 0;
4877
+ const stale = t.testStale === true;
4878
+ // duplicate: any linked source is also covered by another test of same type
4879
+ const duplicate = linkedSources.some(s => {
4880
+ const sp = s.relativePath.replace(/\\/g, '/');
4881
+ const tests = sourceToTests.get(sp) || [];
4882
+ if (tests.length <= 1) return false;
4883
+ const sameTypeTests = tests.filter(tp => {
4884
+ const tm = allTests.find(x => x.relativePath.replace(/\\/g, '/') === tp);
4885
+ return tm && tm.testType === t.testType;
4886
+ });
4887
+ return sameTypeTests.length > 1;
4888
+ });
4889
+ const problems = [];
4890
+ if (noFeature) problems.push('no-feature');
4891
+ if (noSource) problems.push('no-source');
4892
+ if (duplicate) problems.push('duplicate');
4893
+ if (stale) problems.push('stale');
4894
+ return { module: t, linkedSources, features, problems };
4895
+ });
4896
+ }
4897
+
4898
+ function renderTestNavSidebar(extra) {
4899
+ const data = buildTestNavData();
4900
+ const counts = { all: data.length, unit: 0, integration: 0, e2e: 0 };
4901
+ const problemCounts = { 'no-feature': 0, 'no-source': 0, duplicate: 0, stale: 0 };
4902
+ for (const d of data) {
4903
+ if (d.module.testType) counts[d.module.testType] = (counts[d.module.testType] || 0) + 1;
4904
+ for (const p of d.problems) problemCounts[p]++;
4905
+ }
4906
+ const totalProblems = data.filter(d => d.problems.length > 0).length;
4907
+
4908
+ const typeFilters = [
4909
+ { key: 'all', label: 'Все', count: counts.all, color: '#7d8590' },
4910
+ { key: 'unit', label: 'Unit', count: counts.unit, color: '#e3b341' },
4911
+ { key: 'integration', label: 'Integration', count: counts.integration, color: '#58a6ff' },
4912
+ { key: 'e2e', label: 'E2E', count: counts.e2e, color: '#d2a8ff' },
4913
+ ];
4914
+
4915
+ const problemFilters = [
4916
+ { key: null, label: 'Все тесты', count: counts.all },
4917
+ { key: 'no-feature', label: 'Без фичи', count: problemCounts['no-feature'], cls: 'red' },
4918
+ { key: 'no-source', label: 'Без исходника', count: problemCounts['no-source'], cls: 'red' },
4919
+ { key: 'duplicate', label: 'Дубли', count: problemCounts.duplicate, cls: 'yellow' },
4920
+ { key: 'stale', label: 'Устаревшие', count: problemCounts.stale, cls: 'yellow' },
4921
+ ];
4922
+
4923
+ const featureSet = new Map();
4924
+ for (const d of data) {
4925
+ for (const fk of d.features) {
4926
+ if (!featureSet.has(fk)) featureSet.set(fk, 0);
4927
+ featureSet.set(fk, featureSet.get(fk) + 1);
4928
+ }
4929
+ }
4930
+ const noFeatureCount = problemCounts['no-feature'];
4931
+
4932
+ extra.innerHTML = `
4933
+ <div class="tn-sidebar-section">
4934
+ <div class="tn-sidebar-label">Тип</div>
4935
+ ${typeFilters.map(f => `
4936
+ <div class="tn-filter ${testNavType === f.key ? 'active' : ''}" data-tn-type="${f.key}">
4937
+ <span class="type-dot" style="background:${f.color}"></span>
4938
+ ${f.label}
4939
+ <span class="tn-filter-count">${f.count}</span>
4940
+ </div>
4941
+ `).join('')}
4942
+ </div>
4943
+ <div class="tn-sidebar-section">
4944
+ <div class="tn-sidebar-label">Проблемы</div>
4945
+ ${problemFilters.map(f => `
4946
+ <div class="tn-filter ${testNavProblem === f.key ? 'active' : ''}" data-tn-problem="${f.key || ''}">
4947
+ ${f.label}
4948
+ <span class="tn-filter-count ${f.cls || ''}">${f.count}</span>
4949
+ </div>
4950
+ `).join('')}
4951
+ </div>
4952
+ <div class="tn-sidebar-section">
4953
+ <div class="tn-sidebar-label">Фича</div>
4954
+ <div class="tn-filter ${testNavFeature === null ? 'active' : ''}" data-tn-feature="">
4955
+ Все фичи
4956
+ <span class="tn-filter-count">${counts.all}</span>
4957
+ </div>
4958
+ ${Array.from(featureSet.entries()).map(([fk, cnt]) => {
4959
+ const feat = D.features.find(f => f.key === fk);
4960
+ const color = feat ? feat.color : '#484f58';
4961
+ const label = feat ? feat.label : fk;
4962
+ return `<div class="tn-filter ${testNavFeature === fk ? 'active' : ''}" data-tn-feature="${fk}">
4963
+ <span class="type-dot" style="background:${color}"></span>
4964
+ ${label}
4965
+ <span class="tn-filter-count">${cnt}</span>
4966
+ </div>`;
4967
+ }).join('')}
4968
+ ${noFeatureCount > 0 ? `
4969
+ <div class="tn-filter ${testNavFeature === '__none__' ? 'active' : ''}" data-tn-feature="__none__">
4970
+ <span class="type-dot" style="background:#484f58"></span>
4971
+ (Без фичи)
4972
+ <span class="tn-filter-count red">${noFeatureCount}</span>
4973
+ </div>` : ''}
4974
+ </div>
4975
+ `;
4976
+
4977
+ extra.querySelectorAll('[data-tn-type]').forEach(el => {
4978
+ el.onclick = () => { testNavType = el.dataset.tnType; renderSidebar(); renderContent(); };
4979
+ });
4980
+ extra.querySelectorAll('[data-tn-problem]').forEach(el => {
4981
+ el.onclick = () => { testNavProblem = el.dataset.tnProblem || null; renderSidebar(); renderContent(); };
4982
+ });
4983
+ extra.querySelectorAll('[data-tn-feature]').forEach(el => {
4984
+ el.onclick = () => {
4985
+ const v = el.dataset.tnFeature;
4986
+ testNavFeature = v === '' ? null : v;
4987
+ renderSidebar(); renderContent();
4988
+ };
4989
+ });
4990
+ }
4991
+
4992
+ function renderTestNavigator(c) {
4993
+ const data = buildTestNavData();
4994
+ // apply filters
4995
+ let filtered = data;
4996
+ if (testNavType !== 'all') filtered = filtered.filter(d => d.module.testType === testNavType);
4997
+ if (testNavProblem) filtered = filtered.filter(d => d.problems.includes(testNavProblem));
4998
+ if (testNavFeature === '__none__') filtered = filtered.filter(d => d.features.length === 0);
4999
+ else if (testNavFeature) filtered = filtered.filter(d => d.features.includes(testNavFeature));
5000
+
5001
+ const q = searchQuery.toLowerCase();
5002
+ if (q) filtered = filtered.filter(d =>
5003
+ d.module.name.toLowerCase().includes(q) || d.module.relativePath.toLowerCase().includes(q)
5004
+ );
5005
+
5006
+ // stats
5007
+ const totalProblems = filtered.filter(d => d.problems.length > 0).length;
5008
+ const staleCount = filtered.filter(d => d.problems.includes('stale')).length;
5009
+ const typeCounts = { unit: 0, integration: 0, e2e: 0 };
5010
+ for (const d of filtered) { if (d.module.testType) typeCounts[d.module.testType]++; }
5011
+
5012
+ const rowsRenderKey = `testnav:${testNavType}:${testNavProblem}:${testNavFeature}:${q}`;
5013
+ const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
5014
+
5015
+ c.innerHTML = `
5016
+ <div class="tn-summary">
5017
+ <div class="tn-stat-card">
5018
+ <div class="tn-stat-val">${filtered.length}</div>
5019
+ <div class="tn-stat-label">Тестов</div>
5020
+ </div>
5021
+ <div class="tn-stat-card">
5022
+ <div class="tn-stat-val" style="color:${totalProblems > 0 ? 'var(--red)' : 'var(--text)'}">${totalProblems}</div>
5023
+ <div class="tn-stat-label">С проблемами</div>
5024
+ </div>
5025
+ <div class="tn-stat-card">
5026
+ <div class="tn-stat-val" style="color:${staleCount > 0 ? 'var(--yellow)' : 'var(--text)'}">${staleCount}</div>
5027
+ <div class="tn-stat-label">Устаревших</div>
5028
+ </div>
5029
+ <div class="tn-stat-card">
5030
+ <div class="tn-stat-val" style="font-size:14px;line-height:1.8">
5031
+ <span style="color:#e3b341">${typeCounts.unit}</span> unit
5032
+ <span style="color:#58a6ff">${typeCounts.integration}</span> integ
5033
+ <span style="color:#d2a8ff">${typeCounts.e2e}</span> e2e
5034
+ </div>
5035
+ <div class="tn-stat-label">По типам</div>
5036
+ </div>
5037
+ </div>
5038
+ ${!filtered.length ? '<div class="empty">Ничего не найдено</div>' : ''}
5039
+ <div class="tn-rows" id="tnRows"></div>
5040
+ ${hasMoreRows ? `
5041
+ <div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
5042
+ <span>Показано ${visibleRows.length} из ${filtered.length} тестов</span>
5043
+ <button class="bulk-actions-btn" id="tnLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
5044
+ </div>` : ''}
5045
+ `;
5046
+
5047
+ const rowsContainer = document.getElementById('tnRows');
5048
+ for (const d of visibleRows) {
5049
+ const row = document.createElement('div');
5050
+ const isActive = activePanelKey === d.module.id;
5051
+ const hasProblem = d.problems.length > 0;
5052
+ row.className = 'tn-row' + (isActive ? ' active' : '') + (hasProblem ? ' has-problem' : '');
5053
+
5054
+ const ttMeta = TEST_TYPE_META[d.module.testType];
5055
+ const typeIcon = ttMeta ? `<span class="type-dot" style="background:${ttMeta.color}"></span>` : '';
5056
+ const testCountBadge = d.module.testCount ? `<span class="tn-test-count">${d.module.testCount} case${d.module.testCount > 1 ? 's' : ''}</span>` : '';
5057
+
5058
+ const featureBadges = d.features.map(fk => {
5059
+ const feat = D.features.find(f => f.key === fk);
5060
+ const color = feat ? feat.color : '#484f58';
5061
+ const label = feat ? feat.label : fk;
5062
+ return `<span class="tn-feature-badge" data-feature-key="${fk}" title="${label}"><span class="type-dot" style="background:${color}"></span>${label}</span>`;
5063
+ }).join('');
5064
+
5065
+ let sourceHtml = '';
5066
+ if (d.linkedSources.length > 0) {
5067
+ const src = d.linkedSources[0];
5068
+ sourceHtml = `<span class="tn-source-link" data-source-id="${src.id}" title="${src.relativePath}">${src.name}</span>`;
5069
+ if (d.linkedSources.length > 1) sourceHtml += `<span class="tn-source-link"> +${d.linkedSources.length - 1}</span>`;
5070
+ } else {
5071
+ sourceHtml = '<span class="tn-no-source">нет исходника</span>';
5072
+ }
5073
+
5074
+ const problemBadges = d.problems.map(p => {
5075
+ const labels = { 'no-feature': 'без фичи', 'no-source': 'без исходника', duplicate: 'дубль', stale: 'устарел' };
5076
+ const cls = p === 'stale' ? 'stale' : p === 'duplicate' ? 'duplicate' : 'orphan';
5077
+ return `<span class="tn-problem-badge ${cls}">${labels[p]}</span>`;
5078
+ }).join('');
5079
+
5080
+ const dir = d.module.relativePath.replace(/[^/\\]+$/, '').replace(/[/\\]$/, '');
5081
+ row.innerHTML = `
5082
+ ${typeIcon}
5083
+ <span class="tn-row-name">${d.module.name}</span>
5084
+ ${testCountBadge}
5085
+ <div class="tn-badges">${featureBadges}${problemBadges}</div>
5086
+ ${sourceHtml}
5087
+ <span class="tn-row-dir">${dir}</span>
5088
+ `;
5089
+
5090
+ row.onclick = (e) => {
5091
+ if (e.target.closest('[data-feature-key]')) {
5092
+ const fk = e.target.closest('[data-feature-key]').dataset.featureKey;
5093
+ view = 'features';
5094
+ drillFeatureKey = fk;
5095
+ drillTestType = null;
5096
+ window.location.hash = 'feature=' + fk;
5097
+ renderSidebar();
5098
+ renderContent();
5099
+ return;
5100
+ }
5101
+ if (e.target.closest('[data-source-id]')) {
5102
+ const sid = e.target.closest('[data-source-id]').dataset.sourceId;
5103
+ const srcMod = D.modules.find(m => m.id === sid);
5104
+ if (srcMod) openModulePanel(srcMod);
5105
+ return;
5106
+ }
5107
+ openModulePanel(d.module);
5108
+ };
5109
+ rowsContainer.appendChild(row);
5110
+ }
5111
+
5112
+ const loadMoreBtn = document.getElementById('tnLoadMoreBtn');
5113
+ if (loadMoreBtn) {
5114
+ loadMoreBtn.onclick = () => { increaseFileRowsLimit(); renderContent(); };
5115
+ }
5116
+ }
5117
+
4784
5118
  function renderModuleGrid(c) {
4785
5119
  const q = searchQuery.toLowerCase();
4786
5120
  const list = D.modules.filter(m => {
@@ -5074,6 +5408,9 @@ document.querySelectorAll('.view-tab').forEach(tab => {
5074
5408
  activePanelKey = null;
5075
5409
  searchQuery = '';
5076
5410
  activeTypes.clear();
5411
+ testNavType = 'all';
5412
+ testNavFeature = null;
5413
+ testNavProblem = null;
5077
5414
  document.getElementById('searchInput').value = '';
5078
5415
  document.getElementById('panel').classList.remove('open');
5079
5416
  if (view !== 'features') clearFeatureHash();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.111",
3
+ "version": "0.3.112",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {