Add graph options panel and separate view bar
- Move view tabs (Grid/Tree/Graph) to dedicated bar below topbar - Add graph options panel in left sidebar when in graph view: - Stats: node count, edge count - Category filters: Hashtags, Specs, Values, Visions, Missions, Flags - Relation filters: all edge types with color indicators - Visualization: images toggle, labels toggle, node size slider, link distance slider - Left panel shows libraries in grid/tree view, graph options in graph view Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,11 +55,15 @@
|
|||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input type="text" id="search" class="search-input" placeholder="Buscar...">
|
<input type="text" id="search" class="search-input" placeholder="Buscar...">
|
||||||
</div>
|
</div>
|
||||||
<div class="view-tabs">
|
</div>
|
||||||
<button class="view-tab active" data-view="grid">Grid</button>
|
</div>
|
||||||
<button class="view-tab" data-view="tree">Tree</button>
|
|
||||||
<button class="view-tab" data-view="graph">Graph</button>
|
<!-- VIEW BAR -->
|
||||||
</div>
|
<div class="view-bar">
|
||||||
|
<div class="view-tabs">
|
||||||
|
<button class="view-tab active" data-view="grid">Grid</button>
|
||||||
|
<button class="view-tab" data-view="tree">Tree</button>
|
||||||
|
<button class="view-tab" data-view="graph">Graph</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Router } from '@/router/index.ts';
|
|||||||
import { fetchTags, fetchHstTags, fetchGroups, fetchLibraries, fetchLibraryMembers } from '@/api/index.ts';
|
import { fetchTags, fetchHstTags, fetchGroups, fetchLibraries, fetchLibraryMembers } from '@/api/index.ts';
|
||||||
import { GridView, TreeView, GraphView, DetailPanel } from '@/views/index.ts';
|
import { GridView, TreeView, GraphView, DetailPanel } from '@/views/index.ts';
|
||||||
import { $, $$, delegateEvent, toast, createNameMap, resolveGroupName } from '@/utils/index.ts';
|
import { $, $$, delegateEvent, toast, createNameMap, resolveGroupName } from '@/utils/index.ts';
|
||||||
import type { BaseType, ViewType } from '@/types/index.ts';
|
import { CATS, EDGE_COLORS } from '@/config/index.ts';
|
||||||
|
import type { BaseType, ViewType, CategoryKey, EdgeType } from '@/types/index.ts';
|
||||||
import './styles/main.css';
|
import './styles/main.css';
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
@@ -135,6 +136,14 @@ class App {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Show graph options when in graph view
|
||||||
|
if (state.view === 'graph') {
|
||||||
|
container.classList.add('graph-mode');
|
||||||
|
this.renderGraphOptions(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.classList.remove('graph-mode');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="lib-icon ${state.library === 'all' ? 'active' : ''}" data-lib="all" title="Todos">
|
<div class="lib-icon ${state.library === 'all' ? 'active' : ''}" data-lib="all" title="Todos">
|
||||||
<span>ALL</span>
|
<span>ALL</span>
|
||||||
@@ -167,6 +176,169 @@ class App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderGraphOptions(container: HTMLElement): void {
|
||||||
|
const state = store.getState();
|
||||||
|
const { graphFilters, graphSettings, tags, graphEdges } = state;
|
||||||
|
|
||||||
|
// Count nodes and edges
|
||||||
|
const nodeCount = tags.length;
|
||||||
|
const edgeCount = graphEdges.length;
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="graph-options">
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="graph-section">
|
||||||
|
<div class="graph-stat">
|
||||||
|
<span>Nodos</span>
|
||||||
|
<span class="graph-stat-value">${nodeCount}</span>
|
||||||
|
</div>
|
||||||
|
<div class="graph-stat">
|
||||||
|
<span>Edges</span>
|
||||||
|
<span class="graph-stat-value">${edgeCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="graph-section">
|
||||||
|
<div class="graph-section-title">Categorias</div>
|
||||||
|
${Object.entries(CATS).map(([key, config]) => `
|
||||||
|
<label class="graph-checkbox">
|
||||||
|
<input type="checkbox" data-cat="${key}" ${graphFilters.cats.has(key as CategoryKey) ? 'checked' : ''}>
|
||||||
|
<span class="color-dot" style="background: ${config.color}"></span>
|
||||||
|
${config.name}
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edge Types -->
|
||||||
|
<div class="graph-section">
|
||||||
|
<div class="graph-section-title">Relaciones</div>
|
||||||
|
${Object.entries(EDGE_COLORS).map(([key, color]) => `
|
||||||
|
<label class="graph-checkbox">
|
||||||
|
<input type="checkbox" data-edge="${key}" ${graphFilters.edges.has(key as EdgeType) ? 'checked' : ''}>
|
||||||
|
<span class="color-dot" style="background: ${color}"></span>
|
||||||
|
${key}
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visualization -->
|
||||||
|
<div class="graph-section">
|
||||||
|
<div class="graph-section-title">Visualizacion</div>
|
||||||
|
<label class="graph-checkbox">
|
||||||
|
<input type="checkbox" id="graph-show-img" ${graphSettings.showImg ? 'checked' : ''}>
|
||||||
|
Imagenes
|
||||||
|
</label>
|
||||||
|
<label class="graph-checkbox">
|
||||||
|
<input type="checkbox" id="graph-show-lbl" ${graphSettings.showLbl ? 'checked' : ''}>
|
||||||
|
Etiquetas
|
||||||
|
</label>
|
||||||
|
<div class="graph-slider">
|
||||||
|
<div class="graph-slider-label">
|
||||||
|
<span>Nodo</span>
|
||||||
|
<span class="graph-slider-value" id="node-size-val">${graphSettings.nodeSize}px</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="graph-node-size" min="10" max="60" value="${graphSettings.nodeSize}">
|
||||||
|
</div>
|
||||||
|
<div class="graph-slider">
|
||||||
|
<div class="graph-slider-label">
|
||||||
|
<span>Distancia</span>
|
||||||
|
<span class="graph-slider-value" id="link-dist-val">${graphSettings.linkDist}px</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="graph-link-dist" min="30" max="200" value="${graphSettings.linkDist}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Bind events
|
||||||
|
this.bindGraphOptionEvents(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindGraphOptionEvents(container: HTMLElement): void {
|
||||||
|
// Category checkboxes
|
||||||
|
container.querySelectorAll<HTMLInputElement>('[data-cat]').forEach(cb => {
|
||||||
|
cb.addEventListener('change', () => {
|
||||||
|
const cat = cb.dataset.cat as CategoryKey;
|
||||||
|
const state = store.getState();
|
||||||
|
const newCats = new Set(state.graphFilters.cats);
|
||||||
|
if (cb.checked) {
|
||||||
|
newCats.add(cat);
|
||||||
|
} else {
|
||||||
|
newCats.delete(cat);
|
||||||
|
}
|
||||||
|
store.setState({
|
||||||
|
graphFilters: { ...state.graphFilters, cats: newCats }
|
||||||
|
});
|
||||||
|
this.renderView();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edge checkboxes
|
||||||
|
container.querySelectorAll<HTMLInputElement>('[data-edge]').forEach(cb => {
|
||||||
|
cb.addEventListener('change', () => {
|
||||||
|
const edge = cb.dataset.edge as EdgeType;
|
||||||
|
const state = store.getState();
|
||||||
|
const newEdges = new Set(state.graphFilters.edges);
|
||||||
|
if (cb.checked) {
|
||||||
|
newEdges.add(edge);
|
||||||
|
} else {
|
||||||
|
newEdges.delete(edge);
|
||||||
|
}
|
||||||
|
store.setState({
|
||||||
|
graphFilters: { ...state.graphFilters, edges: newEdges }
|
||||||
|
});
|
||||||
|
this.renderView();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show images checkbox
|
||||||
|
const showImgCb = container.querySelector<HTMLInputElement>('#graph-show-img');
|
||||||
|
showImgCb?.addEventListener('change', () => {
|
||||||
|
const state = store.getState();
|
||||||
|
store.setState({
|
||||||
|
graphSettings: { ...state.graphSettings, showImg: showImgCb.checked }
|
||||||
|
});
|
||||||
|
this.renderView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show labels checkbox
|
||||||
|
const showLblCb = container.querySelector<HTMLInputElement>('#graph-show-lbl');
|
||||||
|
showLblCb?.addEventListener('change', () => {
|
||||||
|
const state = store.getState();
|
||||||
|
store.setState({
|
||||||
|
graphSettings: { ...state.graphSettings, showLbl: showLblCb.checked }
|
||||||
|
});
|
||||||
|
this.renderView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Node size slider
|
||||||
|
const nodeSizeSlider = container.querySelector<HTMLInputElement>('#graph-node-size');
|
||||||
|
const nodeSizeVal = container.querySelector('#node-size-val');
|
||||||
|
nodeSizeSlider?.addEventListener('input', () => {
|
||||||
|
const size = parseInt(nodeSizeSlider.value, 10);
|
||||||
|
if (nodeSizeVal) nodeSizeVal.textContent = `${size}px`;
|
||||||
|
const state = store.getState();
|
||||||
|
store.setState({
|
||||||
|
graphSettings: { ...state.graphSettings, nodeSize: size }
|
||||||
|
});
|
||||||
|
this.renderView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Link distance slider
|
||||||
|
const linkDistSlider = container.querySelector<HTMLInputElement>('#graph-link-dist');
|
||||||
|
const linkDistVal = container.querySelector('#link-dist-val');
|
||||||
|
linkDistSlider?.addEventListener('input', () => {
|
||||||
|
const dist = parseInt(linkDistSlider.value, 10);
|
||||||
|
if (linkDistVal) linkDistVal.textContent = `${dist}px`;
|
||||||
|
const state = store.getState();
|
||||||
|
store.setState({
|
||||||
|
graphSettings: { ...state.graphSettings, linkDist: dist }
|
||||||
|
});
|
||||||
|
this.renderView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private updateBaseButtons(): void {
|
private updateBaseButtons(): void {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
$$('.base-btn').forEach(btn => {
|
$$('.base-btn').forEach(btn => {
|
||||||
@@ -212,6 +384,7 @@ class App {
|
|||||||
this.router.updateHash();
|
this.router.updateHash();
|
||||||
this.detailPanel?.close();
|
this.detailPanel?.close();
|
||||||
this.updateViewTabs();
|
this.updateViewTabs();
|
||||||
|
this.renderLibraries(); // Update left panel (graph options vs libraries)
|
||||||
this.renderView();
|
this.renderView();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ body {
|
|||||||
.base-btn:hover { color: var(--text); }
|
.base-btn:hover { color: var(--text); }
|
||||||
.base-btn.active { background: var(--accent); color: #fff; }
|
.base-btn.active { background: var(--accent); color: #fff; }
|
||||||
|
|
||||||
|
/* === VIEW BAR === */
|
||||||
|
.view-bar {
|
||||||
|
height: 40px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* === GROUPS BAR === */
|
/* === GROUPS BAR === */
|
||||||
.groups-bar {
|
.groups-bar {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
@@ -573,3 +585,89 @@ select {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
select:focus { outline: none; border-color: var(--accent); }
|
select:focus { outline: none; border-color: var(--accent); }
|
||||||
|
|
||||||
|
/* === GRAPH OPTIONS PANEL === */
|
||||||
|
.graph-options {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
.graph-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.graph-section-title {
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.graph-stat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.graph-stat-value {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.graph-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.graph-checkbox:hover { color: var(--text); }
|
||||||
|
.graph-checkbox input[type="checkbox"] {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.graph-checkbox .color-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.graph-slider {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.graph-slider-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.graph-slider-value {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.graph-slider input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.graph-slider input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.left-panel.graph-mode {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user