viberadar 0.3.3 → 0.3.4

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.
@@ -339,6 +339,64 @@
339
339
  font-family: monospace;
340
340
  }
341
341
 
342
+ /* ── Feature drill-down ──────────────────────────────────────────────────── */
343
+ .drill-header {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 14px;
347
+ padding-bottom: 14px;
348
+ border-bottom: 1px solid var(--border);
349
+ margin-bottom: 16px;
350
+ flex-wrap: wrap;
351
+ }
352
+ .back-btn {
353
+ background: none;
354
+ border: 1px solid var(--border);
355
+ border-radius: 6px;
356
+ color: var(--muted);
357
+ cursor: pointer;
358
+ padding: 5px 12px;
359
+ font-size: 12px;
360
+ flex-shrink: 0;
361
+ transition: background 0.1s, color 0.1s;
362
+ }
363
+ .back-btn:hover { background: var(--border); color: var(--text); }
364
+ .drill-title {
365
+ display: flex; align-items: center; gap: 8px;
366
+ font-size: 17px; font-weight: 700;
367
+ }
368
+ .drill-stats {
369
+ display: flex; gap: 16px;
370
+ font-size: 12px; color: var(--muted);
371
+ margin-left: auto;
372
+ }
373
+ .drill-desc {
374
+ font-size: 13px; color: var(--muted);
375
+ margin-bottom: 14px; line-height: 1.5;
376
+ }
377
+ .drill-section-label {
378
+ font-size: 10px; text-transform: uppercase;
379
+ letter-spacing: 0.5px; color: var(--muted);
380
+ margin: 14px 0 6px;
381
+ }
382
+ .file-rows { display: flex; flex-direction: column; gap: 2px; }
383
+ .file-row {
384
+ display: grid;
385
+ grid-template-columns: 22px 1fr auto;
386
+ align-items: center;
387
+ gap: 8px;
388
+ padding: 7px 10px;
389
+ border-radius: 6px;
390
+ cursor: pointer;
391
+ font-size: 13px;
392
+ transition: background 0.1s;
393
+ }
394
+ .file-row:hover { background: var(--bg-card); }
395
+ .file-row.active { background: var(--bg-hover); border-left: 2px solid var(--blue); padding-left: 8px; }
396
+ .file-row-icon { font-size: 12px; }
397
+ .file-row-name { font-weight: 500; word-break: break-all; }
398
+ .file-row-dir { font-size: 11px; color: var(--dim); text-align: right; word-break: break-word; }
399
+
342
400
  /* ── Misc ────────────────────────────────────────────────────────────────── */
343
401
  .loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
344
402
  .empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
@@ -388,6 +446,7 @@ let view = 'features';
388
446
  let searchQuery = '';
389
447
  let activeTypes = new Set();
390
448
  let activePanelKey = null;
449
+ let drillFeatureKey = null; // null = grid, string = inside a feature
391
450
 
392
451
  // ─── Color helpers ────────────────────────────────────────────────────────────
393
452
  const TYPE_COLORS = {
@@ -519,7 +578,18 @@ function renderSidebar() {
519
578
  // ─── Content ──────────────────────────────────────────────────────────────────
520
579
  function renderContent() {
521
580
  const c = document.getElementById('content');
522
- view === 'features' ? renderFeatureCards(c) : renderModuleGrid(c);
581
+ if (view === 'features') {
582
+ drillFeatureKey ? renderFeatureDetail(c) : renderFeatureCards(c);
583
+ } else {
584
+ renderModuleGrid(c);
585
+ }
586
+ }
587
+
588
+ function backToFeatures() {
589
+ drillFeatureKey = null;
590
+ activePanelKey = null;
591
+ document.getElementById('panel').classList.remove('open');
592
+ renderContent();
523
593
  }
524
594
 
525
595
  function renderFeatureCards(c) {
@@ -566,7 +636,7 @@ function renderFeatureCards(c) {
566
636
  <span class="feature-progress-label" style="color:${covColor(pct)}">${f.testedCount}/${f.fileCount} ✓</span>
567
637
  </div>
568
638
  </div>`;
569
- card.onclick = () => openFeaturePanel(f.key);
639
+ card.onclick = () => { drillFeatureKey = f.key; activePanelKey = null; document.getElementById('panel').classList.remove('open'); renderContent(); };
570
640
  grid.appendChild(card);
571
641
  });
572
642
 
@@ -602,6 +672,71 @@ function renderFeatureCards(c) {
602
672
  }
603
673
  }
604
674
 
675
+ function renderFeatureDetail(c) {
676
+ const feat = D.features.find(f => f.key === drillFeatureKey);
677
+ if (!feat) { backToFeatures(); return; }
678
+
679
+ const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
680
+ const src = mods.filter(m => m.type !== 'test');
681
+ const tst = mods.filter(m => m.type === 'test');
682
+ const testedCount = src.filter(m => m.hasTests).length;
683
+ const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
684
+
685
+ const q = searchQuery.toLowerCase();
686
+ const filteredSrc = q ? src.filter(m =>
687
+ m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
688
+ ) : src;
689
+ const filteredTst = q ? tst.filter(m =>
690
+ m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
691
+ ) : tst;
692
+
693
+ c.innerHTML = `
694
+ <div class="drill-header">
695
+ <button class="back-btn" onclick="backToFeatures()">← Все фичи</button>
696
+ <div class="drill-title">
697
+ <div style="width:10px;height:10px;border-radius:50%;background:${feat.color};flex-shrink:0"></div>
698
+ <span>${feat.label}</span>
699
+ </div>
700
+ <div class="drill-stats">
701
+ <span>${src.length} файлов</span>
702
+ <span style="color:${covColor(pct)}">${pct}% с тестами</span>
703
+ <span>${tst.length} тест-файлов</span>
704
+ </div>
705
+ </div>
706
+ ${feat.description ? `<div class="drill-desc">${feat.description}</div>` : ''}
707
+ <div class="file-rows" id="fileRows">
708
+ ${filteredSrc.length === 0 && !q
709
+ ? '<div style="font-size:13px;color:var(--dim)">Нет файлов — возможно паттерны в конфиге не совпадают</div>'
710
+ : filteredSrc.map(m => fileRow(m)).join('')
711
+ }
712
+ ${filteredTst.length > 0 ? `
713
+ <div class="drill-section-label">Тест-файлы (${filteredTst.length})</div>
714
+ ${filteredTst.map(m => fileRow(m, true)).join('')}
715
+ ` : ''}
716
+ </div>`;
717
+
718
+ c.querySelectorAll('.file-row[data-id]').forEach(row => {
719
+ row.onclick = () => {
720
+ const m = D.modules.find(m => m.id === row.dataset.id);
721
+ if (m) openModulePanel(m);
722
+ };
723
+ });
724
+ }
725
+
726
+ function fileRow(m, isTest = false) {
727
+ const parts = m.relativePath.replace(/\\/g, '/').split('/');
728
+ const name = parts[parts.length - 1];
729
+ const dir = parts.slice(0, -1).join('/');
730
+ const icon = isTest ? '🧪' : (m.hasTests ? '✅' : '⬜');
731
+ const isActive = activePanelKey === m.id;
732
+ return `
733
+ <div class="file-row${isActive ? ' active' : ''}" data-id="${m.id}">
734
+ <span class="file-row-icon">${icon}</span>
735
+ <span class="file-row-name">${name}</span>
736
+ <span class="file-row-dir">${dir}</span>
737
+ </div>`;
738
+ }
739
+
605
740
  function renderModuleGrid(c) {
606
741
  const q = searchQuery.toLowerCase();
607
742
  const list = D.modules.filter(m => {
@@ -833,6 +968,7 @@ document.querySelectorAll('.view-tab').forEach(tab => {
833
968
  tab.onclick = () => {
834
969
  if (tab.classList.contains('disabled')) return;
835
970
  view = tab.dataset.view;
971
+ drillFeatureKey = null;
836
972
  activePanelKey = null;
837
973
  searchQuery = '';
838
974
  activeTypes.clear();
@@ -871,7 +1007,7 @@ async function refreshData() {
871
1007
  renderSidebar();
872
1008
  renderContent();
873
1009
 
874
- // Re-open panel if it was open
1010
+ // Re-render drill-down or re-open panel
875
1011
  const panelOpen = document.getElementById('panel').classList.contains('open');
876
1012
  if (panelOpen && activePanelKey) {
877
1013
  if (activePanelKey === '__unmapped__') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {