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.
- package/dist/ui/dashboard.html +241 -4
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -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'
|
|
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 = () =>
|
|
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-
|
|
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 (
|
|
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);
|