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,13 +55,17 @@
|
||||
<div class="search-box">
|
||||
<input type="text" id="search" class="search-input" placeholder="Buscar...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VIEW BAR -->
|
||||
<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>
|
||||
|
||||
<!-- GROUPS BAR -->
|
||||
<div id="groups-bar" class="groups-bar"></div>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Router } from '@/router/index.ts';
|
||||
import { fetchTags, fetchHstTags, fetchGroups, fetchLibraries, fetchLibraryMembers } from '@/api/index.ts';
|
||||
import { GridView, TreeView, GraphView, DetailPanel } from '@/views/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';
|
||||
|
||||
class App {
|
||||
@@ -135,6 +136,14 @@ class App {
|
||||
|
||||
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 = `
|
||||
<div class="lib-icon ${state.library === 'all' ? 'active' : ''}" data-lib="all" title="Todos">
|
||||
<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 {
|
||||
const state = store.getState();
|
||||
$$('.base-btn').forEach(btn => {
|
||||
@@ -212,6 +384,7 @@ class App {
|
||||
this.router.updateHash();
|
||||
this.detailPanel?.close();
|
||||
this.updateViewTabs();
|
||||
this.renderLibraries(); // Update left panel (graph options vs libraries)
|
||||
this.renderView();
|
||||
});
|
||||
|
||||
|
||||
@@ -87,6 +87,18 @@ body {
|
||||
.base-btn:hover { color: var(--text); }
|
||||
.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 {
|
||||
height: 44px;
|
||||
@@ -573,3 +585,89 @@ select {
|
||||
cursor: pointer;
|
||||
}
|
||||
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