feat: header graph repensé + filtre tag + Ctrl+survol aperçu contenu
This commit is contained in:
parent
5a76a48e28
commit
3de990cf7d
@ -700,14 +700,19 @@
|
|||||||
<!-- Graph View Modal -->
|
<!-- Graph View Modal -->
|
||||||
<div class="editor-modal" id="graph-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-container" style="max-width:95vw;width:1000px;height:85vh;display:flex;flex-direction:column;">
|
||||||
<div class="editor-header">
|
<div class="editor-header" style="flex-wrap:wrap;gap:4px">
|
||||||
<div class="editor-title" id="graph-title">Vue Graphique</div>
|
<div class="editor-actions" style="flex:1;min-width:0;display:flex;align-items:center;gap:4px;flex-wrap:wrap">
|
||||||
<div class="editor-actions">
|
<span id="graph-info" style="font-size:0.75rem;color:var(--text-muted);margin-right:4px;white-space:nowrap"></span>
|
||||||
<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:2px">Profondeur:</label>
|
||||||
<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:50px" title="Profondeur d'exploration">
|
||||||
<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">🌐 Tout</button>
|
||||||
<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>
|
<span style="color:var(--border-color);margin:0 2px">│</span>
|
||||||
<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">
|
<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">
|
<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>
|
<i data-lucide="zoom-in" style="width:16px;height:16px"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -727,6 +732,7 @@
|
|||||||
<i data-lucide="x" style="width:16px;height:16px"></i>
|
<i data-lucide="x" style="width:16px;height:16px"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
<div class="editor-body" style="flex:1;overflow:hidden;position:relative;padding:0">
|
<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>
|
<canvas id="graph-canvas" style="width:100%;height:100%;cursor:grab"></canvas>
|
||||||
|
|||||||
@ -50,8 +50,12 @@ export const GraphViewManager = {
|
|||||||
_scope: 'directory',
|
_scope: 'directory',
|
||||||
_depth: 1,
|
_depth: 1,
|
||||||
_searchTerm: '',
|
_searchTerm: '',
|
||||||
|
_tagFilter: '',
|
||||||
_hoveredNode: null,
|
_hoveredNode: null,
|
||||||
_tooltipEl: null,
|
_tooltipEl: null,
|
||||||
|
_previewCache: {},
|
||||||
|
_previewPending: null,
|
||||||
|
_ctrlHoverNode: null,
|
||||||
|
|
||||||
async open(vault, path, type) {
|
async open(vault, path, type) {
|
||||||
this._vault = vault;
|
this._vault = vault;
|
||||||
@ -85,7 +89,7 @@ export const GraphViewManager = {
|
|||||||
_cacheKey: '',
|
_cacheKey: '',
|
||||||
|
|
||||||
_getCacheKey() {
|
_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() {
|
async _fetchAndRender() {
|
||||||
@ -111,7 +115,7 @@ export const GraphViewManager = {
|
|||||||
if (this._path) params.set('path', this._path);
|
if (this._path) params.set('path', this._path);
|
||||||
params.set('depth', String(this._depth));
|
params.set('depth', String(this._depth));
|
||||||
params.set('scope', this._scope);
|
params.set('scope', this._scope);
|
||||||
if (this._searchTerm) params.set('tag', this._searchTerm);
|
if (this._tagFilter) params.set('tag', this._tagFilter);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api(
|
const data = await api(
|
||||||
@ -167,6 +171,11 @@ export const GraphViewManager = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setTagFilter(tag) {
|
||||||
|
this._tagFilter = tag;
|
||||||
|
this.reload();
|
||||||
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
const modal = document.getElementById('graph-modal');
|
const modal = document.getElementById('graph-modal');
|
||||||
if (modal) modal.classList.remove('active');
|
if (modal) modal.classList.remove('active');
|
||||||
@ -511,8 +520,9 @@ export const GraphViewManager = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
_showTooltip(node, screenX, screenY) {
|
_showTooltip(node, screenX, screenY, isPreview) {
|
||||||
if (!this._tooltipEl) return;
|
if (!this._tooltipEl) return;
|
||||||
|
if (isPreview) return; // Preview tooltip handled by _showPreviewTooltip
|
||||||
const tags = (node.tags || []).slice(0, 5).join(', ');
|
const tags = (node.tags || []).slice(0, 5).join(', ');
|
||||||
const inc = node.incoming_count || 0;
|
const inc = node.incoming_count || 0;
|
||||||
const out = node.outgoing_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>` : ''}
|
${node.type === 'file' ? `<br><span style="color:${COLORS.muted};font-size:0.7rem">${node.path}</span>` : ''}
|
||||||
${tags ? `<br>🏷️ ${tags}` : ''}
|
${tags ? `<br>🏷️ ${tags}` : ''}
|
||||||
${inc + out > 0 ? `<br>🔗 ${out} sortants · ${inc} entrants` : ''}
|
${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.display = 'block';
|
||||||
|
this._tooltipEl.style.maxWidth = '250px';
|
||||||
this._tooltipEl.style.left = (screenX + 15) + 'px';
|
this._tooltipEl.style.left = (screenX + 15) + 'px';
|
||||||
this._tooltipEl.style.top = (screenY - 10) + 'px';
|
this._tooltipEl.style.top = (screenY - 10) + 'px';
|
||||||
},
|
},
|
||||||
@ -609,17 +621,63 @@ export const GraphViewManager = {
|
|||||||
if (hit) {
|
if (hit) {
|
||||||
this._canvas.style.cursor = 'pointer';
|
this._canvas.style.cursor = 'pointer';
|
||||||
this._canvas.title = hit.node.type === 'file'
|
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)`;
|
: `📁 ${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 {
|
} else {
|
||||||
this._canvas.style.cursor = 'grab';
|
this._canvas.style.cursor = 'grab';
|
||||||
this._canvas.title = '';
|
this._canvas.title = '';
|
||||||
|
this._ctrlHoverNode = null;
|
||||||
this._hideTooltip();
|
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) {
|
_onMouseUp(e) {
|
||||||
if (this._dragging && this._dragNode) {
|
if (this._dragging && this._dragNode) {
|
||||||
const node = this._dragNode.node;
|
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
|
// Export PNG
|
||||||
const exportBtn = document.getElementById('graph-export');
|
const exportBtn = document.getElementById('graph-export');
|
||||||
if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());
|
if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user