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:
ARCHITECT
2026-01-13 00:59:00 +00:00
parent a0b20fc5db
commit 79b7389f1f
3 changed files with 281 additions and 6 deletions

View File

@@ -55,13 +55,17 @@
<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>
</div>
<!-- VIEW BAR -->
<div class="view-bar">
<div class="view-tabs"> <div class="view-tabs">
<button class="view-tab active" data-view="grid">Grid</button> <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="tree">Tree</button>
<button class="view-tab" data-view="graph">Graph</button> <button class="view-tab" data-view="graph">Graph</button>
</div> </div>
</div> </div>
</div>
<!-- GROUPS BAR --> <!-- GROUPS BAR -->
<div id="groups-bar" class="groups-bar"></div> <div id="groups-bar" class="groups-bar"></div>

View File

@@ -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();
}); });

View File

@@ -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;
}