viberadar 0.3.2 → 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,9 +636,105 @@ 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
  });
642
+
643
+ // ── Unmapped card ──────────────────────────────────────────────────────────
644
+ if (!q) {
645
+ const unmappedSrc = D.modules.filter(m =>
646
+ m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
647
+ );
648
+ if (unmappedSrc.length > 0) {
649
+ const isActive = activePanelKey === '__unmapped__';
650
+ const card = document.createElement('div');
651
+ card.className = 'feature-card' + (isActive ? ' active' : '');
652
+ card.style.borderStyle = 'dashed';
653
+ card.style.opacity = '0.75';
654
+ card.innerHTML = `
655
+ <div class="feature-accent" style="background:var(--yellow)"></div>
656
+ <div class="feature-body">
657
+ <div class="feature-title">
658
+ <span style="color:var(--yellow)">⚠ Unmapped</span>
659
+ <span class="feature-file-count">${unmappedSrc.length} ${pluralFiles(unmappedSrc.length)}</span>
660
+ </div>
661
+ <div class="feature-desc">Файлы вне карты фич — не входят ни в одну фичу</div>
662
+ <div class="feature-progress-wrap">
663
+ <div class="feature-progress-bar">
664
+ <div class="feature-progress-fill" style="width:100%;background:var(--border)"></div>
665
+ </div>
666
+ <span class="feature-progress-label" style="color:var(--dim)">нет привязки</span>
667
+ </div>
668
+ </div>`;
669
+ card.onclick = () => openUnmappedPanel(unmappedSrc);
670
+ grid.appendChild(card);
671
+ }
672
+ }
673
+ }
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>`;
572
738
  }
573
739
 
574
740
  function renderModuleGrid(c) {
@@ -726,6 +892,71 @@ function openModulePanel(m) {
726
892
  document.getElementById('panel').classList.add('open');
727
893
  }
728
894
 
895
+ function openUnmappedPanel(files) {
896
+ activePanelKey = '__unmapped__';
897
+ renderContent();
898
+
899
+ // Group by top-level directory
900
+ const byDir = {};
901
+ files.forEach(m => {
902
+ const parts = m.relativePath.replace(/\\/g, '/').split('/');
903
+ const dir = parts.length > 1 ? parts[0] : '(root)';
904
+ if (!byDir[dir]) byDir[dir] = [];
905
+ byDir[dir].push(m);
906
+ });
907
+ const dirs = Object.keys(byDir).sort();
908
+
909
+ // Build plain-text list for copying to AI agent
910
+ const plainList = files
911
+ .map(m => '- ' + m.relativePath.replace(/\\/g, '/'))
912
+ .join('\n');
913
+
914
+ const promptText =
915
+ `В проекте ${files.length} файлов без привязки к фичам (unmapped).\n` +
916
+ `Изучи список ниже и обнови viberadar.config.json:\n` +
917
+ `добавь эти файлы в существующие фичи или создай новые.\n\n` +
918
+ plainList;
919
+
920
+ document.getElementById('panelContent').innerHTML = `
921
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
922
+ <span style="font-size:16px">⚠</span>
923
+ <div class="panel-title" style="color:var(--yellow)">Unmapped файлы</div>
924
+ </div>
925
+ <div class="panel-subtitle">
926
+ Не входят ни в одну фичу.<br>
927
+ Запусти <code style="color:var(--blue)">npx viberadar init</code> — агент предложит куда добавить.
928
+ </div>
929
+
930
+ <button id="copyUnmapped" style="
931
+ width:100%; padding:8px 12px; margin-bottom:16px;
932
+ background:var(--bg); border:1px solid var(--border);
933
+ border-radius:6px; color:var(--blue); font-size:12px;
934
+ cursor:pointer; text-align:left;
935
+ ">📋 Скопировать список для AI-агента</button>
936
+
937
+ ${dirs.map(dir => `
938
+ <div class="panel-section">
939
+ <div class="panel-section-label">${dir}/ (${byDir[dir].length})</div>
940
+ <div class="file-list">
941
+ ${byDir[dir].map(m => fileItem(m)).join('')}
942
+ </div>
943
+ </div>
944
+ `).join('')}`;
945
+
946
+ document.getElementById('copyUnmapped').onclick = function() {
947
+ navigator.clipboard.writeText(promptText).then(() => {
948
+ this.textContent = '✅ Скопировано! Вставь в AI-агента';
949
+ this.style.color = 'var(--green)';
950
+ setTimeout(() => {
951
+ this.textContent = '📋 Скопировать список для AI-агента';
952
+ this.style.color = 'var(--blue)';
953
+ }, 3000);
954
+ });
955
+ };
956
+
957
+ document.getElementById('panel').classList.add('open');
958
+ }
959
+
729
960
  function closePanel() {
730
961
  activePanelKey = null;
731
962
  document.getElementById('panel').classList.remove('open');
@@ -737,6 +968,7 @@ document.querySelectorAll('.view-tab').forEach(tab => {
737
968
  tab.onclick = () => {
738
969
  if (tab.classList.contains('disabled')) return;
739
970
  view = tab.dataset.view;
971
+ drillFeatureKey = null;
740
972
  activePanelKey = null;
741
973
  searchQuery = '';
742
974
  activeTypes.clear();
@@ -775,10 +1007,15 @@ async function refreshData() {
775
1007
  renderSidebar();
776
1008
  renderContent();
777
1009
 
778
- // Re-open panel if it was open
1010
+ // Re-render drill-down or re-open panel
779
1011
  const panelOpen = document.getElementById('panel').classList.contains('open');
780
1012
  if (panelOpen && activePanelKey) {
781
- if (view === 'features' && D.features) {
1013
+ if (activePanelKey === '__unmapped__') {
1014
+ const unmapped = D.modules.filter(m =>
1015
+ m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
1016
+ );
1017
+ openUnmappedPanel(unmapped);
1018
+ } else if (view === 'features' && D.features) {
782
1019
  openFeaturePanel(activePanelKey);
783
1020
  } else {
784
1021
  const m = D.modules.find(m => m.id === activePanelKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.2",
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": {