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>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { View } from '../View.ts';
|
||||
import { getName, getFullImg, copyMrf, delegateEvent } from '@/utils/index.ts';
|
||||
import { fetchChildren, fetchRelated } from '@/api/index.ts';
|
||||
import type { Store } from '@/state/store.ts';
|
||||
import type { AppState, Tag } from '@/types/index.ts';
|
||||
|
||||
export class DetailPanel extends View {
|
||||
private panelEl: HTMLElement;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
store: Store<AppState>
|
||||
) {
|
||||
super(container, store);
|
||||
this.panelEl = container;
|
||||
}
|
||||
|
||||
async showDetail(mrf: string): Promise<void> {
|
||||
const state = this.getState();
|
||||
const tag = state.tags.find(t => t.mrf === mrf);
|
||||
if (!tag) return;
|
||||
|
||||
this.setState({ selectedTag: tag });
|
||||
this.panelEl.classList.add('open');
|
||||
await this.renderDetail(tag);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.panelEl.classList.remove('open');
|
||||
this.setState({ selectedTag: null });
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const state = this.getState();
|
||||
if (state.selectedTag) {
|
||||
this.renderDetail(state.selectedTag);
|
||||
}
|
||||
}
|
||||
|
||||
private async renderDetail(tag: Tag): Promise<void> {
|
||||
const state = this.getState();
|
||||
const img = getFullImg(tag);
|
||||
const name = getName(tag, state.lang);
|
||||
|
||||
this.panelEl.innerHTML = `
|
||||
<div class="detail-header">
|
||||
${img
|
||||
? `<img class="detail-img" src="${img}" alt="${tag.ref}">`
|
||||
: `<div class="detail-placeholder">${tag.ref?.slice(0, 2) || 'T'}</div>`
|
||||
}
|
||||
<button class="detail-close">×</button>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div class="detail-ref">${tag.ref || ''}</div>
|
||||
<div class="detail-mrf" data-mrf="${tag.mrf}">${tag.mrf}</div>
|
||||
<div class="detail-name">${name}</div>
|
||||
<div class="detail-desc">${tag.txt || tag.alias || ''}</div>
|
||||
<div id="children-section" class="detail-section" style="display:none">
|
||||
<h4>Hijos</h4>
|
||||
<div id="children-list" class="chip-list"></div>
|
||||
</div>
|
||||
<div id="related-section" class="detail-section" style="display:none">
|
||||
<h4>Relacionados</h4>
|
||||
<div id="related-list" class="chip-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.bindDetailEvents();
|
||||
await this.loadRelations(tag.mrf);
|
||||
}
|
||||
|
||||
private bindDetailEvents(): void {
|
||||
const closeBtn = this.panelEl.querySelector('.detail-close');
|
||||
closeBtn?.addEventListener('click', () => this.close());
|
||||
|
||||
const mrfEl = this.panelEl.querySelector('.detail-mrf');
|
||||
mrfEl?.addEventListener('click', () => {
|
||||
const mrf = (mrfEl as HTMLElement).dataset.mrf;
|
||||
if (mrf) copyMrf(mrf);
|
||||
});
|
||||
|
||||
delegateEvent<MouseEvent>(this.panelEl, '.tag-chip', 'click', (_, target) => {
|
||||
const mrf = target.dataset.mrf;
|
||||
if (mrf) this.showDetail(mrf);
|
||||
});
|
||||
}
|
||||
|
||||
private async loadRelations(mrf: string): Promise<void> {
|
||||
const [children, related] = await Promise.all([
|
||||
fetchChildren(mrf),
|
||||
fetchRelated(mrf)
|
||||
]);
|
||||
|
||||
const childrenSection = this.panelEl.querySelector('#children-section') as HTMLElement;
|
||||
const childrenList = this.panelEl.querySelector('#children-list') as HTMLElement;
|
||||
if (children.length > 0) {
|
||||
childrenSection.style.display = 'block';
|
||||
childrenList.innerHTML = children.map(c => {
|
||||
const label = c.name_es || c.alias || c.ref || c.mrf.slice(0, 8);
|
||||
return `<span class="tag-chip" data-mrf="${c.mrf}">${label}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const relatedSection = this.panelEl.querySelector('#related-section') as HTMLElement;
|
||||
const relatedList = this.panelEl.querySelector('#related-list') as HTMLElement;
|
||||
if (related.length > 0) {
|
||||
relatedSection.style.display = 'block';
|
||||
relatedList.innerHTML = related.map(r => {
|
||||
const label = r.name_es || r.alias || r.ref || r.mrf.slice(0, 8);
|
||||
return `<span class="tag-chip" data-mrf="${r.mrf}" title="${r.edge_type}">${label}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { View } from '../View.ts';
|
||||
import { filterTags, getName, getImg } from '@/utils/index.ts';
|
||||
import { fetchGraphEdges, fetchTreeEdges } from '@/api/index.ts';
|
||||
import { CATS, EDGE_COLORS } from '@/config/index.ts';
|
||||
import type { Store } from '@/state/store.ts';
|
||||
import type { AppState, GraphNode, CategoryKey, EdgeType } from '@/types/index.ts';
|
||||
|
||||
type D3Module = typeof import('d3');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type D3Selection = any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type D3Simulation = any;
|
||||
|
||||
export class GraphView extends View {
|
||||
private d3: D3Module | null = null;
|
||||
private simulation: D3Simulation | null = null;
|
||||
private showDetail: (mrf: string) => void;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
store: Store<AppState>,
|
||||
showDetail: (mrf: string) => void
|
||||
) {
|
||||
super(container, store);
|
||||
this.showDetail = showDetail;
|
||||
}
|
||||
|
||||
async mount(): Promise<void> {
|
||||
this.container.innerHTML = '<div class="loading">Cargando grafo...</div>';
|
||||
|
||||
// Lazy load D3
|
||||
if (!this.d3) {
|
||||
this.d3 = await import('d3');
|
||||
}
|
||||
|
||||
// Load graph data
|
||||
const state = this.getState();
|
||||
if (state.graphEdges.length === 0) {
|
||||
const [graphEdges, treeEdges] = await Promise.all([
|
||||
fetchGraphEdges(),
|
||||
fetchTreeEdges()
|
||||
]);
|
||||
this.store.setState({ graphEdges, treeEdges });
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render(): void {
|
||||
if (!this.d3) return;
|
||||
const d3 = this.d3;
|
||||
const state = this.getState();
|
||||
|
||||
// Build nodes from filtered tags
|
||||
const filtered = filterTags(state.tags, {
|
||||
search: state.search,
|
||||
group: state.group,
|
||||
library: state.library,
|
||||
libraryMembers: state.libraryMembers,
|
||||
lang: state.lang
|
||||
});
|
||||
|
||||
const nodeMap = new Map<string, GraphNode>();
|
||||
filtered.forEach(tag => {
|
||||
// Determinar categoría del tag (por prefijo de mrf o default 'hst')
|
||||
const tagCat = this.getTagCategory(tag.mrf);
|
||||
|
||||
// Filtrar por categorías activas
|
||||
if (!state.graphFilters.cats.has(tagCat)) return;
|
||||
|
||||
nodeMap.set(tag.mrf, {
|
||||
id: tag.mrf,
|
||||
ref: tag.alias || tag.ref || tag.mrf.slice(0, 8),
|
||||
name: getName(tag, state.lang),
|
||||
img: getImg(tag),
|
||||
cat: tagCat
|
||||
});
|
||||
});
|
||||
|
||||
// Build edges
|
||||
interface GraphLink {
|
||||
source: string | GraphNode;
|
||||
target: string | GraphNode;
|
||||
type: EdgeType;
|
||||
weight: number;
|
||||
}
|
||||
const edges: GraphLink[] = [];
|
||||
|
||||
state.graphEdges.forEach(e => {
|
||||
if (nodeMap.has(e.mrf_a) && nodeMap.has(e.mrf_b)) {
|
||||
if (state.graphFilters.edges.has(e.edge_type)) {
|
||||
edges.push({
|
||||
source: e.mrf_a,
|
||||
target: e.mrf_b,
|
||||
type: e.edge_type,
|
||||
weight: e.weight || 1
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.treeEdges.forEach(e => {
|
||||
if (nodeMap.has(e.mrf_parent) && nodeMap.has(e.mrf_child)) {
|
||||
if (state.graphFilters.edges.has('hierarchy')) {
|
||||
edges.push({
|
||||
source: e.mrf_parent,
|
||||
target: e.mrf_child,
|
||||
type: 'hierarchy',
|
||||
weight: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const nodes = Array.from(nodeMap.values());
|
||||
|
||||
if (nodes.length === 0) {
|
||||
this.container.innerHTML = '<div class="empty">Sin nodos para mostrar</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear and create SVG
|
||||
this.container.innerHTML = '';
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight || 600;
|
||||
|
||||
const svg: D3Selection = d3.select(this.container)
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
// Zoom
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.1, 4])
|
||||
.on('zoom', (event: { transform: string }) => {
|
||||
g.attr('transform', event.transform);
|
||||
});
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
// Simulation
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.simulation = d3.forceSimulation(nodes as any)
|
||||
.force('link', d3.forceLink(edges)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.id((d: any) => d.id)
|
||||
.distance(state.graphSettings.linkDist))
|
||||
.force('charge', d3.forceManyBody().strength(-150))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(state.graphSettings.nodeSize + 5));
|
||||
|
||||
// Links
|
||||
const link = g.append('g')
|
||||
.selectAll('line')
|
||||
.data(edges)
|
||||
.join('line')
|
||||
.attr('stroke', (d: GraphLink) => EDGE_COLORS[d.type] || '#999')
|
||||
.attr('stroke-width', (d: GraphLink) => Math.sqrt(d.weight))
|
||||
.attr('stroke-opacity', 0.6);
|
||||
|
||||
// Nodes
|
||||
const node = g.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.join('g')
|
||||
.attr('cursor', 'pointer')
|
||||
.call(this.createDrag(d3, this.simulation));
|
||||
|
||||
const nodeSize = state.graphSettings.nodeSize;
|
||||
|
||||
if (state.graphSettings.showImg) {
|
||||
node.append('image')
|
||||
.attr('xlink:href', (d: GraphNode) => d.img || '')
|
||||
.attr('width', nodeSize)
|
||||
.attr('height', nodeSize)
|
||||
.attr('x', -nodeSize / 2)
|
||||
.attr('y', -nodeSize / 2)
|
||||
.attr('clip-path', 'circle(50%)');
|
||||
|
||||
// Fallback for nodes without image
|
||||
node.filter((d: GraphNode) => !d.img)
|
||||
.append('circle')
|
||||
.attr('r', nodeSize / 2)
|
||||
.attr('fill', (d: GraphNode) => CATS[d.cat]?.color || '#7c8aff');
|
||||
} else {
|
||||
node.append('circle')
|
||||
.attr('r', nodeSize / 2)
|
||||
.attr('fill', (d: GraphNode) => CATS[d.cat]?.color || '#7c8aff');
|
||||
}
|
||||
|
||||
if (state.graphSettings.showLbl) {
|
||||
node.append('text')
|
||||
.text((d: GraphNode) => d.ref)
|
||||
.attr('dy', nodeSize / 2 + 12)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', 10)
|
||||
.attr('fill', 'var(--text-primary)');
|
||||
}
|
||||
|
||||
node.on('click', (_: MouseEvent, d: GraphNode) => {
|
||||
if (state.selectionMode) {
|
||||
const newSelected = new Set(state.selected);
|
||||
if (newSelected.has(d.id)) {
|
||||
newSelected.delete(d.id);
|
||||
} else {
|
||||
newSelected.add(d.id);
|
||||
}
|
||||
this.store.setState({ selected: newSelected });
|
||||
} else {
|
||||
this.showDetail(d.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
this.simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', (d: GraphLink) => (d.source as GraphNode).x!)
|
||||
.attr('y1', (d: GraphLink) => (d.source as GraphNode).y!)
|
||||
.attr('x2', (d: GraphLink) => (d.target as GraphNode).x!)
|
||||
.attr('y2', (d: GraphLink) => (d.target as GraphNode).y!);
|
||||
|
||||
node.attr('transform', (d: GraphNode) => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private createDrag(d3: D3Module, simulation: D3Simulation): any {
|
||||
return d3.drag()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.on('start', (event: any, d: any) => {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.on('drag', (event: any, d: any) => {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.on('end', (event: any, d: any) => {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determina la categoría de un tag basándose en el prefijo del MRF
|
||||
* CategoryKey: 'hst' | 'spe' | 'vue' | 'vsn' | 'msn' | 'flg'
|
||||
*/
|
||||
private getTagCategory(mrf: string): CategoryKey {
|
||||
// Mapeo de prefijos a categorías
|
||||
if (mrf.startsWith('spe_')) return 'spe'; // Specs
|
||||
if (mrf.startsWith('vue_')) return 'vue'; // Vue (Values)
|
||||
if (mrf.startsWith('vsn_')) return 'vsn'; // Visions
|
||||
if (mrf.startsWith('msn_')) return 'msn'; // Missions
|
||||
if (mrf.startsWith('flg_')) return 'flg'; // Flags
|
||||
return 'hst'; // Default: Hashtags
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
this.simulation?.stop();
|
||||
super.unmount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { View } from '../View.ts';
|
||||
import { filterTags, getName, getImg, delegateEvent } from '@/utils/index.ts';
|
||||
import type { Store } from '@/state/store.ts';
|
||||
import type { AppState } from '@/types/index.ts';
|
||||
|
||||
export class GridView extends View {
|
||||
private showDetail: (mrf: string) => void;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
store: Store<AppState>,
|
||||
showDetail: (mrf: string) => void
|
||||
) {
|
||||
super(container, store);
|
||||
this.showDetail = showDetail;
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const state = this.getState();
|
||||
const filtered = filterTags(state.tags, {
|
||||
search: state.search,
|
||||
group: state.group,
|
||||
library: state.library,
|
||||
libraryMembers: state.libraryMembers,
|
||||
lang: state.lang
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
this.container.innerHTML = '<div class="empty">Sin resultados</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.innerHTML = filtered.map(tag => {
|
||||
const img = getImg(tag);
|
||||
const name = getName(tag, state.lang);
|
||||
const isSelected = state.selected.has(tag.mrf);
|
||||
|
||||
return `
|
||||
<div class="card ${isSelected ? 'selected' : ''}" data-mrf="${tag.mrf}">
|
||||
${state.selectionMode ? `
|
||||
<input type="checkbox" class="card-checkbox" ${isSelected ? 'checked' : ''}>
|
||||
` : ''}
|
||||
${img
|
||||
? `<img class="card-img" src="${img}" alt="${tag.ref}" loading="lazy">`
|
||||
: `<div class="card-placeholder">${tag.ref?.slice(0, 2) || 'T'}</div>`
|
||||
}
|
||||
<div class="card-name">${name}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
delegateEvent<MouseEvent>(this.container, '.card', 'click', (_, target) => {
|
||||
const mrf = target.dataset.mrf;
|
||||
if (!mrf) return;
|
||||
|
||||
// Obtener estado FRESCO en cada click (no del closure)
|
||||
const state = this.getState();
|
||||
|
||||
if (state.selectionMode) {
|
||||
const newSelected = new Set(state.selected);
|
||||
if (newSelected.has(mrf)) {
|
||||
newSelected.delete(mrf);
|
||||
} else {
|
||||
newSelected.add(mrf);
|
||||
}
|
||||
this.setState({ selected: newSelected });
|
||||
} else {
|
||||
this.showDetail(mrf);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { View } from '../View.ts';
|
||||
import { filterTags, getName, getImg, delegateEvent, createNameMap, resolveGroupName } from '@/utils/index.ts';
|
||||
import type { Store } from '@/state/store.ts';
|
||||
import type { AppState, Tag } from '@/types/index.ts';
|
||||
|
||||
export class TreeView extends View {
|
||||
private showDetail: (mrf: string) => void;
|
||||
private expanded: Set<string> = new Set();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
store: Store<AppState>,
|
||||
showDetail: (mrf: string) => void
|
||||
) {
|
||||
super(container, store);
|
||||
this.showDetail = showDetail;
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const state = this.getState();
|
||||
// Use hstTags for group name resolution (set_hst points to hst tags)
|
||||
const nameMap = createNameMap(state.hstTags, state.lang);
|
||||
const filtered = filterTags(state.tags, {
|
||||
search: state.search,
|
||||
group: state.group,
|
||||
library: state.library,
|
||||
libraryMembers: state.libraryMembers,
|
||||
lang: state.lang
|
||||
});
|
||||
|
||||
// Group by set_hst
|
||||
const groups = new Map<string, Tag[]>();
|
||||
filtered.forEach(tag => {
|
||||
const group = tag.set_hst || 'sin-grupo';
|
||||
if (!groups.has(group)) groups.set(group, []);
|
||||
groups.get(group)!.push(tag);
|
||||
});
|
||||
|
||||
if (groups.size === 0) {
|
||||
this.container.innerHTML = '<div class="empty">Sin resultados</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.innerHTML = Array.from(groups.entries()).map(([groupMrf, tags]) => {
|
||||
const isExpanded = this.expanded.has(groupMrf);
|
||||
const groupName = resolveGroupName(groupMrf === 'sin-grupo' ? undefined : groupMrf, nameMap);
|
||||
return `
|
||||
<div class="tree-group">
|
||||
<div class="tree-header" data-group="${groupMrf}">
|
||||
<span class="tree-toggle">${isExpanded ? '−' : '+'}</span>
|
||||
<span class="tree-group-name">${groupName}</span>
|
||||
<span class="tree-count">${tags.length}</span>
|
||||
</div>
|
||||
<div class="tree-items ${isExpanded ? 'expanded' : ''}">
|
||||
${tags.map(tag => {
|
||||
const img = getImg(tag);
|
||||
const name = getName(tag, state.lang);
|
||||
const isSelected = state.selected.has(tag.mrf);
|
||||
return `
|
||||
<div class="tree-item ${isSelected ? 'selected' : ''}" data-mrf="${tag.mrf}">
|
||||
${img
|
||||
? `<img class="tree-img" src="${img}" alt="${tag.ref}">`
|
||||
: `<div class="tree-placeholder">${tag.ref?.slice(0, 1) || 'T'}</div>`
|
||||
}
|
||||
<span class="tree-name">${name}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
delegateEvent<MouseEvent>(this.container, '.tree-header', 'click', (_, target) => {
|
||||
const group = target.dataset.group;
|
||||
if (!group) return;
|
||||
|
||||
if (this.expanded.has(group)) {
|
||||
this.expanded.delete(group);
|
||||
} else {
|
||||
this.expanded.add(group);
|
||||
}
|
||||
this.render();
|
||||
});
|
||||
|
||||
delegateEvent<MouseEvent>(this.container, '.tree-item', 'click', (_, target) => {
|
||||
const mrf = target.dataset.mrf;
|
||||
if (!mrf) return;
|
||||
|
||||
// Obtener estado FRESCO en cada click (no del closure)
|
||||
const state = this.getState();
|
||||
|
||||
if (state.selectionMode) {
|
||||
const newSelected = new Set(state.selected);
|
||||
if (newSelected.has(mrf)) {
|
||||
newSelected.delete(mrf);
|
||||
} else {
|
||||
newSelected.add(mrf);
|
||||
}
|
||||
this.setState({ selected: newSelected });
|
||||
} else {
|
||||
this.showDetail(mrf);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
33
deck-frontend/backups/20260113_221902/src/views/View.ts
Normal file
33
deck-frontend/backups/20260113_221902/src/views/View.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Store } from '@/state/store.ts';
|
||||
import type { AppState } from '@/types/index.ts';
|
||||
|
||||
export abstract class View {
|
||||
protected container: HTMLElement;
|
||||
protected store: Store<AppState>;
|
||||
protected unsubscribe?: () => void;
|
||||
|
||||
constructor(container: HTMLElement, store: Store<AppState>) {
|
||||
this.container = container;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
abstract render(): void;
|
||||
|
||||
mount(): void {
|
||||
this.unsubscribe = this.store.subscribe(() => this.render());
|
||||
this.render();
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
this.unsubscribe?.();
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
|
||||
protected getState(): AppState {
|
||||
return this.store.getState();
|
||||
}
|
||||
|
||||
protected setState(partial: Partial<AppState>): void {
|
||||
this.store.setState(partial);
|
||||
}
|
||||
}
|
||||
5
deck-frontend/backups/20260113_221902/src/views/index.ts
Normal file
5
deck-frontend/backups/20260113_221902/src/views/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { View } from './View.ts';
|
||||
export { GridView } from './GridView/GridView.ts';
|
||||
export { TreeView } from './TreeView/TreeView.ts';
|
||||
export { GraphView } from './GraphView/GraphView.ts';
|
||||
export { DetailPanel } from './DetailPanel/DetailPanel.ts';
|
||||
Reference in New Issue
Block a user