feat: header graph repensé + filtre tag + Ctrl+survol aperçu contenu
All checks were successful
CI / lint (push) Successful in 13s
CI / security (push) Successful in 8s
CI / test (push) Successful in 14s
CI / build (push) Successful in 2s

This commit is contained in:
Bruno Charest 2026-05-29 22:58:22 -04:00
parent 5a76a48e28
commit 3de990cf7d
2 changed files with 87 additions and 13 deletions

View File

@ -700,14 +700,19 @@
<!-- Graph View Modal -->
<div class="editor-modal" id="graph-modal">
<div class="editor-container" style="max-width:95vw;width:1000px;height:85vh;display:flex;flex-direction:column;">
<div class="editor-header">
<div class="editor-title" id="graph-title">Vue Graphique</div>
<div class="editor-actions">
<span id="graph-info" style="font-size:0.75rem;color:var(--text-muted);margin-right:8px"></span>
<label style="font-size:0.7rem;color:var(--text-muted);margin-right:4px">Profondeur:</label>
<input type="range" id="graph-depth" min="0" max="3" value="1" style="width:60px;margin-right:8px" title="Profondeur d'exploration">
<button class="editor-btn" id="graph-full-vault" title="Vue complète du vault" aria-label="Vault complet" style="font-size:0.7rem;margin-right:4px">🌐 Tout</button>
<input type="text" id="graph-search" placeholder="Rechercher..." style="width:100px;font-size:0.75rem;padding:2px 6px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary);margin-right:8px">
<div class="editor-header" style="flex-wrap:wrap;gap:4px">
<div class="editor-actions" style="flex:1;min-width:0;display:flex;align-items:center;gap:4px;flex-wrap:wrap">
<span id="graph-info" style="font-size:0.75rem;color:var(--text-muted);margin-right:4px;white-space:nowrap"></span>
<label style="font-size:0.7rem;color:var(--text-muted);margin-right:2px">Profondeur:</label>
<input type="range" id="graph-depth" min="0" max="3" value="1" style="width:50px" title="Profondeur d'exploration">
<button class="editor-btn" id="graph-full-vault" title="Vue complète du vault" aria-label="Vault complet" style="font-size:0.7rem">🌐 Tout</button>
<span style="color:var(--border-color);margin:0 2px"></span>
<i data-lucide="search" style="width:14px;height:14px;color:var(--text-muted);flex-shrink:0"></i>
<input type="text" id="graph-search" placeholder="Rechercher un nœud..." style="width:160px;font-size:0.75rem;padding:2px 8px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary)">
<i data-lucide="tag" style="width:14px;height:14px;color:var(--text-muted);flex-shrink:0;margin-left:4px"></i>
<input type="text" id="graph-tag-filter" placeholder="Filtrer par tag..." style="width:120px;font-size:0.75rem;padding:2px 8px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-secondary);color:var(--text-primary)">
<span style="font-size:0.65rem;color:var(--text-muted);margin-left:4px;opacity:0.6">Ctrl+survol = aperçu</span>
<span style="flex:1"></span>
<button class="editor-btn" id="graph-zoom-in" title="Zoom avant" aria-label="Zoom avant">
<i data-lucide="zoom-in" style="width:16px;height:16px"></i>
</button>
@ -727,6 +732,7 @@
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
</div>
<div class="editor-title" id="graph-title" style="width:100%;font-size:0.85rem;padding-top:2px;border-top:1px solid var(--border-color);margin-top:2px">Vue Graphique</div>
</div>
<div class="editor-body" style="flex:1;overflow:hidden;position:relative;padding:0">
<canvas id="graph-canvas" style="width:100%;height:100%;cursor:grab"></canvas>

View File

@ -50,8 +50,12 @@ export const GraphViewManager = {
_scope: 'directory',
_depth: 1,
_searchTerm: '',
_tagFilter: '',
_hoveredNode: null,
_tooltipEl: null,
_previewCache: {},
_previewPending: null,
_ctrlHoverNode: null,
async open(vault, path, type) {
this._vault = vault;
@ -85,7 +89,7 @@ export const GraphViewManager = {
_cacheKey: '',
_getCacheKey() {
return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}`;
return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}|${this._tagFilter}`;
},
async _fetchAndRender() {
@ -111,7 +115,7 @@ export const GraphViewManager = {
if (this._path) params.set('path', this._path);
params.set('depth', String(this._depth));
params.set('scope', this._scope);
if (this._searchTerm) params.set('tag', this._searchTerm);
if (this._tagFilter) params.set('tag', this._tagFilter);
try {
const data = await api(
@ -167,6 +171,11 @@ export const GraphViewManager = {
}
},
setTagFilter(tag) {
this._tagFilter = tag;
this.reload();
},
close() {
const modal = document.getElementById('graph-modal');
if (modal) modal.classList.remove('active');
@ -511,8 +520,9 @@ export const GraphViewManager = {
return null;
},
_showTooltip(node, screenX, screenY) {
_showTooltip(node, screenX, screenY, isPreview) {
if (!this._tooltipEl) return;
if (isPreview) return; // Preview tooltip handled by _showPreviewTooltip
const tags = (node.tags || []).slice(0, 5).join(', ');
const inc = node.incoming_count || 0;
const out = node.outgoing_count || 0;
@ -521,8 +531,10 @@ export const GraphViewManager = {
${node.type === 'file' ? `<br><span style="color:${COLORS.muted};font-size:0.7rem">${node.path}</span>` : ''}
${tags ? `<br>🏷️ ${tags}` : ''}
${inc + out > 0 ? `<br>🔗 ${out} sortants · ${inc} entrants` : ''}
<br><span style="color:${COLORS.muted};font-size:0.6rem;opacity:0.7">Ctrl+survol pour aperçu</span>
`;
this._tooltipEl.style.display = 'block';
this._tooltipEl.style.maxWidth = '250px';
this._tooltipEl.style.left = (screenX + 15) + 'px';
this._tooltipEl.style.top = (screenY - 10) + 'px';
},
@ -609,17 +621,63 @@ export const GraphViewManager = {
if (hit) {
this._canvas.style.cursor = 'pointer';
this._canvas.title = hit.node.type === 'file'
? `📄 ${hit.node.name} (cliquer pour ouvrir)`
? `📄 ${hit.node.name} (cliquer pour ouvrir${e.ctrlKey ? ', Ctrl+survol = aperçu' : ''})`
: `📁 ${hit.node.name} (cliquer pour explorer)`;
this._showTooltip(hit.node, e.clientX, e.clientY);
// Ctrl+hover: fetch content preview for file nodes
if (e.ctrlKey && hit.node.type === 'file') {
if (this._ctrlHoverNode !== hit.node) {
this._ctrlHoverNode = hit.node;
this._fetchPreview(hit.node, e.clientX, e.clientY);
}
this._showTooltip(hit.node, e.clientX, e.clientY, true);
} else {
this._ctrlHoverNode = null;
this._showTooltip(hit.node, e.clientX, e.clientY, false);
}
} else {
this._canvas.style.cursor = 'grab';
this._canvas.title = '';
this._ctrlHoverNode = null;
this._hideTooltip();
}
}
},
async _fetchPreview(node, screenX, screenY) {
const cacheKey = `${this._vault}:${node.path}`;
if (this._previewCache[cacheKey] !== undefined) {
this._showPreviewTooltip(node, this._previewCache[cacheKey], screenX, screenY);
return;
}
// Show loading indicator
this._showPreviewTooltip(node, 'Chargement...', screenX, screenY);
try {
const data = await api(`/api/file/${encodeURIComponent(this._vault)}?path=${encodeURIComponent(node.path)}`);
const content = (data.content || '').substring(0, 400).replace(/\\n/g, ' ');
this._previewCache[cacheKey] = content;
this._showPreviewTooltip(node, content, screenX, screenY);
} catch {
this._previewCache[cacheKey] = '(impossible de charger le contenu)';
this._showPreviewTooltip(node, '(impossible de charger le contenu)', screenX, screenY);
}
},
_showPreviewTooltip(node, preview, screenX, screenY) {
if (!this._tooltipEl || this._ctrlHoverNode !== node) return;
const tags = (node.tags || []).slice(0, 3).join(', ');
this._tooltipEl.innerHTML = `
<strong>${node.name}</strong>
<span style="color:${COLORS.muted};font-size:0.65rem;display:block;margin:2px 0">${node.path}</span>
${tags ? `<span style="color:${COLORS.accent};font-size:0.65rem">🏷️ ${tags}</span>` : ''}
<div style="margin-top:4px;padding-top:4px;border-top:1px solid ${COLORS.border};font-size:0.7rem;color:${COLORS.text};line-height:1.4;max-height:120px;overflow:hidden">${preview}</div>
`;
this._tooltipEl.style.display = 'block';
this._tooltipEl.style.maxWidth = '350px';
this._tooltipEl.style.left = Math.min(screenX + 15, window.innerWidth - 370) + 'px';
this._tooltipEl.style.top = Math.min(screenY - 10, window.innerHeight - 180) + 'px';
},
_onMouseUp(e) {
if (this._dragging && this._dragNode) {
const node = this._dragNode.node;
@ -698,6 +756,16 @@ export function initGraphView() {
});
}
// Tag filter input
const tagFilterInput = document.getElementById('graph-tag-filter');
if (tagFilterInput) {
let tagDebounce;
tagFilterInput.addEventListener('input', () => {
clearTimeout(tagDebounce);
tagDebounce = setTimeout(() => gm.setTagFilter(tagFilterInput.value.trim()), 400);
});
}
// Export PNG
const exportBtn = document.getElementById('graph-export');
if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());