Files
captain-claude/deck-frontend/backups/20260113_211524/src/modules/standard/StandardModule.ts
ARCHITECT 9b244138b5 Add pending apps and frontend components
- apps/captain-mobile: Mobile API service
- apps/flow-ui: Flow UI application
- apps/mindlink: Mindlink application
- apps/storage: Storage API and workers
- apps/tzzr-cli: TZZR CLI tool
- deck-frontend/backups: Historical TypeScript versions
- hst-frontend: Standalone HST frontend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 18:26:59 +00:00

324 lines
10 KiB
TypeScript

/**
* StandardModule - Módulo estándar para bases con vistas Grid/Tree/Graph
*
* Usado por: taxonomía (hst, flg, itm, loc, ply), maestros (mst, bck),
* registro (atc, mth)
*/
import { BaseModule } from '../registry.ts';
import { GridView, TreeView, GraphView } from '@/views/index.ts';
import { fetchTags, fetchHstTags, fetchGroups, fetchLibraries, fetchLibraryMembers } from '@/api/index.ts';
import { createNameMap, resolveGroupName, delegateEvent } from '@/utils/index.ts';
import type { ViewType } from '@/types/index.ts';
export class StandardModule extends BaseModule {
private currentView: GridView | TreeView | GraphView | null = null;
async mount(): Promise<void> {
// Show loading
this.ctx.container.innerHTML = '<div class="loading">Cargando...</div>';
// Load data
await this.loadData();
// Render sidebar and groups
this.renderSidebar();
this.renderGroupsBar();
// Render main view
this.render();
this.mounted = true;
}
unmount(): void {
this.currentView?.unmount();
this.currentView = null;
this.unsubscribe?.();
this.unsubscribe = null;
this.mounted = false;
}
async loadData(): Promise<void> {
const config = this.getConfig();
// Fetch tags para esta base
const tags = await fetchTags(config.id);
// Fetch HST tags para resolución de nombres de grupos (si tiene grupos)
const hstTags = config.api.hasGroups
? await fetchHstTags()
: [];
// Fetch grupos (solo si esta base los soporta)
const groups = config.api.hasGroups
? await fetchGroups()
: [];
// Fetch bibliotecas (solo si esta base las soporta)
const libraries = config.api.hasLibraries
? await fetchLibraries(config.id)
: [];
this.setState({
tags,
hstTags,
groups,
libraries,
library: 'all',
libraryMembers: new Set(),
group: 'all'
});
}
render(): void {
const state = this.getState();
const config = this.getConfig();
// Verificar que la vista está soportada
if (!this.isViewSupported(state.view)) {
// Cambiar a vista por defecto
const defaultView = config.defaultView as ViewType;
this.setState({ view: defaultView });
return;
}
// Unmount current view
this.currentView?.unmount();
// Clear container
this.ctx.container.innerHTML = '';
this.ctx.container.className = `content-area ${state.view}-view`;
// Mount new view
switch (state.view) {
case 'grid':
this.currentView = new GridView(this.ctx.container, this.ctx.store, this.ctx.showDetail);
this.currentView.mount();
break;
case 'tree':
if (config.views.tree) {
this.currentView = new TreeView(this.ctx.container, this.ctx.store, this.ctx.showDetail);
this.currentView.mount();
}
break;
case 'graph':
if (config.views.graph) {
this.currentView = new GraphView(this.ctx.container, this.ctx.store, this.ctx.showDetail);
(this.currentView as GraphView).mount();
}
break;
}
}
renderSidebar(): void {
const container = this.ctx.leftPanel;
const state = this.getState();
const config = this.getConfig();
// Si es vista de grafo, mostrar opciones de grafo
if (state.view === 'graph' && config.views.graph) {
container.classList.add('graph-mode');
this.renderGraphOptions(container);
return;
}
container.classList.remove('graph-mode');
// Si no tiene bibliotecas, vaciar sidebar
if (!config.api.hasLibraries) {
container.innerHTML = '<div class="sidebar-empty">Sin bibliotecas</div>';
return;
}
// Ordenar bibliotecas alfabéticamente
const sortedLibs = [...state.libraries].sort((a, b) => {
const nameA = a.name || a.name_es || a.alias || a.ref || '';
const nameB = b.name || b.name_es || b.alias || b.ref || '';
return nameA.localeCompare(nameB);
});
// Renderizar bibliotecas (simple - sin config por ahora)
container.innerHTML = `
<div class="lib-icon ${state.library === 'all' ? 'active' : ''}" data-lib="all" title="Todos">
<span>ALL</span>
</div>
${sortedLibs.map(lib => {
const icon = lib.img_thumb_url || lib.icon_url || '';
const name = lib.name || lib.name_es || lib.alias || lib.ref || lib.mrf.slice(0, 6);
return `
<div class="lib-icon ${state.library === lib.mrf ? 'active' : ''}" data-lib="${lib.mrf}" title="${name}">
${icon ? `<img src="${icon}" alt="">` : ''}
<span>${name.slice(0, 8)}</span>
</div>
`;
}).join('')}
`;
// Bind library clicks
delegateEvent<MouseEvent>(container, '.lib-icon', 'click', async (_, target) => {
const library = target.dataset.lib || 'all';
if (library === 'all') {
this.setState({ library: 'all', libraryMembers: new Set() });
} else {
const currentBase = this.getState().base;
const members = await fetchLibraryMembers(library, currentBase);
this.setState({ library, libraryMembers: new Set(members) });
}
this.renderSidebar();
this.render();
});
}
renderGroupsBar(): void {
const container = this.ctx.groupsBar;
const state = this.getState();
const config = this.getConfig();
// Si no tiene grupos, vaciar
if (!config.api.hasGroups) {
container.innerHTML = '';
return;
}
// Usar hstTags para resolución de nombres
const nameMap = createNameMap(state.hstTags, state.lang);
// Contar tags por grupo
const counts = new Map<string, number>();
state.tags.forEach(tag => {
const group = tag.set_hst || 'sin-grupo';
counts.set(group, (counts.get(group) || 0) + 1);
});
// Ordenar por count y tomar top 20
const sorted = Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 20);
container.innerHTML = `
<button class="group-btn ${state.group === 'all' ? 'active' : ''}" data-group="all">
Todos (${state.tags.length})
</button>
${sorted.map(([groupMrf, count]) => {
const groupName = resolveGroupName(groupMrf === 'sin-grupo' ? undefined : groupMrf, nameMap);
return `
<button class="group-btn ${state.group === groupMrf ? 'active' : ''}" data-group="${groupMrf}">
${groupName} (${count})
</button>
`;
}).join('')}
`;
// Bind group clicks
delegateEvent<MouseEvent>(container, '.group-btn', 'click', (_, target) => {
const group = target.dataset.group || 'all';
this.setState({ group });
this.renderGroupsBar();
this.render();
});
}
private renderGraphOptions(container: HTMLElement): void {
// TODO: Extraer a componente separado
const state = this.getState();
const { graphSettings, tags, graphEdges } = state;
container.innerHTML = `
<div class="graph-options">
<div class="graph-section">
<div class="graph-stat">
<span>Nodos</span>
<span class="graph-stat-value">${tags.length}</span>
</div>
<div class="graph-stat">
<span>Edges</span>
<span class="graph-stat-value">${graphEdges.length}</span>
</div>
</div>
<div class="graph-section">
<div class="graph-section-title">Visualización</div>
<label class="graph-checkbox">
<input type="checkbox" id="graph-show-img" ${graphSettings.showImg ? 'checked' : ''}>
Imágenes
</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>
`;
this.bindGraphOptionEvents(container);
}
private bindGraphOptionEvents(container: HTMLElement): void {
// Show images checkbox
const showImgCb = container.querySelector<HTMLInputElement>('#graph-show-img');
showImgCb?.addEventListener('change', () => {
const state = this.getState();
this.setState({
graphSettings: { ...state.graphSettings, showImg: showImgCb.checked }
});
this.render();
});
// Show labels checkbox
const showLblCb = container.querySelector<HTMLInputElement>('#graph-show-lbl');
showLblCb?.addEventListener('change', () => {
const state = this.getState();
this.setState({
graphSettings: { ...state.graphSettings, showLbl: showLblCb.checked }
});
this.render();
});
// 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 = this.getState();
this.setState({
graphSettings: { ...state.graphSettings, nodeSize: size }
});
this.render();
});
// 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 = this.getState();
this.setState({
graphSettings: { ...state.graphSettings, linkDist: dist }
});
this.render();
});
}
}
export default StandardModule;